UNPKG

svelte

Version:

Cybernetically enhanced web apps

272 lines (240 loc) 7.73 kB
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */ /** @import { AST, Binding } from '#compiler' */ /** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */ /** @import { Analysis } from '../../types.js' */ /** @import { Scope } from '../../scope.js' */ import * as b from '../../../utils/builders.js'; import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js'; import { PROPS_IS_LAZY_INITIAL, PROPS_IS_IMMUTABLE, PROPS_IS_RUNES, PROPS_IS_UPDATED, PROPS_IS_BINDABLE } from '../../../../constants.js'; import { dev } from '../../../state.js'; import { get_value } from './visitors/shared/declarations.js'; /** * @param {Binding} binding * @param {Analysis} analysis * @returns {boolean} */ export function is_state_source(binding, analysis) { return ( (binding.kind === 'state' || binding.kind === 'raw_state') && (!analysis.immutable || binding.reassigned || analysis.accessors) ); } /** * @param {Identifier} node * @param {ClientTransformState} state * @returns {Expression} */ export function build_getter(node, state) { if (Object.hasOwn(state.transform, node.name)) { const binding = state.scope.get(node.name); // don't transform the declaration itself if (node !== binding?.node) { return state.transform[node.name].read(node); } } return node; } /** * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node * @param {ComponentContext} context * @returns {Pattern[]} */ function get_hoisted_params(node, context) { const scope = context.state.scope; /** @type {Identifier[]} */ const params = []; /** * We only want to push if it's not already present to avoid name clashing * @param {Identifier} id */ function push_unique(id) { if (!params.find((param) => param.name === id.name)) { params.push(id); } } for (const [reference] of scope.references) { let 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 push_unique(b.id(binding.node.name)); binding = /** @type {Binding} */ (scope.get(binding.node.name.slice(1))); } let expression = context.state.transform[reference]?.read(b.id(binding.node.name)); if ( // If it's a destructured derived binding, then we can extract the derived signal reference and use that. // TODO this code is bad, we need to kill it expression != null && typeof expression !== 'function' && expression.type === 'MemberExpression' && expression.object.type === 'CallExpression' && expression.object.callee.type === 'Identifier' && expression.object.callee.name === '$.get' && expression.object.arguments[0].type === 'Identifier' ) { push_unique(b.id(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') && !is_prop_source(binding, context.state) ) { push_unique(b.id('$$props')); } else if ( // imports don't need to be hoisted binding.declaration_kind !== 'import' ) { // create a copy to remove start/end tags which would mess up source maps push_unique(b.id(binding.node.name)); // rest props are often accessed through the $$props object for optimization reasons, // but we can't know if the delegated event handler will use it, so we need to add both as params if (binding.kind === 'rest_prop' && context.state.analysis.runes) { push_unique(b.id('$$props')); } } } } return params; } /** * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node * @param {ComponentContext} context * @returns {Pattern[]} */ export function build_hoisted_params(node, context) { const hoisted_params = get_hoisted_params(node, context); node.metadata.hoisted_params = hoisted_params; /** @type {Pattern[]} */ const params = []; if (node.params.length === 0) { if (hoisted_params.length > 0) { // For the event object params.push(b.id(context.state.scope.generate('_'))); } } else { for (const param of node.params) { params.push(/** @type {Pattern} */ (context.visit(param))); } } params.push(...hoisted_params); return params; } /** * @param {Binding} binding * @param {ComponentClientTransformState} state * @param {string} name * @param {Expression | null} [initial] * @returns */ export function get_prop_source(binding, state, name, initial) { /** @type {Expression[]} */ const args = [b.id('$$props'), b.literal(name)]; let flags = 0; if (binding.kind === 'bindable_prop') { flags |= PROPS_IS_BINDABLE; } 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 || (state.analysis.runes && binding.mutated) : binding.updated) ) { flags |= PROPS_IS_UPDATED; } /** @type {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 {Binding} binding * @param {ClientTransformState} state * @returns */ export function is_prop_source(binding, state) { return ( (binding.kind === 'prop' || binding.kind === 'bindable_prop') && (!state.analysis.runes || state.analysis.accessors || binding.reassigned || binding.initial || // Until legacy mode is gone, we also need to use the prop source when only mutated is true, // because the parent could be a legacy component which needs coarse-grained reactivity binding.updated) ); } /** * @param {Expression} node * @param {Scope | null} scope */ export function should_proxy(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 if ( binding !== null && !binding.reassigned && binding.initial !== null && binding.initial.type !== 'FunctionDeclaration' && binding.initial.type !== 'ClassDeclaration' && binding.initial.type !== 'ImportDeclaration' && binding.initial.type !== 'EachBlock' && binding.initial.type !== 'SnippetBlock' ) { return should_proxy(binding.initial, null); } } return true; } /** * Svelte legacy mode should use safe equals in most places, runes mode shouldn't * @param {ComponentClientTransformState} state * @param {Expression} arg */ export function create_derived(state, arg) { return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg); }