Skip to content

Commit de8a38b

Browse files
committed
feat: templateless template generation
1 parent 0af6f20 commit de8a38b

File tree

18 files changed

+265
-38
lines changed

18 files changed

+265
-38
lines changed

.changeset/smart-boats-accept.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: templateless template generation
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* @import { TemplateOperations } from "../types.js"
3+
*/
4+
import { template_to_string } from './to-string';
5+
6+
/**
7+
* @param {TemplateOperations} items
8+
*/
9+
export function transform_template(items) {
10+
// here we will check if we need to use `$.template` or create a series of `document.createElement` calls
11+
return template_to_string(items);
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/**
2+
* @import { TemplateOperations } from "../types.js"
3+
*/
4+
import { is_void } from '../../../../../utils.js';
5+
6+
/**
7+
* @param {TemplateOperations} items
8+
*/
9+
export function template_to_string(items) {
10+
let elements = [];
11+
12+
/**
13+
* @type {Array<Element>}
14+
*/
15+
let elements_stack = [];
16+
17+
/**
18+
* @type {Element | undefined}
19+
*/
20+
let last_current_element;
21+
22+
for (let instruction of items) {
23+
if (instruction.kind === 'push_element' && last_current_element) {
24+
elements_stack.push(last_current_element);
25+
continue;
26+
}
27+
if (instruction.kind === 'pop_element') {
28+
elements_stack.pop();
29+
continue;
30+
}
31+
/**
32+
* @type {Node | void}
33+
*/
34+
// @ts-expect-error we can't be here if `swap_current_element` but TS doesn't know that
35+
const value = map[instruction.kind](
36+
...[
37+
...(instruction.kind === 'set_prop' ? [last_current_element] : []),
38+
...(instruction.args ?? [])
39+
]
40+
);
41+
if (instruction.kind !== 'set_prop') {
42+
if (elements_stack.length >= 1 && value) {
43+
map.insert(/** @type {Element} */ (elements_stack.at(-1)), value);
44+
} else if (value) {
45+
elements.push(value);
46+
}
47+
if (instruction.kind === 'create_element') {
48+
last_current_element = /** @type {Element} */ (value);
49+
}
50+
}
51+
}
52+
53+
return elements.map((el) => stringify(el)).join('');
54+
}
55+
56+
/**
57+
* @typedef {{ kind: "element", element: string, props?: Record<string, string>, children?: Array<Node> }} Element
58+
*/
59+
60+
/**
61+
* @typedef {{ kind: "anchor", data?: string }} Anchor
62+
*/
63+
64+
/**
65+
* @typedef {{ kind: "text", value?: string }} Text
66+
*/
67+
68+
/**
69+
* @typedef { Element | Anchor| Text } Node
70+
*/
71+
72+
/**
73+
*
74+
* @param {string} element
75+
* @returns {Element}
76+
*/
77+
function create_element(element) {
78+
return {
79+
kind: 'element',
80+
element
81+
};
82+
}
83+
84+
/**
85+
* @param {string} data
86+
* @returns {Anchor}
87+
*/
88+
function create_anchor(data) {
89+
return {
90+
kind: 'anchor',
91+
data
92+
};
93+
}
94+
95+
/**
96+
* @param {string} value
97+
* @returns {Text}
98+
*/
99+
function create_text(value) {
100+
return {
101+
kind: 'text',
102+
value
103+
};
104+
}
105+
106+
/**
107+
*
108+
* @param {Element} el
109+
* @param {string} prop
110+
* @param {string} value
111+
*/
112+
function set_prop(el, prop, value) {
113+
el.props ??= {};
114+
el.props[prop] = value;
115+
}
116+
117+
/**
118+
*
119+
* @param {Element} el
120+
* @param {Node} child
121+
* @param {Node} [anchor]
122+
*/
123+
function insert(el, child, anchor) {
124+
el.children ??= [];
125+
el.children.push(child);
126+
}
127+
128+
let map = {
129+
create_element,
130+
create_text,
131+
create_anchor,
132+
set_prop,
133+
insert
134+
};
135+
136+
/**
137+
*
138+
* @param {Node} el
139+
* @returns
140+
*/
141+
function stringify(el) {
142+
let str = ``;
143+
if (el.kind === 'element') {
144+
str += `<${el.element}`;
145+
for (let [prop, value] of Object.entries(el.props ?? {})) {
146+
if (value == null) {
147+
str += ` ${prop}`;
148+
} else {
149+
str += ` ${prop}="${value}"`;
150+
}
151+
}
152+
str += `>`;
153+
for (let child of el.children ?? []) {
154+
str += stringify(child);
155+
}
156+
if (!is_void(el.element)) {
157+
str += `</${el.element}>`;
158+
}
159+
} else if (el.kind === 'text') {
160+
str += el.value;
161+
} else if (el.kind === 'anchor') {
162+
if (el.data) {
163+
str += `<!--${el.data}-->`;
164+
} else {
165+
str += `<!>`;
166+
}
167+
}
168+
169+
return str;
170+
}

packages/svelte/src/compiler/phases/3-transform/client/types.d.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ export interface ClientTransformState extends TransformState {
3939
>;
4040
}
4141

42+
type TemplateOperationsKind =
43+
| 'create_element'
44+
| 'create_text'
45+
| 'create_anchor'
46+
| 'set_prop'
47+
| 'push_element'
48+
| 'pop_element';
49+
50+
type TemplateOperations = Array<{
51+
kind: TemplateOperationsKind;
52+
args?: Array<string>;
53+
}>;
4254
export interface ComponentClientTransformState extends ClientTransformState {
4355
readonly analysis: ComponentAnalysis;
4456
readonly options: ValidatedCompileOptions;
@@ -56,7 +68,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
5668
/** Expressions used inside the render effect */
5769
readonly expressions: Expression[];
5870
/** The HTML template string */
59-
readonly template: Array<string | Expression>;
71+
readonly template: TemplateOperations;
6072
readonly locations: SourceLocation[];
6173
readonly metadata: {
6274
namespace: Namespace;

packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { get_value } from './shared/declarations.js';
1111
* @param {ComponentContext} context
1212
*/
1313
export function AwaitBlock(node, context) {
14-
context.state.template.push('<!>');
14+
context.state.template.push({ kind: 'create_anchor' });
1515

1616
// Visit {#await <expression>} first to ensure that scopes are in the correct order
1717
const expression = b.thunk(/** @type {Expression} */ (context.visit(node.expression)));

packages/svelte/src/compiler/phases/3-transform/client/visitors/Comment.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
*/
88
export function Comment(node, context) {
99
// We'll only get here if comments are not filtered out, which they are unless preserveComments is true
10-
context.state.template.push(`<!--${node.data}-->`);
10+
context.state.template.push({ kind: 'create_anchor', args: [node.data] });
1111
}

packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function EachBlock(node, context) {
3232
);
3333

3434
if (!each_node_meta.is_controlled) {
35-
context.state.template.push('<!>');
35+
context.state.template.push({ kind: 'create_anchor' });
3636
}
3737

3838
let flags = 0;

packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { dev } from '../../../../state.js';
77
import * as b from '../../../../utils/builders.js';
88
import { sanitize_template_string } from '../../../../utils/sanitize_template_string.js';
99
import { clean_nodes, infer_namespace } from '../../utils.js';
10+
import { transform_template } from '../transform-template/index.js';
1011
import { process_children } from './shared/fragment.js';
1112
import { build_render_statement } from './shared/utils.js';
1213

@@ -118,7 +119,7 @@ export function Fragment(node, context) {
118119
});
119120

120121
/** @type {Expression[]} */
121-
const args = [join_template(state.template)];
122+
const args = [b.template([b.quasi(transform_template(state.template), true)], [])];
122123

123124
if (state.metadata.context.template_needs_import_node) {
124125
args.push(b.literal(TEMPLATE_USE_IMPORT_NODE));
@@ -168,11 +169,14 @@ export function Fragment(node, context) {
168169
flags |= TEMPLATE_USE_IMPORT_NODE;
169170
}
170171

171-
if (state.template.length === 1 && state.template[0] === '<!>') {
172+
if (state.template.length === 1 && state.template[0].kind === 'create_anchor') {
172173
// special case — we can use `$.comment` instead of creating a unique template
173174
body.push(b.var(id, b.call('$.comment')));
174175
} else {
175-
add_template(template_name, [join_template(state.template), b.literal(flags)]);
176+
add_template(template_name, [
177+
b.template([b.quasi(transform_template(state.template), true)], []),
178+
b.literal(flags)
179+
]);
176180

177181
body.push(b.var(id, b.call(template_name)));
178182
}

packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as b from '../../../../utils/builders.js';
99
* @param {ComponentContext} context
1010
*/
1111
export function HtmlTag(node, context) {
12-
context.state.template.push('<!>');
12+
context.state.template.push({ kind: 'create_anchor' });
1313

1414
// push into init, so that bindings run afterwards, which might trigger another run and override hydration
1515
context.state.init.push(

packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as b from '../../../../utils/builders.js';
88
* @param {ComponentContext} context
99
*/
1010
export function IfBlock(node, context) {
11-
context.state.template.push('<!>');
11+
context.state.template.push({ kind: 'create_anchor' });
1212
const statements = [];
1313

1414
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));

packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as b from '../../../../utils/builders.js';
88
* @param {ComponentContext} context
99
*/
1010
export function KeyBlock(node, context) {
11-
context.state.template.push('<!>');
11+
context.state.template.push({ kind: 'create_anchor' });
1212

1313
const key = /** @type {Expression} */ (context.visit(node.expression));
1414
const body = /** @type {Expression} */ (context.visit(node.fragment));

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

+21-15
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ export function RegularElement(node, context) {
5252
}
5353

5454
if (node.name === 'noscript') {
55-
context.state.template.push('<noscript></noscript>');
55+
context.state.template.push({
56+
kind: 'create_element',
57+
args: ['noscript']
58+
});
5659
return;
5760
}
5861

@@ -72,7 +75,10 @@ export function RegularElement(node, context) {
7275
context.state.metadata.context.template_contains_script_tag = true;
7376
}
7477

75-
context.state.template.push(`<${node.name}`);
78+
context.state.template.push({
79+
kind: 'create_element',
80+
args: [node.name]
81+
});
7682

7783
/** @type {Array<AST.Attribute | AST.SpreadAttribute>} */
7884
const attributes = [];
@@ -110,7 +116,10 @@ export function RegularElement(node, context) {
110116
const { value } = build_attribute_value(attribute.value, context);
111117

112118
if (value.type === 'Literal' && typeof value.value === 'string') {
113-
context.state.template.push(` is="${escape_html(value.value, true)}"`);
119+
context.state.template.push({
120+
kind: 'set_prop',
121+
args: ['is', escape_html(value.value, true)]
122+
});
114123
continue;
115124
}
116125
}
@@ -286,13 +295,14 @@ export function RegularElement(node, context) {
286295
}
287296

288297
if (name !== 'class' || value) {
289-
context.state.template.push(
290-
` ${attribute.name}${
298+
context.state.template.push({
299+
kind: 'set_prop',
300+
args: [attribute.name].concat(
291301
is_boolean_attribute(name) && value === true
292-
? ''
293-
: `="${value === true ? '' : escape_html(value, true)}"`
294-
}`
295-
);
302+
? []
303+
: [value === true ? '' : escape_html(value, true)]
304+
)
305+
});
296306
}
297307
} else if (name === 'autofocus') {
298308
let { value } = build_attribute_value(attribute.value, context);
@@ -324,8 +334,7 @@ export function RegularElement(node, context) {
324334
) {
325335
context.state.after_update.push(b.stmt(b.call('$.replay_events', node_id)));
326336
}
327-
328-
context.state.template.push('>');
337+
context.state.template.push({ kind: 'push_element' });
329338

330339
const metadata = {
331340
...context.state.metadata,
@@ -446,10 +455,7 @@ export function RegularElement(node, context) {
446455
// @ts-expect-error
447456
location.push(state.locations);
448457
}
449-
450-
if (!is_void(node.name)) {
451-
context.state.template.push(`</${node.name}>`);
452-
}
458+
context.state.template.push({ kind: 'pop_element' });
453459
}
454460

455461
/**

packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as b from '../../../../utils/builders.js';
99
* @param {ComponentContext} context
1010
*/
1111
export function RenderTag(node, context) {
12-
context.state.template.push('<!>');
12+
context.state.template.push({ kind: 'create_anchor' });
1313

1414
const expression = unwrap_optional(node.expression);
1515

0 commit comments

Comments
 (0)