move code, improve generation

pull/12614/head
Rich Harris 2 years ago
parent a16e0c105d
commit 44076f736c

@ -1,11 +1,137 @@
/** @import { AssignmentExpression } from 'estree' */ /** @import { AssignmentExpression, BinaryOperator, Expression, Node, Pattern } from 'estree' */
/** @import { Context } from '../types.js' */ /** @import { SvelteNode } from '#compiler' */
import { serialize_set_binding } from './shared/utils.js'; /** @import { Context, ServerTransformState } from '../types.js' */
import { extract_paths } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { serialize_get_binding } from './shared/utils.js';
/** /**
* @param {AssignmentExpression} node * @param {AssignmentExpression} node
* @param {Context} context * @param {Context} context
*/ */
export function AssignmentExpression(node, context) { export function AssignmentExpression(node, context) {
return serialize_set_binding(node, context, context.next); const parent = /** @type {Node} */ (context.path.at(-1));
const is_standalone = parent.type.endsWith('Statement');
return serialize_assignment(node, context, is_standalone, context.next);
}
/**
* @param {AssignmentExpression} node
* @param {import('zimmerframe').Context<SvelteNode, ServerTransformState>} context
* @param {boolean} is_standalone
* @param {() => any} fallback
* @returns {Expression}
*/
function serialize_assignment(node, context, is_standalone, fallback) {
if (
node.left.type === 'ArrayPattern' ||
node.left.type === 'ObjectPattern' ||
node.left.type === 'RestElement'
) {
const value = /** @type {Expression} */ (context.visit(node.right));
const should_cache = value.type !== 'Identifier';
const rhs = should_cache ? b.id('$$value') : value;
/** @type {Expression[]} */
const assignments = [];
let should_transform = false;
for (const path of extract_paths(node.left)) {
const assignment = b.assignment('=', path.node, path.expression?.(rhs));
let changed = true;
assignments.push(
serialize_assignment(assignment, context, false, () => {
changed = false;
return assignment;
})
);
should_transform ||= changed;
}
if (!should_transform) {
// No change to output -> nothing to transform -> we can keep the original assignment
return fallback();
}
const sequence = b.sequence(assignments);
if (!is_standalone) {
// this is part of an expression, we need the sequence to end with the value
sequence.expressions.push(rhs);
}
if (should_cache) {
return b.call(b.arrow([rhs], sequence), value);
}
return sequence;
}
if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') {
throw new Error(`Unexpected assignment type ${node.left.type}`);
}
let left = node.left;
while (left.type === 'MemberExpression') {
// @ts-expect-error
left = left.object;
}
if (left.type !== 'Identifier' || !is_store_name(left.name)) {
return fallback();
}
const name = left.name.slice(1);
if (!context.state.scope.get(name)) {
// TODO error if it's a computed (or rest prop)? or does that already happen elsewhere?
return fallback();
}
if (left === node.left) {
return b.call('$.store_set', b.id(name), /** @type {Expression} */ (context.visit(node.right)));
}
return b.call(
'$.mutate_store',
b.assignment('??=', b.id('$$store_subs'), b.object([])),
b.literal(left.name),
b.id(name),
b.assignment(
node.operator,
/** @type {Pattern} */ (context.visit(node.left)),
get_assignment_value(node, context)
)
);
}
/**
* @param {AssignmentExpression} node
* @param {Pick<import('zimmerframe').Context<SvelteNode, ServerTransformState>, 'visit' | 'state'>} context
*/
function get_assignment_value(node, { state, visit }) {
if (node.left.type === 'Identifier') {
const operator = node.operator;
return operator === '='
? /** @type {Expression} */ (visit(node.right))
: // turn something like x += 1 into x = x + 1
b.binary(
/** @type {BinaryOperator} */ (operator.slice(0, -1)),
serialize_get_binding(node.left, state),
/** @type {Expression} */ (visit(node.right))
);
}
return /** @type {Expression} */ (visit(node.right));
}
/**
* @param {string} name
*/
function is_store_name(name) {
return name[0] === '$' && /[A-Za-z_]/.test(name[1]);
} }

@ -1,7 +1,7 @@
/** @import { AssignmentExpression, AssignmentOperator, BinaryOperator, Expression, Identifier, Node, Pattern, Statement, TemplateElement } from 'estree' */ /** @import { AssignmentOperator, Expression, Identifier, Node, Statement, TemplateElement } from 'estree' */
/** @import { Attribute, Comment, ExpressionTag, SvelteNode, Text } from '#compiler' */ /** @import { Attribute, Comment, ExpressionTag, SvelteNode, Text } from '#compiler' */
/** @import { ComponentContext, ServerTransformState } from '../../types.js' */ /** @import { ComponentContext, ServerTransformState } from '../../types.js' */
import { extract_paths } from '../../../../../utils/ast.js';
import { escape_html } from '../../../../../../escaping.js'; import { escape_html } from '../../../../../../escaping.js';
import { import {
BLOCK_CLOSE, BLOCK_CLOSE,
@ -232,116 +232,3 @@ export function serialize_get_binding(node, state) {
return node; return node;
} }
/**
* @param {AssignmentExpression} node
* @param {import('zimmerframe').Context<SvelteNode, ServerTransformState>} context
* @param {() => any} fallback
* @returns {Expression}
*/
export function serialize_set_binding(node, context, fallback) {
if (
node.left.type === 'ArrayPattern' ||
node.left.type === 'ObjectPattern' ||
node.left.type === 'RestElement'
) {
const value = /** @type {Expression} */ (context.visit(node.right));
const should_cache = value.type !== 'Identifier';
const rhs = should_cache ? b.id('$$value') : value;
/** @type {Expression[]} */
const assignments = [];
let should_transform = false;
for (const path of extract_paths(node.left)) {
const assignment = b.assignment('=', path.node, path.expression?.(rhs));
let changed = true;
assignments.push(
serialize_set_binding(assignment, context, () => {
changed = false;
return assignment;
})
);
should_transform ||= changed;
}
if (!should_transform) {
// No change to output -> nothing to transform -> we can keep the original assignment
return fallback();
}
if (should_cache) {
return b.call(b.arrow([rhs], b.sequence([...assignments, rhs])), value);
} else {
return b.call(b.thunk(b.sequence([...assignments, rhs])));
}
}
if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') {
throw new Error(`Unexpected assignment type ${node.left.type}`);
}
let left = node.left;
while (left.type === 'MemberExpression') {
// @ts-expect-error
left = left.object;
}
if (left.type !== 'Identifier' || !is_store_name(left.name)) {
return fallback();
}
const name = left.name.slice(1);
if (!context.state.scope.get(name)) {
// TODO error if it's a computed (or rest prop)? or does that already happen elsewhere?
return fallback();
}
if (left === node.left) {
return b.call('$.store_set', b.id(name), /** @type {Expression} */ (context.visit(node.right)));
}
return b.call(
'$.mutate_store',
b.assignment('??=', b.id('$$store_subs'), b.object([])),
b.literal(left.name),
b.id(name),
b.assignment(
node.operator,
/** @type {Pattern} */ (context.visit(node.left)),
get_assignment_value(node, context)
)
);
}
/**
* @param {AssignmentExpression} node
* @param {Pick<import('zimmerframe').Context<SvelteNode, ServerTransformState>, 'visit' | 'state'>} context
*/
function get_assignment_value(node, { state, visit }) {
if (node.left.type === 'Identifier') {
const operator = node.operator;
return operator === '='
? /** @type {Expression} */ (visit(node.right))
: // turn something like x += 1 into x = x + 1
b.binary(
/** @type {BinaryOperator} */ (operator.slice(0, -1)),
serialize_get_binding(node.left, state),
/** @type {Expression} */ (visit(node.right))
);
}
return /** @type {Expression} */ (visit(node.right));
}
/**
* @param {string} name
*/
function is_store_name(name) {
return name[0] === '$' && /[A-Za-z_]/.test(name[1]);
}

Loading…
Cancel
Save