From 7f566621e1a8db3898100c4c814ecf3bcb6c9197 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 6 Aug 2024 13:44:49 -0400 Subject: [PATCH 01/86] clear out getters on new scope --- .../3-transform/client/transform-client.js | 16 +++++++++++++++- packages/svelte/src/compiler/phases/scope.js | 1 - 2 files changed, 15 insertions(+), 2 deletions(-) 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 82d49f0ad7e6..1400c5cccd0f 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 @@ -58,7 +58,21 @@ import { VariableDeclaration } from './visitors/VariableDeclaration.js'; /** @type {Visitors} */ const visitors = { - _: set_scope, + _: function set_scope(node, { next, state }) { + const scope = state.scopes.get(node); + + if (scope && scope !== state.scope) { + const getters = { ...state.getters }; + + for (const name of scope.declarations.keys()) { + delete getters[name]; + } + + next({ ...state, getters, scope }); + } else { + next(); + } + }, AnimateDirective, ArrowFunctionExpression, AssignmentExpression, diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 5eba0d3cf267..0d63a0381ce3 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -532,7 +532,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { references_within.add(id); } } - scopes.set(node.expression, state.scope); // context and children are a new scope const scope = state.scope.child(); From d36d9c47057cf6ba872d8ebeab0155a4c9a0545e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 6 Aug 2024 13:55:44 -0400 Subject: [PATCH 02/86] fix --- .../phases/3-transform/client/visitors/EachBlock.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index a82677817a6e..665522683b2b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -1,6 +1,7 @@ /** @import { AssignmentExpression, BlockStatement, Expression, Identifier, MemberExpression, Pattern, Statement } from 'estree' */ /** @import { Binding, EachBlock } from '#compiler' */ /** @import { ComponentContext, Context } from '../types' */ +/** @import { Scope } from '../../../scope' */ import { EACH_INDEX_REACTIVE, EACH_IS_ANIMATED, @@ -20,7 +21,15 @@ import { get_assignment_value, build_getter, build_setter, with_loc } from '../u */ export function EachBlock(node, context) { const each_node_meta = node.metadata; - const collection = /** @type {Expression} */ (context.visit(node.expression)); + + // expression should be evaluated in the parent scope, not the scope + // created by the each block itself + const collection = /** @type {Expression} */ ( + context.visit(node.expression, { + ...context.state, + scope: /** @type {Scope} */ (context.state.scope.parent) + }) + ); if (!each_node_meta.is_controlled) { context.state.template.push(''); From ffedcf764c505ceb2c13997541fab022b2c07a38 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 6 Aug 2024 14:56:35 -0400 Subject: [PATCH 03/86] fix --- .../compiler/phases/2-analyze/visitors/EachBlock.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js index 2e09c756e3b9..ebf20d5aa612 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js @@ -1,5 +1,6 @@ /** @import { EachBlock } from '#compiler' */ /** @import { Context } from '../types' */ +/** @import { Scope } from '../../scope' */ import * as e from '../../../errors.js'; import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js'; @@ -25,5 +26,13 @@ export function EachBlock(node, context) { node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index; } - context.next(); + // evaluate expression in parent scope + context.visit(node.expression, { + ...context.state, + scope: /** @type {Scope} */ (context.state.scope.parent) + }); + + context.visit(node.body); + if (node.key) context.visit(node.key); + if (node.fallback) context.visit(node.fallback); } From 002b7f77ae8a274d09a6ab1686b48f556de55110 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 6 Aug 2024 15:36:24 -0400 Subject: [PATCH 04/86] fix --- .../src/compiler/phases/3-transform/client/transform-client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1400c5cccd0f..ae7fcf60ae1f 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 @@ -62,7 +62,7 @@ const visitors = { const scope = state.scopes.get(node); if (scope && scope !== state.scope) { - const getters = { ...state.getters }; + const getters = node.type === 'LetDirective' ? state.getters : { ...state.getters }; for (const name of scope.declarations.keys()) { delete getters[name]; From 4fd80f807344a8152a144d70e271c7613593ef6d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 6 Aug 2024 15:56:47 -0400 Subject: [PATCH 05/86] fix --- .../compiler/phases/3-transform/client/transform-client.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 ae7fcf60ae1f..131fca5cc6a4 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 @@ -64,8 +64,10 @@ const visitors = { if (scope && scope !== state.scope) { const getters = node.type === 'LetDirective' ? state.getters : { ...state.getters }; - for (const name of scope.declarations.keys()) { - delete getters[name]; + if (node.type !== 'LetDirective') { + for (const name of scope.declarations.keys()) { + delete getters[name]; + } } next({ ...state, getters, scope }); From cfa5419b2b06d139ca929b7634300d399638250c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 6 Aug 2024 17:03:15 -0400 Subject: [PATCH 06/86] consolidate legacy_reactive_import logic --- .../phases/2-analyze/visitors/Attribute.js | 4 +- .../phases/2-analyze/visitors/Identifier.js | 12 +----- .../3-transform/client/transform-client.js | 37 ++++++++++++++----- .../phases/3-transform/client/utils.js | 15 -------- .../client/visitors/LabeledStatement.js | 2 +- packages/svelte/src/compiler/types/index.d.ts | 4 +- 6 files changed, 32 insertions(+), 42 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 85dfc6c67ec3..5d9a4383fa48 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -187,9 +187,7 @@ function get_delegated_event(event_name, handler, context) { // Bail out if we reference anything from the EachBlock (for now) that mutates in non-runes mode, (((!context.state.analysis.runes && binding.kind === 'each') || // or any normal not reactive bindings that are mutated. - binding.kind === 'normal' || - // or any reactive imports (those are rewritten) (can only happen in legacy mode) - binding.kind === 'legacy_reactive_import') && + binding.kind === 'normal') && binding.mutated)) ) { return unhoisted; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index c00d5216107f..2e4595ec0852 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -80,16 +80,8 @@ export function Identifier(node, context) { binding.declaration_kind !== 'function') || binding.declaration_kind === 'import') ) { - if (binding.declaration_kind === 'import') { - if ( - binding.mutated && - // TODO could be more fine-grained - not every mention in the template implies a state binding - (context.state.reactive_statement || context.state.ast_type === 'template') && - parent.type === 'MemberExpression' - ) { - binding.kind = 'legacy_reactive_import'; - } - } else if ( + if ( + binding.declaration_kind !== 'import' && binding.mutated && // TODO could be more fine-grained - not every mention in the template implies a state binding (context.state.reactive_statement || context.state.ast_type === 'template') 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 131fca5cc6a4..c5c91466dd65 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 @@ -165,6 +165,32 @@ export function client_component(analysis, options) { locations: /** @type {any} */ (null) }; + /** @type {ESTree.Statement[]} */ + const legacy_reactive_imports = []; + + // Very very dirty way of making import statements reactive in legacy mode if needed + if (!analysis.runes) { + for (const [name, binding] of state.scope.declarations) { + if (binding.declaration_kind === 'import' && binding.mutated) { + state.getters[name] = (node) => b.call('$$_import_' + node.name); + + state.setters[name] = (node, context) => + b.call( + '$$_import_' + binding.node.name, + b.assignment( + node.operator, + /** @type {ESTree.Pattern} */ (context.visit(node.left)), + /** @type {ESTree.Expression} */ (context.visit(node.right)) + ) + ); + + legacy_reactive_imports.push( + b.var('$$_import_' + name, b.call('$.reactive_import', b.thunk(b.id(name)))) + ); + } + } + } + const module = /** @type {ESTree.Program} */ ( walk(/** @type {SvelteNode} */ (analysis.module.ast), state, visitors) ); @@ -188,16 +214,7 @@ export function client_component(analysis, options) { ) ); - // Very very dirty way of making import statements reactive in legacy mode if needed - if (!analysis.runes) { - for (const [name, binding] of analysis.module.scope.declarations) { - if (binding.kind === 'legacy_reactive_import') { - instance.body.unshift( - b.var('$$_import_' + name, b.call('$.reactive_import', b.thunk(b.id(name)))) - ); - } - } - } + instance.body.unshift(...legacy_reactive_imports); /** @type {ESTree.Statement[]} */ const store_setup = []; 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 57ede67bb317..efffbecba271 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -106,10 +106,6 @@ export function build_getter(node, state) { return b.member(b.id('$$props'), node); } - if (binding.kind === 'legacy_reactive_import') { - return b.call('$$_import_' + node.name); - } - if ( is_state_source(binding, state) || binding.kind === 'derived' || @@ -271,17 +267,6 @@ export function build_setter(node, context, fallback, prefix, options) { return setter(node, context); } - if (binding.kind === 'legacy_reactive_import') { - return b.call( - '$$_import_' + binding.node.name, - b.assignment( - node.operator, - /** @type {Pattern} */ (visit(node.left)), - /** @type {Expression} */ (visit(node.right)) - ) - ); - } - /** * @param {any} serialized * @returns diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js index 262e44dfcbd9..87f56262a8ba 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js @@ -34,7 +34,7 @@ export function LabeledStatement(node, context) { const sequence = []; for (const binding of reactive_statement.dependencies) { - if (binding.kind === 'normal') continue; + if (binding.kind === 'normal' && binding.declaration_kind !== 'import') continue; const name = binding.node.name; let serialized = build_getter(b.id(name), context.state); diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 47dc9d7eb607..f7d111a267d6 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -271,7 +271,6 @@ export interface Binding { * - `snippet`: A snippet parameter * - `store_sub`: A $store value * - `legacy_reactive`: A `$:` declaration - * - `legacy_reactive_import`: An imported binding that is mutated inside the component */ kind: | 'normal' @@ -284,8 +283,7 @@ export interface Binding { | 'each' | 'snippet' | 'store_sub' - | 'legacy_reactive' - | 'legacy_reactive_import'; + | 'legacy_reactive'; declaration_kind: DeclarationKind; /** * What the value was initialized with. From f4ccd6b865dae9f35f552a02f8bcb14c0d11bead Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 6 Aug 2024 17:03:44 -0400 Subject: [PATCH 07/86] unused --- .../src/compiler/phases/3-transform/client/transform-client.js | 1 - 1 file changed, 1 deletion(-) 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 c5c91466dd65..a0fc0650c22c 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 @@ -4,7 +4,6 @@ /** @import { Visitors, ComponentClientTransformState, ClientTransformState } from './types' */ import { walk } from 'zimmerframe'; import * as b from '../../../utils/builders.js'; -import { set_scope } from '../../scope.js'; import { build_getter } from './utils.js'; import { render_stylesheet } from '../css/index.js'; import { dev, filename } from '../../../state.js'; From 0e7884fa533242f771cb456448913b6c17d616aa Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 6 Aug 2024 17:07:23 -0400 Subject: [PATCH 08/86] $$sanitized_props --- .../compiler/phases/3-transform/client/transform-client.js | 4 +++- .../svelte/src/compiler/phases/3-transform/client/utils.js | 5 ----- 2 files changed, 3 insertions(+), 6 deletions(-) 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 a0fc0650c22c..13e8f60ae4e2 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 @@ -167,9 +167,11 @@ export function client_component(analysis, options) { /** @type {ESTree.Statement[]} */ const legacy_reactive_imports = []; - // Very very dirty way of making import statements reactive in legacy mode if needed if (!analysis.runes) { + state.getters['$$props'] = (node) => ({ ...node, name: '$$sanitized_props' }); + for (const [name, binding] of state.scope.declarations) { + // Very very dirty way of making import statements reactive in legacy mode if needed if (binding.declaration_kind === 'import' && binding.mutated) { state.getters[name] = (node) => b.call('$$_import_' + node.name); 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 efffbecba271..39f9e6b2cf9d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -85,11 +85,6 @@ export function build_getter(node, state) { return typeof getter === 'function' ? getter(node) : getter; } - if (binding.node.name === '$$props') { - // Special case for $$props which only exists in the old world - return b.id('$$sanitized_props'); - } - if (binding.kind === 'store_sub') { return b.call(node); } From 20f1e662f1706a0cfe3e55da7d7b72d9ec29bbd2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 6 Aug 2024 18:39:57 -0400 Subject: [PATCH 09/86] use getters mechanism for store_sub --- .../phases/3-transform/client/transform-client.js | 14 +++++++++++++- .../compiler/phases/3-transform/client/utils.js | 4 ---- 2 files changed, 13 insertions(+), 5 deletions(-) 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 13e8f60ae4e2..3a263c3ab313 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 @@ -198,11 +198,18 @@ export function client_component(analysis, options) { const instance_state = { ...state, + getters: { ...state.getters }, scope: analysis.instance.scope, scopes: analysis.instance.scopes, is_instance: true }; + for (const [name, binding] of instance_state.scope.declarations) { + if (binding.kind === 'store_sub') { + instance_state.getters[name] = (node) => b.call(node); + } + } + const instance = /** @type {ESTree.Program} */ ( walk(/** @type {SvelteNode} */ (analysis.instance.ast), instance_state, visitors) ); @@ -210,7 +217,12 @@ export function client_component(analysis, options) { const template = /** @type {ESTree.Program} */ ( walk( /** @type {SvelteNode} */ (analysis.template.ast), - { ...state, scope: analysis.instance.scope, scopes: analysis.template.scopes }, + { + ...state, + getters: instance_state.getters, + scope: analysis.instance.scope, + scopes: analysis.template.scopes + }, visitors ) ); 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 39f9e6b2cf9d..88f064834aca 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -85,10 +85,6 @@ export function build_getter(node, state) { return typeof getter === 'function' ? getter(node) : getter; } - if (binding.kind === 'store_sub') { - return b.call(node); - } - if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { if (is_prop_source(binding, state)) { return b.call(node); From 4315ac3242f7ddf1a878254c8ef15059248c1478 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 7 Aug 2024 14:31:19 -0400 Subject: [PATCH 10/86] snapshot --- .../2-analyze/visitors/shared/attribute.js | 2 +- .../2-analyze/visitors/shared/component.js | 73 +++++++++- .../3-transform/client/transform-client.js | 13 +- .../client/visitors/BlockStatement.js | 10 ++ .../client/visitors/LetDirective.js | 2 + .../3-transform/client/visitors/Program.js | 32 +++++ .../client/visitors/shared/component.js | 26 +++- .../server/visitors/shared/component.js | 35 ++++- packages/svelte/src/compiler/phases/scope.js | 134 +++++++++++++----- playgrounds/sandbox/run.js | 5 + 10 files changed, 273 insertions(+), 59 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/attribute.js index 8f60a67c62b0..4102460cbaa2 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/attribute.js @@ -1,4 +1,4 @@ -/** @import { Attribute, ElementLike } from '#compiler' */ +/** @import { Attribute, ElementLike, SvelteNode } from '#compiler' */ /** @import { Context } from '../../types' */ import * as e from '../../../../errors.js'; import { is_text_attribute } from '../../../../utils/ast.js'; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js index 347a59488a57..01ecbee33402 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js @@ -1,7 +1,12 @@ /** @import { Component, SvelteComponent, SvelteSelf } from '#compiler' */ /** @import { Context } from '../../types' */ import * as e from '../../../../errors.js'; -import { get_attribute_expression, is_expression_attribute } from '../../../../utils/ast.js'; +import { + get_attribute_expression, + is_expression_attribute, + is_text_attribute +} from '../../../../utils/ast.js'; +import { is_element_node } from '../../../nodes.js'; import { validate_attribute, validate_attribute_name, @@ -60,9 +65,69 @@ export function visit_component(node, context) { } } - context.next({ + const component_slots = new Set(); + + const default_state = { + ...context.state, + scope: node.metadata.default_scope, + parent_element: null, + component_slots + }; + + const named_state = { ...context.state, parent_element: null, - component_slots: new Set() - }); + component_slots + }; + + for (const attribute of node.attributes) { + context.visit(attribute, attribute.type === 'LetDirective' ? default_state : named_state); + } + + const default_slot_nodes = []; + const named_slot_nodes = []; + + for (const child of node.fragment.nodes) { + const is_slotted_content = + is_element_node(child) && + child.attributes.some( + (a) => a.type === 'Attribute' && a.name === 'slot' && is_text_attribute(a) + ); + + if (child.type === 'Comment') { + // ensure svelte-ignore comments are preserved in both cases + // TODO this is brittle + named_slot_nodes.push(child); + default_slot_nodes.push(child); + } else if (is_slotted_content) { + named_slot_nodes.push(child); + } else { + default_slot_nodes.push(child); + } + } + + context.visit({ ...node.fragment, nodes: default_slot_nodes }, default_state); + context.visit({ ...node.fragment, nodes: named_slot_nodes }, named_state); + + // context.visit(child, is_slotted_content ? named_state : default_state); + + // if ( + // is_element_node(child) && + // child.attributes.some( + // (a) => a.type === 'Attribute' && a.name === 'slot' && is_text_attribute(a) + // ) + // ) { + // context.visit(child); + // } else { + // context.visit(child, { scope }); + // } + + // context.visit(node, attribute.type === 'LetDirective' ? default_state : named_state); + // } + + // context.next({ + // ...context.state, + // parent_element: null, + // component_slots: new Set() + // }); } 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 3a263c3ab313..2755357bcb83 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 @@ -14,6 +14,7 @@ import { Attribute } from './visitors/Attribute.js'; import { AwaitBlock } from './visitors/AwaitBlock.js'; import { BinaryExpression } from './visitors/BinaryExpression.js'; import { BindDirective } from './visitors/BindDirective.js'; +import { BlockStatement } from './visitors/BlockStatement.js'; import { BreakStatement } from './visitors/BreakStatement.js'; import { CallExpression } from './visitors/CallExpression.js'; import { ClassBody } from './visitors/ClassBody.js'; @@ -36,6 +37,7 @@ import { LabeledStatement } from './visitors/LabeledStatement.js'; import { LetDirective } from './visitors/LetDirective.js'; import { MemberExpression } from './visitors/MemberExpression.js'; import { OnDirective } from './visitors/OnDirective.js'; +import { Program } from './visitors/Program.js'; import { RegularElement } from './visitors/RegularElement.js'; import { RenderTag } from './visitors/RenderTag.js'; import { SlotElement } from './visitors/SlotElement.js'; @@ -61,10 +63,11 @@ const visitors = { const scope = state.scopes.get(node); if (scope && scope !== state.scope) { - const getters = node.type === 'LetDirective' ? state.getters : { ...state.getters }; + const getters = { ...state.getters }; - if (node.type !== 'LetDirective') { - for (const name of scope.declarations.keys()) { + for (const [name, binding] of scope.declarations) { + // if (binding.kind === 'normal') { + if (binding.kind !== 'each') { delete getters[name]; } } @@ -81,6 +84,7 @@ const visitors = { AwaitBlock, BinaryExpression, BindDirective, + BlockStatement, BreakStatement, CallExpression, ClassBody, @@ -103,6 +107,7 @@ const visitors = { LetDirective, MemberExpression, OnDirective, + Program, RegularElement, RenderTag, SlotElement, @@ -206,7 +211,7 @@ export function client_component(analysis, options) { for (const [name, binding] of instance_state.scope.declarations) { if (binding.kind === 'store_sub') { - instance_state.getters[name] = (node) => b.call(node); + // instance_state.getters[name] = (node) => b.call(node); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js new file mode 100644 index 000000000000..d5f15ebdf156 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js @@ -0,0 +1,10 @@ +/** @import { BlockStatement, Expression } from 'estree' */ +/** @import { ComponentContext } from '../types' */ + +/** + * @param {BlockStatement} node + * @param {ComponentContext} context + */ +export function BlockStatement(node, context) { + context.next(); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js index 2423c6e50e01..ba100bb689fa 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js @@ -42,6 +42,8 @@ export function LetDirective(node, context) { ) ); } else { + context.state.getters[node.name] = (node) => b.call('$.get', node); + return b.const( node.expression === null ? node.name : node.expression.name, create_derived(context.state, b.thunk(b.member(b.id('$$slotProps'), b.id(node.name)))) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js new file mode 100644 index 000000000000..1d850528c566 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -0,0 +1,32 @@ +/** @import { Program, Expression } from 'estree' */ +/** @import { ComponentContext } from '../types' */ +import { is_prop_source } from '../utils.js'; +import * as b from '../../../../utils/builders.js'; + +/** + * @param {Program} node + * @param {ComponentContext} context + */ +export function Program(node, context) { + if (context.state.is_instance) { + for (const [name, binding] of context.state.scope.declarations) { + if (binding.kind === 'store_sub') { + context.state.getters[name] = (node) => b.call(node); + } + + if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { + if (is_prop_source(binding, context.state)) { + context.state.getters[name] = (node) => b.call(node); + } else if (binding.prop_alias) { + const key = b.key(binding.prop_alias); + context.state.getters[name] = (node) => + b.member(b.id('$$props'), key, key.type === 'Literal'); + } else { + context.state.getters[name] = (node) => b.member(b.id('$$props'), node); + } + } + } + } + + context.next(); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 6416e26318c6..9418ed2a404e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -24,6 +24,16 @@ export function build_component(node, component_name, context, anchor = context. /** @type {ExpressionStatement[]} */ const lets = []; + /** + * Children in the default slot are evaluated in the component scope, + * children in named slots are evaluated in the parent scope + */ + const child_state = { + ...context.state, + scope: node.metadata.default_scope, + getters: { ...context.state.getters } + }; + /** @type {Record} */ const children = {}; @@ -66,9 +76,10 @@ export function build_component(node, component_name, context, anchor = context. props_and_spreads.push(props); } } + for (const attribute of node.attributes) { if (attribute.type === 'LetDirective') { - lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); + lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute, child_state))); } else if (attribute.type === 'OnDirective') { if (!attribute.expression) { context.state.analysis.needs_props = true; @@ -242,12 +253,13 @@ export function build_component(node, component_name, context, anchor = context. // @ts-expect-error nodes: children[slot_name] }, - { - ...context.state, - scope: - context.state.scopes.get(slot_name === 'default' ? children[slot_name][0] : node) ?? - context.state.scope - } + slot_name === 'default' ? child_state : context.state + // { + // ...context.state, + // scope: + // context.state.scopes.get(slot_name === 'default' ? children[slot_name][0] : node) ?? + // context.state.scope + // } ) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js index 60b3498510de..cf604ad9568b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js @@ -20,6 +20,15 @@ export function build_inline_component(node, expression, context) { /** @type {Record} */ const lets = { default: [] }; + /** + * Children in the default slot are evaluated in the component scope, + * children in named slots are evaluated in the parent scope + */ + const child_state = { + ...context.state, + scope: node.metadata.default_scope + }; + /** @type {Record} */ const children = {}; @@ -144,13 +153,27 @@ export function build_inline_component(node, expression, context) { // @ts-expect-error nodes: children[slot_name] }, - { - ...context.state, - scope: - context.state.scopes.get(slot_name === 'default' ? children[slot_name][0] : node) ?? - context.state.scope - } + slot_name === 'default' ? child_state : context.state + // { + // ...context.state, + // scope: + // context.state.scopes.get(slot_name === 'default' ? children[slot_name][0] : node) ?? + // context.state.scope + // } ) + // context.visit( + // { + // ...node.fragment, + // // @ts-expect-error + // nodes: children[slot_name] + // }, + // { + // ...context.state, + // scope: + // context.state.scopes.get(slot_name === 'default' ? children[slot_name][0] : node) ?? + // context.state.scope + // } + // ) ); if (block.body.length === 0) continue; diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 0d63a0381ce3..e181e2e0f730 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -9,6 +9,7 @@ import * as e from '../errors.js'; import { extract_identifiers, extract_identifiers_from_destructuring, + is_text_attribute, object, unwrap_pattern } from '../utils/ast.js'; @@ -290,11 +291,81 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { * @type {Visitor} */ const SvelteFragment = (node, { state, next }) => { - const [scope] = analyze_let_directives(node, state.scope); + // const [scope] = analyze_let_directives(node, state.scope); + const scope = state.scope.child(); scopes.set(node, scope); next({ scope }); }; + /** + * @type {Visitor} + */ + const Component = (node, context) => { + const scope = context.state.scope.child(); + node.metadata.default_scope = scope; + // scopes.set(node, scope); + + for (const attribute of node.attributes) { + if (attribute.type === 'LetDirective') { + context.visit(attribute, { scope }); + } else { + context.visit(attribute); + } + } + + for (const child of node.fragment.nodes) { + if ( + is_element_node(child) && + child.attributes.some( + (a) => a.type === 'Attribute' && a.name === 'slot' && is_text_attribute(a) + ) + ) { + context.visit(child); + } else { + context.visit(child, { scope }); + } + } + + // // let:x is super weird: + // // - for the default slot, its scope only applies to children that are not slots themselves + // // - for named slots, its scope applies to the component itself, too + // const [scope, is_default_slot] = analyze_let_directives(node, state.scope); + // if (is_default_slot) { + // for (const attribute of node.attributes) { + // visit(attribute); + // } + // } else { + // scopes.set(node, scope); + + // for (const attribute of node.attributes) { + // visit(attribute, { ...state, scope }); + // } + // } + + // for (const child of node.fragment.nodes) { + // if ( + // is_element_node(child) && + // child.attributes.some( + // (attribute) => attribute.type === 'Attribute' && attribute.name === 'slot' + // ) + // ) { + // //
inherits the scope above the component unless the component is a named slot itself, because slots are hella weird + // scopes.set(child, is_default_slot ? state.scope : scope); + // visit(child, { scope: is_default_slot ? state.scope : scope }); + // } else { + // if (child.type === 'ExpressionTag') { + // // expression tag is a special case — we don't visit it directly, but via process_children, + // // so we need to set the scope on the expression rather than the tag itself + // scopes.set(child.expression, scope); + // } else { + // scopes.set(child, scope); + // } + + // visit(child, { scope }); + // } + // } + }; + /** * @param {ElementLike} node * @param {Scope} parent @@ -384,48 +455,37 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { SvelteElement: SvelteFragment, RegularElement: SvelteFragment, - Component(node, { state, visit, path }) { - state.scope.reference(b.id(node.name), path); + LetDirective(node, context) { + const scope = context.state.scope; - // let:x is super weird: - // - for the default slot, its scope only applies to children that are not slots themselves - // - for named slots, its scope applies to the component itself, too - const [scope, is_default_slot] = analyze_let_directives(node, state.scope); - if (is_default_slot) { - for (const attribute of node.attributes) { - visit(attribute); - } - } else { - scopes.set(node, scope); + /** @type {Binding[]} */ + const bindings = []; + scope.declarators.set(node, bindings); - for (const attribute of node.attributes) { - visit(attribute, { ...state, scope }); + if (node.expression) { + for (const id of extract_identifiers_from_destructuring(node.expression)) { + const binding = scope.declare(id, 'derived', 'const'); + bindings.push(binding); } + } else { + /** @type {Identifier} */ + const id = { + name: node.name, + type: 'Identifier', + start: node.start, + end: node.end + }; + const binding = scope.declare(id, 'derived', 'const'); + bindings.push(binding); } + }, - for (const child of node.fragment.nodes) { - if ( - is_element_node(child) && - child.attributes.some( - (attribute) => attribute.type === 'Attribute' && attribute.name === 'slot' - ) - ) { - //
inherits the scope above the component unless the component is a named slot itself, because slots are hella weird - scopes.set(child, is_default_slot ? state.scope : scope); - visit(child, { scope: is_default_slot ? state.scope : scope }); - } else { - if (child.type === 'ExpressionTag') { - // expression tag is a special case — we don't visit it directly, but via process_children, - // so we need to set the scope on the expression rather than the tag itself - scopes.set(child.expression, scope); - } else { - scopes.set(child, scope); - } - - visit(child, { scope }); - } - } + Component: (node, context) => { + context.state.scope.reference(b.id(node.name), context.path); + Component(node, context); }, + SvelteSelf: Component, + SvelteComponent: Component, // updates AssignmentExpression(node, { state, next }) { diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index 0b72c18f46fe..bbeacb3e85f6 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -63,6 +63,11 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { runes: argv.values.runes }); + for (const warning of compiled.warnings) { + console.warn(warning.code); + console.warn(warning.frame); + } + fs.writeFileSync( output_js, compiled.js.code + '\n//# sourceMappingURL=' + path.basename(output_map) From 4c6120a55b9f9cb245eb951fa1e5fb0c1cf77f4a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 7 Aug 2024 17:47:00 -0400 Subject: [PATCH 11/86] fix --- .../2-analyze/visitors/shared/component.js | 67 +++++++++++-------- .../client/visitors/LetDirective.js | 5 +- .../client/visitors/shared/component.js | 56 +++++++++------- .../client/visitors/shared/element.js | 2 +- .../server/visitors/shared/component.js | 9 ++- packages/svelte/src/compiler/phases/scope.js | 35 ++++++---- packages/svelte/src/compiler/utils/slot.js | 20 ++++++ 7 files changed, 123 insertions(+), 71 deletions(-) create mode 100644 packages/svelte/src/compiler/utils/slot.js diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js index 01ecbee33402..67b5a6d4ff38 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js @@ -6,6 +6,7 @@ import { is_expression_attribute, is_text_attribute } from '../../../../utils/ast.js'; +import { determine_slot } from '../../../../utils/slot.js'; import { is_element_node } from '../../../nodes.js'; import { validate_attribute, @@ -67,47 +68,55 @@ export function visit_component(node, context) { const component_slots = new Set(); - const default_state = { - ...context.state, - scope: node.metadata.default_scope, - parent_element: null, - component_slots - }; + const slot_scope_applies_to_itself = !!determine_slot(node); - const named_state = { - ...context.state, - parent_element: null, - component_slots - }; + const default_state = slot_scope_applies_to_itself + ? context.state + : { + ...context.state, + scope: node.metadata.scopes.default, + parent_element: null, + component_slots + }; for (const attribute of node.attributes) { - context.visit(attribute, attribute.type === 'LetDirective' ? default_state : named_state); + context.visit(attribute, attribute.type === 'LetDirective' ? default_state : context.state); } - const default_slot_nodes = []; - const named_slot_nodes = []; + const comments = []; + const nodes = { + default: [] + }; for (const child of node.fragment.nodes) { - const is_slotted_content = - is_element_node(child) && - child.attributes.some( - (a) => a.type === 'Attribute' && a.name === 'slot' && is_text_attribute(a) - ); - if (child.type === 'Comment') { - // ensure svelte-ignore comments are preserved in both cases - // TODO this is brittle - named_slot_nodes.push(child); - default_slot_nodes.push(child); - } else if (is_slotted_content) { - named_slot_nodes.push(child); + comments.push(child); + continue; + } + + let slot_name = determine_slot(child); + + if (slot_name) { + nodes[slot_name] = [...comments, child]; } else { - default_slot_nodes.push(child); + nodes.default.push(...comments, child); } } - context.visit({ ...node.fragment, nodes: default_slot_nodes }, default_state); - context.visit({ ...node.fragment, nodes: named_slot_nodes }, named_state); + for (const slot_name in nodes) { + context.visit( + { ...node.fragment, nodes: nodes[slot_name] }, + { + ...context.state, + scope: node.metadata.scopes[slot_name], + parent_element: null, + component_slots + } + ); + } + + // context.visit({ ...node.fragment, nodes: default_slot_nodes }, default_state); + // context.visit({ ...node.fragment, nodes: named_slot_nodes }, named_state); // context.visit(child, is_slotted_content ? named_state : default_state); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js index ba100bb689fa..9d42317bda46 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js @@ -42,10 +42,11 @@ export function LetDirective(node, context) { ) ); } else { - context.state.getters[node.name] = (node) => b.call('$.get', node); + const name = node.expression === null ? node.name : node.expression.name; + context.state.getters[name] = (node) => b.call('$.get', node); return b.const( - node.expression === null ? node.name : node.expression.name, + name, create_derived(context.state, b.thunk(b.member(b.id('$$slotProps'), b.id(node.name)))) ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 9418ed2a404e..3d729431fb04 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -9,6 +9,7 @@ import { create_derived, build_setter } from '../../utils.js'; import { build_bind_this, validate_binding } from '../shared/utils.js'; import { build_attribute_value } from '../shared/element.js'; import { build_event_handler } from './events.js'; +import { determine_slot } from '../../../../../utils/slot.js'; /** * @param {Component | SvelteComponent | SvelteSelf} node @@ -24,14 +25,13 @@ export function build_component(node, component_name, context, anchor = context. /** @type {ExpressionStatement[]} */ const lets = []; - /** - * Children in the default slot are evaluated in the component scope, - * children in named slots are evaluated in the parent scope - */ - const child_state = { - ...context.state, - scope: node.metadata.default_scope, - getters: { ...context.state.getters } + /** @type {Record} */ + const states = { + default: { + ...context.state, + scope: node.metadata.scopes.default, + getters: { ...context.state.getters } + } }; /** @type {Record} */ @@ -51,11 +51,13 @@ export function build_component(node, component_name, context, anchor = context. */ const binding_initializers = []; + const self_slot = determine_slot(node); + /** * If this component has a slot property, it is a named slot within another component. In this case * the slot scope applies to the component itself, too, and not just its children. */ - let slot_scope_applies_to_itself = false; + let slot_scope_applies_to_itself = !!self_slot; /** * Components may have a children prop and also have child nodes. In this case, we assume @@ -77,9 +79,19 @@ export function build_component(node, component_name, context, anchor = context. } } + if (slot_scope_applies_to_itself) { + for (const attribute of node.attributes) { + if (attribute.type === 'LetDirective') { + lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); + } + } + } + for (const attribute of node.attributes) { if (attribute.type === 'LetDirective') { - lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute, child_state))); + if (!slot_scope_applies_to_itself) { + lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute, states.default))); + } } else if (attribute.type === 'OnDirective') { if (!attribute.expression) { context.state.analysis.needs_props = true; @@ -225,19 +237,7 @@ export function build_component(node, component_name, context, anchor = context. continue; } - let slot_name = 'default'; - - if (is_element_node(child)) { - const attribute = /** @type {Attribute | undefined} */ ( - child.attributes.find( - (attribute) => attribute.type === 'Attribute' && attribute.name === 'slot' - ) - ); - - if (attribute !== undefined) { - slot_name = /** @type {Text[]} */ (attribute.value)[0].data; - } - } + let slot_name = determine_slot(child) ?? 'default'; (children[slot_name] ||= []).push(child); } @@ -253,7 +253,15 @@ export function build_component(node, component_name, context, anchor = context. // @ts-expect-error nodes: children[slot_name] }, - slot_name === 'default' ? child_state : context.state + slot_name === 'default' + ? slot_scope_applies_to_itself + ? context.state + : states.default + : { + ...context.state, + scope: node.metadata.scopes[slot_name], + getters: { ...context.state.getters } + } // { // ...context.state, // scope: diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 28d6684a576c..7d5c61c05602 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -101,7 +101,7 @@ export function build_attribute_value(value, context) { } return { - has_state: chunk.metadata.expression.has_call, + has_state: chunk.metadata.expression.has_state, has_call: chunk.metadata.expression.has_call, value: /** @type {Expression} */ (context.visit(chunk.expression)) }; diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js index cf604ad9568b..ce2eb246ee61 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js @@ -26,7 +26,7 @@ export function build_inline_component(node, expression, context) { */ const child_state = { ...context.state, - scope: node.metadata.default_scope + scope: node.metadata.scopes.default }; /** @type {Record} */ @@ -153,7 +153,12 @@ export function build_inline_component(node, expression, context) { // @ts-expect-error nodes: children[slot_name] }, - slot_name === 'default' ? child_state : context.state + slot_name === 'default' + ? child_state + : { + ...context.state, + scope: node.metadata.scopes[slot_name] + } // { // ...context.state, // scope: diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index e181e2e0f730..1d3876203d9d 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1,6 +1,6 @@ /** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */ /** @import { Context, Visitor } from 'zimmerframe' */ -/** @import { AnimateDirective, Binding, DeclarationKind, EachBlock, ElementLike, LetDirective, SvelteNode, TransitionDirective, UseDirective } from '#compiler' */ +/** @import { AnimateDirective, Attribute, Binding, DeclarationKind, EachBlock, ElementLike, LetDirective, SvelteNode, TransitionDirective, UseDirective } from '#compiler' */ import is_reference from 'is-reference'; import { walk } from 'zimmerframe'; import { is_element_node } from './nodes.js'; @@ -14,6 +14,7 @@ import { unwrap_pattern } from '../utils/ast.js'; import { is_reserved, is_rune } from '../../utils.js'; +import { determine_slot } from '../utils/slot.js'; export class Scope { /** @type {ScopeRoot} */ @@ -301,29 +302,37 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { * @type {Visitor} */ const Component = (node, context) => { - const scope = context.state.scope.child(); - node.metadata.default_scope = scope; + node.metadata.scopes = { + default: context.state.scope.child() + }; + + const default_state = !!determine_slot(node) + ? context.state + : { scope: node.metadata.scopes.default }; // scopes.set(node, scope); for (const attribute of node.attributes) { if (attribute.type === 'LetDirective') { - context.visit(attribute, { scope }); + context.visit(attribute, default_state); } else { context.visit(attribute); } } for (const child of node.fragment.nodes) { - if ( - is_element_node(child) && - child.attributes.some( - (a) => a.type === 'Attribute' && a.name === 'slot' && is_text_attribute(a) - ) - ) { - context.visit(child); - } else { - context.visit(child, { scope }); + let state = default_state; + + const slot_name = determine_slot(child); + + if (slot_name !== null) { + node.metadata.scopes[slot_name] = context.state.scope.child(); + + state = { + scope: node.metadata.scopes[slot_name] + }; } + + context.visit(child, state); } // // let:x is super weird: diff --git a/packages/svelte/src/compiler/utils/slot.js b/packages/svelte/src/compiler/utils/slot.js new file mode 100644 index 000000000000..1c05ec8ee6c3 --- /dev/null +++ b/packages/svelte/src/compiler/utils/slot.js @@ -0,0 +1,20 @@ +/** @import { SvelteNode } from '#compiler' */ +import { is_element_node } from '../phases/nodes.js'; +import { is_text_attribute } from './ast.js'; + +/** + * @param {SvelteNode} node + */ +export function determine_slot(node) { + if (!is_element_node(node)) return null; + + for (const attribute of node.attributes) { + if (attribute.type !== 'Attribute') continue; + if (attribute.name !== 'slot') continue; + if (!is_text_attribute(attribute)) continue; + + return /** @type {string} */ (attribute.value[0].data); + } + + return null; +} From 6b55ee1cb1519cd16d6e7dcb5b3758c96b211f2d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 7 Aug 2024 23:12:48 -0400 Subject: [PATCH 12/86] tests passing --- .../phases/2-analyze/visitors/shared/component.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js index 67b5a6d4ff38..cfa3aadbcd56 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js @@ -94,13 +94,9 @@ export function visit_component(node, context) { continue; } - let slot_name = determine_slot(child); + let slot_name = determine_slot(child) ?? 'default'; - if (slot_name) { - nodes[slot_name] = [...comments, child]; - } else { - nodes.default.push(...comments, child); - } + (nodes[slot_name] ??= []).push(...comments, child); } for (const slot_name in nodes) { From c74ba5a2529ae342773fdc43a6e102c47022518c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Aug 2024 07:40:51 -0400 Subject: [PATCH 13/86] remove some stuff --- .../src/compiler/phases/3-transform/client/utils.js | 12 ------------ 1 file changed, 12 deletions(-) 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 88f064834aca..c1cf822e3b6a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -85,18 +85,6 @@ export function build_getter(node, state) { return typeof getter === 'function' ? getter(node) : getter; } - if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { - if (is_prop_source(binding, state)) { - return b.call(node); - } - - if (binding.prop_alias) { - const key = b.key(binding.prop_alias); - return b.member(b.id('$$props'), key, key.type === 'Literal'); - } - return b.member(b.id('$$props'), node); - } - if ( is_state_source(binding, state) || binding.kind === 'derived' || From 8e7e95b32bf9345caebe4ac46af53d0c14110b0b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Aug 2024 09:31:03 -0400 Subject: [PATCH 14/86] more --- .../3-transform/client/transform-client.js | 4 +-- .../phases/3-transform/client/utils.js | 12 +++---- .../3-transform/client/visitors/AwaitBlock.js | 36 +++++++------------ .../client/visitors/BlockStatement.js | 4 ++- .../3-transform/client/visitors/Program.js | 3 ++ .../client/visitors/shared/declarations.js | 27 ++++++++++++++ 6 files changed, 52 insertions(+), 34 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js 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 2755357bcb83..ff3fd23346f1 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 @@ -65,9 +65,9 @@ const visitors = { if (scope && scope !== state.scope) { const getters = { ...state.getters }; + // TODO do this in add_state_transformers and visit_function etc? for (const [name, binding] of scope.declarations) { - // if (binding.kind === 'normal') { - if (binding.kind !== 'each') { + if (binding.kind === 'normal') { delete getters[name]; } } 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 c1cf822e3b6a..2d9d9bff8801 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -17,6 +17,7 @@ import { PROPS_IS_UPDATED } from '../../../../constants.js'; import { is_ignored, dev } from '../../../state.js'; +import { get_value } from './visitors/shared/declarations.js'; /** * @template {ClientTransformState} State @@ -85,14 +86,6 @@ export function build_getter(node, state) { return typeof getter === 'function' ? getter(node) : getter; } - if ( - is_state_source(binding, state) || - binding.kind === 'derived' || - binding.kind === 'legacy_reactive' - ) { - return b.call('$.get', node); - } - return node; } @@ -670,6 +663,7 @@ export function with_loc(target, source) { */ export function create_derived_block_argument(node, context) { if (node.type === 'Identifier') { + context.state.getters[node.name] = get_value; return { id: node, declarations: null }; } @@ -687,6 +681,8 @@ export function create_derived_block_argument(node, context) { const declarations = [b.var(value, create_derived(context.state, b.thunk(block)))]; for (const id of identifiers) { + context.state.getters[id.name] = get_value; + declarations.push( b.var(id, create_derived(context.state, b.thunk(b.member(b.call('$.get', value), id)))) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index 21f16f3449bf..f956a5cf3044 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression, Pattern } from 'estree' */ +/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */ /** @import { AwaitBlock } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '../../../../utils/builders.js'; @@ -15,39 +15,29 @@ export function AwaitBlock(node, context) { let catch_block; if (node.then) { + const argument = node.value && create_derived_block_argument(node.value, context); + /** @type {Pattern[]} */ const args = [b.id('$$anchor')]; - const block = /** @type {BlockStatement} */ (context.visit(node.then)); - - if (node.value) { - const argument = create_derived_block_argument(node.value, context); + if (argument) args.push(argument.id); - args.push(argument.id); - - if (argument.declarations !== null) { - block.body.unshift(...argument.declarations); - } - } + const declarations = argument?.declarations ?? []; + const block = /** @type {BlockStatement} */ (context.visit(node.then)); - then_block = b.arrow(args, block); + then_block = b.arrow(args, b.block([...declarations, ...block.body])); } if (node.catch) { + const argument = node.error && create_derived_block_argument(node.error, context); + /** @type {Pattern[]} */ const args = [b.id('$$anchor')]; - const block = /** @type {BlockStatement} */ (context.visit(node.catch)); - - if (node.error) { - const argument = create_derived_block_argument(node.error, context); + if (argument) args.push(argument.id); - args.push(argument.id); - - if (argument.declarations !== null) { - block.body.unshift(...argument.declarations); - } - } + const declarations = argument?.declarations ?? []; + const block = /** @type {BlockStatement} */ (context.visit(node.catch)); - catch_block = b.arrow(args, block); + catch_block = b.arrow(args, b.block([...declarations, ...block.body])); } context.state.init.push( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js index d5f15ebdf156..502fbd471e6a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js @@ -1,10 +1,12 @@ -/** @import { BlockStatement, Expression } from 'estree' */ +/** @import { BlockStatement } from 'estree' */ /** @import { ComponentContext } from '../types' */ +import { add_state_transformers } from './shared/declarations.js'; /** * @param {BlockStatement} node * @param {ComponentContext} context */ export function BlockStatement(node, context) { + add_state_transformers(context); context.next(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index 1d850528c566..6954f052831a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -2,6 +2,7 @@ /** @import { ComponentContext } from '../types' */ import { is_prop_source } from '../utils.js'; import * as b from '../../../../utils/builders.js'; +import { add_state_transformers } from './shared/declarations.js'; /** * @param {Program} node @@ -28,5 +29,7 @@ export function Program(node, context) { } } + add_state_transformers(context); + context.next(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js new file mode 100644 index 000000000000..80627fd8f3f8 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js @@ -0,0 +1,27 @@ +/** @import { Identifier } from 'estree' */ +/** @import { ComponentContext, Context } from '../../types' */ +import { is_state_source } from '../../utils.js'; +import * as b from '../../../../../utils/builders.js'; + +/** + * @param {Identifier} node + */ +export function get_value(node) { + return b.call('$.get', node); +} + +/** + * + * @param {Context | ComponentContext} context + */ +export function add_state_transformers(context) { + for (const [name, binding] of context.state.scope.declarations) { + if ( + is_state_source(binding, context.state) || + binding.kind === 'derived' || + binding.kind === 'legacy_reactive' + ) { + context.state.getters[name] = get_value; + } + } +} From 690f012b644ec21ac7d9f4bb81e5bf5c0bc5c08b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Aug 2024 11:17:15 -0400 Subject: [PATCH 15/86] fix --- .../phases/3-transform/client/transform-client.js | 6 ------ .../3-transform/client/visitors/RegularElement.js | 11 ++++++++--- 2 files changed, 8 insertions(+), 9 deletions(-) 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 ff3fd23346f1..068788477a66 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 @@ -209,12 +209,6 @@ export function client_component(analysis, options) { is_instance: true }; - for (const [name, binding] of instance_state.scope.declarations) { - if (binding.kind === 'store_sub') { - // instance_state.getters[name] = (node) => b.call(node); - } - } - const instance = /** @type {ESTree.Program} */ ( walk(/** @type {SvelteNode} */ (analysis.instance.ast), instance_state, visitors) ); 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 20630277821a..92c3e6071f6a 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 @@ -100,6 +100,13 @@ export function RegularElement(node, context) { metadata.context.template_needs_import_node = true; } + // visit let directives first, to set state + for (const attribute of node.attributes) { + if (attribute.type === 'LetDirective') { + lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); + } + } + for (const attribute of node.attributes) { if (attribute.type === 'Attribute') { attributes.push(attribute); @@ -136,8 +143,6 @@ export function RegularElement(node, context) { class_directives.push(attribute); } else if (attribute.type === 'StyleDirective') { style_directives.push(attribute); - } else if (attribute.type === 'LetDirective') { - lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); } else if (attribute.type === 'OnDirective') { const handler = /** @type {Expression} */ (context.visit(attribute)); const has_action_directive = node.attributes.find((a) => a.type === 'UseDirective'); @@ -145,7 +150,7 @@ export function RegularElement(node, context) { context.state.after_update.push( b.stmt(has_action_directive ? b.call('$.effect', b.thunk(handler)) : handler) ); - } else { + } else if (attribute.type !== 'LetDirective') { if (attribute.type === 'BindDirective') { if (attribute.name === 'group' || attribute.name === 'checked') { needs_special_value_handling = true; From 7192ece1debb1c328311ed8041c0ddc5d89329d7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Aug 2024 11:24:02 -0400 Subject: [PATCH 16/86] tidy up --- .../phases/2-analyze/visitors/shared/component.js | 11 ++++------- packages/svelte/src/compiler/phases/scope.js | 4 ++-- packages/svelte/src/compiler/types/template.d.ts | 8 ++++++++ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js index cfa3aadbcd56..27e2ea561d9b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js @@ -1,13 +1,8 @@ -/** @import { Component, SvelteComponent, SvelteSelf } from '#compiler' */ +/** @import { Component, Fragment, SvelteComponent, SvelteSelf } from '#compiler' */ /** @import { Context } from '../../types' */ import * as e from '../../../../errors.js'; -import { - get_attribute_expression, - is_expression_attribute, - is_text_attribute -} from '../../../../utils/ast.js'; +import { get_attribute_expression, is_expression_attribute } from '../../../../utils/ast.js'; import { determine_slot } from '../../../../utils/slot.js'; -import { is_element_node } from '../../../nodes.js'; import { validate_attribute, validate_attribute_name, @@ -84,6 +79,8 @@ export function visit_component(node, context) { } const comments = []; + + /** @type {Record} */ const nodes = { default: [] }; diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 1d3876203d9d..9a04b8761c28 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1,6 +1,6 @@ /** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */ /** @import { Context, Visitor } from 'zimmerframe' */ -/** @import { AnimateDirective, Attribute, Binding, DeclarationKind, EachBlock, ElementLike, LetDirective, SvelteNode, TransitionDirective, UseDirective } from '#compiler' */ +/** @import { AnimateDirective, Attribute, Binding, Component, DeclarationKind, EachBlock, ElementLike, LetDirective, SvelteComponent, SvelteNode, SvelteSelf, TransitionDirective, UseDirective } from '#compiler' */ import is_reference from 'is-reference'; import { walk } from 'zimmerframe'; import { is_element_node } from './nodes.js'; @@ -299,7 +299,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { }; /** - * @type {Visitor} + * @type {Visitor} */ const Component = (node, context) => { node.metadata.scopes = { diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 86341d07d007..20fdb25ee418 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -16,6 +16,7 @@ import type { ChainExpression, SimpleCallExpression } from 'estree'; +import type { Scope } from '../phases/scope'; export interface BaseNode { type: string; @@ -275,6 +276,7 @@ interface BaseElement extends BaseNode { export interface Component extends BaseElement { type: 'Component'; metadata: { + scopes: Record; dynamic: boolean; }; } @@ -311,6 +313,9 @@ export interface SvelteComponent extends BaseElement { type: 'SvelteComponent'; name: 'svelte:component'; expression: Expression; + metadata: { + scopes: Record; + }; } interface SvelteDocument extends BaseElement { @@ -356,6 +361,9 @@ export interface SvelteOptionsRaw extends BaseElement { export interface SvelteSelf extends BaseElement { type: 'SvelteSelf'; name: 'svelte:self'; + metadata: { + scopes: Record; + }; } interface SvelteWindow extends BaseElement { From e8ec10d98d56a187b49fb471f1d5189882a52502 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Aug 2024 12:24:10 -0400 Subject: [PATCH 17/86] simplify --- .../compiler/phases/3-transform/client/types.d.ts | 2 +- .../src/compiler/phases/3-transform/client/utils.js | 2 +- .../phases/3-transform/client/visitors/ConstTag.js | 7 ++++--- .../phases/3-transform/client/visitors/EachBlock.js | 11 ++++++----- .../3-transform/client/visitors/LetDirective.js | 6 ++---- .../3-transform/client/visitors/SnippetBlock.js | 7 ++++--- .../client/visitors/shared/declarations.js | 1 + .../3-transform/client/visitors/shared/utils.js | 13 ++++++++----- 8 files changed, 27 insertions(+), 22 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 0284c82d1ee6..251c4a15bdd2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -28,7 +28,7 @@ export interface ClientTransformState extends TransformState { * A map of `[name, node]` pairs, where `Identifier` nodes matching `name` * will be replaced with `node` (e.g. `x` -> `$.get(x)`) */ - readonly getters: Record Expression)>; + readonly getters: Record Expression>; /** * Counterpart to `getters` */ 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 2d9d9bff8801..00131991e2de 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -464,7 +464,7 @@ function get_hoisted_params(node, context) { binding = /** @type {Binding} */ (scope.get(binding.node.name.slice(1))); } - const expression = context.state.getters[reference]; + let expression = context.state.getters[reference]?.(b.id(binding.node.name)); if ( // If it's a destructured derived binding, then we can extract the derived signal reference and use that. diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index 10623ce3f5c0..d93bdc2c2f69 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -5,6 +5,7 @@ import { dev } from '../../../../state.js'; import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '../../../../utils/builders.js'; import { create_derived } from '../utils.js'; +import { get_value } from './shared/declarations.js'; /** * @param {ConstTag} node @@ -24,7 +25,7 @@ export function ConstTag(node, context) { ) ); - context.state.getters[declaration.id.name] = b.call('$.get', declaration.id); + context.state.getters[declaration.id.name] = get_value; // we need to eagerly evaluate the expression in order to hit any // 'Cannot access x before initialization' errors @@ -40,7 +41,7 @@ export function ConstTag(node, context) { // Make all identifiers that are declared within the following computed regular // variables, as they are not signals in that context yet for (const node of identifiers) { - getters[node.name] = node; + delete getters[node.name]; } const child_state = { ...context.state, getters }; @@ -67,7 +68,7 @@ export function ConstTag(node, context) { } for (const node of identifiers) { - context.state.getters[node.name] = b.member(b.call('$.get', tmp), node); + context.state.getters[node.name] = (node) => b.member(b.call('$.get', tmp), node); } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 665522683b2b..5db5d646ac20 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -14,6 +14,7 @@ import { dev } from '../../../../state.js'; import { extract_paths, object } from '../../../../utils/ast.js'; import * as b from '../../../../utils/builders.js'; import { get_assignment_value, build_getter, build_setter, with_loc } from '../utils.js'; +import { get_value } from './shared/declarations.js'; /** * @param {EachBlock} node @@ -187,7 +188,7 @@ export function EachBlock(node, context) { return (flags & EACH_INDEX_REACTIVE) === 0 ? index_with_loc : b.call('$.get', index_with_loc); }; - key_state.getters[node.index] = b.id(node.index); + delete key_state.getters[node.index]; } /** @type {Statement[]} */ @@ -202,7 +203,7 @@ export function EachBlock(node, context) { ) ); - key_state.getters[node.context.name] = node.context; + delete key_state.getters[node.context.name]; } else { const unwrapped = getter(binding.node); const paths = extract_paths(node.context); @@ -217,7 +218,7 @@ export function EachBlock(node, context) { declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn)); - const getter = needs_derived ? b.call('$.get', b.id(name)) : b.call(name); + const getter = needs_derived ? get_value : b.call; child_state.getters[name] = getter; child_state.setters[name] = create_mutation( /** @type {Pattern} */ (path.update_expression(unwrapped)) @@ -226,10 +227,10 @@ export function EachBlock(node, context) { // we need to eagerly evaluate the expression in order to hit any // 'Cannot access x before initialization' errors if (dev) { - declarations.push(b.stmt(getter)); + declarations.push(b.stmt(getter(b.id(name)))); } - key_state.getters[name] = path.node; + key_state.getters[name] = () => path.node; } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js index 9d42317bda46..0772dd9948d5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js @@ -16,10 +16,8 @@ export function LetDirective(node, context) { const bindings = context.state.scope.get_bindings(node); for (const binding of bindings) { - context.state.getters[binding.node.name] = b.member( - b.call('$.get', b.id(name)), - b.id(binding.node.name) - ); + context.state.getters[binding.node.name] = (node) => + b.member(b.call('$.get', b.id(name)), node); } return b.const( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js index cdb745544744..62b99bbe0b31 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js @@ -4,6 +4,7 @@ import { dev } from '../../../../state.js'; import { extract_paths } from '../../../../utils/ast.js'; import * as b from '../../../../utils/builders.js'; +import { get_value } from './shared/declarations.js'; /** * @param {SnippetBlock} node @@ -35,7 +36,7 @@ export function SnippetBlock(node, context) { right: b.id('$.noop') }); - getters[argument.name] = b.call(argument); + getters[argument.name] = b.call; continue; } @@ -53,12 +54,12 @@ export function SnippetBlock(node, context) { declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn)); - getters[name] = needs_derived ? b.call('$.get', b.id(name)) : b.call(name); + getters[name] = needs_derived ? get_value : b.call; // we need to eagerly evaluate the expression in order to hit any // 'Cannot access x before initialization' errors if (dev) { - declarations.push(b.stmt(getters[name])); + declarations.push(b.stmt(getters[name](b.id(name)))); } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js index 80627fd8f3f8..2f368d7d33b8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js @@ -4,6 +4,7 @@ import { is_state_source } from '../../utils.js'; import * as b from '../../../../../utils/builders.js'; /** + * Turns `foo` into `$.get(foo)` * @param {Identifier} node */ export function get_value(node) { 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 874ebe31314b..3c6a06f504d7 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 @@ -145,8 +145,10 @@ export function build_bind_this(expression, value, { state, visit }) { /** @type {Expression[]} */ const values = []; - /** @type {typeof state.getters} */ - const getters = {}; + /** @type {string[]} */ + const seen = []; + + const getters = { ...state.getters }; // Pass in each context variables to the get/set functions, so that we can null out old values on teardown. // Note that we only do this for each context variables, the consequence is that the value might be stale in @@ -154,7 +156,8 @@ export function build_bind_this(expression, value, { state, visit }) { // variables, but that was the same case in Svelte 4, too. Once legacy mode is gone completely, we can revisit this. walk(expression, null, { Identifier(node, { path }) { - if (Object.hasOwn(getters, node.name)) return; + if (seen.includes(node.name)) return; + seen.push(node.name); const parent = /** @type {Expression} */ (path.at(-1)); if (!is_reference(node, parent)) return; @@ -166,14 +169,14 @@ export function build_bind_this(expression, value, { state, visit }) { if (owner.type === 'EachBlock' && scope === binding.scope) { ids.push(node); values.push(/** @type {Expression} */ (visit(node))); - getters[node.name] = node; + delete getters[node.name]; break; } } } }); - const child_state = { ...state, getters: { ...state.getters, ...getters } }; + const child_state = { ...state, getters }; const get = /** @type {Expression} */ (visit(expression, child_state)); const set = /** @type {Expression} */ ( From 804772f6d4dc697f187f2463988a3a8b35e97319 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Aug 2024 13:26:33 -0400 Subject: [PATCH 18/86] simplify --- .../compiler/phases/3-transform/client/utils.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) 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 00131991e2de..ee094a1fcd3f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -74,16 +74,13 @@ export function is_state_source(binding, state) { * @returns {Expression} */ export function build_getter(node, state) { - const binding = state.scope.get(node.name); - - if (binding === null || node === binding.node) { - // No associated binding or the declaration itself which shouldn't be transformed - return node; - } - if (Object.hasOwn(state.getters, node.name)) { - const getter = state.getters[node.name]; - return typeof getter === 'function' ? getter(node) : getter; + const binding = state.scope.get(node.name); + + // don't transform the declaration itself + if (node !== binding?.node) { + return state.getters[node.name](node); + } } return node; From 8b3832685865dd50afab6bb3f084133a38182652 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Aug 2024 13:44:26 -0400 Subject: [PATCH 19/86] getters -> transformers --- .../3-transform/client/transform-client.js | 22 ++++++++----- .../phases/3-transform/client/types.d.ts | 12 ++++--- .../phases/3-transform/client/utils.js | 13 +++++--- .../3-transform/client/visitors/ConstTag.js | 14 +++++--- .../3-transform/client/visitors/EachBlock.js | 33 +++++++++++-------- .../3-transform/client/visitors/Fragment.js | 2 +- .../client/visitors/LetDirective.js | 9 +++-- .../3-transform/client/visitors/Program.js | 15 +++++---- .../client/visitors/SnippetBlock.js | 15 ++++++--- .../client/visitors/shared/component.js | 4 +-- .../client/visitors/shared/declarations.js | 4 ++- .../client/visitors/shared/utils.js | 6 ++-- 12 files changed, 91 insertions(+), 58 deletions(-) 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 068788477a66..594a7144ca26 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 @@ -63,16 +63,16 @@ const visitors = { const scope = state.scopes.get(node); if (scope && scope !== state.scope) { - const getters = { ...state.getters }; + const transformers = { ...state.transformers }; // TODO do this in add_state_transformers and visit_function etc? for (const [name, binding] of scope.declarations) { if (binding.kind === 'normal') { - delete getters[name]; + delete transformers[name]; } } - next({ ...state, getters, scope }); + next({ ...state, transformers, scope }); } else { next(); } @@ -156,7 +156,7 @@ export function client_component(analysis, options) { preserve_whitespace: options.preserveWhitespace, public_state: new Map(), private_state: new Map(), - getters: {}, + transformers: {}, setters: {}, in_constructor: false, @@ -173,12 +173,16 @@ export function client_component(analysis, options) { const legacy_reactive_imports = []; if (!analysis.runes) { - state.getters['$$props'] = (node) => ({ ...node, name: '$$sanitized_props' }); + state.transformers['$$props'] = { + read: (node) => ({ ...node, name: '$$sanitized_props' }) + }; for (const [name, binding] of state.scope.declarations) { // Very very dirty way of making import statements reactive in legacy mode if needed if (binding.declaration_kind === 'import' && binding.mutated) { - state.getters[name] = (node) => b.call('$$_import_' + node.name); + state.transformers[name] = { + read: (node) => b.call('$$_import_' + node.name) + }; state.setters[name] = (node, context) => b.call( @@ -203,7 +207,7 @@ export function client_component(analysis, options) { const instance_state = { ...state, - getters: { ...state.getters }, + transformers: { ...state.transformers }, scope: analysis.instance.scope, scopes: analysis.instance.scopes, is_instance: true @@ -218,7 +222,7 @@ export function client_component(analysis, options) { /** @type {SvelteNode} */ (analysis.template.ast), { ...state, - getters: instance_state.getters, + transformers: instance_state.transformers, scope: analysis.instance.scope, scopes: analysis.template.scopes }, @@ -669,7 +673,7 @@ export function client_module(analysis, options) { legacy_reactive_statements: new Map(), public_state: new Map(), private_state: new Map(), - getters: {}, + transformers: {}, setters: {}, in_constructor: false }; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 251c4a15bdd2..b2ce9655be55 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -24,11 +24,13 @@ export interface ClientTransformState extends TransformState { /** The $: calls, which will be ordered in the end */ readonly legacy_reactive_statements: Map; - /** - * A map of `[name, node]` pairs, where `Identifier` nodes matching `name` - * will be replaced with `node` (e.g. `x` -> `$.get(x)`) - */ - readonly getters: Record Expression>; + readonly transformers: Record< + string, + { + /** a function that turns `foo` into e.g. `$.get(foo)` */ + read: (id: Identifier) => Expression; + } + >; /** * Counterpart to `getters` */ 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 ee094a1fcd3f..f1a0f3c7f080 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -74,12 +74,12 @@ export function is_state_source(binding, state) { * @returns {Expression} */ export function build_getter(node, state) { - if (Object.hasOwn(state.getters, node.name)) { + if (Object.hasOwn(state.transformers, node.name)) { const binding = state.scope.get(node.name); // don't transform the declaration itself if (node !== binding?.node) { - return state.getters[node.name](node); + return state.transformers[node.name].read(node); } } @@ -461,7 +461,7 @@ function get_hoisted_params(node, context) { binding = /** @type {Binding} */ (scope.get(binding.node.name.slice(1))); } - let expression = context.state.getters[reference]?.(b.id(binding.node.name)); + let expression = context.state.transformers[reference]?.read(b.id(binding.node.name)); if ( // If it's a destructured derived binding, then we can extract the derived signal reference and use that. @@ -660,7 +660,10 @@ export function with_loc(target, source) { */ export function create_derived_block_argument(node, context) { if (node.type === 'Identifier') { - context.state.getters[node.name] = get_value; + context.state.transformers[node.name] = { + read: get_value + }; + return { id: node, declarations: null }; } @@ -678,7 +681,7 @@ export function create_derived_block_argument(node, context) { const declarations = [b.var(value, create_derived(context.state, b.thunk(block)))]; for (const id of identifiers) { - context.state.getters[id.name] = get_value; + context.state.transformers[id.name] = { read: get_value }; declarations.push( b.var(id, create_derived(context.state, b.thunk(b.member(b.call('$.get', value), id)))) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index d93bdc2c2f69..8d8a8beb0f1b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -25,7 +25,9 @@ export function ConstTag(node, context) { ) ); - context.state.getters[declaration.id.name] = get_value; + context.state.transformers[declaration.id.name] = { + read: get_value + }; // we need to eagerly evaluate the expression in order to hit any // 'Cannot access x before initialization' errors @@ -36,15 +38,15 @@ export function ConstTag(node, context) { const identifiers = extract_identifiers(declaration.id); const tmp = b.id(context.state.scope.generate('computed_const')); - const getters = { ...context.state.getters }; + const transformers = { ...context.state.transformers }; // Make all identifiers that are declared within the following computed regular // variables, as they are not signals in that context yet for (const node of identifiers) { - delete getters[node.name]; + delete transformers[node.name]; } - const child_state = { ...context.state, getters }; + const child_state = { ...context.state, transformers }; // TODO optimise the simple `{ x } = y` case — we can just return `y` // instead of destructuring it only to return a new object @@ -68,7 +70,9 @@ export function ConstTag(node, context) { } for (const node of identifiers) { - context.state.getters[node.name] = (node) => b.member(b.call('$.get', tmp), node); + context.state.transformers[node.name] = { + read: (node) => b.member(b.call('$.get', tmp), node) + }; } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 5db5d646ac20..c82a95f5902e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -120,14 +120,14 @@ export function EachBlock(node, context) { const child_state = { ...context.state, - getters: { ...context.state.getters }, + transformers: { ...context.state.transformers }, setters: { ...context.state.setters } }; /** The state used when generating the key function, if necessary */ const key_state = { ...context.state, - getters: { ...context.state.getters } + transformers: { ...context.state.transformers } }; /** @@ -180,15 +180,21 @@ export function EachBlock(node, context) { const item_with_loc = with_loc(item, id); return b.call('$.unwrap', item_with_loc); }; - child_state.getters[item.name] = getter; + child_state.transformers[item.name] = { + read: getter + }; if (node.index) { - child_state.getters[node.index] = (id) => { - const index_with_loc = with_loc(index, id); - return (flags & EACH_INDEX_REACTIVE) === 0 ? index_with_loc : b.call('$.get', index_with_loc); + child_state.transformers[node.index] = { + read: (id) => { + const index_with_loc = with_loc(index, id); + return (flags & EACH_INDEX_REACTIVE) === 0 + ? index_with_loc + : b.call('$.get', index_with_loc); + } }; - delete key_state.getters[node.index]; + delete key_state.transformers[node.index]; } /** @type {Statement[]} */ @@ -203,14 +209,13 @@ export function EachBlock(node, context) { ) ); - delete key_state.getters[node.context.name]; + delete key_state.transformers[node.context.name]; } else { const unwrapped = getter(binding.node); const paths = extract_paths(node.context); for (const path of paths) { const name = /** @type {Identifier} */ (path.node).name; - const binding = /** @type {Binding} */ (context.state.scope.get(name)); const needs_derived = path.has_default_value; // to ensure that default value is only called once const fn = b.thunk( /** @type {Expression} */ (context.visit(path.expression?.(unwrapped), child_state)) @@ -218,8 +223,8 @@ export function EachBlock(node, context) { declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn)); - const getter = needs_derived ? get_value : b.call; - child_state.getters[name] = getter; + const read = needs_derived ? get_value : b.call; + child_state.transformers[name] = { read }; child_state.setters[name] = create_mutation( /** @type {Pattern} */ (path.update_expression(unwrapped)) ); @@ -227,10 +232,12 @@ export function EachBlock(node, context) { // we need to eagerly evaluate the expression in order to hit any // 'Cannot access x before initialization' errors if (dev) { - declarations.push(b.stmt(getter(b.id(name)))); + declarations.push(b.stmt(read(b.id(name)))); } - key_state.getters[name] = () => path.node; + key_state.transformers[name] = { + read: () => path.node + }; } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index 1696d9213eaf..a842ba07ee52 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -66,7 +66,7 @@ export function Fragment(node, context) { after_update: [], template: [], locations: [], - getters: { ...context.state.getters }, + transformers: { ...context.state.transformers }, metadata: { context: { template_needs_import_node: false, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js index 0772dd9948d5..e4d6d06acf6f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js @@ -16,8 +16,9 @@ export function LetDirective(node, context) { const bindings = context.state.scope.get_bindings(node); for (const binding of bindings) { - context.state.getters[binding.node.name] = (node) => - b.member(b.call('$.get', b.id(name)), node); + context.state.transformers[binding.node.name] = { + read: (node) => b.member(b.call('$.get', b.id(name)), node) + }; } return b.const( @@ -41,7 +42,9 @@ export function LetDirective(node, context) { ); } else { const name = node.expression === null ? node.name : node.expression.name; - context.state.getters[name] = (node) => b.call('$.get', node); + context.state.transformers[name] = { + read: (node) => b.call('$.get', node) + }; return b.const( name, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index 6954f052831a..a2c967edb75b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -1,4 +1,4 @@ -/** @import { Program, Expression } from 'estree' */ +/** @import { Program } from 'estree' */ /** @import { ComponentContext } from '../types' */ import { is_prop_source } from '../utils.js'; import * as b from '../../../../utils/builders.js'; @@ -12,18 +12,21 @@ export function Program(node, context) { if (context.state.is_instance) { for (const [name, binding] of context.state.scope.declarations) { if (binding.kind === 'store_sub') { - context.state.getters[name] = (node) => b.call(node); + context.state.transformers[name] = { read: b.call }; } if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { if (is_prop_source(binding, context.state)) { - context.state.getters[name] = (node) => b.call(node); + context.state.transformers[name] = { read: b.call }; } else if (binding.prop_alias) { const key = b.key(binding.prop_alias); - context.state.getters[name] = (node) => - b.member(b.id('$$props'), key, key.type === 'Literal'); + context.state.transformers[name] = { + read: (node) => b.member(b.id('$$props'), key, key.type === 'Literal') + }; } else { - context.state.getters[name] = (node) => b.member(b.id('$$props'), node); + context.state.transformers[name] = { + read: (node) => b.member(b.id('$$props'), node) + }; } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js index 62b99bbe0b31..d0ac1641be97 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js @@ -21,8 +21,8 @@ export function SnippetBlock(node, context) { /** @type {Statement[]} */ const declarations = []; - const getters = { ...context.state.getters }; - const child_state = { ...context.state, getters }; + const transformers = { ...context.state.transformers }; + const child_state = { ...context.state, transformers }; for (let i = 0; i < node.parameters.length; i++) { const argument = node.parameters[i]; @@ -36,7 +36,10 @@ export function SnippetBlock(node, context) { right: b.id('$.noop') }); - getters[argument.name] = b.call; + transformers[argument.name] = { + read: b.call + }; + continue; } @@ -54,12 +57,14 @@ export function SnippetBlock(node, context) { declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn)); - getters[name] = needs_derived ? get_value : b.call; + transformers[name] = { + read: needs_derived ? get_value : b.call + }; // we need to eagerly evaluate the expression in order to hit any // 'Cannot access x before initialization' errors if (dev) { - declarations.push(b.stmt(getters[name](b.id(name)))); + declarations.push(b.stmt(transformers[name].read(b.id(name)))); } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 3d729431fb04..baf9918b975b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -30,7 +30,7 @@ export function build_component(node, component_name, context, anchor = context. default: { ...context.state, scope: node.metadata.scopes.default, - getters: { ...context.state.getters } + transformers: { ...context.state.transformers } } }; @@ -260,7 +260,7 @@ export function build_component(node, component_name, context, anchor = context. : { ...context.state, scope: node.metadata.scopes[slot_name], - getters: { ...context.state.getters } + transformers: { ...context.state.transformers } } // { // ...context.state, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js index 2f368d7d33b8..7e51b1ce963e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js @@ -22,7 +22,9 @@ export function add_state_transformers(context) { binding.kind === 'derived' || binding.kind === 'legacy_reactive' ) { - context.state.getters[name] = get_value; + context.state.transformers[name] = { + read: get_value + }; } } } 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 3c6a06f504d7..4f865e387efc 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 @@ -148,7 +148,7 @@ export function build_bind_this(expression, value, { state, visit }) { /** @type {string[]} */ const seen = []; - const getters = { ...state.getters }; + const transformers = { ...state.transformers }; // Pass in each context variables to the get/set functions, so that we can null out old values on teardown. // Note that we only do this for each context variables, the consequence is that the value might be stale in @@ -169,14 +169,14 @@ export function build_bind_this(expression, value, { state, visit }) { if (owner.type === 'EachBlock' && scope === binding.scope) { ids.push(node); values.push(/** @type {Expression} */ (visit(node))); - delete getters[node.name]; + delete transformers[node.name]; break; } } } }); - const child_state = { ...state, getters }; + const child_state = { ...state, transformers }; const get = /** @type {Expression} */ (visit(expression, child_state)); const set = /** @type {Expression} */ ( From 099cb880f55dc2b128571fb3b2a4547dfa9474f0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Aug 2024 14:06:54 -0400 Subject: [PATCH 20/86] update --- .../src/compiler/phases/3-transform/client/types.d.ts | 9 +++++++-- .../3-transform/client/visitors/UpdateExpression.js | 6 ++++++ .../3-transform/client/visitors/shared/declarations.js | 9 ++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index b2ce9655be55..de0d438be51f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -5,7 +5,8 @@ import type { Identifier, PrivateIdentifier, Expression, - AssignmentExpression + AssignmentExpression, + UpdateExpression } from 'estree'; import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler'; import type { TransformState } from '../types.js'; @@ -27,8 +28,12 @@ export interface ClientTransformState extends TransformState { readonly transformers: Record< string, { - /** a function that turns `foo` into e.g. `$.get(foo)` */ + /** turn `foo` into e.g. `$.get(foo)` */ read: (id: Identifier) => Expression; + /** turn `foo++` into e.g. `$.update(foo)` */ + update?: (node: UpdateExpression) => Expression; + /** turn `foo.bar++` into e.g. `$.mutate(foo, $.get(foo).bar += 1)` */ + update_property?: (id: Identifier) => Expression; } >; /** diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js index 5f8685eb3bfb..aeb948c547fd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js @@ -10,8 +10,14 @@ import { build_getter, build_setter } from '../utils.js'; */ export function UpdateExpression(node, context) { const argument = node.argument; + const transformers = context.state.transformers; if (argument.type === 'Identifier') { + if (Object.hasOwn(transformers, argument.name) && transformers[argument.name].update) { + const transformer = transformers[argument.name].update; + if (transformer) return transformer(node); + } + const binding = context.state.scope.get(argument.name); const is_store = binding?.kind === 'store_sub'; const name = is_store ? argument.name.slice(1) : argument.name; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js index 7e51b1ce963e..5acb2e67410a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js @@ -23,7 +23,14 @@ export function add_state_transformers(context) { binding.kind === 'legacy_reactive' ) { context.state.transformers[name] = { - read: get_value + read: get_value, + update: (node) => { + return b.call( + node.prefix ? '$.update_pre' : '$.update', + node.argument, + node.operator === '--' && b.literal(-1) + ); + } }; } } From 65f125ea2c13930cc1f9b6fab96b3ca3272dfc2e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Aug 2024 14:19:18 -0400 Subject: [PATCH 21/86] use update transformers --- .../3-transform/client/visitors/Program.js | 25 +++++++++++-- .../client/visitors/UpdateExpression.js | 35 ------------------- 2 files changed, 22 insertions(+), 38 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index a2c967edb75b..fa4af03a5fab 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -1,6 +1,6 @@ /** @import { Program } from 'estree' */ /** @import { ComponentContext } from '../types' */ -import { is_prop_source } from '../utils.js'; +import { build_getter, is_prop_source } from '../utils.js'; import * as b from '../../../../utils/builders.js'; import { add_state_transformers } from './shared/declarations.js'; @@ -12,12 +12,31 @@ export function Program(node, context) { if (context.state.is_instance) { for (const [name, binding] of context.state.scope.declarations) { if (binding.kind === 'store_sub') { - context.state.transformers[name] = { read: b.call }; + context.state.transformers[name] = { + read: b.call, + update: (node) => { + return b.call( + node.prefix ? '$.update_pre_store' : '$.update_store', + build_getter(b.id(name.slice(1)), context.state), + b.call(node.argument), + node.operator === '--' && b.literal(-1) + ); + } + }; } if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { if (is_prop_source(binding, context.state)) { - context.state.transformers[name] = { read: b.call }; + context.state.transformers[name] = { + read: b.call, + update: (node) => { + return b.call( + node.prefix ? '$.update_pre_prop' : '$.update_prop', + node.argument, + node.operator === '--' && b.literal(-1) + ); + } + }; } else if (binding.prop_alias) { const key = b.key(binding.prop_alias); context.state.transformers[name] = { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js index aeb948c547fd..384b9caa27d6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js @@ -18,41 +18,6 @@ export function UpdateExpression(node, context) { if (transformer) return transformer(node); } - const binding = context.state.scope.get(argument.name); - const is_store = binding?.kind === 'store_sub'; - const name = is_store ? argument.name.slice(1) : argument.name; - - // use runtime functions for smaller output - if ( - binding?.kind === 'state' || - binding?.kind === 'frozen_state' || - binding?.kind === 'each' || - binding?.kind === 'legacy_reactive' || - binding?.kind === 'prop' || - binding?.kind === 'bindable_prop' || - is_store - ) { - /** @type {Expression[]} */ - const args = []; - - let fn = '$.update'; - if (node.prefix) fn += '_pre'; - - if (is_store) { - fn += '_store'; - args.push(build_getter(b.id(name), context.state), b.call('$' + name)); - } else { - if (binding.kind === 'prop' || binding.kind === 'bindable_prop') fn += '_prop'; - args.push(b.id(name)); - } - - if (node.operator === '--') { - args.push(b.literal(-1)); - } - - return b.call(fn, ...args); - } - return context.next(); } From c4683eaa5e9219b82b6d666749cccb8db87faf92 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Aug 2024 16:50:14 -0400 Subject: [PATCH 22/86] add assign transformer --- .../phases/3-transform/client/types.d.ts | 2 ++ .../client/visitors/AssignmentExpression.js | 10 ++++++ .../client/visitors/shared/declarations.js | 32 +++++++++++++++++-- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index de0d438be51f..559197c359fd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -30,6 +30,8 @@ export interface ClientTransformState extends TransformState { { /** turn `foo` into e.g. `$.get(foo)` */ read: (id: Identifier) => Expression; + /** turn `foo = bar` into e.g. `$.set(foo, bar)` */ + assign?: (node: AssignmentExpression, visit: (node: SvelteNode) => SvelteNode) => Expression; /** turn `foo++` into e.g. `$.update(foo)` */ update?: (node: UpdateExpression) => Expression; /** turn `foo.bar++` into e.g. `$.mutate(foo, $.get(foo).bar += 1)` */ diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index fefc11bf1f34..208b5bedb0c0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -7,5 +7,15 @@ import { build_setter } from '../utils.js'; * @param {Context} context */ export function AssignmentExpression(node, context) { + if (node.left.type === 'Identifier') { + if (Object.hasOwn(context.state.transformers, node.left.name)) { + const transformer = context.state.transformers[node.left.name]?.assign; + + if (transformer) { + return transformer(node, context.visit); + } + } + } + return build_setter(node, context, context.next); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js index 5acb2e67410a..b0884acb8d95 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js @@ -1,6 +1,6 @@ -/** @import { Identifier } from 'estree' */ +/** @import { BinaryOperator, Expression, Identifier } from 'estree' */ /** @import { ComponentContext, Context } from '../../types' */ -import { is_state_source } from '../../utils.js'; +import { build_proxy_reassignment, is_state_source, should_proxy_or_freeze } from '../../utils.js'; import * as b from '../../../../../utils/builders.js'; /** @@ -24,6 +24,34 @@ export function add_state_transformers(context) { ) { context.state.transformers[name] = { read: get_value, + assign: (node, visit) => { + let left = /** @type {Identifier} */ (node.left); + let value = /** @type {Expression} */ (visit(node.right)); + + if (node.operator !== '=') { + value = b.binary( + /** @type {BinaryOperator} */ (node.operator.slice(0, -1)), + /** @type {Expression} */ (visit(left)), + value + ); + } + + if (context.state.analysis.runes && should_proxy_or_freeze(value, context.state.scope)) { + if (binding.kind === 'frozen_state') { + value = b.call('$.freeze', value); + } else { + value = build_proxy_reassignment(value, left.name); + } + } + + let call = b.call('$.set', left, value); + + if (context.state.scope.get(`$${left.name}`)?.kind === 'store_sub') { + call = b.call('$.store_unsub', call, b.literal(`$${left.name}`), b.id('$$stores')); + } + + return call; + }, update: (node) => { return b.call( node.prefix ? '$.update_pre' : '$.update', From 6c8a8f44995e9ad3d4f4a18b7e40bb2c236f5df2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Aug 2024 20:35:54 -0400 Subject: [PATCH 23/86] more --- .../phases/3-transform/client/types.d.ts | 6 +++- .../phases/3-transform/client/utils.js | 8 +++++ .../client/visitors/AssignmentExpression.js | 10 ------ .../3-transform/client/visitors/Program.js | 31 +++++++++++++++++-- .../client/visitors/shared/declarations.js | 8 +++-- 5 files changed, 48 insertions(+), 15 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 559197c359fd..1a640fd1f975 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -31,7 +31,11 @@ export interface ClientTransformState extends TransformState { /** turn `foo` into e.g. `$.get(foo)` */ read: (id: Identifier) => Expression; /** turn `foo = bar` into e.g. `$.set(foo, bar)` */ - assign?: (node: AssignmentExpression, visit: (node: SvelteNode) => SvelteNode) => Expression; + assign?: ( + node: AssignmentExpression, + visit: (node: SvelteNode) => SvelteNode, + is_primitive: boolean + ) => Expression; /** turn `foo++` into e.g. `$.update(foo)` */ update?: (node: UpdateExpression) => Expression; /** turn `foo.bar++` into e.g. `$.mutate(foo, $.get(foo).bar += 1)` */ 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 f1a0f3c7f080..00fb0089e871 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -272,6 +272,14 @@ export function build_setter(node, context, fallback, prefix, options) { const serialize = () => { if (left === node.left) { + if (Object.hasOwn(context.state.transformers, left.name)) { + const transformer = context.state.transformers[left.name]?.assign; + + if (transformer) { + return transformer(node, context.visit, !!options?.skip_proxy_and_freeze); + } + } + const is_initial_proxy = binding.initial !== null && should_proxy_or_freeze(/**@type {Expression}*/ (binding.initial), context.state.scope); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 208b5bedb0c0..fefc11bf1f34 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -7,15 +7,5 @@ import { build_setter } from '../utils.js'; * @param {Context} context */ export function AssignmentExpression(node, context) { - if (node.left.type === 'Identifier') { - if (Object.hasOwn(context.state.transformers, node.left.name)) { - const transformer = context.state.transformers[node.left.name]?.assign; - - if (transformer) { - return transformer(node, context.visit); - } - } - } - return build_setter(node, context, context.next); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index fa4af03a5fab..3fa935260241 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -1,6 +1,11 @@ -/** @import { Program } from 'estree' */ +/** @import { BinaryOperator, Expression, Identifier, Program } from 'estree' */ /** @import { ComponentContext } from '../types' */ -import { build_getter, is_prop_source } from '../utils.js'; +import { + build_getter, + build_proxy_reassignment, + is_prop_source, + should_proxy_or_freeze +} from '../utils.js'; import * as b from '../../../../utils/builders.js'; import { add_state_transformers } from './shared/declarations.js'; @@ -29,6 +34,28 @@ export function Program(node, context) { if (is_prop_source(binding, context.state)) { context.state.transformers[name] = { read: b.call, + assign: (node, visit) => { + let left = /** @type {Identifier} */ (node.left); + let value = /** @type {Expression} */ (visit(node.right)); + + if (node.operator !== '=') { + value = b.binary( + /** @type {BinaryOperator} */ (node.operator.slice(0, -1)), + /** @type {Expression} */ (visit(left)), + value + ); + } + + if ( + context.state.analysis.runes && + binding.kind === 'bindable_prop' && + should_proxy_or_freeze(value, context.state.scope) + ) { + value = build_proxy_reassignment(value, left.name); + } + + return b.call(left, value); + }, update: (node) => { return b.call( node.prefix ? '$.update_pre_prop' : '$.update_prop', diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js index b0884acb8d95..978f680497c8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js @@ -24,7 +24,7 @@ export function add_state_transformers(context) { ) { context.state.transformers[name] = { read: get_value, - assign: (node, visit) => { + assign: (node, visit, is_primitive) => { let left = /** @type {Identifier} */ (node.left); let value = /** @type {Expression} */ (visit(node.right)); @@ -36,7 +36,11 @@ export function add_state_transformers(context) { ); } - if (context.state.analysis.runes && should_proxy_or_freeze(value, context.state.scope)) { + if ( + !is_primitive && + context.state.analysis.runes && + should_proxy_or_freeze(value, context.state.scope) + ) { if (binding.kind === 'frozen_state') { value = b.call('$.freeze', value); } else { From 433141d56330c776ba30fe4b601a47864034c7d2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Aug 2024 21:11:39 -0400 Subject: [PATCH 24/86] tweak --- .../phases/3-transform/client/types.d.ts | 6 +--- .../phases/3-transform/client/utils.js | 25 ++++++++++++++- .../3-transform/client/visitors/Program.js | 30 ++++++------------ .../client/visitors/shared/declarations.js | 31 +++---------------- 4 files changed, 38 insertions(+), 54 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 1a640fd1f975..304abc621f2a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -31,11 +31,7 @@ export interface ClientTransformState extends TransformState { /** turn `foo` into e.g. `$.get(foo)` */ read: (id: Identifier) => Expression; /** turn `foo = bar` into e.g. `$.set(foo, bar)` */ - assign?: ( - node: AssignmentExpression, - visit: (node: SvelteNode) => SvelteNode, - is_primitive: boolean - ) => Expression; + assign?: (node: Identifier, value: Expression) => Expression; /** turn `foo++` into e.g. `$.update(foo)` */ update?: (node: UpdateExpression) => Expression; /** turn `foo.bar++` into e.g. `$.mutate(foo, $.get(foo).bar += 1)` */ 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 00fb0089e871..cc904456fc51 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -276,7 +276,30 @@ export function build_setter(node, context, fallback, prefix, options) { const transformer = context.state.transformers[left.name]?.assign; if (transformer) { - return transformer(node, context.visit, !!options?.skip_proxy_and_freeze); + let value = /** @type {Expression} */ (visit(node.right)); + + if (node.operator !== '=') { + value = b.binary( + /** @type {BinaryOperator} */ (node.operator.slice(0, -1)), + /** @type {Expression} */ (visit(left)), + value + ); + } + + if ( + !options?.skip_proxy_and_freeze && + binding.kind !== 'prop' && + context.state.analysis.runes && + should_proxy_or_freeze(value, context.state.scope) + ) { + if (binding.kind === 'frozen_state') { + value = b.call('$.freeze', value); + } else { + value = build_proxy_reassignment(value, left.name); + } + } + + return transformer(left, value); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index 3fa935260241..f3f88a9bb51f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -19,6 +19,13 @@ export function Program(node, context) { if (binding.kind === 'store_sub') { context.state.transformers[name] = { read: b.call, + assign: (node, value) => { + return b.call( + '$.store_set', + /** @type {Expression} */ (context.visit(b.id(node.name.slice(1)))), + value + ); + }, update: (node) => { return b.call( node.prefix ? '$.update_pre_store' : '$.update_store', @@ -34,27 +41,8 @@ export function Program(node, context) { if (is_prop_source(binding, context.state)) { context.state.transformers[name] = { read: b.call, - assign: (node, visit) => { - let left = /** @type {Identifier} */ (node.left); - let value = /** @type {Expression} */ (visit(node.right)); - - if (node.operator !== '=') { - value = b.binary( - /** @type {BinaryOperator} */ (node.operator.slice(0, -1)), - /** @type {Expression} */ (visit(left)), - value - ); - } - - if ( - context.state.analysis.runes && - binding.kind === 'bindable_prop' && - should_proxy_or_freeze(value, context.state.scope) - ) { - value = build_proxy_reassignment(value, left.name); - } - - return b.call(left, value); + assign: (node, value) => { + return b.call(node, value); }, update: (node) => { return b.call( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js index 978f680497c8..1c25cd6a9d80 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js @@ -24,34 +24,11 @@ export function add_state_transformers(context) { ) { context.state.transformers[name] = { read: get_value, - assign: (node, visit, is_primitive) => { - let left = /** @type {Identifier} */ (node.left); - let value = /** @type {Expression} */ (visit(node.right)); + assign: (node, value) => { + let call = b.call('$.set', node, value); - if (node.operator !== '=') { - value = b.binary( - /** @type {BinaryOperator} */ (node.operator.slice(0, -1)), - /** @type {Expression} */ (visit(left)), - value - ); - } - - if ( - !is_primitive && - context.state.analysis.runes && - should_proxy_or_freeze(value, context.state.scope) - ) { - if (binding.kind === 'frozen_state') { - value = b.call('$.freeze', value); - } else { - value = build_proxy_reassignment(value, left.name); - } - } - - let call = b.call('$.set', left, value); - - if (context.state.scope.get(`$${left.name}`)?.kind === 'store_sub') { - call = b.call('$.store_unsub', call, b.literal(`$${left.name}`), b.id('$$stores')); + if (context.state.scope.get(`$${node.name}`)?.kind === 'store_sub') { + call = b.call('$.store_unsub', call, b.literal(`$${node.name}`), b.id('$$stores')); } return call; From c4d4052c36fffa451b40982cb27a89a8637104a3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Aug 2024 21:14:18 -0400 Subject: [PATCH 25/86] remove junk --- .../phases/3-transform/client/utils.js | 53 +------------------ 1 file changed, 1 insertion(+), 52 deletions(-) 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 cc904456fc51..0477ef2e2d39 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -303,58 +303,7 @@ export function build_setter(node, context, fallback, prefix, options) { } } - const is_initial_proxy = - binding.initial !== null && - should_proxy_or_freeze(/**@type {Expression}*/ (binding.initial), context.state.scope); - if ((binding.kind === 'prop' || binding.kind === 'bindable_prop') && !is_initial_proxy) { - return b.call(left, value); - } else if (is_store) { - return b.call('$.store_set', build_getter(b.id(left_name), state), value); - } else { - let call; - if (binding.kind === 'state') { - call = b.call( - '$.set', - b.id(left_name), - context.state.analysis.runes && - !options?.skip_proxy_and_freeze && - should_proxy_or_freeze(value, context.state.scope) - ? build_proxy_reassignment(value, left_name) - : value - ); - } else if (binding.kind === 'frozen_state') { - call = b.call( - '$.set', - b.id(left_name), - context.state.analysis.runes && - !options?.skip_proxy_and_freeze && - should_proxy_or_freeze(value, context.state.scope) - ? b.call('$.freeze', value) - : value - ); - } else if ( - (binding.kind === 'prop' || binding.kind === 'bindable_prop') && - is_initial_proxy - ) { - call = b.call( - left, - context.state.analysis.runes && - !options?.skip_proxy_and_freeze && - should_proxy_or_freeze(value, context.state.scope) && - binding.kind === 'bindable_prop' - ? build_proxy_reassignment(value, left_name) - : value - ); - } else { - call = b.call('$.set', b.id(left_name), value); - } - - if (state.scope.get(`$${left.name}`)?.kind === 'store_sub') { - return b.call('$.store_unsub', call, b.literal(`$${left.name}`), b.id('$$stores')); - } else { - return call; - } - } + return node; } else { if (is_store) { // If we are assigning to a store property, we need to ensure we don't From 37be909c340f528f4afcc0703063be36c8a4d845 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Aug 2024 21:19:09 -0400 Subject: [PATCH 26/86] unused --- .../compiler/phases/3-transform/client/visitors/EachBlock.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index c82a95f5902e..faabbb52c57c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -120,8 +120,7 @@ export function EachBlock(node, context) { const child_state = { ...context.state, - transformers: { ...context.state.transformers }, - setters: { ...context.state.setters } + transformers: { ...context.state.transformers } }; /** The state used when generating the key function, if necessary */ From e5b17399a1db999283487a86362aa3240fdf0765 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Aug 2024 21:34:24 -0400 Subject: [PATCH 27/86] simplify --- .../src/compiler/phases/3-transform/client/utils.js | 6 +++++- .../3-transform/client/visitors/BindDirective.js | 12 ++---------- 2 files changed, 7 insertions(+), 11 deletions(-) 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 0477ef2e2d39..2e31baed903f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -286,8 +286,12 @@ export function build_setter(node, context, fallback, prefix, options) { ); } + // special case — if an element binding, we know it's a primitive + const path = context.path.map((node) => node.type); + const is_primitive = path.at(-1) === 'BindDirective' && path.at(-2) === 'RegularElement'; + if ( - !options?.skip_proxy_and_freeze && + !is_primitive && binding.kind !== 'prop' && context.state.analysis.runes && should_proxy_or_freeze(value, context.state.scope) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js index 8b9064d59aec..e0a40835bba7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js @@ -38,18 +38,10 @@ export function BindDirective(node, context) { } const getter = b.thunk(/** @type {Expression} */ (context.visit(expression))); - const assignment = b.assignment('=', expression, b.id('$$value')); + const setter = b.arrow( [b.id('$$value')], - build_setter( - assignment, - context, - () => /** @type {Expression} */ (context.visit(assignment)), - null, - { - skip_proxy_and_freeze: true - } - ) + /** @type {Expression} */ (context.visit(b.assignment('=', expression, b.id('$$value')))) ); /** @type {CallExpression} */ From 75d1350c155643410a57d42d38ba1cf8487b6cb6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Aug 2024 21:42:45 -0400 Subject: [PATCH 28/86] tidy up --- .../phases/3-transform/client/visitors/shared/component.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index baf9918b975b..226a9965fe19 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -197,9 +197,7 @@ export function build_component(node, component_name, context, anchor = context. const assignment = b.assignment('=', attribute.expression, b.id('$$value')); push_prop( - b.set(attribute.name, [ - b.stmt(build_setter(assignment, context, () => context.visit(assignment))) - ]) + b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]) ); } } From 361da4939c9717c0bc3de87bf76b7a7079fb73e1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Aug 2024 21:52:31 -0400 Subject: [PATCH 29/86] tweak --- .../phases/3-transform/client/utils.js | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) 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 2e31baed903f..21c35fc74ffa 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -271,40 +271,40 @@ export function build_setter(node, context, fallback, prefix, options) { const value = get_assignment_value(node, context); const serialize = () => { - if (left === node.left) { - if (Object.hasOwn(context.state.transformers, left.name)) { - const transformer = context.state.transformers[left.name]?.assign; - - if (transformer) { - let value = /** @type {Expression} */ (visit(node.right)); - - if (node.operator !== '=') { - value = b.binary( - /** @type {BinaryOperator} */ (node.operator.slice(0, -1)), - /** @type {Expression} */ (visit(left)), - value - ); - } + const transformers = Object.hasOwn(context.state.transformers, left.name) + ? context.state.transformers[left.name] + : null; - // special case — if an element binding, we know it's a primitive - const path = context.path.map((node) => node.type); - const is_primitive = path.at(-1) === 'BindDirective' && path.at(-2) === 'RegularElement'; + if (left === node.left) { + if (transformers?.assign) { + let value = /** @type {Expression} */ (visit(node.right)); + + if (node.operator !== '=') { + value = b.binary( + /** @type {BinaryOperator} */ (node.operator.slice(0, -1)), + /** @type {Expression} */ (visit(left)), + value + ); + } - if ( - !is_primitive && - binding.kind !== 'prop' && - context.state.analysis.runes && - should_proxy_or_freeze(value, context.state.scope) - ) { - if (binding.kind === 'frozen_state') { - value = b.call('$.freeze', value); - } else { - value = build_proxy_reassignment(value, left.name); - } + // special case — if an element binding, we know it's a primitive + const path = context.path.map((node) => node.type); + const is_primitive = path.at(-1) === 'BindDirective' && path.at(-2) === 'RegularElement'; + + if ( + !is_primitive && + binding.kind !== 'prop' && + context.state.analysis.runes && + should_proxy_or_freeze(value, context.state.scope) + ) { + if (binding.kind === 'frozen_state') { + value = b.call('$.freeze', value); + } else { + value = build_proxy_reassignment(value, left.name); } - - return transformer(left, value); } + + return transformers.assign(left, value); } return node; From 690d3dcb88ac732db9bd66edaef4c302b8c2b657 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 08:08:04 -0400 Subject: [PATCH 30/86] assign_property --- .../src/compiler/phases/3-transform/client/types.d.ts | 2 ++ .../src/compiler/phases/3-transform/client/utils.js | 10 ++++++++++ .../3-transform/client/visitors/shared/declarations.js | 7 +++++++ 3 files changed, 19 insertions(+) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 304abc621f2a..f910308a133d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -32,6 +32,8 @@ export interface ClientTransformState extends TransformState { read: (id: Identifier) => Expression; /** turn `foo = bar` into e.g. `$.set(foo, bar)` */ assign?: (node: Identifier, value: Expression) => Expression; + /** turn `foo.bar = baz` into e.g. `$.mutate(foo, $.get(foo).bar = baz);` */ + assign_property?: (node: Identifier, mutation: Expression) => Expression; /** turn `foo++` into e.g. `$.update(foo)` */ update?: (node: UpdateExpression) => Expression; /** turn `foo.bar++` into e.g. `$.mutate(foo, $.get(foo).bar += 1)` */ 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 21c35fc74ffa..b37dbce1e998 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -309,6 +309,16 @@ export function build_setter(node, context, fallback, prefix, options) { return node; } else { + if (transformers?.assign_property) { + const mutation = b.assignment( + node.operator, + /** @type {Pattern} */ (context.visit(node.left)), + /** @type {Expression} */ (context.visit(node.right)) + ); + + return transformers.assign_property(left, mutation); + } + if (is_store) { // If we are assigning to a store property, we need to ensure we don't // capture the read for the store as part of the member expression to diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js index 1c25cd6a9d80..ae158ba8dbca 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js @@ -33,6 +33,13 @@ export function add_state_transformers(context) { return call; }, + assign_property: (node, mutation) => { + if (context.state.analysis.runes) { + return mutation; + } + + return b.call('$.mutate', node, mutation); + }, update: (node) => { return b.call( node.prefix ? '$.update_pre' : '$.update', From 5189d8005e52fbc3eee5cdf8a98b7efd2b61840d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 08:13:30 -0400 Subject: [PATCH 31/86] fix --- packages/svelte/src/compiler/phases/3-transform/client/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b37dbce1e998..9a5cbc6ebdd4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -316,7 +316,7 @@ export function build_setter(node, context, fallback, prefix, options) { /** @type {Expression} */ (context.visit(node.right)) ); - return transformers.assign_property(left, mutation); + return maybe_skip_ownership_validation(transformers.assign_property(left, mutation)); } if (is_store) { From 1acc3c5fbe0f6a4579143f38e5407d5de9389187 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 08:24:36 -0400 Subject: [PATCH 32/86] tidy up --- .../phases/3-transform/client/utils.js | 22 ------------------- .../3-transform/client/visitors/Program.js | 8 +++++++ 2 files changed, 8 insertions(+), 22 deletions(-) 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 9a5cbc6ebdd4..a6be8929db1e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -354,28 +354,6 @@ export function build_setter(node, context, fallback, prefix, options) { b.call('$.untrack', b.id('$' + left_name)) ) ); - } else if ( - !state.analysis.runes || - // this condition can go away once legacy mode is gone; only necessary for interop with legacy parent bindings - (binding.mutated && binding.kind === 'bindable_prop') - ) { - if (binding.kind === 'bindable_prop') { - return maybe_skip_ownership_validation( - b.call( - left, - b.assignment(node.operator, /** @type {Pattern} */ (visit(node.left)), value), - b.true - ) - ); - } else { - return maybe_skip_ownership_validation( - b.call( - '$.mutate', - b.id(left_name), - b.assignment(node.operator, /** @type {Pattern} */ (visit(node.left)), value) - ) - ); - } } else if ( node.right.type === 'Literal' && prefix != null && diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index f3f88a9bb51f..21d7249ac84e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -44,6 +44,14 @@ export function Program(node, context) { assign: (node, value) => { return b.call(node, value); }, + assign_property: (node, value) => { + if (binding.kind === 'bindable_prop') { + // only necessary for interop with legacy parent bindings + return b.call(node, value, b.true); + } + + return value; + }, update: (node) => { return b.call( node.prefix ? '$.update_pre_prop' : '$.update_prop', From e9635a5d9a558bfc94759d880b630728c045bca5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 08:47:43 -0400 Subject: [PATCH 33/86] tidy up --- .../phases/3-transform/client/visitors/UpdateExpression.js | 2 +- packages/svelte/src/compiler/phases/3-transform/utils.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js index 384b9caa27d6..dd483f2ea3dd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js @@ -2,7 +2,7 @@ /** @import { Context } from '../types' */ import { is_ignored } from '../../../../state.js'; import * as b from '../../../../utils/builders.js'; -import { build_getter, build_setter } from '../utils.js'; +import { build_setter } from '../utils.js'; /** * @param {UpdateExpression} node diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js index 188ae6739f5c..38626b504fcb 100644 --- a/packages/svelte/src/compiler/phases/3-transform/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/utils.js @@ -424,7 +424,7 @@ export function transform_inspect_rune(node, context) { const { state, visit } = context; const as_fn = state.options.generate === 'client'; - if (!dev) return b.unary('void', b.literal(0)); + if (!dev) return b.empty; if (node.callee.type === 'MemberExpression') { const raw_inspect_args = /** @type {CallExpression} */ (node.callee.object).arguments; From e239d235b1bea5829848f11d4f8ca53151e177bb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 10:33:30 -0400 Subject: [PATCH 34/86] move store code --- .../phases/3-transform/client/types.d.ts | 2 +- .../phases/3-transform/client/utils.js | 37 +-------------- .../3-transform/client/visitors/Program.js | 47 +++++++++++++++---- 3 files changed, 39 insertions(+), 47 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index f910308a133d..e4d0ee23aa11 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -33,7 +33,7 @@ export interface ClientTransformState extends TransformState { /** turn `foo = bar` into e.g. `$.set(foo, bar)` */ assign?: (node: Identifier, value: Expression) => Expression; /** turn `foo.bar = baz` into e.g. `$.mutate(foo, $.get(foo).bar = baz);` */ - assign_property?: (node: Identifier, mutation: Expression) => Expression; + assign_property?: (node: Identifier, mutation: AssignmentExpression) => Expression; /** turn `foo++` into e.g. `$.update(foo)` */ update?: (node: UpdateExpression) => Expression; /** turn `foo.bar++` into e.g. `$.mutate(foo, $.get(foo).bar += 1)` */ 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 a6be8929db1e..7874dfaf4e3c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -319,42 +319,7 @@ export function build_setter(node, context, fallback, prefix, options) { return maybe_skip_ownership_validation(transformers.assign_property(left, mutation)); } - if (is_store) { - // If we are assigning to a store property, we need to ensure we don't - // capture the read for the store as part of the member expression to - // keep consistency with how store $ shorthand reads work in Svelte 4. - /** - * - * @param {Expression | Pattern} node - * @returns {Expression} - */ - function visit_node(node) { - if (node.type === 'MemberExpression') { - return { - ...node, - object: visit_node(/** @type {Expression} */ (node.object)), - property: /** @type {MemberExpression} */ (visit(node)).property - }; - } - if (node.type === 'Identifier') { - const binding = state.scope.get(node.name); - - if (binding !== null && binding.kind === 'store_sub') { - return b.call('$.untrack', b.thunk(/** @type {Expression} */ (visit(node)))); - } - } - return /** @type {Expression} */ (visit(node)); - } - - return maybe_skip_ownership_validation( - b.call( - '$.store_mutate', - build_getter(b.id(left_name), state), - b.assignment(node.operator, /** @type {Pattern}} */ (visit_node(node.left)), value), - b.call('$.untrack', b.id('$' + left_name)) - ) - ); - } else if ( + if ( node.right.type === 'Literal' && prefix != null && (node.operator === '+=' || node.operator === '-=') diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index 21d7249ac84e..fa74e8abbb4b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -1,11 +1,6 @@ -/** @import { BinaryOperator, Expression, Identifier, Program } from 'estree' */ +/** @import { Expression, MemberExpression, Program } from 'estree' */ /** @import { ComponentContext } from '../types' */ -import { - build_getter, - build_proxy_reassignment, - is_prop_source, - should_proxy_or_freeze -} from '../utils.js'; +import { build_getter, is_prop_source } from '../utils.js'; import * as b from '../../../../utils/builders.js'; import { add_state_transformers } from './shared/declarations.js'; @@ -17,13 +12,45 @@ export function Program(node, context) { if (context.state.is_instance) { for (const [name, binding] of context.state.scope.declarations) { if (binding.kind === 'store_sub') { + const store = /** @type {Expression} */ (context.visit(b.id(name.slice(1)))); + context.state.transformers[name] = { read: b.call, assign: (node, value) => { + return b.call('$.store_set', store, value); + }, + assign_property: (node, mutation) => { + // We need to untrack the store read, for consistency with Svelte 4 + const untracked = b.call('$.untrack', node); + + /** + * + * @param {Expression} n + * @returns {Expression} + */ + function replace(n) { + if (n.type === 'MemberExpression') { + return { + ...n, + object: replace(/** @type {Expression} */ (n.object)), + property: n.property + }; + } + + return untracked; + } + return b.call( - '$.store_set', - /** @type {Expression} */ (context.visit(b.id(node.name.slice(1)))), - value + '$.store_mutate', + store, + b.assignment( + mutation.operator, + /** @type {MemberExpression} */ ( + replace(/** @type {MemberExpression} */ (mutation.left)) + ), + mutation.right + ), + untracked ); }, update: (node) => { From 549a48873491b4e04776b1139ad8986dfac513c9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 10:38:46 -0400 Subject: [PATCH 35/86] this appears to be unused --- .../phases/3-transform/client/utils.js | 132 ++++++++---------- 1 file changed, 61 insertions(+), 71 deletions(-) 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 7874dfaf4e3c..455705322c1b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -268,86 +268,76 @@ export function build_setter(node, context, fallback, prefix, options) { return fallback(); } - const value = get_assignment_value(node, context); - - const serialize = () => { - const transformers = Object.hasOwn(context.state.transformers, left.name) - ? context.state.transformers[left.name] - : null; - - if (left === node.left) { - if (transformers?.assign) { - let value = /** @type {Expression} */ (visit(node.right)); - - if (node.operator !== '=') { - value = b.binary( - /** @type {BinaryOperator} */ (node.operator.slice(0, -1)), - /** @type {Expression} */ (visit(left)), - value - ); - } - - // special case — if an element binding, we know it's a primitive - const path = context.path.map((node) => node.type); - const is_primitive = path.at(-1) === 'BindDirective' && path.at(-2) === 'RegularElement'; - - if ( - !is_primitive && - binding.kind !== 'prop' && - context.state.analysis.runes && - should_proxy_or_freeze(value, context.state.scope) - ) { - if (binding.kind === 'frozen_state') { - value = b.call('$.freeze', value); - } else { - value = build_proxy_reassignment(value, left.name); - } - } - - return transformers.assign(left, value); - } - - return node; - } else { - if (transformers?.assign_property) { - const mutation = b.assignment( - node.operator, - /** @type {Pattern} */ (context.visit(node.left)), - /** @type {Expression} */ (context.visit(node.right)) + const transformers = Object.hasOwn(context.state.transformers, left.name) + ? context.state.transformers[left.name] + : null; + + if (left === node.left) { + if (transformers?.assign) { + let value = /** @type {Expression} */ (visit(node.right)); + + if (node.operator !== '=') { + value = b.binary( + /** @type {BinaryOperator} */ (node.operator.slice(0, -1)), + /** @type {Expression} */ (visit(left)), + value ); - - return maybe_skip_ownership_validation(transformers.assign_property(left, mutation)); } + // special case — if an element binding, we know it's a primitive + const path = context.path.map((node) => node.type); + const is_primitive = path.at(-1) === 'BindDirective' && path.at(-2) === 'RegularElement'; + if ( - node.right.type === 'Literal' && - prefix != null && - (node.operator === '+=' || node.operator === '-=') + !is_primitive && + binding.kind !== 'prop' && + context.state.analysis.runes && + should_proxy_or_freeze(value, context.state.scope) ) { - return maybe_skip_ownership_validation( - b.update( - node.operator === '+=' ? '++' : '--', - /** @type {Expression} */ (visit(node.left)), - prefix - ) - ); - } else { - return maybe_skip_ownership_validation( - b.assignment( - node.operator, - /** @type {Pattern} */ (visit(node.left)), - /** @type {Expression} */ (visit(node.right)) - ) - ); + if (binding.kind === 'frozen_state') { + value = b.call('$.freeze', value); + } else { + value = build_proxy_reassignment(value, left.name); + } } + + return transformers.assign(left, value); } - }; - if (value.type === 'BinaryExpression' && /** @type {any} */ (value.operator) === '??') { - return b.logical('??', build_getter(b.id(left_name), state), serialize()); - } + return node; + } else { + if (transformers?.assign_property) { + const mutation = b.assignment( + node.operator, + /** @type {Pattern} */ (context.visit(node.left)), + /** @type {Expression} */ (context.visit(node.right)) + ); + + return maybe_skip_ownership_validation(transformers.assign_property(left, mutation)); + } - return serialize(); + if ( + node.right.type === 'Literal' && + prefix != null && + (node.operator === '+=' || node.operator === '-=') + ) { + return maybe_skip_ownership_validation( + b.update( + node.operator === '+=' ? '++' : '--', + /** @type {Expression} */ (visit(node.left)), + prefix + ) + ); + } else { + return maybe_skip_ownership_validation( + b.assignment( + node.operator, + /** @type {Pattern} */ (visit(node.left)), + /** @type {Expression} */ (visit(node.right)) + ) + ); + } + } } /** From 5d1ea4cb56fc59bfd9d2226a970646dd6ac26e49 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 10:40:23 -0400 Subject: [PATCH 36/86] tidy up --- .../src/compiler/phases/3-transform/client/utils.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) 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 455705322c1b..92a910a7bc38 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -222,12 +222,7 @@ export function build_setter(node, context, fallback, prefix, options) { const left = object(assignee); - if (left === null) { - return fallback(); - } - - const binding = state.scope.get(left.name); - + const binding = left && state.scope.get(left.name); if (!binding) return fallback(); if (Object.hasOwn(state.setters, left.name)) { @@ -252,9 +247,6 @@ export function build_setter(node, context, fallback, prefix, options) { return maybe_skip_ownership_validation(fallback()); } - const is_store = binding.kind === 'store_sub'; - const left_name = is_store ? left.name.slice(1) : left.name; - if ( binding.kind !== 'state' && binding.kind !== 'frozen_state' && @@ -262,7 +254,7 @@ export function build_setter(node, context, fallback, prefix, options) { binding.kind !== 'bindable_prop' && binding.kind !== 'each' && binding.kind !== 'legacy_reactive' && - !is_store + binding.kind !== 'store_sub' ) { // TODO error if it's a computed (or rest prop)? or does that already happen elsewhere? return fallback(); From dc6879dfaf4f4a583f69177746aab3b79c4202d1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 10:47:49 -0400 Subject: [PATCH 37/86] tweak --- .../svelte/src/compiler/phases/3-transform/client/utils.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) 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 92a910a7bc38..c6bfc6792645 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -243,10 +243,6 @@ export function build_setter(node, context, fallback, prefix, options) { return serialized; } - if (binding.kind === 'derived') { - return maybe_skip_ownership_validation(fallback()); - } - if ( binding.kind !== 'state' && binding.kind !== 'frozen_state' && @@ -254,7 +250,8 @@ export function build_setter(node, context, fallback, prefix, options) { binding.kind !== 'bindable_prop' && binding.kind !== 'each' && binding.kind !== 'legacy_reactive' && - binding.kind !== 'store_sub' + binding.kind !== 'store_sub' && + binding.kind !== 'derived' ) { // TODO error if it's a computed (or rest prop)? or does that already happen elsewhere? return fallback(); From 2769d36579f782fac8fd7c04997b2e3b767c09d0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 10:50:27 -0400 Subject: [PATCH 38/86] simplify --- .../phases/3-transform/client/utils.js | 114 +++++++++--------- 1 file changed, 56 insertions(+), 58 deletions(-) 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 c6bfc6792645..54f1d8ec7aec 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -261,71 +261,69 @@ export function build_setter(node, context, fallback, prefix, options) { ? context.state.transformers[left.name] : null; - if (left === node.left) { - if (transformers?.assign) { - let value = /** @type {Expression} */ (visit(node.right)); - - if (node.operator !== '=') { - value = b.binary( - /** @type {BinaryOperator} */ (node.operator.slice(0, -1)), - /** @type {Expression} */ (visit(left)), - value - ); - } + // reassignment + if (left === node.left && transformers?.assign) { + let value = /** @type {Expression} */ (visit(node.right)); + + if (node.operator !== '=') { + value = b.binary( + /** @type {BinaryOperator} */ (node.operator.slice(0, -1)), + /** @type {Expression} */ (visit(left)), + value + ); + } - // special case — if an element binding, we know it's a primitive - const path = context.path.map((node) => node.type); - const is_primitive = path.at(-1) === 'BindDirective' && path.at(-2) === 'RegularElement'; + // special case — if an element binding, we know it's a primitive + const path = context.path.map((node) => node.type); + const is_primitive = path.at(-1) === 'BindDirective' && path.at(-2) === 'RegularElement'; - if ( - !is_primitive && - binding.kind !== 'prop' && - context.state.analysis.runes && - should_proxy_or_freeze(value, context.state.scope) - ) { - if (binding.kind === 'frozen_state') { - value = b.call('$.freeze', value); - } else { - value = build_proxy_reassignment(value, left.name); - } + if ( + !is_primitive && + binding.kind !== 'prop' && + context.state.analysis.runes && + should_proxy_or_freeze(value, context.state.scope) + ) { + if (binding.kind === 'frozen_state') { + value = b.call('$.freeze', value); + } else { + value = build_proxy_reassignment(value, left.name); } - - return transformers.assign(left, value); } - return node; - } else { - if (transformers?.assign_property) { - const mutation = b.assignment( - node.operator, - /** @type {Pattern} */ (context.visit(node.left)), - /** @type {Expression} */ (context.visit(node.right)) - ); + return transformers.assign(left, value); + } - return maybe_skip_ownership_validation(transformers.assign_property(left, mutation)); - } + // mutation + if (transformers?.assign_property) { + const mutation = b.assignment( + node.operator, + /** @type {Pattern} */ (context.visit(node.left)), + /** @type {Expression} */ (context.visit(node.right)) + ); - if ( - node.right.type === 'Literal' && - prefix != null && - (node.operator === '+=' || node.operator === '-=') - ) { - return maybe_skip_ownership_validation( - b.update( - node.operator === '+=' ? '++' : '--', - /** @type {Expression} */ (visit(node.left)), - prefix - ) - ); - } else { - return maybe_skip_ownership_validation( - b.assignment( - node.operator, - /** @type {Pattern} */ (visit(node.left)), - /** @type {Expression} */ (visit(node.right)) - ) - ); - } + return maybe_skip_ownership_validation(transformers.assign_property(left, mutation)); + } + + if ( + node.right.type === 'Literal' && + prefix != null && + (node.operator === '+=' || node.operator === '-=') + ) { + return maybe_skip_ownership_validation( + b.update( + node.operator === '+=' ? '++' : '--', + /** @type {Expression} */ (visit(node.left)), + prefix + ) + ); + } else { + return maybe_skip_ownership_validation( + b.assignment( + node.operator, + /** @type {Pattern} */ (visit(node.left)), + /** @type {Expression} */ (visit(node.right)) + ) + ); } } From 420fc63da32cc17a6e146aff555c31f55e5f68b5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 10:52:14 -0400 Subject: [PATCH 39/86] move code --- .../phases/3-transform/client/utils.js | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) 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 54f1d8ec7aec..30d70521908d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -231,18 +231,6 @@ export function build_setter(node, context, fallback, prefix, options) { return setter(node, context); } - /** - * @param {any} serialized - * @returns - */ - function maybe_skip_ownership_validation(serialized) { - if (is_ignored(node, 'ownership_invalid_mutation')) { - return b.call('$.skip_ownership_validation', b.thunk(serialized)); - } - - return serialized; - } - if ( binding.kind !== 'state' && binding.kind !== 'frozen_state' && @@ -293,6 +281,18 @@ export function build_setter(node, context, fallback, prefix, options) { return transformers.assign(left, value); } + /** + * @param {any} serialized + * @returns + */ + const maybe_skip_ownership_validation = (serialized) => { + if (is_ignored(node, 'ownership_invalid_mutation')) { + return b.call('$.skip_ownership_validation', b.thunk(serialized)); + } + + return serialized; + }; + // mutation if (transformers?.assign_property) { const mutation = b.assignment( From 2f1bcb177c67ad3da2a0223f2fced7228f7771de Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 11:01:14 -0400 Subject: [PATCH 40/86] move stuff --- .../3-transform/client/transform-client.js | 22 ++++++--------- .../phases/3-transform/client/utils.js | 27 +++++++++---------- 2 files changed, 21 insertions(+), 28 deletions(-) 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 594a7144ca26..00e26afb661c 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 @@ -179,24 +179,18 @@ export function client_component(analysis, options) { for (const [name, binding] of state.scope.declarations) { // Very very dirty way of making import statements reactive in legacy mode if needed + // TODO move this into the Program visitor if (binding.declaration_kind === 'import' && binding.mutated) { + const id = b.id('$$_import_' + name); + state.transformers[name] = { - read: (node) => b.call('$$_import_' + node.name) + read: (node) => b.call(id), + assign_property: (node, mutation) => { + return b.call(id, mutation); + } }; - state.setters[name] = (node, context) => - b.call( - '$$_import_' + binding.node.name, - b.assignment( - node.operator, - /** @type {ESTree.Pattern} */ (context.visit(node.left)), - /** @type {ESTree.Expression} */ (context.visit(node.right)) - ) - ); - - legacy_reactive_imports.push( - b.var('$$_import_' + name, b.call('$.reactive_import', b.thunk(b.id(name)))) - ); + legacy_reactive_imports.push(b.var(id, b.call('$.reactive_import', b.thunk(b.id(name))))); } } } 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 30d70521908d..024420134c5a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -231,20 +231,6 @@ export function build_setter(node, context, fallback, prefix, options) { return setter(node, context); } - if ( - binding.kind !== 'state' && - binding.kind !== 'frozen_state' && - binding.kind !== 'prop' && - binding.kind !== 'bindable_prop' && - binding.kind !== 'each' && - binding.kind !== 'legacy_reactive' && - binding.kind !== 'store_sub' && - binding.kind !== 'derived' - ) { - // TODO error if it's a computed (or rest prop)? or does that already happen elsewhere? - return fallback(); - } - const transformers = Object.hasOwn(context.state.transformers, left.name) ? context.state.transformers[left.name] : null; @@ -317,6 +303,19 @@ export function build_setter(node, context, fallback, prefix, options) { ) ); } else { + if ( + binding.kind !== 'state' && + binding.kind !== 'frozen_state' && + binding.kind !== 'prop' && + binding.kind !== 'bindable_prop' && + binding.kind !== 'each' && + binding.kind !== 'legacy_reactive' && + binding.kind !== 'store_sub' && + binding.kind !== 'derived' + ) { + // TODO error if it's a computed (or rest prop)? or does that already happen elsewhere? + return fallback(); + } return maybe_skip_ownership_validation( b.assignment( node.operator, From 2d99cf735817f7280327f4984865a27e7d300b6d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 11:19:03 -0400 Subject: [PATCH 41/86] note to self --- .../compiler/phases/3-transform/client/visitors/EachBlock.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index faabbb52c57c..54bcec9c4631 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -208,6 +208,8 @@ export function EachBlock(node, context) { ) ); + // TODO handle `item++` (currently we don't invalidate inner signals) + delete key_state.transformers[node.context.name]; } else { const unwrapped = getter(binding.node); From 7a846acb8108ab3ec2d17738d40513d899413142 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 11:37:01 -0400 Subject: [PATCH 42/86] move stuff --- .../phases/3-transform/client/visitors/EachBlock.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 54bcec9c4631..303244b815cd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -179,9 +179,6 @@ export function EachBlock(node, context) { const item_with_loc = with_loc(item, id); return b.call('$.unwrap', item_with_loc); }; - child_state.transformers[item.name] = { - read: getter - }; if (node.index) { child_state.transformers[node.index] = { @@ -200,6 +197,10 @@ export function EachBlock(node, context) { const declarations = []; if (node.context.type === 'Identifier') { + child_state.transformers[item.name] = { + read: getter + }; + child_state.setters[node.context.name] = create_mutation( b.member( each_node_meta.array_name ? b.call(each_node_meta.array_name) : collection, From c7990ef4227738a04a7940e8c1519e42bcc16577 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 12:22:34 -0400 Subject: [PATCH 43/86] each blocks --- .../phases/3-transform/client/utils.js | 12 ++++---- .../3-transform/client/visitors/EachBlock.js | 28 ++++++++++++++++++- 2 files changed, 33 insertions(+), 7 deletions(-) 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 024420134c5a..d62a94a85754 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -225,12 +225,6 @@ export function build_setter(node, context, fallback, prefix, options) { const binding = left && state.scope.get(left.name); if (!binding) return fallback(); - if (Object.hasOwn(state.setters, left.name)) { - const setter = state.setters[left.name]; - // @ts-expect-error - return setter(node, context); - } - const transformers = Object.hasOwn(context.state.transformers, left.name) ? context.state.transformers[left.name] : null; @@ -267,6 +261,12 @@ export function build_setter(node, context, fallback, prefix, options) { return transformers.assign(left, value); } + if (Object.hasOwn(state.setters, left.name)) { + const setter = state.setters[left.name]; + // @ts-expect-error + return setter(node, context); + } + /** * @param {any} serialized * @returns diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 303244b815cd..af4e5092d3ee 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -196,9 +196,35 @@ export function EachBlock(node, context) { /** @type {Statement[]} */ const declarations = []; + const invalidate = b.call( + '$.invalidate_inner_signals', + b.thunk(b.sequence(indirect_dependencies)) + ); + + const invalidate_store = store_to_invalidate + ? b.call('$.invalidate_store', b.id('$$stores'), b.literal(store_to_invalidate)) + : undefined; + + /** @type {Expression[]} */ + const sequence = []; + if (!context.state.analysis.runes) sequence.push(invalidate); + if (invalidate_store) sequence.push(invalidate_store); + if (node.context.type === 'Identifier') { child_state.transformers[item.name] = { - read: getter + read: getter, + assign: (node, value) => { + const left = b.member( + each_node_meta.array_name ? b.call(each_node_meta.array_name) : collection, + index, + true + ); + + return b.sequence([b.assignment('=', left, value), ...sequence]); + }, + assign_property: (node, mutation) => { + return b.sequence([mutation, ...sequence]); + } }; child_state.setters[node.context.name] = create_mutation( From cdd2f24b821f6fe58ce4e74dda38fab28ac13bdf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 12:55:00 -0400 Subject: [PATCH 44/86] note to self --- .../compiler/phases/3-transform/client/visitors/EachBlock.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index af4e5092d3ee..de8c772ab499 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -225,6 +225,7 @@ export function EachBlock(node, context) { assign_property: (node, mutation) => { return b.sequence([mutation, ...sequence]); } + // TODO update (`item++`) and update_property — these were unaccounted for previously }; child_state.setters[node.context.name] = create_mutation( @@ -235,8 +236,6 @@ export function EachBlock(node, context) { ) ); - // TODO handle `item++` (currently we don't invalidate inner signals) - delete key_state.transformers[node.context.name]; } else { const unwrapped = getter(binding.node); From c7060fd878a57846ea2aab9499e9e787122a88d4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 13:31:24 -0400 Subject: [PATCH 45/86] lengthen stack trace --- .vscode/launch.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 41d8017ce29f..fc593c0d0653 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,7 +21,10 @@ "type": "node", "request": "launch", "name": "Run sandbox", - "program": "${workspaceFolder}/playgrounds/sandbox/run.js" + "program": "${workspaceFolder}/playgrounds/sandbox/run.js", + "env": { + "NODE_OPTIONS": "--stack-trace-limit=10000" + } } ], "compounds": [ From fc7fdb664aafb7fbfe342bd9e2d13a0b496ff55a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 14:15:10 -0400 Subject: [PATCH 46/86] tweak --- .../compiler/phases/3-transform/client/visitors/EachBlock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index de8c772ab499..964858247556 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -211,7 +211,7 @@ export function EachBlock(node, context) { if (invalidate_store) sequence.push(invalidate_store); if (node.context.type === 'Identifier') { - child_state.transformers[item.name] = { + child_state.transformers[node.context.name] = { read: getter, assign: (node, value) => { const left = b.member( From ace6134fa7658ff4406edaa6a4045862b0535bd3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 14:33:01 -0400 Subject: [PATCH 47/86] more --- .../phases/3-transform/client/visitors/EachBlock.js | 8 -------- .../phases/3-transform/client/visitors/shared/utils.js | 9 ++++++++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 964858247556..09afa0a2a1fe 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -228,14 +228,6 @@ export function EachBlock(node, context) { // TODO update (`item++`) and update_property — these were unaccounted for previously }; - child_state.setters[node.context.name] = create_mutation( - b.member( - each_node_meta.array_name ? b.call(each_node_meta.array_name) : collection, - index, - true - ) - ); - delete key_state.transformers[node.context.name]; } else { const unwrapped = getter(binding.node); 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 4f865e387efc..8adea214e67f 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 @@ -169,7 +169,14 @@ export function build_bind_this(expression, value, { state, visit }) { if (owner.type === 'EachBlock' && scope === binding.scope) { ids.push(node); values.push(/** @type {Expression} */ (visit(node))); - delete transformers[node.name]; + + if (transformers[node.name]) { + transformers[node.name] = { + ...transformers[node.name], + read: (node) => node + }; + } + break; } } From 81544e02873d23ab7744ab0aa32f7a15c3cfd890 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 14:40:39 -0400 Subject: [PATCH 48/86] tidy up --- .../3-transform/client/transform-client.js | 2 - .../phases/3-transform/client/types.d.ts | 7 --- .../phases/3-transform/client/utils.js | 6 -- .../3-transform/client/visitors/EachBlock.js | 56 ++++--------------- 4 files changed, 12 insertions(+), 59 deletions(-) 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 00e26afb661c..2fe79569401f 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 @@ -157,7 +157,6 @@ export function client_component(analysis, options) { public_state: new Map(), private_state: new Map(), transformers: {}, - setters: {}, in_constructor: false, // these are set inside the `Fragment` visitor, and cannot be used until then @@ -668,7 +667,6 @@ export function client_module(analysis, options) { public_state: new Map(), private_state: new Map(), transformers: {}, - setters: {}, in_constructor: false }; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index e4d0ee23aa11..c5e5da97a807 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -40,13 +40,6 @@ export interface ClientTransformState extends TransformState { update_property?: (id: Identifier) => Expression; } >; - /** - * Counterpart to `getters` - */ - readonly setters: Record< - string, - (assignment: AssignmentExpression, context: Context) => Expression - >; } export interface ComponentClientTransformState extends ClientTransformState { 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 d62a94a85754..efd895f3524e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -261,12 +261,6 @@ export function build_setter(node, context, fallback, prefix, options) { return transformers.assign(left, value); } - if (Object.hasOwn(state.setters, left.name)) { - const setter = state.setters[left.name]; - // @ts-expect-error - return setter(node, context); - } - /** * @param {any} serialized * @returns diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 09afa0a2a1fe..c65fac786156 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -129,46 +129,6 @@ export function EachBlock(node, context) { transformers: { ...context.state.transformers } }; - /** - * @param {Pattern} expression_for_id - * @returns {(assignment: AssignmentExpression, context: Context) => Expression} - */ - const create_mutation = (expression_for_id) => { - return (assignment, context) => { - if (assignment.left.type !== 'Identifier' && assignment.left.type !== 'MemberExpression') { - // build_setter turns other patterns into IIFEs and separates the assignments - // into separate expressions, at which point this is called again with an identifier or member expression - return build_setter(assignment, context, () => assignment); - } - - const left = object(assignment.left); - const value = get_assignment_value(assignment, context); - const invalidate = b.call( - '$.invalidate_inner_signals', - b.thunk(b.sequence(indirect_dependencies)) - ); - const invalidate_store = store_to_invalidate - ? b.call('$.invalidate_store', b.id('$$stores'), b.literal(store_to_invalidate)) - : undefined; - - const sequence = []; - if (!context.state.analysis.runes) sequence.push(invalidate); - if (invalidate_store) sequence.push(invalidate_store); - - if (left === assignment.left) { - const assign = b.assignment('=', expression_for_id, value); - sequence.unshift(assign); - return b.sequence(sequence); - } else { - const original_left = /** @type {MemberExpression} */ (assignment.left); - const left = /** @type {Pattern} */ (context.visit(original_left)); - const assign = b.assignment(assignment.operator, left, value); - sequence.unshift(assign); - return b.sequence(sequence); - } - }; - }; - // We need to generate a unique identifier in case there's a bind:group below // which needs a reference to the index const index = @@ -243,10 +203,18 @@ export function EachBlock(node, context) { declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn)); const read = needs_derived ? get_value : b.call; - child_state.transformers[name] = { read }; - child_state.setters[name] = create_mutation( - /** @type {Pattern} */ (path.update_expression(unwrapped)) - ); + + child_state.transformers[name] = { + read, + assign: (node, value) => { + const left = /** @type {Pattern} */ (path.update_expression(unwrapped)); + return b.sequence([b.assignment('=', left, value), ...sequence]); + }, + assign_property: (node, mutation) => { + return b.sequence([mutation, ...sequence]); + } + // TODO update (`item++`) and update_property — these were unaccounted for previously + }; // we need to eagerly evaluate the expression in order to hit any // 'Cannot access x before initialization' errors From e29bee5632f9132c9c6de02ae6bb372cceec0031 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 14:42:05 -0400 Subject: [PATCH 49/86] tidy up --- .../phases/3-transform/client/visitors/BindDirective.js | 1 - .../phases/3-transform/client/visitors/EachBlock.js | 6 +++--- .../phases/3-transform/client/visitors/shared/component.js | 5 ++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js index e0a40835bba7..81bafcc18a09 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js @@ -5,7 +5,6 @@ import { dev, is_ignored } from '../../../../state.js'; import { is_text_attribute } from '../../../../utils/ast.js'; import * as b from '../../../../utils/builders.js'; import { binding_properties } from '../../../bindings.js'; -import { build_setter } from '../utils.js'; import { build_attribute_value } from './shared/element.js'; import { build_bind_this, validate_binding } from './shared/utils.js'; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index c65fac786156..e5a2620fb43b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -1,6 +1,6 @@ -/** @import { AssignmentExpression, BlockStatement, Expression, Identifier, MemberExpression, Pattern, Statement } from 'estree' */ +/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */ /** @import { Binding, EachBlock } from '#compiler' */ -/** @import { ComponentContext, Context } from '../types' */ +/** @import { ComponentContext } from '../types' */ /** @import { Scope } from '../../../scope' */ import { EACH_INDEX_REACTIVE, @@ -13,7 +13,7 @@ import { import { dev } from '../../../../state.js'; import { extract_paths, object } from '../../../../utils/ast.js'; import * as b from '../../../../utils/builders.js'; -import { get_assignment_value, build_getter, build_setter, with_loc } from '../utils.js'; +import { build_getter, with_loc } from '../utils.js'; import { get_value } from './shared/declarations.js'; /** diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 226a9965fe19..5eb3367ad591 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -1,11 +1,10 @@ /** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Property, Statement } from 'estree' */ -/** @import { Attribute, Component, SvelteComponent, SvelteSelf, TemplateNode, Text } from '#compiler' */ +/** @import { Component, SvelteComponent, SvelteSelf, TemplateNode } from '#compiler' */ /** @import { ComponentContext } from '../../types.js' */ import { dev, is_ignored } from '../../../../../state.js'; import { get_attribute_chunks } from '../../../../../utils/ast.js'; import * as b from '../../../../../utils/builders.js'; -import { is_element_node } from '../../../../nodes.js'; -import { create_derived, build_setter } from '../../utils.js'; +import { create_derived } from '../../utils.js'; import { build_bind_this, validate_binding } from '../shared/utils.js'; import { build_attribute_value } from '../shared/element.js'; import { build_event_handler } from './events.js'; From c649da8ab263069c8750e0105ab626f51a45e612 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 14:44:42 -0400 Subject: [PATCH 50/86] remove some junk --- .../compiler/phases/3-transform/client/utils.js | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) 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 efd895f3524e..d0204d321daa 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -92,10 +92,9 @@ export function build_getter(node, state) { * @param {import('zimmerframe').Context} context * @param {() => any} fallback * @param {boolean | null} [prefix] - If the assignment is a transformed update expression, set this. Else `null` - * @param {{skip_proxy_and_freeze?: boolean}} [options] * @returns {Expression} */ -export function build_setter(node, context, fallback, prefix, options) { +export function build_setter(node, context, fallback, prefix) { const { state, visit } = context; const assignee = node.left; @@ -119,7 +118,7 @@ export function build_setter(node, context, fallback, prefix, options) { const value = path.expression?.(b.id(tmp_id)); const assignment = b.assignment('=', path.node, value); original_assignments.push(assignment); - assignments.push(build_setter(assignment, context, () => assignment, prefix, options)); + assignments.push(build_setter(assignment, context, () => assignment, prefix)); } if (assignments.every((assignment, i) => assignment === original_assignments[i])) { @@ -166,11 +165,7 @@ export function build_setter(node, context, fallback, prefix, options) { if (private_state !== undefined) { if (state.in_constructor) { // See if we should wrap value in $.proxy - if ( - context.state.analysis.runes && - !options?.skip_proxy_and_freeze && - should_proxy_or_freeze(value, context.state.scope) - ) { + if (context.state.analysis.runes && should_proxy_or_freeze(value, context.state.scope)) { const assignment = fallback(); if (assignment.type === 'AssignmentExpression') { assignment.right = @@ -184,9 +179,7 @@ export function build_setter(node, context, fallback, prefix, options) { return b.call( '$.set', assignee, - context.state.analysis.runes && - !options?.skip_proxy_and_freeze && - should_proxy_or_freeze(value, context.state.scope) + context.state.analysis.runes && should_proxy_or_freeze(value, context.state.scope) ? private_state.kind === 'frozen_state' ? b.call('$.freeze', value) : build_proxy_reassignment(value, private_state.id) @@ -205,7 +198,6 @@ export function build_setter(node, context, fallback, prefix, options) { if ( context.state.analysis.runes && public_state !== undefined && - !options?.skip_proxy_and_freeze && should_proxy_or_freeze(value, context.state.scope) ) { const assignment = fallback(); From 3d6afdf3c9a1459095eedebd8e887c140a18189b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 15:27:09 -0400 Subject: [PATCH 51/86] tidy up --- .../3-transform/client/transform-client.js | 20 ++--- .../phases/3-transform/client/types.d.ts | 6 +- .../phases/3-transform/client/utils.js | 22 +++--- .../3-transform/client/visitors/ConstTag.js | 10 +-- .../3-transform/client/visitors/EachBlock.js | 24 +++--- .../3-transform/client/visitors/Fragment.js | 2 +- .../client/visitors/LetDirective.js | 4 +- .../3-transform/client/visitors/Program.js | 12 +-- .../client/visitors/SnippetBlock.js | 10 +-- .../client/visitors/UpdateExpression.js | 74 ++++++++----------- .../client/visitors/shared/component.js | 4 +- .../client/visitors/shared/declarations.js | 4 +- .../client/visitors/shared/utils.js | 10 +-- 13 files changed, 94 insertions(+), 108 deletions(-) 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 2fe79569401f..6aa45ad40f00 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 @@ -63,16 +63,16 @@ const visitors = { const scope = state.scopes.get(node); if (scope && scope !== state.scope) { - const transformers = { ...state.transformers }; + const transform = { ...state.transform }; // TODO do this in add_state_transformers and visit_function etc? for (const [name, binding] of scope.declarations) { if (binding.kind === 'normal') { - delete transformers[name]; + delete transform[name]; } } - next({ ...state, transformers, scope }); + next({ ...state, transform, scope }); } else { next(); } @@ -156,7 +156,7 @@ export function client_component(analysis, options) { preserve_whitespace: options.preserveWhitespace, public_state: new Map(), private_state: new Map(), - transformers: {}, + transform: {}, in_constructor: false, // these are set inside the `Fragment` visitor, and cannot be used until then @@ -172,7 +172,7 @@ export function client_component(analysis, options) { const legacy_reactive_imports = []; if (!analysis.runes) { - state.transformers['$$props'] = { + state.transform['$$props'] = { read: (node) => ({ ...node, name: '$$sanitized_props' }) }; @@ -182,9 +182,9 @@ export function client_component(analysis, options) { if (binding.declaration_kind === 'import' && binding.mutated) { const id = b.id('$$_import_' + name); - state.transformers[name] = { + state.transform[name] = { read: (node) => b.call(id), - assign_property: (node, mutation) => { + mutate: (node, mutation) => { return b.call(id, mutation); } }; @@ -200,7 +200,7 @@ export function client_component(analysis, options) { const instance_state = { ...state, - transformers: { ...state.transformers }, + transform: { ...state.transform }, scope: analysis.instance.scope, scopes: analysis.instance.scopes, is_instance: true @@ -215,7 +215,7 @@ export function client_component(analysis, options) { /** @type {SvelteNode} */ (analysis.template.ast), { ...state, - transformers: instance_state.transformers, + transform: instance_state.transform, scope: analysis.instance.scope, scopes: analysis.template.scopes }, @@ -666,7 +666,7 @@ export function client_module(analysis, options) { legacy_reactive_statements: new Map(), public_state: new Map(), private_state: new Map(), - transformers: {}, + transform: {}, in_constructor: false }; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index c5e5da97a807..b576ddeecaed 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -25,7 +25,7 @@ export interface ClientTransformState extends TransformState { /** The $: calls, which will be ordered in the end */ readonly legacy_reactive_statements: Map; - readonly transformers: Record< + readonly transform: Record< string, { /** turn `foo` into e.g. `$.get(foo)` */ @@ -33,11 +33,9 @@ export interface ClientTransformState extends TransformState { /** turn `foo = bar` into e.g. `$.set(foo, bar)` */ assign?: (node: Identifier, value: Expression) => Expression; /** turn `foo.bar = baz` into e.g. `$.mutate(foo, $.get(foo).bar = baz);` */ - assign_property?: (node: Identifier, mutation: AssignmentExpression) => Expression; + mutate?: (node: Identifier, mutation: AssignmentExpression) => Expression; /** turn `foo++` into e.g. `$.update(foo)` */ update?: (node: UpdateExpression) => Expression; - /** turn `foo.bar++` into e.g. `$.mutate(foo, $.get(foo).bar += 1)` */ - update_property?: (id: Identifier) => Expression; } >; } 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 d0204d321daa..487f75057a55 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -74,12 +74,12 @@ export function is_state_source(binding, state) { * @returns {Expression} */ export function build_getter(node, state) { - if (Object.hasOwn(state.transformers, node.name)) { + if (Object.hasOwn(state.transform, node.name)) { const binding = state.scope.get(node.name); // don't transform the declaration itself if (node !== binding?.node) { - return state.transformers[node.name].read(node); + return state.transform[node.name].read(node); } } @@ -217,12 +217,12 @@ export function build_setter(node, context, fallback, prefix) { const binding = left && state.scope.get(left.name); if (!binding) return fallback(); - const transformers = Object.hasOwn(context.state.transformers, left.name) - ? context.state.transformers[left.name] + const transform = Object.hasOwn(context.state.transform, left.name) + ? context.state.transform[left.name] : null; // reassignment - if (left === node.left && transformers?.assign) { + if (left === node.left && transform?.assign) { let value = /** @type {Expression} */ (visit(node.right)); if (node.operator !== '=') { @@ -250,7 +250,7 @@ export function build_setter(node, context, fallback, prefix) { } } - return transformers.assign(left, value); + return transform.assign(left, value); } /** @@ -266,14 +266,14 @@ export function build_setter(node, context, fallback, prefix) { }; // mutation - if (transformers?.assign_property) { + if (transform?.mutate) { const mutation = b.assignment( node.operator, /** @type {Pattern} */ (context.visit(node.left)), /** @type {Expression} */ (context.visit(node.right)) ); - return maybe_skip_ownership_validation(transformers.assign_property(left, mutation)); + return maybe_skip_ownership_validation(transform.mutate(left, mutation)); } if ( @@ -360,7 +360,7 @@ function get_hoisted_params(node, context) { binding = /** @type {Binding} */ (scope.get(binding.node.name.slice(1))); } - let expression = context.state.transformers[reference]?.read(b.id(binding.node.name)); + let expression = context.state.transform[reference]?.read(b.id(binding.node.name)); if ( // If it's a destructured derived binding, then we can extract the derived signal reference and use that. @@ -559,7 +559,7 @@ export function with_loc(target, source) { */ export function create_derived_block_argument(node, context) { if (node.type === 'Identifier') { - context.state.transformers[node.name] = { + context.state.transform[node.name] = { read: get_value }; @@ -580,7 +580,7 @@ export function create_derived_block_argument(node, context) { const declarations = [b.var(value, create_derived(context.state, b.thunk(block)))]; for (const id of identifiers) { - context.state.transformers[id.name] = { read: get_value }; + context.state.transform[id.name] = { read: get_value }; declarations.push( b.var(id, create_derived(context.state, b.thunk(b.member(b.call('$.get', value), id)))) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index 8d8a8beb0f1b..1a7b2904d920 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -25,7 +25,7 @@ export function ConstTag(node, context) { ) ); - context.state.transformers[declaration.id.name] = { + context.state.transform[declaration.id.name] = { read: get_value }; @@ -38,15 +38,15 @@ export function ConstTag(node, context) { const identifiers = extract_identifiers(declaration.id); const tmp = b.id(context.state.scope.generate('computed_const')); - const transformers = { ...context.state.transformers }; + const transform = { ...context.state.transform }; // Make all identifiers that are declared within the following computed regular // variables, as they are not signals in that context yet for (const node of identifiers) { - delete transformers[node.name]; + delete transform[node.name]; } - const child_state = { ...context.state, transformers }; + const child_state = { ...context.state, transform }; // TODO optimise the simple `{ x } = y` case — we can just return `y` // instead of destructuring it only to return a new object @@ -70,7 +70,7 @@ export function ConstTag(node, context) { } for (const node of identifiers) { - context.state.transformers[node.name] = { + context.state.transform[node.name] = { read: (node) => b.member(b.call('$.get', tmp), node) }; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index e5a2620fb43b..8491850de896 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -120,13 +120,13 @@ export function EachBlock(node, context) { const child_state = { ...context.state, - transformers: { ...context.state.transformers } + transform: { ...context.state.transform } }; /** The state used when generating the key function, if necessary */ const key_state = { ...context.state, - transformers: { ...context.state.transformers } + transform: { ...context.state.transform } }; // We need to generate a unique identifier in case there's a bind:group below @@ -141,7 +141,7 @@ export function EachBlock(node, context) { }; if (node.index) { - child_state.transformers[node.index] = { + child_state.transform[node.index] = { read: (id) => { const index_with_loc = with_loc(index, id); return (flags & EACH_INDEX_REACTIVE) === 0 @@ -150,7 +150,7 @@ export function EachBlock(node, context) { } }; - delete key_state.transformers[node.index]; + delete key_state.transform[node.index]; } /** @type {Statement[]} */ @@ -171,7 +171,7 @@ export function EachBlock(node, context) { if (invalidate_store) sequence.push(invalidate_store); if (node.context.type === 'Identifier') { - child_state.transformers[node.context.name] = { + child_state.transform[node.context.name] = { read: getter, assign: (node, value) => { const left = b.member( @@ -182,13 +182,13 @@ export function EachBlock(node, context) { return b.sequence([b.assignment('=', left, value), ...sequence]); }, - assign_property: (node, mutation) => { + mutate: (node, mutation) => { return b.sequence([mutation, ...sequence]); } - // TODO update (`item++`) and update_property — these were unaccounted for previously + // TODO update (`item++`) — this was unaccounted for previously }; - delete key_state.transformers[node.context.name]; + delete key_state.transform[node.context.name]; } else { const unwrapped = getter(binding.node); const paths = extract_paths(node.context); @@ -204,16 +204,16 @@ export function EachBlock(node, context) { const read = needs_derived ? get_value : b.call; - child_state.transformers[name] = { + child_state.transform[name] = { read, assign: (node, value) => { const left = /** @type {Pattern} */ (path.update_expression(unwrapped)); return b.sequence([b.assignment('=', left, value), ...sequence]); }, - assign_property: (node, mutation) => { + mutate: (node, mutation) => { return b.sequence([mutation, ...sequence]); } - // TODO update (`item++`) and update_property — these were unaccounted for previously + // TODO update (`item++`) — this was unaccounted for previously }; // we need to eagerly evaluate the expression in order to hit any @@ -222,7 +222,7 @@ export function EachBlock(node, context) { declarations.push(b.stmt(read(b.id(name)))); } - key_state.transformers[name] = { + key_state.transform[name] = { read: () => path.node }; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index a842ba07ee52..e7312d15aaf3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -66,7 +66,7 @@ export function Fragment(node, context) { after_update: [], template: [], locations: [], - transformers: { ...context.state.transformers }, + transform: { ...context.state.transform }, metadata: { context: { template_needs_import_node: false, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js index e4d6d06acf6f..c455ea32193e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js @@ -16,7 +16,7 @@ export function LetDirective(node, context) { const bindings = context.state.scope.get_bindings(node); for (const binding of bindings) { - context.state.transformers[binding.node.name] = { + context.state.transform[binding.node.name] = { read: (node) => b.member(b.call('$.get', b.id(name)), node) }; } @@ -42,7 +42,7 @@ export function LetDirective(node, context) { ); } else { const name = node.expression === null ? node.name : node.expression.name; - context.state.transformers[name] = { + context.state.transform[name] = { read: (node) => b.call('$.get', node) }; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index fa74e8abbb4b..07fb459f3eb5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -14,12 +14,12 @@ export function Program(node, context) { if (binding.kind === 'store_sub') { const store = /** @type {Expression} */ (context.visit(b.id(name.slice(1)))); - context.state.transformers[name] = { + context.state.transform[name] = { read: b.call, assign: (node, value) => { return b.call('$.store_set', store, value); }, - assign_property: (node, mutation) => { + mutate: (node, mutation) => { // We need to untrack the store read, for consistency with Svelte 4 const untracked = b.call('$.untrack', node); @@ -66,12 +66,12 @@ export function Program(node, context) { if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { if (is_prop_source(binding, context.state)) { - context.state.transformers[name] = { + context.state.transform[name] = { read: b.call, assign: (node, value) => { return b.call(node, value); }, - assign_property: (node, value) => { + mutate: (node, value) => { if (binding.kind === 'bindable_prop') { // only necessary for interop with legacy parent bindings return b.call(node, value, b.true); @@ -89,11 +89,11 @@ export function Program(node, context) { }; } else if (binding.prop_alias) { const key = b.key(binding.prop_alias); - context.state.transformers[name] = { + context.state.transform[name] = { read: (node) => b.member(b.id('$$props'), key, key.type === 'Literal') }; } else { - context.state.transformers[name] = { + context.state.transform[name] = { read: (node) => b.member(b.id('$$props'), node) }; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js index d0ac1641be97..750b1b6a3b0d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js @@ -21,8 +21,8 @@ export function SnippetBlock(node, context) { /** @type {Statement[]} */ const declarations = []; - const transformers = { ...context.state.transformers }; - const child_state = { ...context.state, transformers }; + const transform = { ...context.state.transform }; + const child_state = { ...context.state, transform }; for (let i = 0; i < node.parameters.length; i++) { const argument = node.parameters[i]; @@ -36,7 +36,7 @@ export function SnippetBlock(node, context) { right: b.id('$.noop') }); - transformers[argument.name] = { + transform[argument.name] = { read: b.call }; @@ -57,14 +57,14 @@ export function SnippetBlock(node, context) { declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn)); - transformers[name] = { + transform[name] = { read: needs_derived ? get_value : b.call }; // we need to eagerly evaluate the expression in order to hit any // 'Cannot access x before initialization' errors if (dev) { - declarations.push(b.stmt(transformers[name].read(b.id(name)))); + declarations.push(b.stmt(transform[name].read(b.id(name)))); } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js index dd483f2ea3dd..91383a56793c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js @@ -1,8 +1,8 @@ -/** @import { Expression, Pattern, Statement, UpdateExpression } from 'estree' */ +/** @import { Expression, Node, Pattern, Statement, UpdateExpression } from 'estree' */ /** @import { Context } from '../types' */ import { is_ignored } from '../../../../state.js'; +import { object } from '../../../../utils/ast.js'; import * as b from '../../../../utils/builders.js'; -import { build_setter } from '../utils.js'; /** * @param {UpdateExpression} node @@ -10,16 +10,6 @@ import { build_setter } from '../utils.js'; */ export function UpdateExpression(node, context) { const argument = node.argument; - const transformers = context.state.transformers; - - if (argument.type === 'Identifier') { - if (Object.hasOwn(transformers, argument.name) && transformers[argument.name].update) { - const transformer = transformers[argument.name].update; - if (transformer) return transformer(node); - } - - return context.next(); - } if ( argument.type === 'MemberExpression' && @@ -39,43 +29,41 @@ export function UpdateExpression(node, context) { return b.call(fn, ...args); } - /** @param {any} serialized */ - function maybe_skip_ownership_validation(serialized) { - if (is_ignored(node, 'ownership_invalid_mutation')) { - return b.call('$.skip_ownership_validation', b.thunk(serialized)); - } - - return serialized; + if (argument.type !== 'Identifier' && argument.type !== 'MemberExpression') { + throw new Error('An impossible state was reached'); } - // turn it into an IIFE assignment expression: i++ -> (() => { const $$value = i; i+=1; return $$value; }) - const assignment = b.assignment( - node.operator === '++' ? '+=' : '-=', - /** @type {Pattern} */ (argument), - b.literal(1) - ); + const left = object(argument); + if (left === null) return context.next(); - const serialized_assignment = build_setter(assignment, context, () => assignment, node.prefix); + if (left === argument) { + const transform = context.state.transform; + const update = transform[left.name]?.update; - const value = /** @type {Expression} */ (context.visit(argument)); - - if (serialized_assignment === assignment) { - // No change to output -> nothing to transform -> we can keep the original update expression - return maybe_skip_ownership_validation(context.next()); + if (update && Object.hasOwn(transform, left.name)) { + return update(node); + } } - if (context.state.analysis.runes) { - return maybe_skip_ownership_validation(serialized_assignment); - } + const assignment = /** @type {Expression} */ ( + context.visit( + b.assignment( + node.operator === '++' ? '+=' : '-=', + /** @type {Pattern} */ (argument), + b.literal(1) + ) + ) + ); - /** @type {Statement[]} */ - let statements; - if (node.prefix) { - statements = [b.stmt(serialized_assignment), b.return(value)]; - } else { - const tmp_id = context.state.scope.generate('$$value'); - statements = [b.const(tmp_id, value), b.stmt(serialized_assignment), b.return(b.id(tmp_id))]; - } + const parent = /** @type {Node} */ (context.path.at(-1)); + const is_standalone = parent.type === 'ExpressionStatement'; // TODO and possibly others, but not e.g. the `test` of a WhileStatement + + const update = + node.prefix || is_standalone + ? assignment + : b.binary(node.operator === '++' ? '-' : '+', assignment, b.literal(1)); - return maybe_skip_ownership_validation(b.call(b.thunk(b.block(statements)))); + return is_ignored(node, 'ownership_invalid_mutation') + ? b.call('$.skip_ownership_validation', b.thunk(update)) + : update; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 5eb3367ad591..aaf2b024bd74 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -29,7 +29,7 @@ export function build_component(node, component_name, context, anchor = context. default: { ...context.state, scope: node.metadata.scopes.default, - transformers: { ...context.state.transformers } + transform: { ...context.state.transform } } }; @@ -257,7 +257,7 @@ export function build_component(node, component_name, context, anchor = context. : { ...context.state, scope: node.metadata.scopes[slot_name], - transformers: { ...context.state.transformers } + transform: { ...context.state.transform } } // { // ...context.state, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js index ae158ba8dbca..5a03fd9be4d9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js @@ -22,7 +22,7 @@ export function add_state_transformers(context) { binding.kind === 'derived' || binding.kind === 'legacy_reactive' ) { - context.state.transformers[name] = { + context.state.transform[name] = { read: get_value, assign: (node, value) => { let call = b.call('$.set', node, value); @@ -33,7 +33,7 @@ export function add_state_transformers(context) { return call; }, - assign_property: (node, mutation) => { + mutate: (node, mutation) => { if (context.state.analysis.runes) { return mutation; } 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 8adea214e67f..b11c2ed22407 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 @@ -148,7 +148,7 @@ export function build_bind_this(expression, value, { state, visit }) { /** @type {string[]} */ const seen = []; - const transformers = { ...state.transformers }; + const transform = { ...state.transform }; // Pass in each context variables to the get/set functions, so that we can null out old values on teardown. // Note that we only do this for each context variables, the consequence is that the value might be stale in @@ -170,9 +170,9 @@ export function build_bind_this(expression, value, { state, visit }) { ids.push(node); values.push(/** @type {Expression} */ (visit(node))); - if (transformers[node.name]) { - transformers[node.name] = { - ...transformers[node.name], + if (transform[node.name]) { + transform[node.name] = { + ...transform[node.name], read: (node) => node }; } @@ -183,7 +183,7 @@ export function build_bind_this(expression, value, { state, visit }) { } }); - const child_state = { ...state, transformers }; + const child_state = { ...state, transform }; const get = /** @type {Expression} */ (visit(expression, child_state)); const set = /** @type {Expression} */ ( From 84cc1e7599cb0230dade6885d727f9a59989532c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 15:30:38 -0400 Subject: [PATCH 52/86] move stuff --- .../phases/3-transform/client/utils.js | 275 +----------------- .../client/visitors/AssignmentExpression.js | 273 ++++++++++++++++- 2 files changed, 273 insertions(+), 275 deletions(-) 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 487f75057a55..ce9825ad018f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -1,61 +1,18 @@ -/** @import { ArrowFunctionExpression, AssignmentExpression, BinaryOperator, Expression, FunctionDeclaration, FunctionExpression, Identifier, MemberExpression, Node, Pattern, PrivateIdentifier, Statement } from 'estree' */ +/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */ /** @import { Binding, SvelteNode } from '#compiler' */ /** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */ /** @import { Scope } from '../../scope.js' */ import * as b from '../../../utils/builders.js'; -import { - extract_identifiers, - extract_paths, - is_expression_async, - is_simple_expression, - object -} from '../../../utils/ast.js'; +import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js'; import { PROPS_IS_LAZY_INITIAL, PROPS_IS_IMMUTABLE, PROPS_IS_RUNES, PROPS_IS_UPDATED } from '../../../../constants.js'; -import { is_ignored, dev } from '../../../state.js'; +import { dev } from '../../../state.js'; import { get_value } from './visitors/shared/declarations.js'; -/** - * @template {ClientTransformState} State - * @param {AssignmentExpression} node - * @param {import('zimmerframe').Context} context - * @returns - */ -export function get_assignment_value(node, { state, visit }) { - if (node.left.type === 'Identifier') { - const operator = node.operator; - return operator === '=' - ? /** @type {Expression} */ (visit(node.right)) - : // turn something like x += 1 into x = x + 1 - b.binary( - /** @type {BinaryOperator} */ (operator.slice(0, -1)), - build_getter(node.left, state), - /** @type {Expression} */ (visit(node.right)) - ); - } else if ( - node.left.type === 'MemberExpression' && - node.left.object.type === 'ThisExpression' && - node.left.property.type === 'PrivateIdentifier' && - state.private_state.has(node.left.property.name) - ) { - const operator = node.operator; - return operator === '=' - ? /** @type {Expression} */ (visit(node.right)) - : // turn something like x += 1 into x = x + 1 - b.binary( - /** @type {BinaryOperator} */ (operator.slice(0, -1)), - /** @type {Expression} */ (visit(node.left)), - /** @type {Expression} */ (visit(node.right)) - ); - } else { - return /** @type {Expression} */ (visit(node.right)); - } -} - /** * @param {Binding} binding * @param {ClientTransformState} state @@ -86,232 +43,6 @@ export function build_getter(node, state) { return node; } -/** - * @template {ClientTransformState} State - * @param {AssignmentExpression} node - * @param {import('zimmerframe').Context} context - * @param {() => any} fallback - * @param {boolean | null} [prefix] - If the assignment is a transformed update expression, set this. Else `null` - * @returns {Expression} - */ -export function build_setter(node, context, fallback, prefix) { - const { state, visit } = context; - - const assignee = node.left; - if ( - assignee.type === 'ArrayPattern' || - assignee.type === 'ObjectPattern' || - assignee.type === 'RestElement' - ) { - // Turn assignment into an IIFE, so that `$.set` calls etc don't produce invalid code - const tmp_id = context.state.scope.generate('tmp'); - - /** @type {AssignmentExpression[]} */ - const original_assignments = []; - - /** @type {Expression[]} */ - const assignments = []; - - const paths = extract_paths(assignee); - - for (const path of paths) { - const value = path.expression?.(b.id(tmp_id)); - const assignment = b.assignment('=', path.node, value); - original_assignments.push(assignment); - assignments.push(build_setter(assignment, context, () => assignment, prefix)); - } - - if (assignments.every((assignment, i) => assignment === original_assignments[i])) { - // No change to output -> nothing to transform -> we can keep the original assignment - return fallback(); - } - - const rhs_expression = /** @type {Expression} */ (visit(node.right)); - - const iife_is_async = - is_expression_async(rhs_expression) || - assignments.some((assignment) => is_expression_async(assignment)); - - const iife = b.arrow( - [], - b.block([ - b.const(tmp_id, rhs_expression), - b.stmt(b.sequence(assignments)), - // return because it could be used in a nested expression where the value is needed. - // example: { foo: ({ bar } = { bar: 1 })} - b.return(b.id(tmp_id)) - ]) - ); - - if (iife_is_async) { - return b.await(b.call(b.async(iife))); - } else { - return b.call(iife); - } - } - - if (assignee.type !== 'Identifier' && assignee.type !== 'MemberExpression') { - throw new Error(`Unexpected assignment type ${assignee.type}`); - } - - // Handle class private/public state assignment cases - if (assignee.type === 'MemberExpression') { - if ( - assignee.object.type === 'ThisExpression' && - assignee.property.type === 'PrivateIdentifier' - ) { - const private_state = context.state.private_state.get(assignee.property.name); - const value = get_assignment_value(node, context); - if (private_state !== undefined) { - if (state.in_constructor) { - // See if we should wrap value in $.proxy - if (context.state.analysis.runes && should_proxy_or_freeze(value, context.state.scope)) { - const assignment = fallback(); - if (assignment.type === 'AssignmentExpression') { - assignment.right = - private_state.kind === 'frozen_state' - ? b.call('$.freeze', value) - : build_proxy_reassignment(value, private_state.id); - return assignment; - } - } - } else { - return b.call( - '$.set', - assignee, - context.state.analysis.runes && should_proxy_or_freeze(value, context.state.scope) - ? private_state.kind === 'frozen_state' - ? b.call('$.freeze', value) - : build_proxy_reassignment(value, private_state.id) - : value - ); - } - } - } else if ( - assignee.object.type === 'ThisExpression' && - assignee.property.type === 'Identifier' && - state.in_constructor - ) { - const public_state = context.state.public_state.get(assignee.property.name); - const value = get_assignment_value(node, context); - // See if we should wrap value in $.proxy - if ( - context.state.analysis.runes && - public_state !== undefined && - should_proxy_or_freeze(value, context.state.scope) - ) { - const assignment = fallback(); - if (assignment.type === 'AssignmentExpression') { - assignment.right = - public_state.kind === 'frozen_state' - ? b.call('$.freeze', value) - : build_proxy_reassignment(value, public_state.id); - return assignment; - } - } - } - } - - const left = object(assignee); - - const binding = left && state.scope.get(left.name); - if (!binding) return fallback(); - - const transform = Object.hasOwn(context.state.transform, left.name) - ? context.state.transform[left.name] - : null; - - // reassignment - if (left === node.left && transform?.assign) { - let value = /** @type {Expression} */ (visit(node.right)); - - if (node.operator !== '=') { - value = b.binary( - /** @type {BinaryOperator} */ (node.operator.slice(0, -1)), - /** @type {Expression} */ (visit(left)), - value - ); - } - - // special case — if an element binding, we know it's a primitive - const path = context.path.map((node) => node.type); - const is_primitive = path.at(-1) === 'BindDirective' && path.at(-2) === 'RegularElement'; - - if ( - !is_primitive && - binding.kind !== 'prop' && - context.state.analysis.runes && - should_proxy_or_freeze(value, context.state.scope) - ) { - if (binding.kind === 'frozen_state') { - value = b.call('$.freeze', value); - } else { - value = build_proxy_reassignment(value, left.name); - } - } - - return transform.assign(left, value); - } - - /** - * @param {any} serialized - * @returns - */ - const maybe_skip_ownership_validation = (serialized) => { - if (is_ignored(node, 'ownership_invalid_mutation')) { - return b.call('$.skip_ownership_validation', b.thunk(serialized)); - } - - return serialized; - }; - - // mutation - if (transform?.mutate) { - const mutation = b.assignment( - node.operator, - /** @type {Pattern} */ (context.visit(node.left)), - /** @type {Expression} */ (context.visit(node.right)) - ); - - return maybe_skip_ownership_validation(transform.mutate(left, mutation)); - } - - if ( - node.right.type === 'Literal' && - prefix != null && - (node.operator === '+=' || node.operator === '-=') - ) { - return maybe_skip_ownership_validation( - b.update( - node.operator === '+=' ? '++' : '--', - /** @type {Expression} */ (visit(node.left)), - prefix - ) - ); - } else { - if ( - binding.kind !== 'state' && - binding.kind !== 'frozen_state' && - binding.kind !== 'prop' && - binding.kind !== 'bindable_prop' && - binding.kind !== 'each' && - binding.kind !== 'legacy_reactive' && - binding.kind !== 'store_sub' && - binding.kind !== 'derived' - ) { - // TODO error if it's a computed (or rest prop)? or does that already happen elsewhere? - return fallback(); - } - return maybe_skip_ownership_validation( - b.assignment( - node.operator, - /** @type {Pattern} */ (visit(node.left)), - /** @type {Expression} */ (visit(node.right)) - ) - ); - } -} - /** * @param {Expression} value * @param {PrivateIdentifier | string} proxy_reference diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index fefc11bf1f34..5967065b79b8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -1,6 +1,10 @@ -/** @import { AssignmentExpression } from 'estree' */ -/** @import { Context } from '../types' */ -import { build_setter } from '../utils.js'; +/** @import { AssignmentExpression, BinaryOperator, Expression, Pattern } from 'estree' */ +/** @import { SvelteNode } from '#compiler' */ +/** @import { ClientTransformState, Context } from '../types.js' */ +import * as b from '../../../../utils/builders.js'; +import { extract_paths, is_expression_async, object } from '../../../../utils/ast.js'; +import { is_ignored } from '../../../../state.js'; +import { build_getter, build_proxy_reassignment, should_proxy_or_freeze } from '../utils.js'; /** * @param {AssignmentExpression} node @@ -9,3 +13,266 @@ import { build_setter } from '../utils.js'; export function AssignmentExpression(node, context) { return build_setter(node, context, context.next); } + +/** + * @template {ClientTransformState} State + * @param {AssignmentExpression} node + * @param {import('zimmerframe').Context} context + * @param {() => any} fallback + * @param {boolean | null} [prefix] - If the assignment is a transformed update expression, set this. Else `null` + * @returns {Expression} + */ +export function build_setter(node, context, fallback, prefix) { + const { state, visit } = context; + + const assignee = node.left; + if ( + assignee.type === 'ArrayPattern' || + assignee.type === 'ObjectPattern' || + assignee.type === 'RestElement' + ) { + // Turn assignment into an IIFE, so that `$.set` calls etc don't produce invalid code + const tmp_id = context.state.scope.generate('tmp'); + + /** @type {AssignmentExpression[]} */ + const original_assignments = []; + + /** @type {Expression[]} */ + const assignments = []; + + const paths = extract_paths(assignee); + + for (const path of paths) { + const value = path.expression?.(b.id(tmp_id)); + const assignment = b.assignment('=', path.node, value); + original_assignments.push(assignment); + assignments.push(build_setter(assignment, context, () => assignment, prefix)); + } + + if (assignments.every((assignment, i) => assignment === original_assignments[i])) { + // No change to output -> nothing to transform -> we can keep the original assignment + return fallback(); + } + + const rhs_expression = /** @type {Expression} */ (visit(node.right)); + + const iife_is_async = + is_expression_async(rhs_expression) || + assignments.some((assignment) => is_expression_async(assignment)); + + const iife = b.arrow( + [], + b.block([ + b.const(tmp_id, rhs_expression), + b.stmt(b.sequence(assignments)), + // return because it could be used in a nested expression where the value is needed. + // example: { foo: ({ bar } = { bar: 1 })} + b.return(b.id(tmp_id)) + ]) + ); + + if (iife_is_async) { + return b.await(b.call(b.async(iife))); + } else { + return b.call(iife); + } + } + + if (assignee.type !== 'Identifier' && assignee.type !== 'MemberExpression') { + throw new Error(`Unexpected assignment type ${assignee.type}`); + } + + // Handle class private/public state assignment cases + if (assignee.type === 'MemberExpression') { + if ( + assignee.object.type === 'ThisExpression' && + assignee.property.type === 'PrivateIdentifier' + ) { + const private_state = context.state.private_state.get(assignee.property.name); + const value = get_assignment_value(node, context); + if (private_state !== undefined) { + if (state.in_constructor) { + // See if we should wrap value in $.proxy + if (context.state.analysis.runes && should_proxy_or_freeze(value, context.state.scope)) { + const assignment = fallback(); + if (assignment.type === 'AssignmentExpression') { + assignment.right = + private_state.kind === 'frozen_state' + ? b.call('$.freeze', value) + : build_proxy_reassignment(value, private_state.id); + return assignment; + } + } + } else { + return b.call( + '$.set', + assignee, + context.state.analysis.runes && should_proxy_or_freeze(value, context.state.scope) + ? private_state.kind === 'frozen_state' + ? b.call('$.freeze', value) + : build_proxy_reassignment(value, private_state.id) + : value + ); + } + } + } else if ( + assignee.object.type === 'ThisExpression' && + assignee.property.type === 'Identifier' && + state.in_constructor + ) { + const public_state = context.state.public_state.get(assignee.property.name); + const value = get_assignment_value(node, context); + // See if we should wrap value in $.proxy + if ( + context.state.analysis.runes && + public_state !== undefined && + should_proxy_or_freeze(value, context.state.scope) + ) { + const assignment = fallback(); + if (assignment.type === 'AssignmentExpression') { + assignment.right = + public_state.kind === 'frozen_state' + ? b.call('$.freeze', value) + : build_proxy_reassignment(value, public_state.id); + return assignment; + } + } + } + } + + const left = object(assignee); + + const binding = left && state.scope.get(left.name); + if (!binding) return fallback(); + + const transform = Object.hasOwn(context.state.transform, left.name) + ? context.state.transform[left.name] + : null; + + // reassignment + if (left === node.left && transform?.assign) { + let value = /** @type {Expression} */ (visit(node.right)); + + if (node.operator !== '=') { + value = b.binary( + /** @type {BinaryOperator} */ (node.operator.slice(0, -1)), + /** @type {Expression} */ (visit(left)), + value + ); + } + + // special case — if an element binding, we know it's a primitive + const path = context.path.map((node) => node.type); + const is_primitive = path.at(-1) === 'BindDirective' && path.at(-2) === 'RegularElement'; + + if ( + !is_primitive && + binding.kind !== 'prop' && + context.state.analysis.runes && + should_proxy_or_freeze(value, context.state.scope) + ) { + if (binding.kind === 'frozen_state') { + value = b.call('$.freeze', value); + } else { + value = build_proxy_reassignment(value, left.name); + } + } + + return transform.assign(left, value); + } + + /** + * @param {any} serialized + * @returns + */ + const maybe_skip_ownership_validation = (serialized) => { + if (is_ignored(node, 'ownership_invalid_mutation')) { + return b.call('$.skip_ownership_validation', b.thunk(serialized)); + } + + return serialized; + }; + + // mutation + if (transform?.mutate) { + const mutation = b.assignment( + node.operator, + /** @type {Pattern} */ (context.visit(node.left)), + /** @type {Expression} */ (context.visit(node.right)) + ); + + return maybe_skip_ownership_validation(transform.mutate(left, mutation)); + } + + if ( + node.right.type === 'Literal' && + prefix != null && + (node.operator === '+=' || node.operator === '-=') + ) { + return maybe_skip_ownership_validation( + b.update( + node.operator === '+=' ? '++' : '--', + /** @type {Expression} */ (visit(node.left)), + prefix + ) + ); + } else { + if ( + binding.kind !== 'state' && + binding.kind !== 'frozen_state' && + binding.kind !== 'prop' && + binding.kind !== 'bindable_prop' && + binding.kind !== 'each' && + binding.kind !== 'legacy_reactive' && + binding.kind !== 'store_sub' && + binding.kind !== 'derived' + ) { + // TODO error if it's a computed (or rest prop)? or does that already happen elsewhere? + return fallback(); + } + return maybe_skip_ownership_validation( + b.assignment( + node.operator, + /** @type {Pattern} */ (visit(node.left)), + /** @type {Expression} */ (visit(node.right)) + ) + ); + } +} + +/** + * @template {ClientTransformState} State + * @param {AssignmentExpression} node + * @param {import('zimmerframe').Context} context + * @returns + */ +function get_assignment_value(node, { state, visit }) { + if (node.left.type === 'Identifier') { + const operator = node.operator; + return operator === '=' + ? /** @type {Expression} */ (visit(node.right)) + : // turn something like x += 1 into x = x + 1 + b.binary( + /** @type {BinaryOperator} */ (operator.slice(0, -1)), + build_getter(node.left, state), + /** @type {Expression} */ (visit(node.right)) + ); + } else if ( + node.left.type === 'MemberExpression' && + node.left.object.type === 'ThisExpression' && + node.left.property.type === 'PrivateIdentifier' && + state.private_state.has(node.left.property.name) + ) { + const operator = node.operator; + return operator === '=' + ? /** @type {Expression} */ (visit(node.right)) + : // turn something like x += 1 into x = x + 1 + b.binary( + /** @type {BinaryOperator} */ (operator.slice(0, -1)), + /** @type {Expression} */ (visit(node.left)), + /** @type {Expression} */ (visit(node.right)) + ); + } else { + return /** @type {Expression} */ (visit(node.right)); + } +} From 41b355e9fd2668d0c5f750d3ae3006d7a1019956 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 15:32:18 -0400 Subject: [PATCH 53/86] remove stuff --- .../client/visitors/AssignmentExpression.js | 54 +++++++------------ 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 5967065b79b8..8b6381c422be 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -19,10 +19,9 @@ export function AssignmentExpression(node, context) { * @param {AssignmentExpression} node * @param {import('zimmerframe').Context} context * @param {() => any} fallback - * @param {boolean | null} [prefix] - If the assignment is a transformed update expression, set this. Else `null` * @returns {Expression} */ -export function build_setter(node, context, fallback, prefix) { +export function build_setter(node, context, fallback) { const { state, visit } = context; const assignee = node.left; @@ -46,7 +45,7 @@ export function build_setter(node, context, fallback, prefix) { const value = path.expression?.(b.id(tmp_id)); const assignment = b.assignment('=', path.node, value); original_assignments.push(assignment); - assignments.push(build_setter(assignment, context, () => assignment, prefix)); + assignments.push(build_setter(assignment, context, () => assignment)); } if (assignments.every((assignment, i) => assignment === original_assignments[i])) { @@ -205,39 +204,26 @@ export function build_setter(node, context, fallback, prefix) { } if ( - node.right.type === 'Literal' && - prefix != null && - (node.operator === '+=' || node.operator === '-=') + binding.kind !== 'state' && + binding.kind !== 'frozen_state' && + binding.kind !== 'prop' && + binding.kind !== 'bindable_prop' && + binding.kind !== 'each' && + binding.kind !== 'legacy_reactive' && + binding.kind !== 'store_sub' && + binding.kind !== 'derived' ) { - return maybe_skip_ownership_validation( - b.update( - node.operator === '+=' ? '++' : '--', - /** @type {Expression} */ (visit(node.left)), - prefix - ) - ); - } else { - if ( - binding.kind !== 'state' && - binding.kind !== 'frozen_state' && - binding.kind !== 'prop' && - binding.kind !== 'bindable_prop' && - binding.kind !== 'each' && - binding.kind !== 'legacy_reactive' && - binding.kind !== 'store_sub' && - binding.kind !== 'derived' - ) { - // TODO error if it's a computed (or rest prop)? or does that already happen elsewhere? - return fallback(); - } - return maybe_skip_ownership_validation( - b.assignment( - node.operator, - /** @type {Pattern} */ (visit(node.left)), - /** @type {Expression} */ (visit(node.right)) - ) - ); + // TODO error if it's a computed (or rest prop)? or does that already happen elsewhere? + return fallback(); } + + return maybe_skip_ownership_validation( + b.assignment( + node.operator, + /** @type {Pattern} */ (visit(node.left)), + /** @type {Expression} */ (visit(node.right)) + ) + ); } /** From 2a7d864d659b3579f02033a3284670fd4791edb7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 15:36:12 -0400 Subject: [PATCH 54/86] tweak --- .../client/visitors/AssignmentExpression.js | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 8b6381c422be..376df97005d9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -1,4 +1,4 @@ -/** @import { AssignmentExpression, BinaryOperator, Expression, Pattern } from 'estree' */ +/** @import { AssignmentExpression, BinaryOperator, Expression, Identifier, MemberExpression, Pattern } from 'estree' */ /** @import { SvelteNode } from '#compiler' */ /** @import { ClientTransformState, Context } from '../types.js' */ import * as b from '../../../../utils/builders.js'; @@ -11,19 +11,6 @@ import { build_getter, build_proxy_reassignment, should_proxy_or_freeze } from ' * @param {Context} context */ export function AssignmentExpression(node, context) { - return build_setter(node, context, context.next); -} - -/** - * @template {ClientTransformState} State - * @param {AssignmentExpression} node - * @param {import('zimmerframe').Context} context - * @param {() => any} fallback - * @returns {Expression} - */ -export function build_setter(node, context, fallback) { - const { state, visit } = context; - const assignee = node.left; if ( assignee.type === 'ArrayPattern' || @@ -50,10 +37,10 @@ export function build_setter(node, context, fallback) { if (assignments.every((assignment, i) => assignment === original_assignments[i])) { // No change to output -> nothing to transform -> we can keep the original assignment - return fallback(); + return; } - const rhs_expression = /** @type {Expression} */ (visit(node.right)); + const rhs_expression = /** @type {Expression} */ (context.visit(node.right)); const iife_is_async = is_expression_async(rhs_expression) || @@ -81,6 +68,21 @@ export function build_setter(node, context, fallback) { throw new Error(`Unexpected assignment type ${assignee.type}`); } + return build_setter(node, context, context.next); +} + +/** + * @template {ClientTransformState} State + * @param {AssignmentExpression} node + * @param {import('zimmerframe').Context} context + * @param {() => any} fallback + * @returns {Expression} + */ +export function build_setter(node, context, fallback) { + const { state, visit } = context; + + const assignee = /** @type {Identifier | MemberExpression} */ (node.left); + // Handle class private/public state assignment cases if (assignee.type === 'MemberExpression') { if ( From 08c0fddf1fa737cc5ac51c5d3e553c920136ce89 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 19:51:33 -0400 Subject: [PATCH 55/86] tweak --- .../client/visitors/AssignmentExpression.js | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 376df97005d9..1f1eab001a87 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -84,17 +84,20 @@ export function build_setter(node, context, fallback) { const assignee = /** @type {Identifier | MemberExpression} */ (node.left); // Handle class private/public state assignment cases - if (assignee.type === 'MemberExpression') { - if ( - assignee.object.type === 'ThisExpression' && - assignee.property.type === 'PrivateIdentifier' - ) { + if ( + context.state.analysis.runes && + assignee.type === 'MemberExpression' && + assignee.object.type === 'ThisExpression' + ) { + if (assignee.property.type === 'PrivateIdentifier') { const private_state = context.state.private_state.get(assignee.property.name); - const value = get_assignment_value(node, context); + if (private_state !== undefined) { + const value = get_assignment_value(node, context); + if (state.in_constructor) { // See if we should wrap value in $.proxy - if (context.state.analysis.runes && should_proxy_or_freeze(value, context.state.scope)) { + if (should_proxy_or_freeze(value, context.state.scope)) { const assignment = fallback(); if (assignment.type === 'AssignmentExpression') { assignment.right = @@ -108,7 +111,7 @@ export function build_setter(node, context, fallback) { return b.call( '$.set', assignee, - context.state.analysis.runes && should_proxy_or_freeze(value, context.state.scope) + should_proxy_or_freeze(value, context.state.scope) ? private_state.kind === 'frozen_state' ? b.call('$.freeze', value) : build_proxy_reassignment(value, private_state.id) @@ -116,19 +119,12 @@ export function build_setter(node, context, fallback) { ); } } - } else if ( - assignee.object.type === 'ThisExpression' && - assignee.property.type === 'Identifier' && - state.in_constructor - ) { + } else if (assignee.property.type === 'Identifier' && state.in_constructor) { const public_state = context.state.public_state.get(assignee.property.name); const value = get_assignment_value(node, context); + // See if we should wrap value in $.proxy - if ( - context.state.analysis.runes && - public_state !== undefined && - should_proxy_or_freeze(value, context.state.scope) - ) { + if (public_state !== undefined && should_proxy_or_freeze(value, context.state.scope)) { const assignment = fallback(); if (assignment.type === 'AssignmentExpression') { assignment.right = From 71e057ea64ebfb77f59b33bcfc3401730f1649a2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 21:00:10 -0400 Subject: [PATCH 56/86] fix --- .../client/visitors/AssignmentExpression.js | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 1f1eab001a87..064d35b0c2ee 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -93,30 +93,27 @@ export function build_setter(node, context, fallback) { const private_state = context.state.private_state.get(assignee.property.name); if (private_state !== undefined) { - const value = get_assignment_value(node, context); + let value = get_assignment_value(node, context); + let transformed = false; + + if (should_proxy_or_freeze(value, context.state.scope)) { + transformed = true; + value = + private_state.kind === 'frozen_state' + ? b.call('$.freeze', value) + : build_proxy_reassignment(value, private_state.id); + } if (state.in_constructor) { - // See if we should wrap value in $.proxy - if (should_proxy_or_freeze(value, context.state.scope)) { - const assignment = fallback(); - if (assignment.type === 'AssignmentExpression') { - assignment.right = - private_state.kind === 'frozen_state' - ? b.call('$.freeze', value) - : build_proxy_reassignment(value, private_state.id); - return assignment; - } + if (transformed) { + return b.assignment( + node.operator, + /** @type {Pattern} */ (context.visit(node.left)), + value + ); } } else { - return b.call( - '$.set', - assignee, - should_proxy_or_freeze(value, context.state.scope) - ? private_state.kind === 'frozen_state' - ? b.call('$.freeze', value) - : build_proxy_reassignment(value, private_state.id) - : value - ); + return b.call('$.set', assignee, value); } } } else if (assignee.property.type === 'Identifier' && state.in_constructor) { @@ -241,7 +238,9 @@ function get_assignment_value(node, { state, visit }) { build_getter(node.left, state), /** @type {Expression} */ (visit(node.right)) ); - } else if ( + } + + if ( node.left.type === 'MemberExpression' && node.left.object.type === 'ThisExpression' && node.left.property.type === 'PrivateIdentifier' && @@ -256,7 +255,7 @@ function get_assignment_value(node, { state, visit }) { /** @type {Expression} */ (visit(node.left)), /** @type {Expression} */ (visit(node.right)) ); - } else { - return /** @type {Expression} */ (visit(node.right)); } + + return /** @type {Expression} */ (visit(node.right)); } From 833b749eeabd156ad8da952b3736109bd9caeb28 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 21:12:10 -0400 Subject: [PATCH 57/86] tweak --- .../client/visitors/AssignmentExpression.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 064d35b0c2ee..4d180a11ccdf 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -120,16 +120,14 @@ export function build_setter(node, context, fallback) { const public_state = context.state.public_state.get(assignee.property.name); const value = get_assignment_value(node, context); - // See if we should wrap value in $.proxy if (public_state !== undefined && should_proxy_or_freeze(value, context.state.scope)) { - const assignment = fallback(); - if (assignment.type === 'AssignmentExpression') { - assignment.right = - public_state.kind === 'frozen_state' - ? b.call('$.freeze', value) - : build_proxy_reassignment(value, public_state.id); - return assignment; - } + return b.assignment( + node.operator, + /** @type {Pattern} */ (context.visit(node.left)), + public_state.kind === 'frozen_state' + ? b.call('$.freeze', value) + : build_proxy_reassignment(value, public_state.id) + ); } } } From 95b517fd1ccbce195910881ca33fe8f83f0517ed Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 21:48:01 -0400 Subject: [PATCH 58/86] tidy up --- .../client/visitors/AssignmentExpression.js | 66 ++++++++++--------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 4d180a11ccdf..8224ca93f3a4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -32,7 +32,7 @@ export function AssignmentExpression(node, context) { const value = path.expression?.(b.id(tmp_id)); const assignment = b.assignment('=', path.node, value); original_assignments.push(assignment); - assignments.push(build_setter(assignment, context, () => assignment)); + assignments.push(build_assignment(assignment, context, () => assignment)); } if (assignments.every((assignment, i) => assignment === original_assignments[i])) { @@ -68,7 +68,7 @@ export function AssignmentExpression(node, context) { throw new Error(`Unexpected assignment type ${assignee.type}`); } - return build_setter(node, context, context.next); + return build_assignment(node, context, context.next); } /** @@ -78,10 +78,11 @@ export function AssignmentExpression(node, context) { * @param {() => any} fallback * @returns {Expression} */ -export function build_setter(node, context, fallback) { +export function build_assignment(node, context, fallback) { const { state, visit } = context; + const { operator, left, right } = node; - const assignee = /** @type {Identifier | MemberExpression} */ (node.left); + const assignee = /** @type {Identifier | MemberExpression} */ (left); // Handle class private/public state assignment cases if ( @@ -106,11 +107,7 @@ export function build_setter(node, context, fallback) { if (state.in_constructor) { if (transformed) { - return b.assignment( - node.operator, - /** @type {Pattern} */ (context.visit(node.left)), - value - ); + return b.assignment(operator, /** @type {Pattern} */ (context.visit(left)), value); } } else { return b.call('$.set', assignee, value); @@ -122,8 +119,8 @@ export function build_setter(node, context, fallback) { if (public_state !== undefined && should_proxy_or_freeze(value, context.state.scope)) { return b.assignment( - node.operator, - /** @type {Pattern} */ (context.visit(node.left)), + operator, + /** @type {Pattern} */ (context.visit(left)), public_state.kind === 'frozen_state' ? b.call('$.freeze', value) : build_proxy_reassignment(value, public_state.id) @@ -132,23 +129,32 @@ export function build_setter(node, context, fallback) { } } - const left = object(assignee); + let object = left; - const binding = left && state.scope.get(left.name); + while (object.type === 'MemberExpression') { + // @ts-expect-error + object = object.object; + } + + if (object.type !== 'Identifier') { + return fallback(); + } + + const binding = state.scope.get(object.name); if (!binding) return fallback(); - const transform = Object.hasOwn(context.state.transform, left.name) - ? context.state.transform[left.name] + const transform = Object.hasOwn(context.state.transform, object.name) + ? context.state.transform[object.name] : null; // reassignment - if (left === node.left && transform?.assign) { - let value = /** @type {Expression} */ (visit(node.right)); + if (object === left && transform?.assign) { + let value = /** @type {Expression} */ (visit(right)); - if (node.operator !== '=') { + if (operator !== '=') { value = b.binary( - /** @type {BinaryOperator} */ (node.operator.slice(0, -1)), - /** @type {Expression} */ (visit(left)), + /** @type {BinaryOperator} */ (operator.slice(0, -1)), + /** @type {Expression} */ (visit(object)), value ); } @@ -166,11 +172,11 @@ export function build_setter(node, context, fallback) { if (binding.kind === 'frozen_state') { value = b.call('$.freeze', value); } else { - value = build_proxy_reassignment(value, left.name); + value = build_proxy_reassignment(value, object.name); } } - return transform.assign(left, value); + return transform.assign(object, value); } /** @@ -178,7 +184,7 @@ export function build_setter(node, context, fallback) { * @returns */ const maybe_skip_ownership_validation = (serialized) => { - if (is_ignored(node, 'ownership_invalid_mutation')) { + if (is_ignored(left, 'ownership_invalid_mutation')) { return b.call('$.skip_ownership_validation', b.thunk(serialized)); } @@ -188,12 +194,12 @@ export function build_setter(node, context, fallback) { // mutation if (transform?.mutate) { const mutation = b.assignment( - node.operator, - /** @type {Pattern} */ (context.visit(node.left)), - /** @type {Expression} */ (context.visit(node.right)) + operator, + /** @type {Pattern} */ (context.visit(left)), + /** @type {Expression} */ (context.visit(right)) ); - return maybe_skip_ownership_validation(transform.mutate(left, mutation)); + return maybe_skip_ownership_validation(transform.mutate(object, mutation)); } if ( @@ -212,9 +218,9 @@ export function build_setter(node, context, fallback) { return maybe_skip_ownership_validation( b.assignment( - node.operator, - /** @type {Pattern} */ (visit(node.left)), - /** @type {Expression} */ (visit(node.right)) + operator, + /** @type {Pattern} */ (visit(left)), + /** @type {Expression} */ (visit(right)) ) ); } From a4d57677e1fd226f0718e389e3bc6957f786a09b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 21:52:39 -0400 Subject: [PATCH 59/86] tidy up --- .../client/visitors/AssignmentExpression.js | 60 +++++-------------- 1 file changed, 15 insertions(+), 45 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 8224ca93f3a4..c76e0b1ec955 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -79,7 +79,6 @@ export function AssignmentExpression(node, context) { * @returns {Expression} */ export function build_assignment(node, context, fallback) { - const { state, visit } = context; const { operator, left, right } = node; const assignee = /** @type {Identifier | MemberExpression} */ (left); @@ -105,7 +104,7 @@ export function build_assignment(node, context, fallback) { : build_proxy_reassignment(value, private_state.id); } - if (state.in_constructor) { + if (context.state.in_constructor) { if (transformed) { return b.assignment(operator, /** @type {Pattern} */ (context.visit(left)), value); } @@ -113,7 +112,7 @@ export function build_assignment(node, context, fallback) { return b.call('$.set', assignee, value); } } - } else if (assignee.property.type === 'Identifier' && state.in_constructor) { + } else if (assignee.property.type === 'Identifier' && context.state.in_constructor) { const public_state = context.state.public_state.get(assignee.property.name); const value = get_assignment_value(node, context); @@ -140,7 +139,7 @@ export function build_assignment(node, context, fallback) { return fallback(); } - const binding = state.scope.get(object.name); + const binding = context.state.scope.get(object.name); if (!binding) return fallback(); const transform = Object.hasOwn(context.state.transform, object.name) @@ -149,12 +148,12 @@ export function build_assignment(node, context, fallback) { // reassignment if (object === left && transform?.assign) { - let value = /** @type {Expression} */ (visit(right)); + let value = /** @type {Expression} */ (context.visit(right)); if (operator !== '=') { value = b.binary( /** @type {BinaryOperator} */ (operator.slice(0, -1)), - /** @type {Expression} */ (visit(object)), + /** @type {Expression} */ (context.visit(object)), value ); } @@ -179,50 +178,21 @@ export function build_assignment(node, context, fallback) { return transform.assign(object, value); } - /** - * @param {any} serialized - * @returns - */ - const maybe_skip_ownership_validation = (serialized) => { - if (is_ignored(left, 'ownership_invalid_mutation')) { - return b.call('$.skip_ownership_validation', b.thunk(serialized)); - } - - return serialized; - }; + /** @type {Expression} */ + let mutation = b.assignment( + operator, + /** @type {Pattern} */ (context.visit(left)), + /** @type {Expression} */ (context.visit(right)) + ); // mutation if (transform?.mutate) { - const mutation = b.assignment( - operator, - /** @type {Pattern} */ (context.visit(left)), - /** @type {Expression} */ (context.visit(right)) - ); - - return maybe_skip_ownership_validation(transform.mutate(object, mutation)); + mutation = transform.mutate(object, mutation); } - if ( - binding.kind !== 'state' && - binding.kind !== 'frozen_state' && - binding.kind !== 'prop' && - binding.kind !== 'bindable_prop' && - binding.kind !== 'each' && - binding.kind !== 'legacy_reactive' && - binding.kind !== 'store_sub' && - binding.kind !== 'derived' - ) { - // TODO error if it's a computed (or rest prop)? or does that already happen elsewhere? - return fallback(); - } - - return maybe_skip_ownership_validation( - b.assignment( - operator, - /** @type {Pattern} */ (visit(left)), - /** @type {Expression} */ (visit(right)) - ) - ); + return is_ignored(left, 'ownership_invalid_mutation') + ? b.call('$.skip_ownership_validation', b.thunk(mutation)) + : mutation; } /** From 768db25cc51f5d910f9e8b1feb154fdecc98c1ab Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 21:57:07 -0400 Subject: [PATCH 60/86] tidy up --- .../client/visitors/AssignmentExpression.js | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index c76e0b1ec955..65dd7ac9c397 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -1,4 +1,4 @@ -/** @import { AssignmentExpression, BinaryOperator, Expression, Identifier, MemberExpression, Pattern } from 'estree' */ +/** @import { AssignmentExpression, AssignmentOperator, BinaryOperator, Expression, Identifier, MemberExpression, Pattern } from 'estree' */ /** @import { SvelteNode } from '#compiler' */ /** @import { ClientTransformState, Context } from '../types.js' */ import * as b from '../../../../utils/builders.js'; @@ -93,7 +93,7 @@ export function build_assignment(node, context, fallback) { const private_state = context.state.private_state.get(assignee.property.name); if (private_state !== undefined) { - let value = get_assignment_value(node, context); + let value = get_assignment_value(operator, left, right, context); let transformed = false; if (should_proxy_or_freeze(value, context.state.scope)) { @@ -114,7 +114,7 @@ export function build_assignment(node, context, fallback) { } } else if (assignee.property.type === 'Identifier' && context.state.in_constructor) { const public_state = context.state.public_state.get(assignee.property.name); - const value = get_assignment_value(node, context); + const value = get_assignment_value(operator, left, right, context); if (public_state !== undefined && should_proxy_or_freeze(value, context.state.scope)) { return b.assignment( @@ -197,39 +197,39 @@ export function build_assignment(node, context, fallback) { /** * @template {ClientTransformState} State - * @param {AssignmentExpression} node + * @param {AssignmentOperator} operator + * @param {Pattern} left + * @param {Expression} right * @param {import('zimmerframe').Context} context * @returns */ -function get_assignment_value(node, { state, visit }) { - if (node.left.type === 'Identifier') { - const operator = node.operator; +function get_assignment_value(operator, left, right, { state, visit }) { + if (left.type === 'Identifier') { return operator === '=' - ? /** @type {Expression} */ (visit(node.right)) + ? /** @type {Expression} */ (visit(right)) : // turn something like x += 1 into x = x + 1 b.binary( /** @type {BinaryOperator} */ (operator.slice(0, -1)), - build_getter(node.left, state), - /** @type {Expression} */ (visit(node.right)) + build_getter(left, state), + /** @type {Expression} */ (visit(right)) ); } if ( - node.left.type === 'MemberExpression' && - node.left.object.type === 'ThisExpression' && - node.left.property.type === 'PrivateIdentifier' && - state.private_state.has(node.left.property.name) + left.type === 'MemberExpression' && + left.object.type === 'ThisExpression' && + left.property.type === 'PrivateIdentifier' && + state.private_state.has(left.property.name) ) { - const operator = node.operator; return operator === '=' - ? /** @type {Expression} */ (visit(node.right)) + ? /** @type {Expression} */ (visit(right)) : // turn something like x += 1 into x = x + 1 b.binary( /** @type {BinaryOperator} */ (operator.slice(0, -1)), - /** @type {Expression} */ (visit(node.left)), - /** @type {Expression} */ (visit(node.right)) + /** @type {Expression} */ (visit(left)), + /** @type {Expression} */ (visit(right)) ); } - return /** @type {Expression} */ (visit(node.right)); + return /** @type {Expression} */ (visit(right)); } From 94f6e65d3e209aa9ca55aa50f04d37b93344d0bd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 22:03:21 -0400 Subject: [PATCH 61/86] tweak --- .../client/visitors/AssignmentExpression.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 65dd7ac9c397..72bb34fa57dc 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -32,7 +32,7 @@ export function AssignmentExpression(node, context) { const value = path.expression?.(b.id(tmp_id)); const assignment = b.assignment('=', path.node, value); original_assignments.push(assignment); - assignments.push(build_assignment(assignment, context, () => assignment)); + assignments.push(build_assignment('=', path.node, value, context, () => assignment)); } if (assignments.every((assignment, i) => assignment === original_assignments[i])) { @@ -68,19 +68,19 @@ export function AssignmentExpression(node, context) { throw new Error(`Unexpected assignment type ${assignee.type}`); } - return build_assignment(node, context, context.next); + return build_assignment(node.operator, node.left, node.right, context, context.next); } /** * @template {ClientTransformState} State - * @param {AssignmentExpression} node + * @param {AssignmentOperator} operator + * @param {Pattern} left + * @param {Expression} right * @param {import('zimmerframe').Context} context * @param {() => any} fallback * @returns {Expression} */ -export function build_assignment(node, context, fallback) { - const { operator, left, right } = node; - +export function build_assignment(operator, left, right, context, fallback) { const assignee = /** @type {Identifier | MemberExpression} */ (left); // Handle class private/public state assignment cases From 21d787bd766e8d71ba47e0261d300b19dd0858d9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 22:08:00 -0400 Subject: [PATCH 62/86] simplify --- .../client/visitors/AssignmentExpression.js | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 72bb34fa57dc..f71a71e12805 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -17,27 +17,22 @@ export function AssignmentExpression(node, context) { assignee.type === 'ObjectPattern' || assignee.type === 'RestElement' ) { - // Turn assignment into an IIFE, so that `$.set` calls etc don't produce invalid code - const tmp_id = context.state.scope.generate('tmp'); + const rhs = b.id('$$value'); - /** @type {AssignmentExpression[]} */ - const original_assignments = []; + let changed = false; - /** @type {Expression[]} */ - const assignments = []; + const assignments = extract_paths(node.left).map((path) => { + const value = path.expression?.(rhs); - const paths = extract_paths(assignee); + let assignment = build_assignment('=', path.node, value, context); + if (assignment !== null) changed = true; - for (const path of paths) { - const value = path.expression?.(b.id(tmp_id)); - const assignment = b.assignment('=', path.node, value); - original_assignments.push(assignment); - assignments.push(build_assignment('=', path.node, value, context, () => assignment)); - } + return assignment ?? /** @type {Expression} */ (context.next()); + }); - if (assignments.every((assignment, i) => assignment === original_assignments[i])) { + if (!changed) { // No change to output -> nothing to transform -> we can keep the original assignment - return; + return context.next(); } const rhs_expression = /** @type {Expression} */ (context.visit(node.right)); @@ -49,11 +44,11 @@ export function AssignmentExpression(node, context) { const iife = b.arrow( [], b.block([ - b.const(tmp_id, rhs_expression), + b.const(rhs, rhs_expression), b.stmt(b.sequence(assignments)), // return because it could be used in a nested expression where the value is needed. // example: { foo: ({ bar } = { bar: 1 })} - b.return(b.id(tmp_id)) + b.return(rhs) ]) ); @@ -68,7 +63,10 @@ export function AssignmentExpression(node, context) { throw new Error(`Unexpected assignment type ${assignee.type}`); } - return build_assignment(node.operator, node.left, node.right, context, context.next); + return ( + build_assignment(node.operator, node.left, node.right, context) ?? + /** @type {Expression} */ (context.next()) + ); } /** @@ -77,10 +75,9 @@ export function AssignmentExpression(node, context) { * @param {Pattern} left * @param {Expression} right * @param {import('zimmerframe').Context} context - * @param {() => any} fallback - * @returns {Expression} + * @returns {Expression | null} */ -export function build_assignment(operator, left, right, context, fallback) { +export function build_assignment(operator, left, right, context) { const assignee = /** @type {Identifier | MemberExpression} */ (left); // Handle class private/public state assignment cases @@ -136,11 +133,11 @@ export function build_assignment(operator, left, right, context, fallback) { } if (object.type !== 'Identifier') { - return fallback(); + return null; } const binding = context.state.scope.get(object.name); - if (!binding) return fallback(); + if (!binding) return null; const transform = Object.hasOwn(context.state.transform, object.name) ? context.state.transform[object.name] From 0a6e48c751ef6bf3932213eb136a6464ff14117f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 22:09:20 -0400 Subject: [PATCH 63/86] tidy up --- .../3-transform/client/visitors/AssignmentExpression.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index f71a71e12805..4afa2f6d505d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -52,11 +52,7 @@ export function AssignmentExpression(node, context) { ]) ); - if (iife_is_async) { - return b.await(b.call(b.async(iife))); - } else { - return b.call(iife); - } + return iife_is_async ? b.await(b.call(b.async(iife))) : b.call(iife); } if (assignee.type !== 'Identifier' && assignee.type !== 'MemberExpression') { From cff694a38f8b3295fc248805c986f26ac96d5c4b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 22:18:49 -0400 Subject: [PATCH 64/86] simplify --- .../client/visitors/AssignmentExpression.js | 54 +++++-------------- 1 file changed, 13 insertions(+), 41 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 4afa2f6d505d..7f0e16789e10 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -107,9 +107,10 @@ export function build_assignment(operator, left, right, context) { } } else if (assignee.property.type === 'Identifier' && context.state.in_constructor) { const public_state = context.state.public_state.get(assignee.property.name); - const value = get_assignment_value(operator, left, right, context); - if (public_state !== undefined && should_proxy_or_freeze(value, context.state.scope)) { + if (public_state !== undefined && should_proxy_or_freeze(right, context.state.scope)) { + const value = /** @type {Expression} */ (context.visit(right)); + return b.assignment( operator, /** @type {Pattern} */ (context.visit(left)), @@ -141,15 +142,7 @@ export function build_assignment(operator, left, right, context) { // reassignment if (object === left && transform?.assign) { - let value = /** @type {Expression} */ (context.visit(right)); - - if (operator !== '=') { - value = b.binary( - /** @type {BinaryOperator} */ (operator.slice(0, -1)), - /** @type {Expression} */ (context.visit(object)), - value - ); - } + let value = get_assignment_value(operator, left, right, context); // special case — if an element binding, we know it's a primitive const path = context.path.map((node) => node.type); @@ -194,35 +187,14 @@ export function build_assignment(operator, left, right, context) { * @param {Pattern} left * @param {Expression} right * @param {import('zimmerframe').Context} context - * @returns */ -function get_assignment_value(operator, left, right, { state, visit }) { - if (left.type === 'Identifier') { - return operator === '=' - ? /** @type {Expression} */ (visit(right)) - : // turn something like x += 1 into x = x + 1 - b.binary( - /** @type {BinaryOperator} */ (operator.slice(0, -1)), - build_getter(left, state), - /** @type {Expression} */ (visit(right)) - ); - } - - if ( - left.type === 'MemberExpression' && - left.object.type === 'ThisExpression' && - left.property.type === 'PrivateIdentifier' && - state.private_state.has(left.property.name) - ) { - return operator === '=' - ? /** @type {Expression} */ (visit(right)) - : // turn something like x += 1 into x = x + 1 - b.binary( - /** @type {BinaryOperator} */ (operator.slice(0, -1)), - /** @type {Expression} */ (visit(left)), - /** @type {Expression} */ (visit(right)) - ); - } - - return /** @type {Expression} */ (visit(right)); +function get_assignment_value(operator, left, right, { visit }) { + return operator === '=' + ? /** @type {Expression} */ (visit(right)) + : // turn something like x += 1 into x = x + 1 + b.binary( + /** @type {BinaryOperator} */ (operator.slice(0, -1)), + /** @type {Expression} */ (visit(left)), + /** @type {Expression} */ (visit(right)) + ); } From ce4166dd2ef3c0bad7a237a10008b98121a43589 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 22:20:43 -0400 Subject: [PATCH 65/86] tidy up --- .../client/visitors/AssignmentExpression.js | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 7f0e16789e10..2064b2990364 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -1,10 +1,10 @@ -/** @import { AssignmentExpression, AssignmentOperator, BinaryOperator, Expression, Identifier, MemberExpression, Pattern } from 'estree' */ +/** @import { AssignmentExpression, AssignmentOperator, BinaryOperator, Expression, Pattern } from 'estree' */ /** @import { SvelteNode } from '#compiler' */ /** @import { ClientTransformState, Context } from '../types.js' */ import * as b from '../../../../utils/builders.js'; -import { extract_paths, is_expression_async, object } from '../../../../utils/ast.js'; +import { extract_paths, is_expression_async } from '../../../../utils/ast.js'; import { is_ignored } from '../../../../state.js'; -import { build_getter, build_proxy_reassignment, should_proxy_or_freeze } from '../utils.js'; +import { build_proxy_reassignment, should_proxy_or_freeze } from '../utils.js'; /** * @param {AssignmentExpression} node @@ -74,16 +74,14 @@ export function AssignmentExpression(node, context) { * @returns {Expression | null} */ export function build_assignment(operator, left, right, context) { - const assignee = /** @type {Identifier | MemberExpression} */ (left); - // Handle class private/public state assignment cases if ( context.state.analysis.runes && - assignee.type === 'MemberExpression' && - assignee.object.type === 'ThisExpression' + left.type === 'MemberExpression' && + left.object.type === 'ThisExpression' ) { - if (assignee.property.type === 'PrivateIdentifier') { - const private_state = context.state.private_state.get(assignee.property.name); + if (left.property.type === 'PrivateIdentifier') { + const private_state = context.state.private_state.get(left.property.name); if (private_state !== undefined) { let value = get_assignment_value(operator, left, right, context); @@ -102,11 +100,11 @@ export function build_assignment(operator, left, right, context) { return b.assignment(operator, /** @type {Pattern} */ (context.visit(left)), value); } } else { - return b.call('$.set', assignee, value); + return b.call('$.set', left, value); } } - } else if (assignee.property.type === 'Identifier' && context.state.in_constructor) { - const public_state = context.state.public_state.get(assignee.property.name); + } else if (left.property.type === 'Identifier' && context.state.in_constructor) { + const public_state = context.state.public_state.get(left.property.name); if (public_state !== undefined && should_proxy_or_freeze(right, context.state.scope)) { const value = /** @type {Expression} */ (context.visit(right)); @@ -154,11 +152,10 @@ export function build_assignment(operator, left, right, context) { context.state.analysis.runes && should_proxy_or_freeze(value, context.state.scope) ) { - if (binding.kind === 'frozen_state') { - value = b.call('$.freeze', value); - } else { - value = build_proxy_reassignment(value, object.name); - } + value = + binding.kind === 'frozen_state' + ? b.call('$.freeze', value) + : build_proxy_reassignment(value, object.name); } return transform.assign(object, value); From 0a0b0396e489d8dc55e48c412191d50be2f2d025 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 22:36:02 -0400 Subject: [PATCH 66/86] improve output --- .../client/visitors/AssignmentExpression.js | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 2064b2990364..97a013025ec0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -1,4 +1,4 @@ -/** @import { AssignmentExpression, AssignmentOperator, BinaryOperator, Expression, Pattern } from 'estree' */ +/** @import { AssignmentExpression, AssignmentOperator, BinaryOperator, Expression, Node, Pattern } from 'estree' */ /** @import { SvelteNode } from '#compiler' */ /** @import { ClientTransformState, Context } from '../types.js' */ import * as b from '../../../../utils/builders.js'; @@ -11,13 +11,17 @@ import { build_proxy_reassignment, should_proxy_or_freeze } from '../utils.js'; * @param {Context} context */ export function AssignmentExpression(node, context) { - const assignee = node.left; + const parent = /** @type {Node} */ (context.path.at(-1)); + const is_standalone = parent.type.endsWith('Statement'); + if ( - assignee.type === 'ArrayPattern' || - assignee.type === 'ObjectPattern' || - assignee.type === 'RestElement' + node.left.type === 'ArrayPattern' || + node.left.type === 'ObjectPattern' || + node.left.type === 'RestElement' ) { - const rhs = b.id('$$value'); + const value = /** @type {Expression} */ (context.visit(node.right)); + const should_cache = value.type !== 'Identifier'; + const rhs = should_cache ? b.id('$$value') : value; let changed = false; @@ -35,28 +39,29 @@ export function AssignmentExpression(node, context) { return context.next(); } - const rhs_expression = /** @type {Expression} */ (context.visit(node.right)); + const sequence = b.sequence(assignments); + + if (!is_standalone) { + // this is part of an expression, we need the sequence to end with the value + sequence.expressions.push(rhs); + } - const iife_is_async = - is_expression_async(rhs_expression) || - assignments.some((assignment) => is_expression_async(assignment)); + if (should_cache) { + // the right hand side is a complex expression, wrap in an IIFE to cache it + const iife = b.arrow([rhs], sequence); - const iife = b.arrow( - [], - b.block([ - b.const(rhs, rhs_expression), - b.stmt(b.sequence(assignments)), - // return because it could be used in a nested expression where the value is needed. - // example: { foo: ({ bar } = { bar: 1 })} - b.return(rhs) - ]) - ); + const iife_is_async = + is_expression_async(value) || + assignments.some((assignment) => is_expression_async(assignment)); + + return iife_is_async ? b.await(b.call(b.async(iife), value)) : b.call(iife, value); + } - return iife_is_async ? b.await(b.call(b.async(iife))) : b.call(iife); + return sequence; } - if (assignee.type !== 'Identifier' && assignee.type !== 'MemberExpression') { - throw new Error(`Unexpected assignment type ${assignee.type}`); + if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') { + throw new Error(`Unexpected assignment type ${node.left.type}`); } return ( From f1ebccda599dcf35806f461f363ec08e7b82243f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 23:13:49 -0400 Subject: [PATCH 67/86] delete comments --- .../2-analyze/visitors/shared/component.js | 25 ------------------- .../client/visitors/shared/component.js | 6 ----- .../server/visitors/shared/component.js | 19 -------------- 3 files changed, 50 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js index 27e2ea561d9b..b728c7382034 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js @@ -107,29 +107,4 @@ export function visit_component(node, context) { } ); } - - // context.visit({ ...node.fragment, nodes: default_slot_nodes }, default_state); - // context.visit({ ...node.fragment, nodes: named_slot_nodes }, named_state); - - // context.visit(child, is_slotted_content ? named_state : default_state); - - // if ( - // is_element_node(child) && - // child.attributes.some( - // (a) => a.type === 'Attribute' && a.name === 'slot' && is_text_attribute(a) - // ) - // ) { - // context.visit(child); - // } else { - // context.visit(child, { scope }); - // } - - // context.visit(node, attribute.type === 'LetDirective' ? default_state : named_state); - // } - - // context.next({ - // ...context.state, - // parent_element: null, - // component_slots: new Set() - // }); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index aaf2b024bd74..799fcd765827 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -259,12 +259,6 @@ export function build_component(node, component_name, context, anchor = context. scope: node.metadata.scopes[slot_name], transform: { ...context.state.transform } } - // { - // ...context.state, - // scope: - // context.state.scopes.get(slot_name === 'default' ? children[slot_name][0] : node) ?? - // context.state.scope - // } ) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js index ce2eb246ee61..941088228a1a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js @@ -159,26 +159,7 @@ export function build_inline_component(node, expression, context) { ...context.state, scope: node.metadata.scopes[slot_name] } - // { - // ...context.state, - // scope: - // context.state.scopes.get(slot_name === 'default' ? children[slot_name][0] : node) ?? - // context.state.scope - // } ) - // context.visit( - // { - // ...node.fragment, - // // @ts-expect-error - // nodes: children[slot_name] - // }, - // { - // ...context.state, - // scope: - // context.state.scopes.get(slot_name === 'default' ? children[slot_name][0] : node) ?? - // context.state.scope - // } - // ) ); if (block.body.length === 0) continue; From a738c99ecceaa363de5537cfecaa6988ff53e376 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Aug 2024 23:15:00 -0400 Subject: [PATCH 68/86] more --- packages/svelte/src/compiler/phases/scope.js | 86 +------------------- 1 file changed, 1 insertion(+), 85 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 9a04b8761c28..15ed9d219a2a 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1,15 +1,13 @@ /** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */ /** @import { Context, Visitor } from 'zimmerframe' */ -/** @import { AnimateDirective, Attribute, Binding, Component, DeclarationKind, EachBlock, ElementLike, LetDirective, SvelteComponent, SvelteNode, SvelteSelf, TransitionDirective, UseDirective } from '#compiler' */ +/** @import { AnimateDirective, Binding, Component, DeclarationKind, EachBlock, ElementLike, LetDirective, SvelteComponent, SvelteNode, SvelteSelf, TransitionDirective, UseDirective } from '#compiler' */ import is_reference from 'is-reference'; import { walk } from 'zimmerframe'; -import { is_element_node } from './nodes.js'; import * as b from '../utils/builders.js'; import * as e from '../errors.js'; import { extract_identifiers, extract_identifiers_from_destructuring, - is_text_attribute, object, unwrap_pattern } from '../utils/ast.js'; @@ -292,7 +290,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { * @type {Visitor} */ const SvelteFragment = (node, { state, next }) => { - // const [scope] = analyze_let_directives(node, state.scope); const scope = state.scope.child(); scopes.set(node, scope); next({ scope }); @@ -334,89 +331,8 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { context.visit(child, state); } - - // // let:x is super weird: - // // - for the default slot, its scope only applies to children that are not slots themselves - // // - for named slots, its scope applies to the component itself, too - // const [scope, is_default_slot] = analyze_let_directives(node, state.scope); - // if (is_default_slot) { - // for (const attribute of node.attributes) { - // visit(attribute); - // } - // } else { - // scopes.set(node, scope); - - // for (const attribute of node.attributes) { - // visit(attribute, { ...state, scope }); - // } - // } - - // for (const child of node.fragment.nodes) { - // if ( - // is_element_node(child) && - // child.attributes.some( - // (attribute) => attribute.type === 'Attribute' && attribute.name === 'slot' - // ) - // ) { - // //
inherits the scope above the component unless the component is a named slot itself, because slots are hella weird - // scopes.set(child, is_default_slot ? state.scope : scope); - // visit(child, { scope: is_default_slot ? state.scope : scope }); - // } else { - // if (child.type === 'ExpressionTag') { - // // expression tag is a special case — we don't visit it directly, but via process_children, - // // so we need to set the scope on the expression rather than the tag itself - // scopes.set(child.expression, scope); - // } else { - // scopes.set(child, scope); - // } - - // visit(child, { scope }); - // } - // } }; - /** - * @param {ElementLike} node - * @param {Scope} parent - */ - function analyze_let_directives(node, parent) { - const scope = parent.child(); - let is_default_slot = true; - - for (const attribute of node.attributes) { - if (attribute.type === 'LetDirective') { - /** @type {Binding[]} */ - const bindings = []; - scope.declarators.set(attribute, bindings); - - // attach the scope to the directive itself, as well as the - // contents to which it applies - scopes.set(attribute, scope); - - if (attribute.expression) { - for (const id of extract_identifiers_from_destructuring(attribute.expression)) { - const binding = scope.declare(id, 'derived', 'const'); - bindings.push(binding); - } - } else { - /** @type {Identifier} */ - const id = { - name: attribute.name, - type: 'Identifier', - start: attribute.start, - end: attribute.end - }; - const binding = scope.declare(id, 'derived', 'const'); - bindings.push(binding); - } - } else if (attribute.type === 'Attribute' && attribute.name === 'slot') { - is_default_slot = false; - } - } - - return /** @type {const} */ ([scope, is_default_slot]); - } - /** * @type {Visitor} */ From 771dabaac1ec7079e1085befba39aef5a52c4fea Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 13:36:05 -0400 Subject: [PATCH 69/86] unused --- .../src/compiler/phases/2-analyze/visitors/shared/attribute.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/attribute.js index 4102460cbaa2..8f60a67c62b0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/attribute.js @@ -1,4 +1,4 @@ -/** @import { Attribute, ElementLike, SvelteNode } from '#compiler' */ +/** @import { Attribute, ElementLike } from '#compiler' */ /** @import { Context } from '../../types' */ import * as e from '../../../../errors.js'; import { is_text_attribute } from '../../../../utils/ast.js'; From 8f579d5e961dbc82e4e85210360e5d77f80aaa87 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 13:41:30 -0400 Subject: [PATCH 70/86] tidy up --- .../2-analyze/visitors/shared/component.js | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js index b728c7382034..4a2b8b1406c0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js @@ -1,4 +1,4 @@ -/** @import { Component, Fragment, SvelteComponent, SvelteSelf } from '#compiler' */ +/** @import { Comment, Component, Fragment, SvelteComponent, SvelteSelf } from '#compiler' */ /** @import { Context } from '../../types' */ import * as e from '../../../../errors.js'; import { get_attribute_expression, is_expression_attribute } from '../../../../utils/ast.js'; @@ -63,27 +63,25 @@ export function visit_component(node, context) { const component_slots = new Set(); - const slot_scope_applies_to_itself = !!determine_slot(node); - - const default_state = slot_scope_applies_to_itself + // If the component has a slot attribute — `` — + // then `let:` directives apply to other attributes, instead of just the + // top-level contents of the component. Yes, this is very weird. + const default_state = !!determine_slot(node) ? context.state : { ...context.state, - scope: node.metadata.scopes.default, - parent_element: null, - component_slots + scope: node.metadata.scopes.default }; for (const attribute of node.attributes) { context.visit(attribute, attribute.type === 'LetDirective' ? default_state : context.state); } - const comments = []; + /** @type {Comment[]} */ + let comments = []; /** @type {Record} */ - const nodes = { - default: [] - }; + const nodes = { default: [] }; for (const child of node.fragment.nodes) { if (child.type === 'Comment') { @@ -91,9 +89,10 @@ export function visit_component(node, context) { continue; } - let slot_name = determine_slot(child) ?? 'default'; - + const slot_name = determine_slot(child) ?? 'default'; (nodes[slot_name] ??= []).push(...comments, child); + + comments = []; } for (const slot_name in nodes) { From 61ad1298d97480d710369a06da2dee0fda1b502a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 13:42:27 -0400 Subject: [PATCH 71/86] tidy up --- .../2-analyze/visitors/shared/component.js | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js index 4a2b8b1406c0..19895595cf8d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js @@ -61,17 +61,12 @@ export function visit_component(node, context) { } } - const component_slots = new Set(); - // If the component has a slot attribute — `` — // then `let:` directives apply to other attributes, instead of just the // top-level contents of the component. Yes, this is very weird. const default_state = !!determine_slot(node) ? context.state - : { - ...context.state, - scope: node.metadata.scopes.default - }; + : { ...context.state, scope: node.metadata.scopes.default }; for (const attribute of node.attributes) { context.visit(attribute, attribute.type === 'LetDirective' ? default_state : context.state); @@ -95,15 +90,16 @@ export function visit_component(node, context) { comments = []; } + const component_slots = new Set(); + for (const slot_name in nodes) { - context.visit( - { ...node.fragment, nodes: nodes[slot_name] }, - { - ...context.state, - scope: node.metadata.scopes[slot_name], - parent_element: null, - component_slots - } - ); + const state = { + ...context.state, + scope: node.metadata.scopes[slot_name], + parent_element: null, + component_slots + }; + + context.visit({ ...node.fragment, nodes: nodes[slot_name] }, state); } } From 09d714d6923e1a33a48ad3d2563bbf40fa49beb8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 13:48:24 -0400 Subject: [PATCH 72/86] fix --- .../src/compiler/phases/2-analyze/visitors/shared/component.js | 2 +- .../src/compiler/phases/3-transform/client/transform-client.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js index 19895595cf8d..e9d58e5b4747 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js @@ -87,7 +87,7 @@ export function visit_component(node, context) { const slot_name = determine_slot(child) ?? 'default'; (nodes[slot_name] ??= []).push(...comments, child); - comments = []; + if (slot_name !== 'default') comments = []; } const component_slots = new Set(); 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 72ac67e0f5fc..3a6a6707996b 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 @@ -65,7 +65,6 @@ const visitors = { if (scope && scope !== state.scope) { const transform = { ...state.transform }; - // TODO do this in add_state_transformers and visit_function etc? for (const [name, binding] of scope.declarations) { if (binding.kind === 'normal') { delete transform[name]; From bf5fb61b12eda3648f092491970ea03f6e2e3ba9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 13:55:43 -0400 Subject: [PATCH 73/86] move some stuff --- .../3-transform/client/transform-client.js | 29 +-- .../phases/3-transform/client/types.d.ts | 3 + .../3-transform/client/visitors/Program.js | 165 ++++++++++-------- 3 files changed, 98 insertions(+), 99 deletions(-) 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 3a6a6707996b..c4709f565993 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 @@ -142,6 +142,7 @@ export function client_component(analysis, options) { is_instance: false, hoisted: [b.import_all('$', 'svelte/internal/client')], node: /** @type {any} */ (null), // populated by the root node + legacy_reactive_imports: [], legacy_reactive_statements: new Map(), metadata: { context: { @@ -167,32 +168,6 @@ export function client_component(analysis, options) { locations: /** @type {any} */ (null) }; - /** @type {ESTree.Statement[]} */ - const legacy_reactive_imports = []; - - if (!analysis.runes) { - state.transform['$$props'] = { - read: (node) => ({ ...node, name: '$$sanitized_props' }) - }; - - for (const [name, binding] of state.scope.declarations) { - // Very very dirty way of making import statements reactive in legacy mode if needed - // TODO move this into the Program visitor - if (binding.declaration_kind === 'import' && binding.mutated) { - const id = b.id('$$_import_' + name); - - state.transform[name] = { - read: (node) => b.call(id), - mutate: (node, mutation) => { - return b.call(id, mutation); - } - }; - - legacy_reactive_imports.push(b.var(id, b.call('$.reactive_import', b.thunk(b.id(name))))); - } - } - } - const module = /** @type {ESTree.Program} */ ( walk(/** @type {SvelteNode} */ (analysis.module.ast), state, visitors) ); @@ -222,7 +197,7 @@ export function client_component(analysis, options) { ) ); - instance.body.unshift(...legacy_reactive_imports); + instance.body.unshift(...state.legacy_reactive_imports); /** @type {ESTree.Statement[]} */ const store_setup = []; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index b576ddeecaed..ba0c9016d017 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -23,6 +23,9 @@ export interface ClientTransformState extends TransformState { */ readonly in_constructor: boolean; + /** Imports that should be re-evaluated in legacy mode following a mutation */ + readonly legacy_reactive_imports: Statement[]; + /** The $: calls, which will be ordered in the end */ readonly legacy_reactive_statements: Map; readonly transform: Record< diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index 07fb459f3eb5..e1bf60454218 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -9,94 +9,115 @@ import { add_state_transformers } from './shared/declarations.js'; * @param {ComponentContext} context */ export function Program(node, context) { - if (context.state.is_instance) { + if (!context.state.analysis.runes) { + context.state.transform['$$props'] = { + read: (node) => ({ ...node, name: '$$sanitized_props' }) + }; + for (const [name, binding] of context.state.scope.declarations) { - if (binding.kind === 'store_sub') { - const store = /** @type {Expression} */ (context.visit(b.id(name.slice(1)))); + if (binding.declaration_kind === 'import' && binding.mutated) { + const id = b.id('$$_import_' + name); context.state.transform[name] = { - read: b.call, - assign: (node, value) => { - return b.call('$.store_set', store, value); - }, + read: (node) => b.call(id), mutate: (node, mutation) => { - // We need to untrack the store read, for consistency with Svelte 4 - const untracked = b.call('$.untrack', node); + return b.call(id, mutation); + } + }; - /** - * - * @param {Expression} n - * @returns {Expression} - */ - function replace(n) { - if (n.type === 'MemberExpression') { - return { - ...n, - object: replace(/** @type {Expression} */ (n.object)), - property: n.property - }; - } + context.state.legacy_reactive_imports.push( + b.var(id, b.call('$.reactive_import', b.thunk(b.id(name)))) + ); + } + } + } - return untracked; + for (const [name, binding] of context.state.scope.declarations) { + if (binding.kind === 'store_sub') { + const store = /** @type {Expression} */ (context.visit(b.id(name.slice(1)))); + + context.state.transform[name] = { + read: b.call, + assign: (node, value) => { + return b.call('$.store_set', store, value); + }, + mutate: (node, mutation) => { + // We need to untrack the store read, for consistency with Svelte 4 + const untracked = b.call('$.untrack', node); + + /** + * + * @param {Expression} n + * @returns {Expression} + */ + function replace(n) { + if (n.type === 'MemberExpression') { + return { + ...n, + object: replace(/** @type {Expression} */ (n.object)), + property: n.property + }; } - return b.call( - '$.store_mutate', - store, - b.assignment( - mutation.operator, - /** @type {MemberExpression} */ ( - replace(/** @type {MemberExpression} */ (mutation.left)) - ), - mutation.right + return untracked; + } + + return b.call( + '$.store_mutate', + store, + b.assignment( + mutation.operator, + /** @type {MemberExpression} */ ( + replace(/** @type {MemberExpression} */ (mutation.left)) ), - untracked - ); + mutation.right + ), + untracked + ); + }, + update: (node) => { + return b.call( + node.prefix ? '$.update_pre_store' : '$.update_store', + build_getter(b.id(name.slice(1)), context.state), + b.call(node.argument), + node.operator === '--' && b.literal(-1) + ); + } + }; + } + + if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { + if (is_prop_source(binding, context.state)) { + context.state.transform[name] = { + read: b.call, + assign: (node, value) => { + return b.call(node, value); + }, + mutate: (node, value) => { + if (binding.kind === 'bindable_prop') { + // only necessary for interop with legacy parent bindings + return b.call(node, value, b.true); + } + + return value; }, update: (node) => { return b.call( - node.prefix ? '$.update_pre_store' : '$.update_store', - build_getter(b.id(name.slice(1)), context.state), - b.call(node.argument), + node.prefix ? '$.update_pre_prop' : '$.update_prop', + node.argument, node.operator === '--' && b.literal(-1) ); } }; - } - - if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { - if (is_prop_source(binding, context.state)) { - context.state.transform[name] = { - read: b.call, - assign: (node, value) => { - return b.call(node, value); - }, - mutate: (node, value) => { - if (binding.kind === 'bindable_prop') { - // only necessary for interop with legacy parent bindings - return b.call(node, value, b.true); - } - - return value; - }, - update: (node) => { - return b.call( - node.prefix ? '$.update_pre_prop' : '$.update_prop', - node.argument, - node.operator === '--' && b.literal(-1) - ); - } - }; - } else if (binding.prop_alias) { - const key = b.key(binding.prop_alias); - context.state.transform[name] = { - read: (node) => b.member(b.id('$$props'), key, key.type === 'Literal') - }; - } else { - context.state.transform[name] = { - read: (node) => b.member(b.id('$$props'), node) - }; - } + } else if (binding.prop_alias) { + const key = b.key(binding.prop_alias); + context.state.transform[name] = { + read: (node) => b.member(b.id('$$props'), key, key.type === 'Literal') + }; + } else { + context.state.transform[name] = { + read: (node) => b.member(b.id('$$props'), node) + }; } } } From ee666d559fa504d5258979c826535bc54200278f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 13:58:23 -0400 Subject: [PATCH 74/86] tweak --- .../src/compiler/phases/3-transform/client/visitors/Program.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index e1bf60454218..4cb6046b85e5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -111,6 +111,7 @@ export function Program(node, context) { }; } else if (binding.prop_alias) { const key = b.key(binding.prop_alias); + context.state.transform[name] = { read: (node) => b.member(b.id('$$props'), key, key.type === 'Literal') }; From e9fda1ea69a52a9f77e0cd3fe115d098138ca0e8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 14:00:36 -0400 Subject: [PATCH 75/86] tidy up --- .../3-transform/client/visitors/AssignmentExpression.js | 4 +--- .../3-transform/server/visitors/AssignmentExpression.js | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 97a013025ec0..9acf61f9cf67 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -11,9 +11,6 @@ import { build_proxy_reassignment, should_proxy_or_freeze } from '../utils.js'; * @param {Context} context */ export function AssignmentExpression(node, context) { - const parent = /** @type {Node} */ (context.path.at(-1)); - const is_standalone = parent.type.endsWith('Statement'); - if ( node.left.type === 'ArrayPattern' || node.left.type === 'ObjectPattern' || @@ -39,6 +36,7 @@ export function AssignmentExpression(node, context) { return context.next(); } + const is_standalone = /** @type {Node} */ (context.path.at(-1)).type.endsWith('Statement'); const sequence = b.sequence(assignments); if (!is_standalone) { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js index c349f64b3d27..4910b53d97ea 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js @@ -10,9 +10,6 @@ import { build_getter } from './shared/utils.js'; * @param {Context} context */ export function AssignmentExpression(node, context) { - const parent = /** @type {Node} */ (context.path.at(-1)); - const is_standalone = parent.type.endsWith('Statement'); - if ( node.left.type === 'ArrayPattern' || node.left.type === 'ObjectPattern' || @@ -38,6 +35,7 @@ export function AssignmentExpression(node, context) { return context.next(); } + const is_standalone = /** @type {Node} */ (context.path.at(-1)).type.endsWith('Statement'); const sequence = b.sequence(assignments); if (!is_standalone) { From b1eaeaecb70b892163e82902797cf986ac193b50 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 14:09:38 -0400 Subject: [PATCH 76/86] DRY --- .../client/visitors/AssignmentExpression.js | 32 +++++++------------ .../server/visitors/AssignmentExpression.js | 15 +++------ packages/svelte/src/compiler/utils/ast.js | 12 +++++++ 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 9acf61f9cf67..074b3816a9e6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -2,7 +2,11 @@ /** @import { SvelteNode } from '#compiler' */ /** @import { ClientTransformState, Context } from '../types.js' */ import * as b from '../../../../utils/builders.js'; -import { extract_paths, is_expression_async } from '../../../../utils/ast.js'; +import { + build_assignment_value, + extract_paths, + is_expression_async +} from '../../../../utils/ast.js'; import { is_ignored } from '../../../../state.js'; import { build_proxy_reassignment, should_proxy_or_freeze } from '../utils.js'; @@ -87,7 +91,9 @@ export function build_assignment(operator, left, right, context) { const private_state = context.state.private_state.get(left.property.name); if (private_state !== undefined) { - let value = get_assignment_value(operator, left, right, context); + let value = /** @type {Expression} */ ( + context.visit(build_assignment_value(operator, left, right)) + ); let transformed = false; if (should_proxy_or_freeze(value, context.state.scope)) { @@ -143,7 +149,9 @@ export function build_assignment(operator, left, right, context) { // reassignment if (object === left && transform?.assign) { - let value = get_assignment_value(operator, left, right, context); + let value = /** @type {Expression} */ ( + context.visit(build_assignment_value(operator, left, right)) + ); // special case — if an element binding, we know it's a primitive const path = context.path.map((node) => node.type); @@ -180,21 +188,3 @@ export function build_assignment(operator, left, right, context) { ? b.call('$.skip_ownership_validation', b.thunk(mutation)) : mutation; } - -/** - * @template {ClientTransformState} State - * @param {AssignmentOperator} operator - * @param {Pattern} left - * @param {Expression} right - * @param {import('zimmerframe').Context} context - */ -function get_assignment_value(operator, left, right, { visit }) { - return operator === '=' - ? /** @type {Expression} */ (visit(right)) - : // turn something like x += 1 into x = x + 1 - b.binary( - /** @type {BinaryOperator} */ (operator.slice(0, -1)), - /** @type {Expression} */ (visit(left)), - /** @type {Expression} */ (visit(right)) - ); -} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js index 4910b53d97ea..5e0a34eb520c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js @@ -2,7 +2,7 @@ /** @import { SvelteNode } from '#compiler' */ /** @import { Context, ServerTransformState } from '../types.js' */ import * as b from '../../../../utils/builders.js'; -import { extract_paths } from '../../../../utils/ast.js'; +import { build_assignment_value, extract_paths } from '../../../../utils/ast.js'; import { build_getter } from './shared/utils.js'; /** @@ -81,16 +81,9 @@ function build_assignment(operator, left, right, context) { } if (object === left) { - let value = /** @type {Expression} */ (context.visit(right)); - - if (operator !== '=') { - // turn `x += 1` into `x = x + 1` - value = b.binary( - /** @type {BinaryOperator} */ (operator.slice(0, -1)), - build_getter(left, context.state), - value - ); - } + let value = /** @type {Expression} */ ( + context.visit(build_assignment_value(operator, left, right)) + ); return b.call('$.store_set', b.id(name), value); } diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index 95b5d2fb2d7e..47bbdb945fb4 100644 --- a/packages/svelte/src/compiler/utils/ast.js +++ b/packages/svelte/src/compiler/utils/ast.js @@ -563,3 +563,15 @@ export function build_fallback(expression, fallback) { ? b.await(b.call('$.fallback', expression, b.thunk(fallback, true), b.true)) : b.call('$.fallback', expression, b.thunk(fallback), b.true); } + +/** + * @param {ESTree.AssignmentOperator} operator + * @param {ESTree.Identifier | ESTree.MemberExpression} left + * @param {ESTree.Expression} right + */ +export function build_assignment_value(operator, left, right) { + return operator === '=' + ? right + : // turn something like x += 1 into x = x + 1 + b.binary(/** @type {ESTree.BinaryOperator} */ (operator.slice(0, -1)), left, right); +} From e7a390d7ce3366002bb1de5e9c962dd1a8440e76 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 14:15:43 -0400 Subject: [PATCH 77/86] synchronise --- .../3-transform/client/visitors/AssignmentExpression.js | 9 ++++++++- .../3-transform/server/visitors/AssignmentExpression.js | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 074b3816a9e6..11db2d1bcb10 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -32,7 +32,14 @@ export function AssignmentExpression(node, context) { let assignment = build_assignment('=', path.node, value, context); if (assignment !== null) changed = true; - return assignment ?? /** @type {Expression} */ (context.next()); + return ( + assignment ?? + b.assignment( + '=', + /** @type {Pattern} */ (context.visit(path.node)), + /** @type {Expression} */ (context.visit(value)) + ) + ); }); if (!changed) { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js index 5e0a34eb520c..d5a511cdbfd1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js @@ -27,7 +27,14 @@ export function AssignmentExpression(node, context) { let assignment = build_assignment('=', path.node, value, context); if (assignment !== null) changed = true; - return assignment ?? b.assignment('=', path.node, value); + return ( + assignment ?? + b.assignment( + '=', + /** @type {Pattern} */ (context.visit(path.node)), + /** @type {Expression} */ (context.visit(value)) + ) + ); }); if (!changed) { From 7a4354298565c53877010b56c4c321a41978b25e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 14:26:25 -0400 Subject: [PATCH 78/86] DRY out --- .../client/visitors/AssignmentExpression.js | 78 ++----------------- .../server/visitors/AssignmentExpression.js | 56 +------------ .../phases/3-transform/shared/assignments.js | 77 ++++++++++++++++++ 3 files changed, 87 insertions(+), 124 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/shared/assignments.js diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 11db2d1bcb10..b94dc34923f7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -1,90 +1,24 @@ -/** @import { AssignmentExpression, AssignmentOperator, BinaryOperator, Expression, Node, Pattern } from 'estree' */ -/** @import { SvelteNode } from '#compiler' */ -/** @import { ClientTransformState, Context } from '../types.js' */ +/** @import { AssignmentExpression, AssignmentOperator, Expression, Pattern } from 'estree' */ +/** @import { Context } from '../types.js' */ import * as b from '../../../../utils/builders.js'; -import { - build_assignment_value, - extract_paths, - is_expression_async -} from '../../../../utils/ast.js'; +import { build_assignment_value } from '../../../../utils/ast.js'; import { is_ignored } from '../../../../state.js'; import { build_proxy_reassignment, should_proxy_or_freeze } from '../utils.js'; +import { visit_assignment_expression } from '../../shared/assignments.js'; /** * @param {AssignmentExpression} node * @param {Context} context */ export function AssignmentExpression(node, context) { - if ( - node.left.type === 'ArrayPattern' || - node.left.type === 'ObjectPattern' || - node.left.type === 'RestElement' - ) { - const value = /** @type {Expression} */ (context.visit(node.right)); - const should_cache = value.type !== 'Identifier'; - const rhs = should_cache ? b.id('$$value') : value; - - let changed = false; - - const assignments = extract_paths(node.left).map((path) => { - const value = path.expression?.(rhs); - - let assignment = build_assignment('=', path.node, value, context); - if (assignment !== null) changed = true; - - return ( - assignment ?? - b.assignment( - '=', - /** @type {Pattern} */ (context.visit(path.node)), - /** @type {Expression} */ (context.visit(value)) - ) - ); - }); - - if (!changed) { - // No change to output -> nothing to transform -> we can keep the original assignment - return context.next(); - } - - const is_standalone = /** @type {Node} */ (context.path.at(-1)).type.endsWith('Statement'); - const sequence = b.sequence(assignments); - - if (!is_standalone) { - // this is part of an expression, we need the sequence to end with the value - sequence.expressions.push(rhs); - } - - if (should_cache) { - // the right hand side is a complex expression, wrap in an IIFE to cache it - const iife = b.arrow([rhs], sequence); - - const iife_is_async = - is_expression_async(value) || - assignments.some((assignment) => is_expression_async(assignment)); - - return iife_is_async ? b.await(b.call(b.async(iife), value)) : b.call(iife, value); - } - - return sequence; - } - - if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') { - throw new Error(`Unexpected assignment type ${node.left.type}`); - } - - return ( - build_assignment(node.operator, node.left, node.right, context) ?? - /** @type {Expression} */ (context.next()) - ); + return visit_assignment_expression(node, context, build_assignment); } /** - * @template {ClientTransformState} State * @param {AssignmentOperator} operator * @param {Pattern} left * @param {Expression} right - * @param {import('zimmerframe').Context} context + * @param {Context} context * @returns {Expression | null} */ export function build_assignment(operator, left, right, context) { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js index d5a511cdbfd1..e92d54c3b79b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js @@ -1,64 +1,16 @@ -/** @import { AssignmentExpression, AssignmentOperator, BinaryOperator, Expression, Node, Pattern } from 'estree' */ +/** @import { AssignmentExpression, AssignmentOperator, Expression, Pattern } from 'estree' */ /** @import { SvelteNode } from '#compiler' */ /** @import { Context, ServerTransformState } from '../types.js' */ import * as b from '../../../../utils/builders.js'; -import { build_assignment_value, extract_paths } from '../../../../utils/ast.js'; -import { build_getter } from './shared/utils.js'; +import { build_assignment_value } from '../../../../utils/ast.js'; +import { visit_assignment_expression } from '../../shared/assignments.js'; /** * @param {AssignmentExpression} node * @param {Context} context */ export function AssignmentExpression(node, context) { - if ( - node.left.type === 'ArrayPattern' || - node.left.type === 'ObjectPattern' || - node.left.type === 'RestElement' - ) { - const value = /** @type {Expression} */ (context.visit(node.right)); - const should_cache = value.type !== 'Identifier'; - const rhs = should_cache ? b.id('$$value') : value; - - let changed = false; - - const assignments = extract_paths(node.left).map((path) => { - const value = path.expression?.(rhs); - - let assignment = build_assignment('=', path.node, value, context); - if (assignment !== null) changed = true; - - return ( - assignment ?? - b.assignment( - '=', - /** @type {Pattern} */ (context.visit(path.node)), - /** @type {Expression} */ (context.visit(value)) - ) - ); - }); - - if (!changed) { - // No change to output -> nothing to transform -> we can keep the original assignment - return context.next(); - } - - const is_standalone = /** @type {Node} */ (context.path.at(-1)).type.endsWith('Statement'); - const sequence = b.sequence(assignments); - - if (!is_standalone) { - // this is part of an expression, we need the sequence to end with the value - sequence.expressions.push(rhs); - } - - if (should_cache) { - // the right hand side is a complex expression, wrap in an IIFE to cache it - return b.call(b.arrow([rhs], sequence), value); - } - - return sequence; - } - - return build_assignment(node.operator, node.left, node.right, context) || context.next(); + return visit_assignment_expression(node, context, build_assignment); } /** diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js new file mode 100644 index 000000000000..ce7595a06e13 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js @@ -0,0 +1,77 @@ +/** @import { AssignmentExpression, AssignmentOperator, Expression, Node, Pattern } from 'estree' */ +/** @import { Context as ClientContext } from '../client/types.js' */ +/** @import { Context as ServerContext } from '../server/types.js' */ +import { extract_paths, is_expression_async } from '../../../utils/ast.js'; +import * as b from '../../../utils/builders.js'; + +/** + * @template {ClientContext | ServerContext} Context + * @param {AssignmentExpression} node + * @param {Context} context + * @param {(operator: AssignmentOperator, left: Pattern, right: Expression, context: Context) => Expression | null} build_assignment + * @returns + */ +export function visit_assignment_expression(node, context, build_assignment) { + if ( + node.left.type === 'ArrayPattern' || + node.left.type === 'ObjectPattern' || + node.left.type === 'RestElement' + ) { + const value = /** @type {Expression} */ (context.visit(node.right)); + const should_cache = value.type !== 'Identifier'; + const rhs = should_cache ? b.id('$$value') : value; + + let changed = false; + + const assignments = extract_paths(node.left).map((path) => { + const value = path.expression?.(rhs); + + let assignment = build_assignment('=', path.node, value, context); + if (assignment !== null) changed = true; + + return ( + assignment ?? + b.assignment( + '=', + /** @type {Pattern} */ (context.visit(path.node)), + /** @type {Expression} */ (context.visit(value)) + ) + ); + }); + + if (!changed) { + // No change to output -> nothing to transform -> we can keep the original assignment + return context.next(); + } + + const is_standalone = /** @type {Node} */ (context.path.at(-1)).type.endsWith('Statement'); + const sequence = b.sequence(assignments); + + if (!is_standalone) { + // this is part of an expression, we need the sequence to end with the value + sequence.expressions.push(rhs); + } + + if (should_cache) { + // the right hand side is a complex expression, wrap in an IIFE to cache it + const iife = b.arrow([rhs], sequence); + + const iife_is_async = + is_expression_async(value) || + assignments.some((assignment) => is_expression_async(assignment)); + + return iife_is_async ? b.await(b.call(b.async(iife), value)) : b.call(iife, value); + } + + return sequence; + } + + if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') { + throw new Error(`Unexpected assignment type ${node.left.type}`); + } + + return ( + build_assignment(node.operator, node.left, node.right, context) ?? + /** @type {Expression} */ (context.next()) + ); +} From 1a1b099413728e337d31db1e426d52d31750821d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 14:30:15 -0400 Subject: [PATCH 79/86] tidy up --- .../svelte/src/compiler/phases/3-transform/client/utils.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 ce9825ad018f..d5e6420d5d2f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -290,10 +290,7 @@ export function with_loc(target, source) { */ export function create_derived_block_argument(node, context) { if (node.type === 'Identifier') { - context.state.transform[node.name] = { - read: get_value - }; - + context.state.transform[node.name] = { read: get_value }; return { id: node, declarations: null }; } From 115e2855427dbbe0137b2957e051eb1ee4002416 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 14:37:49 -0400 Subject: [PATCH 80/86] tidy up --- .../client/visitors/AssignmentExpression.js | 10 ++++------ .../3-transform/client/visitors/ConstTag.js | 4 +--- .../3-transform/client/visitors/EachBlock.js | 4 +--- .../3-transform/client/visitors/Program.js | 20 +++++++------------ 4 files changed, 13 insertions(+), 25 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index b94dc34923f7..afd2d50ba40c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -32,10 +32,10 @@ export function build_assignment(operator, left, right, context) { const private_state = context.state.private_state.get(left.property.name); if (private_state !== undefined) { + let transformed = false; let value = /** @type {Expression} */ ( context.visit(build_assignment_value(operator, left, right)) ); - let transformed = false; if (should_proxy_or_freeze(value, context.state.scope)) { transformed = true; @@ -45,12 +45,10 @@ export function build_assignment(operator, left, right, context) { : build_proxy_reassignment(value, private_state.id); } - if (context.state.in_constructor) { - if (transformed) { - return b.assignment(operator, /** @type {Pattern} */ (context.visit(left)), value); - } - } else { + if (!context.state.in_constructor) { return b.call('$.set', left, value); + } else if (transformed) { + return b.assignment(operator, /** @type {Pattern} */ (context.visit(left)), value); } } } else if (left.property.type === 'Identifier' && context.state.in_constructor) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index 1a7b2904d920..bfff38338bb2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -25,9 +25,7 @@ export function ConstTag(node, context) { ) ); - context.state.transform[declaration.id.name] = { - read: get_value - }; + context.state.transform[declaration.id.name] = { read: get_value }; // we need to eagerly evaluate the expression in order to hit any // 'Cannot access x before initialization' errors diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 8491850de896..a5b96d64e642 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -222,9 +222,7 @@ export function EachBlock(node, context) { declarations.push(b.stmt(read(b.id(name)))); } - key_state.transform[name] = { - read: () => path.node - }; + delete key_state.transform[name]; } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index 4cb6046b85e5..c222d74d6e16 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -5,10 +5,10 @@ import * as b from '../../../../utils/builders.js'; import { add_state_transformers } from './shared/declarations.js'; /** - * @param {Program} node + * @param {Program} _ * @param {ComponentContext} context */ -export function Program(node, context) { +export function Program(_, context) { if (!context.state.analysis.runes) { context.state.transform['$$props'] = { read: (node) => ({ ...node, name: '$$sanitized_props' }) @@ -19,10 +19,8 @@ export function Program(node, context) { const id = b.id('$$_import_' + name); context.state.transform[name] = { - read: (node) => b.call(id), - mutate: (node, mutation) => { - return b.call(id, mutation); - } + read: (_) => b.call(id), + mutate: (_, mutation) => b.call(id, mutation) }; context.state.legacy_reactive_imports.push( @@ -38,9 +36,7 @@ export function Program(node, context) { context.state.transform[name] = { read: b.call, - assign: (node, value) => { - return b.call('$.store_set', store, value); - }, + assign: (_, value) => b.call('$.store_set', store, value), mutate: (node, mutation) => { // We need to untrack the store read, for consistency with Svelte 4 const untracked = b.call('$.untrack', node); @@ -90,9 +86,7 @@ export function Program(node, context) { if (is_prop_source(binding, context.state)) { context.state.transform[name] = { read: b.call, - assign: (node, value) => { - return b.call(node, value); - }, + assign: (node, value) => b.call(node, value), mutate: (node, value) => { if (binding.kind === 'bindable_prop') { // only necessary for interop with legacy parent bindings @@ -113,7 +107,7 @@ export function Program(node, context) { const key = b.key(binding.prop_alias); context.state.transform[name] = { - read: (node) => b.member(b.id('$$props'), key, key.type === 'Literal') + read: (_) => b.member(b.id('$$props'), key, key.type === 'Literal') }; } else { context.state.transform[name] = { From 97f3bd7aad527978b197ad6c3999dc676189cfa5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 14:48:44 -0400 Subject: [PATCH 81/86] tidy up --- .../phases/3-transform/client/visitors/EachBlock.js | 8 ++------ .../phases/3-transform/client/visitors/SnippetBlock.js | 4 +--- .../3-transform/client/visitors/shared/component.js | 4 +--- packages/svelte/src/compiler/phases/scope.js | 1 - 4 files changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index a5b96d64e642..fa19509c066b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -173,7 +173,7 @@ export function EachBlock(node, context) { if (node.context.type === 'Identifier') { child_state.transform[node.context.name] = { read: getter, - assign: (node, value) => { + assign: (_, value) => { const left = b.member( each_node_meta.array_name ? b.call(each_node_meta.array_name) : collection, index, @@ -182,10 +182,7 @@ export function EachBlock(node, context) { return b.sequence([b.assignment('=', left, value), ...sequence]); }, - mutate: (node, mutation) => { - return b.sequence([mutation, ...sequence]); - } - // TODO update (`item++`) — this was unaccounted for previously + mutate: (_, mutation) => b.sequence([mutation, ...sequence]) }; delete key_state.transform[node.context.name]; @@ -213,7 +210,6 @@ export function EachBlock(node, context) { mutate: (node, mutation) => { return b.sequence([mutation, ...sequence]); } - // TODO update (`item++`) — this was unaccounted for previously }; // we need to eagerly evaluate the expression in order to hit any diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js index 750b1b6a3b0d..b62a58c8d797 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js @@ -36,9 +36,7 @@ export function SnippetBlock(node, context) { right: b.id('$.noop') }); - transform[argument.name] = { - read: b.call - }; + transform[argument.name] = { read: b.call }; continue; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 799fcd765827..44f943ddec43 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -50,13 +50,11 @@ export function build_component(node, component_name, context, anchor = context. */ const binding_initializers = []; - const self_slot = determine_slot(node); - /** * If this component has a slot property, it is a named slot within another component. In this case * the slot scope applies to the component itself, too, and not just its children. */ - let slot_scope_applies_to_itself = !!self_slot; + let slot_scope_applies_to_itself = !!determine_slot(node); /** * Components may have a children prop and also have child nodes. In this case, we assume diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 15ed9d219a2a..17957292a5fe 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -306,7 +306,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { const default_state = !!determine_slot(node) ? context.state : { scope: node.metadata.scopes.default }; - // scopes.set(node, scope); for (const attribute of node.attributes) { if (attribute.type === 'LetDirective') { From 8a665b97e887e7a294278dc44fbe157d4874ee7e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 14:51:04 -0400 Subject: [PATCH 82/86] add test that fails on main --- .../samples/each-blocks-update/_config.js | 27 +++++++++++++++++++ .../samples/each-blocks-update/main.svelte | 9 +++++++ 2 files changed, 36 insertions(+) create mode 100644 packages/svelte/tests/runtime-legacy/samples/each-blocks-update/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/each-blocks-update/main.svelte diff --git a/packages/svelte/tests/runtime-legacy/samples/each-blocks-update/_config.js b/packages/svelte/tests/runtime-legacy/samples/each-blocks-update/_config.js new file mode 100644 index 000000000000..a00605a634a5 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/each-blocks-update/_config.js @@ -0,0 +1,27 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

1, 2, 3

+ `, + + test({ assert, target }) { + let buttons = target.querySelectorAll('button'); + + flushSync(() => buttons[2].click()); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

1, 2, 4

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/each-blocks-update/main.svelte b/packages/svelte/tests/runtime-legacy/samples/each-blocks-update/main.svelte new file mode 100644 index 000000000000..0abded02ff93 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/each-blocks-update/main.svelte @@ -0,0 +1,9 @@ + + +{#each arr as n} + +{/each} + +

{arr.join(', ')}

From b241c6f799362b43ffcb8f4525fa1f114cfb8b5f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 14:59:54 -0400 Subject: [PATCH 83/86] snapshot test --- .../_expected/client/index.svelte.js | 12 ++++++++++++ .../_expected/server/index.svelte.js | 9 +++++++++ .../samples/destructured-assignments/index.svelte.js | 6 ++++++ 3 files changed, 27 insertions(+) create mode 100644 packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/destructured-assignments/index.svelte.js diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js new file mode 100644 index 000000000000..9400b5271840 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js @@ -0,0 +1,12 @@ +/* index.svelte.js generated by Svelte VERSION */ +import * as $ from "svelte/internal/client"; + +let a = $.source(1); +let b = $.source(2); + +export function update(array) { + ( + $.set(a, $.proxy(array[0])), + $.set(b, $.proxy(array[1])) + ); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/server/index.svelte.js new file mode 100644 index 000000000000..846ed4845858 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/server/index.svelte.js @@ -0,0 +1,9 @@ +/* index.svelte.js generated by Svelte VERSION */ +import * as $ from "svelte/internal/server"; + +let a = 1; +let b = 2; + +export function update(array) { + [a, b] = array; +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/index.svelte.js new file mode 100644 index 000000000000..9c0da7558a50 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/index.svelte.js @@ -0,0 +1,6 @@ +let a = $state(1); +let b = $state(2); + +export function update(array) { + [a, b] = array; +} From 46c63adbae979ea39c84d9cd01ee9c88196d338d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 15:00:51 -0400 Subject: [PATCH 84/86] changesets --- .changeset/nine-ants-invite.md | 5 +++++ .changeset/perfect-hairs-matter.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/nine-ants-invite.md create mode 100644 .changeset/perfect-hairs-matter.md diff --git a/.changeset/nine-ants-invite.md b/.changeset/nine-ants-invite.md new file mode 100644 index 000000000000..fdf33c27a9c8 --- /dev/null +++ b/.changeset/nine-ants-invite.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: invalidate signals following ++/-- inside each block diff --git a/.changeset/perfect-hairs-matter.md b/.changeset/perfect-hairs-matter.md new file mode 100644 index 000000000000..8ec02f7507ea --- /dev/null +++ b/.changeset/perfect-hairs-matter.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: better code generation for destructuring assignments From b8b07e0128199c884c42d7f9b461eaac4d87dffc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 15:03:13 -0400 Subject: [PATCH 85/86] lint --- .../phases/3-transform/client/transform-client.js | 1 - .../src/compiler/phases/3-transform/client/types.d.ts | 11 ++++++----- packages/svelte/types/index.d.ts | 11 ++++++++--- 3 files changed, 14 insertions(+), 9 deletions(-) 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 c4709f565993..c4af60f24b8d 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 @@ -639,7 +639,6 @@ export function client_module(analysis, options) { options, scope: analysis.module.scope, scopes: analysis.module.scopes, - legacy_reactive_statements: new Map(), public_state: new Map(), private_state: new Map(), transform: {}, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index ba0c9016d017..cae1a10940ae 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -23,11 +23,6 @@ export interface ClientTransformState extends TransformState { */ readonly in_constructor: boolean; - /** Imports that should be re-evaluated in legacy mode following a mutation */ - readonly legacy_reactive_imports: Statement[]; - - /** The $: calls, which will be ordered in the end */ - readonly legacy_reactive_statements: Map; readonly transform: Record< string, { @@ -84,6 +79,12 @@ export interface ComponentClientTransformState extends ClientTransformState { /** The anchor node for the current context */ readonly node: Identifier; + + /** Imports that should be re-evaluated in legacy mode following a mutation */ + readonly legacy_reactive_imports: Statement[]; + + /** The $: calls, which will be ordered in the end */ + readonly legacy_reactive_statements: Map; } export interface StateField { diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 969f89222da3..df012254f64f 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -920,7 +920,6 @@ declare module 'svelte/compiler' { * - `snippet`: A snippet parameter * - `store_sub`: A $store value * - `legacy_reactive`: A `$:` declaration - * - `legacy_reactive_import`: An imported binding that is mutated inside the component */ kind: | 'normal' @@ -933,8 +932,7 @@ declare module 'svelte/compiler' { | 'each' | 'snippet' | 'store_sub' - | 'legacy_reactive' - | 'legacy_reactive_import'; + | 'legacy_reactive'; declaration_kind: DeclarationKind; /** * What the value was initialized with. @@ -1733,6 +1731,7 @@ declare module 'svelte/compiler' { interface Component extends BaseElement { type: 'Component'; metadata: { + scopes: Record; dynamic: boolean; }; } @@ -1769,6 +1768,9 @@ declare module 'svelte/compiler' { type: 'SvelteComponent'; name: 'svelte:component'; expression: Expression; + metadata: { + scopes: Record; + }; } interface SvelteDocument extends BaseElement { @@ -1814,6 +1816,9 @@ declare module 'svelte/compiler' { interface SvelteSelf extends BaseElement { type: 'SvelteSelf'; name: 'svelte:self'; + metadata: { + scopes: Record; + }; } interface SvelteWindow extends BaseElement { From 576763a16774e92e403de48743bbf8b169cd3f23 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 15:07:42 -0400 Subject: [PATCH 86/86] ugh --- .../src/compiler/phases/2-analyze/visitors/shared/component.js | 2 +- packages/svelte/src/compiler/phases/scope.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js index e9d58e5b4747..e6bb3c067edb 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js @@ -64,7 +64,7 @@ export function visit_component(node, context) { // If the component has a slot attribute — `` — // then `let:` directives apply to other attributes, instead of just the // top-level contents of the component. Yes, this is very weird. - const default_state = !!determine_slot(node) + const default_state = determine_slot(node) ? context.state : { ...context.state, scope: node.metadata.scopes.default }; diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 17957292a5fe..633f326080ba 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -303,7 +303,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { default: context.state.scope.child() }; - const default_state = !!determine_slot(node) + const default_state = determine_slot(node) ? context.state : { scope: node.metadata.scopes.default };