ComputerGuy 6 days 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 { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.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'; import type { ExpressionMetadata } from '../nodes.js';
export interface AnalysisState { export interface AnalysisState {
@ -22,7 +22,7 @@ export interface AnalysisState {
expression: ExpressionMetadata | null; expression: ExpressionMetadata | null;
/** Used to analyze class state */ /** Used to analyze class state */
state_fields: Map<string, StateField>; state_fields: StateFields;
function_depth: number; function_depth: number;

@ -313,7 +313,7 @@ function is_variable_declaration(parent, context) {
* @param {AST.SvelteNode} parent * @param {AST.SvelteNode} parent
*/ */
function is_class_property_definition(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.type === 'AssignmentExpression' &&
node.operator === '=' && node.operator === '=' &&
node.left.type === 'MemberExpression' && node.left.type === 'MemberExpression' &&
node.left.object.type === 'ThisExpression' && node.left.object.type === 'ThisExpression'
((node.left.property.type === 'Identifier' && !node.left.computed) ||
node.left.property.type === 'PrivateIdentifier' ||
node.left.property.type === 'Literal')
) { ) {
// MethodDefinition (-5) -> FunctionExpression (-4) -> BlockStatement (-3) -> ExpressionStatement (-2) -> AssignmentExpression (-1) // MethodDefinition (-5) -> FunctionExpression (-4) -> BlockStatement (-3) -> ExpressionStatement (-2) -> AssignmentExpression (-1)
const parent = get_parent(context.path, -5); 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(); 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(); const fields = new Map();
context.state.analysis.classes.set(node, state_fields); context.state.analysis.classes.set(node, state_fields);
@ -41,28 +41,36 @@ export function ClassBody(node, context) {
/** @type {MethodDefinition | null} */ /** @type {MethodDefinition | null} */
let constructor = 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 {PropertyDefinition | AssignmentExpression} node
* @param {Expression | PrivateIdentifier} key * @param {Expression | PrivateIdentifier} key
* @param {Expression | null | undefined} value * @param {Expression | null | undefined} value
* @param {boolean} [computed]
*/ */
function handle(node, key, value) { function handle(node, key, value, computed = false) {
const name = get_name(key); const name = computed ? increment_computed() : get_name(key);
if (name === null) return; if (name === null) return;
const rune = get_rune(value, context.state.scope); const rune = get_rune(value, context.state.scope);
if (rune && is_state_creation_rune(rune)) { 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); 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); const field = fields.get(_key);
// if there's already a method or assigned field, error // if there's already a method or assigned field, error
if (field && !(field.length === 1 && field[0] === 'prop')) { 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, { state_fields.set(name, {
@ -70,15 +78,19 @@ export function ClassBody(node, context) {
type: rune, type: rune,
// @ts-expect-error for public state this is filled out in a moment // @ts-expect-error for public state this is filled out in a moment
key: key.type === 'PrivateIdentifier' ? key : null, 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) { for (const child of node.body) {
if (child.type === 'PropertyDefinition' && !child.computed && !child.static) { if (child.type === 'PropertyDefinition' && !child.static) {
handle(child, child.key, child.value); handle(child, child.key, child.value, child.computed && child.key.type !== 'Literal');
const key = /** @type {string} */ (get_name(child.key)); const key = get_name(child.key);
if (key === null) {
continue;
}
const field = fields.get(key); const field = fields.get(key);
if (!field) { if (!field) {
fields.set(key, [child.value ? 'assigned_prop' : 'prop']); fields.set(key, [child.value ? 'assigned_prop' : 'prop']);
@ -91,7 +103,11 @@ export function ClassBody(node, context) {
if (child.kind === 'constructor') { if (child.kind === 'constructor') {
constructor = child; constructor = child;
} else if (!child.computed) { } 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); const field = fields.get(key);
if (!field) { if (!field) {
fields.set(key, [child.kind]); fields.set(key, [child.kind]);
@ -132,18 +148,25 @@ export function ClassBody(node, context) {
if (left.type !== 'MemberExpression') continue; if (left.type !== 'MemberExpression') continue;
if (left.object.type !== 'ThisExpression') 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) { for (const [name, field] of state_fields) {
if (name[0] === '#') { if (typeof name === 'string' && name[0] === '#') {
continue; 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)) { while (private_ids.includes(deconflicted)) {
deconflicted = '_' + deconflicted; deconflicted = '_' + deconflicted;
} }

@ -19,6 +19,8 @@ import { BlockStatement } from './visitors/BlockStatement.js';
import { BreakStatement } from './visitors/BreakStatement.js'; import { BreakStatement } from './visitors/BreakStatement.js';
import { CallExpression } from './visitors/CallExpression.js'; import { CallExpression } from './visitors/CallExpression.js';
import { ClassBody } from './visitors/ClassBody.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 { Comment } from './visitors/Comment.js';
import { Component } from './visitors/Component.js'; import { Component } from './visitors/Component.js';
import { ConstTag } from './visitors/ConstTag.js'; import { ConstTag } from './visitors/ConstTag.js';
@ -45,6 +47,7 @@ import { RenderTag } from './visitors/RenderTag.js';
import { SlotElement } from './visitors/SlotElement.js'; import { SlotElement } from './visitors/SlotElement.js';
import { SnippetBlock } from './visitors/SnippetBlock.js'; import { SnippetBlock } from './visitors/SnippetBlock.js';
import { SpreadAttribute } from './visitors/SpreadAttribute.js'; import { SpreadAttribute } from './visitors/SpreadAttribute.js';
import { StaticBlock } from './visitors/StaticBlock.js';
import { SvelteBody } from './visitors/SvelteBody.js'; import { SvelteBody } from './visitors/SvelteBody.js';
import { SvelteComponent } from './visitors/SvelteComponent.js'; import { SvelteComponent } from './visitors/SvelteComponent.js';
import { SvelteDocument } from './visitors/SvelteDocument.js'; import { SvelteDocument } from './visitors/SvelteDocument.js';
@ -97,6 +100,8 @@ const visitors = {
BreakStatement, BreakStatement,
CallExpression, CallExpression,
ClassBody, ClassBody,
ClassDeclaration,
ClassExpression,
Comment, Comment,
Component, Component,
ConstTag, ConstTag,
@ -123,6 +128,7 @@ const visitors = {
SlotElement, SlotElement,
SnippetBlock, SnippetBlock,
SpreadAttribute, SpreadAttribute,
StaticBlock,
SvelteBody, SvelteBody,
SvelteComponent, SvelteComponent,
SvelteDocument, SvelteDocument,
@ -168,6 +174,7 @@ export function client_component(analysis, options) {
in_constructor: false, in_constructor: false,
instance_level_snippets: [], instance_level_snippets: [],
module_level_snippets: [], module_level_snippets: [],
computed_field_declarations: null,
// these are set inside the `Fragment` visitor, and cannot be used until then // these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null), init: /** @type {any} */ (null),
@ -714,7 +721,8 @@ export function client_module(analysis, options) {
state_fields: new Map(), state_fields: new Map(),
transform: {}, transform: {},
in_constructor: false, in_constructor: false,
is_instance: false is_instance: false,
computed_field_declarations: null
}; };
const module = /** @type {ESTree.Program} */ ( 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 { ComponentContext } from '../types' */
import { add_state_transformers } from './shared/declarations.js'; import { add_state_transformers } from './shared/declarations.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
@ -11,6 +11,24 @@ export function BlockStatement(node, context) {
add_state_transformers(context); add_state_transformers(context);
const tracing = context.state.scope.tracing; 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) { if (tracing !== null) {
const parent = const parent =
/** @type {ArrowFunctionExpression | FunctionDeclaration | FunctionExpression} */ ( /** @type {ArrowFunctionExpression | FunctionDeclaration | FunctionExpression} */ (
@ -22,11 +40,10 @@ export function BlockStatement(node, context) {
const call = b.call( const call = b.call(
'$.trace', '$.trace',
/** @type {Expression} */ (tracing), /** @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)]); return b.block([b.return(is_async ? b.await(call) : call)]);
} }
return b.block(body);
context.next();
} }

@ -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 { StateField } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { dev } from '../../../../state.js'; import { dev } from '../../../../state.js';
import { get_parent } from '../../../../utils/ast.js'; import { get_parent } from '../../../../utils/ast.js';
import { get_name } from '../../../nodes.js'; import { get_name } from '../../../nodes.js';
import { is_constant } from '../../../scope.js';
/** /**
* @param {ClassBody} node * @param {ClassBody} node
@ -23,33 +24,94 @@ export function ClassBody(node, context) {
const body = []; const body = [];
const child_state = { ...context.state, state_fields }; 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) { for (const [name, field] of state_fields) {
if (name[0] === '#') { if (
(typeof name === 'string' && name[0] === '#') ||
field.node.type !== 'AssignmentExpression'
) {
continue; continue;
} }
const member = b.member(b.this, field.key);
// insert backing fields for stuff declared in the constructor const should_proxy = field.type === '$state' && true; // TODO
if (field.node.type === 'AssignmentExpression') {
const member = b.member(b.this, field.key); if (typeof name === 'number' && field.computed_key) {
const computed_key = /** @type {Expression} */ (context.visit(field.computed_key));
const should_proxy = field.type === '$state' && true; // TODO const evaluation = context.state.scope.evaluate(computed_key);
if (evaluation.is_known) {
const key = b.key(name); 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( body.push(
b.prop_def(field.key, null), b.prop_def(field.key, null),
b.method(
b.method('get', key, [], [b.return(b.call('$.get', member))]), 'get',
b.assignment('=', b.id(key), computed_key),
[],
[b.return(b.call('$.get', member))],
true
),
b.method( b.method(
'set', 'set',
key, b.id(key),
[b.id('value')], [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} */ ( const declaration = /** @type {ClassDeclaration | ClassExpression} */ (
@ -65,15 +127,20 @@ export function ClassBody(node, context) {
continue; continue;
} }
const name = get_name(definition.key); const name = definition.computed
const field = name && /** @type {StateField} */ (state_fields.get(name)); ? [...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) { if (!field) {
body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state))); body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
continue; 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 let value = definition.value
? /** @type {CallExpression} */ (context.visit(definition.value, child_state)) ? /** @type {CallExpression} */ (context.visit(definition.value, child_state))
: undefined; : undefined;
@ -83,14 +150,12 @@ export function ClassBody(node, context) {
} }
body.push(b.prop_def(definition.key, value)); 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)); let call = /** @type {CallExpression} */ (context.visit(field.value, child_state));
if (dev) { if (dev) {
call = b.call('$.tag', call, b.literal(`${declaration.id?.name ?? '[class]'}.${name}`)); 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( body.push(
b.prop_def(field.key, call), 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))] [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 { ComponentContext } from '../types' */
import { build_getter, is_prop_source } from '../utils.js'; import { build_getter, is_prop_source } from '../utils.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { add_state_transformers } from './shared/declarations.js'; import { add_state_transformers } from './shared/declarations.js';
/** /**
* @param {Program} _ * @param {Program} node
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function Program(_, context) { export function Program(node, context) {
if (!context.state.analysis.runes) { if (!context.state.analysis.runes) {
context.state.transform['$$props'] = { context.state.transform['$$props'] = {
read: (node) => ({ ...node, name: '$$sanitized_props' }) read: (node) => ({ ...node, name: '$$sanitized_props' })
@ -137,5 +137,26 @@ export function Program(_, context) {
add_state_transformers(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 { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { AwaitBlock } from './visitors/AwaitBlock.js'; import { AwaitBlock } from './visitors/AwaitBlock.js';
import { AwaitExpression } from './visitors/AwaitExpression.js'; import { AwaitExpression } from './visitors/AwaitExpression.js';
import { BlockStatement } from './visitors/BlockStatement.js';
import { CallExpression } from './visitors/CallExpression.js'; import { CallExpression } from './visitors/CallExpression.js';
import { ClassBody } from './visitors/ClassBody.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 { Component } from './visitors/Component.js';
import { ConstTag } from './visitors/ConstTag.js'; import { ConstTag } from './visitors/ConstTag.js';
import { DebugTag } from './visitors/DebugTag.js'; import { DebugTag } from './visitors/DebugTag.js';
@ -25,12 +28,14 @@ import { IfBlock } from './visitors/IfBlock.js';
import { KeyBlock } from './visitors/KeyBlock.js'; import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js'; import { LabeledStatement } from './visitors/LabeledStatement.js';
import { MemberExpression } from './visitors/MemberExpression.js'; import { MemberExpression } from './visitors/MemberExpression.js';
import { Program } from './visitors/Program.js';
import { PropertyDefinition } from './visitors/PropertyDefinition.js'; import { PropertyDefinition } from './visitors/PropertyDefinition.js';
import { RegularElement } from './visitors/RegularElement.js'; import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js'; import { RenderTag } from './visitors/RenderTag.js';
import { SlotElement } from './visitors/SlotElement.js'; import { SlotElement } from './visitors/SlotElement.js';
import { SnippetBlock } from './visitors/SnippetBlock.js'; import { SnippetBlock } from './visitors/SnippetBlock.js';
import { SpreadAttribute } from './visitors/SpreadAttribute.js'; import { SpreadAttribute } from './visitors/SpreadAttribute.js';
import { StaticBlock } from './visitors/StaticBlock.js';
import { SvelteComponent } from './visitors/SvelteComponent.js'; import { SvelteComponent } from './visitors/SvelteComponent.js';
import { SvelteElement } from './visitors/SvelteElement.js'; import { SvelteElement } from './visitors/SvelteElement.js';
import { SvelteFragment } from './visitors/SvelteFragment.js'; import { SvelteFragment } from './visitors/SvelteFragment.js';
@ -47,13 +52,18 @@ const global_visitors = {
_: set_scope, _: set_scope,
AssignmentExpression, AssignmentExpression,
AwaitExpression, AwaitExpression,
BlockStatement,
CallExpression, CallExpression,
ClassBody, ClassBody,
ClassDeclaration,
ClassExpression,
ExpressionStatement, ExpressionStatement,
Identifier, Identifier,
LabeledStatement, LabeledStatement,
MemberExpression, MemberExpression,
Program,
PropertyDefinition, PropertyDefinition,
StaticBlock,
UpdateExpression, UpdateExpression,
VariableDeclaration VariableDeclaration
}; };
@ -103,7 +113,8 @@ export function server_component(analysis, options) {
namespace: options.namespace, namespace: options.namespace,
preserve_whitespace: options.preserveWhitespace, preserve_whitespace: options.preserveWhitespace,
state_fields: new Map(), state_fields: new Map(),
skip_hydration_boundaries: false skip_hydration_boundaries: false,
computed_field_declarations: null
}; };
const module = /** @type {ESTree.Program} */ ( 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 // to be present for `javascript_visitors_legacy` and so is included in module
// transform state as well as component transform state // transform state as well as component transform state
legacy_reactive_statements: new Map(), legacy_reactive_statements: new Map(),
state_fields: new Map() state_fields: new Map(),
computed_field_declarations: null
}; };
const module = /** @type {ESTree.Program} */ ( 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 { Context } from '../types.js' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { get_name } from '../../../nodes.js'; import { get_name } from '../../../nodes.js';
import { is_constant } from '../../../scope.js';
/** /**
* @param {ClassBody} node * @param {ClassBody} node
@ -20,24 +21,83 @@ export function ClassBody(node, context) {
const body = []; const body = [];
const child_state = { ...context.state, state_fields }; 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) { for (const [name, field] of state_fields) {
if (name[0] === '#') { if (
(typeof name === 'string' && name[0] === '#') ||
field.node.type !== 'AssignmentExpression'
) {
continue; continue;
} }
// insert backing fields for stuff declared in the constructor const member = b.member(b.this, field.key);
if ( if (typeof name !== 'string' && field.computed_key) {
field && const computed_key = /** @type {Expression} */ (context.visit(field.computed_key));
field.node.type === 'AssignmentExpression' && const evaluation = context.state.scope.evaluate(computed_key);
(field.type === '$derived' || field.type === '$derived.by') if (evaluation.is_known) {
) { body.push(
const member = b.member(b.this, field.key); 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( body.push(
b.prop_def(field.key, null), b.prop_def(field.key, null),
b.method('get', b.key(name), [], [b.return(b.call(member))]), b.method('get', b.key(/** @type {string} */ (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(
'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; continue;
} }
const name = get_name(definition.key); const name = definition.computed
const field = name && state_fields.get(name); ? [...state_fields.entries()].find(([, field]) => field.node === definition)?.[0] ?? null
: get_name(definition.key);
const field = name !== null && state_fields.get(name);
if (!field) { if (!field) {
body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state))); body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
continue; 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))); 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 // $derived / $derived.by
const member = b.member(b.this, field.key);
body.push( body.push(
b.prop_def( b.prop_def(
field.key, field.key,
@ -74,6 +139,66 @@ export function ClassBody(node, context) {
b.method('get', definition.key, [], [b.return(b.call(member))]), 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')))]) 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 { 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 { Analysis } from '../types.js';
import type { VariableDeclaration } from 'estree';
export interface TransformState { export interface TransformState {
readonly analysis: Analysis; readonly analysis: Analysis;
@ -8,5 +9,6 @@ export interface TransformState {
readonly scope: Scope; readonly scope: Scope;
readonly scopes: Map<AST.SvelteNode, 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; 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. * Returns the name of the rune if the given expression is a `CallExpression` using a rune.
* @param {Expression | Super} node * @param {Expression | Super} node

@ -1,4 +1,4 @@
import type { AST, Binding, StateField } from '#compiler'; import type { AST, Binding, StateFields } from '#compiler';
import type { import type {
AwaitExpression, AwaitExpression,
CallExpression, CallExpression,
@ -40,7 +40,7 @@ export interface Analysis {
tracing: boolean; tracing: boolean;
comments: AST.JSComment[]; comments: AST.JSComment[];
classes: Map<ClassBody, Map<string, StateField>>; classes: Map<ClassBody, StateFields>;
// TODO figure out if we can move this to ComponentAnalysis // TODO figure out if we can move this to ComponentAnalysis
accessors: boolean; accessors: boolean;

@ -6,6 +6,7 @@ import type { StateCreationRuneName } from '../../utils.js';
import type { import type {
AssignmentExpression, AssignmentExpression,
CallExpression, CallExpression,
Expression,
PrivateIdentifier, PrivateIdentifier,
PropertyDefinition PropertyDefinition
} from 'estree'; } from 'estree';
@ -299,8 +300,11 @@ export interface StateField {
node: PropertyDefinition | AssignmentExpression; node: PropertyDefinition | AssignmentExpression;
key: PrivateIdentifier; key: PrivateIdentifier;
value: CallExpression; value: CallExpression;
computed_key: Expression | null;
} }
export type StateFields = Map<string | number, StateField>;
export * from './template.js'; export * from './template.js';
export { Binding, Scope } from '../phases/scope.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.UpdateOperator} operator
* @param {ESTree.Expression} argument * @param {ESTree.Expression} argument
@ -671,7 +702,8 @@ export {
if_builder as if, if_builder as if,
this_instance as this, this_instance as this,
null_instance as null, 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' { declare module 'svelte/compiler' {
import type { SourceMap } from 'magic-string'; 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'; import type { Location } from 'locate-character';
/** /**
* `compile` converts your `.svelte` source code into a JavaScript module that exports a component * `compile` converts your `.svelte` source code into a JavaScript module that exports a component

Loading…
Cancel
Save