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