mirror of https://github.com/sveltejs/svelte
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
635 lines
18 KiB
635 lines
18 KiB
import * as b from '../../../utils/builders.js';
|
|
import {
|
|
extract_paths,
|
|
is_expression_async,
|
|
is_simple_expression,
|
|
object
|
|
} from '../../../utils/ast.js';
|
|
import { error } from '../../../errors.js';
|
|
import {
|
|
PROPS_IS_LAZY_INITIAL,
|
|
PROPS_IS_IMMUTABLE,
|
|
PROPS_IS_RUNES,
|
|
PROPS_IS_UPDATED
|
|
} from '../../../../constants.js';
|
|
|
|
/**
|
|
* @template {import('./types').ClientTransformState} State
|
|
* @param {import('estree').AssignmentExpression} node
|
|
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, State>} context
|
|
* @returns
|
|
*/
|
|
export function get_assignment_value(node, { state, visit }) {
|
|
if (node.left.type === 'Identifier') {
|
|
const operator = node.operator;
|
|
return operator === '='
|
|
? /** @type {import('estree').Expression} */ (visit(node.right))
|
|
: // turn something like x += 1 into x = x + 1
|
|
b.binary(
|
|
/** @type {import('estree').BinaryOperator} */ (operator.slice(0, -1)),
|
|
serialize_get_binding(node.left, state),
|
|
/** @type {import('estree').Expression} */ (visit(node.right))
|
|
);
|
|
} else if (
|
|
node.left.type === 'MemberExpression' &&
|
|
node.left.object.type === 'ThisExpression' &&
|
|
node.left.property.type === 'PrivateIdentifier' &&
|
|
state.private_state.has(node.left.property.name)
|
|
) {
|
|
const operator = node.operator;
|
|
return operator === '='
|
|
? /** @type {import('estree').Expression} */ (visit(node.right))
|
|
: // turn something like x += 1 into x = x + 1
|
|
b.binary(
|
|
/** @type {import('estree').BinaryOperator} */ (operator.slice(0, -1)),
|
|
/** @type {import('estree').Expression} */ (visit(node.left)),
|
|
/** @type {import('estree').Expression} */ (visit(node.right))
|
|
);
|
|
} else {
|
|
return /** @type {import('estree').Expression} */ (visit(node.right));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {import('#compiler').Binding} binding
|
|
* @param {import('./types').ClientTransformState} state
|
|
* @returns {boolean}
|
|
*/
|
|
export function is_state_source(binding, state) {
|
|
return (
|
|
(binding.kind === 'state' || binding.kind === 'frozen_state') &&
|
|
(!state.analysis.immutable || binding.reassigned || state.analysis.accessors)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {import('estree').Identifier} node
|
|
* @param {import('./types').ClientTransformState} state
|
|
* @returns {import('estree').Expression}
|
|
*/
|
|
export function serialize_get_binding(node, state) {
|
|
const binding = state.scope.get(node.name);
|
|
|
|
if (binding === null || node === binding.node) {
|
|
// No associated binding or the declaration itself which shouldn't be transformed
|
|
return node;
|
|
}
|
|
|
|
if (binding.kind === 'store_sub') {
|
|
return b.call(node);
|
|
}
|
|
|
|
if (binding.expression) {
|
|
return typeof binding.expression === 'function' ? binding.expression(node) : binding.expression;
|
|
}
|
|
|
|
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') {
|
|
if (binding.node.name === '$$props') {
|
|
// Special case for $$props which only exists in the old world
|
|
// TODO this probably shouldn't have a 'prop' binding kind
|
|
return node;
|
|
}
|
|
|
|
if (
|
|
state.analysis.accessors ||
|
|
(state.analysis.immutable ? binding.reassigned : binding.mutated) ||
|
|
binding.initial
|
|
) {
|
|
return b.call(node);
|
|
}
|
|
|
|
if (binding.prop_alias) {
|
|
const key = b.key(binding.prop_alias);
|
|
return b.member(b.id('$$props'), key, key.type === 'Literal');
|
|
}
|
|
return b.member(b.id('$$props'), node);
|
|
}
|
|
|
|
if (binding.kind === 'legacy_reactive_import') {
|
|
return b.call('$$_import_' + node.name);
|
|
}
|
|
|
|
if (
|
|
is_state_source(binding, state) ||
|
|
binding.kind === 'derived' ||
|
|
binding.kind === 'legacy_reactive'
|
|
) {
|
|
return b.call('$.get', node);
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* @template {import('./types').ClientTransformState} State
|
|
* @param {import('estree').AssignmentExpression} node
|
|
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, State>} context
|
|
* @param {() => any} fallback
|
|
* @param {{skip_proxy_and_freeze?: boolean}} [options]
|
|
* @returns {import('estree').Expression}
|
|
*/
|
|
export function serialize_set_binding(node, context, fallback, options) {
|
|
const { state, visit } = context;
|
|
|
|
const assignee = node.left;
|
|
if (
|
|
assignee.type === 'ArrayPattern' ||
|
|
assignee.type === 'ObjectPattern' ||
|
|
assignee.type === 'RestElement'
|
|
) {
|
|
// Turn assignment into an IIFE, so that `$.set` calls etc don't produce invalid code
|
|
const tmp_id = context.state.scope.generate('tmp');
|
|
|
|
/** @type {import('estree').AssignmentExpression[]} */
|
|
const original_assignments = [];
|
|
|
|
/** @type {import('estree').Expression[]} */
|
|
const assignments = [];
|
|
|
|
const paths = extract_paths(assignee);
|
|
|
|
for (const path of paths) {
|
|
const value = path.expression?.(b.id(tmp_id));
|
|
const assignment = b.assignment('=', path.node, value);
|
|
original_assignments.push(assignment);
|
|
assignments.push(serialize_set_binding(assignment, context, () => assignment, options));
|
|
}
|
|
|
|
if (assignments.every((assignment, i) => assignment === original_assignments[i])) {
|
|
// No change to output -> nothing to transform -> we can keep the original assignment
|
|
return fallback();
|
|
}
|
|
|
|
const rhs_expression = /** @type {import('estree').Expression} */ (visit(node.right));
|
|
|
|
const iife_is_async =
|
|
is_expression_async(rhs_expression) ||
|
|
assignments.some((assignment) => is_expression_async(assignment));
|
|
|
|
const iife = b.arrow(
|
|
[],
|
|
b.block([
|
|
b.const(tmp_id, rhs_expression),
|
|
b.stmt(b.sequence(assignments)),
|
|
// return because it could be used in a nested expression where the value is needed.
|
|
// example: { foo: ({ bar } = { bar: 1 })}
|
|
b.return(b.id(tmp_id))
|
|
])
|
|
);
|
|
|
|
if (iife_is_async) {
|
|
return b.await(b.call(b.async(iife)));
|
|
} else {
|
|
return b.call(iife);
|
|
}
|
|
}
|
|
|
|
if (assignee.type !== 'Identifier' && assignee.type !== 'MemberExpression') {
|
|
error(node, 'INTERNAL', `Unexpected assignment type ${assignee.type}`);
|
|
}
|
|
|
|
// Handle class private/public state assignment cases
|
|
if (assignee.type === 'MemberExpression') {
|
|
if (
|
|
assignee.object.type === 'ThisExpression' &&
|
|
assignee.property.type === 'PrivateIdentifier'
|
|
) {
|
|
const private_state = context.state.private_state.get(assignee.property.name);
|
|
const value = get_assignment_value(node, context);
|
|
if (private_state !== undefined) {
|
|
if (state.in_constructor) {
|
|
// See if we should wrap value in $.proxy
|
|
if (
|
|
context.state.analysis.runes &&
|
|
!options?.skip_proxy_and_freeze &&
|
|
should_proxy_or_freeze(value, context.state.scope)
|
|
) {
|
|
const assignment = fallback();
|
|
if (assignment.type === 'AssignmentExpression') {
|
|
assignment.right =
|
|
private_state.kind === 'frozen_state'
|
|
? b.call('$.freeze', value)
|
|
: b.call('$.proxy', value);
|
|
return assignment;
|
|
}
|
|
}
|
|
} else {
|
|
return b.call(
|
|
'$.set',
|
|
assignee,
|
|
context.state.analysis.runes &&
|
|
!options?.skip_proxy_and_freeze &&
|
|
should_proxy_or_freeze(value, context.state.scope)
|
|
? private_state.kind === 'frozen_state'
|
|
? b.call('$.freeze', value)
|
|
: b.call('$.proxy', value)
|
|
: value
|
|
);
|
|
}
|
|
}
|
|
} else if (
|
|
assignee.object.type === 'ThisExpression' &&
|
|
assignee.property.type === 'Identifier' &&
|
|
state.in_constructor
|
|
) {
|
|
const public_state = context.state.public_state.get(assignee.property.name);
|
|
const value = get_assignment_value(node, context);
|
|
// See if we should wrap value in $.proxy
|
|
if (
|
|
context.state.analysis.runes &&
|
|
public_state !== undefined &&
|
|
!options?.skip_proxy_and_freeze &&
|
|
should_proxy_or_freeze(value, context.state.scope)
|
|
) {
|
|
const assignment = fallback();
|
|
if (assignment.type === 'AssignmentExpression') {
|
|
assignment.right =
|
|
public_state.kind === 'frozen_state'
|
|
? b.call('$.freeze', value)
|
|
: b.call('$.proxy', value);
|
|
return assignment;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const left = object(assignee);
|
|
|
|
if (left === null) {
|
|
return fallback();
|
|
}
|
|
|
|
const binding = state.scope.get(left.name);
|
|
|
|
if (!binding) return fallback();
|
|
|
|
if (binding.mutation !== null) {
|
|
return binding.mutation(node, context);
|
|
}
|
|
|
|
if (binding.kind === 'legacy_reactive_import') {
|
|
return b.call(
|
|
'$$_import_' + binding.node.name,
|
|
b.assignment(
|
|
node.operator,
|
|
/** @type {import('estree').Pattern} */ (visit(node.left)),
|
|
/** @type {import('estree').Expression} */ (visit(node.right))
|
|
)
|
|
);
|
|
}
|
|
|
|
const is_store = binding.kind === 'store_sub';
|
|
const left_name = is_store ? left.name.slice(1) : left.name;
|
|
|
|
if (
|
|
binding.kind !== 'state' &&
|
|
binding.kind !== 'frozen_state' &&
|
|
binding.kind !== 'prop' &&
|
|
binding.kind !== 'bindable_prop' &&
|
|
binding.kind !== 'each' &&
|
|
binding.kind !== 'legacy_reactive' &&
|
|
!is_store
|
|
) {
|
|
// TODO error if it's a computed (or rest prop)? or does that already happen elsewhere?
|
|
return fallback();
|
|
}
|
|
|
|
const value = get_assignment_value(node, context);
|
|
|
|
const serialize = () => {
|
|
if (left === node.left) {
|
|
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') {
|
|
return b.call(left, value);
|
|
} else if (is_store) {
|
|
return b.call('$.store_set', serialize_get_binding(b.id(left_name), state), value);
|
|
} else {
|
|
let call;
|
|
if (binding.kind === 'state') {
|
|
call = b.call(
|
|
'$.set',
|
|
b.id(left_name),
|
|
context.state.analysis.runes &&
|
|
!options?.skip_proxy_and_freeze &&
|
|
should_proxy_or_freeze(value, context.state.scope)
|
|
? b.call('$.proxy', value)
|
|
: value
|
|
);
|
|
} else if (binding.kind === 'frozen_state') {
|
|
call = b.call(
|
|
'$.set',
|
|
b.id(left_name),
|
|
context.state.analysis.runes &&
|
|
!options?.skip_proxy_and_freeze &&
|
|
should_proxy_or_freeze(value, context.state.scope)
|
|
? b.call('$.freeze', value)
|
|
: value
|
|
);
|
|
} else {
|
|
call = b.call('$.set', b.id(left_name), value);
|
|
}
|
|
|
|
if (state.scope.get(`$${left.name}`)?.kind === 'store_sub') {
|
|
return b.call('$.store_unsub', call, b.literal(`$${left.name}`), b.id('$$subscriptions'));
|
|
} else {
|
|
return call;
|
|
}
|
|
}
|
|
} else {
|
|
if (is_store) {
|
|
// If we are assigning to a store property, we need to ensure we don't
|
|
// capture the read for the store as part of the member expression to
|
|
// keep consistency with how store $ shorthand reads work in Svelte 4.
|
|
/**
|
|
*
|
|
* @param {import("estree").Expression | import("estree").Pattern} node
|
|
* @returns {import("estree").Expression}
|
|
*/
|
|
function visit_node(node) {
|
|
if (node.type === 'MemberExpression') {
|
|
return {
|
|
...node,
|
|
object: visit_node(/** @type {import("estree").Expression} */ (node.object)),
|
|
property: /** @type {import("estree").MemberExpression} */ (visit(node)).property
|
|
};
|
|
}
|
|
if (node.type === 'Identifier') {
|
|
const binding = state.scope.get(node.name);
|
|
|
|
if (binding !== null && binding.kind === 'store_sub') {
|
|
return b.call(
|
|
'$.untrack',
|
|
b.thunk(/** @type {import('estree').Expression} */ (visit(node)))
|
|
);
|
|
}
|
|
}
|
|
return /** @type {import("estree").Expression} */ (visit(node));
|
|
}
|
|
|
|
return b.call(
|
|
'$.mutate_store',
|
|
serialize_get_binding(b.id(left_name), state),
|
|
b.assignment(
|
|
node.operator,
|
|
/** @type {import("estree").Pattern}} */ (visit_node(node.left)),
|
|
value
|
|
),
|
|
b.call('$.untrack', b.id('$' + left_name))
|
|
);
|
|
} else if (!state.analysis.runes) {
|
|
if (binding.kind === 'bindable_prop') {
|
|
return b.call(
|
|
left,
|
|
b.sequence([
|
|
b.assignment(
|
|
node.operator,
|
|
/** @type {import('estree').Pattern} */ (visit(node.left)),
|
|
value
|
|
),
|
|
b.call(left)
|
|
])
|
|
);
|
|
} else {
|
|
return b.call(
|
|
'$.mutate',
|
|
b.id(left_name),
|
|
b.assignment(
|
|
node.operator,
|
|
/** @type {import('estree').Pattern} */ (visit(node.left)),
|
|
value
|
|
)
|
|
);
|
|
}
|
|
} else {
|
|
return b.assignment(
|
|
node.operator,
|
|
/** @type {import('estree').Pattern} */ (visit(node.left)),
|
|
/** @type {import('estree').Expression} */ (visit(node.right))
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (value.type === 'BinaryExpression' && /** @type {any} */ (value.operator) === '??') {
|
|
return b.logical('??', serialize_get_binding(b.id(left_name), state), serialize());
|
|
}
|
|
|
|
return serialize();
|
|
}
|
|
|
|
/**
|
|
* @param {import('estree').ArrowFunctionExpression | import('estree').FunctionExpression} node
|
|
* @param {import('./types').ComponentContext} context
|
|
*/
|
|
export const function_visitor = (node, context) => {
|
|
const metadata = node.metadata;
|
|
|
|
let state = context.state;
|
|
|
|
if (node.type === 'FunctionExpression') {
|
|
const parent = /** @type {import('estree').Node} */ (context.path.at(-1));
|
|
const in_constructor = parent.type === 'MethodDefinition' && parent.kind === 'constructor';
|
|
|
|
state = { ...context.state, in_constructor };
|
|
} else {
|
|
state = { ...context.state, in_constructor: false };
|
|
}
|
|
|
|
if (metadata?.hoistable === true) {
|
|
const params = serialize_hoistable_params(node, context);
|
|
|
|
return /** @type {import('estree').FunctionExpression} */ ({
|
|
...node,
|
|
params,
|
|
body: context.visit(node.body, state)
|
|
});
|
|
}
|
|
|
|
context.next(state);
|
|
};
|
|
|
|
/**
|
|
* @param {import('estree').FunctionDeclaration | import('estree').FunctionExpression | import('estree').ArrowFunctionExpression} node
|
|
* @param {import('./types').ComponentContext} context
|
|
* @returns {import('estree').Pattern[]}
|
|
*/
|
|
function get_hoistable_params(node, context) {
|
|
const scope = context.state.scope;
|
|
|
|
/** @type {import('estree').Pattern[]} */
|
|
const params = [];
|
|
let added_props = false;
|
|
|
|
for (const [reference] of scope.references) {
|
|
const binding = scope.get(reference);
|
|
|
|
if (binding !== null && !scope.declarations.has(reference) && binding.initial !== node) {
|
|
if (binding.kind === 'store_sub') {
|
|
// We need both the subscription for getting the value and the store for updating
|
|
params.push(b.id(binding.node.name.slice(1)));
|
|
params.push(b.id(binding.node.name));
|
|
} else if (
|
|
// If it's a destructured derived binding, then we can extract the derived signal reference and use that.
|
|
binding.expression !== null &&
|
|
typeof binding.expression !== 'function' &&
|
|
binding.expression.type === 'MemberExpression' &&
|
|
binding.expression.object.type === 'CallExpression' &&
|
|
binding.expression.object.callee.type === 'Identifier' &&
|
|
binding.expression.object.callee.name === '$.get' &&
|
|
binding.expression.object.arguments[0].type === 'Identifier'
|
|
) {
|
|
params.push(b.id(binding.expression.object.arguments[0].name));
|
|
} else if (
|
|
// If we are referencing a simple $$props value, then we need to reference the object property instead
|
|
(binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
|
|
!binding.reassigned &&
|
|
binding.initial === null &&
|
|
!context.state.analysis.accessors
|
|
) {
|
|
// Handle $$props.something use-cases
|
|
if (!added_props) {
|
|
added_props = true;
|
|
params.push(b.id('$$props'));
|
|
}
|
|
} else {
|
|
// create a copy to remove start/end tags which would mess up source maps
|
|
params.push(b.id(binding.node.name));
|
|
}
|
|
}
|
|
}
|
|
return params;
|
|
}
|
|
|
|
/**
|
|
* @param {import('estree').FunctionDeclaration | import('estree').FunctionExpression | import('estree').ArrowFunctionExpression} node
|
|
* @param {import('./types').ComponentContext} context
|
|
* @returns {import('estree').Pattern[]}
|
|
*/
|
|
export function serialize_hoistable_params(node, context) {
|
|
const hoistable_params = get_hoistable_params(node, context);
|
|
node.metadata.hoistable_params = hoistable_params;
|
|
|
|
/** @type {import('estree').Pattern[]} */
|
|
const params = [];
|
|
|
|
if (node.params.length === 0) {
|
|
if (hoistable_params.length > 0) {
|
|
// For the event object
|
|
params.push(b.id('_'));
|
|
}
|
|
} else {
|
|
for (const param of node.params) {
|
|
params.push(/** @type {import('estree').Pattern} */ (context.visit(param)));
|
|
}
|
|
}
|
|
|
|
params.push(...hoistable_params);
|
|
return params;
|
|
}
|
|
|
|
/**
|
|
* @param {import('#compiler').Binding} binding
|
|
* @param {import('./types').ComponentClientTransformState} state
|
|
* @param {string} name
|
|
* @param {import('estree').Expression | null} [initial]
|
|
* @returns
|
|
*/
|
|
export function get_prop_source(binding, state, name, initial) {
|
|
/** @type {import('estree').Expression[]} */
|
|
const args = [b.id('$$props'), b.literal(name)];
|
|
|
|
let flags = 0;
|
|
|
|
if (state.analysis.immutable) {
|
|
flags |= PROPS_IS_IMMUTABLE;
|
|
}
|
|
|
|
if (state.analysis.runes) {
|
|
flags |= PROPS_IS_RUNES;
|
|
}
|
|
|
|
if (
|
|
state.analysis.accessors ||
|
|
(state.analysis.immutable ? binding.reassigned : binding.mutated)
|
|
) {
|
|
flags |= PROPS_IS_UPDATED;
|
|
}
|
|
|
|
/** @type {import('estree').Expression | undefined} */
|
|
let arg;
|
|
|
|
if (initial) {
|
|
// To avoid eagerly evaluating the right-hand-side, we wrap it in a thunk if necessary
|
|
if (is_simple_expression(initial)) {
|
|
arg = initial;
|
|
} else {
|
|
if (
|
|
initial.type === 'CallExpression' &&
|
|
initial.callee.type === 'Identifier' &&
|
|
initial.arguments.length === 0
|
|
) {
|
|
arg = initial.callee;
|
|
} else {
|
|
arg = b.thunk(initial);
|
|
}
|
|
|
|
flags |= PROPS_IS_LAZY_INITIAL;
|
|
}
|
|
}
|
|
|
|
if (flags || arg) {
|
|
args.push(b.literal(flags));
|
|
if (arg) args.push(arg);
|
|
}
|
|
|
|
return b.call('$.prop', ...args);
|
|
}
|
|
|
|
/**
|
|
* @param {import('estree').Expression} node
|
|
* @param {import("../../scope.js").Scope | null} scope
|
|
*/
|
|
export function should_proxy_or_freeze(node, scope) {
|
|
if (
|
|
!node ||
|
|
node.type === 'Literal' ||
|
|
node.type === 'TemplateLiteral' ||
|
|
node.type === 'ArrowFunctionExpression' ||
|
|
node.type === 'FunctionExpression' ||
|
|
node.type === 'UnaryExpression' ||
|
|
node.type === 'BinaryExpression' ||
|
|
(node.type === 'Identifier' && node.name === 'undefined')
|
|
) {
|
|
return false;
|
|
}
|
|
if (node.type === 'Identifier' && scope !== null) {
|
|
const binding = scope.get(node.name);
|
|
// Let's see if the reference is something that can be proxied or frozen
|
|
if (
|
|
binding !== null &&
|
|
!binding.reassigned &&
|
|
binding.initial !== null &&
|
|
binding.initial.type !== 'FunctionDeclaration' &&
|
|
binding.initial.type !== 'ClassDeclaration' &&
|
|
binding.initial.type !== 'ImportDeclaration' &&
|
|
binding.initial.type !== 'EachBlock'
|
|
) {
|
|
return should_proxy_or_freeze(binding.initial, null);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Port over the location information from the source to the target identifier.
|
|
* but keep the target as-is (i.e. a new id is created).
|
|
* This ensures esrap can generate accurate source maps.
|
|
* @param {import('estree').Identifier} target
|
|
* @param {import('estree').Identifier} source
|
|
*/
|
|
export function with_loc(target, source) {
|
|
if (source.loc) {
|
|
return { ...target, loc: source.loc };
|
|
}
|
|
return target;
|
|
}
|