Skip to content

Commit 8a737f1

Browse files
committed
feat: add option preventTemplateCloning and functions transformation
1 parent 575908a commit 8a737f1

File tree

12 files changed

+393
-119
lines changed

12 files changed

+393
-119
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ export function client_component(analysis, options) {
167167
in_constructor: false,
168168
instance_level_snippets: [],
169169
module_level_snippets: [],
170+
prevent_template_cloning: options.preventTemplateCloning,
170171

171172
// these are set inside the `Fragment` visitor, and cannot be used until then
172173
init: /** @type {any} */ (null),
Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,98 @@
11
/**
2-
* @import { TemplateOperations } from "../types.js"
2+
* @import { ComponentContext, TemplateOperations, ComponentClientTransformState } from "../types.js"
3+
* @import { Identifier, Expression } from "estree"
4+
* @import { AST, Namespace } from '#compiler'
5+
* @import { SourceLocation } from '#shared'
36
*/
7+
import { TEMPLATE_FRAGMENT } from '../../../../../constants.js';
8+
import { dev } from '../../../../state.js';
9+
import * as b from '../../../../utils/builders.js';
10+
import { template_to_functions } from './to-functions.js';
411
import { template_to_string } from './to-string.js';
512

613
/**
7-
* @param {TemplateOperations} items
14+
*
15+
* @param {Namespace} namespace
16+
* @param {ComponentClientTransformState} state
17+
* @returns
818
*/
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);
19+
function get_template_function(namespace, state) {
20+
const contains_script_tag = state.metadata.context.template_contains_script_tag;
21+
return namespace === 'svg'
22+
? contains_script_tag
23+
? '$.svg_template_with_script'
24+
: '$.ns_template'
25+
: namespace === 'mathml'
26+
? '$.mathml_template'
27+
: contains_script_tag
28+
? '$.template_with_script'
29+
: '$.template';
30+
}
31+
32+
/**
33+
* @param {SourceLocation[]} locations
34+
*/
35+
function build_locations(locations) {
36+
return b.array(
37+
locations.map((loc) => {
38+
const expression = b.array([b.literal(loc[0]), b.literal(loc[1])]);
39+
40+
if (loc.length === 3) {
41+
expression.elements.push(build_locations(loc[2]));
42+
}
43+
44+
return expression;
45+
})
46+
);
47+
}
48+
49+
/**
50+
* @param {ComponentClientTransformState} state
51+
* @param {ComponentContext} context
52+
* @param {Namespace} namespace
53+
* @param {Identifier} template_name
54+
* @param {number} [flags]
55+
*/
56+
export function transform_template(state, context, namespace, template_name, flags) {
57+
if (context.state.prevent_template_cloning) {
58+
context.state.hoisted.push(
59+
b.var(
60+
template_name,
61+
template_to_functions(
62+
state.template,
63+
namespace,
64+
flags != null && (flags & TEMPLATE_FRAGMENT) !== 0
65+
)
66+
)
67+
);
68+
69+
return;
70+
}
71+
72+
/**
73+
* @param {Identifier} template_name
74+
* @param {Expression[]} args
75+
*/
76+
const add_template = (template_name, args) => {
77+
let call = b.call(get_template_function(namespace, state), ...args);
78+
if (dev) {
79+
call = b.call(
80+
'$.add_locations',
81+
call,
82+
b.member(b.id(context.state.analysis.name), '$.FILENAME', true),
83+
build_locations(state.locations)
84+
);
85+
}
86+
87+
context.state.hoisted.push(b.var(template_name, call));
88+
};
89+
90+
/** @type {Expression[]} */
91+
const args = [b.template([b.quasi(template_to_string(state.template), true)], [])];
92+
93+
if (flags) {
94+
args.push(b.literal(flags));
95+
}
96+
97+
add_template(template_name, args);
1298
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/**
2+
* @import { TemplateOperations } from "../types.js"
3+
* @import { Namespace } from "#compiler"
4+
* @import { Statement } from "estree"
5+
*/
6+
import { NAMESPACE_SVG } from 'svelte/internal/client';
7+
import * as b from '../../../../utils/builders.js';
8+
import { NAMESPACE_MATHML } from '../../../../../constants.js';
9+
10+
class Scope {
11+
declared = new Map();
12+
13+
/**
14+
* @param {string} _name
15+
*/
16+
generate(_name) {
17+
let name = _name.replace(/[^a-zA-Z0-9_$]/g, '_').replace(/^[0-9]/, '_');
18+
if (!this.declared.has(name)) {
19+
this.declared.set(name, 1);
20+
return name;
21+
}
22+
let count = this.declared.get(name);
23+
this.declared.set(name, count + 1);
24+
return `${name}_${count}`;
25+
}
26+
}
27+
28+
/**
29+
* @param {TemplateOperations} items
30+
* @param {Namespace} namespace
31+
* @param {boolean} use_fragment
32+
*/
33+
export function template_to_functions(items, namespace, use_fragment = false) {
34+
let elements = [];
35+
36+
let body = [];
37+
38+
let scope = new Scope();
39+
40+
/**
41+
* @type {Array<Element>}
42+
*/
43+
let elements_stack = [];
44+
45+
/**
46+
* @type {Element | undefined}
47+
*/
48+
let last_current_element;
49+
50+
for (let instruction of items) {
51+
if (instruction.kind === 'push_element' && last_current_element) {
52+
elements_stack.push(last_current_element);
53+
continue;
54+
}
55+
if (instruction.kind === 'pop_element') {
56+
elements_stack.pop();
57+
continue;
58+
}
59+
60+
// @ts-expect-error we can't be here if `swap_current_element` but TS doesn't know that
61+
const value = map[instruction.kind](
62+
...[
63+
...(instruction.kind === 'set_prop' ? [last_current_element] : [scope]),
64+
...(instruction.kind === 'create_element' ? [namespace] : []),
65+
...(instruction.args ?? [])
66+
]
67+
);
68+
69+
if (value) {
70+
body.push(value.call);
71+
}
72+
73+
if (instruction.kind !== 'set_prop') {
74+
if (elements_stack.length >= 1 && value) {
75+
const { call } = map.insert(/** @type {Element} */ (elements_stack.at(-1)), value);
76+
body.push(call);
77+
} else if (value) {
78+
elements.push(b.id(value.name));
79+
}
80+
if (instruction.kind === 'create_element') {
81+
last_current_element = /** @type {Element} */ (value);
82+
}
83+
}
84+
}
85+
if (elements.length > 1 || use_fragment) {
86+
const fragment = scope.generate('fragment');
87+
body.push(b.var(fragment, b.call('document.createDocumentFragment')));
88+
body.push(b.call(fragment + '.append', ...elements));
89+
body.push(b.return(b.id(fragment)));
90+
} else {
91+
body.push(b.return(elements[0]));
92+
}
93+
94+
return b.arrow([], b.block(body));
95+
}
96+
97+
/**
98+
* @typedef {{ call: Statement, name: string }} Element
99+
*/
100+
101+
/**
102+
* @typedef {{ call: Statement, name: string }} Anchor
103+
*/
104+
105+
/**
106+
* @typedef {{ call: Statement, name: string }} Text
107+
*/
108+
109+
/**
110+
* @typedef { Element | Anchor| Text } Node
111+
*/
112+
113+
/**
114+
* @param {Scope} scope
115+
* @param {Namespace} namespace
116+
* @param {string} element
117+
* @returns {Element}
118+
*/
119+
function create_element(scope, namespace, element) {
120+
const name = scope.generate(element);
121+
let fn = namespace !== 'html' ? 'document.createElementNS' : 'document.createElement';
122+
let args = [b.literal(element)];
123+
if (namespace !== 'html') {
124+
args.unshift(namespace === 'svg' ? b.literal(NAMESPACE_SVG) : b.literal(NAMESPACE_MATHML));
125+
}
126+
return {
127+
call: b.var(name, b.call(fn, ...args)),
128+
name
129+
};
130+
}
131+
132+
/**
133+
* @param {Scope} scope
134+
* @param {string} data
135+
* @returns {Anchor}
136+
*/
137+
function create_anchor(scope, data = '') {
138+
const name = scope.generate('comment');
139+
return {
140+
call: b.var(name, b.call('document.createComment', b.literal(data))),
141+
name
142+
};
143+
}
144+
145+
/**
146+
* @param {Scope} scope
147+
* @param {string} value
148+
* @returns {Text}
149+
*/
150+
function create_text(scope, value) {
151+
const name = scope.generate('text');
152+
return {
153+
call: b.var(name, b.call('document.createTextNode', b.literal(value))),
154+
name
155+
};
156+
}
157+
158+
/**
159+
*
160+
* @param {Element} el
161+
* @param {string} prop
162+
* @param {string} value
163+
*/
164+
function set_prop(el, prop, value) {
165+
return {
166+
call: b.call(el.name + '.setAttribute', b.literal(prop), b.literal(value))
167+
};
168+
}
169+
170+
/**
171+
*
172+
* @param {Element} el
173+
* @param {Node} child
174+
* @param {Node} [anchor]
175+
*/
176+
function insert(el, child, anchor) {
177+
return {
178+
call: b.call(el.name + '.insertBefore', b.id(child.name), b.id(anchor?.name ?? 'undefined'))
179+
};
180+
}
181+
182+
let map = {
183+
create_element,
184+
create_text,
185+
create_anchor,
186+
set_prop,
187+
insert
188+
};

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
9090
};
9191
};
9292
readonly preserve_whitespace: boolean;
93+
readonly prevent_template_cloning?: boolean;
9394

9495
/** The anchor node for the current context */
9596
readonly node: Identifier;

0 commit comments

Comments
 (0)