Skip to content

Commit 92940ff

Browse files
feat: It works I think?
1 parent adb6e71 commit 92940ff

File tree

24 files changed

+381
-33
lines changed

24 files changed

+381
-33
lines changed

packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import { validate_mutation } from './shared/utils.js';
1717
* @param {Context} context
1818
*/
1919
export function AssignmentExpression(node, context) {
20-
context.state.class_analysis?.register_assignment(node, context);
20+
const stripped_node = context.state.class_analysis?.register_assignment(node, context);
21+
if (stripped_node) {
22+
return stripped_node;
23+
}
24+
2125
const expression = /** @type {Expression} */ (
2226
visit_assignment_expression(node, context, build_assignment) ?? context.next()
2327
);

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-analysis.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { should_proxy } from '../../utils.js';
1313
export function create_client_class_analysis(body) {
1414
/** @type {StateFieldBuilder<Context>} */
1515
function build_state_field({ is_private, field, node, context }) {
16-
let original_id = node.type === 'AssignmentExpression' ? node.left : node.key;
16+
let original_id = node.type === 'AssignmentExpression' ? node.left.property : node.key;
1717
let value;
1818
if (node.type === 'AssignmentExpression') {
1919
// 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) {
5151

5252
/** @type {AssignmentBuilder<Context>} */
5353
function build_assignment({ field, node, context }) {
54-
// ...swap out the assignment to go directly against the private field
55-
node.left.property = field.id;
56-
// ...and swap out the assignment's value for the state field init
57-
node.right = build_init_value(field.kind, node.right.arguments[0], context);
54+
return {
55+
...node,
56+
left: {
57+
...node.left,
58+
// ...swap out the assignment to go directly against the private field
59+
property: field.id
60+
},
61+
// ...and swap out the assignment's value for the state field init
62+
right: build_init_value(field.kind, node.right.arguments[0], context)
63+
};
5864
}
5965

6066
return create_class_analysis(body, build_state_field, build_assignment);
@@ -67,7 +73,9 @@ export function create_client_class_analysis(body) {
6773
* @param {Context} context
6874
*/
6975
function build_init_value(kind, arg, context) {
70-
const init = /** @type {Expression} **/ (context.visit(arg, context.state));
76+
const init = /** @type {Expression} **/ (
77+
context.visit(arg, { ...context.state, in_constructor: false })
78+
);
7179

7280
switch (kind) {
7381
case '$state':

packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import { visit_assignment_expression } from '../../shared/assignments.js';
1010
* @param {Context} context
1111
*/
1212
export function AssignmentExpression(node, context) {
13+
const stripped_node = context.state.class_analysis?.register_assignment(node, context);
14+
if (stripped_node) {
15+
return stripped_node;
16+
}
17+
1318
return visit_assignment_expression(node, context, build_assignment) ?? context.next();
1419
}
1520

packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-analysis.js

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
/** @import { Expression, MethodDefinition, StaticBlock, PropertyDefinition } from 'estree' */
1+
/** @import { Expression, MethodDefinition, StaticBlock, PropertyDefinition, SpreadElement } from 'estree' */
22
/** @import { Context } from '../../types.js' */
33
/** @import { AssignmentBuilder, StateFieldBuilder } from '../../../shared/types.js' */
44
/** @import { ClassAnalysis } from '../../../shared/types.js' */
5+
/** @import { StateCreationRuneName } from '../../../../../../utils.js' */
56

67
import * as b from '#compiler/builders';
78
import { dev } from '../../../../../state.js';
@@ -14,7 +15,7 @@ import { create_class_analysis } from '../../../shared/class_analysis.js';
1415
export function create_server_class_analysis(body) {
1516
/** @type {StateFieldBuilder<Context>} */
1617
function build_state_field({ is_private, field, node, context }) {
17-
let original_id = node.type === 'AssignmentExpression' ? node.left : node.key;
18+
let original_id = node.type === 'AssignmentExpression' ? node.left.property : node.key;
1819
let value;
1920
if (node.type === 'AssignmentExpression') {
2021
// 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) {
3738
// #foo;
3839
const member = b.member(b.this, field.id);
3940

41+
/** @type {Array<MethodDefinition | PropertyDefinition>} */
4042
const defs = [
4143
// #foo;
42-
b.prop_def(field.id, value),
43-
// get foo() { return this.#foo; }
44-
b.method('get', original_id, [], [b.return(b.call(member))])
44+
b.prop_def(field.id, value)
4545
];
4646

47+
// get foo() { return this.#foo; }
48+
if (field.kind === '$state' || field.kind === '$state.raw') {
49+
defs.push(b.method('get', original_id, [], [b.return(member)]));
50+
} else {
51+
defs.push(b.method('get', original_id, [], [b.return(b.call(member))]));
52+
}
53+
4754
// TODO make this work on server
4855
if (dev) {
4956
defs.push(
@@ -61,11 +68,37 @@ export function create_server_class_analysis(body) {
6168

6269
/** @type {AssignmentBuilder<Context>} */
6370
function build_assignment({ field, node, context }) {
64-
node.left.property = field.id;
65-
const init = /** @type {Expression} **/ (context.visit(node.right.arguments[0], context.state));
66-
node.right =
67-
field.kind === '$derived.by' ? b.call('$.once', init) : b.call('$.once', b.thunk(init));
71+
return {
72+
...node,
73+
left: {
74+
...node.left,
75+
// ...swap out the assignment to go directly against the private field
76+
property: field.id
77+
},
78+
// ...and swap out the assignment's value for the state field init
79+
right: build_init_value(field.kind, node.right.arguments[0], context)
80+
};
6881
}
6982

7083
return create_class_analysis(body, build_state_field, build_assignment);
7184
}
85+
86+
/**
87+
*
88+
* @param {StateCreationRuneName} kind
89+
* @param {Expression | SpreadElement} arg
90+
* @param {Context} context
91+
*/
92+
function build_init_value(kind, arg, context) {
93+
const init = /** @type {Expression} **/ (context.visit(arg, context.state));
94+
95+
switch (kind) {
96+
case '$state':
97+
case '$state.raw':
98+
return init;
99+
case '$derived':
100+
return b.call('$.once', b.thunk(init));
101+
case '$derived.by':
102+
return b.call('$.once', init);
103+
}
104+
}

packages/svelte/src/compiler/phases/3-transform/shared/class_analysis.js

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,9 @@ export function create_class_analysis(body, build_state_field, build_assignment)
107107
}
108108

109109
/**
110-
* Important note: It is a syntax error in JavaScript to try to assign to a private class field
111-
* that was not declared in the class body. So there is absolutely no risk of unresolvable conflicts here.
112-
*
113-
* This function will modify the assignment expression passed to it if it is registered as a state field.
114110
* @param {AssignmentExpression} node
115111
* @param {TContext} context
112+
* @returns {AssignmentExpression | null} The node, if `register_assignment` handled its transformation.
116113
*/
117114
function register_assignment(node, context) {
118115
const child_context = create_child_context(context);
@@ -121,30 +118,46 @@ export function create_class_analysis(body, build_state_field, build_assignment)
121118
node.operator === '=' &&
122119
node.left.type === 'MemberExpression' &&
123120
node.left.object.type === 'ThisExpression' &&
124-
node.left.property.type === 'Identifier'
121+
(node.left.property.type === 'Identifier' ||
122+
node.left.property.type === 'PrivateIdentifier')
125123
)
126124
) {
127-
return;
125+
return null;
128126
}
129127

130128
const name = get_name(node.left.property);
131129
if (!name) {
132-
return;
130+
return null;
133131
}
134132

135133
const parsed = parse_stateful_assignment(node, child_context.state.scope);
136134
if (!parsed) {
137-
return;
135+
return null;
138136
}
139137
const { stateful_assignment, rune } = parsed;
140138

141-
const id = deconflict(name);
142-
const field = { kind: rune, id };
143-
public_fields.set(name, field);
139+
const is_private = stateful_assignment.left.property.type === 'PrivateIdentifier';
140+
141+
let field;
142+
if (is_private) {
143+
field = {
144+
kind: rune,
145+
id: /** @type {PrivateIdentifier} */ (stateful_assignment.left.property)
146+
};
147+
private_fields.set(name, field);
148+
} else {
149+
field = {
150+
kind: rune,
151+
// it's safe to do this upfront now because we're guaranteed to already know about all private
152+
// identifiers (they had to have been declared at the class root, before we visited the constructor)
153+
id: deconflict(name)
154+
};
155+
public_fields.set(name, field);
156+
}
144157

145158
const replacer = () => {
146159
const nodes = build_state_field({
147-
is_private: false,
160+
is_private,
148161
field,
149162
node: stateful_assignment,
150163
context: child_context
@@ -156,9 +169,9 @@ export function create_class_analysis(body, build_state_field, build_assignment)
156169
};
157170
replacers.push(replacer);
158171

159-
build_assignment({
160-
node: stateful_assignment,
172+
return build_assignment({
161173
field,
174+
node: stateful_assignment,
162175
context: child_context
163176
});
164177
}
@@ -219,7 +232,20 @@ export function create_class_analysis(body, build_state_field, build_assignment)
219232

220233
const parsed = prop_def_is_stateful(node, child_context.state.scope);
221234
if (!parsed) {
222-
return false;
235+
// this isn't a stateful field definition, but if could become one in the constructor -- so we register
236+
// it, but conditionally -- so that if it's added as a field in the constructor (which causes us to create)
237+
// a field definition for it), we don't end up with a duplicate definition (this one, plus the one we create)
238+
replacers.push(() => {
239+
if (!get_field(name, is_private)) {
240+
new_body.push(
241+
/** @type {PropertyDefinition | MethodDefinition} */ (
242+
// @ts-expect-error generics silliness
243+
child_context.visit(node, child_context.state)
244+
)
245+
);
246+
}
247+
});
248+
return true;
223249
}
224250
const { stateful_prop_def, rune } = parsed;
225251

packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export type AssignmentBuilderParams<TContext extends ServerContext | ClientConte
3434
context: TContext;
3535
};
3636

37-
export type AssignmentBuilder<TContext extends ServerContext | ClientContext> = (params: AssignmentBuilderParams<TContext>) => void;
37+
export type AssignmentBuilder<TContext extends ServerContext | ClientContext> = (params: AssignmentBuilderParams<TContext>) => AssignmentExpression;
3838

3939
export type ClassAnalysis<TContext extends ServerContext | ClientContext> = {
4040
/**
@@ -59,5 +59,5 @@ export type ClassAnalysis<TContext extends ServerContext | ClientContext> = {
5959
* a state field on the class. If it is, it registers that state field and modifies the
6060
* assignment expression.
6161
*/
62-
register_assignment: (node: AssignmentExpression, context: TContext) => void;
62+
register_assignment: (node: AssignmentExpression, context: TContext) => AssignmentExpression | null;
6363
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
html: `<button>10</button>`,
6+
ssrHtml: `<button>0</button>`,
7+
8+
async test({ assert, target }) {
9+
flushSync();
10+
11+
assert.htmlEqual(target.innerHTML, `<button>10</button>`);
12+
}
13+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
class Counter {
3+
constructor() {
4+
this.count = $state(0);
5+
$effect(() => {
6+
this.count = 10;
7+
});
8+
}
9+
}
10+
const counter = new Counter();
11+
</script>
12+
13+
<button on:click={() => counter.count++}>{counter.count}</button>

0 commit comments

Comments
 (0)