diff --git a/.changeset/fuzzy-wings-sin.md b/.changeset/fuzzy-wings-sin.md new file mode 100644 index 0000000000..fb236c9552 --- /dev/null +++ b/.changeset/fuzzy-wings-sin.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: allow `$state` in computed class fields diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index ae9c5911f6..a1c66c4052 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -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; + state_fields: StateFields; function_depth: number; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js index dd21637174..8b3638ac97 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js @@ -30,7 +30,7 @@ export function ClassBody(node, context) { } } - /** @type {Map} */ + /** @type {Map} */ const state_fields = new Map(); /** @type {Map>} */ @@ -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; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 2629379f63..51707f4acc 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -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} */ ( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js index 5bd9add2a5..5f6e48dc11 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js @@ -1,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 + ) + ); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassDeclaration.js new file mode 100644 index 0000000000..96bd5c3d29 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassDeclaration.js @@ -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); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassExpression.js new file mode 100644 index 0000000000..fc0bf4e07f --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassExpression.js @@ -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); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index b22b95f5aa..7ebc8d08aa 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -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} */ ( diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js index 432d0142cd..004f17c707 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js @@ -1,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 + ) + ); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassDeclaration.js new file mode 100644 index 0000000000..8543cb4083 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassDeclaration.js @@ -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); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassExpression.js new file mode 100644 index 0000000000..16fcaedc28 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassExpression.js @@ -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); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/types.d.ts index 887ad447b9..69ed5c6a55 100644 --- a/packages/svelte/src/compiler/phases/3-transform/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/types.d.ts @@ -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; - readonly state_fields: Map; + readonly state_fields: StateFields; + readonly computed_field_declarations: VariableDeclaration[] | null; } diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 4e287fd199..a40e25e621 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -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>; + classes: Map; // TODO figure out if we can move this to ComponentAnalysis accessors: boolean; diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 9bd4b91d58..9ca371b9e2 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -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; + export * from './template.js'; export { Binding, Scope } from '../phases/scope.js'; diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index 99306ce4d9..f53ecedb76 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.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 }; /**