From 2edb05172adb56a68964233e578337093e8a3233 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:06:23 -0700 Subject: [PATCH 01/15] init --- .changeset/fuzzy-wings-sin.md | 5 + .../src/compiler/phases/2-analyze/types.d.ts | 4 +- .../phases/2-analyze/visitors/ClassBody.js | 20 ++-- .../3-transform/client/transform-client.js | 8 +- .../3-transform/client/visitors/ClassBody.js | 103 +++++++++++++++--- .../client/visitors/ClassDeclaration.js | 32 ++++++ .../client/visitors/ClassExpression.js | 31 ++++++ .../3-transform/server/transform-server.js | 10 +- .../3-transform/server/visitors/ClassBody.js | 96 +++++++++++++--- .../server/visitors/ClassDeclaration.js | 32 ++++++ .../server/visitors/ClassExpression.js | 31 ++++++ .../compiler/phases/3-transform/types.d.ts | 6 +- .../svelte/src/compiler/phases/types.d.ts | 4 +- packages/svelte/src/compiler/types/index.d.ts | 4 + .../svelte/src/compiler/utils/builders.js | 34 +++++- 15 files changed, 375 insertions(+), 45 deletions(-) create mode 100644 .changeset/fuzzy-wings-sin.md create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassDeclaration.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassExpression.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassDeclaration.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassExpression.js diff --git a/.changeset/fuzzy-wings-sin.md b/.changeset/fuzzy-wings-sin.md new file mode 100644 index 0000000000..fb236c9552 --- /dev/null +++ b/.changeset/fuzzy-wings-sin.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: allow `$state` in computed class fields diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index ae9c5911f6..a1c66c4052 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -1,6 +1,6 @@ import type { Scope } from '../scope.js'; import type { ComponentAnalysis, ReactiveStatement } from '../types.js'; -import type { AST, ExpressionMetadata, StateField, ValidatedCompileOptions } from '#compiler'; +import type { AST, ExpressionMetadata, StateFields, ValidatedCompileOptions } from '#compiler'; export interface AnalysisState { scope: Scope; @@ -21,7 +21,7 @@ export interface AnalysisState { expression: ExpressionMetadata | null; /** Used to analyze class state */ - state_fields: Map; + state_fields: StateFields; function_depth: number; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js index dd21637174..8b3638ac97 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js @@ -30,7 +30,7 @@ export function ClassBody(node, context) { } } - /** @type {Map} */ + /** @type {Map} */ const state_fields = new Map(); /** @type {Map>} */ @@ -41,19 +41,24 @@ export function ClassBody(node, context) { /** @type {MethodDefinition | null} */ let constructor = null; + function increment_computed() { + const numbered_keys = [...state_fields.keys()].filter((key) => typeof key === 'number'); + return numbered_keys.length; + } /** * @param {PropertyDefinition | AssignmentExpression} node * @param {Expression | PrivateIdentifier} key * @param {Expression | null | undefined} value + * @param {boolean} [computed] */ - function handle(node, key, value) { - const name = get_name(key); + function handle(node, key, value, computed = false) { + const name = computed ? increment_computed() : get_name(key); if (name === null) return; const rune = get_rune(value, context.state.scope); if (rune && is_state_creation_rune(rune)) { - if (state_fields.has(name)) { + if (typeof name === 'string' && state_fields.has(name)) { e.state_field_duplicate(node, name); } @@ -70,7 +75,8 @@ export function ClassBody(node, context) { type: rune, // @ts-expect-error for public state this is filled out in a moment key: key.type === 'PrivateIdentifier' ? key : null, - value: /** @type {CallExpression} */ (value) + value: /** @type {CallExpression} */ (value), + computed_key: computed ? /** @type {Expression} */ (key) : null }); } } @@ -139,11 +145,11 @@ export function ClassBody(node, context) { } for (const [name, field] of state_fields) { - if (name[0] === '#') { + if (typeof name === 'string' && name[0] === '#') { continue; } - let deconflicted = name.replace(regex_invalid_identifier_chars, '_'); + let deconflicted = `${name}`.replace(regex_invalid_identifier_chars, '_'); while (private_ids.includes(deconflicted)) { deconflicted = '_' + deconflicted; } 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 2629379f63..51707f4acc 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 @@ -19,6 +19,8 @@ import { BlockStatement } from './visitors/BlockStatement.js'; import { BreakStatement } from './visitors/BreakStatement.js'; import { CallExpression } from './visitors/CallExpression.js'; import { ClassBody } from './visitors/ClassBody.js'; +import { ClassDeclaration } from './visitors/ClassDeclaration.js'; +import { ClassExpression } from './visitors/ClassExpression.js'; import { Comment } from './visitors/Comment.js'; import { Component } from './visitors/Component.js'; import { ConstTag } from './visitors/ConstTag.js'; @@ -97,6 +99,8 @@ const visitors = { BreakStatement, CallExpression, ClassBody, + ClassDeclaration, + ClassExpression, Comment, Component, ConstTag, @@ -168,6 +172,7 @@ export function client_component(analysis, options) { in_constructor: false, instance_level_snippets: [], module_level_snippets: [], + computed_field_declarations: null, // these are set inside the `Fragment` visitor, and cannot be used until then init: /** @type {any} */ (null), @@ -711,7 +716,8 @@ export function client_module(analysis, options) { state_fields: new Map(), transform: {}, in_constructor: false, - is_instance: false + is_instance: false, + computed_field_declarations: null }; const module = /** @type {ESTree.Program} */ ( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js index 5bd9add2a5..5f6e48dc11 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js @@ -1,4 +1,4 @@ -/** @import { CallExpression, ClassBody, ClassDeclaration, ClassExpression, MethodDefinition, PropertyDefinition, StaticBlock } from 'estree' */ +/** @import { CallExpression, ClassBody, ClassDeclaration, ClassExpression, Expression, MethodDefinition, PropertyDefinition, StaticBlock, VariableDeclaration } from 'estree' */ /** @import { StateField } from '#compiler' */ /** @import { Context } from '../types' */ import * as b from '#compiler/builders'; @@ -23,33 +23,68 @@ export function ClassBody(node, context) { const body = []; const child_state = { ...context.state, state_fields }; + const computed_field_declarations = /** @type {VariableDeclaration[]} */ ( + context.state.computed_field_declarations + ); + // insert backing fields for stuff declared in the constructor for (const [name, field] of state_fields) { - if (name[0] === '#') { + if ( + (typeof name === 'string' && name[0] === '#') || + field.node.type !== 'AssignmentExpression' + ) { continue; } - // insert backing fields for stuff declared in the constructor - if (field.node.type === 'AssignmentExpression') { + if (typeof name === 'number' && field.computed_key) { + const key = context.state.scope.generate(`key_${name}`); + computed_field_declarations.push(b.let(key)); const member = b.member(b.this, field.key); const should_proxy = field.type === '$state' && true; // TODO - const key = b.key(name); - body.push( b.prop_def(field.key, null), - - b.method('get', key, [], [b.return(b.call('$.get', member))]), - + b.method( + 'get', + b.assignment( + '=', + b.id(key), + /** @type {Expression} */ (context.visit(field.computed_key)) + ), + [], + [b.return(b.call('$.get', member))], + true + ), b.method( 'set', - key, + b.id(key), [b.id('value')], - [b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))] + [b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))], + true ) ); + continue; } + + const member = b.member(b.this, field.key); + + const should_proxy = field.type === '$state' && true; // TODO + + const key = b.key(/** @type {string} */ (name)); + + body.push( + b.prop_def(field.key, null), + + b.method('get', key, [], [b.return(b.call('$.get', member))]), + + b.method( + 'set', + key, + [b.id('value')], + [b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))] + ) + ); } const declaration = /** @type {ClassDeclaration | ClassExpression} */ ( @@ -65,15 +100,17 @@ export function ClassBody(node, context) { continue; } - const name = get_name(definition.key); - const field = name && /** @type {StateField} */ (state_fields.get(name)); + const name = definition.computed + ? [...state_fields.entries()].find(([, field]) => field.node === definition)?.[0] ?? null + : get_name(definition.key); + const field = name !== null && /** @type {StateField} */ (state_fields.get(name)); if (!field) { body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state))); continue; } - if (name[0] === '#') { + if (typeof name === 'string' && name[0] === '#') { let value = definition.value ? /** @type {CallExpression} */ (context.visit(definition.value, child_state)) : undefined; @@ -83,7 +120,7 @@ export function ClassBody(node, context) { } body.push(b.prop_def(definition.key, value)); - } else if (field.node === definition) { + } else if (field.node === definition && typeof name === 'string') { let call = /** @type {CallExpression} */ (context.visit(field.value, child_state)); if (dev) { @@ -104,6 +141,42 @@ export function ClassBody(node, context) { [b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))] ) ); + } else if (field.computed_key) { + let call = /** @type {CallExpression} */ (context.visit(field.value, child_state)); + if (dev) { + call = b.call( + '$.tag', + call, + b.literal(`${declaration.id?.name ?? '[class]'}[computed key]`) + ); + } + const key = context.state.scope.generate(`key_${name}`); + computed_field_declarations.push(b.let(key)); + const member = b.member(b.this, field.key); + + const should_proxy = field.type === '$state' && true; // TODO + + body.push( + b.prop_def(field.key, call), + b.method( + 'get', + b.assignment( + '=', + b.id(key), + /** @type {Expression} */ (context.visit(field.computed_key)) + ), + [], + [b.return(b.call('$.get', member))], + true + ), + b.method( + 'set', + b.id(key), + [b.id('value')], + [b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))], + true + ) + ); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassDeclaration.js new file mode 100644 index 0000000000..96bd5c3d29 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassDeclaration.js @@ -0,0 +1,32 @@ +/** @import { ClassBody, ClassDeclaration, Expression, VariableDeclaration } from 'estree' */ +/** @import { ClientTransformState, Context } from '../types' */ +import * as b from '#compiler/builders'; + +/** + * @param {ClassDeclaration} node + * @param {Context} context + */ +export function ClassDeclaration(node, context) { + /** @type {ClientTransformState & { computed_field_declarations: VariableDeclaration[] }} */ + const state = { + ...context.state, + computed_field_declarations: [] + }; + const super_class = node.superClass + ? /** @type {Expression} */ (context.visit(node.superClass)) + : null; + const body = /** @type {ClassBody} */ (context.visit(node.body, state)); + if (state.computed_field_declarations.length > 0) { + const init = b.call( + b.arrow( + [], + b.block([ + ...state.computed_field_declarations, + b.return(b.class(node.id, body, super_class)) + ]) + ) + ); + return node.id ? b.var(node.id, init) : init; + } + return b.class_declaration(node.id, body, super_class); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassExpression.js new file mode 100644 index 0000000000..fc0bf4e07f --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassExpression.js @@ -0,0 +1,31 @@ +/** @import { ClassBody, ClassExpression, Expression, VariableDeclaration } from 'estree' */ +/** @import { ClientTransformState, Context } from '../types' */ +import * as b from '#compiler/builders'; + +/** + * @param {ClassExpression} node + * @param {Context} context + */ +export function ClassExpression(node, context) { + /** @type {ClientTransformState & { computed_field_declarations: VariableDeclaration[] }} */ + const state = { + ...context.state, + computed_field_declarations: [] + }; + const super_class = node.superClass + ? /** @type {Expression} */ (context.visit(node.superClass)) + : null; + const body = /** @type {ClassBody} */ (context.visit(node.body, state)); + if (state.computed_field_declarations.length > 0) { + return b.call( + b.arrow( + [], + b.block([ + ...state.computed_field_declarations, + b.return(b.class(node.id, body, super_class)) + ]) + ) + ); + } + return b.class(node.id, body, super_class); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index b22b95f5aa..7ebc8d08aa 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -13,6 +13,8 @@ import { AwaitBlock } from './visitors/AwaitBlock.js'; import { AwaitExpression } from './visitors/AwaitExpression.js'; import { CallExpression } from './visitors/CallExpression.js'; import { ClassBody } from './visitors/ClassBody.js'; +import { ClassDeclaration } from './visitors/ClassDeclaration.js'; +import { ClassExpression } from './visitors/ClassExpression.js'; import { Component } from './visitors/Component.js'; import { ConstTag } from './visitors/ConstTag.js'; import { DebugTag } from './visitors/DebugTag.js'; @@ -49,6 +51,8 @@ const global_visitors = { AwaitExpression, CallExpression, ClassBody, + ClassDeclaration, + ClassExpression, ExpressionStatement, Identifier, LabeledStatement, @@ -103,7 +107,8 @@ export function server_component(analysis, options) { namespace: options.namespace, preserve_whitespace: options.preserveWhitespace, state_fields: new Map(), - skip_hydration_boundaries: false + skip_hydration_boundaries: false, + computed_field_declarations: null }; const module = /** @type {Program} */ ( @@ -408,7 +413,8 @@ export function server_module(analysis, options) { // to be present for `javascript_visitors_legacy` and so is included in module // transform state as well as component transform state legacy_reactive_statements: new Map(), - state_fields: new Map() + state_fields: new Map(), + computed_field_declarations: null }; const module = /** @type {Program} */ ( diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js index 432d0142cd..004f17c707 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js @@ -1,4 +1,4 @@ -/** @import { CallExpression, ClassBody, MethodDefinition, PropertyDefinition, StaticBlock } from 'estree' */ +/** @import { CallExpression, ClassBody, Expression, MethodDefinition, PropertyDefinition, StaticBlock, VariableDeclaration } from 'estree' */ /** @import { Context } from '../types.js' */ import * as b from '#compiler/builders'; import { get_name } from '../../../nodes.js'; @@ -20,24 +20,59 @@ export function ClassBody(node, context) { const body = []; const child_state = { ...context.state, state_fields }; + const computed_field_declarations = /** @type {VariableDeclaration[]} */ ( + context.state.computed_field_declarations + ); for (const [name, field] of state_fields) { - if (name[0] === '#') { + if ( + (typeof name === 'string' && name[0] === '#') || + field.node.type !== 'AssignmentExpression' + ) { + continue; + } + + if (typeof name !== 'string' && field.computed_key) { + const key = context.state.scope.generate(`key_${name}`); + computed_field_declarations.push(b.let(key)); + const member = b.member(b.this, field.key); + body.push( + b.prop_def(field.key, null), + b.method( + 'get', + b.assignment( + '=', + b.id(key), + /** @type {Expression} */ (context.visit(field.computed_key)) + ), + [], + [b.return(b.call(member))], + true + ), + b.method( + 'set', + b.id(key), + [b.id('$$value')], + [b.return(b.call(member, b.id('$$value')))], + true + ) + ); continue; } // insert backing fields for stuff declared in the constructor - if ( - field && - field.node.type === 'AssignmentExpression' && - (field.type === '$derived' || field.type === '$derived.by') - ) { + if (field.type === '$derived' || field.type === '$derived.by') { const member = b.member(b.this, field.key); body.push( b.prop_def(field.key, null), - b.method('get', b.key(name), [], [b.return(b.call(member))]), - b.method('set', b.key(name), [b.id('$$value')], [b.return(b.call(member, b.id('$$value')))]) + b.method('get', b.key(/** @type {string} */ (name)), [], [b.return(b.call(member))]), + b.method( + 'set', + b.key(/** @type {string} */ (name)), + [b.id('$$value')], + [b.return(b.call(member, b.id('$$value')))] + ) ); } } @@ -51,17 +86,23 @@ export function ClassBody(node, context) { continue; } - const name = get_name(definition.key); - const field = name && state_fields.get(name); + const name = definition.computed + ? [...state_fields.entries()].find(([, field]) => field.node === definition)?.[0] ?? null + : get_name(definition.key); + const field = name !== null && state_fields.get(name); if (!field) { body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state))); continue; } - if (name[0] === '#' || field.type === '$state' || field.type === '$state.raw') { + if ( + (typeof name === 'string' && name[0] === '#') || + field.type === '$state' || + field.type === '$state.raw' + ) { body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state))); - } else if (field.node === definition) { + } else if (field.node === definition && typeof name === 'string') { // $derived / $derived.by const member = b.member(b.this, field.key); @@ -74,6 +115,35 @@ export function ClassBody(node, context) { b.method('get', definition.key, [], [b.return(b.call(member))]), b.method('set', b.key(name), [b.id('$$value')], [b.return(b.call(member, b.id('$$value')))]) ); + } else if (field.computed_key) { + // $derived / $derived.by + const member = b.member(b.this, field.key); + const key = context.state.scope.generate(`key_${name}`); + computed_field_declarations.push(b.let(key)); + body.push( + b.prop_def( + field.key, + /** @type {CallExpression} */ (context.visit(field.value, child_state)) + ), + b.method( + 'get', + b.assignment( + '=', + b.id(key), + /** @type {Expression} */ (context.visit(field.computed_key)) + ), + [], + [b.return(b.call(member))], + true + ), + b.method( + 'set', + b.id(key), + [b.id('$$value')], + [b.return(b.call(member, b.id('$$value')))], + true + ) + ); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassDeclaration.js new file mode 100644 index 0000000000..8543cb4083 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassDeclaration.js @@ -0,0 +1,32 @@ +/** @import { ClassBody, ClassDeclaration, Expression, VariableDeclaration } from 'estree' */ +/** @import { ServerTransformState, Context } from '../types' */ +import * as b from '#compiler/builders'; + +/** + * @param {ClassDeclaration} node + * @param {Context} context + */ +export function ClassDeclaration(node, context) { + /** @type {ServerTransformState & { computed_field_declarations: VariableDeclaration[] }} */ + const state = { + ...context.state, + computed_field_declarations: [] + }; + const super_class = node.superClass + ? /** @type {Expression} */ (context.visit(node.superClass)) + : null; + const body = /** @type {ClassBody} */ (context.visit(node.body, state)); + if (state.computed_field_declarations.length > 0) { + const init = b.call( + b.arrow( + [], + b.block([ + ...state.computed_field_declarations, + b.return(b.class(node.id, body, super_class)) + ]) + ) + ); + return node.id ? b.var(node.id, init) : init; + } + return b.class_declaration(node.id, body, super_class); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassExpression.js new file mode 100644 index 0000000000..16fcaedc28 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassExpression.js @@ -0,0 +1,31 @@ +/** @import { ClassBody, ClassExpression, Expression, VariableDeclaration } from 'estree' */ +/** @import { ServerTransformState, Context } from '../types' */ +import * as b from '#compiler/builders'; + +/** + * @param {ClassExpression} node + * @param {Context} context + */ +export function ClassExpression(node, context) { + /** @type {ServerTransformState & { computed_field_declarations: VariableDeclaration[] }} */ + const state = { + ...context.state, + computed_field_declarations: [] + }; + const super_class = node.superClass + ? /** @type {Expression} */ (context.visit(node.superClass)) + : null; + const body = /** @type {ClassBody} */ (context.visit(node.body, state)); + if (state.computed_field_declarations.length > 0) { + return b.call( + b.arrow( + [], + b.block([ + ...state.computed_field_declarations, + b.return(b.class(node.id, body, super_class)) + ]) + ) + ); + } + return b.class(node.id, body, super_class); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/types.d.ts index 887ad447b9..69ed5c6a55 100644 --- a/packages/svelte/src/compiler/phases/3-transform/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/types.d.ts @@ -1,6 +1,7 @@ import type { Scope } from '../scope.js'; -import type { AST, StateField, ValidatedModuleCompileOptions } from '#compiler'; +import type { AST, StateFields, ValidatedModuleCompileOptions } from '#compiler'; import type { Analysis } from '../types.js'; +import type { VariableDeclaration } from 'estree'; export interface TransformState { readonly analysis: Analysis; @@ -8,5 +9,6 @@ export interface TransformState { readonly scope: Scope; readonly scopes: Map; - readonly state_fields: Map; + readonly state_fields: StateFields; + readonly computed_field_declarations: VariableDeclaration[] | null; } diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 4e287fd199..a40e25e621 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -1,4 +1,4 @@ -import type { AST, Binding, StateField } from '#compiler'; +import type { AST, Binding, StateField, StateFields } from '#compiler'; import type { AwaitExpression, CallExpression, @@ -40,7 +40,7 @@ export interface Analysis { tracing: boolean; comments: AST.JSComment[]; - classes: Map>; + classes: Map; // TODO figure out if we can move this to ComponentAnalysis accessors: boolean; diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 9bd4b91d58..9ca371b9e2 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -6,6 +6,7 @@ import type { StateCreationRuneName } from '../../utils.js'; import type { AssignmentExpression, CallExpression, + Expression, PrivateIdentifier, PropertyDefinition } from 'estree'; @@ -316,8 +317,11 @@ export interface StateField { node: PropertyDefinition | AssignmentExpression; key: PrivateIdentifier; value: CallExpression; + computed_key: Expression | null; } +export type StateFields = Map; + export * from './template.js'; export { Binding, Scope } from '../phases/scope.js'; diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index 99306ce4d9..f53ecedb76 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -487,6 +487,37 @@ function new_builder(expression, ...args) { }; } +/** + * @param {ESTree.Identifier | null | undefined} id + * @param {ESTree.ClassBody} body + * @param {ESTree.Expression | null | undefined} [super_class] + * @returns {ESTree.ClassExpression} + */ +function class_builder(id, body, super_class = null) { + return { + type: 'ClassExpression', + id, + body, + superClass: super_class, + decorators: [] + }; +} +/** + * @param {ESTree.Identifier} id + * @param {ESTree.ClassBody} body + * @param {ESTree.Expression | null} [super_class] + * @returns {ESTree.ClassDeclaration} + */ +export function class_declaration(id, body, super_class = null) { + return { + type: 'ClassDeclaration', + id, + body, + superClass: super_class, + decorators: [] + }; +} + /** * @param {ESTree.UpdateOperator} operator * @param {ESTree.Expression} argument @@ -674,7 +705,8 @@ export { if_builder as if, this_instance as this, null_instance as null, - debugger_builder as debugger + debugger_builder as debugger, + class_builder as class }; /** From 5f93015f747ebf45509ef94015e80ee729013b3a Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:12:31 -0700 Subject: [PATCH 02/15] fix --- .../compiler/phases/2-analyze/visitors/CallExpression.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) 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 76d9cecd9a..9db4148a2e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -313,7 +313,7 @@ function is_variable_declaration(parent, context) { * @param {AST.SvelteNode} parent */ function is_class_property_definition(parent) { - return parent.type === 'PropertyDefinition' && !parent.static && !parent.computed; + return parent.type === 'PropertyDefinition' && !parent.static; } /** @@ -325,10 +325,7 @@ function is_class_property_assignment_at_constructor_root(node, context) { node.type === 'AssignmentExpression' && node.operator === '=' && node.left.type === 'MemberExpression' && - node.left.object.type === 'ThisExpression' && - ((node.left.property.type === 'Identifier' && !node.left.computed) || - node.left.property.type === 'PrivateIdentifier' || - node.left.property.type === 'Literal') + node.left.object.type === 'ThisExpression' ) { // MethodDefinition (-5) -> FunctionExpression (-4) -> BlockStatement (-3) -> ExpressionStatement (-2) -> AssignmentExpression (-1) const parent = get_parent(context.path, -5); From 2ed0f2f797eaabe84e5a56886e5982e1c14d15bb Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:16:47 -0700 Subject: [PATCH 03/15] more --- .../src/compiler/phases/2-analyze/visitors/ClassBody.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js index 8b3638ac97..a0f72470df 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js @@ -82,8 +82,8 @@ export function ClassBody(node, context) { } for (const child of node.body) { - if (child.type === 'PropertyDefinition' && !child.computed && !child.static) { - handle(child, child.key, child.value); + if (child.type === 'PropertyDefinition' && !child.static) { + handle(child, child.key, child.value, child.computed); const key = /** @type {string} */ (get_name(child.key)); const field = fields.get(key); if (!field) { @@ -138,9 +138,8 @@ export function ClassBody(node, context) { if (left.type !== 'MemberExpression') continue; if (left.object.type !== 'ThisExpression') continue; - if (left.computed && left.property.type !== 'Literal') continue; - handle(statement.expression, left.property, right); + handle(statement.expression, left.property, right, left.computed); } } From 686238bee82e5a7ca333c3bd2cfabdc2654eb17a Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:19:16 -0700 Subject: [PATCH 04/15] tweak --- .../compiler/phases/3-transform/client/visitors/ClassBody.js | 4 ++-- .../compiler/phases/3-transform/server/visitors/ClassBody.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js index 5f6e48dc11..14e8b61e42 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js @@ -37,7 +37,7 @@ export function ClassBody(node, context) { } if (typeof name === 'number' && field.computed_key) { - const key = context.state.scope.generate(`key_${name}`); + const key = context.state.scope.generate('key'); computed_field_declarations.push(b.let(key)); const member = b.member(b.this, field.key); @@ -150,7 +150,7 @@ export function ClassBody(node, context) { b.literal(`${declaration.id?.name ?? '[class]'}[computed key]`) ); } - const key = context.state.scope.generate(`key_${name}`); + const key = context.state.scope.generate('key'); computed_field_declarations.push(b.let(key)); const member = b.member(b.this, field.key); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js index 004f17c707..9e5c3d1c45 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js @@ -33,7 +33,7 @@ export function ClassBody(node, context) { } if (typeof name !== 'string' && field.computed_key) { - const key = context.state.scope.generate(`key_${name}`); + const key = context.state.scope.generate('key'); computed_field_declarations.push(b.let(key)); const member = b.member(b.this, field.key); body.push( @@ -118,7 +118,7 @@ export function ClassBody(node, context) { } else if (field.computed_key) { // $derived / $derived.by const member = b.member(b.this, field.key); - const key = context.state.scope.generate(`key_${name}`); + const key = context.state.scope.generate('key'); computed_field_declarations.push(b.let(key)); body.push( b.prop_def( From 043fe1e3f9e29e880ff61ff5bad2fa1dee2e767e Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:26:53 -0700 Subject: [PATCH 05/15] deconflict number keys --- .../src/compiler/phases/2-analyze/visitors/ClassBody.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js index a0f72470df..59fdf388e7 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js @@ -148,7 +148,10 @@ export function ClassBody(node, context) { continue; } - let deconflicted = `${name}`.replace(regex_invalid_identifier_chars, '_'); + let deconflicted = `${typeof name === 'number' ? '0' : ''}${name}`.replace( + regex_invalid_identifier_chars, + '_' + ); while (private_ids.includes(deconflicted)) { deconflicted = '_' + deconflicted; } From 10e90c7e5a7f19a6e0b0f08c9bf8a10e66645a45 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:39:15 -0700 Subject: [PATCH 06/15] fix? --- .../compiler/phases/2-analyze/visitors/ClassBody.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js index 59fdf388e7..5cd24fef88 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js @@ -33,7 +33,7 @@ export function ClassBody(node, context) { /** @type {Map} */ const state_fields = new Map(); - /** @type {Map>} */ + /** @type {Map>} */ const fields = new Map(); context.state.analysis.classes.set(node, state_fields); @@ -62,12 +62,15 @@ export function ClassBody(node, context) { e.state_field_duplicate(node, name); } - const _key = (node.type === 'AssignmentExpression' || !node.static ? '' : '@') + name; + const _key = + typeof name === 'string' + ? (node.type === 'AssignmentExpression' || !node.static ? '' : '@') + name + : name; const field = fields.get(_key); // if there's already a method or assigned field, error if (field && !(field.length === 1 && field[0] === 'prop')) { - e.duplicate_class_field(node, _key); + e.duplicate_class_field(node, typeof _key === 'string' ? _key : '[computed key]'); } state_fields.set(name, { @@ -148,7 +151,7 @@ export function ClassBody(node, context) { continue; } - let deconflicted = `${typeof name === 'number' ? '0' : ''}${name}`.replace( + let deconflicted = `${typeof name === 'number' ? '_' : ''}${name}`.replace( regex_invalid_identifier_chars, '_' ); From ded9f0590103835d6186a820640d6a2af1a8f9a4 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:44:44 -0700 Subject: [PATCH 07/15] fix one test --- .../samples/class-state-constructor-7/errors.json | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-7/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-7/errors.json index 64e56f8d5c..fe51488c70 100644 --- a/packages/svelte/tests/validator/samples/class-state-constructor-7/errors.json +++ b/packages/svelte/tests/validator/samples/class-state-constructor-7/errors.json @@ -1,14 +1 @@ -[ - { - "code": "state_invalid_placement", - "message": "`$state(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.", - "start": { - "line": 5, - "column": 16 - }, - "end": { - "line": 5, - "column": 25 - } - } -] +[] From 7948cb928fb666c937dace89ee8a96adb51b2899 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:48:07 -0700 Subject: [PATCH 08/15] generate types --- packages/svelte/types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index d260b738c3..942478e19a 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -807,7 +807,7 @@ declare module 'svelte/attachments' { declare module 'svelte/compiler' { import type { SourceMap } from 'magic-string'; - import type { ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, Expression, Identifier, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree'; + import type { Expression, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, Identifier, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree'; import type { Location } from 'locate-character'; /** * `compile` converts your `.svelte` source code into a JavaScript module that exports a component From 17308a5a4c7b9d787a065b50d87cde438f59905b Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:51:39 -0700 Subject: [PATCH 09/15] lint --- .../server/visitors/ClassDeclaration.js | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassDeclaration.js index 8543cb4083..dfacea85f8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassDeclaration.js @@ -7,26 +7,26 @@ import * as b from '#compiler/builders'; * @param {Context} context */ export function ClassDeclaration(node, context) { - /** @type {ServerTransformState & { computed_field_declarations: VariableDeclaration[] }} */ - const state = { - ...context.state, - computed_field_declarations: [] - }; - const super_class = node.superClass - ? /** @type {Expression} */ (context.visit(node.superClass)) - : null; - const body = /** @type {ClassBody} */ (context.visit(node.body, state)); - if (state.computed_field_declarations.length > 0) { - const init = b.call( - b.arrow( - [], - b.block([ - ...state.computed_field_declarations, - b.return(b.class(node.id, body, super_class)) - ]) - ) - ); - return node.id ? b.var(node.id, init) : init; - } - return b.class_declaration(node.id, body, super_class); + /** @type {ServerTransformState & { computed_field_declarations: VariableDeclaration[] }} */ + const state = { + ...context.state, + computed_field_declarations: [] + }; + const super_class = node.superClass + ? /** @type {Expression} */ (context.visit(node.superClass)) + : null; + const body = /** @type {ClassBody} */ (context.visit(node.body, state)); + if (state.computed_field_declarations.length > 0) { + const init = b.call( + b.arrow( + [], + b.block([ + ...state.computed_field_declarations, + b.return(b.class(node.id, body, super_class)) + ]) + ) + ); + return node.id ? b.var(node.id, init) : init; + } + return b.class_declaration(node.id, body, super_class); } From 5565d06bc1ad3c454e613010e93552464f11c867 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 23 Oct 2025 22:03:39 -0700 Subject: [PATCH 10/15] fix --- .../src/compiler/phases/2-analyze/visitors/ClassBody.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js index 5cd24fef88..65c81c4eb2 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js @@ -86,7 +86,7 @@ export function ClassBody(node, context) { for (const child of node.body) { if (child.type === 'PropertyDefinition' && !child.static) { - handle(child, child.key, child.value, child.computed); + handle(child, child.key, child.value, child.computed && child.key.type !== 'Literal'); const key = /** @type {string} */ (get_name(child.key)); const field = fields.get(key); if (!field) { @@ -142,7 +142,12 @@ export function ClassBody(node, context) { if (left.type !== 'MemberExpression') continue; if (left.object.type !== 'ThisExpression') continue; - handle(statement.expression, left.property, right, left.computed); + handle( + statement.expression, + left.property, + right, + left.computed && left.property.type !== 'Literal' + ); } } From 02194d68f4e508a47a8b3e419467c8001a420995 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 26 Oct 2025 14:30:55 -0700 Subject: [PATCH 11/15] evaluate keys, remove closure for `ClassDeclaration` --- .../src/compiler/phases/2-analyze/types.d.ts | 2 +- .../3-transform/client/transform-client.js | 2 + .../client/visitors/BlockStatement.js | 25 ++++++++++-- .../3-transform/client/visitors/ClassBody.js | 38 ++++++++++++------- .../client/visitors/ClassDeclaration.js | 29 +++++++++----- .../3-transform/client/visitors/Program.js | 29 ++++++++++++-- .../client/visitors/StaticBlock.js | 31 +++++++++++++++ .../3-transform/server/transform-server.js | 6 +++ .../server/visitors/BlockStatement.js | 31 +++++++++++++++ .../3-transform/server/visitors/ClassBody.js | 27 ++++++++++--- .../server/visitors/ClassDeclaration.js | 29 +++++++++----- .../3-transform/server/visitors/Program.js | 31 +++++++++++++++ .../server/visitors/StaticBlock.js | 31 +++++++++++++++ .../svelte/src/compiler/phases/types.d.ts | 2 +- 14 files changed, 264 insertions(+), 49 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/StaticBlock.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/BlockStatement.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/StaticBlock.js diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index 25a71eed86..0ded021cf7 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -1,6 +1,6 @@ import type { Scope } from '../scope.js'; import type { ComponentAnalysis, ReactiveStatement } from '../types.js'; -import type { AST, ExpressionMetadata, StateFields, ValidatedCompileOptions } from '#compiler'; +import type { AST, StateFields, ValidatedCompileOptions } from '#compiler'; import type { ExpressionMetadata } from '../nodes.js'; export interface AnalysisState { 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 2adafafc42..e3f05dd36f 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 @@ -47,6 +47,7 @@ import { RenderTag } from './visitors/RenderTag.js'; import { SlotElement } from './visitors/SlotElement.js'; import { SnippetBlock } from './visitors/SnippetBlock.js'; import { SpreadAttribute } from './visitors/SpreadAttribute.js'; +import { StaticBlock } from './visitors/StaticBlock.js'; import { SvelteBody } from './visitors/SvelteBody.js'; import { SvelteComponent } from './visitors/SvelteComponent.js'; import { SvelteDocument } from './visitors/SvelteDocument.js'; @@ -127,6 +128,7 @@ const visitors = { SlotElement, SnippetBlock, SpreadAttribute, + StaticBlock, SvelteBody, SvelteComponent, SvelteDocument, 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 d1c0978a81..607cc8e2d3 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,4 +1,4 @@ -/** @import { ArrowFunctionExpression, BlockStatement, Expression, FunctionDeclaration, FunctionExpression, Statement } from 'estree' */ +/** @import { ArrowFunctionExpression, BlockStatement, Declaration, Expression, FunctionDeclaration, FunctionExpression, Statement, VariableDeclaration } from 'estree' */ /** @import { ComponentContext } from '../types' */ import { add_state_transformers } from './shared/declarations.js'; import * as b from '#compiler/builders'; @@ -11,6 +11,24 @@ export function BlockStatement(node, context) { add_state_transformers(context); const tracing = context.state.scope.tracing; + /** @type {BlockStatement['body']} */ + const body = []; + for (const child of node.body) { + const visited = /** @type {Declaration | Statement} */ (context.visit(child)); + if ( + visited.type === 'ClassDeclaration' && + 'metadata' in visited && + visited.metadata !== null && + typeof visited.metadata === 'object' && + 'computed_field_declarations' in visited.metadata + ) { + body.push( + .../** @type {VariableDeclaration[]} */ (visited.metadata.computed_field_declarations) + ); + } + body.push(visited); + } + if (tracing !== null) { const parent = /** @type {ArrowFunctionExpression | FunctionDeclaration | FunctionExpression} */ ( @@ -22,11 +40,10 @@ export function BlockStatement(node, context) { const call = b.call( '$.trace', /** @type {Expression} */ (tracing), - b.thunk(b.block(node.body.map((n) => /** @type {Statement} */ (context.visit(n)))), is_async) + b.thunk(b.block(body), is_async) ); return b.block([b.return(is_async ? b.await(call) : call)]); } - - context.next(); + return b.block(body); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js index 14e8b61e42..018ecc597f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js @@ -35,23 +35,40 @@ export function ClassBody(node, context) { ) { continue; } + const member = b.member(b.this, field.key); + const should_proxy = field.type === '$state' && true; // TODO if (typeof name === 'number' && field.computed_key) { + const computed_key = /** @type {Expression} */ (context.visit(field.computed_key)); + const evaluation = context.state.scope.evaluate(computed_key); + if (evaluation.is_known) { + body.push( + b.prop_def(field.key, null), + b.method( + 'get', + b.literal(evaluation.value), + [], + [b.return(b.call('$.get', member))], + true + ), + b.method( + 'set', + b.literal(evaluation.value), + [b.id('value')], + [b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))], + true + ) + ); + continue; + } const key = context.state.scope.generate('key'); computed_field_declarations.push(b.let(key)); - const member = b.member(b.this, field.key); - - const should_proxy = field.type === '$state' && true; // TODO body.push( b.prop_def(field.key, null), b.method( 'get', - b.assignment( - '=', - b.id(key), - /** @type {Expression} */ (context.visit(field.computed_key)) - ), + b.assignment('=', b.id(key), computed_key), [], [b.return(b.call('$.get', member))], true @@ -66,11 +83,6 @@ export function ClassBody(node, context) { ); continue; } - - const member = b.member(b.this, field.key); - - const should_proxy = field.type === '$state' && true; // TODO - const key = b.key(/** @type {string} */ (name)); body.push( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassDeclaration.js index 96bd5c3d29..630b3fbd66 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassDeclaration.js @@ -17,16 +17,25 @@ export function ClassDeclaration(node, context) { : null; const body = /** @type {ClassBody} */ (context.visit(node.body, state)); if (state.computed_field_declarations.length > 0) { - const init = b.call( - b.arrow( - [], - b.block([ - ...state.computed_field_declarations, - b.return(b.class(node.id, body, super_class)) - ]) - ) - ); - return node.id ? b.var(node.id, init) : init; + if (context.path.at(-1)?.type === 'ExportDefaultDeclaration') { + const init = b.call( + b.arrow( + [], + b.block([ + ...state.computed_field_declarations, + b.return(b.class(node.id, body, super_class)) + ]) + ) + ); + return node.id ? b.var(node.id, init) : init; + } else { + return { + ...b.class_declaration(node.id, body, super_class), + metadata: { + computed_field_declarations: state.computed_field_declarations + } + } + } } return b.class_declaration(node.id, body, super_class); } 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 07342da314..7183ac135f 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,14 +1,14 @@ -/** @import { Expression, ImportDeclaration, MemberExpression, Program } from 'estree' */ +/** @import { Declaration, Expression, ImportDeclaration, MemberExpression, Program, Statement, VariableDeclaration } from 'estree' */ /** @import { ComponentContext } from '../types' */ import { build_getter, is_prop_source } from '../utils.js'; import * as b from '#compiler/builders'; import { add_state_transformers } from './shared/declarations.js'; /** - * @param {Program} _ + * @param {Program} node * @param {ComponentContext} context */ -export function Program(_, context) { +export function Program(node, context) { if (!context.state.analysis.runes) { context.state.transform['$$props'] = { read: (node) => ({ ...node, name: '$$sanitized_props' }) @@ -137,5 +137,26 @@ export function Program(_, context) { add_state_transformers(context); - context.next(); + /** @type {Program['body']} */ + const body = []; + for (const child of node.body) { + const visited = /** @type {Declaration | Statement} */ (context.visit(child)); + if ( + visited.type === 'ClassDeclaration' && + 'metadata' in visited && + visited.metadata !== null && + typeof visited.metadata === 'object' && + 'computed_field_declarations' in visited.metadata + ) { + body.push( + .../** @type {VariableDeclaration[]} */ (visited.metadata.computed_field_declarations) + ); + } + body.push(visited); + } + + return { + ...node, + body + }; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/StaticBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/StaticBlock.js new file mode 100644 index 0000000000..d578f69406 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/StaticBlock.js @@ -0,0 +1,31 @@ +/** @import { Declaration, Statement, StaticBlock, VariableDeclaration } from 'estree' */ +/** @import { ComponentContext } from '../types' */ + +/** + * @param {StaticBlock} node + * @param {ComponentContext} context + */ +export function StaticBlock(node, context) { + /** @type {StaticBlock['body']} */ + const body = []; + for (const child of node.body) { + const visited = /** @type {Declaration | Statement} */ (context.visit(child)); + if ( + visited.type === 'ClassDeclaration' && + 'metadata' in visited && + visited.metadata !== null && + typeof visited.metadata === 'object' && + 'computed_field_declarations' in visited.metadata + ) { + body.push( + .../** @type {VariableDeclaration[]} */ (visited.metadata.computed_field_declarations) + ); + } + body.push(visited); + } + + return { + ...node, + body + }; +} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 16ca48d9be..558f9ca305 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -11,6 +11,7 @@ import { render_stylesheet } from '../css/index.js'; import { AssignmentExpression } from './visitors/AssignmentExpression.js'; import { AwaitBlock } from './visitors/AwaitBlock.js'; import { AwaitExpression } from './visitors/AwaitExpression.js'; +import { BlockStatement } from './visitors/BlockStatement.js'; import { CallExpression } from './visitors/CallExpression.js'; import { ClassBody } from './visitors/ClassBody.js'; import { ClassDeclaration } from './visitors/ClassDeclaration.js'; @@ -27,12 +28,14 @@ import { IfBlock } from './visitors/IfBlock.js'; import { KeyBlock } from './visitors/KeyBlock.js'; import { LabeledStatement } from './visitors/LabeledStatement.js'; import { MemberExpression } from './visitors/MemberExpression.js'; +import { Program } from './visitors/Program.js'; import { PropertyDefinition } from './visitors/PropertyDefinition.js'; import { RegularElement } from './visitors/RegularElement.js'; import { RenderTag } from './visitors/RenderTag.js'; import { SlotElement } from './visitors/SlotElement.js'; import { SnippetBlock } from './visitors/SnippetBlock.js'; import { SpreadAttribute } from './visitors/SpreadAttribute.js'; +import { StaticBlock } from './visitors/StaticBlock.js'; import { SvelteComponent } from './visitors/SvelteComponent.js'; import { SvelteElement } from './visitors/SvelteElement.js'; import { SvelteFragment } from './visitors/SvelteFragment.js'; @@ -49,6 +52,7 @@ const global_visitors = { _: set_scope, AssignmentExpression, AwaitExpression, + BlockStatement, CallExpression, ClassBody, ClassDeclaration, @@ -57,7 +61,9 @@ const global_visitors = { Identifier, LabeledStatement, MemberExpression, + Program, PropertyDefinition, + StaticBlock, UpdateExpression, VariableDeclaration }; diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/BlockStatement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/BlockStatement.js new file mode 100644 index 0000000000..8001fffb83 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/BlockStatement.js @@ -0,0 +1,31 @@ +/** @import { BlockStatement, Declaration, Statement, VariableDeclaration } from 'estree' */ +/** @import { Context } from '../types' */ + +/** + * @param {BlockStatement} node + * @param {Context} context + */ +export function BlockStatement(node, context) { + /** @type {BlockStatement['body']} */ + const body = []; + for (const child of node.body) { + const visited = /** @type {Declaration | Statement} */ (context.visit(child)); + if ( + visited.type === 'ClassDeclaration' && + 'metadata' in visited && + visited.metadata !== null && + typeof visited.metadata === 'object' && + 'computed_field_declarations' in visited.metadata + ) { + body.push( + .../** @type {VariableDeclaration[]} */ (visited.metadata.computed_field_declarations) + ); + } + body.push(visited); + } + + return { + ...node, + body + }; +} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js index 9e5c3d1c45..d3c04af9ca 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js @@ -32,10 +32,10 @@ export function ClassBody(node, context) { continue; } + const member = b.member(b.this, field.key); if (typeof name !== 'string' && field.computed_key) { const key = context.state.scope.generate('key'); computed_field_declarations.push(b.let(key)); - const member = b.member(b.this, field.key); body.push( b.prop_def(field.key, null), b.method( @@ -62,8 +62,6 @@ export function ClassBody(node, context) { // insert backing fields for stuff declared in the constructor if (field.type === '$derived' || field.type === '$derived.by') { - const member = b.member(b.this, field.key); - body.push( b.prop_def(field.key, null), b.method('get', b.key(/** @type {string} */ (name)), [], [b.return(b.call(member))]), @@ -95,6 +93,7 @@ export function ClassBody(node, context) { body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state))); continue; } + const member = b.member(b.this, field.key); if ( (typeof name === 'string' && name[0] === '#') || @@ -104,8 +103,6 @@ export function ClassBody(node, context) { body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state))); } else if (field.node === definition && typeof name === 'string') { // $derived / $derived.by - const member = b.member(b.this, field.key); - body.push( b.prop_def( field.key, @@ -117,7 +114,25 @@ export function ClassBody(node, context) { ); } else if (field.computed_key) { // $derived / $derived.by - const member = b.member(b.this, field.key); + const computed_key = /** @type {Expression} */ (context.visit(field.computed_key)); + const evaluation = context.state.scope.evaluate(computed_key); + if (evaluation.is_known) { + body.push( + b.prop_def( + field.key, + /** @type {CallExpression} */ (context.visit(field.value, child_state)) + ), + b.method('get', b.literal(evaluation.value), [], [b.return(b.call(member))], true), + b.method( + 'set', + b.literal(evaluation.value), + [b.id('$$value')], + [b.return(b.call(member, b.id('$$value')))], + true + ) + ); + continue; + } const key = context.state.scope.generate('key'); computed_field_declarations.push(b.let(key)); body.push( diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassDeclaration.js index dfacea85f8..dbe9c8a787 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassDeclaration.js @@ -17,16 +17,25 @@ export function ClassDeclaration(node, context) { : null; const body = /** @type {ClassBody} */ (context.visit(node.body, state)); if (state.computed_field_declarations.length > 0) { - const init = b.call( - b.arrow( - [], - b.block([ - ...state.computed_field_declarations, - b.return(b.class(node.id, body, super_class)) - ]) - ) - ); - return node.id ? b.var(node.id, init) : init; + if (context.path.at(-1)?.type === 'ExportDefaultDeclaration') { + const init = b.call( + b.arrow( + [], + b.block([ + ...state.computed_field_declarations, + b.return(b.class(node.id, body, super_class)) + ]) + ) + ); + return node.id ? b.var(node.id, init) : init; + } else { + return { + ...b.class_declaration(node.id, body, super_class), + metadata: { + computed_field_declarations: state.computed_field_declarations + } + }; + } } return b.class_declaration(node.id, body, super_class); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js new file mode 100644 index 0000000000..7ad1a5d946 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js @@ -0,0 +1,31 @@ +/** @import { Declaration, Program, Statement, VariableDeclaration } from 'estree' */ +/** @import { Context } from '../types.js' */ + +/** + * @param {Program} node + * @param {Context} context + */ +export function Program(node, context) { + /** @type {Program['body']} */ + const body = []; + for (const child of node.body) { + const visited = /** @type {Declaration | Statement} */ (context.visit(child)); + if ( + visited.type === 'ClassDeclaration' && + 'metadata' in visited && + visited.metadata !== null && + typeof visited.metadata === 'object' && + 'computed_field_declarations' in visited.metadata + ) { + body.push( + .../** @type {VariableDeclaration[]} */ (visited.metadata.computed_field_declarations) + ); + } + body.push(visited); + } + + return { + ...node, + body + }; +} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/StaticBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/StaticBlock.js new file mode 100644 index 0000000000..d5c8c3b87c --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/StaticBlock.js @@ -0,0 +1,31 @@ +/** @import { Declaration, Statement, StaticBlock, VariableDeclaration } from 'estree' */ +/** @import { Context } from '../types' */ + +/** + * @param {StaticBlock} node + * @param {Context} context + */ +export function StaticBlock(node, context) { + /** @type {StaticBlock['body']} */ + const body = []; + for (const child of node.body) { + const visited = /** @type {Declaration | Statement} */ (context.visit(child)); + if ( + visited.type === 'ClassDeclaration' && + 'metadata' in visited && + visited.metadata !== null && + typeof visited.metadata === 'object' && + 'computed_field_declarations' in visited.metadata + ) { + body.push( + .../** @type {VariableDeclaration[]} */ (visited.metadata.computed_field_declarations) + ); + } + body.push(visited); + } + + return { + ...node, + body + }; +} diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 84c3787b47..cfddf51681 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -1,4 +1,4 @@ -import type { AST, Binding, StateField, StateFields } from '#compiler'; +import type { AST, Binding, StateFields } from '#compiler'; import type { AwaitExpression, CallExpression, From 1c00d7dd841af06ce033617cd54b3c5812092c83 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 26 Oct 2025 14:36:19 -0700 Subject: [PATCH 12/15] lint --- .../phases/3-transform/client/visitors/ClassDeclaration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassDeclaration.js index 630b3fbd66..1f8d0fba19 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassDeclaration.js @@ -34,7 +34,7 @@ export function ClassDeclaration(node, context) { metadata: { computed_field_declarations: state.computed_field_declarations } - } + }; } } return b.class_declaration(node.id, body, super_class); From 54368558c01fbc87066f986d6f1ba4b8707e9478 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 26 Oct 2025 14:44:23 -0700 Subject: [PATCH 13/15] fix? --- .../compiler/phases/2-analyze/visitors/ClassBody.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js index 65c81c4eb2..ce11887c00 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js @@ -87,7 +87,10 @@ export function ClassBody(node, context) { for (const child of node.body) { if (child.type === 'PropertyDefinition' && !child.static) { handle(child, child.key, child.value, child.computed && child.key.type !== 'Literal'); - const key = /** @type {string} */ (get_name(child.key)); + const key = get_name(child.key); + if (key === null) { + continue; + } const field = fields.get(key); if (!field) { fields.set(key, [child.value ? 'assigned_prop' : 'prop']); @@ -100,7 +103,11 @@ export function ClassBody(node, context) { if (child.kind === 'constructor') { constructor = child; } else if (!child.computed) { - const key = (child.static ? '@' : '') + get_name(child.key); + const name = get_name(child.key); + if (name === null) { + continue; + } + const key = (child.static ? '@' : '') + name; const field = fields.get(key); if (!field) { fields.set(key, [child.kind]); From 1e55c1f11e18c2f28607c8a11eb2efa8f876dd9a Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 26 Oct 2025 14:49:03 -0700 Subject: [PATCH 14/15] DRY --- .../phases/3-transform/client/visitors/ClassBody.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js index 018ecc597f..d4b24469bd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js @@ -121,6 +121,9 @@ export function ClassBody(node, context) { body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state))); continue; } + const member = b.member(b.this, field.key); + + const should_proxy = field.type === '$state' && true; // TODO if (typeof name === 'string' && name[0] === '#') { let value = definition.value @@ -138,8 +141,6 @@ export function ClassBody(node, context) { if (dev) { call = b.call('$.tag', call, b.literal(`${declaration.id?.name ?? '[class]'}.${name}`)); } - const member = b.member(b.this, field.key); - const should_proxy = field.type === '$state' && true; // TODO body.push( b.prop_def(field.key, call), @@ -164,9 +165,6 @@ export function ClassBody(node, context) { } const key = context.state.scope.generate('key'); computed_field_declarations.push(b.let(key)); - const member = b.member(b.this, field.key); - - const should_proxy = field.type === '$state' && true; // TODO body.push( b.prop_def(field.key, call), From 5b5e9efb92ba9c2934e2d8e77c301c55962c87f7 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 26 Oct 2025 15:09:37 -0700 Subject: [PATCH 15/15] don't alias constant expressions --- .../3-transform/client/visitors/ClassBody.js | 57 +++++++++++++++-- .../3-transform/server/visitors/ClassBody.js | 60 ++++++++++++++--- packages/svelte/src/compiler/phases/scope.js | 64 +++++++++++++++++++ 3 files changed, 166 insertions(+), 15 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js index d4b24469bd..3585660cba 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js @@ -5,6 +5,7 @@ import * as b from '#compiler/builders'; import { dev } from '../../../../state.js'; import { get_parent } from '../../../../utils/ast.js'; import { get_name } from '../../../nodes.js'; +import { is_constant } from '../../../scope.js'; /** * @param {ClassBody} node @@ -61,6 +62,20 @@ export function ClassBody(node, context) { ); continue; } + if (is_constant(computed_key, context.state.scope)) { + body.push( + b.prop_def(field.key, null), + b.method('get', computed_key, [], [b.return(b.call('$.get', member))], true), + b.method( + 'set', + computed_key, + [b.id('value')], + [b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))], + true + ) + ); + continue; + } const key = context.state.scope.generate('key'); computed_field_declarations.push(b.let(key)); @@ -163,6 +178,42 @@ export function ClassBody(node, context) { b.literal(`${declaration.id?.name ?? '[class]'}[computed key]`) ); } + const computed_key = /** @type {Expression} */ (context.visit(field.computed_key)); + const evaluation = context.state.scope.evaluate(computed_key); + if (evaluation.is_known) { + body.push( + b.prop_def(field.key, call), + b.method( + 'get', + b.literal(evaluation.value), + [], + [b.return(b.call('$.get', member))], + true + ), + b.method( + 'set', + b.literal(evaluation.value), + [b.id('value')], + [b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))], + true + ) + ); + continue; + } + if (is_constant(computed_key, context.state.scope)) { + body.push( + b.prop_def(field.key, call), + b.method('get', computed_key, [], [b.return(b.call('$.get', member))], true), + b.method( + 'set', + computed_key, + [b.id('value')], + [b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))], + true + ) + ); + continue; + } const key = context.state.scope.generate('key'); computed_field_declarations.push(b.let(key)); @@ -170,11 +221,7 @@ export function ClassBody(node, context) { b.prop_def(field.key, call), b.method( 'get', - b.assignment( - '=', - b.id(key), - /** @type {Expression} */ (context.visit(field.computed_key)) - ), + b.assignment('=', b.id(key), computed_key), [], [b.return(b.call('$.get', member))], true diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js index d3c04af9ca..44926f1a67 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js @@ -2,6 +2,7 @@ /** @import { Context } from '../types.js' */ import * as b from '#compiler/builders'; import { get_name } from '../../../nodes.js'; +import { is_constant } from '../../../scope.js'; /** * @param {ClassBody} node @@ -34,17 +35,43 @@ export function ClassBody(node, context) { const member = b.member(b.this, field.key); if (typeof name !== 'string' && field.computed_key) { + const computed_key = /** @type {Expression} */ (context.visit(field.computed_key)); + const evaluation = context.state.scope.evaluate(computed_key); + if (evaluation.is_known) { + body.push( + b.prop_def(field.key, null), + b.method('get', b.literal(evaluation.value), [], [b.return(b.call(member))], true), + b.method( + 'set', + b.literal(evaluation.value), + [b.id('$$value')], + [b.return(b.call(member, b.id('$$value')))], + true + ) + ); + continue; + } + if (is_constant(computed_key, context.state.scope)) { + body.push( + b.prop_def(field.key, null), + b.method('get', computed_key, [], [b.return(b.call(member))], true), + b.method( + 'set', + computed_key, + [b.id('$$value')], + [b.return(b.call(member, b.id('$$value')))], + true + ) + ); + continue; + } const key = context.state.scope.generate('key'); computed_field_declarations.push(b.let(key)); body.push( b.prop_def(field.key, null), b.method( 'get', - b.assignment( - '=', - b.id(key), - /** @type {Expression} */ (context.visit(field.computed_key)) - ), + b.assignment('=', b.id(key), computed_key), [], [b.return(b.call(member))], true @@ -133,6 +160,23 @@ export function ClassBody(node, context) { ); continue; } + if (is_constant(computed_key, context.state.scope)) { + body.push( + b.prop_def( + field.key, + /** @type {CallExpression} */ (context.visit(field.value, child_state)) + ), + b.method('get', computed_key, [], [b.return(b.call(member))], true), + b.method( + 'set', + computed_key, + [b.id('$$value')], + [b.return(b.call(member, b.id('$$value')))], + true + ) + ); + continue; + } const key = context.state.scope.generate('key'); computed_field_declarations.push(b.let(key)); body.push( @@ -142,11 +186,7 @@ export function ClassBody(node, context) { ), b.method( 'get', - b.assignment( - '=', - b.id(key), - /** @type {Expression} */ (context.visit(field.computed_key)) - ), + b.assignment('=', b.id(key), computed_key), [], [b.return(b.call(member))], true diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 7dbdf47967..c319a7999d 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1358,6 +1358,70 @@ export function get_rune(node, scope) { return keypath; } +/** + * @param {Expression} expression + * @param {Scope} scope + */ +export function is_constant(expression, scope) { + const evaluation = scope.evaluate(expression); + if (evaluation.is_known) { + return true; + } + let constant = true; + walk(/** @type {Node} */ (expression), null, { + Identifier(node, { path, stop }) { + if (is_reference(node, /** @type {Node} */ (path.at(-1)))) { + const binding = scope.get(node.name); + if (!binding || binding.reassigned) { + constant = false; + stop(); + return; + } + } + }, + ArrowFunctionExpression(_, { stop }) { + constant = false; + stop(); + }, + FunctionExpression(_, { stop }) { + constant = false; + stop(); + }, + ClassExpression(_, { stop }) { + constant = false; + stop(); + }, + MemberExpression(_, { stop }) { + constant = false; + stop(); + }, + CallExpression(node, { stop }) { + if (scope.evaluate(node).is_known) { + return; + } + constant = false; + stop(); + }, + UpdateExpression(_, { stop }) { + constant = false; + stop(); + }, + AssignmentExpression(_, { stop }) { + constant = false; + stop(); + }, + UnaryExpression(node, { next, stop }) { + if (node.operator === 'delete') { + constant = false; + stop(); + return; + } + next(); + } + }); + return constant; +} + /** * Returns the name of the rune if the given expression is a `CallExpression` using a rune. * @param {Expression | Super} node