Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/merge-sibling-templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@solidjs/babel-plugin": patch
"@solidjs/compiler": patch
"@solidjs/web": patch
---

Merge adjacent fully static DOM roots in JSX fragments into a single template clone.
6 changes: 5 additions & 1 deletion packages/babel-plugin/src/dom/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ export function appendTemplates(path: NodePath<t.Program>, templates: TemplateRe
raw: escapeStringForTemplate(templateText)
};

const flag = template.isWrapped ? 2 : template.isImportNode ? 1 : null;
const flag =
(template.isImportNode ? 1 : 0) |
(template.isWrapped ? 2 : 0) |
(template.isMultiRoot ? 4 : 0);

return t.variableDeclarator(
template.id,
Expand Down Expand Up @@ -121,6 +124,7 @@ function registerTemplate(path: NodePath, results: TransformResult) {
templateWithClosingTags: results.templateWithClosingTags as string,
isImportNode: results.isImportNode,
isWrapped: results.isWrapped,
isMultiRoot: results.isMultiRoot,
renderer: "dom",
// templates dedupe on markup, so the FIRST site carries the blame
// for a validate failure (#3099) — good enough: every site with
Expand Down
103 changes: 83 additions & 20 deletions packages/babel-plugin/src/shared/fragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,99 @@ import * as t from "@babel/types";
import { decode } from "html-entities";
import { filterChildren, trimWhitespace, checkLength } from "./utils";
import { transformNode, getCreateTemplate } from "./transform";
import { VoidElements } from "../../../web/src/constants.js";
import type { NodePath } from "@babel/traverse";
import type { PluginConfig } from "../config";
import type { JSXNode, TransformResult } from "../types";

type FragmentTemplate = {
path: NodePath<JSXNode>;
result: TransformResult;
};

function isMergeableStaticDOMTemplate(result: TransformResult, config: PluginConfig) {
return (
!config.hydratable &&
result.renderer === "dom" &&
!!result.id &&
!!result.tagName &&
typeof result.template === "string" &&
!result.skipTemplate &&
!result.isWrapped &&
result.declarations.length === 0 &&
result.exprs.length === 0 &&
result.dynamics.length === 0 &&
!result.postExprs?.length
);
}

function closeRootTemplate(result: TransformResult) {
const template = result.template as string;
const close = `</${result.tagName}>`;
return VoidElements.has(result.tagName!) || template.endsWith(close)
? template
: template + close;
}

function createFragmentTemplate(templates: FragmentTemplate[], config: PluginConfig): t.Expression {
if (templates.length === 1) {
const { path, result } = templates[0];
return getCreateTemplate(config, path, result)(path, result, true) as t.Expression;
}

const first = templates[0];
const result: TransformResult = {
template: templates.map(({ result }) => closeRootTemplate(result)).join(""),
templateWithClosingTags: templates
.map(({ result }) => result.templateWithClosingTags || result.template)
.join(""),
declarations: [],
exprs: [],
dynamics: [],
postExprs: [],
id: first.result.id,
tagName: first.result.tagName,
renderer: "dom",
isImportNode: templates.some(({ result }) => result.isImportNode),
isMultiRoot: true
};
return getCreateTemplate(config, first.path, result)(first.path, result, true) as t.Expression;
}

export default function transformFragmentChildren(
children: NodePath<JSXNode>[],
results: TransformResult,
config: PluginConfig
) {
const filteredChildren = filterChildren(children),
childNodes = filteredChildren.reduce((memo: t.Expression[], path: NodePath<JSXNode>) => {
if (t.isJSXText(path.node)) {
const v = decode(trimWhitespace((path.node.extra?.raw as string | undefined) ?? ""));
if (v.length) memo.push(t.stringLiteral(v));
} else {
const child = transformNode(path, {
topLevel: true,
fragmentChild: true,
lastElement: true
});
if (child)
memo.push(
getCreateTemplate(config, path, child as TransformResult)(
path,
child as TransformResult,
true
) as t.Expression
);
childNodes: t.Expression[] = [];
let templates: FragmentTemplate[] = [];
const flushTemplates = () => {
if (!templates.length) return;
childNodes.push(createFragmentTemplate(templates, config));
templates = [];
};

filteredChildren.forEach((path: NodePath<JSXNode>) => {
if (t.isJSXText(path.node)) {
flushTemplates();
const v = decode(trimWhitespace((path.node.extra?.raw as string | undefined) ?? ""));
if (v.length) childNodes.push(t.stringLiteral(v));
} else {
const child = transformNode(path, {
topLevel: true,
fragmentChild: true,
lastElement: true
});
if (!child) return;
if (isMergeableStaticDOMTemplate(child, config)) {
templates.push({ path, result: child });
return;
}
return memo;
}, []);
flushTemplates();
childNodes.push(getCreateTemplate(config, path, child)(path, child, true) as t.Expression);
}
});
flushTemplates();
results.exprs.push(childNodes.length === 1 ? childNodes[0] : t.arrayExpression(childNodes));
}
2 changes: 2 additions & 0 deletions packages/babel-plugin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface TemplateRecord {
templateWithClosingTags?: string | t.Expression | t.ArrayExpression;
isImportNode?: boolean;
isWrapped?: boolean;
isMultiRoot?: boolean;
renderer: RendererName;
/** First registration site, so `validate` failures point at the JSX (#3099). */
path?: NodePath;
Expand Down Expand Up @@ -76,6 +77,7 @@ export interface TransformResult {
renderer?: RendererName;
isImportNode?: boolean;
isWrapped?: boolean;
isMultiRoot?: boolean;
skipTemplate?: boolean;
templateWithClosingTags?: string;
children?: TransformResult[];
Expand Down
31 changes: 31 additions & 0 deletions packages/babel-plugin/test/__dom_fixtures__/fragments/code.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,34 @@ const multiLineTrailing = (
<span>3</span>
</>
);

const groupedAroundExpression = (
<>
<header>First</header>
<div>Second</div>
{inserted}
<span>Third</span>
<footer>Fourth</footer>
</>
);

const adjacentDynamicRoots = (
<>
<div id={state.first}>First</div>
<div id={state.last}>Last</div>
</>
);

const adjacentVoidRoots = (
<>
<input />
<img />
</>
);

const adjacentImportRoots = (
<>
<img loading="lazy" />
<iframe loading="lazy" />
</>
);
67 changes: 49 additions & 18 deletions packages/babel-plugin/test/__dom_fixtures__/fragments/output.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,23 @@ import { createComponent as _$createComponent } from "r-dom";
import { memo as _$memo } from "r-dom";
import { setAttribute as _$setAttribute } from "r-dom";
import { effect as _$effect } from "r-dom";
var _tmpl$ = /*#__PURE__*/ _$template(`<div>First`),
_tmpl$2 = /*#__PURE__*/ _$template(`<div>Last`),
_tmpl$3 = /*#__PURE__*/ _$template(`<div>`),
_tmpl$4 = /*#__PURE__*/ _$template(`<span>1`),
_tmpl$5 = /*#__PURE__*/ _$template(`<span>2`),
_tmpl$6 = /*#__PURE__*/ _$template(`<span>3`);
const multiStatic = [_tmpl$(), _tmpl$2()];
const multiExpression = [_tmpl$(), inserted, _tmpl$2(), "After"];
var _tmpl$ = /*#__PURE__*/ _$template(`<div>First</div><div>Last</div>`, 4),
_tmpl$2 = /*#__PURE__*/ _$template(`<div>First`),
_tmpl$3 = /*#__PURE__*/ _$template(`<div>Last`),
_tmpl$4 = /*#__PURE__*/ _$template(`<div>`),
_tmpl$5 = /*#__PURE__*/ _$template(`<span>1`),
_tmpl$6 = /*#__PURE__*/ _$template(`<span>2`),
_tmpl$7 = /*#__PURE__*/ _$template(`<span>3`),
_tmpl$8 = /*#__PURE__*/ _$template(`<span>1</span><span>2</span><span>3</span>`, 4),
_tmpl$9 = /*#__PURE__*/ _$template(`<header>First</header><div>Second</div>`, 4),
_tmpl$0 = /*#__PURE__*/ _$template(`<span>Third</span><footer>Fourth</footer>`, 4),
_tmpl$1 = /*#__PURE__*/ _$template(`<input><img>`, 4),
_tmpl$10 = /*#__PURE__*/ _$template(`<img loading=lazy><iframe loading=lazy></iframe>`, 5);
const multiStatic = _tmpl$();
const multiExpression = [_tmpl$2(), inserted, _tmpl$3(), "After"];
const multiDynamic = [
(() => {
var _el$5 = _tmpl$();
var _el$5 = _tmpl$2();
_$effect(
() => state.first,
_v$ => {
Expand All @@ -24,7 +30,7 @@ const multiDynamic = [
})(),
_$memo(() => state.inserted),
(() => {
var _el$6 = _tmpl$2();
var _el$6 = _tmpl$3();
_$effect(
() => state.last,
_v$ => {
Expand All @@ -37,11 +43,36 @@ const multiDynamic = [
];
const singleExpression = inserted;
const singleDynamic = _$memo(inserted);
const firstStatic = [inserted, _tmpl$3()];
const firstDynamic = [_$memo(inserted), _tmpl$3()];
const firstComponent = [_$createComponent(Component, {}), _tmpl$3()];
const lastStatic = [_tmpl$3(), inserted];
const lastDynamic = [_tmpl$3(), _$memo(inserted)];
const lastComponent = [_tmpl$3(), _$createComponent(Component, {})];
const spaces = [_tmpl$4(), " ", _tmpl$5(), " ", _tmpl$6()];
const multiLineTrailing = [_tmpl$4(), _tmpl$5(), _tmpl$6()];
const firstStatic = [inserted, _tmpl$4()];
const firstDynamic = [_$memo(inserted), _tmpl$4()];
const firstComponent = [_$createComponent(Component, {}), _tmpl$4()];
const lastStatic = [_tmpl$4(), inserted];
const lastDynamic = [_tmpl$4(), _$memo(inserted)];
const lastComponent = [_tmpl$4(), _$createComponent(Component, {})];
const spaces = [_tmpl$5(), " ", _tmpl$6(), " ", _tmpl$7()];
const multiLineTrailing = _tmpl$8();
const groupedAroundExpression = [_tmpl$9(), inserted, _tmpl$0()];
const adjacentDynamicRoots = [
(() => {
var _el$21 = _tmpl$2();
_$effect(
() => state.first,
_v$ => {
_$setAttribute(_el$21, "id", _v$);
}
);
return _el$21;
})(),
(() => {
var _el$22 = _tmpl$3();
_$effect(
() => state.last,
_v$ => {
_$setAttribute(_el$22, "id", _v$);
}
);
return _el$22;
})()
];
const adjacentVoidRoots = _tmpl$1();
const adjacentImportRoots = _tmpl$10();
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
import { template as _$template } from "r-dom";
import { createComponent as _$createComponent } from "r-dom";
import { setAttribute as _$setAttribute } from "r-dom";
var _tmpl$ = /*#__PURE__*/ _$template(`<div>First`),
_tmpl$2 = /*#__PURE__*/ _$template(`<div>Last`),
_tmpl$3 = /*#__PURE__*/ _$template(`<div>`);
const multiStatic = [_tmpl$(), _tmpl$2()];
const multiExpression = [_tmpl$(), inserted, _tmpl$2(), "After"];
var _tmpl$ = /*#__PURE__*/ _$template(`<div>First</div><div>Last</div>`, 4),
_tmpl$2 = /*#__PURE__*/ _$template(`<div>First`),
_tmpl$3 = /*#__PURE__*/ _$template(`<div>Last`),
_tmpl$4 = /*#__PURE__*/ _$template(`<div>`);
const multiStatic = _tmpl$();
const multiExpression = [_tmpl$2(), inserted, _tmpl$3(), "After"];
const multiDynamic = [
(() => {
var _el$5 = _tmpl$();
var _el$5 = _tmpl$2();
_$setAttribute(_el$5, "id", state.first);
return _el$5;
})(),
() => state.inserted,
(() => {
var _el$6 = _tmpl$2();
var _el$6 = _tmpl$3();
_$setAttribute(_el$6, "id", state.last);
return _el$6;
})(),
"After"
];
const singleExpression = inserted;
const singleDynamic = inserted;
const firstStatic = [inserted, _tmpl$3()];
const firstDynamic = [inserted, _tmpl$3()];
const firstComponent = [_$createComponent(Component, {}), _tmpl$3()];
const lastStatic = [_tmpl$3(), inserted];
const lastDynamic = [_tmpl$3(), inserted];
const lastComponent = [_tmpl$3(), _$createComponent(Component, {})];
const firstStatic = [inserted, _tmpl$4()];
const firstDynamic = [inserted, _tmpl$4()];
const firstComponent = [_$createComponent(Component, {}), _tmpl$4()];
const lastStatic = [_tmpl$4(), inserted];
const lastDynamic = [_tmpl$4(), inserted];
const lastComponent = [_tmpl$4(), _$createComponent(Component, {})];
38 changes: 20 additions & 18 deletions packages/babel-plugin/test/__dynamic_fixtures__/fragments/output.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@ import { createComponent as _$createComponent } from "r-custom";
import { memo as _$memo } from "r-custom";
import { setAttribute as _$setAttribute } from "r-dom";
import { effect as _$effect } from "r-custom";
var _tmpl$ = /*#__PURE__*/ _$template(`<div>First`),
_tmpl$2 = /*#__PURE__*/ _$template(`<div>Last`),
_tmpl$3 = /*#__PURE__*/ _$template(`<div>`),
_tmpl$4 = /*#__PURE__*/ _$template(`<span>1`),
_tmpl$5 = /*#__PURE__*/ _$template(`<span>2`),
_tmpl$6 = /*#__PURE__*/ _$template(`<span>3`);
const multiStatic = [_tmpl$(), _tmpl$2()];
const multiExpression = [_tmpl$(), inserted, _tmpl$2(), "After"];
var _tmpl$ = /*#__PURE__*/ _$template(`<div>First</div><div>Last</div>`, 4),
_tmpl$2 = /*#__PURE__*/ _$template(`<div>First`),
_tmpl$3 = /*#__PURE__*/ _$template(`<div>Last`),
_tmpl$4 = /*#__PURE__*/ _$template(`<div>`),
_tmpl$5 = /*#__PURE__*/ _$template(`<span>1`),
_tmpl$6 = /*#__PURE__*/ _$template(`<span>2`),
_tmpl$7 = /*#__PURE__*/ _$template(`<span>3`),
_tmpl$8 = /*#__PURE__*/ _$template(`<span>1</span><span>2</span><span>3</span>`, 4);
const multiStatic = _tmpl$();
const multiExpression = [_tmpl$2(), inserted, _tmpl$3(), "After"];
const multiDynamic = [
(() => {
var _el$5 = _tmpl$();
var _el$5 = _tmpl$2();
_$effect(
() => state.first,
_v$ => {
Expand All @@ -24,7 +26,7 @@ const multiDynamic = [
})(),
_$memo(() => state.inserted),
(() => {
var _el$6 = _tmpl$2();
var _el$6 = _tmpl$3();
_$effect(
() => state.last,
_v$ => {
Expand All @@ -39,11 +41,11 @@ const singleExpression = inserted;
const singleDynamic = _$memo(inserted);
const greeting = x => "Hello " + x;
const singleTemplateLiteral = _$memo(() => greeting`world`);
const firstStatic = [inserted, _tmpl$3()];
const firstDynamic = [_$memo(inserted), _tmpl$3()];
const firstComponent = [_$createComponent(Component, {}), _tmpl$3()];
const lastStatic = [_tmpl$3(), inserted];
const lastDynamic = [_tmpl$3(), _$memo(inserted)];
const lastComponent = [_tmpl$3(), _$createComponent(Component, {})];
const spaces = [_tmpl$4(), " ", _tmpl$5(), " ", _tmpl$6()];
const multiLineTrailing = [_tmpl$4(), _tmpl$5(), _tmpl$6()];
const firstStatic = [inserted, _tmpl$4()];
const firstDynamic = [_$memo(inserted), _tmpl$4()];
const firstComponent = [_$createComponent(Component, {}), _tmpl$4()];
const lastStatic = [_tmpl$4(), inserted];
const lastDynamic = [_tmpl$4(), _$memo(inserted)];
const lastComponent = [_tmpl$4(), _$createComponent(Component, {})];
const spaces = [_tmpl$5(), " ", _tmpl$6(), " ", _tmpl$7()];
const multiLineTrailing = _tmpl$8();
Loading
Loading