-
Notifications
You must be signed in to change notification settings - Fork 382
/
stylesheet.ts
376 lines (339 loc) · 14.4 KB
/
stylesheet.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
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
/*
* 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 { ArrayMap, ArrayPush, isArray, isNull, isUndefined, KEY__SCOPED_CSS } from '@lwc/shared';
import { logError } from '../shared/logger';
import api from './api';
import { RenderMode, ShadowMode, VM } from './vm';
import { computeHasScopedStyles, hasStyles, Template } from './template';
import { getStyleOrSwappedStyle } from './hot-swaps';
import { VCustomElement, VNode } from './vnodes';
import { checkVersionMismatch } from './check-version-mismatch';
import { getComponentInternalDef } from './def';
import { assertNotProd } from './utils';
// These are only used for HMR in dev mode
// The "pure" annotations are so that Rollup knows for sure it can remove these from prod mode
let stylesheetsToCssContent: WeakMap<StylesheetFactory, Set<string>> = /*@__PURE__@*/ new WeakMap();
let cssContentToAbortControllers: Map<string, AbortController> = /*@__PURE__@*/ new Map();
// Only used in LWC's Karma tests
if (process.env.NODE_ENV === 'test-karma-lwc') {
// Used to reset the global state between test runs
(window as any).__lwcResetStylesheetCache = () => {
stylesheetsToCssContent = new WeakMap();
cssContentToAbortControllers = new Map();
};
}
/**
* Function producing style based on a host and a shadow selector. This function is invoked by
* the engine with different values depending on the mode that the component is running on.
*/
export type StylesheetFactory = (
stylesheetToken: string | undefined,
useActualHostSelector: boolean,
useNativeDirPseudoclass: boolean
) => string;
/**
* The list of stylesheets associated with a template. Each entry is either a StylesheetFactory or a
* TemplateStylesheetFactory a given stylesheet depends on other external stylesheets (via
* the @import CSS declaration).
*/
export type TemplateStylesheetFactories = Array<StylesheetFactory | TemplateStylesheetFactories>;
function linkStylesheetToCssContentInDevMode(stylesheet: StylesheetFactory, cssContent: string) {
// Should never leak to prod; only used for HMR
assertNotProd();
let cssContents = stylesheetsToCssContent.get(stylesheet);
if (isUndefined(cssContents)) {
cssContents = new Set();
stylesheetsToCssContent.set(stylesheet, cssContents);
}
cssContents.add(cssContent);
}
function getOrCreateAbortControllerInDevMode(cssContent: string) {
// Should never leak to prod; only used for HMR
assertNotProd();
let abortController = cssContentToAbortControllers.get(cssContent);
if (isUndefined(abortController)) {
abortController = new AbortController();
cssContentToAbortControllers.set(cssContent, abortController);
}
return abortController;
}
function getOrCreateAbortSignal(cssContent: string): AbortSignal | undefined {
// abort controller/signal is only used for HMR in development
if (process.env.NODE_ENV !== 'production') {
return getOrCreateAbortControllerInDevMode(cssContent).signal;
}
return undefined;
}
function makeHostToken(token: string) {
// Note: if this ever changes, update the `cssScopeTokens` returned by `@lwc/compiler`
return `${token}-host`;
}
function createInlineStyleVNode(content: string): VNode {
return api.h(
'style',
{
key: 'style', // special key
attrs: {
type: 'text/css',
},
},
[api.t(content)]
);
}
// TODO [#3733]: remove support for legacy scope tokens
export function updateStylesheetToken(vm: VM, template: Template, legacy: boolean) {
const {
elm,
context,
renderMode,
shadowMode,
renderer: { getClassList, removeAttribute, setAttribute },
} = vm;
const { stylesheets: newStylesheets } = template;
const newStylesheetToken = legacy ? template.legacyStylesheetToken : template.stylesheetToken;
const { stylesheets: newVmStylesheets } = vm;
const isSyntheticShadow =
renderMode === RenderMode.Shadow && shadowMode === ShadowMode.Synthetic;
const { hasScopedStyles } = context;
let newToken: string | undefined;
let newHasTokenInClass: boolean | undefined;
let newHasTokenInAttribute: boolean | undefined;
// Reset the styling token applied to the host element.
let oldToken;
let oldHasTokenInClass;
let oldHasTokenInAttribute;
if (legacy) {
oldToken = context.legacyStylesheetToken;
oldHasTokenInClass = context.hasLegacyTokenInClass;
oldHasTokenInAttribute = context.hasLegacyTokenInAttribute;
} else {
oldToken = context.stylesheetToken;
oldHasTokenInClass = context.hasTokenInClass;
oldHasTokenInAttribute = context.hasTokenInAttribute;
}
if (!isUndefined(oldToken)) {
if (oldHasTokenInClass) {
getClassList(elm).remove(makeHostToken(oldToken));
}
if (oldHasTokenInAttribute) {
removeAttribute(elm, makeHostToken(oldToken));
}
}
// Apply the new template styling token to the host element, if the new template has any
// associated stylesheets. In the case of light DOM, also ensure there is at least one scoped stylesheet.
const hasNewStylesheets = hasStyles(newStylesheets);
const hasNewVmStylesheets = hasStyles(newVmStylesheets);
if (hasNewStylesheets || hasNewVmStylesheets) {
newToken = newStylesheetToken;
}
// Set the new styling token on the host element
if (!isUndefined(newToken)) {
if (hasScopedStyles) {
getClassList(elm).add(makeHostToken(newToken));
newHasTokenInClass = true;
}
if (isSyntheticShadow) {
setAttribute(elm, makeHostToken(newToken), '');
newHasTokenInAttribute = true;
}
}
// Update the styling tokens present on the context object.
if (legacy) {
context.legacyStylesheetToken = newToken;
context.hasLegacyTokenInClass = newHasTokenInClass;
context.hasLegacyTokenInAttribute = newHasTokenInAttribute;
} else {
context.stylesheetToken = newToken;
context.hasTokenInClass = newHasTokenInClass;
context.hasTokenInAttribute = newHasTokenInAttribute;
}
}
function evaluateStylesheetsContent(
stylesheets: TemplateStylesheetFactories,
stylesheetToken: string | undefined,
vm: VM
): string[] {
const content: string[] = [];
let root: VM | null | undefined;
for (let i = 0; i < stylesheets.length; i++) {
let stylesheet = stylesheets[i];
if (isArray(stylesheet)) {
ArrayPush.apply(content, evaluateStylesheetsContent(stylesheet, stylesheetToken, vm));
} else {
if (process.env.NODE_ENV !== 'production') {
// Check for compiler version mismatch in dev mode only
checkVersionMismatch(stylesheet, 'stylesheet');
// in dev-mode, we support hot swapping of stylesheet, which means that
// the component instance might be attempting to use an old version of
// the stylesheet, while internally, we have a replacement for it.
stylesheet = getStyleOrSwappedStyle(stylesheet);
}
const isScopedCss = (stylesheet as any)[KEY__SCOPED_CSS];
if (
lwcRuntimeFlags.DISABLE_LIGHT_DOM_UNSCOPED_CSS &&
!isScopedCss &&
vm.renderMode === RenderMode.Light
) {
logError(
'Unscoped CSS is not supported in Light DOM in this environment. Please use scoped CSS ' +
'(*.scoped.css) instead of unscoped CSS (*.css). See also: https://sfdc.co/scoped-styles-light-dom'
);
continue;
}
// Apply the scope token only if the stylesheet itself is scoped, or if we're rendering synthetic shadow.
const scopeToken =
isScopedCss ||
(vm.shadowMode === ShadowMode.Synthetic && vm.renderMode === RenderMode.Shadow)
? stylesheetToken
: undefined;
// Use the actual `:host` selector if we're rendering global CSS for light DOM, or if we're rendering
// native shadow DOM. Synthetic shadow DOM never uses `:host`.
const useActualHostSelector =
vm.renderMode === RenderMode.Light
? !isScopedCss
: vm.shadowMode === ShadowMode.Native;
// Use the native :dir() pseudoclass only in native shadow DOM. Otherwise, in synthetic shadow,
// we use an attribute selector on the host to simulate :dir().
let useNativeDirPseudoclass;
if (vm.renderMode === RenderMode.Shadow) {
useNativeDirPseudoclass = vm.shadowMode === ShadowMode.Native;
} else {
// Light DOM components should only render `[dir]` if they're inside of a synthetic shadow root.
// At the top level (root is null) or inside of a native shadow root, they should use `:dir()`.
if (isUndefined(root)) {
// Only calculate the root once as necessary
root = getNearestShadowComponent(vm);
}
useNativeDirPseudoclass = isNull(root) || root.shadowMode === ShadowMode.Native;
}
const cssContent = stylesheet(
scopeToken,
useActualHostSelector,
useNativeDirPseudoclass
);
if (process.env.NODE_ENV !== 'production') {
linkStylesheetToCssContentInDevMode(stylesheet, cssContent);
}
ArrayPush.call(content, cssContent);
}
}
return content;
}
export function getStylesheetsContent(vm: VM, template: Template): string[] {
const { stylesheets, stylesheetToken } = template;
const { stylesheets: vmStylesheets } = vm;
let content: string[] = [];
if (hasStyles(stylesheets)) {
content = evaluateStylesheetsContent(stylesheets, stylesheetToken, vm);
}
// VM (component) stylesheets apply after template stylesheets
if (hasStyles(vmStylesheets)) {
ArrayPush.apply(content, evaluateStylesheetsContent(vmStylesheets, stylesheetToken, vm));
}
return content;
}
// It might be worth caching this to avoid doing the lookup repeatedly, but
// perf testing has not shown it to be a huge improvement yet:
// https://github.com/salesforce/lwc/pull/2460#discussion_r691208892
function getNearestShadowComponent(vm: VM): VM | null {
let owner: VM | null = vm;
while (!isNull(owner)) {
if (owner.renderMode === RenderMode.Shadow) {
return owner;
}
owner = owner.owner;
}
return owner;
}
/**
* If the component that is currently being rendered uses scoped styles,
* this returns the unique token for that scoped stylesheet. Otherwise
* it returns null.
* @param owner
* @param legacy
*/
// TODO [#3733]: remove support for legacy scope tokens
export function getScopeTokenClass(owner: VM, legacy: boolean): string | null {
const { cmpTemplate, context } = owner;
return (
(context.hasScopedStyles &&
(legacy ? cmpTemplate?.legacyStylesheetToken : cmpTemplate?.stylesheetToken)) ||
null
);
}
/**
* This function returns the host style token for a custom element if it
* exists. Otherwise it returns null.
*
* A host style token is applied to the component if scoped styles are used.
* @param vnode
*/
export function getStylesheetTokenHost(vnode: VCustomElement): string | null {
const { template } = getComponentInternalDef(vnode.ctor);
const { vm } = vnode;
const { stylesheetToken } = template;
return !isUndefined(stylesheetToken) && computeHasScopedStyles(template, vm)
? makeHostToken(stylesheetToken)
: null;
}
function getNearestNativeShadowComponent(vm: VM): VM | null {
const owner = getNearestShadowComponent(vm);
if (!isNull(owner) && owner.shadowMode === ShadowMode.Synthetic) {
// Synthetic-within-native is impossible. So if the nearest shadow component is
// synthetic, we know we won't find a native component if we go any further.
return null;
}
return owner;
}
export function createStylesheet(vm: VM, stylesheets: string[]): VNode[] | null {
const {
renderMode,
shadowMode,
renderer: { insertStylesheet },
} = vm;
if (renderMode === RenderMode.Shadow && shadowMode === ShadowMode.Synthetic) {
for (let i = 0; i < stylesheets.length; i++) {
const stylesheet = stylesheets[i];
insertStylesheet(stylesheet, undefined, getOrCreateAbortSignal(stylesheet));
}
} else if (!process.env.IS_BROWSER || vm.hydrated) {
// Note: We need to ensure that during hydration, the stylesheets method is the same as those in ssr.
// This works in the client, because the stylesheets are created, and cached in the VM
// the first time the VM renders.
// native shadow or light DOM, SSR
return ArrayMap.call(stylesheets, createInlineStyleVNode) as VNode[];
} else {
// native shadow or light DOM, DOM renderer
const root = getNearestNativeShadowComponent(vm);
// null root means a global style
const target = isNull(root) ? undefined : root.shadowRoot!;
for (let i = 0; i < stylesheets.length; i++) {
const stylesheet = stylesheets[i];
insertStylesheet(stylesheet, target, getOrCreateAbortSignal(stylesheet));
}
}
return null;
}
export function unrenderStylesheet(stylesheet: StylesheetFactory) {
// should never leak to prod; only used for HMR
assertNotProd();
const cssContents = stylesheetsToCssContent.get(stylesheet);
/* istanbul ignore if */
if (isUndefined(cssContents)) {
throw new Error('Cannot unrender stylesheet which was never rendered');
}
for (const cssContent of cssContents) {
const abortController = cssContentToAbortControllers.get(cssContent);
/* istanbul ignore if */
if (isUndefined(abortController)) {
throw new Error('Cannot find AbortController for CSS content');
}
abortController.abort();
// remove association with AbortController in case stylesheet is rendered again
cssContentToAbortControllers.delete(cssContent);
}
}