gh-17012
ComputerGuy 1 week ago
parent c8ef540985
commit 2edb05172a

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: allow `$state` in computed class fields

@ -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<string, StateField>;
state_fields: StateFields;
function_depth: number;

@ -30,7 +30,7 @@ export function ClassBody(node, context) {
}
}
/** @type {Map<string, StateField>} */
/** @type {Map<string | number, StateField>} */
const state_fields = new Map();
/** @type {Map<string, Array<MethodDefinition['kind'] | 'prop' | 'assigned_prop'>>} */
@ -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;
}

@ -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} */ (

@ -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
)
);
}
}

@ -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);
}

@ -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);
}

@ -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} */ (

@ -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
)
);
}
}

@ -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);
}

@ -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);
}

@ -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<AST.SvelteNode, Scope>;
readonly state_fields: Map<string, StateField>;
readonly state_fields: StateFields;
readonly computed_field_declarations: VariableDeclaration[] | null;
}

@ -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<ClassBody, Map<string, StateField>>;
classes: Map<ClassBody, StateFields>;
// TODO figure out if we can move this to ComponentAnalysis
accessors: boolean;

@ -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<string | number, StateField>;
export * from './template.js';
export { Binding, Scope } from '../phases/scope.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
};
/**

Loading…
Cancel
Save