pull/18320/head
Rich Harris 2 months ago
parent 525550b0a5
commit f184ceb6fe

@ -1,4 +1,4 @@
/** @import { Expression, Identifier } from 'estree' */
/** @import { TemplateLiteral } from 'estree' */
/** @import { Namespace } from '#compiler' */
/** @import { ComponentClientTransformState } from '../types.js' */
/** @import { Node } from './types.js' */
@ -32,14 +32,29 @@ function build_locations(nodes) {
/**
* @param {ComponentClientTransformState} state
* @param {string} name
* @param {Namespace} namespace
* @param {number} [flags]
*/
export function transform_template(state, namespace, flags = 0) {
export function transform_template(state, name, namespace, flags = 0) {
const tree = state.options.fragments === 'tree';
const expression = tree ? state.template.as_tree() : state.template.as_html();
const key =
tree || dev
? null
: get_template_key(
/** @type {TemplateLiteral} */ (expression),
state.metadata.namespace,
flags
);
if (key !== null) {
const existing = state.templates.get(key);
if (existing !== undefined) return existing;
}
if (tree) {
if (namespace === 'svg') flags |= TEMPLATE_USE_SVG;
if (namespace === 'mathml') flags |= TEMPLATE_USE_MATHML;
@ -64,27 +79,8 @@ export function transform_template(state, namespace, flags = 0) {
);
}
return call;
}
/**
* Hoists a template factory (`$.from_html(...)` etc.) to module scope, reusing an
* existing identical template within the same component rather than emitting a duplicate.
* @param {ComponentClientTransformState} state
* @param {string} name - the base name for a newly hoisted template
* @param {Expression} template
* @returns {Identifier}
*/
export function hoist_template(state, name, template) {
const key = get_template_key(template);
if (key !== null) {
const existing = state.templates.get(key);
if (existing !== undefined) return existing;
}
const id = state.scope.root.unique(name);
state.hoisted.push(b.var(id, template));
state.hoisted.push(b.var(id, call));
if (key !== null) {
state.templates.set(key, id);
@ -98,33 +94,11 @@ export function hoist_template(state, name, template) {
* `$.from_html`/`from_svg`/`from_mathml` factories with literal arguments - or `null`
* for anything else. Dev-mode templates are wrapped in `$.add_locations(...)`, which
* embeds per-call-site locations, so they never produce a key and are never shared.
* @param {Expression} template
* @param {TemplateLiteral} template
* @param {Namespace} namespace
* @param {number} flags
* @returns {string | null}
*/
function get_template_key(template) {
if (template.type !== 'CallExpression' || template.callee.type !== 'Identifier') {
return null;
}
const name = template.callee.name;
if (name !== '$.from_html' && name !== '$.from_svg' && name !== '$.from_mathml') {
return null;
}
const [content, flags] = template.arguments;
if (
content?.type !== 'TemplateLiteral' ||
content.expressions.length !== 0 ||
content.quasis.length !== 1
) {
return null;
}
if (flags !== undefined && (flags.type !== 'Literal' || typeof flags.value !== 'number')) {
return null;
}
return `${name} ${flags ? flags.value : 0} ${content.quasis[0].value.raw}`;
function get_template_key(template, namespace, flags) {
return `${namespace} ${flags} ${template.quasis[0].value.raw}`;
}

@ -4,7 +4,7 @@
import * as b from '#compiler/builders';
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../../../constants.js';
import { clean_nodes, infer_namespace } from '../../utils.js';
import { hoist_template, transform_template } from '../transform-template/index.js';
import { transform_template } from '../transform-template/index.js';
import { Template } from '../transform-template/template.js';
import { process_children } from './shared/fragment.js';
import { build_render_statement, Memoizer } from './shared/utils.js';
@ -95,8 +95,7 @@ export function Fragment(node, context) {
let flags = state.template.needs_import_node ? TEMPLATE_USE_IMPORT_NODE : undefined;
const template = transform_template(state, namespace, flags);
const template_name = hoist_template(state, 'root', template);
const template_name = transform_template(state, 'root', namespace, flags);
state.init.unshift(b.var(id, b.call(template_name)));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
@ -146,8 +145,7 @@ export function Fragment(node, context) {
// special case — we can use `$.comment` instead of creating a unique template
state.init.unshift(b.var(id, b.call('$.comment')));
} else {
const template = transform_template(state, namespace, flags);
const template_name = hoist_template(state, 'root', template);
const template_name = transform_template(state, 'root', namespace, flags);
state.init.unshift(b.var(id, b.call(template_name)));
}

@ -30,7 +30,7 @@ import { process_children, is_static_element } from './shared/fragment.js';
import { build_render_statement, build_template_chunk, Memoizer } from './shared/utils.js';
import { visit_event_attribute } from './shared/events.js';
import { Template } from '../transform-template/template.js';
import { hoist_template, transform_template } from '../transform-template/index.js';
import { transform_template } from '../transform-template/index.js';
import { TEMPLATE_FRAGMENT } from '../../../../../constants.js';
/**
@ -398,8 +398,12 @@ export function RegularElement(node, context) {
);
// Transform the template to $.from_html(...) and hoist it (deduplicating identical templates)
const template = transform_template(select_state, metadata.namespace, TEMPLATE_FRAGMENT);
const template_name = hoist_template(context.state, `${name}_content`, template);
const template_name = transform_template(
select_state,
`${name}_content`,
metadata.namespace,
TEMPLATE_FRAGMENT
);
// Build the rich content function body
// The anchor is the child of the element (a hydration marker during hydration)

Loading…
Cancel
Save