diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-transformer.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-transformer.js deleted file mode 100644 index 767a99ea69..0000000000 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-transformer.js +++ /dev/null @@ -1,95 +0,0 @@ -/** @import { Context } from '../../types.js' */ -/** @import { MethodDefinition, PropertyDefinition, Expression, StaticBlock, SpreadElement } from 'estree' */ -/** @import { StateCreationRuneName } from '../../../../../../utils.js' */ -/** @import { AssignmentBuilder, ClassTransformer, StateFieldBuilder } from '../../../shared/types.js' */ -import * as b from '#compiler/builders'; -import { create_class_transformer } from '../../../shared/class_transformer.js'; -import { should_proxy } from '../../utils.js'; - -/** - * @param {Array} body - * @returns {ClassTransformer} - */ -export function create_client_class_transformer(body) { - /** @type {StateFieldBuilder} */ - function build_state_field({ is_private, field, node, context }) { - let original_id = node.type === 'AssignmentExpression' ? node.left.property : node.key; - let value; - if (node.type === 'AssignmentExpression') { - // if there's no call expression, this is state that's created in the constructor. - // it's guaranteed to be the very first assignment to this field, so we initialize - // the field but don't assign to it. - value = null; - } else if (node.value.arguments.length > 0) { - value = build_init_value(field.kind, node.value.arguments[0], context); - } else { - // if no arguments, we know it's state as `$derived()` is a compile error - value = b.call('$.state'); - } - - if (is_private) { - return [b.prop_def(field.id, value)]; - } - - const member = b.member(b.this, field.id); - const val = b.id('value'); - - return [ - // #foo; - b.prop_def(field.id, value), - // get foo() { return this.#foo; } - b.method('get', original_id, [], [b.return(b.call('$.get', member))]), - // set foo(value) { this.#foo = value; } - b.method( - 'set', - original_id, - [val], - [b.stmt(b.call('$.set', member, val, field.kind === '$state' && b.true))] - ) - ]; - } - - /** @type {AssignmentBuilder} */ - function build_assignment({ field, node, context }) { - return { - ...node, - left: { - ...node.left, - // ...swap out the assignment to go directly against the private field - property: field.id, - // this could be a transformation from `this.[1]` to `this.#_` (the private field we generated) - // -- private fields are never computed - computed: false - }, - // ...and swap out the assignment's value for the state field init - right: build_init_value(field.kind, node.right.arguments[0], context) - }; - } - - return create_class_transformer(body, build_state_field, build_assignment); -} - -/** - * @param {StateCreationRuneName} kind - * @param {Expression | SpreadElement} arg - * @param {Context} context - */ -function build_init_value(kind, arg, context) { - const init = arg - ? /** @type {Expression} **/ (context.visit(arg, { ...context.state, in_constructor: false })) - : b.void0; - - switch (kind) { - case '$state': - return b.call( - '$.state', - should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init - ); - case '$state.raw': - return b.call('$.state', init); - case '$derived': - return b.call('$.derived', b.thunk(init)); - case '$derived.by': - return b.call('$.derived', init); - } -} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-transformer.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-transformer.js deleted file mode 100644 index 7a52f14731..0000000000 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-transformer.js +++ /dev/null @@ -1,103 +0,0 @@ -/** @import { Expression, MethodDefinition, StaticBlock, PropertyDefinition, SpreadElement } from 'estree' */ -/** @import { Context } from '../../types.js' */ -/** @import { AssignmentBuilder, StateFieldBuilder } from '../../../shared/types.js' */ -/** @import { ClassTransformer } from '../../../shared/types.js' */ -/** @import { StateCreationRuneName } from '../../../../../../utils.js' */ - -import * as b from '#compiler/builders'; -import { dev } from '../../../../../state.js'; -import { create_class_transformer } from '../../../shared/class_transformer.js'; - -/** - * @param {Array} body - * @returns {ClassTransformer} - */ -export function create_server_class_transformer(body) { - /** @type {StateFieldBuilder} */ - function build_state_field({ is_private, field, node, context }) { - let original_id = node.type === 'AssignmentExpression' ? node.left.property : node.key; - let value; - if (node.type === 'AssignmentExpression') { - // This means it's a state assignment in the constructor (this.foo = $state('bar')) - // which means the state field needs to have no default value so that the initial - // value can be assigned in the constructor. - value = null; - } else if (field.kind !== '$derived' && field.kind !== '$derived.by') { - return [/** @type {PropertyDefinition} */ (context.visit(node))]; - } else { - const init = /** @type {Expression} **/ (context.visit(node.value.arguments[0])); - value = - field.kind === '$derived.by' ? b.call('$.once', init) : b.call('$.once', b.thunk(init)); - } - - if (is_private) { - return [b.prop_def(field.id, value)]; - } - // #foo; - const member = b.member(b.this, field.id); - - /** @type {Array} */ - const defs = [ - // #foo; - b.prop_def(field.id, value) - ]; - - // get foo() { return this.#foo; } - if (field.kind === '$state' || field.kind === '$state.raw') { - defs.push(b.method('get', original_id, [], [b.return(member)])); - } else { - defs.push(b.method('get', original_id, [], [b.return(b.call(member))])); - } - - // TODO make this work on server - if (dev) { - defs.push( - b.method( - 'set', - original_id, - [b.id('_')], - [b.throw_error(`Cannot update a derived property ('${name}')`)] - ) - ); - } - - return defs; - } - - /** @type {AssignmentBuilder} */ - function build_assignment({ field, node, context }) { - return { - ...node, - left: { - ...node.left, - // ...swap out the assignment to go directly against the private field - property: field.id, - computed: false - }, - // ...and swap out the assignment's value for the state field init - right: build_init_value(field.kind, node.right.arguments[0], context) - }; - } - - return create_class_transformer(body, build_state_field, build_assignment); -} - -/** - * - * @param {StateCreationRuneName} kind - * @param {Expression | SpreadElement} arg - * @param {Context} context - */ -function build_init_value(kind, arg, context) { - const init = arg ? /** @type {Expression} **/ (context.visit(arg)) : b.void0; - - switch (kind) { - case '$state': - case '$state.raw': - return init; - case '$derived': - return b.call('$.once', b.thunk(init)); - case '$derived.by': - return b.call('$.once', init); - } -} diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/class_transformer.js b/packages/svelte/src/compiler/phases/3-transform/shared/class_transformer.js deleted file mode 100644 index 469a178cc2..0000000000 --- a/packages/svelte/src/compiler/phases/3-transform/shared/class_transformer.js +++ /dev/null @@ -1,392 +0,0 @@ -/** @import { AssignmentExpression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition, StaticBlock } from 'estree' */ -/** @import { StateField } from '../types.js' */ -/** @import { Context as ClientContext } from '../client/types.js' */ -/** @import { Context as ServerContext } from '../server/types.js' */ -/** @import { StateCreationRuneName } from '../../../../utils.js' */ -/** @import { AssignmentBuilder, ClassTransformer, StateFieldBuilder, StatefulAssignment, StatefulPropertyDefinition } from './types.js' */ -/** @import { Scope } from '../../scope.js' */ -import * as b from '#compiler/builders'; -import { once } from '../../../../internal/server/index.js'; -import { is_state_creation_rune, STATE_CREATION_RUNES } from '../../../../utils.js'; -import { regex_invalid_identifier_chars } from '../../patterns.js'; -import { get_rune } from '../../scope.js'; - -/** - * @template {ClientContext | ServerContext} TContext - * @param {Array} body - * @param {StateFieldBuilder} build_state_field - * @param {AssignmentBuilder} build_assignment - * @returns {ClassTransformer} - */ -export function create_class_transformer(body, build_state_field, build_assignment) { - /** - * Public, stateful fields. - * @type {Map} - */ - const public_fields = new Map(); - - /** - * Private, stateful fields. These are namespaced separately because - * public and private fields can have the same name in the AST -- ex. - * `count` and `#count` are both named `count` -- and because it's useful - * in a couple of cases to be able to check for only one or the other. - * @type {Map} - */ - const private_fields = new Map(); - - /** - * Accumulates nodes for the new class body. - * @type {Array} - */ - const new_body = []; - - /** - * Private identifiers in use by this analysis. - * Factoid: Unlike public class fields, private fields _must_ be declared in the class body - * before use. So the following is actually a JavaScript syntax error, which means we can - * be 100% certain we know all private fields after parsing the class body: - * - * ```ts - * class Example { - * constructor() { - * this.public = 'foo'; // not a problem! - * this.#private = 'bar'; // JavaScript parser error - * } - * } - * ``` - * @type {Set} - */ - const private_ids = new Set(); - - /** - * A registry of functions to call to complete body modifications. - * Replacements may insert more than one node to the body. The original - * body should not be modified -- instead, replacers should push new - * nodes to new_body. - * - * @type {Array<() => void>} - */ - const replacers = []; - - /** - * Get a state field by name. - * - * @param {string} name - * @param {boolean} is_private - * @param {ReadonlyArray} [kinds] - */ - function get_field(name, is_private, kinds = STATE_CREATION_RUNES) { - const value = (is_private ? private_fields : public_fields).get(name); - if (value && kinds.includes(value.kind)) { - return value; - } - } - - /** - * Create a child context that makes sense for passing to the child analyzers. - * @param {TContext} context - * @returns {TContext} - */ - function create_child_context(context) { - const state = { - ...context.state, - class_transformer - }; - // @ts-expect-error - I can't find a way to make TypeScript happy with these - const visit = (node, state_override) => context.visit(node, { ...state, ...state_override }); - // @ts-expect-error - I can't find a way to make TypeScript happy with these - const next = (state_override) => context.next({ ...state, ...state_override }); - return { - ...context, - state, - visit, - next - }; - } - - /** - * Generate a new body for the class. Ensure there is a visitor for AssignmentExpression that - * calls `generate_assignment` to capture any stateful fields declared in the constructor. - * @param {TContext} context - */ - function generate_body(context) { - const child_context = create_child_context(context); - for (const node of body) { - const was_registered = register_body_definition(node, child_context); - if (!was_registered) { - new_body.push( - /** @type {PropertyDefinition | MethodDefinition} */ ( - // @ts-expect-error generics silliness - child_context.visit(node, child_context.state) - ) - ); - } - } - - for (const replacer of replacers) { - replacer(); - } - - return new_body; - } - - /** - * Given an assignment expression, check to see if that assignment expression declares - * a stateful field. If it does, register that field and then return the processed - * assignment expression. If an assignment expression is returned from this function, - * it should be considered _fully processed_ and should replace the existing assignment - * expression node. - * @param {AssignmentExpression} node - * @param {TContext} context - * @returns {AssignmentExpression | null} The node, if `register_assignment` handled its transformation. - */ - function generate_assignment(node, context) { - const child_context = create_child_context(context); - if ( - !( - node.operator === '=' && - node.left.type === 'MemberExpression' && - node.left.object.type === 'ThisExpression' && - (node.left.property.type === 'Identifier' || - node.left.property.type === 'PrivateIdentifier' || - node.left.property.type === 'Literal') - ) - ) { - return null; - } - - const name = get_name(node.left.property); - if (!name) { - return null; - } - - const parsed = parse_stateful_assignment(node, child_context.state.scope); - if (!parsed) { - return null; - } - const { stateful_assignment, rune } = parsed; - - const is_private = stateful_assignment.left.property.type === 'PrivateIdentifier'; - - let field; - if (is_private) { - field = { - kind: rune, - id: /** @type {PrivateIdentifier} */ (stateful_assignment.left.property) - }; - private_fields.set(name, field); - } else { - field = { - kind: rune, - // it's safe to do this upfront now because we're guaranteed to already know about all private - // identifiers (they had to have been declared at the class root, before we visited the constructor) - id: deconflict(name) - }; - public_fields.set(name, field); - } - - const replacer = () => { - const nodes = build_state_field({ - is_private, - field, - node: stateful_assignment, - context: child_context - }); - if (!nodes) { - return; - } - new_body.push(...nodes); - }; - replacers.push(replacer); - - return build_assignment({ - field, - node: stateful_assignment, - context: child_context - }); - } - - /** - * Register a class body definition. - * - * @param {PropertyDefinition | MethodDefinition | StaticBlock} node - * @param {TContext} child_context - * @returns {boolean} if this node is stateful and was registered - */ - function register_body_definition(node, child_context) { - if (node.type === 'MethodDefinition' && node.kind === 'constructor') { - // life is easier to reason about if we've visited the constructor - // and registered its public state field before we start building - // anything else - replacers.unshift(() => { - new_body.push( - /** @type {MethodDefinition} */ ( - // @ts-expect-error generics silliness - child_context.visit(node, child_context.state) - ) - ); - }); - return true; - } - - if ( - !( - (node.type === 'PropertyDefinition' || node.type === 'MethodDefinition') && - (node.key.type === 'Identifier' || - node.key.type === 'PrivateIdentifier' || - node.key.type === 'Literal') - ) - ) { - return false; - } - - /* - * We don't know if the node is stateful yet, but we still need to register some details. - * For example: If the node is a private identifier, we could accidentally conflict with it later - * if we create a private field for public state (as would happen in this example:) - * - * ```ts - * class Foo { - * #count = 0; - * count = $state(0); // would become #count if we didn't know about the private field above - * } - */ - - const name = get_name(node.key); - if (!name) { - return false; - } - - const is_private = node.key.type === 'PrivateIdentifier'; - if (is_private) { - private_ids.add(name); - } - - const parsed = prop_def_is_stateful(node, child_context.state.scope); - if (!parsed) { - // this isn't a stateful field definition, but if could become one in the constructor -- so we register - // it, but conditionally -- so that if it's added as a field in the constructor (which causes us to create) - // a field definition for it), we don't end up with a duplicate definition (this one, plus the one we create) - replacers.push(() => { - if (!get_field(name, is_private)) { - new_body.push( - /** @type {PropertyDefinition | MethodDefinition} */ ( - // @ts-expect-error generics silliness - child_context.visit(node, child_context.state) - ) - ); - } - }); - return true; - } - const { stateful_prop_def, rune } = parsed; - - let field; - if (is_private) { - field = { - kind: rune, - id: /** @type {PrivateIdentifier} */ (stateful_prop_def.key) - }; - private_fields.set(name, field); - } else { - // We can't set the ID until we've identified all of the private state fields, - // otherwise we might conflict with them. After registering all property definitions, - // call `finalize_property_definitions` to populate the IDs. So long as we don't - // access the ID before the end of this loop, we're fine! - const id = once(() => deconflict(name)); - field = { - kind: rune, - get id() { - return id(); - } - }; - public_fields.set(name, field); - } - - const replacer = () => { - const nodes = build_state_field({ - is_private, - field, - node: stateful_prop_def, - context: child_context - }); - if (!nodes) { - return; - } - new_body.push(...nodes); - }; - replacers.push(replacer); - - return true; - } - - /** - * @param {string} name - * @returns {PrivateIdentifier} - */ - function deconflict(name) { - let deconflicted = name; - while (private_ids.has(deconflicted)) { - deconflicted = '_' + deconflicted; - } - - private_ids.add(deconflicted); - return b.private_id(deconflicted); - } - - /** - * @param {Identifier | PrivateIdentifier | Literal} node - */ - function get_name(node) { - if (node.type === 'Literal') { - let name = node.value?.toString().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_fields` because private state - // can't have literal keys - while (name && public_fields.has(name)) { - name = '_' + name; - } - return name; - } else { - return node.name; - } - } - - const class_transformer = { - get_field, - generate_body, - generate_assignment - }; - - return class_transformer; -} - -/** - * `get_rune` is really annoying because it really guarantees this already - * we just need this to tell the type system about it - * @param {AssignmentExpression} node - * @param {Scope} scope - * @returns {{ stateful_assignment: StatefulAssignment, rune: StateCreationRuneName } | null} - */ -function parse_stateful_assignment(node, scope) { - const rune = get_rune(node.right, scope); - if (!rune || !is_state_creation_rune(rune)) { - return null; - } - return { stateful_assignment: /** @type {StatefulAssignment} */ (node), rune }; -} - -/** - * @param {PropertyDefinition | MethodDefinition} node - * @param {Scope} scope - * @returns {{ stateful_prop_def: StatefulPropertyDefinition, rune: StateCreationRuneName } | null} - */ -function prop_def_is_stateful(node, scope) { - const rune = get_rune(node.value, scope); - if (!rune || !is_state_creation_rune(rune)) { - return null; - } - return { stateful_prop_def: /** @type {StatefulPropertyDefinition} */ (node), rune }; -} diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts deleted file mode 100644 index 7c03be2235..0000000000 --- a/packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { - AssignmentExpression, - CallExpression, - Identifier, - MemberExpression, - PropertyDefinition, - MethodDefinition, - PrivateIdentifier, - ThisExpression, - Literal -} from 'estree'; -import type { StateField } from '../types'; -import type { Context as ServerContext } from '../server/types'; -import type { Context as ClientContext } from '../client/types'; -import type { StateCreationRuneName } from '../../../../utils'; - -export type StatefulAssignment = AssignmentExpression & { - left: MemberExpression & { - object: ThisExpression; - property: Identifier | PrivateIdentifier | Literal; - }; - right: CallExpression; -}; - -export type StatefulPropertyDefinition = PropertyDefinition & { - key: Identifier | PrivateIdentifier | Literal; - value: CallExpression; -}; - -export type StateFieldBuilderParams = { - is_private: boolean; - field: StateField; - node: StatefulAssignment | StatefulPropertyDefinition; - context: TContext; -}; - -export type StateFieldBuilder = ( - params: StateFieldBuilderParams -) => Array; - -export type AssignmentBuilderParams = { - node: StatefulAssignment; - field: StateField; - context: TContext; -}; - -export type AssignmentBuilder = ( - params: AssignmentBuilderParams -) => AssignmentExpression; - -export type ClassTransformer = { - /** - * @param name - The name of the field. - * @param is_private - Whether the field is private (whether its name starts with '#'). - * @param kinds - What kinds of state creation runes you're looking for, eg. only '$derived.by'. - * @returns The field if it exists and matches the given criteria, or null. - */ - get_field: ( - name: string, - is_private: boolean, - kinds?: Array - ) => StateField | undefined; - - /** - * Given the body of a class, generate a new body with stateful fields. - * This assumes that {@link register_assignment} is registered to be called - * for all `AssignmentExpression` nodes in the class body. - * @param context - The context associated with the `ClassBody`. - * @returns The new body. - */ - generate_body: (context: TContext) => Array; - - /** - * Register an assignment expression. This checks to see if the assignment is creating - * a state field on the class. If it is, it registers that state field and modifies the - * assignment expression. - */ - generate_assignment: ( - node: AssignmentExpression, - context: TContext - ) => AssignmentExpression | null; -};