mirror of https://github.com/sveltejs/svelte
pull/15820/head
parent
4d6422cca4
commit
adb6e712e9
@ -1,301 +0,0 @@
|
||||
/** @import { Context, StateField } from '../../types.js' */
|
||||
/** @import { AssignmentExpression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition, CallExpression, Expression, StaticBlock, SpreadElement } from 'estree' */
|
||||
/** @import { Scope } from '#compiler' */
|
||||
/** @import { StateCreationRuneName } from '../../../../../../utils.js' */
|
||||
import * as b from '#compiler/builders';
|
||||
import { is_state_creation_rune } from '../../../../../../utils.js';
|
||||
import { regex_invalid_identifier_chars } from '../../../../patterns.js';
|
||||
import { get_rune } from '../../../../scope.js';
|
||||
import { should_proxy } from '../../utils.js';
|
||||
|
||||
export class ClassAnalysis {
|
||||
/** @type {Map<string, StateField>} */
|
||||
public_state = new Map();
|
||||
|
||||
/** @type {Map<string, StateField>} */
|
||||
private_state = new Map();
|
||||
|
||||
/**
|
||||
* Any state fields discovered from {@link register_assignment} that need to be added to the class body.
|
||||
* @type {Array<PropertyDefinition | MethodDefinition>}
|
||||
*/
|
||||
constructor_state_fields = [];
|
||||
|
||||
/** @type {Map<(MethodDefinition | PropertyDefinition)["key"], string>} */
|
||||
#definition_names = new Map();
|
||||
|
||||
/** @type {Set<string>} */
|
||||
#private_ids = new Set();
|
||||
|
||||
/**
|
||||
* @param {MethodDefinition | PropertyDefinition | StaticBlock} node
|
||||
* @param {Scope} scope
|
||||
*/
|
||||
register_body_definition(node, scope) {
|
||||
if (
|
||||
!(
|
||||
(node.type === 'PropertyDefinition' || node.type === 'MethodDefinition') &&
|
||||
(node.key.type === 'Identifier' ||
|
||||
node.key.type === 'PrivateIdentifier' ||
|
||||
node.key.type === 'Literal')
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* We don't know if the node is stateful yet, but we still need to register some details.
|
||||
* For example: If the node is a private identifier, we could accidentally conflict with it later
|
||||
* if we create a private field for public state (as would happen in this example:)
|
||||
*
|
||||
* ```ts
|
||||
* class Foo {
|
||||
* #count = 0;
|
||||
* count = $state(0); // would become #count if we didn't know about the private field above
|
||||
* }
|
||||
*/
|
||||
|
||||
const name = ClassAnalysis.#get_name(node.key, this.public_state);
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we store the deconflicted name in the map so that we can access it later
|
||||
this.#definition_names.set(node.key, name);
|
||||
|
||||
const is_private = node.key.type === 'PrivateIdentifier';
|
||||
if (is_private) {
|
||||
this.#private_ids.add(name);
|
||||
}
|
||||
|
||||
const rune = get_rune(node.value, scope);
|
||||
if (!rune || !is_state_creation_rune(rune)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_private) {
|
||||
this.private_state.set(name, { kind: rune, id: /** @type {PrivateIdentifier} */ (node.key) });
|
||||
} else {
|
||||
// We can't set the ID until we've identified all of the private state fields,
|
||||
// otherwise we might conflict with them. After registering all property definitions,
|
||||
// call `finalize_property_definitions` to populate the IDs.
|
||||
// @ts-expect-error this is set in `finalize_property_definitions`
|
||||
this.public_state.set(name, { kind: rune, id: undefined });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves all of the registered public state fields to their final private IDs.
|
||||
* Must be called after all property definitions have been registered.
|
||||
*/
|
||||
finalize_property_definitions() {
|
||||
for (const [name, field] of this.public_state) {
|
||||
field.id = this.#deconflict(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {Context} context
|
||||
*/
|
||||
register_assignment(node, context) {
|
||||
if (
|
||||
!(
|
||||
node.operator === '=' &&
|
||||
node.left.type === 'MemberExpression' &&
|
||||
node.left.object.type === 'ThisExpression' &&
|
||||
node.left.property.type === 'Identifier'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = ClassAnalysis.#get_name(node.left.property, this.public_state);
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rune = get_rune(node.right, context.state.scope);
|
||||
if (!rune || !is_state_creation_rune(rune)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = this.#deconflict(name);
|
||||
const field = { kind: rune, id };
|
||||
this.public_state.set(name, field);
|
||||
|
||||
// We need to do two things:
|
||||
// - Communicate to the class body visitor that it needs to append nodes to create a state field
|
||||
// - Modify the assignment expression so that it's valid
|
||||
this.constructor_state_fields.push(
|
||||
...this.build_state_field(
|
||||
false,
|
||||
field,
|
||||
node.left.property,
|
||||
// this will initialize the field without assigning to it, delegating to the constructor
|
||||
null,
|
||||
context
|
||||
)
|
||||
);
|
||||
|
||||
// ...swap out the assignment to go directly against the private field
|
||||
node.left.property = id;
|
||||
// ...and swap out the assignment's value for the state field init
|
||||
node.right = this.#build_init_value(
|
||||
rune,
|
||||
/** @type {CallExpression} */ (node.right).arguments[0],
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {PropertyDefinition | MethodDefinition | StaticBlock} node
|
||||
* @param {Context} context
|
||||
* @returns {Array<PropertyDefinition | MethodDefinition> | null}
|
||||
*/
|
||||
build_state_field_from_body_definition(node, context) {
|
||||
if (
|
||||
!(
|
||||
node.type === 'PropertyDefinition' &&
|
||||
(node.key.type === 'Identifier' ||
|
||||
node.key.type === 'PrivateIdentifier' ||
|
||||
node.key.type === 'Literal')
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = this.#definition_names.get(node.key);
|
||||
if (!name) throw new Error('This should not be possible'); // TODO
|
||||
|
||||
const is_private = node.key.type === 'PrivateIdentifier';
|
||||
const field = (is_private ? this.private_state : this.public_state).get(name);
|
||||
|
||||
if (!field) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.build_state_field(
|
||||
is_private,
|
||||
field,
|
||||
node.key,
|
||||
// we can cast this because if we have a field for this definition it definitely is a call
|
||||
// expression, otherwise it wouldn't have produced a rune earlier.
|
||||
/** @type {CallExpression} */ (node.value),
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} is_private
|
||||
* @param {StateField} field
|
||||
* @param {(MethodDefinition | PropertyDefinition)["key"]} original_definition_id
|
||||
* @param {CallExpression | null} call_expression
|
||||
* @param {Context} context
|
||||
*
|
||||
* @returns {Array<PropertyDefinition | MethodDefinition>}
|
||||
*/
|
||||
build_state_field(is_private, field, original_definition_id, call_expression, context) {
|
||||
let value;
|
||||
if (!call_expression) {
|
||||
// if there's no call expression, this is state that's created in the constructor.
|
||||
// it's guaranteed to be the very first assignment to this field, so we initialize
|
||||
// the field but don't assign to it.
|
||||
value = null;
|
||||
} else if (call_expression.arguments.length > 0) {
|
||||
value = this.#build_init_value(field.kind, call_expression.arguments[0], context);
|
||||
} else {
|
||||
// if no arguments, we know it's state as `$derived()` is a compile error
|
||||
value = b.call('$.state');
|
||||
}
|
||||
|
||||
if (is_private) {
|
||||
return [b.prop_def(field.id, value)];
|
||||
}
|
||||
|
||||
const member = b.member(b.this, field.id);
|
||||
const val = b.id('value');
|
||||
return [
|
||||
// #foo;
|
||||
b.prop_def(field.id, value),
|
||||
// get foo() { return this.#foo; }
|
||||
b.method('get', original_definition_id, [], [b.return(b.call('$.get', member))]),
|
||||
// set foo(value) { this.#foo = value; }
|
||||
b.method(
|
||||
'set',
|
||||
original_definition_id,
|
||||
[val],
|
||||
[b.stmt(b.call('$.set', member, val, field.kind === '$state' && b.true))]
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {StateCreationRuneName} kind
|
||||
* @param {Expression | SpreadElement} arg
|
||||
* @param {Context} context
|
||||
*/
|
||||
#build_init_value(kind, arg, context) {
|
||||
const init = /** @type {Expression} **/ (
|
||||
context.visit(arg, {
|
||||
...context.state,
|
||||
class_analysis: this
|
||||
})
|
||||
);
|
||||
|
||||
switch (kind) {
|
||||
case '$state':
|
||||
return b.call(
|
||||
'$.state',
|
||||
should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init
|
||||
);
|
||||
case '$state.raw':
|
||||
return b.call('$.state', init);
|
||||
case '$derived':
|
||||
return b.call('$.derived', b.thunk(init));
|
||||
case '$derived.by':
|
||||
return b.call('$.derived', init);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {PrivateIdentifier}
|
||||
*/
|
||||
#deconflict(name) {
|
||||
let deconflicted = name;
|
||||
while (this.#private_ids.has(deconflicted)) {
|
||||
deconflicted = '_' + deconflicted;
|
||||
}
|
||||
|
||||
this.#private_ids.add(deconflicted);
|
||||
return b.private_id(deconflicted);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Identifier | PrivateIdentifier | Literal} node
|
||||
* @param {Map<string, unknown>} public_state
|
||||
*/
|
||||
static #get_name(node, public_state) {
|
||||
if (node.type === 'Literal') {
|
||||
let name = node.value?.toString().replace(regex_invalid_identifier_chars, '_');
|
||||
|
||||
// the above could generate conflicts because it has to generate a valid identifier
|
||||
// so stuff like `0` and `1` or `state%` and `state^` will result in the same string
|
||||
// so we have to de-conflict. We can only check `public_state` because private state
|
||||
// can't have literal keys
|
||||
while (name && public_state.has(name)) {
|
||||
name = '_' + name;
|
||||
}
|
||||
return name;
|
||||
} else {
|
||||
return node.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
/** @import { Context } from '../../types.js' */
|
||||
/** @import { MethodDefinition, PropertyDefinition, Expression, StaticBlock, SpreadElement } from 'estree' */
|
||||
/** @import { StateCreationRuneName } from '../../../../../../utils.js' */
|
||||
/** @import { AssignmentBuilder, ClassAnalysis, StateFieldBuilder } from '../../../shared/types.js' */
|
||||
import * as b from '#compiler/builders';
|
||||
import { create_class_analysis } from '../../../shared/class_analysis.js';
|
||||
import { should_proxy } from '../../utils.js';
|
||||
|
||||
/**
|
||||
* @param {Array<MethodDefinition | PropertyDefinition | StaticBlock>} body
|
||||
* @returns {ClassAnalysis<Context>}
|
||||
*/
|
||||
export function create_client_class_analysis(body) {
|
||||
/** @type {StateFieldBuilder<Context>} */
|
||||
function build_state_field({ is_private, field, node, context }) {
|
||||
let original_id = node.type === 'AssignmentExpression' ? node.left : node.key;
|
||||
let value;
|
||||
if (node.type === 'AssignmentExpression') {
|
||||
// if there's no call expression, this is state that's created in the constructor.
|
||||
// it's guaranteed to be the very first assignment to this field, so we initialize
|
||||
// the field but don't assign to it.
|
||||
value = null;
|
||||
} else if (node.value.arguments.length > 0) {
|
||||
value = build_init_value(field.kind, node.value.arguments[0], context);
|
||||
} else {
|
||||
// if no arguments, we know it's state as `$derived()` is a compile error
|
||||
value = b.call('$.state');
|
||||
}
|
||||
|
||||
if (is_private) {
|
||||
return [b.prop_def(field.id, value)];
|
||||
}
|
||||
|
||||
const member = b.member(b.this, field.id);
|
||||
const val = b.id('value');
|
||||
|
||||
return [
|
||||
// #foo;
|
||||
b.prop_def(field.id, value),
|
||||
// get foo() { return this.#foo; }
|
||||
b.method('get', original_id, [], [b.return(b.call('$.get', member))]),
|
||||
// set foo(value) { this.#foo = value; }
|
||||
b.method(
|
||||
'set',
|
||||
original_id,
|
||||
[val],
|
||||
[b.stmt(b.call('$.set', member, val, field.kind === '$state' && b.true))]
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
/** @type {AssignmentBuilder<Context>} */
|
||||
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 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':
|
||||
return b.call(
|
||||
'$.state',
|
||||
should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init
|
||||
);
|
||||
case '$state.raw':
|
||||
return b.call('$.state', init);
|
||||
case '$derived':
|
||||
return b.call('$.derived', b.thunk(init));
|
||||
case '$derived.by':
|
||||
return b.call('$.derived', init);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
/** @import { Expression, MethodDefinition, StaticBlock, PropertyDefinition } from 'estree' */
|
||||
/** @import { Context } from '../../types.js' */
|
||||
/** @import { AssignmentBuilder, StateFieldBuilder } from '../../../shared/types.js' */
|
||||
/** @import { ClassAnalysis } from '../../../shared/types.js' */
|
||||
|
||||
import * as b from '#compiler/builders';
|
||||
import { dev } from '../../../../../state.js';
|
||||
import { create_class_analysis } from '../../../shared/class_analysis.js';
|
||||
|
||||
/**
|
||||
* @param {Array<MethodDefinition | PropertyDefinition | StaticBlock>} body
|
||||
* @returns {ClassAnalysis<Context>}
|
||||
*/
|
||||
export function create_server_class_analysis(body) {
|
||||
/** @type {StateFieldBuilder<Context>} */
|
||||
function build_state_field({ is_private, field, node, context }) {
|
||||
let original_id = node.type === 'AssignmentExpression' ? node.left : node.key;
|
||||
let value;
|
||||
if (node.type === 'AssignmentExpression') {
|
||||
// This means it's a state assignment in the constructor (this.foo = $state('bar'))
|
||||
// which means the state field needs to have no default value so that the initial
|
||||
// value can be assigned in the constructor.
|
||||
value = null;
|
||||
} else if (field.kind !== '$derived' && field.kind !== '$derived.by') {
|
||||
return [/** @type {PropertyDefinition} */ (context.visit(node, context.state))];
|
||||
} else {
|
||||
const init = /** @type {Expression} **/ (
|
||||
context.visit(node.value.arguments[0], context.state)
|
||||
);
|
||||
value =
|
||||
field.kind === '$derived.by' ? b.call('$.once', init) : b.call('$.once', b.thunk(init));
|
||||
}
|
||||
|
||||
if (is_private) {
|
||||
return [b.prop_def(field.id, value)];
|
||||
}
|
||||
// #foo;
|
||||
const member = b.member(b.this, field.id);
|
||||
|
||||
const defs = [
|
||||
// #foo;
|
||||
b.prop_def(field.id, value),
|
||||
// get foo() { return this.#foo; }
|
||||
b.method('get', original_id, [], [b.return(b.call(member))])
|
||||
];
|
||||
|
||||
// TODO make this work on server
|
||||
if (dev) {
|
||||
defs.push(
|
||||
b.method(
|
||||
'set',
|
||||
original_id,
|
||||
[b.id('_')],
|
||||
[b.throw_error(`Cannot update a derived property ('${name}')`)]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return defs;
|
||||
}
|
||||
|
||||
/** @type {AssignmentBuilder<Context>} */
|
||||
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 create_class_analysis(body, build_state_field, build_assignment);
|
||||
}
|
||||
@ -0,0 +1,334 @@
|
||||
/** @import { AssignmentExpression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition, StaticBlock } from 'estree' */
|
||||
/** @import { StateField } from '../types.js' */
|
||||
/** @import { Context as ClientContext } from '../client/types.js' */
|
||||
/** @import { Context as ServerContext } from '../server/types.js' */
|
||||
/** @import { StateCreationRuneName } from '../../../../utils.js' */
|
||||
/** @import { AssignmentBuilder, ClassAnalysis, StateFieldBuilder, StatefulAssignment, StatefulPropertyDefinition } from './types.js' */
|
||||
/** @import { Scope } from '../../scope.js' */
|
||||
import * as b from '#compiler/builders';
|
||||
import { once } from '../../../../internal/server/index.js';
|
||||
import { is_state_creation_rune, STATE_CREATION_RUNES } from '../../../../utils.js';
|
||||
import { regex_invalid_identifier_chars } from '../../patterns.js';
|
||||
import { get_rune } from '../../scope.js';
|
||||
|
||||
/**
|
||||
* @template {ClientContext | ServerContext} TContext
|
||||
* @param {Array<PropertyDefinition | MethodDefinition | StaticBlock>} body
|
||||
* @param {StateFieldBuilder<TContext>} build_state_field
|
||||
* @param {AssignmentBuilder<TContext>} build_assignment
|
||||
* @returns {ClassAnalysis<TContext>}
|
||||
*/
|
||||
export function create_class_analysis(body, build_state_field, build_assignment) {
|
||||
/** @type {Map<string, StateField>} */
|
||||
const public_fields = new Map();
|
||||
|
||||
/** @type {Map<string, StateField>} */
|
||||
const private_fields = new Map();
|
||||
|
||||
/** @type {Array<PropertyDefinition | MethodDefinition>} */
|
||||
const new_body = [];
|
||||
|
||||
/**
|
||||
* Private identifiers in use by this analysis.
|
||||
* Factoid: Unlike public class fields, private fields _must_ be declared in the class body
|
||||
* before use. So the following is actually a JavaScript syntax error, which means we can
|
||||
* be 100% certain we know all private fields after parsing the class body:
|
||||
*
|
||||
* ```ts
|
||||
* class Example {
|
||||
* constructor() {
|
||||
* this.public = 'foo'; // not a problem!
|
||||
* this.#private = 'bar'; // JavaScript parser error
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
const private_ids = new Set();
|
||||
|
||||
/**
|
||||
* A registry of functions to call to complete body modifications.
|
||||
* Replacements may insert more than one node to the body. The original
|
||||
* body should not be modified -- instead, replacers should push new
|
||||
* nodes to new_body.
|
||||
*
|
||||
* @type {Array<() => void>}
|
||||
*/
|
||||
const replacers = [];
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {boolean} is_private
|
||||
* @param {ReadonlyArray<StateCreationRuneName>} [kinds]
|
||||
*/
|
||||
function get_field(name, is_private, kinds = STATE_CREATION_RUNES) {
|
||||
const value = (is_private ? private_fields : public_fields).get(name);
|
||||
if (value && kinds.includes(value.kind)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TContext} context
|
||||
* @returns {TContext}
|
||||
*/
|
||||
function create_child_context(context) {
|
||||
return {
|
||||
...context,
|
||||
state: {
|
||||
...context.state,
|
||||
class_analysis
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TContext} context
|
||||
*/
|
||||
function generate_body(context) {
|
||||
const child_context = create_child_context(context);
|
||||
for (const node of body) {
|
||||
const was_registered = register_body_definition(node, child_context);
|
||||
if (!was_registered) {
|
||||
new_body.push(
|
||||
/** @type {PropertyDefinition | MethodDefinition} */ (
|
||||
// @ts-expect-error generics silliness
|
||||
child_context.visit(node, child_context.state)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const replacer of replacers) {
|
||||
replacer();
|
||||
}
|
||||
|
||||
return new_body;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
function register_assignment(node, context) {
|
||||
const child_context = create_child_context(context);
|
||||
if (
|
||||
!(
|
||||
node.operator === '=' &&
|
||||
node.left.type === 'MemberExpression' &&
|
||||
node.left.object.type === 'ThisExpression' &&
|
||||
node.left.property.type === 'Identifier'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = get_name(node.left.property);
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parse_stateful_assignment(node, child_context.state.scope);
|
||||
if (!parsed) {
|
||||
return;
|
||||
}
|
||||
const { stateful_assignment, rune } = parsed;
|
||||
|
||||
const id = deconflict(name);
|
||||
const field = { kind: rune, id };
|
||||
public_fields.set(name, field);
|
||||
|
||||
const replacer = () => {
|
||||
const nodes = build_state_field({
|
||||
is_private: false,
|
||||
field,
|
||||
node: stateful_assignment,
|
||||
context: child_context
|
||||
});
|
||||
if (!nodes) {
|
||||
return;
|
||||
}
|
||||
new_body.push(...nodes);
|
||||
};
|
||||
replacers.push(replacer);
|
||||
|
||||
build_assignment({
|
||||
node: stateful_assignment,
|
||||
field,
|
||||
context: child_context
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PropertyDefinition | MethodDefinition | StaticBlock} node
|
||||
* @param {TContext} child_context
|
||||
* @returns {boolean} if this node is stateful and was registered
|
||||
*/
|
||||
function register_body_definition(node, child_context) {
|
||||
if (node.type === 'MethodDefinition' && node.kind === 'constructor') {
|
||||
// life is easier to reason about if we've visited the constructor
|
||||
// and registered its public state field before we start building
|
||||
// anything else
|
||||
replacers.unshift(() => {
|
||||
new_body.push(
|
||||
/** @type {MethodDefinition} */ (
|
||||
// @ts-expect-error generics silliness
|
||||
child_context.visit(node, child_context.state)
|
||||
)
|
||||
);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
!(
|
||||
(node.type === 'PropertyDefinition' || node.type === 'MethodDefinition') &&
|
||||
(node.key.type === 'Identifier' ||
|
||||
node.key.type === 'PrivateIdentifier' ||
|
||||
node.key.type === 'Literal')
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* We don't know if the node is stateful yet, but we still need to register some details.
|
||||
* For example: If the node is a private identifier, we could accidentally conflict with it later
|
||||
* if we create a private field for public state (as would happen in this example:)
|
||||
*
|
||||
* ```ts
|
||||
* class Foo {
|
||||
* #count = 0;
|
||||
* count = $state(0); // would become #count if we didn't know about the private field above
|
||||
* }
|
||||
*/
|
||||
|
||||
const name = get_name(node.key);
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const is_private = node.key.type === 'PrivateIdentifier';
|
||||
if (is_private) {
|
||||
private_ids.add(name);
|
||||
}
|
||||
|
||||
const parsed = prop_def_is_stateful(node, child_context.state.scope);
|
||||
if (!parsed) {
|
||||
return false;
|
||||
}
|
||||
const { stateful_prop_def, rune } = parsed;
|
||||
|
||||
let field;
|
||||
if (is_private) {
|
||||
field = {
|
||||
kind: rune,
|
||||
id: /** @type {PrivateIdentifier} */ (stateful_prop_def.key)
|
||||
};
|
||||
private_fields.set(name, field);
|
||||
} else {
|
||||
// We can't set the ID until we've identified all of the private state fields,
|
||||
// otherwise we might conflict with them. After registering all property definitions,
|
||||
// call `finalize_property_definitions` to populate the IDs. So long as we don't
|
||||
// access the ID before the end of this loop, we're fine!
|
||||
const id = once(() => deconflict(name));
|
||||
field = {
|
||||
kind: rune,
|
||||
get id() {
|
||||
return id();
|
||||
}
|
||||
};
|
||||
public_fields.set(name, field);
|
||||
}
|
||||
|
||||
const replacer = () => {
|
||||
const nodes = build_state_field({
|
||||
is_private,
|
||||
field,
|
||||
node: stateful_prop_def,
|
||||
context: child_context
|
||||
});
|
||||
if (!nodes) {
|
||||
return;
|
||||
}
|
||||
new_body.push(...nodes);
|
||||
};
|
||||
replacers.push(replacer);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {PrivateIdentifier}
|
||||
*/
|
||||
function deconflict(name) {
|
||||
let deconflicted = name;
|
||||
while (private_ids.has(deconflicted)) {
|
||||
deconflicted = '_' + deconflicted;
|
||||
}
|
||||
|
||||
private_ids.add(deconflicted);
|
||||
return b.private_id(deconflicted);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Identifier | PrivateIdentifier | Literal} node
|
||||
*/
|
||||
function get_name(node) {
|
||||
if (node.type === 'Literal') {
|
||||
let name = node.value?.toString().replace(regex_invalid_identifier_chars, '_');
|
||||
|
||||
// the above could generate conflicts because it has to generate a valid identifier
|
||||
// so stuff like `0` and `1` or `state%` and `state^` will result in the same string
|
||||
// so we have to de-conflict. We can only check `public_fields` because private state
|
||||
// can't have literal keys
|
||||
while (name && public_fields.has(name)) {
|
||||
name = '_' + name;
|
||||
}
|
||||
return name;
|
||||
} else {
|
||||
return node.name;
|
||||
}
|
||||
}
|
||||
|
||||
const class_analysis = {
|
||||
get_field,
|
||||
generate_body,
|
||||
register_assignment
|
||||
};
|
||||
|
||||
return class_analysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* `get_rune` is really annoying because it really guarantees this already
|
||||
* we just need this to tell the type system about it
|
||||
* @param {AssignmentExpression} node
|
||||
* @param {Scope} scope
|
||||
* @returns {{ stateful_assignment: StatefulAssignment, rune: StateCreationRuneName } | null}
|
||||
*/
|
||||
function parse_stateful_assignment(node, scope) {
|
||||
const rune = get_rune(node.right, scope);
|
||||
if (!rune || !is_state_creation_rune(rune)) {
|
||||
return null;
|
||||
}
|
||||
return { stateful_assignment: /** @type {StatefulAssignment} */ (node), rune };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PropertyDefinition | MethodDefinition} node
|
||||
* @param {Scope} scope
|
||||
* @returns {{ stateful_prop_def: StatefulPropertyDefinition, rune: StateCreationRuneName } | null}
|
||||
*/
|
||||
function prop_def_is_stateful(node, scope) {
|
||||
const rune = get_rune(node.value, scope);
|
||||
if (!rune || !is_state_creation_rune(rune)) {
|
||||
return null;
|
||||
}
|
||||
return { stateful_prop_def: /** @type {StatefulPropertyDefinition} */ (node), rune };
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
import type { AssignmentExpression, CallExpression, Identifier, MemberExpression, PropertyDefinition, MethodDefinition, PrivateIdentifier, ThisExpression } from 'estree';
|
||||
import type { StateField } from '../types';
|
||||
import type { Context as ServerContext } from '../server/types';
|
||||
import type { Context as ClientContext } from '../client/types';
|
||||
import type { StateCreationRuneName } from '../../../../utils';
|
||||
|
||||
export type StatefulAssignment = AssignmentExpression & {
|
||||
left: MemberExpression & {
|
||||
object: ThisExpression;
|
||||
property: Identifier | PrivateIdentifier
|
||||
};
|
||||
right: CallExpression;
|
||||
};
|
||||
|
||||
export type StatefulPropertyDefinition = PropertyDefinition & {
|
||||
key: Identifier | PrivateIdentifier;
|
||||
value: CallExpression;
|
||||
};
|
||||
|
||||
export type StateFieldBuilderParams<TContext extends ServerContext | ClientContext> = {
|
||||
is_private: boolean;
|
||||
field: StateField;
|
||||
node: StatefulAssignment | StatefulPropertyDefinition;
|
||||
context: TContext;
|
||||
};
|
||||
|
||||
export type StateFieldBuilder<TContext extends ServerContext | ClientContext> = (
|
||||
params: StateFieldBuilderParams<TContext>
|
||||
) => Array<PropertyDefinition | MethodDefinition>;
|
||||
|
||||
export type AssignmentBuilderParams<TContext extends ServerContext | ClientContext> = {
|
||||
node: StatefulAssignment;
|
||||
field: StateField;
|
||||
context: TContext;
|
||||
};
|
||||
|
||||
export type AssignmentBuilder<TContext extends ServerContext | ClientContext> = (params: AssignmentBuilderParams<TContext>) => void;
|
||||
|
||||
export type ClassAnalysis<TContext extends ServerContext | ClientContext> = {
|
||||
/**
|
||||
* @param name - The name of the field.
|
||||
* @param is_private - Whether the field is private (whether its name starts with '#').
|
||||
* @param kinds - What kinds of state creation runes you're looking for, eg. only '$derived.by'.
|
||||
* @returns The field if it exists and matches the given criteria, or null.
|
||||
*/
|
||||
get_field: (name: string, is_private: boolean, kinds?: Array<StateCreationRuneName>) => StateField | undefined;
|
||||
|
||||
/**
|
||||
* Given the body of a class, generate a new body with stateful fields.
|
||||
* This assumes that {@link register_assignment} is registered to be called
|
||||
* for all `AssignmentExpression` nodes in the class body.
|
||||
* @param context - The context associated with the `ClassBody`.
|
||||
* @returns The new body.
|
||||
*/
|
||||
generate_body: (context: TContext) => Array<PropertyDefinition | MethodDefinition>;
|
||||
|
||||
/**
|
||||
* Register an assignment expression. This checks to see if the assignment is creating
|
||||
* 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;
|
||||
}
|
||||
Loading…
Reference in new issue