From c93fa1087bc15b1ac581f0e8e677198192ebe5cf Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 21 Mar 2025 23:30:14 -0700 Subject: [PATCH 01/11] init --- .../2-analyze/visitors/CallExpression.js | 3 +- .../3-transform/client/transform-client.js | 2 + .../client/visitors/ObjectExpression.js | 77 +++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 2eac934b332c..2aba9f3c41c8 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -118,7 +118,8 @@ export function CallExpression(node, context) { if ( (parent.type !== 'VariableDeclarator' || get_parent(context.path, -3).type === 'ConstTag') && - !(parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) + !(parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) && + !(parent.type === 'Property' && parent.value === node) ) { e.state_invalid_placement(node, rune); } 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 0bdfbae746d0..8c60596e99af 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 @@ -36,6 +36,7 @@ import { KeyBlock } from './visitors/KeyBlock.js'; import { LabeledStatement } from './visitors/LabeledStatement.js'; import { LetDirective } from './visitors/LetDirective.js'; import { MemberExpression } from './visitors/MemberExpression.js'; +import { ObjectExpression } from './visitors/ObjectExpression.js'; import { OnDirective } from './visitors/OnDirective.js'; import { Program } from './visitors/Program.js'; import { RegularElement } from './visitors/RegularElement.js'; @@ -111,6 +112,7 @@ const visitors = { LabeledStatement, LetDirective, MemberExpression, + ObjectExpression, OnDirective, Program, RegularElement, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js new file mode 100644 index 000000000000..77a3736e431a --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js @@ -0,0 +1,77 @@ +/** @import { ObjectExpression, Property, CallExpression, Expression, SpreadElement } from 'estree' */ +/** @import { Context } from '../types' */ +import * as b from '../../../../utils/builders.js'; +import { get_rune } from '../../../scope.js'; +import { should_proxy } from '../utils.js'; + +/** + * @param {ObjectExpression} node + * @param {Context} context + */ +export function ObjectExpression(node, context) { + let has_runes = false; + /** + * @type {Array<{rune: NonNullable>, property: Property & {value: CallExpression}}>} + */ + let reactive_properties = []; + for (let property of node.properties) { + if (property.type !== 'Property') continue; + const rune = get_rune(property.value, context.state.scope); + if (rune) { + has_runes = true; + reactive_properties.push({ + rune, + property: /**@type {Property & {value: CallExpression}} */ (property) + }); + } + } + if (!has_runes) return; + let body = []; + let sources = new Map(); + let counter = 0; + for (let { rune, property } of reactive_properties) { + const name = context.state.scope.generate(`$$${++counter}`); + const deep = rune !== '$state.raw'; + const call = rune.match(/^\$state/) ? '$.state' : '$.derived'; + /** @type {Expression} */ + let value = /** @type {Expression} */ (context.visit(property.value.arguments[0] ?? b.void0)); + value = deep && should_proxy(value, context.state.scope) ? b.call('$.proxy', value) : value; + sources.set(property, [deep, name]); + body.push(b.let(name, b.call(call, value))); + } + /** @type {(Property | SpreadElement)[]} */ + let properties = []; + for (let property of node.properties) { + if (property.type === 'SpreadElement') { + properties.push(/** @type {SpreadElement} */ (context.visit(property))); + continue; + } + if (sources.has(property)) { + let [deep, name] = sources.get(property); + properties.push( + b.prop( + 'get', + /** @type {Expression} */ (context.visit(/**@type {Expression} */ (property.key))), + b.function(null, [], b.block([b.return(b.call('$.get', b.id(name)))])), + property.computed + ), + b.prop( + 'set', + /** @type {Expression} */ (context.visit(property.key)), + b.function( + null, + [b.id('$$value')], + b.block([ + b.stmt(b.call('$.set', b.id(name), b.id('$$value'), deep ? b.true : undefined)) + ]) + ), + property.computed + ) + ); + } else { + properties.push(/** @type {Property} */ (context.visit(property))); + } + } + body.push(b.return(b.object(properties))); + return b.call(b.arrow([], b.block(body))); +} From 9a6064ef1551a1a492df4d7a74d04ac8c5533737 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 22 Mar 2025 11:23:02 -0700 Subject: [PATCH 02/11] thunkify deriveds in properties --- .../phases/3-transform/client/visitors/ObjectExpression.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js index 77a3736e431a..6a22ae7e3522 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js @@ -35,7 +35,12 @@ export function ObjectExpression(node, context) { const call = rune.match(/^\$state/) ? '$.state' : '$.derived'; /** @type {Expression} */ let value = /** @type {Expression} */ (context.visit(property.value.arguments[0] ?? b.void0)); - value = deep && should_proxy(value, context.state.scope) ? b.call('$.proxy', value) : value; + value = + deep && should_proxy(value, context.state.scope) + ? b.call('$.proxy', value) + : rune === '$derived' + ? b.thunk(value) + : value; sources.set(property, [deep, name]); body.push(b.let(name, b.call(call, value))); } From 2ff4e4f80ac5e03caed2bb8367749baa408ca8e2 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 22 Mar 2025 11:24:28 -0700 Subject: [PATCH 03/11] reformat logic --- .../3-transform/client/visitors/ObjectExpression.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js index 6a22ae7e3522..35f9f86e556a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js @@ -36,10 +36,10 @@ export function ObjectExpression(node, context) { /** @type {Expression} */ let value = /** @type {Expression} */ (context.visit(property.value.arguments[0] ?? b.void0)); value = - deep && should_proxy(value, context.state.scope) - ? b.call('$.proxy', value) - : rune === '$derived' - ? b.thunk(value) + rune === '$derived' + ? b.thunk(value) + : rune !== '$derived.by' && deep && should_proxy(value, context.state.scope) + ? b.call('$.proxy', value) : value; sources.set(property, [deep, name]); body.push(b.let(name, b.call(call, value))); From a46d062b52c76e927244beb007fe18fe36078eb9 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 22 Mar 2025 11:26:44 -0700 Subject: [PATCH 04/11] simplify proxying logic --- .../3-transform/client/visitors/ObjectExpression.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js index 35f9f86e556a..8221a3ede88d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js @@ -38,10 +38,10 @@ export function ObjectExpression(node, context) { value = rune === '$derived' ? b.thunk(value) - : rune !== '$derived.by' && deep && should_proxy(value, context.state.scope) + : rune === '$state' && should_proxy(value, context.state.scope) ? b.call('$.proxy', value) : value; - sources.set(property, [deep, name]); + sources.set(property, [name, rune]); body.push(b.let(name, b.call(call, value))); } /** @type {(Property | SpreadElement)[]} */ @@ -52,7 +52,7 @@ export function ObjectExpression(node, context) { continue; } if (sources.has(property)) { - let [deep, name] = sources.get(property); + let [name, rune] = sources.get(property); properties.push( b.prop( 'get', @@ -67,7 +67,9 @@ export function ObjectExpression(node, context) { null, [b.id('$$value')], b.block([ - b.stmt(b.call('$.set', b.id(name), b.id('$$value'), deep ? b.true : undefined)) + b.stmt( + b.call('$.set', b.id(name), b.id('$$value'), rune === '$state' ? b.true : undefined) + ) ]) ), property.computed From ffab0a3e4d7c24ef05f387e5f0e7c56391b6d5c9 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 22 Mar 2025 11:36:22 -0700 Subject: [PATCH 05/11] changeset --- .changeset/sweet-brooms-wonder.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sweet-brooms-wonder.md diff --git a/.changeset/sweet-brooms-wonder.md b/.changeset/sweet-brooms-wonder.md new file mode 100644 index 000000000000..557ce89e9ed0 --- /dev/null +++ b/.changeset/sweet-brooms-wonder.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: allow runes in POJO properties From f2ce6f49971bc9d9bb2bcded910f794bfad8c6ba Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 22 Mar 2025 11:57:26 -0700 Subject: [PATCH 06/11] transform `this` references, fix failing tests --- .../client/visitors/ObjectExpression.js | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js index 8221a3ede88d..61e2008390bd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js @@ -3,6 +3,7 @@ import * as b from '../../../../utils/builders.js'; import { get_rune } from '../../../scope.js'; import { should_proxy } from '../utils.js'; +import { walk } from 'zimmerframe'; /** * @param {ObjectExpression} node @@ -25,16 +26,39 @@ export function ObjectExpression(node, context) { }); } } - if (!has_runes) return; + if (!has_runes) { + context.next(); + return; + } let body = []; let sources = new Map(); + let has_this_reference = false; let counter = 0; + let to_push = []; for (let { rune, property } of reactive_properties) { const name = context.state.scope.generate(`$$${++counter}`); - const deep = rune !== '$state.raw'; const call = rune.match(/^\$state/) ? '$.state' : '$.derived'; /** @type {Expression} */ let value = /** @type {Expression} */ (context.visit(property.value.arguments[0] ?? b.void0)); + value = walk(value, null, { + FunctionExpression() { + return; + }, + //@ts-ignore + FunctionDeclaration() { + return; + }, + ObjectExpression() { + return; + }, + ThisExpression() { + has_this_reference = true; + return b.id('$$object'); + }, + ClassBody() { + return; + } + }); value = rune === '$derived' ? b.thunk(value) @@ -42,7 +66,7 @@ export function ObjectExpression(node, context) { ? b.call('$.proxy', value) : value; sources.set(property, [name, rune]); - body.push(b.let(name, b.call(call, value))); + to_push.push(b.let(name, b.call(call, value))); } /** @type {(Property | SpreadElement)[]} */ let properties = []; @@ -79,6 +103,13 @@ export function ObjectExpression(node, context) { properties.push(/** @type {Property} */ (context.visit(property))); } } - body.push(b.return(b.object(properties))); + if (has_this_reference) { + body.push(b.let('$$object', b.object(properties))); + body.push(...to_push); + body.push(b.return(b.id('$$object'))); + } else { + body.push(...to_push); + body.push(b.return(b.object(properties))); + } return b.call(b.arrow([], b.block(body))); } From b82e182f8fa3f10656a9a4e10fb341e548ed5ad0 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 22 Mar 2025 12:02:36 -0700 Subject: [PATCH 07/11] more fixes --- .../phases/3-transform/client/visitors/ObjectExpression.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js index 61e2008390bd..42b931d2c6a5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js @@ -15,10 +15,11 @@ export function ObjectExpression(node, context) { * @type {Array<{rune: NonNullable>, property: Property & {value: CallExpression}}>} */ let reactive_properties = []; + let valid_property_runes = ['$state', '$derived', '$state.raw', '$derived.by']; for (let property of node.properties) { if (property.type !== 'Property') continue; const rune = get_rune(property.value, context.state.scope); - if (rune) { + if (rune && valid_property_runes.includes(rune)) { has_runes = true; reactive_properties.push({ rune, From 9799b896f3cf9c59daef3c170eb6c1475785634b Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 23 Mar 2025 15:06:54 -0700 Subject: [PATCH 08/11] either fixed it or broke it, not sure yet --- .../phases/3-transform/client/utils.js | 9 ++ .../client/visitors/ObjectExpression.js | 138 ++++++++++++++++-- 2 files changed, 133 insertions(+), 14 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 28e3fabb1990..c10c61711e7c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -258,6 +258,15 @@ export function should_proxy(node, scope) { } } + if (node.type === 'ObjectExpression' && scope !== null) { + for (let property of node.properties) { + if (property.type === 'Property') { + // if there are any getters/setters, return false + if (property.kind !== 'init') return false; + } + } + } + return true; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js index 42b931d2c6a5..3bbebd151595 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js @@ -1,4 +1,4 @@ -/** @import { ObjectExpression, Property, CallExpression, Expression, SpreadElement } from 'estree' */ +/** @import { ObjectExpression, Property, CallExpression, Expression, SpreadElement, Node, Identifier, PrivateIdentifier, Statement } from 'estree' */ /** @import { Context } from '../types' */ import * as b from '../../../../utils/builders.js'; import { get_rune } from '../../../scope.js'; @@ -10,12 +10,15 @@ import { walk } from 'zimmerframe'; * @param {Context} context */ export function ObjectExpression(node, context) { + /** + * @typedef {[string, NonNullable>, '$.state' | '$.derived', Expression, boolean]} ReactiveProperty + */ let has_runes = false; /** * @type {Array<{rune: NonNullable>, property: Property & {value: CallExpression}}>} */ - let reactive_properties = []; - let valid_property_runes = ['$state', '$derived', '$state.raw', '$derived.by']; + const reactive_properties = []; + const valid_property_runes = ['$state', '$derived', '$state.raw', '$derived.by']; for (let property of node.properties) { if (property.type !== 'Property') continue; const rune = get_rune(property.value, context.state.scope); @@ -31,14 +34,61 @@ export function ObjectExpression(node, context) { context.next(); return; } - let body = []; - let sources = new Map(); + /** @type {Statement[]} */ + const body = []; + /** @type {Map} */ + const sources = new Map(); let has_this_reference = false; let counter = 0; - let to_push = []; + /** @type {Statement[]} */ + const before = []; + /** @type {Statement[]} */ + const after = []; + /** @type {string[]} */ + const declarations = []; + /** @type {Map} */ + const initial_declarations = new Map(); + // if a computed property is accessed, we treat it as if all of the object's properties have been accessed + let all_are_referenced = false; + /** @type {Set} */ + const is_referenced = new Set(); + for (let property of node.properties) { + walk(property, null, { + //@ts-ignore + FunctionExpression() { + return; + }, + //@ts-ignore + FunctionDeclaration() { + return; + }, + ObjectExpression() { + return; + }, + /** + * + * @param {Node} node + * @param {import('zimmerframe').Context} context + */ + ThisExpression(node, context) { + const parent = context.path.at(-1); + if (parent?.type === 'MemberExpression') { + if (parent.computed) { + all_are_referenced = true; + } else { + is_referenced.add(/** @type {Identifier | PrivateIdentifier} */ (parent.property).name); + } + } + }, + ClassBody() { + return; + } + }); + } for (let { rune, property } of reactive_properties) { const name = context.state.scope.generate(`$$${++counter}`); const call = rune.match(/^\$state/) ? '$.state' : '$.derived'; + let references_this = false; /** @type {Expression} */ let value = /** @type {Expression} */ (context.visit(property.value.arguments[0] ?? b.void0)); value = walk(value, null, { @@ -54,6 +104,7 @@ export function ObjectExpression(node, context) { }, ThisExpression() { has_this_reference = true; + references_this = true; return b.id('$$object'); }, ClassBody() { @@ -66,23 +117,76 @@ export function ObjectExpression(node, context) { : rune === '$state' && should_proxy(value, context.state.scope) ? b.call('$.proxy', value) : value; - sources.set(property, [name, rune]); - to_push.push(b.let(name, b.call(call, value))); + let key = property.computed + ? Symbol() + : property.key.type === 'Literal' + ? property.key.value + : /** @type {Identifier} */ (property.key).name; + if (rune.match(/^\$state/) && !(all_are_referenced || is_referenced.has(key))) { + let should_be_declared = false; + walk(value, null, { + CallExpression(node, context) { + should_be_declared = true; + context.stop(); + }, + MemberExpression(node, context) { + should_be_declared = true; + context.stop(); + } + }); + if (should_be_declared) { + const value_name = context.state.scope.generate('$$initial'); + initial_declarations.set(value_name, value); + value = b.id(value_name); + } + } + /** @type {ReactiveProperty} */ + const source = [ + name, + rune, + call, + value, + (value.type === 'Identifier' && initial_declarations.has(value.name)) || references_this + ]; + sources.set(property, source); + if (references_this) { + declarations.push(name); + } else if (source[4]) { + before.push(b.let(name, value)); + } else { + before.push(b.let(name, b.call(call, value))); + } + } + if (declarations.length) { + before.push( + b.declaration( + 'let', + declarations.map((name) => b.declarator(name)) + ) + ); + } + for (let [name, value] of initial_declarations) { + after.push(b.let(name, value)); } /** @type {(Property | SpreadElement)[]} */ - let properties = []; + const properties = []; for (let property of node.properties) { if (property.type === 'SpreadElement') { properties.push(/** @type {SpreadElement} */ (context.visit(property))); continue; } if (sources.has(property)) { - let [name, rune] = sources.get(property); + const [name, rune, call, value, initially_declared] = /** @type {ReactiveProperty} */ ( + sources.get(property) + ); + let maybe_assign = initially_declared + ? b.assignment('??=', b.id(name), b.call(call, value)) + : b.id(name); properties.push( b.prop( 'get', /** @type {Expression} */ (context.visit(/**@type {Expression} */ (property.key))), - b.function(null, [], b.block([b.return(b.call('$.get', b.id(name)))])), + b.function(null, [], b.block([b.return(b.call('$.get', maybe_assign))])), property.computed ), b.prop( @@ -93,7 +197,12 @@ export function ObjectExpression(node, context) { [b.id('$$value')], b.block([ b.stmt( - b.call('$.set', b.id(name), b.id('$$value'), rune === '$state' ? b.true : undefined) + b.call( + '$.set', + maybe_assign, + b.id('$$value'), + rune === '$state' ? b.true : undefined + ) ) ]) ), @@ -105,11 +214,12 @@ export function ObjectExpression(node, context) { } } if (has_this_reference) { + body.push(...before); body.push(b.let('$$object', b.object(properties))); - body.push(...to_push); + body.push(...after); body.push(b.return(b.id('$$object'))); } else { - body.push(...to_push); + body.push(...before, ...after); body.push(b.return(b.object(properties))); } return b.call(b.arrow([], b.block(body))); From 6140fdea227b24b8142f48bc5af1ef1b748a18b3 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 23 Mar 2025 15:12:09 -0700 Subject: [PATCH 09/11] revert `should_proxy` change --- .../src/compiler/phases/3-transform/client/utils.js | 9 --------- 1 file changed, 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 c10c61711e7c..28e3fabb1990 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -258,15 +258,6 @@ export function should_proxy(node, scope) { } } - if (node.type === 'ObjectExpression' && scope !== null) { - for (let property of node.properties) { - if (property.type === 'Property') { - // if there are any getters/setters, return false - if (property.kind !== 'init') return false; - } - } - } - return true; } From 91b7d1df6580a57f61e535d17aa6d62609a73e38 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 23 Mar 2025 15:49:26 -0700 Subject: [PATCH 10/11] I was stupid and forgot how `this` works in POJOs, disregard my last few commits --- .../client/visitors/ObjectExpression.js | 194 +++--------------- 1 file changed, 27 insertions(+), 167 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js index 3bbebd151595..20e4f8b2e69a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js @@ -1,9 +1,8 @@ -/** @import { ObjectExpression, Property, CallExpression, Expression, SpreadElement, Node, Identifier, PrivateIdentifier, Statement } from 'estree' */ +/** @import { ObjectExpression, Property, CallExpression, Expression, SpreadElement, Statement } from 'estree' */ /** @import { Context } from '../types' */ import * as b from '../../../../utils/builders.js'; import { get_rune } from '../../../scope.js'; import { should_proxy } from '../utils.js'; -import { walk } from 'zimmerframe'; /** * @param {ObjectExpression} node @@ -11,163 +10,42 @@ import { walk } from 'zimmerframe'; */ export function ObjectExpression(node, context) { /** - * @typedef {[string, NonNullable>, '$.state' | '$.derived', Expression, boolean]} ReactiveProperty + * @typedef {[string, NonNullable>]} ReactiveProperty */ let has_runes = false; - /** - * @type {Array<{rune: NonNullable>, property: Property & {value: CallExpression}}>} - */ - const reactive_properties = []; const valid_property_runes = ['$state', '$derived', '$state.raw', '$derived.by']; + /** @type {Statement[]} */ + const body = []; + /** @type {Map} */ + const sources = new Map(); + let counter = 0; for (let property of node.properties) { if (property.type !== 'Property') continue; const rune = get_rune(property.value, context.state.scope); if (rune && valid_property_runes.includes(rune)) { has_runes = true; - reactive_properties.push({ - rune, - property: /**@type {Property & {value: CallExpression}} */ (property) - }); + const name = context.state.scope.generate(`$$${++counter}`); + const call = rune.match(/^\$state/) ? '$.state' : '$.derived'; + /** @type {Expression} */ + let value = /** @type {Expression} */ ( + context.visit(/** @type {CallExpression} */ (property.value).arguments[0] ?? b.void0) + ); + value = + rune === '$derived' + ? b.thunk(value) + : rune === '$state' && should_proxy(value, context.state.scope) + ? b.call('$.proxy', value) + : value; + /** @type {ReactiveProperty} */ + const source = [name, rune]; + sources.set(property, source); + body.push(b.let(name, b.call(call, value))); } } if (!has_runes) { context.next(); return; } - /** @type {Statement[]} */ - const body = []; - /** @type {Map} */ - const sources = new Map(); - let has_this_reference = false; - let counter = 0; - /** @type {Statement[]} */ - const before = []; - /** @type {Statement[]} */ - const after = []; - /** @type {string[]} */ - const declarations = []; - /** @type {Map} */ - const initial_declarations = new Map(); - // if a computed property is accessed, we treat it as if all of the object's properties have been accessed - let all_are_referenced = false; - /** @type {Set} */ - const is_referenced = new Set(); - for (let property of node.properties) { - walk(property, null, { - //@ts-ignore - FunctionExpression() { - return; - }, - //@ts-ignore - FunctionDeclaration() { - return; - }, - ObjectExpression() { - return; - }, - /** - * - * @param {Node} node - * @param {import('zimmerframe').Context} context - */ - ThisExpression(node, context) { - const parent = context.path.at(-1); - if (parent?.type === 'MemberExpression') { - if (parent.computed) { - all_are_referenced = true; - } else { - is_referenced.add(/** @type {Identifier | PrivateIdentifier} */ (parent.property).name); - } - } - }, - ClassBody() { - return; - } - }); - } - for (let { rune, property } of reactive_properties) { - const name = context.state.scope.generate(`$$${++counter}`); - const call = rune.match(/^\$state/) ? '$.state' : '$.derived'; - let references_this = false; - /** @type {Expression} */ - let value = /** @type {Expression} */ (context.visit(property.value.arguments[0] ?? b.void0)); - value = walk(value, null, { - FunctionExpression() { - return; - }, - //@ts-ignore - FunctionDeclaration() { - return; - }, - ObjectExpression() { - return; - }, - ThisExpression() { - has_this_reference = true; - references_this = true; - return b.id('$$object'); - }, - ClassBody() { - return; - } - }); - value = - rune === '$derived' - ? b.thunk(value) - : rune === '$state' && should_proxy(value, context.state.scope) - ? b.call('$.proxy', value) - : value; - let key = property.computed - ? Symbol() - : property.key.type === 'Literal' - ? property.key.value - : /** @type {Identifier} */ (property.key).name; - if (rune.match(/^\$state/) && !(all_are_referenced || is_referenced.has(key))) { - let should_be_declared = false; - walk(value, null, { - CallExpression(node, context) { - should_be_declared = true; - context.stop(); - }, - MemberExpression(node, context) { - should_be_declared = true; - context.stop(); - } - }); - if (should_be_declared) { - const value_name = context.state.scope.generate('$$initial'); - initial_declarations.set(value_name, value); - value = b.id(value_name); - } - } - /** @type {ReactiveProperty} */ - const source = [ - name, - rune, - call, - value, - (value.type === 'Identifier' && initial_declarations.has(value.name)) || references_this - ]; - sources.set(property, source); - if (references_this) { - declarations.push(name); - } else if (source[4]) { - before.push(b.let(name, value)); - } else { - before.push(b.let(name, b.call(call, value))); - } - } - if (declarations.length) { - before.push( - b.declaration( - 'let', - declarations.map((name) => b.declarator(name)) - ) - ); - } - for (let [name, value] of initial_declarations) { - after.push(b.let(name, value)); - } /** @type {(Property | SpreadElement)[]} */ const properties = []; for (let property of node.properties) { @@ -176,17 +54,12 @@ export function ObjectExpression(node, context) { continue; } if (sources.has(property)) { - const [name, rune, call, value, initially_declared] = /** @type {ReactiveProperty} */ ( - sources.get(property) - ); - let maybe_assign = initially_declared - ? b.assignment('??=', b.id(name), b.call(call, value)) - : b.id(name); + const [name, rune] = /** @type {ReactiveProperty} */ (sources.get(property)); properties.push( b.prop( 'get', /** @type {Expression} */ (context.visit(/**@type {Expression} */ (property.key))), - b.function(null, [], b.block([b.return(b.call('$.get', maybe_assign))])), + b.function(null, [], b.block([b.return(b.call('$.get', b.id(name)))])), property.computed ), b.prop( @@ -197,12 +70,7 @@ export function ObjectExpression(node, context) { [b.id('$$value')], b.block([ b.stmt( - b.call( - '$.set', - maybe_assign, - b.id('$$value'), - rune === '$state' ? b.true : undefined - ) + b.call('$.set', b.id(name), b.id('$$value'), rune === '$state' ? b.true : undefined) ) ]) ), @@ -213,14 +81,6 @@ export function ObjectExpression(node, context) { properties.push(/** @type {Property} */ (context.visit(property))); } } - if (has_this_reference) { - body.push(...before); - body.push(b.let('$$object', b.object(properties))); - body.push(...after); - body.push(b.return(b.id('$$object'))); - } else { - body.push(...before, ...after); - body.push(b.return(b.object(properties))); - } + body.push(b.return(b.object(properties))); return b.call(b.arrow([], b.block(body))); } From 1b0918c31c7dceead1fdf493222b0cdd06c2e22c Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 23 Mar 2025 16:38:49 -0700 Subject: [PATCH 11/11] refactor logic to only use one loop --- .../client/visitors/ObjectExpression.js | 41 +++++++------------ 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js index 20e4f8b2e69a..d2d798d8a816 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ObjectExpression.js @@ -9,18 +9,18 @@ import { should_proxy } from '../utils.js'; * @param {Context} context */ export function ObjectExpression(node, context) { - /** - * @typedef {[string, NonNullable>]} ReactiveProperty - */ let has_runes = false; const valid_property_runes = ['$state', '$derived', '$state.raw', '$derived.by']; /** @type {Statement[]} */ const body = []; - /** @type {Map} */ - const sources = new Map(); let counter = 0; + /** @type {(Property | SpreadElement)[]} */ + const properties = []; for (let property of node.properties) { - if (property.type !== 'Property') continue; + if (property.type !== 'Property') { + properties.push(/** @type {SpreadElement} */ (context.visit(property))); + continue; + } const rune = get_rune(property.value, context.state.scope); if (rune && valid_property_runes.includes(rune)) { has_runes = true; @@ -30,41 +30,23 @@ export function ObjectExpression(node, context) { let value = /** @type {Expression} */ ( context.visit(/** @type {CallExpression} */ (property.value).arguments[0] ?? b.void0) ); + const key = /** @type {Expression} */ (context.visit(property.key)); value = rune === '$derived' ? b.thunk(value) : rune === '$state' && should_proxy(value, context.state.scope) ? b.call('$.proxy', value) : value; - /** @type {ReactiveProperty} */ - const source = [name, rune]; - sources.set(property, source); - body.push(b.let(name, b.call(call, value))); - } - } - if (!has_runes) { - context.next(); - return; - } - /** @type {(Property | SpreadElement)[]} */ - const properties = []; - for (let property of node.properties) { - if (property.type === 'SpreadElement') { - properties.push(/** @type {SpreadElement} */ (context.visit(property))); - continue; - } - if (sources.has(property)) { - const [name, rune] = /** @type {ReactiveProperty} */ (sources.get(property)); properties.push( b.prop( 'get', - /** @type {Expression} */ (context.visit(/**@type {Expression} */ (property.key))), + key, b.function(null, [], b.block([b.return(b.call('$.get', b.id(name)))])), property.computed ), b.prop( 'set', - /** @type {Expression} */ (context.visit(property.key)), + key, b.function( null, [b.id('$$value')], @@ -77,10 +59,15 @@ export function ObjectExpression(node, context) { property.computed ) ); + body.push(b.let(name, b.call(call, value))); } else { properties.push(/** @type {Property} */ (context.visit(property))); } } + if (!has_runes) { + context.next(); + return; + } body.push(b.return(b.object(properties))); return b.call(b.arrow([], b.block(body))); }