svelte
Version:
Cybernetically enhanced web apps
298 lines (254 loc) • 9.11 kB
JavaScript
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState } from '../../types' */
import { walk } from 'zimmerframe';
import { object } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_is_valid_identifier } from '../../../../patterns.js';
import is_reference from 'is-reference';
import { locator } from '../../../../../state.js';
import { create_derived } from '../../utils.js';
/**
* @param {ComponentClientTransformState} state
* @param {Expression} value
*/
export function memoize_expression(state, value) {
const id = b.id(state.scope.generate('expression'));
state.init.push(b.const(id, create_derived(state, b.thunk(value))));
return b.call('$.get', id);
}
/**
*
* @param {ComponentClientTransformState} state
* @param {Expression} value
*/
export function get_expression_id(state, value) {
return b.id(`$${state.expressions.push(value) - 1}`);
}
/**
* @param {Array<AST.Text | AST.ExpressionTag>} values
* @param {(node: AST.SvelteNode, state: any) => any} visit
* @param {ComponentClientTransformState} state
* @param {(value: Expression, metadata: ExpressionMetadata) => Expression} memoize
* @returns {{ value: Expression, has_state: boolean }}
*/
export function build_template_chunk(
values,
visit,
state,
memoize = (value, metadata) => (metadata.has_call ? get_expression_id(state, value) : value)
) {
/** @type {Expression[]} */
const expressions = [];
let quasi = b.quasi('');
const quasis = [quasi];
let has_state = false;
for (let i = 0; i < values.length; i++) {
const node = values[i];
if (node.type === 'Text') {
quasi.value.cooked += node.data;
} else if (node.expression.type === 'Literal') {
if (node.expression.value != null) {
quasi.value.cooked += node.expression.value + '';
}
} else if (
node.expression.type !== 'Identifier' ||
node.expression.name !== 'undefined' ||
state.scope.get('undefined')
) {
let value = memoize(
/** @type {Expression} */ (visit(node.expression, state)),
node.metadata.expression
);
has_state ||= node.metadata.expression.has_state;
if (values.length === 1) {
// If we have a single expression, then pass that in directly to possibly avoid doing
// extra work in the template_effect (instead we do the work in set_text).
return { value, has_state };
}
if (
value.type === 'LogicalExpression' &&
value.right.type === 'Literal' &&
(value.operator === '??' || value.operator === '||')
) {
// `foo ?? null` -=> `foo ?? ''`
// otherwise leave the expression untouched
if (value.right.value === null) {
value = { ...value, right: b.literal('') };
}
}
const is_defined =
value.type === 'BinaryExpression' ||
(value.type === 'UnaryExpression' && value.operator !== 'void') ||
(value.type === 'LogicalExpression' && value.right.type === 'Literal') ||
(value.type === 'Identifier' && value.name === state.analysis.props_id?.name);
if (!is_defined) {
// add `?? ''` where necessary (TODO optimise more cases)
value = b.logical('??', value, b.literal(''));
}
expressions.push(value);
quasi = b.quasi('', i + 1 === values.length);
quasis.push(quasi);
}
}
for (const quasi of quasis) {
quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
}
const value =
expressions.length > 0
? b.template(quasis, expressions)
: b.literal(/** @type {string} */ (quasi.value.cooked));
return { value, has_state };
}
/**
* @param {ComponentClientTransformState} state
*/
export function build_render_statement(state) {
return b.stmt(
b.call(
'$.template_effect',
b.arrow(
state.expressions.map((_, i) => b.id(`$${i}`)),
state.update.length === 1 && state.update[0].type === 'ExpressionStatement'
? state.update[0].expression
: b.block(state.update)
),
state.expressions.length > 0 &&
b.array(state.expressions.map((expression) => b.thunk(expression))),
state.expressions.length > 0 && !state.analysis.runes && b.id('$.derived_safe_equal')
)
);
}
/**
* For unfortunate legacy reasons, directive names can look like this `use:a.b-c`
* This turns that string into a member expression
* @param {string} name
*/
export function parse_directive_name(name) {
// this allow for accessing members of an object
const parts = name.split('.');
let part = /** @type {string} */ (parts.shift());
/** @type {Identifier | MemberExpression} */
let expression = b.id(part);
while ((part = /** @type {string} */ (parts.shift()))) {
const computed = !regex_is_valid_identifier.test(part);
expression = b.member(expression, computed ? b.literal(part) : b.id(part), computed);
}
return expression;
}
/**
* @param {ComponentClientTransformState} state
* @param {string} id
* @param {Expression | undefined} init
* @param {Expression} value
* @param {ExpressionStatement} update
*/
export function build_update_assignment(state, id, init, value, update) {
state.init.push(b.var(id, init));
state.update.push(
b.if(b.binary('!==', b.id(id), b.assignment('=', b.id(id), value)), b.block([update]))
);
}
/**
* Serializes `bind:this` for components and elements.
* @param {Identifier | MemberExpression | SequenceExpression} expression
* @param {Expression} value
* @param {import('zimmerframe').Context<AST.SvelteNode, ComponentClientTransformState>} context
*/
export function build_bind_this(expression, value, { state, visit }) {
if (expression.type === 'SequenceExpression') {
const [get, set] = /** @type {SequenceExpression} */ (visit(expression)).expressions;
return b.call('$.bind_this', value, set, get);
}
/** @type {Identifier[]} */
const ids = [];
/** @type {Expression[]} */
const values = [];
/** @type {string[]} */
const seen = [];
const transform = { ...state.transform };
// Pass in each context variables to the get/set functions, so that we can null out old values on teardown.
// Note that we only do this for each context variables, the consequence is that the value might be stale in
// some scenarios where the value is a member expression with changing computed parts or using a combination of multiple
// variables, but that was the same case in Svelte 4, too. Once legacy mode is gone completely, we can revisit this.
walk(expression, null, {
Identifier(node, { path }) {
if (seen.includes(node.name)) return;
seen.push(node.name);
const parent = /** @type {Expression} */ (path.at(-1));
if (!is_reference(node, parent)) return;
const binding = state.scope.get(node.name);
if (!binding) return;
for (const [owner, scope] of state.scopes) {
if (owner.type === 'EachBlock' && scope === binding.scope) {
ids.push(node);
values.push(/** @type {Expression} */ (visit(node)));
if (transform[node.name]) {
transform[node.name] = {
...transform[node.name],
read: (node) => node
};
}
break;
}
}
}
});
const child_state = { ...state, transform };
const get = /** @type {Expression} */ (visit(expression, child_state));
const set = /** @type {Expression} */ (
visit(b.assignment('=', expression, b.id('$$value')), child_state)
);
// If we're mutating a property, then it might already be non-existent.
// If we make all the object nodes optional, then it avoids any runtime exceptions.
/** @type {Expression | Super} */
let node = get;
while (node.type === 'MemberExpression') {
node.optional = true;
node = node.object;
}
return b.call(
'$.bind_this',
value,
b.arrow([b.id('$$value'), ...ids], set),
b.arrow([...ids], get),
values.length > 0 && b.thunk(b.array(values))
);
}
/**
* @param {ComponentClientTransformState} state
* @param {AST.BindDirective} binding
* @param {MemberExpression} expression
*/
export function validate_binding(state, binding, expression) {
if (binding.expression.type === 'SequenceExpression') {
return;
}
// If we are referencing a $store.foo then we don't need to add validation
const left = object(binding.expression);
const left_binding = left && state.scope.get(left.name);
if (left_binding?.kind === 'store_sub') return;
const loc = locator(binding.start);
const obj = /** @type {Expression} */ (expression.object);
state.init.push(
b.stmt(
b.call(
'$.validate_binding',
b.literal(state.analysis.source.slice(binding.start, binding.end)),
b.thunk(
state.store_to_invalidate ? b.sequence([b.call('$.mark_store_binding'), obj]) : obj
),
b.thunk(
/** @type {Expression} */ (
expression.computed
? expression.property
: b.literal(/** @type {Identifier} */ (expression.property).name)
)
),
loc && b.literal(loc.line),
loc && b.literal(loc.column)
)
)
);
}