-
Notifications
You must be signed in to change notification settings - Fork 382
/
template.ts
355 lines (311 loc) · 13 KB
/
template.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import {
ArrayUnshift,
assert,
create,
isArray,
isNull,
isTrue,
isUndefined,
KEY__SCOPED_CSS,
keys,
noop,
toString,
} from '@lwc/shared';
import { logError } from '../shared/logger';
import { getComponentTag } from '../shared/format';
import api, { RenderAPI } from './api';
import {
RenderMode,
resetComponentRoot,
runWithBoundaryProtection,
ShadowMode,
SlotSet,
TemplateCache,
VM,
} from './vm';
import { assertNotProd } from './utils';
import { defaultEmptyTemplate, isTemplateRegistered } from './secure-template';
import {
createStylesheet,
getStylesheetsContent,
TemplateStylesheetFactories,
updateStylesheetToken,
} from './stylesheet';
import { logOperationEnd, logOperationStart, OperationId } from './profiler';
import { getTemplateOrSwappedTemplate, setActiveVM } from './hot-swaps';
import { VNodes } from './vnodes';
import { RendererAPI } from './renderer';
export interface Template {
(api: RenderAPI, cmp: object, slotSet: SlotSet, cache: TemplateCache): VNodes;
/** The list of slot names used in the template. */
slots?: string[];
/** The stylesheet associated with the template. */
stylesheets?: TemplateStylesheetFactories;
/** The string used for synthetic shadow style scoping and light DOM style scoping. */
stylesheetToken?: string;
/** Same as the above, but for legacy use cases (pre-LWC v3.0.0) */
// TODO [#3733]: remove support for legacy scope tokens
legacyStylesheetToken?: string;
/** Render mode for the template. Could be light or undefined (which means it's shadow) */
renderMode?: 'light';
/** True if this template contains template refs, undefined or false otherwise */
hasRefs?: boolean;
}
export let isUpdatingTemplate: boolean = false;
let vmBeingRendered: VM | null = null;
export function getVMBeingRendered(): VM | null {
return vmBeingRendered;
}
export function setVMBeingRendered(vm: VM | null) {
vmBeingRendered = vm;
}
function validateSlots(vm: VM) {
assertNotProd(); // this method should never leak to prod
const { cmpSlots } = vm;
for (const slotName in cmpSlots.slotAssignments) {
assert.isTrue(
isArray(cmpSlots.slotAssignments[slotName]),
`Slots can only be set to an array, instead received ${toString(
cmpSlots.slotAssignments[slotName]
)} for slot "${slotName}" in ${vm}.`
);
}
}
function validateLightDomTemplate(template: Template, vm: VM) {
assertNotProd(); // should never leak to prod mode
if (template === defaultEmptyTemplate) {
return;
}
if (vm.renderMode === RenderMode.Light) {
if (template.renderMode !== 'light') {
logError(
`Light DOM components can't render shadow DOM templates. Add an 'lwc:render-mode="light"' directive to the root template tag of ${getComponentTag(
vm
)}.`
);
}
} else {
if (!isUndefined(template.renderMode)) {
logError(
`Shadow DOM components template can't render light DOM templates. Either remove the 'lwc:render-mode' directive from ${getComponentTag(
vm
)} or set it to 'lwc:render-mode="shadow"`
);
}
}
}
const enum FragmentCache {
HAS_SCOPED_STYLE = 1 << 0,
SHADOW_MODE_SYNTHETIC = 1 << 1,
}
// This should be a no-op outside of LWC's Karma tests, where it's not needed
let registerFragmentCache: (fragmentCache: any) => void = noop;
// Only used in LWC's Karma tests
if (process.env.NODE_ENV === 'test-karma-lwc') {
// Keep track of fragmentCaches, so we can clear them in LWC's Karma tests
const fragmentCaches: any[] = [];
registerFragmentCache = (fragmentCache: any) => {
fragmentCaches.push(fragmentCache);
};
(window as any).__lwcResetFragmentCaches = () => {
for (const fragmentCache of fragmentCaches) {
for (const key of keys(fragmentCache)) {
delete fragmentCache[key];
}
}
};
}
function buildParseFragmentFn(
createFragmentFn: (html: string, renderer: RendererAPI) => Element
): (strings: string[], ...keys: number[]) => () => Element {
return (strings: string[], ...keys: number[]) => {
const cache = create(null);
registerFragmentCache(cache);
return function (): Element {
const {
context: { hasScopedStyles, stylesheetToken, legacyStylesheetToken },
shadowMode,
renderer,
} = getVMBeingRendered()!;
const hasStyleToken = !isUndefined(stylesheetToken);
const isSyntheticShadow = shadowMode === ShadowMode.Synthetic;
const hasLegacyToken =
lwcRuntimeFlags.ENABLE_LEGACY_SCOPE_TOKENS && !isUndefined(legacyStylesheetToken);
let cacheKey = 0;
if (hasStyleToken && hasScopedStyles) {
cacheKey |= FragmentCache.HAS_SCOPED_STYLE;
}
if (hasStyleToken && isSyntheticShadow) {
cacheKey |= FragmentCache.SHADOW_MODE_SYNTHETIC;
}
if (!isUndefined(cache[cacheKey])) {
return cache[cacheKey];
}
// If legacy stylesheet tokens are required, then add them to the rendered string
const stylesheetTokenToRender =
stylesheetToken + (hasLegacyToken ? ` ${legacyStylesheetToken}` : '');
const classToken =
hasScopedStyles && hasStyleToken ? ' ' + stylesheetTokenToRender : '';
const classAttrToken =
hasScopedStyles && hasStyleToken ? ` class="${stylesheetTokenToRender}"` : '';
const attrToken =
hasStyleToken && isSyntheticShadow ? ' ' + stylesheetTokenToRender : '';
let htmlFragment = '';
for (let i = 0, n = keys.length; i < n; i++) {
switch (keys[i]) {
case 0: // styleToken in existing class attr
htmlFragment += strings[i] + classToken;
break;
case 1: // styleToken for added class attr
htmlFragment += strings[i] + classAttrToken;
break;
case 2: // styleToken as attr
htmlFragment += strings[i] + attrToken;
break;
case 3: // ${1}${2}
htmlFragment += strings[i] + classAttrToken + attrToken;
break;
}
}
htmlFragment += strings[strings.length - 1];
cache[cacheKey] = createFragmentFn(htmlFragment, renderer);
return cache[cacheKey];
};
};
}
// Note: at the moment this code executes, we don't have a renderer yet.
export const parseFragment = buildParseFragmentFn((html, renderer) => {
const { createFragment } = renderer;
return createFragment(html);
});
export const parseSVGFragment = buildParseFragmentFn((html, renderer) => {
const { createFragment, getFirstChild } = renderer;
const fragment = createFragment('<svg>' + html + '</svg>');
return getFirstChild(fragment);
});
export function evaluateTemplate(vm: VM, html: Template): VNodes {
if (process.env.NODE_ENV !== 'production') {
// in dev-mode, we support hot swapping of templates, which means that
// the component instance might be attempting to use an old version of
// the template, while internally, we have a replacement for it.
html = getTemplateOrSwappedTemplate(html);
}
const isUpdatingTemplateInception = isUpdatingTemplate;
const vmOfTemplateBeingUpdatedInception = vmBeingRendered;
let vnodes: VNodes = [];
runWithBoundaryProtection(
vm,
vm.owner,
() => {
// pre
vmBeingRendered = vm;
logOperationStart(OperationId.Render, vm);
},
() => {
// job
const { component, context, cmpSlots, cmpTemplate, tro } = vm;
tro.observe(() => {
// Reset the cache memoizer for template when needed.
if (html !== cmpTemplate) {
// Check that the template was built by the compiler.
if (!isTemplateRegistered(html)) {
throw new TypeError(
`Invalid template returned by the render() method on ${
vm.tagName
}. It must return an imported template (e.g.: \`import html from "./${
vm.def.name
}.html"\`), instead, it has returned: ${toString(html)}.`
);
}
if (process.env.NODE_ENV !== 'production') {
validateLightDomTemplate(html, vm);
}
// Perf opt: do not reset the shadow root during the first rendering (there is
// nothing to reset).
if (!isNull(cmpTemplate)) {
// It is important to reset the content to avoid reusing similar elements
// generated from a different template, because they could have similar IDs,
// and snabbdom just rely on the IDs.
resetComponentRoot(vm);
}
vm.cmpTemplate = html;
// Create a brand new template cache for the swapped templated.
context.tplCache = create(null);
// Set the computeHasScopedStyles property in the context, to avoid recomputing it repeatedly.
context.hasScopedStyles = computeHasScopedStyles(html, vm);
// Update the scoping token on the host element.
updateStylesheetToken(vm, html, /* legacy */ false);
if (lwcRuntimeFlags.ENABLE_LEGACY_SCOPE_TOKENS) {
updateStylesheetToken(vm, html, /* legacy */ true);
}
// Evaluate, create stylesheet and cache the produced VNode for future
// re-rendering.
const stylesheetsContent = getStylesheetsContent(vm, html);
context.styleVNodes =
stylesheetsContent.length === 0
? null
: createStylesheet(vm, stylesheetsContent);
}
if (process.env.NODE_ENV !== 'production') {
// validating slots in every rendering since the allocated content might change over time
validateSlots(vm);
// add the VM to the list of host VMs that can be re-rendered if html is swapped
setActiveVM(vm);
}
// right before producing the vnodes, we clear up all internal references
// to custom elements from the template.
vm.velements = [];
// Set the global flag that template is being updated
isUpdatingTemplate = true;
vnodes = html.call(undefined, api, component, cmpSlots, context.tplCache);
const { styleVNodes } = context;
if (!isNull(styleVNodes)) {
ArrayUnshift.apply(vnodes, styleVNodes);
}
});
},
() => {
// post
isUpdatingTemplate = isUpdatingTemplateInception;
vmBeingRendered = vmOfTemplateBeingUpdatedInception;
logOperationEnd(OperationId.Render, vm);
}
);
if (process.env.NODE_ENV !== 'production') {
if (!isArray(vnodes)) {
logError(`Compiler should produce html functions that always return an array.`);
}
}
return vnodes;
}
function computeHasScopedStylesInStylesheets(
stylesheets: TemplateStylesheetFactories | undefined | null
): boolean {
if (hasStyles(stylesheets)) {
for (let i = 0; i < stylesheets.length; i++) {
if (isTrue((stylesheets[i] as any)[KEY__SCOPED_CSS])) {
return true;
}
}
}
return false;
}
export function computeHasScopedStyles(template: Template, vm: VM | undefined): boolean {
const { stylesheets } = template;
const vmStylesheets = !isUndefined(vm) ? vm.stylesheets : null;
return (
computeHasScopedStylesInStylesheets(stylesheets) ||
computeHasScopedStylesInStylesheets(vmStylesheets)
);
}
export function hasStyles(
stylesheets: TemplateStylesheetFactories | undefined | null
): stylesheets is TemplateStylesheetFactories {
return !isUndefined(stylesheets) && !isNull(stylesheets) && stylesheets.length > 0;
}