mirror of https://github.com/sveltejs/svelte
parent
737bfb5740
commit
c536ec6f2b
@ -1,95 +0,0 @@
|
|||||||
/** @import { Context } from '../../types.js' */
|
|
||||||
/** @import { MethodDefinition, PropertyDefinition, Expression, StaticBlock, SpreadElement } from 'estree' */
|
|
||||||
/** @import { StateCreationRuneName } from '../../../../../../utils.js' */
|
|
||||||
/** @import { AssignmentBuilder, ClassTransformer, StateFieldBuilder } from '../../../shared/types.js' */
|
|
||||||
import * as b from '#compiler/builders';
|
|
||||||
import { create_class_transformer } from '../../../shared/class_transformer.js';
|
|
||||||
import { should_proxy } from '../../utils.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Array<MethodDefinition | PropertyDefinition | StaticBlock>} body
|
|
||||||
* @returns {ClassTransformer<Context>}
|
|
||||||
*/
|
|
||||||
export function create_client_class_transformer(body) {
|
|
||||||
/** @type {StateFieldBuilder<Context>} */
|
|
||||||
function build_state_field({ is_private, field, node, context }) {
|
|
||||||
let original_id = node.type === 'AssignmentExpression' ? node.left.property : 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 }) {
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
left: {
|
|
||||||
...node.left,
|
|
||||||
// ...swap out the assignment to go directly against the private field
|
|
||||||
property: field.id,
|
|
||||||
// this could be a transformation from `this.[1]` to `this.#_` (the private field we generated)
|
|
||||||
// -- private fields are never computed
|
|
||||||
computed: false
|
|
||||||
},
|
|
||||||
// ...and swap out the assignment's value for the state field init
|
|
||||||
right: build_init_value(field.kind, node.right.arguments[0], context)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return create_class_transformer(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 = arg
|
|
||||||
? /** @type {Expression} **/ (context.visit(arg, { ...context.state, in_constructor: false }))
|
|
||||||
: b.void0;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,103 +0,0 @@
|
|||||||
/** @import { Expression, MethodDefinition, StaticBlock, PropertyDefinition, SpreadElement } from 'estree' */
|
|
||||||
/** @import { Context } from '../../types.js' */
|
|
||||||
/** @import { AssignmentBuilder, StateFieldBuilder } from '../../../shared/types.js' */
|
|
||||||
/** @import { ClassTransformer } from '../../../shared/types.js' */
|
|
||||||
/** @import { StateCreationRuneName } from '../../../../../../utils.js' */
|
|
||||||
|
|
||||||
import * as b from '#compiler/builders';
|
|
||||||
import { dev } from '../../../../../state.js';
|
|
||||||
import { create_class_transformer } from '../../../shared/class_transformer.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Array<MethodDefinition | PropertyDefinition | StaticBlock>} body
|
|
||||||
* @returns {ClassTransformer<Context>}
|
|
||||||
*/
|
|
||||||
export function create_server_class_transformer(body) {
|
|
||||||
/** @type {StateFieldBuilder<Context>} */
|
|
||||||
function build_state_field({ is_private, field, node, context }) {
|
|
||||||
let original_id = node.type === 'AssignmentExpression' ? node.left.property : 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))];
|
|
||||||
} else {
|
|
||||||
const init = /** @type {Expression} **/ (context.visit(node.value.arguments[0]));
|
|
||||||
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);
|
|
||||||
|
|
||||||
/** @type {Array<MethodDefinition | PropertyDefinition>} */
|
|
||||||
const defs = [
|
|
||||||
// #foo;
|
|
||||||
b.prop_def(field.id, value)
|
|
||||||
];
|
|
||||||
|
|
||||||
// get foo() { return this.#foo; }
|
|
||||||
if (field.kind === '$state' || field.kind === '$state.raw') {
|
|
||||||
defs.push(b.method('get', original_id, [], [b.return(member)]));
|
|
||||||
} else {
|
|
||||||
defs.push(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 }) {
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
left: {
|
|
||||||
...node.left,
|
|
||||||
// ...swap out the assignment to go directly against the private field
|
|
||||||
property: field.id,
|
|
||||||
computed: false
|
|
||||||
},
|
|
||||||
// ...and swap out the assignment's value for the state field init
|
|
||||||
right: build_init_value(field.kind, node.right.arguments[0], context)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return create_class_transformer(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 = arg ? /** @type {Expression} **/ (context.visit(arg)) : b.void0;
|
|
||||||
|
|
||||||
switch (kind) {
|
|
||||||
case '$state':
|
|
||||||
case '$state.raw':
|
|
||||||
return init;
|
|
||||||
case '$derived':
|
|
||||||
return b.call('$.once', b.thunk(init));
|
|
||||||
case '$derived.by':
|
|
||||||
return b.call('$.once', init);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,392 +0,0 @@
|
|||||||
/** @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, ClassTransformer, 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 {ClassTransformer<TContext>}
|
|
||||||
*/
|
|
||||||
export function create_class_transformer(body, build_state_field, build_assignment) {
|
|
||||||
/**
|
|
||||||
* Public, stateful fields.
|
|
||||||
* @type {Map<string, StateField>}
|
|
||||||
*/
|
|
||||||
const public_fields = new Map();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Private, stateful fields. These are namespaced separately because
|
|
||||||
* public and private fields can have the same name in the AST -- ex.
|
|
||||||
* `count` and `#count` are both named `count` -- and because it's useful
|
|
||||||
* in a couple of cases to be able to check for only one or the other.
|
|
||||||
* @type {Map<string, StateField>}
|
|
||||||
*/
|
|
||||||
const private_fields = new Map();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Accumulates nodes for the new class body.
|
|
||||||
* @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 = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a state field by name.
|
|
||||||
*
|
|
||||||
* @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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a child context that makes sense for passing to the child analyzers.
|
|
||||||
* @param {TContext} context
|
|
||||||
* @returns {TContext}
|
|
||||||
*/
|
|
||||||
function create_child_context(context) {
|
|
||||||
const state = {
|
|
||||||
...context.state,
|
|
||||||
class_transformer
|
|
||||||
};
|
|
||||||
// @ts-expect-error - I can't find a way to make TypeScript happy with these
|
|
||||||
const visit = (node, state_override) => context.visit(node, { ...state, ...state_override });
|
|
||||||
// @ts-expect-error - I can't find a way to make TypeScript happy with these
|
|
||||||
const next = (state_override) => context.next({ ...state, ...state_override });
|
|
||||||
return {
|
|
||||||
...context,
|
|
||||||
state,
|
|
||||||
visit,
|
|
||||||
next
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a new body for the class. Ensure there is a visitor for AssignmentExpression that
|
|
||||||
* calls `generate_assignment` to capture any stateful fields declared in the constructor.
|
|
||||||
* @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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given an assignment expression, check to see if that assignment expression declares
|
|
||||||
* a stateful field. If it does, register that field and then return the processed
|
|
||||||
* assignment expression. If an assignment expression is returned from this function,
|
|
||||||
* it should be considered _fully processed_ and should replace the existing assignment
|
|
||||||
* expression node.
|
|
||||||
* @param {AssignmentExpression} node
|
|
||||||
* @param {TContext} context
|
|
||||||
* @returns {AssignmentExpression | null} The node, if `register_assignment` handled its transformation.
|
|
||||||
*/
|
|
||||||
function generate_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' ||
|
|
||||||
node.left.property.type === 'PrivateIdentifier' ||
|
|
||||||
node.left.property.type === 'Literal')
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = get_name(node.left.property);
|
|
||||||
if (!name) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = parse_stateful_assignment(node, child_context.state.scope);
|
|
||||||
if (!parsed) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const { stateful_assignment, rune } = parsed;
|
|
||||||
|
|
||||||
const is_private = stateful_assignment.left.property.type === 'PrivateIdentifier';
|
|
||||||
|
|
||||||
let field;
|
|
||||||
if (is_private) {
|
|
||||||
field = {
|
|
||||||
kind: rune,
|
|
||||||
id: /** @type {PrivateIdentifier} */ (stateful_assignment.left.property)
|
|
||||||
};
|
|
||||||
private_fields.set(name, field);
|
|
||||||
} else {
|
|
||||||
field = {
|
|
||||||
kind: rune,
|
|
||||||
// it's safe to do this upfront now because we're guaranteed to already know about all private
|
|
||||||
// identifiers (they had to have been declared at the class root, before we visited the constructor)
|
|
||||||
id: deconflict(name)
|
|
||||||
};
|
|
||||||
public_fields.set(name, field);
|
|
||||||
}
|
|
||||||
|
|
||||||
const replacer = () => {
|
|
||||||
const nodes = build_state_field({
|
|
||||||
is_private,
|
|
||||||
field,
|
|
||||||
node: stateful_assignment,
|
|
||||||
context: child_context
|
|
||||||
});
|
|
||||||
if (!nodes) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
new_body.push(...nodes);
|
|
||||||
};
|
|
||||||
replacers.push(replacer);
|
|
||||||
|
|
||||||
return build_assignment({
|
|
||||||
field,
|
|
||||||
node: stateful_assignment,
|
|
||||||
context: child_context
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a class body definition.
|
|
||||||
*
|
|
||||||
* @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) {
|
|
||||||
// this isn't a stateful field definition, but if could become one in the constructor -- so we register
|
|
||||||
// it, but conditionally -- so that if it's added as a field in the constructor (which causes us to create)
|
|
||||||
// a field definition for it), we don't end up with a duplicate definition (this one, plus the one we create)
|
|
||||||
replacers.push(() => {
|
|
||||||
if (!get_field(name, is_private)) {
|
|
||||||
new_body.push(
|
|
||||||
/** @type {PropertyDefinition | MethodDefinition} */ (
|
|
||||||
// @ts-expect-error generics silliness
|
|
||||||
child_context.visit(node, child_context.state)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
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_transformer = {
|
|
||||||
get_field,
|
|
||||||
generate_body,
|
|
||||||
generate_assignment
|
|
||||||
};
|
|
||||||
|
|
||||||
return class_transformer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* `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 };
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
import type {
|
|
||||||
AssignmentExpression,
|
|
||||||
CallExpression,
|
|
||||||
Identifier,
|
|
||||||
MemberExpression,
|
|
||||||
PropertyDefinition,
|
|
||||||
MethodDefinition,
|
|
||||||
PrivateIdentifier,
|
|
||||||
ThisExpression,
|
|
||||||
Literal
|
|
||||||
} 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 | Literal;
|
|
||||||
};
|
|
||||||
right: CallExpression;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type StatefulPropertyDefinition = PropertyDefinition & {
|
|
||||||
key: Identifier | PrivateIdentifier | Literal;
|
|
||||||
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>
|
|
||||||
) => AssignmentExpression;
|
|
||||||
|
|
||||||
export type ClassTransformer<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.
|
|
||||||
*/
|
|
||||||
generate_assignment: (
|
|
||||||
node: AssignmentExpression,
|
|
||||||
context: TContext
|
|
||||||
) => AssignmentExpression | null;
|
|
||||||
};
|
|
Loading…
Reference in new issue