move stuff around

tidy-up-analysis
Rich Harris 12 months ago
parent a67cc7603a
commit 425077a92c

@ -1,28 +1,19 @@
import is_reference from 'is-reference'; import is_reference from 'is-reference';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { error } from '../../errors.js'; import { error } from '../../errors.js';
import * as assert from '../../utils/assert.js'; import { extract_identifiers, extract_paths, get_callee_name } from '../../utils/ast.js';
import {
extract_identifiers,
extract_paths,
get_callee_name,
is_event_attribute,
is_text_attribute,
object
} from '../../utils/ast.js';
import * as b from '../../utils/builders.js'; import * as b from '../../utils/builders.js';
import { DelegatedEvents, ReservedKeywords, Runes, SVGElements } from '../constants.js'; import { ReservedKeywords, Runes } from '../constants.js';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js'; import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import { merge } from '../visitors.js'; import { merge } from '../visitors.js';
import Stylesheet from './css/Stylesheet.js'; import Stylesheet from './css/Stylesheet.js';
import { warn } from '../../warnings.js'; import { warn } from '../../warnings.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js'; import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
import { regex_starts_with_newline } from '../patterns.js';
import { create_attribute, is_element_node } from '../nodes.js';
import { validate_template } from './visitors/validate-template.js'; import { validate_template } from './visitors/validate-template.js';
import { validate_a11y } from './visitors/validate-a11y.js'; import { validate_a11y } from './visitors/validate-a11y.js';
import { validate_legacy } from './visitors/validate-legacy.js'; import { validate_legacy } from './visitors/validate-legacy.js';
import { validate_runes } from './visitors/validate-runes.js'; import { validate_runes } from './visitors/validate-runes.js';
import { common_visitors } from './visitors/common.js';
/** /**
* @param {import('#compiler').Script | null} script * @param {import('#compiler').Script | null} script
@ -60,119 +51,6 @@ function get_component_name(filename) {
return name[0].toUpperCase() + name.slice(1); return name[0].toUpperCase() + name.slice(1);
} }
/**
* @param {Pick<import('#compiler').OnDirective, 'expression'| 'name' | 'modifiers'>} node
* @param {import('./types').Context} context
* @returns {null | import('#compiler').DelegatedEvent}
*/
function get_delegated_event(node, context) {
const handler = node.expression;
const event_name = node.name;
// Handle delegated event handlers. Bail-out if not a delegated event.
if (!handler || node.modifiers.includes('capture') || !DelegatedEvents.includes(event_name)) {
return null;
}
// If we are not working with a RegularElement/SlotElement, then bail-out.
const element = context.path.at(-1);
if (element == null || (element.type !== 'RegularElement' && element.type !== 'SlotElement')) {
return null;
}
// If we have multiple OnDirectives of the same type, bail-out.
if (
element.attributes.filter((attr) => attr.type === 'OnDirective' && attr.name === event_name)
.length > 1
) {
return null;
}
/** @type {import('#compiler').DelegatedEvent} */
const non_hoistable = { type: 'non-hoistable' };
/** @type {import('estree').FunctionExpression | import('estree').FunctionDeclaration | import('estree').ArrowFunctionExpression | null} */
let target_function = null;
let binding = null;
if (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') {
target_function = handler;
} else if (handler.type === 'Identifier') {
binding = context.state.scope.get(handler.name);
if (binding != null) {
for (const { path } of binding.references) {
const parent = path.at(-1);
if (parent == null) {
return non_hoistable;
}
const element =
parent.type === 'OnDirective'
? path.at(-2)
: parent.type === 'ExpressionTag' &&
is_event_attribute(/** @type {import('#compiler').Attribute} */ (path.at(-2)))
? path.at(-3)
: null;
if (element) {
if (element.type !== 'RegularElement' && element.type !== 'SlotElement') {
return non_hoistable;
}
} else if (parent.type !== 'FunctionDeclaration' && parent.type !== 'VariableDeclarator') {
return non_hoistable;
}
}
}
// If the binding is exported, bail-out
if (context.state.analysis.exports.find((node) => node.name === handler.name)) {
return non_hoistable;
}
if (binding !== null && binding.initial !== null && !binding.mutated && !binding.is_called) {
const binding_type = binding.initial.type;
if (
binding_type === 'ArrowFunctionExpression' ||
binding_type === 'FunctionDeclaration' ||
binding_type === 'FunctionExpression'
) {
target_function = binding.initial;
}
}
}
// If we can't find a function, bail-out
if (target_function == null) {
return non_hoistable;
}
// If the function is marked as non-hoistable, bail-out
if (target_function.metadata.hoistable === 'impossible') {
return non_hoistable;
}
// If the function has more than one arg, then bail-out
if (target_function.params.length > 1) {
return non_hoistable;
}
const scope = target_function.metadata.scope;
for (const [reference] of scope.references) {
const binding = scope.get(reference);
if (
binding !== null &&
// 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' && context.state.analysis.runes) ||
// or any reactive imports (those are rewritten) (can only happen in legacy mode)
(binding.kind === 'state' && binding.declaration_kind === 'import')) &&
binding.mutated
) {
return non_hoistable;
}
}
return { type: 'hoistable', function: target_function };
}
/** /**
* @param {import('estree').Program} ast * @param {import('estree').Program} ast
* @param {import('#compiler').ValidatedModuleCompileOptions} options * @param {import('#compiler').ValidatedModuleCompileOptions} options
@ -700,328 +578,6 @@ const runes_scope_tweaker = {
} }
}; };
/**
* @param {import('estree').CallExpression} node
* @param {import('./types').Context} context
* @returns {boolean}
*/
function is_known_safe_call(node, context) {
const callee = node.callee;
// Check for selector() API calls
if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier') {
const binding = context.state.scope.get(callee.object.name);
const selector_binding = context.state.scope.get('selector');
if (
selector_binding !== null &&
selector_binding.declaration_kind === 'import' &&
selector_binding.initial !== null &&
selector_binding.initial.type === 'ImportDeclaration' &&
selector_binding.initial.source.value === 'svelte' &&
binding !== null &&
binding.initial !== null &&
binding.initial.type === 'CallExpression' &&
binding.initial.callee.type === 'Identifier' &&
binding.initial.callee.name === 'selector'
) {
return true;
}
}
// String / Number / BigInt / Boolean casting calls
if (callee.type === 'Identifier') {
const name = callee.name;
const binding = context.state.scope.get(name);
if (
binding === null &&
(name === 'BigInt' || name === 'String' || name === 'Number' || name === 'Boolean')
) {
return true;
}
}
return false;
}
/**
* @param {import('estree').ArrowFunctionExpression | import('estree').FunctionExpression | import('estree').FunctionDeclaration} node
* @param {import('./types').Context} context
*/
export const function_visitor = (node, context) => {
// TODO retire this in favour of a more general solution based on bindings
node.metadata = {
// module context -> already hoisted
hoistable: context.state.ast_type === 'module' ? 'impossible' : false,
hoistable_params: [],
scope: context.state.scope
};
context.next({
...context.state,
function_depth: context.state.function_depth + 1
});
};
/** @type {import('./types').Visitors} */
const common_visitors = {
Attribute(node, context) {
if (node.value === true) return;
context.next();
node.metadata.dynamic = node.value.some((chunk) => {
if (chunk.type !== 'ExpressionTag') {
return false;
}
if (
chunk.expression.type === 'FunctionExpression' ||
chunk.expression.type === 'ArrowFunctionExpression'
) {
return false;
}
return chunk.metadata.dynamic;
});
if (is_event_attribute(node)) {
/** @type {string[]} */
const modifiers = [];
const expression = node.value[0].expression;
let name = node.name.slice(2);
if (
name.endsWith('capture') &&
name !== 'ongotpointercapture' &&
name !== 'onlostpointercapture'
) {
name = name.slice(0, -7);
modifiers.push('capture');
}
const delegated_event = get_delegated_event({ name, expression, modifiers }, context);
if (delegated_event !== null) {
if (delegated_event.type === 'hoistable') {
delegated_event.function.metadata.hoistable = true;
}
node.metadata.delegated = delegated_event;
}
}
},
ClassDirective(node, context) {
context.next({ ...context.state, expression: node });
},
SpreadAttribute(node, context) {
context.next({ ...context.state, expression: node });
},
SlotElement(node, context) {
let name = 'default';
for (const attr of node.attributes) {
if (attr.type === 'Attribute' && attr.name === 'name' && is_text_attribute(attr)) {
name = attr.value[0].data;
break;
}
}
context.state.analysis.slot_names.add(name);
},
StyleDirective(node, context) {
if (node.value === true) {
const binding = context.state.scope.get(node.name);
if (binding?.kind !== 'normal') {
node.metadata.dynamic = true;
}
} else {
context.next();
node.metadata.dynamic = node.value.some(
(node) => node.type === 'ExpressionTag' && node.metadata.dynamic
);
}
},
ExpressionTag(node, context) {
context.next({ ...context.state, expression: node });
},
Identifier(node, context) {
const parent = /** @type {import('estree').Node} */ (context.path.at(-1));
if (!is_reference(node, parent)) return;
const binding = context.state.scope.get(node.name);
// if no binding, means some global variable
if (binding && binding.kind !== 'normal') {
if (context.state.expression) {
context.state.expression.metadata.dynamic = true;
}
if (
node !== binding.node &&
(binding.kind === 'state' || binding.kind === 'derived') &&
context.state.function_depth === binding.scope.function_depth
) {
warn(context.state.analysis.warnings, node, context.path, 'static-state-reference');
}
}
},
CallExpression(node, context) {
if (context.state.expression?.type === 'ExpressionTag' && !is_known_safe_call(node, context)) {
context.state.expression.metadata.contains_call_expression = true;
}
const callee = node.callee;
if (callee.type === 'Identifier') {
const binding = context.state.scope.get(callee.name);
if (binding !== null) {
binding.is_called = true;
}
if (get_rune(node, context.state.scope) === '$derived') {
// special case — `$derived(foo)` is treated as `$derived(() => foo)`
// for the purposes of identifying static state references
context.next({
...context.state,
function_depth: context.state.function_depth + 1
});
return;
}
}
context.next();
},
MemberExpression(node, context) {
if (context.state.expression) {
context.state.expression.metadata.dynamic = true;
}
context.next();
},
BindDirective(node, context) {
let i = context.path.length;
while (i--) {
const parent = context.path[i];
if (
parent.type === 'Component' ||
parent.type === 'SvelteComponent' ||
parent.type === 'SvelteSelf'
) {
context.state.analysis.uses_component_bindings = true;
break;
} else if (is_element_node(parent)) {
break;
}
}
if (node.name !== 'group') return;
i = context.path.length;
while (i--) {
const parent = context.path[i];
if (parent.type === 'EachBlock') {
parent.metadata.contains_group_binding = true;
for (const binding of parent.metadata.references) {
binding.mutated = true;
}
}
}
const id = object(node.expression);
const binding = id === null ? null : context.state.scope.get(id.name);
assert.ok(binding);
let group = context.state.analysis.binding_groups.get(binding);
if (!group) {
group = {
name: context.state.scope.root.unique('binding_group'),
directives: []
};
context.state.analysis.binding_groups.set(binding, group);
}
group.directives.push(node);
node.metadata = {
binding_group_name: group.name,
parent_each_blocks: /** @type {import('#compiler').EachBlock[]} */ (
context.path.filter((p) => p.type === 'EachBlock')
)
};
},
OnDirective(node, context) {
node.metadata = { delegated: null };
context.next();
const delegated_event = get_delegated_event(node, context);
if (delegated_event !== null) {
if (delegated_event.type === 'hoistable') {
delegated_event.function.metadata.hoistable = true;
}
node.metadata.delegated = delegated_event;
}
},
ArrowFunctionExpression: function_visitor,
FunctionExpression: function_visitor,
FunctionDeclaration: function_visitor,
RegularElement(node, context) {
if (context.state.options.namespace !== 'foreign' && SVGElements.includes(node.name)) {
node.metadata.svg = true;
}
// Special case: Move the children of <textarea> into a value attribute if they are dynamic
if (
context.state.options.namespace !== 'foreign' &&
node.name === 'textarea' &&
node.fragment.nodes.length > 0
) {
if (node.fragment.nodes.length > 1 || node.fragment.nodes[0].type !== 'Text') {
const first = node.fragment.nodes[0];
if (first.type === 'Text') {
// The leading newline character needs to be stripped because of a qirk:
// It is ignored by browsers if the tag and its contents are set through
// innerHTML, but we're now setting it through the value property at which
// point it is _not_ ignored, so we need to strip it ourselves.
// see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
// see https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
first.data = first.data.replace(regex_starts_with_newline, '');
first.raw = first.raw.replace(regex_starts_with_newline, '');
}
node.attributes.push(
create_attribute(
'value',
/** @type {import('#compiler').Text} */ (node.fragment.nodes.at(0)).start,
/** @type {import('#compiler').Text} */ (node.fragment.nodes.at(-1)).end,
// @ts-ignore
node.fragment.nodes
)
);
node.fragment.nodes = [];
}
}
// Special case: single expression tag child of option element -> add "fake" attribute
// to ensure that value types are the same (else for example numbers would be strings)
if (
context.state.options.namespace !== 'foreign' &&
node.name === 'option' &&
node.fragment.nodes?.length === 1 &&
node.fragment.nodes[0].type === 'ExpressionTag' &&
!node.attributes.some(
(attribute) => attribute.type === 'Attribute' && attribute.name === 'value'
)
) {
const child = node.fragment.nodes[0];
node.attributes.push(create_attribute('value', child.start, child.end, [child]));
}
context.state.analysis.elements.push(node);
},
SvelteElement(node, { state }) {
state.analysis.elements.push(node);
}
};
/** /**
* @param {Map<import('estree').LabeledStatement, import('../types.js').ReactiveStatement>} unsorted_reactive_declarations * @param {Map<import('estree').LabeledStatement, import('../types.js').ReactiveStatement>} unsorted_reactive_declarations
*/ */

@ -1,6 +1,25 @@
import { error } from '../../errors.js'; import { error } from '../../errors.js';
import { get_rune } from '../scope'; import { get_rune } from '../scope';
/**
* @param {import('estree').ArrowFunctionExpression | import('estree').FunctionExpression | import('estree').FunctionDeclaration} node
* @param {import('./types').Context} context
*/
export const function_visitor = (node, context) => {
// TODO retire this in favour of a more general solution based on bindings
node.metadata = {
// module context -> already hoisted
hoistable: context.state.ast_type === 'module' ? 'impossible' : false,
hoistable_params: [],
scope: context.state.scope
};
context.next({
...context.state,
function_depth: context.state.function_depth + 1
});
};
/** /**
* @param {import('../../errors.js').NodeLike} node * @param {import('../../errors.js').NodeLike} node
* @param {import('estree').Pattern | import('estree').Expression} argument * @param {import('estree').Pattern | import('estree').Expression} argument

@ -0,0 +1,425 @@
import is_reference from 'is-reference';
import { is_event_attribute, is_text_attribute, object } from '../../../utils/ast.js';
import { warn } from '../../../warnings.js';
import { DelegatedEvents, SVGElements } from '../../constants.js';
import { create_attribute, is_element_node } from '../../nodes.js';
import { get_rune } from '../../scope.js';
import * as assert from '../../../utils/assert.js';
import { function_visitor } from '../utils.js';
import { regex_starts_with_newline } from '../../patterns.js';
/**
* @param {Pick<import('#compiler').OnDirective, 'expression'| 'name' | 'modifiers'>} node
* @param {import('../types').Context} context
* @returns {null | import('#compiler').DelegatedEvent}
*/
function get_delegated_event(node, context) {
const handler = node.expression;
const event_name = node.name;
// Handle delegated event handlers. Bail-out if not a delegated event.
if (!handler || node.modifiers.includes('capture') || !DelegatedEvents.includes(event_name)) {
return null;
}
// If we are not working with a RegularElement/SlotElement, then bail-out.
const element = context.path.at(-1);
if (element == null || (element.type !== 'RegularElement' && element.type !== 'SlotElement')) {
return null;
}
// If we have multiple OnDirectives of the same type, bail-out.
if (
element.attributes.filter((attr) => attr.type === 'OnDirective' && attr.name === event_name)
.length > 1
) {
return null;
}
/** @type {import('#compiler').DelegatedEvent} */
const non_hoistable = { type: 'non-hoistable' };
/** @type {import('estree').FunctionExpression | import('estree').FunctionDeclaration | import('estree').ArrowFunctionExpression | null} */
let target_function = null;
let binding = null;
if (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') {
target_function = handler;
} else if (handler.type === 'Identifier') {
binding = context.state.scope.get(handler.name);
if (binding != null) {
for (const { path } of binding.references) {
const parent = path.at(-1);
if (parent == null) {
return non_hoistable;
}
const element =
parent.type === 'OnDirective'
? path.at(-2)
: parent.type === 'ExpressionTag' &&
is_event_attribute(/** @type {import('#compiler').Attribute} */ (path.at(-2)))
? path.at(-3)
: null;
if (element) {
if (element.type !== 'RegularElement' && element.type !== 'SlotElement') {
return non_hoistable;
}
} else if (parent.type !== 'FunctionDeclaration' && parent.type !== 'VariableDeclarator') {
return non_hoistable;
}
}
}
// If the binding is exported, bail-out
if (context.state.analysis.exports.find((node) => node.name === handler.name)) {
return non_hoistable;
}
if (binding !== null && binding.initial !== null && !binding.mutated && !binding.is_called) {
const binding_type = binding.initial.type;
if (
binding_type === 'ArrowFunctionExpression' ||
binding_type === 'FunctionDeclaration' ||
binding_type === 'FunctionExpression'
) {
target_function = binding.initial;
}
}
}
// If we can't find a function, bail-out
if (target_function == null) {
return non_hoistable;
}
// If the function is marked as non-hoistable, bail-out
if (target_function.metadata.hoistable === 'impossible') {
return non_hoistable;
}
// If the function has more than one arg, then bail-out
if (target_function.params.length > 1) {
return non_hoistable;
}
const scope = target_function.metadata.scope;
for (const [reference] of scope.references) {
const binding = scope.get(reference);
if (
binding !== null &&
// 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' && context.state.analysis.runes) ||
// or any reactive imports (those are rewritten) (can only happen in legacy mode)
(binding.kind === 'state' && binding.declaration_kind === 'import')) &&
binding.mutated
) {
return non_hoistable;
}
}
return { type: 'hoistable', function: target_function };
}
/**
* @param {import('estree').CallExpression} node
* @param {import('../types').Context} context
* @returns {boolean}
*/
function is_known_safe_call(node, context) {
const callee = node.callee;
// Check for selector() API calls
if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier') {
const binding = context.state.scope.get(callee.object.name);
const selector_binding = context.state.scope.get('selector');
if (
selector_binding !== null &&
selector_binding.declaration_kind === 'import' &&
selector_binding.initial !== null &&
selector_binding.initial.type === 'ImportDeclaration' &&
selector_binding.initial.source.value === 'svelte' &&
binding !== null &&
binding.initial !== null &&
binding.initial.type === 'CallExpression' &&
binding.initial.callee.type === 'Identifier' &&
binding.initial.callee.name === 'selector'
) {
return true;
}
}
// String / Number / BigInt / Boolean casting calls
if (callee.type === 'Identifier') {
const name = callee.name;
const binding = context.state.scope.get(name);
if (
binding === null &&
(name === 'BigInt' || name === 'String' || name === 'Number' || name === 'Boolean')
) {
return true;
}
}
return false;
}
/** @type {import('../types').Visitors} */
export const common_visitors = {
Attribute(node, context) {
if (node.value === true) return;
context.next();
node.metadata.dynamic = node.value.some((chunk) => {
if (chunk.type !== 'ExpressionTag') {
return false;
}
if (
chunk.expression.type === 'FunctionExpression' ||
chunk.expression.type === 'ArrowFunctionExpression'
) {
return false;
}
return chunk.metadata.dynamic;
});
if (is_event_attribute(node)) {
/** @type {string[]} */
const modifiers = [];
const expression = node.value[0].expression;
let name = node.name.slice(2);
if (
name.endsWith('capture') &&
name !== 'ongotpointercapture' &&
name !== 'onlostpointercapture'
) {
name = name.slice(0, -7);
modifiers.push('capture');
}
const delegated_event = get_delegated_event({ name, expression, modifiers }, context);
if (delegated_event !== null) {
if (delegated_event.type === 'hoistable') {
delegated_event.function.metadata.hoistable = true;
}
node.metadata.delegated = delegated_event;
}
}
},
ClassDirective(node, context) {
context.next({ ...context.state, expression: node });
},
SpreadAttribute(node, context) {
context.next({ ...context.state, expression: node });
},
SlotElement(node, context) {
let name = 'default';
for (const attr of node.attributes) {
if (attr.type === 'Attribute' && attr.name === 'name' && is_text_attribute(attr)) {
name = attr.value[0].data;
break;
}
}
context.state.analysis.slot_names.add(name);
},
StyleDirective(node, context) {
if (node.value === true) {
const binding = context.state.scope.get(node.name);
if (binding?.kind !== 'normal') {
node.metadata.dynamic = true;
}
} else {
context.next();
node.metadata.dynamic = node.value.some(
(node) => node.type === 'ExpressionTag' && node.metadata.dynamic
);
}
},
ExpressionTag(node, context) {
context.next({ ...context.state, expression: node });
},
Identifier(node, context) {
const parent = /** @type {import('estree').Node} */ (context.path.at(-1));
if (!is_reference(node, parent)) return;
const binding = context.state.scope.get(node.name);
// if no binding, means some global variable
if (binding && binding.kind !== 'normal') {
if (context.state.expression) {
context.state.expression.metadata.dynamic = true;
}
if (
node !== binding.node &&
(binding.kind === 'state' || binding.kind === 'derived') &&
context.state.function_depth === binding.scope.function_depth
) {
warn(context.state.analysis.warnings, node, context.path, 'static-state-reference');
}
}
},
CallExpression(node, context) {
if (context.state.expression?.type === 'ExpressionTag' && !is_known_safe_call(node, context)) {
context.state.expression.metadata.contains_call_expression = true;
}
const callee = node.callee;
if (callee.type === 'Identifier') {
const binding = context.state.scope.get(callee.name);
if (binding !== null) {
binding.is_called = true;
}
if (get_rune(node, context.state.scope) === '$derived') {
// special case — `$derived(foo)` is treated as `$derived(() => foo)`
// for the purposes of identifying static state references
context.next({
...context.state,
function_depth: context.state.function_depth + 1
});
return;
}
}
context.next();
},
MemberExpression(node, context) {
if (context.state.expression) {
context.state.expression.metadata.dynamic = true;
}
context.next();
},
BindDirective(node, context) {
let i = context.path.length;
while (i--) {
const parent = context.path[i];
if (
parent.type === 'Component' ||
parent.type === 'SvelteComponent' ||
parent.type === 'SvelteSelf'
) {
context.state.analysis.uses_component_bindings = true;
break;
} else if (is_element_node(parent)) {
break;
}
}
if (node.name !== 'group') return;
i = context.path.length;
while (i--) {
const parent = context.path[i];
if (parent.type === 'EachBlock') {
parent.metadata.contains_group_binding = true;
for (const binding of parent.metadata.references) {
binding.mutated = true;
}
}
}
const id = object(node.expression);
const binding = id === null ? null : context.state.scope.get(id.name);
assert.ok(binding);
let group = context.state.analysis.binding_groups.get(binding);
if (!group) {
group = {
name: context.state.scope.root.unique('binding_group'),
directives: []
};
context.state.analysis.binding_groups.set(binding, group);
}
group.directives.push(node);
node.metadata = {
binding_group_name: group.name,
parent_each_blocks: /** @type {import('#compiler').EachBlock[]} */ (
context.path.filter((p) => p.type === 'EachBlock')
)
};
},
OnDirective(node, context) {
node.metadata = { delegated: null };
context.next();
const delegated_event = get_delegated_event(node, context);
if (delegated_event !== null) {
if (delegated_event.type === 'hoistable') {
delegated_event.function.metadata.hoistable = true;
}
node.metadata.delegated = delegated_event;
}
},
ArrowFunctionExpression: function_visitor,
FunctionExpression: function_visitor,
FunctionDeclaration: function_visitor,
RegularElement(node, context) {
if (context.state.options.namespace !== 'foreign' && SVGElements.includes(node.name)) {
node.metadata.svg = true;
}
// Special case: Move the children of <textarea> into a value attribute if they are dynamic
if (
context.state.options.namespace !== 'foreign' &&
node.name === 'textarea' &&
node.fragment.nodes.length > 0
) {
if (node.fragment.nodes.length > 1 || node.fragment.nodes[0].type !== 'Text') {
const first = node.fragment.nodes[0];
if (first.type === 'Text') {
// The leading newline character needs to be stripped because of a qirk:
// It is ignored by browsers if the tag and its contents are set through
// innerHTML, but we're now setting it through the value property at which
// point it is _not_ ignored, so we need to strip it ourselves.
// see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
// see https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
first.data = first.data.replace(regex_starts_with_newline, '');
first.raw = first.raw.replace(regex_starts_with_newline, '');
}
node.attributes.push(
create_attribute(
'value',
/** @type {import('#compiler').Text} */ (node.fragment.nodes.at(0)).start,
/** @type {import('#compiler').Text} */ (node.fragment.nodes.at(-1)).end,
// @ts-ignore
node.fragment.nodes
)
);
node.fragment.nodes = [];
}
}
// Special case: single expression tag child of option element -> add "fake" attribute
// to ensure that value types are the same (else for example numbers would be strings)
if (
context.state.options.namespace !== 'foreign' &&
node.name === 'option' &&
node.fragment.nodes?.length === 1 &&
node.fragment.nodes[0].type === 'ExpressionTag' &&
!node.attributes.some(
(attribute) => attribute.type === 'Attribute' && attribute.name === 'value'
)
) {
const child = node.fragment.nodes[0];
node.attributes.push(create_attribute('value', child.start, child.end, [child]));
}
context.state.analysis.elements.push(node);
},
SvelteElement(node, { state }) {
state.analysis.elements.push(node);
}
};
Loading…
Cancel
Save