Skip to content

Commit 005ba29

Browse files
feat: client
1 parent 033a466 commit 005ba29

File tree

8 files changed

+344
-168
lines changed

8 files changed

+344
-168
lines changed

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

+3-5
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,7 @@ export function client_component(analysis, options) {
161161
},
162162
events: new Set(),
163163
preserve_whitespace: options.preserveWhitespace,
164-
public_state: new Map(),
165-
private_state: new Map(),
164+
class_analysis: null,
166165
transform: {},
167166
in_constructor: false,
168167
instance_level_snippets: [],
@@ -669,10 +668,9 @@ export function client_module(analysis, options) {
669668
options,
670669
scope: analysis.module.scope,
671670
scopes: analysis.module.scopes,
672-
public_state: new Map(),
673-
private_state: new Map(),
674671
transform: {},
675-
in_constructor: false
672+
in_constructor: false,
673+
class_analysis: null
676674
};
677675

678676
const module = /** @type {ESTree.Program} */ (

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
1313
import type { TransformState } from '../types.js';
1414
import type { ComponentAnalysis } from '../../types.js';
1515
import type { SourceLocation } from '#shared';
16+
import type { StateCreationRuneName } from '../../../../utils.js';
17+
import type { ClassAnalysis } from './visitors/shared/class-analysis.js';
1618

1719
export interface ClientTransformState extends TransformState {
18-
readonly private_state: Map<string, StateField>;
19-
readonly public_state: Map<string, StateField>;
20+
readonly class_analysis: ClassAnalysis | null;
2021

2122
/**
2223
* `true` if the current lexical scope belongs to a class constructor. this allows
@@ -95,7 +96,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
9596
}
9697

9798
export interface StateField {
98-
kind: 'state' | 'raw_state' | 'derived' | 'derived_by';
99+
kind: StateCreationRuneName;
99100
id: PrivateIdentifier;
100101
}
101102

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Pattern } from 'estree' */
1+
/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Pattern, MemberExpression, ThisExpression, PrivateIdentifier, CallExpression } from 'estree' */
22
/** @import { AST } from '#compiler' */
33
/** @import { Context } from '../types.js' */
44
import * as b from '#compiler/builders';
@@ -17,6 +17,7 @@ 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);
2021
const expression = /** @type {Expression} */ (
2122
visit_assignment_expression(node, context, build_assignment) ?? context.next()
2223
);
@@ -56,15 +57,15 @@ function build_assignment(operator, left, right, context) {
5657
left.type === 'MemberExpression' &&
5758
left.property.type === 'PrivateIdentifier'
5859
) {
59-
const private_state = context.state.private_state.get(left.property.name);
60+
const private_state = context.state.class_analysis?.private_state.get(left.property.name);
6061

6162
if (private_state !== undefined) {
6263
let value = /** @type {Expression} */ (
6364
context.visit(build_assignment_value(operator, left, right))
6465
);
6566

6667
const needs_proxy =
67-
private_state.kind === 'state' &&
68+
private_state.kind === '$state' &&
6869
is_non_coercive_operator(operator) &&
6970
should_proxy(value, context.state.scope);
7071

Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
/** @import { ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */
1+
/** @import { ClassBody, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */
22
/** @import { Context, StateField } from '../types' */
3-
import * as b from '#compiler/builders';
43
import { regex_invalid_identifier_chars } from '../../../patterns.js';
5-
import { get_rune } from '../../../scope.js';
6-
import { should_proxy } from '../utils.js';
4+
import { ClassAnalysis } from './shared/class-analysis.js';
75

86
/**
97
* @param {ClassBody} node
@@ -15,170 +13,46 @@ export function ClassBody(node, context) {
1513
return;
1614
}
1715

18-
/** @type {Map<string, StateField>} */
19-
const public_state = new Map();
20-
21-
/** @type {Map<string, StateField>} */
22-
const private_state = new Map();
23-
24-
/** @type {Map<(MethodDefinition|PropertyDefinition)["key"], string>} */
25-
const definition_names = new Map();
26-
27-
/** @type {string[]} */
28-
const private_ids = [];
16+
const class_analysis = new ClassAnalysis();
2917

3018
for (const definition of node.body) {
31-
if (
32-
(definition.type === 'PropertyDefinition' || definition.type === 'MethodDefinition') &&
33-
(definition.key.type === 'Identifier' ||
34-
definition.key.type === 'PrivateIdentifier' ||
35-
definition.key.type === 'Literal')
36-
) {
37-
const type = definition.key.type;
38-
const name = get_name(definition.key, public_state);
39-
if (!name) continue;
40-
41-
// we store the deconflicted name in the map so that we can access it later
42-
definition_names.set(definition.key, name);
43-
44-
const is_private = type === 'PrivateIdentifier';
45-
if (is_private) private_ids.push(name);
46-
47-
if (definition.value?.type === 'CallExpression') {
48-
const rune = get_rune(definition.value, context.state.scope);
49-
if (
50-
rune === '$state' ||
51-
rune === '$state.raw' ||
52-
rune === '$derived' ||
53-
rune === '$derived.by'
54-
) {
55-
/** @type {StateField} */
56-
const field = {
57-
kind:
58-
rune === '$state'
59-
? 'state'
60-
: rune === '$state.raw'
61-
? 'raw_state'
62-
: rune === '$derived.by'
63-
? 'derived_by'
64-
: 'derived',
65-
// @ts-expect-error this is set in the next pass
66-
id: is_private ? definition.key : null
67-
};
68-
69-
if (is_private) {
70-
private_state.set(name, field);
71-
} else {
72-
public_state.set(name, field);
73-
}
74-
}
75-
}
76-
}
19+
class_analysis.register_body_definition(definition, context.state.scope);
7720
}
7821

79-
// each `foo = $state()` needs a backing `#foo` field
80-
for (const [name, field] of public_state) {
81-
let deconflicted = name;
82-
while (private_ids.includes(deconflicted)) {
83-
deconflicted = '_' + deconflicted;
84-
}
85-
86-
private_ids.push(deconflicted);
87-
field.id = b.private_id(deconflicted);
88-
}
22+
class_analysis.finalize_property_definitions();
8923

9024
/** @type {Array<MethodDefinition | PropertyDefinition>} */
9125
const body = [];
9226

93-
const child_state = { ...context.state, public_state, private_state };
27+
const child_state = {
28+
...context.state,
29+
class_analysis
30+
};
31+
32+
// we need to visit the constructor first so that it can add to the field maps.
33+
const constructor_node = node.body.find(
34+
(child) => child.type === 'MethodDefinition' && child.kind === 'constructor'
35+
);
36+
const constructor = constructor_node && context.visit(constructor_node, child_state);
9437

9538
// Replace parts of the class body
9639
for (const definition of node.body) {
97-
if (
98-
definition.type === 'PropertyDefinition' &&
99-
(definition.key.type === 'Identifier' ||
100-
definition.key.type === 'PrivateIdentifier' ||
101-
definition.key.type === 'Literal')
102-
) {
103-
const name = definition_names.get(definition.key);
104-
if (!name) continue;
105-
106-
const is_private = definition.key.type === 'PrivateIdentifier';
107-
const field = (is_private ? private_state : public_state).get(name);
108-
109-
if (definition.value?.type === 'CallExpression' && field !== undefined) {
110-
let value = null;
111-
112-
if (definition.value.arguments.length > 0) {
113-
const init = /** @type {Expression} **/ (
114-
context.visit(definition.value.arguments[0], child_state)
115-
);
116-
117-
value =
118-
field.kind === 'state'
119-
? b.call(
120-
'$.state',
121-
should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init
122-
)
123-
: field.kind === 'raw_state'
124-
? b.call('$.state', init)
125-
: field.kind === 'derived_by'
126-
? b.call('$.derived', init)
127-
: b.call('$.derived', b.thunk(init));
128-
} else {
129-
// if no arguments, we know it's state as `$derived()` is a compile error
130-
value = b.call('$.state');
131-
}
132-
133-
if (is_private) {
134-
body.push(b.prop_def(field.id, value));
135-
} else {
136-
// #foo;
137-
const member = b.member(b.this, field.id);
138-
body.push(b.prop_def(field.id, value));
139-
140-
// get foo() { return this.#foo; }
141-
body.push(b.method('get', definition.key, [], [b.return(b.call('$.get', member))]));
40+
if (definition === constructor_node) {
41+
body.push(/** @type {MethodDefinition} */ (constructor));
42+
continue;
43+
}
14244

143-
// set foo(value) { this.#foo = value; }
144-
const val = b.id('value');
45+
const state_field = class_analysis.build_state_field_from_body_definition(definition, context);
14546

146-
body.push(
147-
b.method(
148-
'set',
149-
definition.key,
150-
[val],
151-
[b.stmt(b.call('$.set', member, val, field.kind === 'state' && b.true))]
152-
)
153-
);
154-
}
155-
continue;
156-
}
47+
if (state_field) {
48+
body.push(...state_field);
49+
continue;
15750
}
15851

15952
body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state)));
16053
}
16154

162-
return { ...node, body };
163-
}
55+
body.push(...class_analysis.constructor_state_fields);
16456

165-
/**
166-
* @param {Identifier | PrivateIdentifier | Literal} node
167-
* @param {Map<string, StateField>} public_state
168-
*/
169-
function get_name(node, public_state) {
170-
if (node.type === 'Literal') {
171-
let name = node.value?.toString().replace(regex_invalid_identifier_chars, '_');
172-
173-
// the above could generate conflicts because it has to generate a valid identifier
174-
// so stuff like `0` and `1` or `state%` and `state^` will result in the same string
175-
// so we have to de-conflict. We can only check `public_state` because private state
176-
// can't have literal keys
177-
while (name && public_state.has(name)) {
178-
name = '_' + name;
179-
}
180-
return name;
181-
} else {
182-
return node.name;
183-
}
57+
return { ...node, body };
18458
}

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import * as b from '#compiler/builders';
99
export function MemberExpression(node, context) {
1010
// rewrite `this.#foo` as `this.#foo.v` inside a constructor
1111
if (node.property.type === 'PrivateIdentifier') {
12-
const field = context.state.private_state.get(node.property.name);
12+
const field = context.state.class_analysis?.private_state.get(node.property.name);
1313
if (field) {
14-
return context.state.in_constructor && (field.kind === 'raw_state' || field.kind === 'state')
14+
return context.state.in_constructor &&
15+
(field.kind === '$state.raw' || field.kind === '$state')
1516
? b.member(node, 'v')
1617
: b.call('$.get', node);
1718
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function UpdateExpression(node, context) {
1515
argument.type === 'MemberExpression' &&
1616
argument.object.type === 'ThisExpression' &&
1717
argument.property.type === 'PrivateIdentifier' &&
18-
context.state.private_state.has(argument.property.name)
18+
context.state.class_analysis?.private_state.has(argument.property.name)
1919
) {
2020
let fn = '$.update';
2121
if (node.prefix) fn += '_pre';

0 commit comments

Comments
 (0)