svelte
Version:
Cybernetically enhanced web apps
240 lines (204 loc) • 7.26 kB
JavaScript
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */
/** @import { AST, DelegatedEvent } from '#compiler' */
/** @import { Context } from '../types' */
import { cannot_be_set_statically, is_capture_event, is_delegated } from '../../../../utils.js';
import {
get_attribute_chunks,
get_attribute_expression,
is_event_attribute
} from '../../../utils/ast.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.Attribute} node
* @param {Context} context
*/
export function Attribute(node, context) {
context.next();
const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1));
if (parent.type === 'RegularElement') {
// special case <option value="" />
if (node.name === 'value' && parent.name === 'option') {
mark_subtree_dynamic(context.path);
}
}
if (is_event_attribute(node)) {
mark_subtree_dynamic(context.path);
}
if (cannot_be_set_statically(node.name)) {
mark_subtree_dynamic(context.path);
}
// class={[...]} or class={{...}} or `class={x}` need clsx to resolve the classes
if (
node.name === 'class' &&
!Array.isArray(node.value) &&
node.value !== true &&
node.value.expression.type !== 'Literal' &&
node.value.expression.type !== 'TemplateLiteral' &&
node.value.expression.type !== 'BinaryExpression'
) {
mark_subtree_dynamic(context.path);
node.metadata.needs_clsx = true;
}
if (node.value !== true) {
for (const chunk of get_attribute_chunks(node.value)) {
if (chunk.type !== 'ExpressionTag') continue;
if (
chunk.expression.type === 'FunctionExpression' ||
chunk.expression.type === 'ArrowFunctionExpression'
) {
continue;
}
}
if (is_event_attribute(node)) {
const parent = context.path.at(-1);
if (parent?.type === 'RegularElement' || parent?.type === 'SvelteElement') {
context.state.analysis.uses_event_attributes = true;
}
const expression = get_attribute_expression(node);
const delegated_event = get_delegated_event(node.name.slice(2), expression, context);
if (delegated_event !== null) {
if (delegated_event.hoisted) {
delegated_event.function.metadata.hoisted = true;
}
node.metadata.delegated = delegated_event;
}
}
}
}
/** @type {DelegatedEvent} */
const unhoisted = { hoisted: false };
/**
* Checks if given event attribute can be delegated/hoisted and returns the corresponding info if so
* @param {string} event_name
* @param {Expression | null} handler
* @param {Context} context
* @returns {null | DelegatedEvent}
*/
function get_delegated_event(event_name, handler, context) {
// Handle delegated event handlers. Bail out if not a delegated event.
if (!handler || !is_delegated(event_name)) {
return null;
}
// If we are not working with a RegularElement, then bail out.
const element = context.path.at(-1);
if (element?.type !== 'RegularElement') {
return null;
}
/** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | null} */
let target_function = null;
let binding = null;
if (element.metadata.has_spread) {
// event attribute becomes part of the dynamic spread array
return unhoisted;
}
if (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') {
target_function = handler;
} else if (handler.type === 'Identifier') {
binding = context.state.scope.get(handler.name);
if (context.state.analysis.module.scope.references.has(handler.name)) {
// If a binding with the same name is referenced in the module scope (even if not declared there), bail out
return unhoisted;
}
if (binding != null) {
for (const { path } of binding.references) {
const parent = path.at(-1);
if (parent === undefined) return unhoisted;
const grandparent = path.at(-2);
/** @type {AST.RegularElement | null} */
let element = null;
/** @type {string | null} */
let event_name = null;
if (parent.type === 'OnDirective') {
element = /** @type {AST.RegularElement} */ (grandparent);
event_name = parent.name;
} else if (
parent.type === 'ExpressionTag' &&
grandparent?.type === 'Attribute' &&
is_event_attribute(grandparent)
) {
element = /** @type {AST.RegularElement} */ (path.at(-3));
const attribute = /** @type {AST.Attribute} */ (grandparent);
event_name = get_attribute_event_name(attribute.name);
}
if (element && event_name) {
if (
element.type !== 'RegularElement' ||
element.metadata.has_spread ||
!is_delegated(event_name)
) {
return unhoisted;
}
} else if (parent.type !== 'FunctionDeclaration' && parent.type !== 'VariableDeclarator') {
return unhoisted;
}
}
}
// If the binding is exported, bail out
if (context.state.analysis.exports.find((node) => node.name === handler.name)) {
return unhoisted;
}
if (binding?.is_function()) {
target_function = binding.initial;
}
}
// If we can't find a function, or the function has multiple parameters, bail out
if (target_function == null || target_function.params.length > 1) {
return unhoisted;
}
const visited_references = new Set();
const scope = target_function.metadata.scope;
for (const [reference] of scope.references) {
// Bail out if the arguments keyword is used or $host is referenced
if (reference === 'arguments' || reference === '$host') return unhoisted;
// Bail out if references a store subscription
if (scope.get(`$${reference}`)?.kind === 'store_sub') return unhoisted;
const binding = scope.get(reference);
const local_binding = context.state.scope.get(reference);
// if the function access a snippet that can't be hoisted we bail out
if (
local_binding !== null &&
local_binding.initial?.type === 'SnippetBlock' &&
!local_binding.initial.metadata.can_hoist
) {
return unhoisted;
}
// If we are referencing a binding that is shadowed in another scope then bail out.
if (local_binding !== null && binding !== null && local_binding.node !== binding.node) {
return unhoisted;
}
// If we have multiple references to the same store using $ prefix, bail out.
if (
binding !== null &&
binding.kind === 'store_sub' &&
visited_references.has(reference.slice(1))
) {
return unhoisted;
}
// If we reference the index within an each block, then bail out.
if (binding !== null && binding.initial?.type === 'EachBlock') return unhoisted;
if (
binding !== null &&
// Bail out if the the binding is a rest param
(binding.declaration_kind === 'rest_param' ||
// Bail out if we reference anything from the EachBlock (for now) that mutates in non-runes mode,
(((!context.state.analysis.runes && binding.kind === 'each') ||
// or any normal not reactive bindings that are mutated.
binding.kind === 'normal') &&
binding.updated))
) {
return unhoisted;
}
visited_references.add(reference);
}
return { hoisted: true, function: target_function };
}
/**
* @param {string} event_name
*/
function get_attribute_event_name(event_name) {
event_name = event_name.slice(2);
if (is_capture_event(event_name)) {
event_name = event_name.slice(0, -7);
}
return event_name;
}