mirror of https://github.com/sveltejs/svelte
feat: State declarations in class constructors (#15820)
* feat: State declarations in class constructors * feat: Analysis phase * misc * feat: client * improvements * feat: It is now at least backwards compatible. though the new stuff may still be wrong * feat: It works I think? * final cleanup?? * tests * test for better types * changeset * rename functions (the function doesn't test call-expression-ness) * small readability tweak * failing test * fix * disallow computed state fields * tweak message to better accommodate the case in which state is declared after a regular property * failing test * wildly confusing to have so many things called 'class analysis' - rename the transform-phrase ones to transformers * missed a spot * and another * store analysis for use during transformation * move code to where it is used * do the analysis upfront, it's way simpler * skip failing test for now * simplify * get rid of the class * on second thoughts * reduce indirection * make analysis available at transform time * WIP * WIP * WIP * fix * remove unused stuff * revert snapshot tests * unused * note to self * fix * unused * unused * remove some unused stuff * unused * lint, tidy up * reuse helper * tweak * simplify/DRY * unused * tweak * unused * more * tweak * tweak * fix proxying logic * tweak * tweak * adjust message to accommodate more cases * unskip and fix test * fix * move * revert unneeded drive-by change * fix * fix * update docs --------- Co-authored-by: Rich Harris <rich.harris@vercel.com>pull/15947/head
parent
42e7e8168d
commit
d103adff9c
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: allow state fields to be declared inside class constructors
|
@ -1,30 +1,107 @@
|
|||||||
/** @import { ClassBody } from 'estree' */
|
/** @import { AssignmentExpression, CallExpression, ClassBody, PropertyDefinition, Expression, PrivateIdentifier, MethodDefinition } from 'estree' */
|
||||||
|
/** @import { StateField } from '#compiler' */
|
||||||
/** @import { Context } from '../types' */
|
/** @import { Context } from '../types' */
|
||||||
|
import * as b from '#compiler/builders';
|
||||||
import { get_rune } from '../../scope.js';
|
import { get_rune } from '../../scope.js';
|
||||||
|
import * as e from '../../../errors.js';
|
||||||
|
import { is_state_creation_rune } from '../../../../utils.js';
|
||||||
|
import { get_name } from '../../nodes.js';
|
||||||
|
import { regex_invalid_identifier_chars } from '../../patterns.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ClassBody} node
|
* @param {ClassBody} node
|
||||||
* @param {Context} context
|
* @param {Context} context
|
||||||
*/
|
*/
|
||||||
export function ClassBody(node, context) {
|
export function ClassBody(node, context) {
|
||||||
/** @type {{name: string, private: boolean}[]} */
|
if (!context.state.analysis.runes) {
|
||||||
const derived_state = [];
|
context.next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {string[]} */
|
||||||
|
const private_ids = [];
|
||||||
|
|
||||||
for (const definition of node.body) {
|
for (const prop of node.body) {
|
||||||
if (
|
if (
|
||||||
definition.type === 'PropertyDefinition' &&
|
(prop.type === 'MethodDefinition' || prop.type === 'PropertyDefinition') &&
|
||||||
(definition.key.type === 'PrivateIdentifier' || definition.key.type === 'Identifier') &&
|
prop.key.type === 'PrivateIdentifier'
|
||||||
definition.value?.type === 'CallExpression'
|
|
||||||
) {
|
) {
|
||||||
const rune = get_rune(definition.value, context.state.scope);
|
private_ids.push(prop.key.name);
|
||||||
if (rune === '$derived' || rune === '$derived.by') {
|
}
|
||||||
derived_state.push({
|
}
|
||||||
name: definition.key.name,
|
|
||||||
private: definition.key.type === 'PrivateIdentifier'
|
/** @type {Map<string, StateField>} */
|
||||||
});
|
const state_fields = new Map();
|
||||||
|
|
||||||
|
context.state.analysis.classes.set(node, state_fields);
|
||||||
|
|
||||||
|
/** @type {MethodDefinition | null} */
|
||||||
|
let constructor = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {PropertyDefinition | AssignmentExpression} node
|
||||||
|
* @param {Expression | PrivateIdentifier} key
|
||||||
|
* @param {Expression | null | undefined} value
|
||||||
|
*/
|
||||||
|
function handle(node, key, value) {
|
||||||
|
const name = 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)) {
|
||||||
|
e.state_field_duplicate(node, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state_fields.set(name, {
|
||||||
|
node,
|
||||||
|
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)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const child of node.body) {
|
||||||
|
if (child.type === 'PropertyDefinition' && !child.computed && !child.static) {
|
||||||
|
handle(child, child.key, child.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (child.type === 'MethodDefinition' && child.kind === 'constructor') {
|
||||||
|
constructor = child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constructor) {
|
||||||
|
for (const statement of constructor.value.body.body) {
|
||||||
|
if (statement.type !== 'ExpressionStatement') continue;
|
||||||
|
if (statement.expression.type !== 'AssignmentExpression') continue;
|
||||||
|
|
||||||
|
const { left, right } = statement.expression;
|
||||||
|
|
||||||
|
if (left.type !== 'MemberExpression') continue;
|
||||||
|
if (left.object.type !== 'ThisExpression') continue;
|
||||||
|
if (left.computed && left.property.type !== 'Literal') continue;
|
||||||
|
|
||||||
|
handle(statement.expression, left.property, right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [name, field] of state_fields) {
|
||||||
|
if (name[0] === '#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let deconflicted = name.replace(regex_invalid_identifier_chars, '_');
|
||||||
|
while (private_ids.includes(deconflicted)) {
|
||||||
|
deconflicted = '_' + deconflicted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private_ids.push(deconflicted);
|
||||||
|
field.key = b.private_id(deconflicted);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.next({ ...context.state, derived_state });
|
context.next({ ...context.state, state_fields });
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
/** @import { PropertyDefinition } from 'estree' */
|
||||||
|
/** @import { Context } from '../types' */
|
||||||
|
import * as e from '../../../errors.js';
|
||||||
|
import { get_name } from '../../nodes.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {PropertyDefinition} node
|
||||||
|
* @param {Context} context
|
||||||
|
*/
|
||||||
|
export function PropertyDefinition(node, context) {
|
||||||
|
const name = get_name(node.key);
|
||||||
|
const field = name && context.state.state_fields.get(name);
|
||||||
|
|
||||||
|
if (field && node !== field.node && node.value) {
|
||||||
|
if (/** @type {number} */ (node.start) < /** @type {number} */ (field.node.start)) {
|
||||||
|
e.state_field_invalid_assignment(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.next();
|
||||||
|
}
|
@ -1,184 +1,96 @@
|
|||||||
/** @import { ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */
|
/** @import { CallExpression, ClassBody, MethodDefinition, PropertyDefinition, StaticBlock } from 'estree' */
|
||||||
/** @import { Context, StateField } from '../types' */
|
/** @import { StateField } from '#compiler' */
|
||||||
|
/** @import { Context } from '../types' */
|
||||||
import * as b from '#compiler/builders';
|
import * as b from '#compiler/builders';
|
||||||
import { regex_invalid_identifier_chars } from '../../../patterns.js';
|
import { get_name } from '../../../nodes.js';
|
||||||
import { get_rune } from '../../../scope.js';
|
|
||||||
import { should_proxy } from '../utils.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ClassBody} node
|
* @param {ClassBody} node
|
||||||
* @param {Context} context
|
* @param {Context} context
|
||||||
*/
|
*/
|
||||||
export function ClassBody(node, context) {
|
export function ClassBody(node, context) {
|
||||||
if (!context.state.analysis.runes) {
|
const state_fields = context.state.analysis.classes.get(node);
|
||||||
|
|
||||||
|
if (!state_fields) {
|
||||||
|
// in legacy mode, do nothing
|
||||||
context.next();
|
context.next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {Map<string, StateField>} */
|
/** @type {Array<MethodDefinition | PropertyDefinition | StaticBlock>} */
|
||||||
const public_state = new Map();
|
const body = [];
|
||||||
|
|
||||||
/** @type {Map<string, StateField>} */
|
const child_state = { ...context.state, state_fields };
|
||||||
const private_state = new Map();
|
|
||||||
|
|
||||||
/** @type {Map<(MethodDefinition|PropertyDefinition)["key"], string>} */
|
for (const [name, field] of state_fields) {
|
||||||
const definition_names = new Map();
|
if (name[0] === '#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {string[]} */
|
// insert backing fields for stuff declared in the constructor
|
||||||
const private_ids = [];
|
if (field.node.type === 'AssignmentExpression') {
|
||||||
|
const member = b.member(b.this, field.key);
|
||||||
|
|
||||||
for (const definition of node.body) {
|
const should_proxy = field.type === '$state' && true; // TODO
|
||||||
if (
|
|
||||||
(definition.type === 'PropertyDefinition' || definition.type === 'MethodDefinition') &&
|
|
||||||
(definition.key.type === 'Identifier' ||
|
|
||||||
definition.key.type === 'PrivateIdentifier' ||
|
|
||||||
definition.key.type === 'Literal')
|
|
||||||
) {
|
|
||||||
const type = definition.key.type;
|
|
||||||
const name = get_name(definition.key, public_state);
|
|
||||||
if (!name) continue;
|
|
||||||
|
|
||||||
// we store the deconflicted name in the map so that we can access it later
|
|
||||||
definition_names.set(definition.key, name);
|
|
||||||
|
|
||||||
const is_private = type === 'PrivateIdentifier';
|
|
||||||
if (is_private) private_ids.push(name);
|
|
||||||
|
|
||||||
if (definition.value?.type === 'CallExpression') {
|
|
||||||
const rune = get_rune(definition.value, context.state.scope);
|
|
||||||
if (
|
|
||||||
rune === '$state' ||
|
|
||||||
rune === '$state.raw' ||
|
|
||||||
rune === '$derived' ||
|
|
||||||
rune === '$derived.by'
|
|
||||||
) {
|
|
||||||
/** @type {StateField} */
|
|
||||||
const field = {
|
|
||||||
kind:
|
|
||||||
rune === '$state'
|
|
||||||
? 'state'
|
|
||||||
: rune === '$state.raw'
|
|
||||||
? 'raw_state'
|
|
||||||
: rune === '$derived.by'
|
|
||||||
? 'derived_by'
|
|
||||||
: 'derived',
|
|
||||||
// @ts-expect-error this is set in the next pass
|
|
||||||
id: is_private ? definition.key : null
|
|
||||||
};
|
|
||||||
|
|
||||||
if (is_private) {
|
|
||||||
private_state.set(name, field);
|
|
||||||
} else {
|
|
||||||
public_state.set(name, field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// each `foo = $state()` needs a backing `#foo` field
|
const key = b.key(name);
|
||||||
for (const [name, field] of public_state) {
|
|
||||||
let deconflicted = name;
|
|
||||||
while (private_ids.includes(deconflicted)) {
|
|
||||||
deconflicted = '_' + deconflicted;
|
|
||||||
}
|
|
||||||
|
|
||||||
private_ids.push(deconflicted);
|
body.push(
|
||||||
field.id = b.private_id(deconflicted);
|
b.prop_def(field.key, null),
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {Array<MethodDefinition | PropertyDefinition>} */
|
b.method('get', key, [], [b.return(b.call('$.get', member))]),
|
||||||
const body = [];
|
|
||||||
|
|
||||||
const child_state = { ...context.state, public_state, private_state };
|
b.method(
|
||||||
|
'set',
|
||||||
|
key,
|
||||||
|
[b.id('value')],
|
||||||
|
[b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Replace parts of the class body
|
// Replace parts of the class body
|
||||||
for (const definition of node.body) {
|
for (const definition of node.body) {
|
||||||
if (
|
if (definition.type !== 'PropertyDefinition') {
|
||||||
definition.type === 'PropertyDefinition' &&
|
body.push(
|
||||||
(definition.key.type === 'Identifier' ||
|
/** @type {MethodDefinition | StaticBlock} */ (context.visit(definition, child_state))
|
||||||
definition.key.type === 'PrivateIdentifier' ||
|
);
|
||||||
definition.key.type === 'Literal')
|
continue;
|
||||||
) {
|
|
||||||
const name = definition_names.get(definition.key);
|
|
||||||
if (!name) continue;
|
|
||||||
|
|
||||||
const is_private = definition.key.type === 'PrivateIdentifier';
|
|
||||||
const field = (is_private ? private_state : public_state).get(name);
|
|
||||||
|
|
||||||
if (definition.value?.type === 'CallExpression' && field !== undefined) {
|
|
||||||
let value = null;
|
|
||||||
|
|
||||||
if (definition.value.arguments.length > 0) {
|
|
||||||
const init = /** @type {Expression} **/ (
|
|
||||||
context.visit(definition.value.arguments[0], child_state)
|
|
||||||
);
|
|
||||||
|
|
||||||
value =
|
|
||||||
field.kind === 'state'
|
|
||||||
? b.call(
|
|
||||||
'$.state',
|
|
||||||
should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init
|
|
||||||
)
|
|
||||||
: field.kind === 'raw_state'
|
|
||||||
? b.call('$.state', init)
|
|
||||||
: field.kind === 'derived_by'
|
|
||||||
? b.call('$.derived', init)
|
|
||||||
: b.call('$.derived', b.thunk(init));
|
|
||||||
} else {
|
|
||||||
// if no arguments, we know it's state as `$derived()` is a compile error
|
|
||||||
value = b.call('$.state');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_private) {
|
|
||||||
body.push(b.prop_def(field.id, value));
|
|
||||||
} else {
|
|
||||||
// #foo;
|
|
||||||
const member = b.member(b.this, field.id);
|
|
||||||
body.push(b.prop_def(field.id, value));
|
|
||||||
|
|
||||||
// get foo() { return this.#foo; }
|
|
||||||
body.push(b.method('get', definition.key, [], [b.return(b.call('$.get', member))]));
|
|
||||||
|
|
||||||
// set foo(value) { this.#foo = value; }
|
|
||||||
const val = b.id('value');
|
|
||||||
|
|
||||||
body.push(
|
|
||||||
b.method(
|
|
||||||
'set',
|
|
||||||
definition.key,
|
|
||||||
[val],
|
|
||||||
[b.stmt(b.call('$.set', member, val, field.kind === 'state' && b.true))]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state)));
|
const name = get_name(definition.key);
|
||||||
}
|
const field = name && /** @type {StateField} */ (state_fields.get(name));
|
||||||
|
|
||||||
return { ...node, body };
|
if (!field) {
|
||||||
}
|
body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
if (name[0] === '#') {
|
||||||
* @param {Identifier | PrivateIdentifier | Literal} node
|
body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
|
||||||
* @param {Map<string, StateField>} public_state
|
} else if (field.node === definition) {
|
||||||
*/
|
const member = b.member(b.this, field.key);
|
||||||
function get_name(node, public_state) {
|
|
||||||
if (node.type === 'Literal') {
|
const should_proxy = field.type === '$state' && true; // TODO
|
||||||
let name = node.value?.toString().replace(regex_invalid_identifier_chars, '_');
|
|
||||||
|
body.push(
|
||||||
// the above could generate conflicts because it has to generate a valid identifier
|
b.prop_def(
|
||||||
// so stuff like `0` and `1` or `state%` and `state^` will result in the same string
|
field.key,
|
||||||
// so we have to de-conflict. We can only check `public_state` because private state
|
/** @type {CallExpression} */ (context.visit(field.value, child_state))
|
||||||
// can't have literal keys
|
),
|
||||||
while (name && public_state.has(name)) {
|
|
||||||
name = '_' + name;
|
b.method('get', definition.key, [], [b.return(b.call('$.get', member))]),
|
||||||
|
|
||||||
|
b.method(
|
||||||
|
'set',
|
||||||
|
definition.key,
|
||||||
|
[b.id('value')],
|
||||||
|
[b.stmt(b.call('$.set', member, b.id('value'), should_proxy && b.true))]
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return name;
|
|
||||||
} else {
|
|
||||||
return node.name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { ...node, body };
|
||||||
}
|
}
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
/** @import { MemberExpression } from 'estree' */
|
|
||||||
/** @import { Context } from '../types.js' */
|
|
||||||
import * as b from '#compiler/builders';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {MemberExpression} node
|
|
||||||
* @param {Context} context
|
|
||||||
*/
|
|
||||||
export function MemberExpression(node, context) {
|
|
||||||
if (
|
|
||||||
context.state.analysis.runes &&
|
|
||||||
node.object.type === 'ThisExpression' &&
|
|
||||||
node.property.type === 'PrivateIdentifier'
|
|
||||||
) {
|
|
||||||
const field = context.state.private_derived.get(node.property.name);
|
|
||||||
|
|
||||||
if (field) {
|
|
||||||
return b.call(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context.next();
|
|
||||||
}
|
|
@ -0,0 +1,13 @@
|
|||||||
|
import { flushSync } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
html: `<button>10</button>`,
|
||||||
|
ssrHtml: `<button>0</button>`,
|
||||||
|
|
||||||
|
async test({ assert, target }) {
|
||||||
|
flushSync();
|
||||||
|
|
||||||
|
assert.htmlEqual(target.innerHTML, `<button>10</button>`);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,13 @@
|
|||||||
|
<script>
|
||||||
|
class Counter {
|
||||||
|
constructor() {
|
||||||
|
this.count = $state(0);
|
||||||
|
$effect(() => {
|
||||||
|
this.count = 10;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const counter = new Counter();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button on:click={() => counter.count++}>{counter.count}</button>
|
@ -0,0 +1,13 @@
|
|||||||
|
import { flushSync } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
html: `<button>10</button>`,
|
||||||
|
ssrHtml: `<button>0</button>`,
|
||||||
|
|
||||||
|
async test({ assert, target }) {
|
||||||
|
flushSync();
|
||||||
|
|
||||||
|
assert.htmlEqual(target.innerHTML, `<button>10</button>`);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,12 @@
|
|||||||
|
<script>
|
||||||
|
const counter = new class Counter {
|
||||||
|
constructor() {
|
||||||
|
this.count = $state(0);
|
||||||
|
$effect(() => {
|
||||||
|
this.count = 10;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button on:click={() => counter.count++}>{counter.count}</button>
|
@ -0,0 +1,3 @@
|
|||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({});
|
@ -0,0 +1,9 @@
|
|||||||
|
<script>
|
||||||
|
class Test {
|
||||||
|
0 = $state();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this[1] = $state();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -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'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,37 @@
|
|||||||
|
<script module>
|
||||||
|
class SomeLogic {
|
||||||
|
trigger() {
|
||||||
|
this.someValue++;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.someValue = $state(0);
|
||||||
|
this.isAboveThree = $derived(this.someValue > 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const someLogic = new SomeLogic();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function increment() {
|
||||||
|
someLogic.trigger();
|
||||||
|
}
|
||||||
|
|
||||||
|
let localDerived = $derived(someLogic.someValue > 3);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
console.log(someLogic.someValue);
|
||||||
|
});
|
||||||
|
$effect(() => {
|
||||||
|
console.log('class trigger ' + someLogic.isAboveThree)
|
||||||
|
});
|
||||||
|
$effect(() => {
|
||||||
|
console.log('local trigger ' + localDerived)
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button on:click={increment}>
|
||||||
|
clicks: {someLogic.someValue}
|
||||||
|
</button>
|
@ -0,0 +1,20 @@
|
|||||||
|
import { flushSync } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
html: `<button>0</button>`,
|
||||||
|
|
||||||
|
test({ assert, target }) {
|
||||||
|
const btn = target.querySelector('button');
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
btn?.click();
|
||||||
|
});
|
||||||
|
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
btn?.click();
|
||||||
|
});
|
||||||
|
assert.htmlEqual(target.innerHTML, `<button>2</button>`);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,12 @@
|
|||||||
|
<script>
|
||||||
|
class Counter {
|
||||||
|
count;
|
||||||
|
|
||||||
|
constructor(count) {
|
||||||
|
this.count = $state(count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const counter = new Counter(0);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={() => counter.count++}>{counter.count}</button>
|
@ -0,0 +1,20 @@
|
|||||||
|
import { flushSync } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
html: `<button>10: 20</button>`,
|
||||||
|
|
||||||
|
test({ assert, target }) {
|
||||||
|
const btn = target.querySelector('button');
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
btn?.click();
|
||||||
|
});
|
||||||
|
assert.htmlEqual(target.innerHTML, `<button>11: 22</button>`);
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
btn?.click();
|
||||||
|
});
|
||||||
|
assert.htmlEqual(target.innerHTML, `<button>12: 24</button>`);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,22 @@
|
|||||||
|
<script>
|
||||||
|
class Counter {
|
||||||
|
constructor(initial) {
|
||||||
|
this.count = $state(initial);
|
||||||
|
}
|
||||||
|
|
||||||
|
increment = () => {
|
||||||
|
this.count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PluggableCounter extends Counter {
|
||||||
|
constructor(initial, plugin) {
|
||||||
|
super(initial)
|
||||||
|
this.custom = $derived(plugin(this.count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const counter = new PluggableCounter(10, (count) => count * 2);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={counter.increment}>{counter.count}: {counter.custom}</button>
|
@ -0,0 +1,20 @@
|
|||||||
|
import { flushSync } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
html: `<button>20</button>`,
|
||||||
|
|
||||||
|
test({ assert, target }) {
|
||||||
|
const btn = target.querySelector('button');
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
btn?.click();
|
||||||
|
});
|
||||||
|
assert.htmlEqual(target.innerHTML, `<button>22</button>`);
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
btn?.click();
|
||||||
|
});
|
||||||
|
assert.htmlEqual(target.innerHTML, `<button>24</button>`);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,18 @@
|
|||||||
|
<script>
|
||||||
|
class Counter {
|
||||||
|
/** @type {number} */
|
||||||
|
#count;
|
||||||
|
|
||||||
|
constructor(initial) {
|
||||||
|
this.#count = $state(initial);
|
||||||
|
this.doubled = $derived(this.#count * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
increment = () => {
|
||||||
|
this.#count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const counter = new Counter(10);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={counter.increment}>{counter.doubled}</button>
|
@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"code": "state_field_duplicate",
|
||||||
|
"message": "`count` has already been declared on this class",
|
||||||
|
"start": {
|
||||||
|
"line": 5,
|
||||||
|
"column": 2
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 5,
|
||||||
|
"column": 24
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,7 @@
|
|||||||
|
export class Counter {
|
||||||
|
count = $state(0);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.count = $state(0);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"code": "state_field_invalid_assignment",
|
||||||
|
"message": "Cannot assign to a state field before its declaration",
|
||||||
|
"start": {
|
||||||
|
"line": 4,
|
||||||
|
"column": 3
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 4,
|
||||||
|
"column": 18
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,9 @@
|
|||||||
|
export class Counter {
|
||||||
|
constructor() {
|
||||||
|
if (true) {
|
||||||
|
this.count = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.count = $state(0);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"code": "state_field_duplicate",
|
||||||
|
"message": "`count` has already been declared on this class",
|
||||||
|
"start": {
|
||||||
|
"line": 5,
|
||||||
|
"column": 2
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 5,
|
||||||
|
"column": 24
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,7 @@
|
|||||||
|
export class Counter {
|
||||||
|
constructor() {
|
||||||
|
this.count = $state(0);
|
||||||
|
this.count = 1;
|
||||||
|
this.count = $state(0);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"code": "state_field_duplicate",
|
||||||
|
"message": "`count` has already been declared on this class",
|
||||||
|
"start": {
|
||||||
|
"line": 5,
|
||||||
|
"column": 2
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 5,
|
||||||
|
"column": 28
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,7 @@
|
|||||||
|
export class Counter {
|
||||||
|
constructor() {
|
||||||
|
this.count = $state(0);
|
||||||
|
this.count = 1;
|
||||||
|
this.count = $state.raw(0);
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,7 @@
|
|||||||
|
export class Counter {
|
||||||
|
constructor() {
|
||||||
|
if (true) {
|
||||||
|
this.count = $state(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"code": "state_field_duplicate",
|
||||||
|
"message": "`count` has already been declared on this class",
|
||||||
|
"start": {
|
||||||
|
"line": 5,
|
||||||
|
"column": 2
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 5,
|
||||||
|
"column": 27
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,7 @@
|
|||||||
|
export class Counter {
|
||||||
|
// prettier-ignore
|
||||||
|
'count' = $state(0);
|
||||||
|
constructor() {
|
||||||
|
this['count'] = $state(0);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"code": "state_field_duplicate",
|
||||||
|
"message": "`count` has already been declared on this class",
|
||||||
|
"start": {
|
||||||
|
"line": 4,
|
||||||
|
"column": 2
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 4,
|
||||||
|
"column": 27
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,6 @@
|
|||||||
|
export class Counter {
|
||||||
|
count = $state(0);
|
||||||
|
constructor() {
|
||||||
|
this['count'] = $state(0);
|
||||||
|
}
|
||||||
|
}
|
@ -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": 5,
|
||||||
|
"column": 16
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 5,
|
||||||
|
"column": 25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,7 @@
|
|||||||
|
const count = 'count';
|
||||||
|
|
||||||
|
export class Counter {
|
||||||
|
constructor() {
|
||||||
|
this[count] = $state(0);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"code": "state_field_invalid_assignment",
|
||||||
|
"message": "Cannot assign to a state field before its declaration",
|
||||||
|
"start": {
|
||||||
|
"line": 3,
|
||||||
|
"column": 2
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 3,
|
||||||
|
"column": 17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,6 @@
|
|||||||
|
export class Counter {
|
||||||
|
constructor() {
|
||||||
|
this.count = -1;
|
||||||
|
this.count = $state(0);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"code": "state_field_invalid_assignment",
|
||||||
|
"message": "Cannot assign to a state field before its declaration",
|
||||||
|
"start": {
|
||||||
|
"line": 2,
|
||||||
|
"column": 1
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 2,
|
||||||
|
"column": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,7 @@
|
|||||||
|
export class Counter {
|
||||||
|
count = -1;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.count = $state(0);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue