From 2efb766f2360d095ceda3c09b527dd51f077f398 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 May 2025 16:25:16 -0400 Subject: [PATCH] WIP --- .../phases/2-analyze/visitors/ClassBody.js | 5 +- .../3-transform/client/transform-client.js | 8 +- .../phases/3-transform/client/types.d.ts | 9 +- .../client/visitors/AssignmentExpression.js | 66 ++++-- .../client/visitors/CallExpression.js | 23 +- .../3-transform/client/visitors/ClassBody.js | 211 +++++++++++++++++- .../client/visitors/MemberExpression.js | 4 +- .../client/visitors/UpdateExpression.js | 2 +- .../3-transform/server/transform-server.js | 4 +- .../phases/3-transform/server/types.d.ts | 4 +- .../server/visitors/AssignmentExpression.js | 31 ++- .../server/visitors/CallExpression.js | 9 + .../3-transform/server/visitors/ClassBody.js | 120 +++++++++- .../server/visitors/MemberExpression.js | 5 +- .../compiler/phases/3-transform/types.d.ts | 7 - packages/svelte/src/compiler/phases/nodes.js | 12 + packages/svelte/src/compiler/types/index.d.ts | 3 +- 17 files changed, 458 insertions(+), 65 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 46e1fc1a17..1fe22128f4 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js @@ -1,4 +1,4 @@ -/** @import { AssignmentExpression, ClassBody, PropertyDefinition, Expression, PrivateIdentifier, MethodDefinition } from 'estree' */ +/** @import { AssignmentExpression, CallExpression, ClassBody, PropertyDefinition, Expression, PrivateIdentifier, MethodDefinition } from 'estree' */ /** @import { StateField } from '#compiler' */ /** @import { Context } from '../types' */ import { get_rune } from '../../scope.js'; @@ -48,7 +48,8 @@ export function ClassBody(node, context) { state_fields[name] = { node, - type: rune + type: rune, + value: /** @type {CallExpression} */ (value) }; } 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 740bcae479..9a2f4dd34c 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 @@ -163,7 +163,8 @@ export function client_component(analysis, options) { }, events: new Set(), preserve_whitespace: options.preserveWhitespace, - class_transformer: null, + public_state: new Map(), + private_state: new Map(), transform: {}, in_constructor: false, instance_level_snippets: [], @@ -670,9 +671,10 @@ export function client_module(analysis, options) { options, scope: analysis.module.scope, scopes: analysis.module.scopes, + public_state: new Map(), + private_state: new Map(), transform: {}, - in_constructor: false, - class_transformer: null + in_constructor: false }; const module = /** @type {ESTree.Program} */ ( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index c2292292da..0401053425 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -3,6 +3,7 @@ import type { Statement, LabeledStatement, Identifier, + PrivateIdentifier, Expression, AssignmentExpression, UpdateExpression, @@ -12,10 +13,10 @@ import type { AST, Namespace, ValidatedCompileOptions } from '#compiler'; import type { TransformState } from '../types.js'; import type { ComponentAnalysis } from '../../types.js'; import type { SourceLocation } from '#shared'; -import type { ClassTransformer } from '../shared/types.js'; export interface ClientTransformState extends TransformState { - readonly class_transformer: ClassTransformer | null; + readonly private_state: Map; + readonly public_state: Map; /** * `true` if the current lexical scope belongs to a class constructor. this allows @@ -93,6 +94,10 @@ export interface ComponentClientTransformState extends ClientTransformState { readonly module_level_snippets: VariableDeclaration[]; } +export interface StateField { + type: '$state' | '$state.raw' | '$derived' | '$derived.by'; +} + export type Context = import('zimmerframe').Context; export type Visitors = import('zimmerframe').Visitors; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index d11938af97..d23ecb26db 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -1,4 +1,4 @@ -/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Pattern, MemberExpression, ThisExpression, PrivateIdentifier, CallExpression } from 'estree' */ +/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Pattern } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { Context } from '../types.js' */ import * as b from '#compiler/builders'; @@ -11,17 +11,14 @@ import { dev, locate_node } from '../../../../state.js'; import { should_proxy } from '../utils.js'; import { visit_assignment_expression } from '../../shared/assignments.js'; import { validate_mutation } from './shared/utils.js'; +import { get_rune } from '../../../scope.js'; +import { get_name } from '../../../nodes.js'; /** * @param {AssignmentExpression} node * @param {Context} context */ export function AssignmentExpression(node, context) { - const stripped_node = context.state.class_transformer?.generate_assignment(node, context); - if (stripped_node) { - return stripped_node; - } - const expression = /** @type {Expression} */ ( visit_assignment_expression(node, context, build_assignment) ?? context.next() ); @@ -55,25 +52,50 @@ const callees = { * @returns {Expression | null} */ function build_assignment(operator, left, right, context) { - // Handle class private/public state assignment cases - if ( - context.state.analysis.runes && - left.type === 'MemberExpression' && - left.property.type === 'PrivateIdentifier' - ) { - const private_state = context.state.class_transformer?.get_field(left.property.name, true); + if (context.state.analysis.runes && left.type === 'MemberExpression') { + // special case — state declaration in class constructor + const ancestor = context.path.at(-4); + + if (ancestor?.type === 'MethodDefinition' && ancestor.kind === 'constructor') { + const rune = get_rune(right, context.state.scope); + + if (rune) { + const name = get_name(left.property); + + const child_state = { + ...context.state, + in_constructor: rune !== '$derived' && rune !== '$derived.by' + }; - if (private_state !== undefined) { - let value = /** @type {Expression} */ ( - context.visit(build_assignment_value(operator, left, right)) - ); + const l = b.member( + b.this, + left.property.type === 'PrivateIdentifier' + ? left.property + : context.state.backing_fields[name] + ); - const needs_proxy = - private_state?.kind === '$state' && - is_non_coercive_operator(operator) && - should_proxy(value, context.state.scope); + const r = /** @type {Expression} */ (context.visit(right, child_state)); - return b.call('$.set', left, value, needs_proxy && b.true); + return b.assignment(operator, l, r); + } + } + + // special case — assignment to private state field + if (left.property.type === 'PrivateIdentifier') { + const private_state = context.state.private_state.get(left.property.name); + + if (private_state !== undefined) { + let value = /** @type {Expression} */ ( + context.visit(build_assignment_value(operator, left, right)) + ); + + const needs_proxy = + private_state.type === '$state' && + is_non_coercive_operator(operator) && + should_proxy(value, context.state.scope); + + return b.call('$.set', left, value, needs_proxy && b.true); + } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index b110f8eae8..dd336e397d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -10,13 +10,34 @@ import { transform_inspect_rune } from '../../utils.js'; * @param {Context} context */ export function CallExpression(node, context) { - switch (get_rune(node, context.state.scope)) { + const rune = get_rune(node, context.state.scope); + + switch (rune) { case '$host': return b.id('$$props.$$host'); case '$effect.tracking': return b.call('$.effect_tracking'); + case '$state': + case '$state.raw': { + let should_proxy = rune === '$state' && true; // TODO + + return b.call( + '$.state', + node.arguments[0] && /** @type {Expression} */ (context.visit(node.arguments[0])), + should_proxy && b.true + ); + } + + case '$derived': + case '$derived.by': { + let fn = /** @type {Expression} */ (context.visit(node.arguments[0])); + if (rune === '$derived') fn = b.thunk(fn); + + return b.call('$.derived', fn); + } + case '$state.snapshot': return b.call( '$.snapshot', 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 75f2b35cca..5501e5f72d 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,19 +1,218 @@ -/** @import { ClassBody } from 'estree' */ -/** @import { Context } from '../types' */ -import { create_client_class_transformer } from './shared/client-class-transformer.js'; +/** @import { CallExpression, ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition, StaticBlock } from 'estree' */ +/** @import { Context, StateField } from '../types' */ +import * as b from '#compiler/builders'; +import { get_name } from '../../../nodes.js'; +import { regex_invalid_identifier_chars } from '../../../patterns.js'; +import { get_rune } from '../../../scope.js'; +import { should_proxy } from '../utils.js'; /** * @param {ClassBody} node * @param {Context} context */ export function ClassBody(node, context) { - if (!context.state.analysis.runes) { + const state_fields = context.state.analysis.classes.get(node); + + if (!state_fields) { + // in legacy mode, do nothing context.next(); return; } - const class_transformer = create_client_class_transformer(node.body); - const body = class_transformer.generate_body(context); + /** @type {string[]} */ + const private_ids = []; + + for (const prop of node.body) { + if ( + (prop.type === 'MethodDefinition' || prop.type === 'PropertyDefinition') && + prop.key.type === 'PrivateIdentifier' + ) { + private_ids.push(prop.key.name); + } + } + + const private_state = new Map(); + + /** + * each `foo = $state()` needs a backing `#foo` field + * @type {Record} + */ + const backing_fields = {}; + + for (const name in state_fields) { + if (name[0] === '#') { + private_state.set(name.slice(1), state_fields[name]); + continue; + } + + let deconflicted = name.replace(regex_invalid_identifier_chars, '_'); + while (private_ids.includes(deconflicted)) { + deconflicted = '_' + deconflicted; + } + + private_ids.push(deconflicted); + backing_fields[name] = b.private_id(deconflicted); + } + + /** @type {Array} */ + const body = []; + + const child_state = { ...context.state, state_fields, backing_fields, private_state }; // TODO populate private_state + + for (const name in state_fields) { + if (name[0] === '#') { + continue; + } + + const field = state_fields[name]; + + // insert backing fields for stuff declared in the constructor + if (field.node.type === 'AssignmentExpression') { + const backing = backing_fields[name]; + const member = b.member(b.this, backing); + + const should_proxy = field.type === '$state' && true; // TODO + + const key = b.key(name); + + body.push( + b.prop_def(backing, 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))] + ) + ); + } + } + + // Replace parts of the class body + for (const definition of node.body) { + if (definition.type === 'MethodDefinition' || definition.type === 'StaticBlock') { + body.push( + /** @type {MethodDefinition | StaticBlock} */ (context.visit(definition, child_state)) + ); + continue; + } + + const name = get_name(definition.key); + if (name === null || !Object.hasOwn(state_fields, name)) { + body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state))); + continue; + } + + const field = state_fields[name]; + + if (name[0] === '#') { + body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state))); + } else { + const backing = backing_fields[name]; + const member = b.member(b.this, backing); + + const should_proxy = field.type === '$state' && true; // TODO + + body.push( + b.prop_def( + backing, + /** @type {CallExpression} */ ( + context.visit(definition.value ?? field.value, child_state) + ) + ), + + b.method('get', definition.key, [], [b.return(b.call('$.get', member))]), + + b.method( + 'set', + definition.key, + [b.id('value')], + [b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))] + ) + ); + } + + // if (definition.type === 'PropertyDefinition') { + // const original_name = get_name(definition.key); + // if (original_name === null) continue; + + // const name = definition_names[original_name]; + + // const is_private = definition.key.type === 'PrivateIdentifier'; + // const field = (is_private ? private_state : public_state).get(name); + + // if (definition.value?.type === 'CallExpression' && field !== undefined) { + // let value = null; + + // if (definition.value.arguments.length > 0) { + // const init = /** @type {Expression} **/ ( + // context.visit(definition.value.arguments[0], child_state) + // ); + + // value = + // field.kind === 'state' + // ? b.call( + // '$.state', + // should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init + // ) + // : field.kind === 'raw_state' + // ? b.call('$.state', init) + // : field.kind === 'derived_by' + // ? b.call('$.derived', init) + // : b.call('$.derived', b.thunk(init)); + // } else { + // // if no arguments, we know it's state as `$derived()` is a compile error + // value = b.call('$.state'); + // } + + // if (is_private) { + // body.push(b.prop_def(field.id, value)); + // } else { + // // #foo; + // const member = b.member(b.this, field.id); + // body.push(b.prop_def(field.id, value)); + + // // get foo() { return this.#foo; } + // body.push(b.method('get', definition.key, [], [b.return(b.call('$.get', member))])); + + // // set foo(value) { this.#foo = value; } + // const val = b.id('value'); + + // body.push( + // b.method( + // 'set', + // definition.key, + // [val], + // [b.stmt(b.call('$.set', member, val, field.kind === 'state' && b.true))] + // ) + // ); + // } + // continue; + // } + // } + + // body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state))); + } return { ...node, body }; } + +/** + * @param {string} name + * @param {Map} public_state + */ +function get_deconflicted_name(name, public_state) { + name = name.replace(regex_invalid_identifier_chars, '_'); + + // the above could generate conflicts because it has to generate a valid identifier + // so stuff like `0` and `1` or `state%` and `state^` will result in the same string + // so we have to de-conflict. We can only check `public_state` because private state + // can't have literal keys + while (name && public_state.has(name)) { + name = '_' + name; + } + + return name; +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js index 02052942f5..bc9d263670 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js @@ -9,10 +9,10 @@ import * as b from '#compiler/builders'; export function MemberExpression(node, context) { // rewrite `this.#foo` as `this.#foo.v` inside a constructor if (node.property.type === 'PrivateIdentifier') { - const field = context.state.class_transformer?.get_field(node.property.name, true); + const field = context.state.private_state.get(node.property.name); if (field) { return context.state.in_constructor && - (field.kind === '$state.raw' || field.kind === '$state') + (field.type === '$state.raw' || field.type === '$state') ? b.member(node, 'v') : b.call('$.get', node); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js index ca81fa1493..96be119b84 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js @@ -15,7 +15,7 @@ export function UpdateExpression(node, context) { argument.type === 'MemberExpression' && argument.object.type === 'ThisExpression' && argument.property.type === 'PrivateIdentifier' && - context.state.class_transformer?.get_field(argument.property.name, true) + context.state.private_state.has(argument.property.name) ) { let fn = '$.update'; if (node.prefix) fn += '_pre'; 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 661b73d659..e7896991d9 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 @@ -99,7 +99,7 @@ export function server_component(analysis, options) { template: /** @type {any} */ (null), namespace: options.namespace, preserve_whitespace: options.preserveWhitespace, - class_transformer: null, + private_derived: new Map(), skip_hydration_boundaries: false }; @@ -395,7 +395,7 @@ 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(), - class_transformer: null + private_derived: new Map() }; const module = /** @type {Program} */ ( diff --git a/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts index 24b35d3761..971271642c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts @@ -2,12 +2,12 @@ import type { Expression, Statement, ModuleDeclaration, LabeledStatement } from import type { AST, Namespace, ValidatedCompileOptions } from '#compiler'; import type { TransformState } from '../types.js'; import type { ComponentAnalysis } from '../../types.js'; -import type { ClassTransformer } from '../shared/types.js'; +import type { StateField } from '../client/types.js'; export interface ServerTransformState extends TransformState { /** The $: calls, which will be ordered in the end */ readonly legacy_reactive_statements: Map; - readonly class_transformer: ClassTransformer | null; + readonly private_derived: Map; } export interface ComponentServerTransformState extends ServerTransformState { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js index 939dd45ca2..c0dd77c64e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js @@ -3,6 +3,8 @@ /** @import { Context, ServerTransformState } from '../types.js' */ import * as b from '#compiler/builders'; import { build_assignment_value } from '../../../../utils/ast.js'; +import { get_name } from '../../../nodes.js'; +import { get_rune } from '../../../scope.js'; import { visit_assignment_expression } from '../../shared/assignments.js'; /** @@ -10,11 +12,6 @@ import { visit_assignment_expression } from '../../shared/assignments.js'; * @param {Context} context */ export function AssignmentExpression(node, context) { - const stripped_node = context.state.class_transformer?.generate_assignment(node, context); - if (stripped_node) { - return stripped_node; - } - return visit_assignment_expression(node, context, build_assignment) ?? context.next(); } @@ -27,6 +24,30 @@ export function AssignmentExpression(node, context) { * @returns {Expression | null} */ function build_assignment(operator, left, right, context) { + if (context.state.analysis.runes && left.type === 'MemberExpression') { + // special case — state declaration in class constructor + const ancestor = context.path.at(-4); + + if (ancestor?.type === 'MethodDefinition' && ancestor.kind === 'constructor') { + const rune = get_rune(right, context.state.scope); + + if (rune) { + const name = get_name(left.property); + + const l = b.member( + b.this, + left.property.type === 'PrivateIdentifier' || rune === '$state' || rune === '$state.raw' + ? left.property + : context.state.backing_fields[name] + ); + + const r = /** @type {Expression} */ (context.visit(right)); + + return b.assignment(operator, l, r); + } + } + } + let object = left; while (object.type === 'MemberExpression') { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js index 5bcbdee9fb..e36dc820b3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js @@ -25,6 +25,15 @@ export function CallExpression(node, context) { return b.arrow([], b.block([])); } + if (rune === '$state' || rune === '$state.raw') { + return node.arguments[0] ? context.visit(node.arguments[0]) : b.void0; + } + + if (rune === '$derived' || rune === '$derived.by') { + const fn = /** @type {Expression} */ (context.visit(node.arguments[0])); + return b.call('$.once', rune === '$derived' ? b.thunk(fn) : fn); + } + if (rune === '$state.snapshot') { return b.call( '$.snapshot', 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 3bab89cf9b..a59dea5cc1 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,19 +1,129 @@ -/** @import { ClassBody } from 'estree' */ +/** @import { CallExpression, ClassBody, Expression, MethodDefinition, PrivateIdentifier, PropertyDefinition, StaticBlock } from 'estree' */ /** @import { Context } from '../types.js' */ -import { create_server_class_transformer } from './shared/server-class-transformer.js'; +/** @import { StateField } from '../../client/types.js' */ +import { dev } from '../../../../state.js'; +import * as b from '#compiler/builders'; +import { get_rune } from '../../../scope.js'; +import { regex_invalid_identifier_chars } from '../../../patterns.js'; +import { get_name } from '../../../nodes.js'; /** * @param {ClassBody} node * @param {Context} context */ export function ClassBody(node, context) { - if (!context.state.analysis.runes) { + const state_fields = context.state.analysis.classes.get(node); + + if (!state_fields) { + // in legacy mode, do nothing context.next(); return; } - const class_transformer = create_server_class_transformer(node.body); - const body = class_transformer.generate_body(context); + /** @type {string[]} */ + const private_ids = []; + + for (const prop of node.body) { + if ( + (prop.type === 'MethodDefinition' || prop.type === 'PropertyDefinition') && + prop.key.type === 'PrivateIdentifier' + ) { + private_ids.push(prop.key.name); + } + } + + const private_state = new Map(); + + /** + * each `foo = $state()` needs a backing `#foo` field + * @type {Record} + */ + const backing_fields = {}; + + for (const name in state_fields) { + if (name[0] === '#') { + private_state.set(name.slice(1), state_fields[name]); + continue; + } + + let deconflicted = name.replace(regex_invalid_identifier_chars, '_'); + while (private_ids.includes(deconflicted)) { + deconflicted = '_' + deconflicted; + } + + private_ids.push(deconflicted); + backing_fields[name] = b.private_id(deconflicted); + } + + /** @type {Array} */ + const body = []; + + const child_state = { ...context.state, state_fields, backing_fields, private_state }; // TODO populate private_state + + for (const name in state_fields) { + if (name[0] === '#') { + continue; + } + + const field = state_fields[name]; + + // insert backing fields for stuff declared in the constructor + if ( + field.node.type === 'AssignmentExpression' && + (field.type === '$derived' || field.type === '$derived.by') + ) { + const backing = backing_fields[name]; + const member = b.member(b.this, backing); + + const should_proxy = field.type === '$state' && true; // TODO + + const key = b.key(name); + + body.push( + b.prop_def(backing, null), + + b.method('get', key, [], [b.return(b.call(member))]) + ); + } + } + + // Replace parts of the class body + for (const definition of node.body) { + if (definition.type === 'MethodDefinition' || definition.type === 'StaticBlock') { + body.push( + /** @type {MethodDefinition | StaticBlock} */ (context.visit(definition, child_state)) + ); + continue; + } + + const name = get_name(definition.key); + if (name === null || !Object.hasOwn(state_fields, name)) { + body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state))); + continue; + } + + const field = state_fields[name]; + + if (name[0] === '#' || field.type === '$state' || field.type === '$state.raw') { + body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state))); + } else { + const backing = backing_fields[name]; + const member = b.member(b.this, backing); + + const should_proxy = field.type === '$state' && true; // TODO + + body.push( + b.prop_def( + backing, + /** @type {CallExpression} */ ( + context.visit(definition.value ?? field.value, child_state) + ) + ), + + b.method('get', definition.key, [], [b.return(b.call(member))]) + ); + } + } return { ...node, body }; } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js index 2a90307293..73631395e6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js @@ -12,10 +12,7 @@ export function MemberExpression(node, context) { node.object.type === 'ThisExpression' && node.property.type === 'PrivateIdentifier' ) { - const field = context.state.class_transformer?.get_field(node.property.name, true, [ - '$derived', - '$derived.by' - ]); + const field = context.state.private_derived.get(node.property.name); if (field) { return b.call(node); 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 c610110cef..5d860207dd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/types.d.ts @@ -1,8 +1,6 @@ import type { Scope } from '../scope.js'; import type { AST, ValidatedModuleCompileOptions } from '#compiler'; import type { Analysis } from '../types.js'; -import type { StateCreationRuneName } from '../../../utils.js'; -import type { PrivateIdentifier } from 'estree'; export interface TransformState { readonly analysis: Analysis; @@ -10,8 +8,3 @@ export interface TransformState { readonly scope: Scope; readonly scopes: Map; } - -export interface StateField { - kind: StateCreationRuneName; - id: PrivateIdentifier; -} diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index 29fb8c5c83..2043747ed0 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -1,3 +1,4 @@ +/** @import { Expression, PrivateIdentifier } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ /** @@ -65,3 +66,14 @@ export function create_expression_metadata() { has_call: false }; } + +/** + * @param {Expression | PrivateIdentifier} node + */ +export function get_name(node) { + if (node.type === 'Literal') return String(node.value); + if (node.type === 'PrivateIdentifier') return '#' + node.name; + if (node.type === 'Identifier') return node.name; + + return null; +} diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 8f96b3308f..f91bceac80 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -3,7 +3,7 @@ import type { Binding } from '../phases/scope.js'; import type { AST, Namespace } from './template.js'; import type { ICompileDiagnostic } from '../utils/compile_diagnostic.js'; import type { StateCreationRuneName } from '../../utils.js'; -import type { AssignmentExpression, PropertyDefinition } from 'estree'; +import type { AssignmentExpression, CallExpression, PropertyDefinition } from 'estree'; /** The return value of `compile` from `svelte/compiler` */ export interface CompileResult { @@ -274,6 +274,7 @@ export interface ExpressionMetadata { export interface StateField { type: StateCreationRuneName; node: PropertyDefinition | AssignmentExpression; + value: CallExpression; } export * from './template.js';