From 92940ffbfdb46dd7a1edb25fd361bc3a3c2092f8 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Tue, 29 Apr 2025 12:15:08 -0400 Subject: [PATCH] feat: It works I think? --- .../client/visitors/AssignmentExpression.js | 6 +- .../visitors/shared/client-class-analysis.js | 20 +++++-- .../server/visitors/AssignmentExpression.js | 5 ++ .../visitors/shared/server-class-analysis.js | 51 ++++++++++++++--- .../3-transform/shared/class_analysis.js | 56 ++++++++++++++----- .../phases/3-transform/shared/types.d.ts | 4 +- .../_config.js | 0 .../main.svelte | 0 .../_config.js | 13 +++++ .../main.svelte | 13 +++++ .../_config.js | 45 +++++++++++++++ .../main.svelte | 37 ++++++++++++ .../_config.js | 20 +++++++ .../main.svelte | 22 ++++++++ .../class-state-constructor/_config.js | 20 +++++++ .../class-state-constructor/main.svelte | 18 ++++++ .../class-state-constructor-1/errors.json | 14 +++++ .../class-state-constructor-1/input.svelte.js | 7 +++ .../class-state-constructor-2/errors.json | 14 +++++ .../class-state-constructor-2/input.svelte.js | 7 +++ .../class-state-constructor-3/errors.json | 14 +++++ .../class-state-constructor-3/input.svelte.js | 7 +++ .../class-state-constructor-4/errors.json | 14 +++++ .../class-state-constructor-4/input.svelte.js | 7 +++ 24 files changed, 381 insertions(+), 33 deletions(-) rename packages/svelte/tests/runtime-runes/samples/{class-state-constructor-closure-private => class-state-constructor-closure-private-1}/_config.js (100%) rename packages/svelte/tests/runtime-runes/samples/{class-state-constructor-closure-private => class-state-constructor-closure-private-1}/main.svelte (100%) create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 7d0840ab80..ff8d2b5ce1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -17,7 +17,11 @@ import { validate_mutation } from './shared/utils.js'; * @param {Context} context */ export function AssignmentExpression(node, context) { - context.state.class_analysis?.register_assignment(node, context); + const stripped_node = context.state.class_analysis?.register_assignment(node, context); + if (stripped_node) { + return stripped_node; + } + const expression = /** @type {Expression} */ ( visit_assignment_expression(node, context, build_assignment) ?? context.next() ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-analysis.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-analysis.js index fad75b142d..70dd364676 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-analysis.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-analysis.js @@ -13,7 +13,7 @@ import { should_proxy } from '../../utils.js'; export function create_client_class_analysis(body) { /** @type {StateFieldBuilder} */ function build_state_field({ is_private, field, node, context }) { - let original_id = node.type === 'AssignmentExpression' ? node.left : node.key; + let original_id = node.type === 'AssignmentExpression' ? node.left.property : node.key; let value; if (node.type === 'AssignmentExpression') { // if there's no call expression, this is state that's created in the constructor. @@ -51,10 +51,16 @@ export function create_client_class_analysis(body) { /** @type {AssignmentBuilder} */ function build_assignment({ field, node, context }) { - // ...swap out the assignment to go directly against the private field - node.left.property = field.id; - // ...and swap out the assignment's value for the state field init - node.right = build_init_value(field.kind, node.right.arguments[0], context); + return { + ...node, + left: { + ...node.left, + // ...swap out the assignment to go directly against the private field + property: field.id + }, + // ...and swap out the assignment's value for the state field init + right: build_init_value(field.kind, node.right.arguments[0], context) + }; } return create_class_analysis(body, build_state_field, build_assignment); @@ -67,7 +73,9 @@ export function create_client_class_analysis(body) { * @param {Context} context */ function build_init_value(kind, arg, context) { - const init = /** @type {Expression} **/ (context.visit(arg, context.state)); + const init = /** @type {Expression} **/ ( + context.visit(arg, { ...context.state, in_constructor: false }) + ); switch (kind) { case '$state': diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js index 071a12f9bc..9977c979db 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js @@ -10,6 +10,11 @@ import { visit_assignment_expression } from '../../shared/assignments.js'; * @param {Context} context */ export function AssignmentExpression(node, context) { + const stripped_node = context.state.class_analysis?.register_assignment(node, context); + if (stripped_node) { + return stripped_node; + } + return visit_assignment_expression(node, context, build_assignment) ?? context.next(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-analysis.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-analysis.js index a94972ed93..44a6415087 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-analysis.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-analysis.js @@ -1,7 +1,8 @@ -/** @import { Expression, MethodDefinition, StaticBlock, PropertyDefinition } from 'estree' */ +/** @import { Expression, MethodDefinition, StaticBlock, PropertyDefinition, SpreadElement } from 'estree' */ /** @import { Context } from '../../types.js' */ /** @import { AssignmentBuilder, StateFieldBuilder } from '../../../shared/types.js' */ /** @import { ClassAnalysis } from '../../../shared/types.js' */ +/** @import { StateCreationRuneName } from '../../../../../../utils.js' */ import * as b from '#compiler/builders'; import { dev } from '../../../../../state.js'; @@ -14,7 +15,7 @@ import { create_class_analysis } from '../../../shared/class_analysis.js'; export function create_server_class_analysis(body) { /** @type {StateFieldBuilder} */ function build_state_field({ is_private, field, node, context }) { - let original_id = node.type === 'AssignmentExpression' ? node.left : node.key; + let original_id = node.type === 'AssignmentExpression' ? node.left.property : node.key; let value; if (node.type === 'AssignmentExpression') { // This means it's a state assignment in the constructor (this.foo = $state('bar')) @@ -37,13 +38,19 @@ export function create_server_class_analysis(body) { // #foo; const member = b.member(b.this, field.id); + /** @type {Array} */ const defs = [ // #foo; - b.prop_def(field.id, value), - // get foo() { return this.#foo; } - b.method('get', original_id, [], [b.return(b.call(member))]) + b.prop_def(field.id, value) ]; + // get foo() { return this.#foo; } + if (field.kind === '$state' || field.kind === '$state.raw') { + defs.push(b.method('get', original_id, [], [b.return(member)])); + } else { + defs.push(b.method('get', original_id, [], [b.return(b.call(member))])); + } + // TODO make this work on server if (dev) { defs.push( @@ -61,11 +68,37 @@ export function create_server_class_analysis(body) { /** @type {AssignmentBuilder} */ function build_assignment({ field, node, context }) { - node.left.property = field.id; - const init = /** @type {Expression} **/ (context.visit(node.right.arguments[0], context.state)); - node.right = - field.kind === '$derived.by' ? b.call('$.once', init) : b.call('$.once', b.thunk(init)); + return { + ...node, + left: { + ...node.left, + // ...swap out the assignment to go directly against the private field + property: field.id + }, + // ...and swap out the assignment's value for the state field init + right: build_init_value(field.kind, node.right.arguments[0], context) + }; } return create_class_analysis(body, build_state_field, build_assignment); } + +/** + * + * @param {StateCreationRuneName} kind + * @param {Expression | SpreadElement} arg + * @param {Context} context + */ +function build_init_value(kind, arg, context) { + const init = /** @type {Expression} **/ (context.visit(arg, context.state)); + + switch (kind) { + case '$state': + case '$state.raw': + return init; + case '$derived': + return b.call('$.once', b.thunk(init)); + case '$derived.by': + return b.call('$.once', init); + } +} diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/class_analysis.js b/packages/svelte/src/compiler/phases/3-transform/shared/class_analysis.js index 5eb001d438..e482f10d49 100644 --- a/packages/svelte/src/compiler/phases/3-transform/shared/class_analysis.js +++ b/packages/svelte/src/compiler/phases/3-transform/shared/class_analysis.js @@ -107,12 +107,9 @@ export function create_class_analysis(body, build_state_field, build_assignment) } /** - * Important note: It is a syntax error in JavaScript to try to assign to a private class field - * that was not declared in the class body. So there is absolutely no risk of unresolvable conflicts here. - * - * This function will modify the assignment expression passed to it if it is registered as a state field. * @param {AssignmentExpression} node * @param {TContext} context + * @returns {AssignmentExpression | null} The node, if `register_assignment` handled its transformation. */ function register_assignment(node, context) { const child_context = create_child_context(context); @@ -121,30 +118,46 @@ export function create_class_analysis(body, build_state_field, build_assignment) node.operator === '=' && node.left.type === 'MemberExpression' && node.left.object.type === 'ThisExpression' && - node.left.property.type === 'Identifier' + (node.left.property.type === 'Identifier' || + node.left.property.type === 'PrivateIdentifier') ) ) { - return; + return null; } const name = get_name(node.left.property); if (!name) { - return; + return null; } const parsed = parse_stateful_assignment(node, child_context.state.scope); if (!parsed) { - return; + return null; } const { stateful_assignment, rune } = parsed; - const id = deconflict(name); - const field = { kind: rune, id }; - public_fields.set(name, field); + const is_private = stateful_assignment.left.property.type === 'PrivateIdentifier'; + + let field; + if (is_private) { + field = { + kind: rune, + id: /** @type {PrivateIdentifier} */ (stateful_assignment.left.property) + }; + private_fields.set(name, field); + } else { + field = { + kind: rune, + // it's safe to do this upfront now because we're guaranteed to already know about all private + // identifiers (they had to have been declared at the class root, before we visited the constructor) + id: deconflict(name) + }; + public_fields.set(name, field); + } const replacer = () => { const nodes = build_state_field({ - is_private: false, + is_private, field, node: stateful_assignment, context: child_context @@ -156,9 +169,9 @@ export function create_class_analysis(body, build_state_field, build_assignment) }; replacers.push(replacer); - build_assignment({ - node: stateful_assignment, + return build_assignment({ field, + node: stateful_assignment, context: child_context }); } @@ -219,7 +232,20 @@ export function create_class_analysis(body, build_state_field, build_assignment) const parsed = prop_def_is_stateful(node, child_context.state.scope); if (!parsed) { - return false; + // this isn't a stateful field definition, but if could become one in the constructor -- so we register + // it, but conditionally -- so that if it's added as a field in the constructor (which causes us to create) + // a field definition for it), we don't end up with a duplicate definition (this one, plus the one we create) + replacers.push(() => { + if (!get_field(name, is_private)) { + new_body.push( + /** @type {PropertyDefinition | MethodDefinition} */ ( + // @ts-expect-error generics silliness + child_context.visit(node, child_context.state) + ) + ); + } + }); + return true; } const { stateful_prop_def, rune } = parsed; diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts index 45bd3b5a00..96fe27ca5d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts @@ -34,7 +34,7 @@ export type AssignmentBuilderParams = (params: AssignmentBuilderParams) => void; +export type AssignmentBuilder = (params: AssignmentBuilderParams) => AssignmentExpression; export type ClassAnalysis = { /** @@ -59,5 +59,5 @@ export type ClassAnalysis = { * a state field on the class. If it is, it registers that state field and modifies the * assignment expression. */ - register_assignment: (node: AssignmentExpression, context: TContext) => void; + register_assignment: (node: AssignmentExpression, context: TContext) => AssignmentExpression | null; } diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-1/_config.js similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private/_config.js rename to packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-1/_config.js diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-1/main.svelte similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private/main.svelte rename to packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-1/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/_config.js new file mode 100644 index 0000000000..dd847ce2f2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ``, + ssrHtml: ``, + + async test({ assert, target }) { + flushSync(); + + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/main.svelte new file mode 100644 index 0000000000..3d8ea41418 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/main.svelte @@ -0,0 +1,13 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js new file mode 100644 index 0000000000..4cf1aea213 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js @@ -0,0 +1,45 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + // The component context class instance gets shared between tests, strangely, causing hydration to fail? + mode: ['client', 'server'], + + async test({ assert, target, logs }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + + assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1]); + + flushSync(() => { + btn?.click(); + }); + + assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1, 2]); + + flushSync(() => { + btn?.click(); + }); + + assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1, 2, 3]); + + flushSync(() => { + btn?.click(); + }); + + assert.deepEqual(logs, [ + 0, + 'class trigger false', + 'local trigger false', + 1, + 2, + 3, + 4, + 'class trigger true', + 'local trigger true' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte new file mode 100644 index 0000000000..03687d01bb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte @@ -0,0 +1,37 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js new file mode 100644 index 0000000000..32cca6c693 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ``, + + test({ assert, target }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte new file mode 100644 index 0000000000..d8feb554cd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte @@ -0,0 +1,22 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js new file mode 100644 index 0000000000..f35dc57228 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ``, + + test({ assert, target }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte new file mode 100644 index 0000000000..aa8ba1658b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte @@ -0,0 +1,18 @@ + + + diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json new file mode 100644 index 0000000000..eaff12b896 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "constructor_state_reassignment", + "message": "Cannot redeclare stateful field `count` in the constructor. The field was originally declared here: `(unknown):2:1`", + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 24 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js new file mode 100644 index 0000000000..05cd4d9d9d --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + count = $state(0); + + constructor() { + this.count = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json new file mode 100644 index 0000000000..a27d7411d1 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "constructor_state_reassignment", + "message": "Cannot redeclare stateful field `count` in the constructor. The field was originally declared here: `(unknown):3:2`", + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 24 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js new file mode 100644 index 0000000000..e37be4b3e6 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + constructor() { + this.count = $state(0); + this.count = 1; + this.count = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json new file mode 100644 index 0000000000..8017794a67 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "constructor_state_reassignment", + "message": "Cannot redeclare stateful field `count` in the constructor. The field was originally declared here: `(unknown):3:2`", + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 28 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js new file mode 100644 index 0000000000..f9196ff3cd --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + constructor() { + this.count = $state(0); + this.count = 1; + this.count = $state.raw(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json new file mode 100644 index 0000000000..9f959874c8 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json @@ -0,0 +1,14 @@ +[ + { + "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": 4, + "column": 16 + }, + "end": { + "line": 4, + "column": 25 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js new file mode 100644 index 0000000000..bf1aada1b5 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + constructor() { + if (true) { + this.count = $state(0); + } + } +}