fix: avoid marking subtree as dynamic for inlined attributes (#14269)

* fix: avoid marking subtree as dynamic for inlined attributes

* fix: i'm a silly goose 🪿

* chore: refactor `is_inlinable_expression` to accept the attribute

* feat: inline dom expression too

* fix: special case literals with `"` in it and fix standalone case

* chore: simpler check first

Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>

* typo

* add more stuff to snapshot test

* simplify/speedup by doing the work once, during analysis

* simplify

* simplify - no reason these cases should prevent inlining

* return template

* name is incorrect

* name is incorrect

* fix escaping

* no longer necessary

* remove obsolete description

* better concatenation

* fix test

* do the work at runtime

* fix another thing

* tidy

* tidy up

* simplify

* simplify

* fix

* note to self

* another

* simplify

* more accurate name

* simplify

* simplify

* explain what is happening

* tidy up

* simplify

* better inlining

* update test

* colocate some code

* better inlining

* use attribute metadata

* Update packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js

Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>

* Apply suggestions from code review

---------

Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/14302/head
Paolo Ricciuti 1 month ago committed by GitHub
parent 6a7146bee7
commit 8a8e6f70e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: better inlining of static attributes

@ -1,7 +1,7 @@
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */
/** @import { AST, DelegatedEvent, SvelteNode } from '#compiler' */
/** @import { Context } from '../types' */
import { is_capture_event, is_delegated } from '../../../../utils.js';
import { is_boolean_attribute, is_capture_event, is_delegated } from '../../../../utils.js';
import {
get_attribute_chunks,
get_attribute_expression,
@ -16,14 +16,23 @@ import { mark_subtree_dynamic } from './shared/fragment.js';
export function Attribute(node, context) {
context.next();
const parent = /** @type {SvelteNode} */ (context.path.at(-1));
// special case
if (node.name === 'value') {
const parent = /** @type {SvelteNode} */ (context.path.at(-1));
if (parent.type === 'RegularElement' && parent.name === 'option') {
mark_subtree_dynamic(context.path);
}
}
if (node.name.startsWith('on')) {
mark_subtree_dynamic(context.path);
}
if (parent.type === 'RegularElement' && is_boolean_attribute(node.name.toLowerCase())) {
node.metadata.expression.can_inline = false;
}
if (node.value !== true) {
for (const chunk of get_attribute_chunks(node.value)) {
if (chunk.type !== 'ExpressionTag') continue;
@ -37,6 +46,7 @@ export function Attribute(node, context) {
node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
node.metadata.expression.has_call ||= chunk.metadata.expression.has_call;
node.metadata.expression.can_inline &&= chunk.metadata.expression.can_inline;
}
if (is_event_attribute(node)) {

@ -178,6 +178,7 @@ export function CallExpression(node, context) {
if (!is_pure(node.callee, context) || context.state.expression.dependencies.size > 0) {
context.state.expression.has_call = true;
context.state.expression.has_state = true;
context.state.expression.can_inline = false;
}
}
}

@ -2,7 +2,6 @@
/** @import { Context } from '../types' */
import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js';
import * as e from '../../../errors.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.ExpressionTag} node
@ -15,9 +14,5 @@ export function ExpressionTag(node, context) {
}
}
// TODO ideally we wouldn't do this here, we'd just do it on encountering
// an `Identifier` within the tag. But we currently need to handle `{42}` etc
mark_subtree_dynamic(context.path);
context.next({ ...context.state, expression: node.metadata.expression });
}

@ -1,5 +1,4 @@
/** @import { Expression, Identifier } from 'estree' */
/** @import { EachBlock } from '#compiler' */
/** @import { Context } from '../types' */
import is_reference from 'is-reference';
import { should_proxy } from '../../3-transform/client/utils.js';
@ -20,8 +19,6 @@ export function Identifier(node, context) {
return;
}
mark_subtree_dynamic(context.path);
// If we are using arguments outside of a function, then throw an error
if (
node.name === 'arguments' &&
@ -87,6 +84,12 @@ export function Identifier(node, context) {
}
}
// no binding means global, and we can't inline e.g. `<span>{location}</span>`
// because it could change between component renders. if there _is_ a
// binding and it is outside module scope, the expression cannot
// be inlined (TODO allow inlining in more cases - e.g. primitive consts)
let can_inline = !!binding && !binding.scope.parent && binding.kind === 'normal';
if (binding) {
if (context.state.expression) {
context.state.expression.dependencies.add(binding);
@ -122,4 +125,17 @@ export function Identifier(node, context) {
w.reactive_declaration_module_script_dependency(node);
}
}
if (!can_inline && context.state.expression) {
context.state.expression.can_inline = false;
}
/**
* if the identifier is part of an expression tag of an attribute we want to check if it's inlinable
* before marking the subtree as dynamic. This is because if it's inlinable it will be inlined in the template
* directly making the whole thing actually static.
*/
if (!can_inline || !context.path.find((node) => node.type === 'Attribute')) {
mark_subtree_dynamic(context.path);
}
}

@ -19,6 +19,7 @@ export function MemberExpression(node, context) {
if (context.state.expression && !is_pure(node, context)) {
context.state.expression.has_state = true;
context.state.expression.can_inline = false;
}
if (!is_safe_identifier(node, context.state.scope)) {

@ -10,6 +10,7 @@ export function TaggedTemplateExpression(node, context) {
if (context.state.expression && !is_pure(node.tag, context)) {
context.state.expression.has_call = true;
context.state.expression.has_state = true;
context.state.expression.can_inline = false;
}
if (node.tag.type === 'Identifier') {

@ -1,18 +1,18 @@
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */
/** @import { AST, Binding, SvelteNode } from '#compiler' */
/** @import { Binding, SvelteNode } 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_BINDABLE,
PROPS_IS_IMMUTABLE,
PROPS_IS_LAZY_INITIAL,
PROPS_IS_RUNES,
PROPS_IS_UPDATED,
PROPS_IS_BINDABLE
PROPS_IS_UPDATED
} from '../../../../constants.js';
import { dev } from '../../../state.js';
import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js';
import * as b from '../../../utils/builders.js';
import { get_value } from './visitors/shared/declarations.js';
/**
@ -311,43 +311,3 @@ export function create_derived_block_argument(node, context) {
export function create_derived(state, arg) {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
}
/**
* Whether a variable can be referenced directly from template string.
* @param {import('#compiler').Binding | undefined} binding
* @returns {boolean}
*/
export function can_inline_variable(binding) {
return (
!!binding &&
// in a `<script module>` block
!binding.scope.parent &&
// to prevent the need for escaping
binding.initial?.type === 'Literal'
);
}
/**
* @param {(AST.Text | AST.ExpressionTag) | (AST.Text | AST.ExpressionTag)[]} node_or_nodes
* @param {import('./types.js').ComponentClientTransformState} state
*/
export function is_inlinable_expression(node_or_nodes, state) {
let nodes = Array.isArray(node_or_nodes) ? node_or_nodes : [node_or_nodes];
let has_expression_tag = false;
for (let value of nodes) {
if (value.type === 'ExpressionTag') {
if (value.expression.type === 'Identifier') {
const binding = state.scope
.owner(value.expression.name)
?.declarations.get(value.expression.name);
if (!can_inline_variable(binding)) {
return false;
}
} else {
return false;
}
has_expression_tag = true;
}
}
return has_expression_tag;
}

@ -1,4 +1,4 @@
/** @import { Expression, Identifier, Statement } from 'estree' */
/** @import { Expression, Identifier, Statement, TemplateElement } from 'estree' */
/** @import { AST, Namespace } from '#compiler' */
/** @import { SourceLocation } from '#shared' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
@ -141,14 +141,14 @@ export function Fragment(node, context) {
const id = b.id(context.state.scope.generate('fragment'));
const use_space_template =
trimmed.some((node) => node.type === 'ExpressionTag') &&
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag');
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') &&
trimmed.some((node) => node.type === 'ExpressionTag' && !node.metadata.expression.can_inline);
if (use_space_template) {
// special case — we can use `$.text` instead of creating a unique template
const id = b.id(context.state.scope.generate('text'));
process_children(trimmed, () => id, false, {
process_children(trimmed, () => id, null, {
...context,
state
});
@ -158,12 +158,12 @@ export function Fragment(node, context) {
} else {
if (is_standalone) {
// no need to create a template, we can just use the existing block's anchor
process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state });
process_children(trimmed, () => b.id('$$anchor'), null, { ...context, state });
} else {
/** @type {(is_text: boolean) => Expression} */
const expression = (is_text) => b.call('$.first_child', id, is_text && b.true);
process_children(trimmed, expression, false, { ...context, state });
process_children(trimmed, expression, null, { ...context, state });
let flags = TEMPLATE_FRAGMENT;
@ -212,12 +212,34 @@ function join_template(items) {
let quasi = b.quasi('');
const template = b.template([quasi], []);
/**
* @param {Expression} expression
*/
function push(expression) {
if (expression.type === 'TemplateLiteral') {
for (let i = 0; i < expression.expressions.length; i += 1) {
const q = expression.quasis[i];
const e = expression.expressions[i];
quasi.value.cooked += /** @type {string} */ (q.value.cooked);
push(e);
}
const last = /** @type {TemplateElement} */ (expression.quasis.at(-1));
quasi.value.cooked += /** @type {string} */ (last.value.cooked);
} else if (expression.type === 'Literal') {
/** @type {string} */ (quasi.value.cooked) += expression.value;
} else {
template.expressions.push(expression);
template.quasis.push((quasi = b.quasi('')));
}
}
for (const item of items) {
if (typeof item === 'string') {
quasi.value.cooked += item;
} else {
template.expressions.push(item);
template.quasis.push((quasi = b.quasi('')));
push(item);
}
}

@ -1,46 +1,36 @@
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */
/** @import { Expression, ExpressionStatement, Identifier, Literal, MemberExpression, ObjectExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { SourceLocation } from '#shared' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
/** @import { Scope } from '../../../scope' */
import { escape_html } from '../../../../../escaping.js';
import {
is_boolean_attribute,
is_dom_property,
is_load_error_element,
is_void
} from '../../../../../utils.js';
import { escape_html } from '../../../../../escaping.js';
import { dev, is_ignored, locator } from '../../../../state.js';
import {
get_attribute_expression,
is_event_attribute,
is_text_attribute
} from '../../../../utils/ast.js';
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { is_custom_element_node } from '../../../nodes.js';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_getter, create_derived } from '../utils.js';
import {
build_getter,
can_inline_variable,
create_derived,
is_inlinable_expression
} from '../utils.js';
import {
get_attribute_name,
build_attribute_value,
build_class_directives,
build_set_attributes,
build_style_directives,
build_set_attributes
get_attribute_name
} from './shared/element.js';
import { visit_event_attribute } from './shared/events.js';
import { process_children } from './shared/fragment.js';
import {
build_render_statement,
build_template_literal,
build_template_chunk,
build_update,
build_update_assignment,
get_states_and_calls
build_update_assignment
} from './shared/utils.js';
import { visit_event_attribute } from './shared/events.js';
/**
* @param {AST.RegularElement} node
@ -362,28 +352,32 @@ export function RegularElement(node, context) {
// special case — if an element that only contains text, we don't need
// to descend into it if the text is non-reactive
const states_and_calls =
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') &&
trimmed.some((node) => node.type === 'ExpressionTag') &&
get_states_and_calls(trimmed);
const is_text = trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag');
// in the rare case that we have static text that can't be inlined
// (e.g. `<span>{location}</span>`), set `textContent` programmatically
const use_text_content =
is_text &&
trimmed.every((node) => node.type === 'Text' || !node.metadata.expression.has_state) &&
trimmed.some((node) => node.type === 'ExpressionTag' && !node.metadata.expression.can_inline);
if (use_text_content) {
let { value } = build_template_chunk(trimmed, context.visit, child_state);
if (states_and_calls && states_and_calls.states === 0) {
child_state.init.push(
b.stmt(
b.assignment(
'=',
b.member(context.state.node, 'textContent'),
build_template_literal(trimmed, context.visit, child_state).value
)
)
b.stmt(b.assignment('=', b.member(context.state.node, 'textContent'), value))
);
} else {
/** @type {Expression} */
let arg = context.state.node;
// If `hydrate_node` is set inside the element, we need to reset it
// after the element has been hydrated
let needs_reset = trimmed.some((node) => node.type !== 'Text');
// after the element has been hydrated (we don't need to reset if it's been inlined)
let needs_reset = !trimmed.every(
(node) =>
node.type === 'Text' ||
(node.type === 'ExpressionTag' && node.metadata.expression.can_inline)
);
// The same applies if it's a `<template>` element, since we need to
// set the value of `hydrate_node` to `node.content`
@ -393,7 +387,7 @@ export function RegularElement(node, context) {
arg = b.member(arg, 'content');
}
process_children(trimmed, (is_text) => b.call('$.child', arg, is_text && b.true), true, {
process_children(trimmed, (is_text) => b.call('$.child', arg, is_text && b.true), node, {
...context,
state: child_state
});
@ -586,10 +580,6 @@ function build_element_attribute_update_assignment(element, node_id, attribute,
);
}
const inlinable_expression =
attribute.value === true
? false // not an expression
: is_inlinable_expression(attribute.value, context.state);
if (attribute.metadata.expression.has_state) {
if (has_call) {
state.init.push(build_update(update));
@ -597,14 +587,44 @@ function build_element_attribute_update_assignment(element, node_id, attribute,
state.update.push(update);
}
return true;
}
// we need to special case textarea value because it's not an actual attribute
const can_inline =
(attribute.name !== 'value' || element.name !== 'textarea') &&
attribute.metadata.expression.can_inline;
if (can_inline) {
/** @type {Literal | undefined} */
let literal = undefined;
if (value.type === 'Literal') {
literal = value;
} else if (value.type === 'Identifier') {
const binding = context.state.scope.get(value.name);
if (binding && binding.initial?.type === 'Literal' && !binding.reassigned) {
literal = binding.initial;
}
}
if (literal && escape_html(literal.value, true) === String(literal.value)) {
if (is_boolean_attribute(name)) {
if (literal.value) {
context.state.template.push(` ${name}`);
}
} else {
if (inlinable_expression) {
context.state.template.push(` ${name}="`, value, '"');
}
} else {
context.state.template.push(
b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true)
);
}
} else {
state.init.push(update);
}
return false;
}
}
/**

@ -1,14 +1,14 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { build_template_literal } from './shared/utils.js';
import { build_template_chunk } from './shared/utils.js';
/**
* @param {AST.TitleElement} node
* @param {ComponentContext} context
*/
export function TitleElement(node, context) {
const { has_state, value } = build_template_literal(
const { has_state, value } = build_template_chunk(
/** @type {any} */ (node.fragment.nodes),
context.visit,
context.state

@ -6,7 +6,7 @@ import { is_ignored } from '../../../../../state.js';
import { get_attribute_expression, is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { build_getter, create_derived } from '../../utils.js';
import { build_template_literal, build_update } from './utils.js';
import { build_template_chunk, build_update } from './utils.js';
/**
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
@ -200,7 +200,7 @@ export function build_attribute_value(value, context) {
};
}
return build_template_literal(value, context.visit, context.state);
return build_template_chunk(value, context.visit, context.state);
}
/**

@ -1,10 +1,11 @@
/** @import { Expression } from 'estree' */
/** @import { AST, SvelteNode } from '#compiler' */
/** @import { Scope } from '../../../../scope.js' */
/** @import { ComponentContext } from '../../types' */
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
import { escape_html } from '../../../../../../escaping.js';
import { is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { is_inlinable_expression } from '../../utils.js';
import { build_template_literal, build_update } from './utils.js';
import { build_template_chunk, build_update } from './utils.js';
/**
* Processes an array of template nodes, joining sibling text/expression nodes
@ -12,10 +13,10 @@ import { build_template_literal, build_update } from './utils.js';
* corresponding template node references these updates are applied to.
* @param {SvelteNode[]} nodes
* @param {(is_text: boolean) => Expression} initial
* @param {boolean} is_element
* @param {AST.RegularElement | null} element
* @param {ComponentContext} context
*/
export function process_children(nodes, initial, is_element, { visit, state }) {
export function process_children(nodes, initial, element, { visit, state }) {
const within_bound_contenteditable = state.metadata.bound_contenteditable;
let prev = initial;
let skipped = 0;
@ -61,16 +62,17 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
* @param {Sequence} sequence
*/
function flush_sequence(sequence) {
if (sequence.every((node) => node.type === 'Text')) {
const { has_state, has_call, value, can_inline } = build_template_chunk(sequence, visit, state);
if (can_inline) {
skipped += 1;
state.template.push(sequence.map((node) => node.raw).join(''));
const raw = element?.name === 'script' || element?.name === 'style';
state.template.push(raw ? value : escape_inline_expression(value, state.scope));
return;
}
state.template.push(' ');
const { has_state, has_call, value } = build_template_literal(sequence, visit, state);
// if this is a standalone `{expression}`, make sure we handle the case where
// no text node was created because the expression was empty during SSR
const is_text = sequence.length === 1;
@ -98,9 +100,9 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
let child_state = state;
if (is_static_element(node, state)) {
if (is_static_element(node)) {
skipped += 1;
} else if (node.type === 'EachBlock' && nodes.length === 1 && is_element) {
} else if (node.type === 'EachBlock' && nodes.length === 1 && element) {
node.metadata.is_controlled = true;
} else {
const id = flush_node(false, node.type === 'RegularElement' ? node.name : 'node');
@ -125,9 +127,8 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
/**
* @param {SvelteNode} node
* @param {ComponentContext["state"]} state
*/
function is_static_element(node, state) {
function is_static_element(node) {
if (node.type !== 'RegularElement') return false;
if (node.fragment.metadata.dynamic) return false;
if (node.name.includes('-')) return false; // we're setting all attributes on custom elements through properties
@ -154,16 +155,49 @@ function is_static_element(node, state) {
return false;
}
if (
attribute.value !== true &&
!is_text_attribute(attribute) &&
// If the attribute is not a text attribute but is inlinable we will directly inline it in the
// the template so before returning false we need to check that the attribute is not inlinable
!is_inlinable_expression(attribute.value, state)
) {
if (!attribute.metadata.expression.can_inline) {
return false;
}
}
return true;
}
/**
* @param {Expression} node
* @param {Scope} scope
* @returns {Expression}
*/
function escape_inline_expression(node, scope) {
if (node.type === 'Literal') {
if (typeof node.value === 'string') {
return b.literal(escape_html(node.value));
}
return node;
}
if (node.type === 'TemplateLiteral') {
return b.template(
node.quasis.map((q) => b.quasi(escape_html(q.value.cooked))),
node.expressions.map((expression) => escape_inline_expression(expression, scope))
);
}
/**
* If we can't determine the range of possible values statically, wrap in
* `$.escape(...)`. TODO expand this to cover more cases
*/
let needs_escape = true;
if (node.type === 'Identifier') {
const binding = scope.get(node.name);
// TODO handle more cases
if (binding?.initial?.type === 'Literal' && !binding.reassigned) {
needs_escape = escape_html(binding.initial.value) !== String(binding.initial.value);
}
}
return needs_escape ? b.call('$.escape', node) : node;
}

@ -1,4 +1,4 @@
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement, Super } from 'estree' */
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement, Super, TemplateLiteral, Node } from 'estree' */
/** @import { AST, SvelteNode } from '#compiler' */
/** @import { ComponentClientTransformState } from '../../types' */
import { walk } from 'zimmerframe';
@ -10,87 +10,91 @@ import { create_derived } from '../../utils.js';
import is_reference from 'is-reference';
import { locator } from '../../../../../state.js';
/**
* @param {Array<AST.Text | AST.ExpressionTag>} values
*/
export function get_states_and_calls(values) {
let states = 0;
let calls = 0;
for (let i = 0; i < values.length; i++) {
const node = values[i];
if (node.type === 'ExpressionTag') {
if (node.metadata.expression.has_call) {
calls++;
}
if (node.metadata.expression.has_state) {
states++;
}
}
}
return { states, calls };
}
/**
* @param {Array<AST.Text | AST.ExpressionTag>} values
* @param {(node: SvelteNode, state: any) => any} visit
* @param {ComponentClientTransformState} state
* @returns {{ value: Expression, has_state: boolean, has_call: boolean }}
* @returns {{ value: Expression, has_state: boolean, has_call: boolean, can_inline: boolean }}
*/
export function build_template_literal(values, visit, state) {
export function build_template_chunk(values, visit, state) {
/** @type {Expression[]} */
const expressions = [];
let quasi = b.quasi('');
const quasis = [quasi];
const { states, calls } = get_states_and_calls(values);
let has_call = false;
let has_state = false;
let can_inline = true;
let contains_multiple_call_expression = false;
for (const node of values) {
if (node.type === 'ExpressionTag') {
if (node.metadata.expression.has_call) {
if (has_call) contains_multiple_call_expression = true;
has_call = true;
}
if (node.metadata.expression.has_state) {
has_state = true;
}
let has_call = calls > 0;
let has_state = states > 0;
let contains_multiple_call_expression = calls > 1;
if (!node.metadata.expression.can_inline) {
can_inline = false;
}
}
}
for (let i = 0; i < values.length; i++) {
const node = values[i];
if (node.type === 'Text') {
quasi.value.cooked += node.data;
} else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') {
if (node.expression.value != null) {
quasi.value.cooked += node.expression.value + '';
} else {
const expression = /** @type {Expression} */ (visit(node.expression, state));
if (expression.type === 'Literal') {
if (expression.value != null) {
quasi.value.cooked += expression.value + '';
}
} else {
let value = expression;
// if we don't know the value, we need to add `?? ''` to replace
// `null` and `undefined` with the empty string
let needs_fallback = true;
if (value.type === 'Identifier') {
const binding = state.scope.get(value.name);
if (binding && binding.initial?.type === 'Literal' && !binding.reassigned) {
needs_fallback = binding.initial.value === null;
}
}
if (needs_fallback) {
value = b.logical('??', expression, b.literal(''));
}
if (contains_multiple_call_expression) {
const id = b.id(state.scope.generate('stringified_text'));
state.init.push(
b.const(
id,
create_derived(
state,
b.thunk(
b.logical(
'??',
/** @type {Expression} */ (visit(node.expression, state)),
b.literal('')
)
)
)
)
);
state.init.push(b.const(id, create_derived(state, b.thunk(value))));
expressions.push(b.call('$.get', id));
} else if (values.length === 1) {
// If we have a single expression, then pass that in directly to possibly avoid doing
// extra work in the template_effect (instead we do the work in set_text).
return { value: visit(node.expression, state), has_state, has_call };
return { value: visit(node.expression, state), has_state, has_call, can_inline };
} else {
expressions.push(b.logical('??', visit(node.expression, state), b.literal('')));
expressions.push(value);
}
quasi = b.quasi('', i + 1 === values.length);
quasis.push(quasi);
}
}
}
for (const quasi of quasis) {
quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
@ -98,7 +102,7 @@ export function build_template_literal(values, visit, state) {
const value = b.template(quasis, expressions);
return { value, has_state, has_call };
return { value, has_state, has_call, can_inline };
}
/**

@ -58,6 +58,7 @@ export function create_expression_metadata() {
return {
dependencies: new Set(),
has_state: false,
has_call: false
has_call: false,
can_inline: true
};
}

@ -317,6 +317,8 @@ export interface ExpressionMetadata {
has_state: boolean;
/** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */
has_call: boolean;
/** True if the expression can be inlined into a template */
can_inline: boolean;
}
export * from './template.js';

@ -1,4 +1,5 @@
export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js';
export { escape_html as escape } from '../../escaping.js';
export { cleanup_styles } from './dev/css.js';
export { add_locations } from './dev/elements.js';
export { hmr } from './dev/hmr.js';
@ -155,6 +156,7 @@ export {
$window as window,
$document as document
} from './dom/operations.js';
export { attr } from '../shared/attributes.js';
export { snapshot } from '../shared/clone.js';
export { noop, fallback } from '../shared/utils.js';
export {

@ -2,6 +2,7 @@
/** @import { Component, Payload, RenderOutput } from '#server' */
/** @import { Store } from '#shared' */
export { FILENAME, HMR } from '../../constants.js';
import { attr } from '../shared/attributes.js';
import { is_promise, noop } from '../shared/utils.js';
import { subscribe_to_store } from '../../store/utils.js';
import {
@ -153,33 +154,6 @@ export function head(payload, fn) {
head_payload.out += BLOCK_CLOSE;
}
/**
* `<div translate={false}>` should be rendered as `<div translate="no">` and _not_
* `<div translate="false">`, which is equivalent to `<div translate="yes">`. There
* may be other odd cases that need to be added to this list in future
* @type {Record<string, Map<any, string>>}
*/
const replacements = {
translate: new Map([
[true, 'yes'],
[false, 'no']
])
};
/**
* @template V
* @param {string} name
* @param {V} value
* @param {boolean} [is_boolean]
* @returns {string}
*/
export function attr(name, value, is_boolean = false) {
if (value == null || (!value && is_boolean) || (value === '' && name === 'class')) return '';
const normalized = (name in replacements && replacements[name].get(value)) || value;
const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`;
return ` ${name}${assignment}`;
}
/**
* @param {Payload} payload
* @param {boolean} is_html
@ -549,6 +523,8 @@ export function once(get_value) {
};
}
export { attr };
export { html } from './blocks/html.js';
export { push, pop } from './context.js';

@ -0,0 +1,28 @@
import { escape_html } from '../../escaping.js';
/**
* `<div translate={false}>` should be rendered as `<div translate="no">` and _not_
* `<div translate="false">`, which is equivalent to `<div translate="yes">`. There
* may be other odd cases that need to be added to this list in future
* @type {Record<string, Map<any, string>>}
*/
const replacements = {
translate: new Map([
[true, 'yes'],
[false, 'no']
])
};
/**
* @template V
* @param {string} name
* @param {V} value
* @param {boolean} [is_boolean]
* @returns {string}
*/
export function attr(name, value, is_boolean = false) {
if (value == null || (!value && is_boolean) || (value === '' && name === 'class')) return '';
const normalized = (name in replacements && replacements[name].get(value)) || value;
const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`;
return ` ${name}${assignment}`;
}

@ -1 +1 @@
<input READONLY={true} REQUIRED={false}>
<input READONLY={!0} REQUIRED={!1}>

@ -5,12 +5,13 @@ const __DECLARED_ASSET_0__ = "__VITE_ASSET__2AM7_y_a__ 1440w, __VITE_ASSET__2AM7
const __DECLARED_ASSET_1__ = "__VITE_ASSET__2AM7_y_c__ 1440w, __VITE_ASSET__2AM7_y_d__ 960w";
const __DECLARED_ASSET_2__ = "__VITE_ASSET__2AM7_y_e__ 1440w, __VITE_ASSET__2AM7_y_f__ 960w";
const __DECLARED_ASSET_3__ = "__VITE_ASSET__2AM7_y_g__";
var root = $.template(`<picture><source srcset="${__DECLARED_ASSET_0__}" type="image/avif"> <source srcset="${__DECLARED_ASSET_1__}" type="image/webp"> <source srcset="${__DECLARED_ASSET_2__}" type="image/png"> <img src="${__DECLARED_ASSET_3__}" alt="production test" width="1440" height="1440"></picture>`);
const a = 1;
const b = 2;
var root = $.template(`<picture><source srcset="${__DECLARED_ASSET_0__}" type="image/avif"> <source srcset="${__DECLARED_ASSET_1__}" type="image/webp"> <source srcset="${__DECLARED_ASSET_2__}" type="image/png"> <img src="${__DECLARED_ASSET_3__}" alt="production test" width="1440" height="1440"></picture> <p>${a} + ${b} = ${$.escape(a + b ?? "")}</p>`, 1);
export default function Inline_module_vars($$anchor) {
var picture = root();
var fragment = root();
var p = $.sibling($.first_child(fragment), 2);
$.next(6);
$.reset(picture);
$.append($$anchor, picture);
$.append($$anchor, fragment);
}

@ -4,7 +4,9 @@ const __DECLARED_ASSET_0__ = "__VITE_ASSET__2AM7_y_a__ 1440w, __VITE_ASSET__2AM7
const __DECLARED_ASSET_1__ = "__VITE_ASSET__2AM7_y_c__ 1440w, __VITE_ASSET__2AM7_y_d__ 960w";
const __DECLARED_ASSET_2__ = "__VITE_ASSET__2AM7_y_e__ 1440w, __VITE_ASSET__2AM7_y_f__ 960w";
const __DECLARED_ASSET_3__ = "__VITE_ASSET__2AM7_y_g__";
const a = 1;
const b = 2;
export default function Inline_module_vars($$payload) {
$$payload.out += `<picture><source${$.attr("srcset", __DECLARED_ASSET_0__)} type="image/avif"> <source${$.attr("srcset", __DECLARED_ASSET_1__)} type="image/webp"> <source${$.attr("srcset", __DECLARED_ASSET_2__)} type="image/png"> <img${$.attr("src", __DECLARED_ASSET_3__)} alt="production test" width="1440" height="1440"></picture>`;
$$payload.out += `<picture><source${$.attr("srcset", __DECLARED_ASSET_0__)} type="image/avif"> <source${$.attr("srcset", __DECLARED_ASSET_1__)} type="image/webp"> <source${$.attr("srcset", __DECLARED_ASSET_2__)} type="image/png"> <img${$.attr("src", __DECLARED_ASSET_3__)} alt="production test"${$.attr("width", 1440)}${$.attr("height", 1440)}></picture> <p>${$.escape(a)} + ${$.escape(b)} = ${$.escape(a + b)}</p>`;
}

@ -5,11 +5,16 @@
const __DECLARED_ASSET_1__ = "__VITE_ASSET__2AM7_y_c__ 1440w, __VITE_ASSET__2AM7_y_d__ 960w";
const __DECLARED_ASSET_2__ = "__VITE_ASSET__2AM7_y_e__ 1440w, __VITE_ASSET__2AM7_y_f__ 960w";
const __DECLARED_ASSET_3__ = "__VITE_ASSET__2AM7_y_g__";
const a = 1;
const b = 2;
</script>
<picture>
<source srcset={__DECLARED_ASSET_0__} type="image/avif" />
<source srcset={__DECLARED_ASSET_1__} type="image/webp" />
<source srcset={__DECLARED_ASSET_2__} type="image/png" />
<img src={__DECLARED_ASSET_3__} alt="production test" width=1440 height=1440 />
<img src={__DECLARED_ASSET_3__} alt="production test" width={1440} height={1440} />
</picture>
<p>{a} + {b} = {a + b}</p>

Loading…
Cancel
Save