pull/17022/merge
ComputerGuy 23 hours ago committed by GitHub
commit 2d57078af7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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, StateField, ValidatedCompileOptions } from '#compiler';
import type { AST, StateFields, ValidatedCompileOptions } from '#compiler';
import type { ExpressionMetadata } from '../nodes.js';
export interface AnalysisState {
@ -22,7 +22,7 @@ export interface AnalysisState {
expression: ExpressionMetadata | null;
/** Used to analyze class state */
state_fields: Map<string, StateField>;
state_fields: StateFields;
function_depth: number;

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

@ -30,10 +30,10 @@ 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'>>} */
/** @type {Map<string | number, Array<MethodDefinition['kind'] | 'prop' | 'assigned_prop'>>} */
const fields = new Map();
context.state.analysis.classes.set(node, state_fields);
@ -41,28 +41,36 @@ 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);
}
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, {
@ -70,15 +78,19 @@ 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
});
}
}
for (const child of node.body) {
if (child.type === 'PropertyDefinition' && !child.computed && !child.static) {
handle(child, child.key, child.value);
const key = /** @type {string} */ (get_name(child.key));
if (child.type === 'PropertyDefinition' && !child.static) {
handle(child, child.key, child.value, child.computed && child.key.type !== 'Literal');
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']);
@ -91,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]);
@ -132,18 +148,25 @@ 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 && left.property.type !== 'Literal'
);
}
}
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 = `${typeof name === 'number' ? '_' : ''}${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';
@ -45,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';
@ -97,6 +100,8 @@ const visitors = {
BreakStatement,
CallExpression,
ClassBody,
ClassDeclaration,
ClassExpression,
Comment,
Component,
ConstTag,
@ -123,6 +128,7 @@ const visitors = {
SlotElement,
SnippetBlock,
SpreadAttribute,
StaticBlock,
SvelteBody,
SvelteComponent,
SvelteDocument,
@ -168,6 +174,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),
@ -714,7 +721,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 { 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);
}

@ -1,10 +1,11 @@
/** @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';
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
@ -23,33 +24,94 @@ 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') {
const member = b.member(b.this, field.key);
const should_proxy = field.type === '$state' && true; // TODO
const key = b.key(name);
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;
}
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));
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), 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 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 +127,20 @@ 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;
}
const member = b.member(b.this, field.key);
const should_proxy = field.type === '$state' && true; // TODO
if (name[0] === '#') {
if (typeof name === 'string' && name[0] === '#') {
let value = definition.value
? /** @type {CallExpression} */ (context.visit(definition.value, child_state))
: undefined;
@ -83,14 +150,12 @@ 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) {
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),
@ -104,6 +169,71 @@ 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 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));
body.push(
b.prop_def(field.key, call),
b.method(
'get',
b.assignment('=', b.id(key), 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,41 @@
/** @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) {
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);
}

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

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

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

@ -11,8 +11,11 @@ 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';
import { ClassExpression } from './visitors/ClassExpression.js';
import { Component } from './visitors/Component.js';
import { ConstTag } from './visitors/ConstTag.js';
import { DebugTag } from './visitors/DebugTag.js';
@ -25,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';
@ -47,13 +52,18 @@ const global_visitors = {
_: set_scope,
AssignmentExpression,
AwaitExpression,
BlockStatement,
CallExpression,
ClassBody,
ClassDeclaration,
ClassExpression,
ExpressionStatement,
Identifier,
LabeledStatement,
MemberExpression,
Program,
PropertyDefinition,
StaticBlock,
UpdateExpression,
VariableDeclaration
};
@ -103,7 +113,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 {ESTree.Program} */ (
@ -408,7 +419,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 {ESTree.Program} */ (

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

@ -1,7 +1,8 @@
/** @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';
import { is_constant } from '../../../scope.js';
/**
* @param {ClassBody} node
@ -20,24 +21,83 @@ 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;
}
// insert backing fields for stuff declared in the constructor
if (
field &&
field.node.type === 'AssignmentExpression' &&
(field.type === '$derived' || field.type === '$derived.by')
) {
const member = b.member(b.this, field.key);
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), 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.type === '$derived' || field.type === '$derived.by') {
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,20 +111,25 @@ 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;
}
const member = b.member(b.this, field.key);
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);
body.push(
b.prop_def(
field.key,
@ -74,6 +139,66 @@ 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 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;
}
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(
b.prop_def(
field.key,
/** @type {CallExpression} */ (context.visit(field.value, child_state))
),
b.method(
'get',
b.assignment('=', b.id(key), 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,41 @@
/** @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) {
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);
}

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

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

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

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

@ -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

@ -1,4 +1,4 @@
import type { AST, Binding, StateField } from '#compiler';
import type { AST, Binding, 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';
@ -299,8 +300,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';

@ -485,6 +485,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
@ -671,7 +702,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
};
/**

@ -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
}
}
]
[]

@ -841,7 +841,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

Loading…
Cancel
Save