svelte
Version:
Cybernetically enhanced web apps
362 lines (302 loc) • 10.9 kB
JavaScript
/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
/** @import { ComponentContext } from '../types' */
/** @import { Scope } from '../../../scope' */
import {
EACH_INDEX_REACTIVE,
EACH_IS_ANIMATED,
EACH_IS_CONTROLLED,
EACH_ITEM_IMMUTABLE,
EACH_ITEM_REACTIVE
} from '../../../../../constants.js';
import { dev } from '../../../../state.js';
import { extract_paths, object } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { build_getter } from '../utils.js';
import { get_value } from './shared/declarations.js';
/**
* @param {AST.EachBlock} node
* @param {ComponentContext} context
*/
export function EachBlock(node, context) {
const each_node_meta = node.metadata;
// expression should be evaluated in the parent scope, not the scope
// created by the each block itself
const collection = /** @type {Expression} */ (
context.visit(node.expression, {
...context.state,
scope: /** @type {Scope} */ (context.state.scope.parent)
})
);
if (!each_node_meta.is_controlled) {
context.state.template.push('<!>');
}
let flags = 0;
if (node.metadata.keyed && node.index) {
flags |= EACH_INDEX_REACTIVE;
}
const key_is_item =
node.key?.type === 'Identifier' &&
node.context?.type === 'Identifier' &&
node.context?.name === node.key.name;
// if the each block expression references a store subscription, we need
// to use mutable stores internally
let uses_store;
for (const binding of node.metadata.expression.dependencies) {
if (binding.kind === 'store_sub') {
uses_store = true;
break;
}
}
for (const binding of node.metadata.expression.dependencies) {
// if the expression doesn't reference any external state, we don't need to
// create a source for the item. TODO cover more cases (e.g. `x.filter(y)`
// should also qualify if `y` doesn't reference state, and non-state
// bindings should also be fine
if (binding.scope.function_depth >= context.state.scope.function_depth) {
continue;
}
if (!context.state.analysis.runes || !key_is_item || uses_store) {
flags |= EACH_ITEM_REACTIVE;
break;
}
}
if (context.state.analysis.runes && !uses_store) {
flags |= EACH_ITEM_IMMUTABLE;
}
// Since `animate:` can only appear on elements that are the sole child of a keyed each block,
// we can determine at compile time whether the each block is animated or not (in which
// case it should measure animated elements before and after reconciliation).
if (
node.key &&
node.body.nodes.some((child) => {
if (child.type !== 'RegularElement' && child.type !== 'SvelteElement') return false;
return child.attributes.some((attr) => attr.type === 'AnimateDirective');
})
) {
flags |= EACH_IS_ANIMATED;
}
if (each_node_meta.is_controlled) {
flags |= EACH_IS_CONTROLLED;
}
// If the array is a store expression, we need to invalidate it when the array is changed.
// This doesn't catch all cases, but all the ones that Svelte 4 catches, too.
let store_to_invalidate = '';
if (node.expression.type === 'Identifier' || node.expression.type === 'MemberExpression') {
const id = object(node.expression);
if (id) {
const binding = context.state.scope.get(id.name);
if (binding?.kind === 'store_sub') {
store_to_invalidate = id.name;
}
}
}
// Legacy mode: find the parent each blocks which contain the arrays to invalidate
const indirect_dependencies = collect_parent_each_blocks(context).flatMap((block) => {
const array = /** @type {Expression} */ (context.visit(block.expression));
const transitive_dependencies = build_transitive_dependencies(
block.metadata.expression.dependencies,
context
);
return [array, ...transitive_dependencies];
});
/** @type {Identifier | null} */
let collection_id = null;
// Check if inner scope shadows something from outer scope.
// This is necessary because we need access to the array expression of the each block
// in the inner scope if bindings are used, in order to invalidate the array.
for (const [name] of context.state.scope.declarations) {
if (context.state.scope.parent?.get(name) != null) {
collection_id = context.state.scope.root.unique('$$array');
break;
}
}
if (collection_id) {
indirect_dependencies.push(b.call(collection_id));
} else {
indirect_dependencies.push(collection);
const transitive_dependencies = build_transitive_dependencies(
each_node_meta.expression.dependencies,
context
);
indirect_dependencies.push(...transitive_dependencies);
}
const child_state = {
...context.state,
transform: { ...context.state.transform },
store_to_invalidate
};
/** The state used when generating the key function, if necessary */
const key_state = {
...context.state,
transform: { ...context.state.transform }
};
// We need to generate a unique identifier in case there's a bind:group below
// which needs a reference to the index
const index =
each_node_meta.contains_group_binding || !node.index ? each_node_meta.index : b.id(node.index);
const item = node.context?.type === 'Identifier' ? node.context : b.id('$$item');
let uses_index = each_node_meta.contains_group_binding;
let key_uses_index = false;
if (node.index) {
child_state.transform[node.index] = {
read: (node) => {
uses_index = true;
return (flags & EACH_INDEX_REACTIVE) !== 0 ? get_value(node) : node;
}
};
key_state.transform[node.index] = {
read: (node) => {
key_uses_index = true;
return node;
}
};
}
/** @type {Statement[]} */
const declarations = [];
const invalidate = b.call(
'$.invalidate_inner_signals',
b.thunk(b.sequence(indirect_dependencies))
);
const invalidate_store = store_to_invalidate
? b.call('$.invalidate_store', b.id('$$stores'), b.literal(store_to_invalidate))
: undefined;
/** @type {Expression[]} */
const sequence = [];
if (!context.state.analysis.runes) sequence.push(invalidate);
if (invalidate_store) sequence.push(invalidate_store);
if (node.context?.type === 'Identifier') {
const binding = /** @type {Binding} */ (context.state.scope.get(node.context.name));
child_state.transform[node.context.name] = {
read: (node) => {
if (binding.reassigned) {
// we need to do `array[$$index]` instead of `$$item` or whatever
// TODO 6.0 this only applies in legacy mode, reassignments are
// forbidden in runes mode
return b.member(
collection_id ? b.call(collection_id) : collection,
(flags & EACH_INDEX_REACTIVE) !== 0 ? get_value(index) : index,
true
);
}
return (flags & EACH_ITEM_REACTIVE) !== 0 ? get_value(node) : node;
},
assign: (_, value) => {
uses_index = true;
const left = b.member(
collection_id ? b.call(collection_id) : collection,
(flags & EACH_INDEX_REACTIVE) !== 0 ? get_value(index) : index,
true
);
return b.sequence([b.assignment('=', left, value), ...sequence]);
},
mutate: (_, mutation) => {
uses_index = true;
return b.sequence([mutation, ...sequence]);
}
};
delete key_state.transform[node.context.name];
} else if (node.context) {
const unwrapped = (flags & EACH_ITEM_REACTIVE) !== 0 ? b.call('$.get', item) : item;
for (const path of extract_paths(node.context)) {
const name = /** @type {Identifier} */ (path.node).name;
const needs_derived = path.has_default_value; // to ensure that default value is only called once
const fn = b.thunk(
/** @type {Expression} */ (context.visit(path.expression?.(unwrapped), child_state))
);
declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn));
const read = needs_derived ? get_value : b.call;
child_state.transform[name] = {
read,
assign: (_, value) => {
const left = /** @type {Pattern} */ (path.update_expression(unwrapped));
return b.sequence([b.assignment('=', left, value), ...sequence]);
},
mutate: (_, mutation) => {
return b.sequence([mutation, ...sequence]);
}
};
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (dev) {
declarations.push(b.stmt(read(b.id(name))));
}
delete key_state.transform[name];
}
}
const block = /** @type {BlockStatement} */ (context.visit(node.body, child_state));
/** @type {Expression} */
let key_function = b.id('$.index');
if (node.metadata.keyed) {
const pattern = /** @type {Pattern} */ (node.context); // can only be keyed when a context is provided
const expression = /** @type {Expression} */ (
context.visit(/** @type {Expression} */ (node.key), key_state)
);
key_function = b.arrow(key_uses_index ? [pattern, index] : [pattern], expression);
}
if (node.index && each_node_meta.contains_group_binding) {
// We needed to create a unique identifier for the index above, but we want to use the
// original index name in the template, therefore create another binding
declarations.push(b.let(node.index, index));
}
if (dev && node.metadata.keyed) {
context.state.init.push(
b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function))
);
}
const render_args = [b.id('$$anchor'), item];
if (uses_index || collection_id) render_args.push(index);
if (collection_id) render_args.push(collection_id);
/** @type {Expression[]} */
const args = [
context.state.node,
b.literal(flags),
b.thunk(collection),
key_function,
b.arrow(render_args, b.block(declarations.concat(block.body)))
];
if (node.fallback) {
args.push(
b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fallback)))
);
}
context.state.init.push(b.stmt(b.call('$.each', ...args)));
}
/**
* @param {ComponentContext} context
*/
function collect_parent_each_blocks(context) {
return /** @type {AST.EachBlock[]} */ (context.path.filter((node) => node.type === 'EachBlock'));
}
/**
* @param {Set<Binding>} references
* @param {ComponentContext} context
*/
function build_transitive_dependencies(references, context) {
/** @type {Set<Binding>} */
const dependencies = new Set();
for (const ref of references) {
const deps = collect_transitive_dependencies(ref);
for (const dep of deps) {
dependencies.add(dep);
}
}
return [...dependencies].map((dep) => build_getter({ ...dep.node }, context.state));
}
/**
* @param {Binding} binding
* @param {Set<Binding>} seen
* @returns {Binding[]}
*/
function collect_transitive_dependencies(binding, seen = new Set()) {
if (binding.kind !== 'legacy_reactive') return [];
for (const dep of binding.legacy_dependencies) {
if (!seen.has(dep)) {
seen.add(dep);
for (const transitive_dep of collect_transitive_dependencies(dep, seen)) {
seen.add(transitive_dep);
}
}
}
return [...seen];
}