chore: remove template expression inlining (#14374)

* chore: remove template expression inlining

* missed some

* fix

* feedback

* feedback

* Update packages/svelte/src/compiler/phases/3-transform/client/utils.js

Co-authored-by: Rich Harris <rich.harris@vercel.com>

* Update packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js

Co-authored-by: Rich Harris <rich.harris@vercel.com>

* Update packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js

Co-authored-by: Rich Harris <rich.harris@vercel.com>

* Update packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js

Co-authored-by: Rich Harris <rich.harris@vercel.com>

* Update packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js

Co-authored-by: Rich Harris <rich.harris@vercel.com>

* Update packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js

Co-authored-by: Rich Harris <rich.harris@vercel.com>

* fix

* Update .changeset/calm-mice-perform.md

Co-authored-by: Rich Harris <rich.harris@vercel.com>

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/14377/head
Dominic Gannaway 1 month ago committed by GitHub
parent f616c22053
commit 9d12fd1a01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: remove template expression inlining

@ -1,7 +1,7 @@
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */
/** @import { AST, DelegatedEvent, SvelteNode } from '#compiler' */
/** @import { Context } from '../types' */
import { is_boolean_attribute, is_capture_event, is_delegated } from '../../../../utils.js';
import { cannot_be_set_statically, is_capture_event, is_delegated } from '../../../../utils.js';
import {
get_attribute_chunks,
get_attribute_expression,
@ -30,12 +30,12 @@ export function Attribute(node, context) {
}
}
if (node.name.startsWith('on')) {
if (is_event_attribute(node)) {
mark_subtree_dynamic(context.path);
}
if (parent.type === 'RegularElement' && is_boolean_attribute(node.name.toLowerCase())) {
node.metadata.expression.can_inline = false;
if (cannot_be_set_statically(node.name)) {
mark_subtree_dynamic(context.path);
}
if (node.value !== true) {
@ -51,7 +51,6 @@ 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)) {

@ -179,8 +179,6 @@ 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;
mark_subtree_dynamic(context.path);
}
}
}

@ -2,6 +2,7 @@
/** @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
@ -14,5 +15,9 @@ 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,4 +1,5 @@
/** @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';
@ -19,6 +20,8 @@ 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' &&
@ -84,12 +87,6 @@ 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);
@ -125,12 +122,4 @@ export function Identifier(node, context) {
w.reactive_declaration_module_script_dependency(node);
}
}
if (!can_inline) {
if (context.state.expression) {
context.state.expression.can_inline = false;
}
mark_subtree_dynamic(context.path);
}
}

@ -20,9 +20,6 @@ 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;
mark_subtree_dynamic(context.path);
}
if (!is_safe_identifier(node, context.state.scope)) {

@ -1,6 +1,6 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { cannot_be_set_statically, is_mathml, is_svg, is_void } from '../../../../utils.js';
import { is_mathml, is_svg, is_void } from '../../../../utils.js';
import {
is_tag_valid_with_ancestor,
is_tag_valid_with_parent
@ -75,14 +75,6 @@ export function RegularElement(node, context) {
node.attributes.push(create_attribute('value', child.start, child.end, [child]));
}
if (
node.attributes.some(
(attribute) => attribute.type === 'Attribute' && cannot_be_set_statically(attribute.name)
)
) {
mark_subtree_dynamic(context.path);
}
const binding = context.state.scope.get(node.name);
if (
binding !== null &&

@ -10,7 +10,6 @@ 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') {

@ -3,16 +3,16 @@
/** @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_BINDABLE,
PROPS_IS_IMMUTABLE,
PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE,
PROPS_IS_RUNES,
PROPS_IS_UPDATED
PROPS_IS_UPDATED,
PROPS_IS_BINDABLE
} 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';
/**

@ -141,14 +141,14 @@ export function Fragment(node, context) {
const id = b.id(context.state.scope.generate('fragment'));
const use_space_template =
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') &&
trimmed.some((node) => node.type === 'ExpressionTag' && !node.metadata.expression.can_inline);
trimmed.some((node) => node.type === 'ExpressionTag') &&
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag');
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, null, {
process_children(trimmed, () => id, false, {
...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'), null, { ...context, state });
process_children(trimmed, () => b.id('$$anchor'), false, { ...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, null, { ...context, state });
process_children(trimmed, expression, false, { ...context, state });
let flags = TEMPLATE_FRAGMENT;

@ -1,9 +1,8 @@
/** @import { Expression, ExpressionStatement, Identifier, Literal, MemberExpression, ObjectExpression, Statement } from 'estree' */
/** @import { Expression, ExpressionStatement, Identifier, 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 {
cannot_be_set_statically,
is_boolean_attribute,
@ -11,6 +10,7 @@ import {
is_load_error_element,
is_void
} from '../../../../../utils.js';
import { escape_html } from '../../../../../escaping.js';
import { dev, is_ignored, locator } from '../../../../state.js';
import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
@ -18,13 +18,12 @@ 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 {
get_attribute_name,
build_attribute_value,
build_class_directives,
build_set_attributes,
build_style_directives,
get_attribute_name
build_set_attributes
} from './shared/element.js';
import { visit_event_attribute } from './shared/events.js';
import { process_children } from './shared/fragment.js';
import {
build_render_statement,
@ -32,6 +31,7 @@ import {
build_update,
build_update_assignment
} from './shared/utils.js';
import { visit_event_attribute } from './shared/events.js';
/**
* @param {AST.RegularElement} node
@ -352,32 +352,30 @@ 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 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.type === 'ExpressionTag') &&
trimmed.every((node) => node.type === 'Text' || !node.metadata.expression.has_state) &&
trimmed.some((node) => node.type === 'ExpressionTag' && !node.metadata.expression.can_inline);
trimmed.some((node) => node.type === 'ExpressionTag');
if (use_text_content) {
let { value } = build_template_chunk(trimmed, context.visit, child_state);
child_state.init.push(
b.stmt(b.assignment('=', b.member(context.state.node, 'textContent'), value))
b.stmt(
b.assignment(
'=',
b.member(context.state.node, 'textContent'),
build_template_chunk(trimmed, context.visit, child_state).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 (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)
);
// after the element has been hydrated
let needs_reset = trimmed.some((node) => node.type !== 'Text');
// The same applies if it's a `<template>` element, since we need to
// set the value of `hydrate_node` to `node.content`
@ -387,7 +385,7 @@ export function RegularElement(node, context) {
arg = b.member(arg, 'content');
}
process_children(trimmed, (is_text) => b.call('$.child', arg, is_text && b.true), node, {
process_children(trimmed, (is_text) => b.call('$.child', arg, is_text && b.true), true, {
...context,
state: child_state
});
@ -587,45 +585,11 @@ 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 {
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;
}
}
/**
* Like `build_element_attribute_update_assignment` but without any special attribute treatment.

@ -1,10 +1,8 @@
/** @import { Expression } from 'estree' */
/** @import { AST, SvelteNode } from '#compiler' */
/** @import { Scope } from '../../../../scope.js' */
/** @import { ComponentContext } from '../../types' */
import { escape_html } from '../../../../../../escaping.js';
import { cannot_be_set_statically } from '../../../../../../utils.js';
import { is_event_attribute } from '../../../../../utils/ast.js';
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { build_template_chunk, build_update } from './utils.js';
@ -14,10 +12,10 @@ import { build_template_chunk, build_update } from './utils.js';
* corresponding template node references these updates are applied to.
* @param {SvelteNode[]} nodes
* @param {(is_text: boolean) => Expression} initial
* @param {AST.RegularElement | null} element
* @param {boolean} is_element
* @param {ComponentContext} context
*/
export function process_children(nodes, initial, element, { visit, state }) {
export function process_children(nodes, initial, is_element, { visit, state }) {
const within_bound_contenteditable = state.metadata.bound_contenteditable;
let prev = initial;
let skipped = 0;
@ -63,17 +61,16 @@ export function process_children(nodes, initial, element, { visit, state }) {
* @param {Sequence} sequence
*/
function flush_sequence(sequence) {
const { has_state, has_call, value, can_inline } = build_template_chunk(sequence, visit, state);
if (can_inline) {
if (sequence.every((node) => node.type === 'Text')) {
skipped += 1;
const raw = element?.name === 'script' || element?.name === 'style';
state.template.push(raw ? value : escape_inline_expression(value, state.scope));
state.template.push(sequence.map((node) => node.raw).join(''));
return;
}
state.template.push(' ');
const { has_state, has_call, value } = build_template_chunk(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;
@ -101,9 +98,9 @@ export function process_children(nodes, initial, element, { visit, state }) {
let child_state = state;
if (is_static_element(node)) {
if (is_static_element(node, state)) {
skipped += 1;
} else if (node.type === 'EachBlock' && nodes.length === 1 && element) {
} else if (node.type === 'EachBlock' && nodes.length === 1 && is_element) {
node.metadata.is_controlled = true;
} else {
const id = flush_node(false, node.type === 'RegularElement' ? node.name : 'node');
@ -128,8 +125,9 @@ export function process_children(nodes, initial, element, { visit, state }) {
/**
* @param {SvelteNode} node
* @param {ComponentContext["state"]} state
*/
function is_static_element(node) {
function is_static_element(node, state) {
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
@ -156,49 +154,10 @@ function is_static_element(node) {
return false;
}
if (!attribute.metadata.expression.can_inline) {
if (attribute.value !== true && !is_text_attribute(attribute)) {
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, TemplateLiteral, Node } from 'estree' */
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement, Super } from 'estree' */
/** @import { AST, SvelteNode } from '#compiler' */
/** @import { ComponentClientTransformState } from '../../types' */
import { walk } from 'zimmerframe';
@ -14,7 +14,7 @@ import { locator } from '../../../../../state.js';
* @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, can_inline: boolean }}
* @returns {{ value: Expression, has_state: boolean, has_call: boolean }}
*/
export function build_template_chunk(values, visit, state) {
/** @type {Expression[]} */
@ -25,23 +25,15 @@ export function build_template_chunk(values, visit, state) {
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;
}
const metadata = node.metadata.expression;
if (!node.metadata.expression.can_inline) {
can_inline = false;
}
contains_multiple_call_expression ||= has_call && metadata.has_call;
has_call ||= metadata.has_call;
has_state ||= metadata.has_state;
}
}
@ -50,51 +42,41 @@ export function build_template_chunk(values, visit, state) {
if (node.type === 'Text') {
quasi.value.cooked += node.data;
} else {
const expression = /** @type {Expression} */ (visit(node.expression, state));
if (expression.type === 'Literal') {
if (expression.value != null) {
quasi.value.cooked += expression.value + '';
} else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') {
if (node.expression.value != null) {
quasi.value.cooked += node.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(value))));
state.init.push(
b.const(
id,
create_derived(
state,
b.thunk(
b.logical(
'??',
/** @type {Expression} */ (visit(node.expression, state)),
b.literal('')
)
)
)
)
);
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, can_inline };
return { value: visit(node.expression, state), has_state, has_call };
} else {
expressions.push(value);
expressions.push(b.logical('??', visit(node.expression, state), b.literal('')));
}
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));
@ -102,7 +84,7 @@ export function build_template_chunk(values, visit, state) {
const value = b.template(quasis, expressions);
return { value, has_state, has_call, can_inline };
return { value, has_state, has_call };
}
/**

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

@ -317,8 +317,6 @@ 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,5 +1,4 @@
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';

@ -1,17 +0,0 @@
import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
const __DECLARED_ASSET_0__ = "__VITE_ASSET__2AM7_y_a__ 1440w, __VITE_ASSET__2AM7_y_b__ 960w";
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;
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 fragment = root();
$.next(2);
$.append($$anchor, fragment);
}

@ -1,12 +0,0 @@
import * as $ from "svelte/internal/server";
const __DECLARED_ASSET_0__ = "__VITE_ASSET__2AM7_y_a__ 1440w, __VITE_ASSET__2AM7_y_b__ 960w";
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"${$.attr("width", 1440)}${$.attr("height", 1440)}></picture> <p>${$.escape(a)} + ${$.escape(b)} = ${$.escape(a + b)}</p>`;
}

@ -1,20 +0,0 @@
<svelte:options runes={true} />
<script module>
const __DECLARED_ASSET_0__ = "__VITE_ASSET__2AM7_y_a__ 1440w, __VITE_ASSET__2AM7_y_b__ 960w";
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} />
</picture>
<p>{a} + {b} = {a + b}</p>
Loading…
Cancel
Save