Skip to content

Commit c3e294b

Browse files
authored
feat: add flag for forced shadow migrate mode (#3894)
1 parent 53646a7 commit c3e294b

File tree

16 files changed

+310
-9
lines changed

16 files changed

+310
-9
lines changed

packages/@lwc/engine-core/src/framework/base-lightning-element.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ import {
2020
freeze,
2121
hasOwnProperty,
2222
isFunction,
23-
isString,
2423
isNull,
2524
isObject,
25+
isString,
2626
isUndefined,
2727
KEY__SYNTHETIC_MODE,
2828
keys,
@@ -35,7 +35,7 @@ import { applyAriaReflection } from '../libs/aria-reflection/aria-reflection';
3535

3636
import { HTMLElementOriginalDescriptors } from './html-properties';
3737
import { getWrappedComponentsListener } from './component';
38-
import { vmBeingConstructed, isBeingConstructed, isInvokingRender } from './invoker';
38+
import { isBeingConstructed, isInvokingRender, vmBeingConstructed } from './invoker';
3939
import {
4040
associateVM,
4141
getAssociatedVM,
@@ -47,16 +47,17 @@ import {
4747
} from './vm';
4848
import { componentValueObserved } from './mutation-tracker';
4949
import {
50-
patchShadowRootWithRestrictions,
51-
patchLightningElementPrototypeWithRestrictions,
5250
patchCustomElementWithRestrictions,
51+
patchLightningElementPrototypeWithRestrictions,
52+
patchShadowRootWithRestrictions,
5353
} from './restrictions';
54-
import { Template, isUpdatingTemplate, getVMBeingRendered } from './template';
54+
import { getVMBeingRendered, isUpdatingTemplate, Template } from './template';
5555
import { HTMLElementConstructor } from './base-bridge-element';
5656
import { updateComponentValue } from './update-component-value';
5757
import { markLockerLiveObject } from './membrane';
5858
import { TemplateStylesheetFactories } from './stylesheet';
5959
import { instrumentInstance } from './runtime-instrumentation';
60+
import { applyShadowMigrateMode } from './shadow-migration-mode';
6061

6162
/**
6263
* This operation is called with a descriptor of an standard html property
@@ -289,6 +290,14 @@ function doAttachShadow(vm: VM): ShadowRoot {
289290
patchShadowRootWithRestrictions(shadowRoot);
290291
}
291292

293+
if (
294+
process.env.IS_BROWSER &&
295+
lwcRuntimeFlags.ENABLE_FORCE_SHADOW_MIGRATE_MODE &&
296+
vm.shadowMigrateMode
297+
) {
298+
applyShadowMigrateMode(shadowRoot);
299+
}
300+
292301
return shadowRoot;
293302
}
294303

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright (c) 2023, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: MIT
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6+
*/
7+
import { logWarnOnce } from '../shared/logger';
8+
9+
let globalStylesheet: CSSStyleSheet | undefined;
10+
11+
function isStyleElement(elm: Element): elm is HTMLStyleElement {
12+
return elm.tagName === 'STYLE';
13+
}
14+
15+
async function fetchStylesheet(elm: HTMLStyleElement | HTMLLinkElement) {
16+
if (isStyleElement(elm)) {
17+
return elm.textContent;
18+
} else {
19+
// <link>
20+
const { href } = elm;
21+
try {
22+
return await (await fetch(href)).text();
23+
} catch (err) {
24+
logWarnOnce(`Ignoring cross-origin stylesheet in migrate mode: ${href}`);
25+
// ignore errors with cross-origin stylesheets - nothing we can do for those
26+
return '';
27+
}
28+
}
29+
}
30+
31+
function initGlobalStylesheet() {
32+
const stylesheet = new CSSStyleSheet();
33+
const elmsToPromises = new Map();
34+
let lastSeenLength = 0;
35+
36+
const copyToGlobalStylesheet = () => {
37+
const elms = document.head.querySelectorAll(
38+
'style:not([data-rendered-by-lwc]),link[rel="stylesheet"]'
39+
);
40+
if (elms.length === lastSeenLength) {
41+
return; // nothing to update
42+
}
43+
lastSeenLength = elms.length;
44+
const promises = [...(elms as unknown as Iterable<HTMLStyleElement | HTMLLinkElement>)].map(
45+
(elm) => {
46+
let promise = elmsToPromises.get(elm);
47+
if (!promise) {
48+
// Cache the promise
49+
promise = fetchStylesheet(elm);
50+
elmsToPromises.set(elm, promise);
51+
}
52+
return promise;
53+
}
54+
);
55+
Promise.all(promises).then((stylesheetTexts) => {
56+
// When replaceSync() is called, the entire contents of the constructable stylesheet are replaced
57+
// with the copied+concatenated styles. This means that any shadow root's adoptedStyleSheets that
58+
// contains this constructable stylesheet will immediately get the new styles.
59+
stylesheet.replaceSync(stylesheetTexts.join('\n'));
60+
});
61+
};
62+
63+
const headObserver = new MutationObserver(copyToGlobalStylesheet);
64+
65+
// By observing only the childList, note that we are not covering the case where someone changes an `href`
66+
// on an existing <link>`, or the textContent on an existing `<style>`. This is assumed to be an uncommon
67+
// case and not worth covering.
68+
headObserver.observe(document.head, {
69+
childList: true,
70+
});
71+
72+
copyToGlobalStylesheet();
73+
74+
return stylesheet;
75+
}
76+
77+
export function applyShadowMigrateMode(shadowRoot: ShadowRoot) {
78+
if (!globalStylesheet) {
79+
globalStylesheet = initGlobalStylesheet();
80+
}
81+
82+
(shadowRoot as any).synthetic = true; // pretend to be synthetic mode
83+
shadowRoot.adoptedStyleSheets.push(globalStylesheet);
84+
}

packages/@lwc/engine-core/src/framework/vm.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ export interface VM<N = HostNode, E = HostElement> {
144144
/** Rendering operations associated with the VM */
145145
renderMode: RenderMode;
146146
shadowMode: ShadowMode;
147+
/** True if shadow migrate mode is in effect, i.e. this is native with synthetic-like modifications */
148+
shadowMigrateMode: boolean;
147149
/** The component creation index. */
148150
idx: number;
149151
/** Component state, analogous to Element.isConnected */
@@ -353,6 +355,7 @@ export function createVM<HostNode, HostElement>(
353355
// Properties set right after VM creation.
354356
tro: null!,
355357
shadowMode: null!,
358+
shadowMigrateMode: false,
356359
stylesheets: null!,
357360

358361
// Properties set by the LightningElement constructor.
@@ -373,7 +376,13 @@ export function createVM<HostNode, HostElement>(
373376
}
374377

375378
vm.stylesheets = computeStylesheets(vm, def.ctor);
376-
vm.shadowMode = computeShadowMode(def, vm.owner, renderer);
379+
const computedShadowMode = computeShadowMode(def, vm.owner, renderer);
380+
if (lwcRuntimeFlags.ENABLE_FORCE_SHADOW_MIGRATE_MODE) {
381+
vm.shadowMode = ShadowMode.Native;
382+
vm.shadowMigrateMode = computedShadowMode === ShadowMode.Synthetic;
383+
} else {
384+
vm.shadowMode = computedShadowMode;
385+
}
377386
vm.tro = getTemplateReactiveObserver(vm);
378387

379388
// We don't need to report the shadow mode if we're rendering in light DOM
@@ -490,7 +499,8 @@ function computeShadowMode(def: ComponentDef, owner: VM | null, renderer: Render
490499
const { isSyntheticShadowDefined } = renderer;
491500

492501
let shadowMode;
493-
if (isSyntheticShadowDefined) {
502+
// If ENABLE_FORCE_SHADOW_MIGRATE_MODE is true, then ShadowMode.Synthetic here will mean "force-migrate" mode.
503+
if (isSyntheticShadowDefined || lwcRuntimeFlags.ENABLE_FORCE_SHADOW_MIGRATE_MODE) {
494504
if (def.renderMode === RenderMode.Light) {
495505
// ShadowMode.Native implies "not synthetic shadow" which is consistent with how
496506
// everything defaults to native when the synthetic shadow polyfill is unavailable.

packages/@lwc/engine-dom/src/styles.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ function createFreshStyleElement(content: string) {
6262
const elm = document.createElement('style');
6363
elm.type = 'text/css';
6464
elm.textContent = content;
65+
// Add an attribute to distinguish global styles added by LWC as opposed to other frameworks/libraries on the page
66+
elm.setAttribute('data-rendered-by-lwc', '');
6567
return elm;
6668
}
6769

packages/@lwc/features/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const features: FeatureFlagMap = {
1818
DISABLE_LIGHT_DOM_UNSCOPED_CSS: null,
1919
ENABLE_FROZEN_TEMPLATE: null,
2020
ENABLE_LEGACY_SCOPE_TOKENS: null,
21+
ENABLE_FORCE_SHADOW_MIGRATE_MODE: null,
2122
};
2223

2324
// eslint-disable-next-line no-restricted-properties

packages/@lwc/features/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ export interface FeatureFlagMap {
6666
*/
6767
// TODO [#3733]: remove support for legacy scope tokens
6868
ENABLE_LEGACY_SCOPE_TOKENS: FeatureFlagValue;
69+
/**
70+
* If true, enable experimental shadow DOM migration mode globally.
71+
*/
72+
ENABLE_FORCE_SHADOW_MIGRATE_MODE: FeatureFlagValue;
6973
}
7074

7175
export type FeatureFlagName = keyof FeatureFlagMap;
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { createElement, setFeatureFlagForTest } from 'lwc';
2+
import Native from 'x/native';
3+
import Synthetic from 'x/synthetic';
4+
import StyledLight from 'x/styledLight';
5+
6+
function doubleMicrotask() {
7+
// wait for applyShadowMigrateMode()
8+
return Promise.resolve().then(() => Promise.resolve());
9+
}
10+
11+
function isActuallyNativeShadow(shadowRoot) {
12+
return shadowRoot.constructor.toString().includes('[native code]');
13+
}
14+
15+
function isClaimingToBeSyntheticShadow(shadowRoot) {
16+
return !!shadowRoot.synthetic;
17+
}
18+
19+
// This test only makes sense for true native shadow mode, because if ENABLE_FORCE_SHADOW_MIGRATE_MODE is true,
20+
// then the polyfill should not be loaded at all.
21+
if (process.env.NATIVE_SHADOW && !process.env.MIXED_SHADOW) {
22+
describe('shadow migrate mode', () => {
23+
beforeEach(async () => {
24+
const style = document.createElement('style');
25+
style.textContent = 'h1 { color: blue }';
26+
document.head.appendChild(style);
27+
});
28+
29+
describe('flag on', () => {
30+
beforeEach(() => {
31+
setFeatureFlagForTest('ENABLE_FORCE_SHADOW_MIGRATE_MODE', true);
32+
});
33+
34+
afterEach(() => {
35+
setFeatureFlagForTest('ENABLE_FORCE_SHADOW_MIGRATE_MODE', false);
36+
});
37+
38+
it('uses global styles for synthetic components', async () => {
39+
const elm = createElement('x-synthetic', { is: Synthetic });
40+
document.body.appendChild(elm);
41+
42+
await doubleMicrotask();
43+
44+
expect(isActuallyNativeShadow(elm.shadowRoot)).toBe(true);
45+
expect(isClaimingToBeSyntheticShadow(elm.shadowRoot)).toBe(true);
46+
expect(getComputedStyle(elm.shadowRoot.querySelector('h1')).color).toBe(
47+
'rgb(0, 0, 255)'
48+
);
49+
});
50+
51+
it('does not use global styles for native components', async () => {
52+
const elm = createElement('x-native', { is: Native });
53+
document.body.appendChild(elm);
54+
55+
await doubleMicrotask();
56+
57+
expect(isActuallyNativeShadow(elm.shadowRoot)).toBe(true);
58+
expect(isClaimingToBeSyntheticShadow(elm.shadowRoot)).toBe(false);
59+
expect(getComputedStyle(elm.shadowRoot.querySelector('h1')).color).toBe(
60+
'rgb(0, 0, 0)'
61+
);
62+
});
63+
64+
it('does not apply styles from global light DOM components to synthetic components', async () => {
65+
const light = createElement('x-styled-light', { is: StyledLight });
66+
document.body.appendChild(light);
67+
68+
const elm = createElement('x-synthetic', { is: Synthetic });
69+
document.body.appendChild(elm);
70+
71+
await doubleMicrotask();
72+
73+
expect(getComputedStyle(elm.shadowRoot.querySelector('h1')).opacity).toBe('1');
74+
expect(getComputedStyle(light.querySelector('h1')).opacity).toBe('0.5');
75+
});
76+
77+
it('uses new styles added to the head after component is rendered', async () => {
78+
const elm = createElement('x-synthetic', { is: Synthetic });
79+
document.body.appendChild(elm);
80+
81+
await doubleMicrotask();
82+
83+
expect(getComputedStyle(elm.shadowRoot.querySelector('h1')).color).toBe(
84+
'rgb(0, 0, 255)'
85+
);
86+
87+
const style = document.createElement('style');
88+
style.textContent = `h1 { color: purple; background-color: crimson }`;
89+
document.head.appendChild(style);
90+
91+
await doubleMicrotask();
92+
93+
expect(getComputedStyle(elm.shadowRoot.querySelector('h1')).color).toBe(
94+
'rgb(128, 0, 128)'
95+
);
96+
expect(getComputedStyle(elm.shadowRoot.querySelector('h1')).backgroundColor).toBe(
97+
'rgb(220, 20, 60)'
98+
);
99+
});
100+
101+
it('local styles are defined after global styles', async () => {
102+
const elm = createElement('x-synthetic', { is: Synthetic });
103+
document.body.appendChild(elm);
104+
105+
const style = document.createElement('style');
106+
style.textContent = `h1 { font-family: sans-serif; background-color: green; }`;
107+
document.head.appendChild(style);
108+
109+
await doubleMicrotask();
110+
111+
expect(getComputedStyle(elm.shadowRoot.querySelector('h1')).backgroundColor).toBe(
112+
'rgb(0, 128, 0)'
113+
);
114+
expect(getComputedStyle(elm.shadowRoot.querySelector('h1')).fontFamily).toBe(
115+
'monospace'
116+
);
117+
});
118+
});
119+
120+
describe('flag off', () => {
121+
it('does not use global styles for synthetic components', async () => {
122+
const elm = createElement('x-synthetic', { is: Synthetic });
123+
document.body.appendChild(elm);
124+
125+
await doubleMicrotask();
126+
expect(isActuallyNativeShadow(elm.shadowRoot)).toBe(true);
127+
expect(isClaimingToBeSyntheticShadow(elm.shadowRoot)).toBe(false);
128+
expect(getComputedStyle(elm.shadowRoot.querySelector('h1')).color).toBe(
129+
'rgb(0, 0, 0)'
130+
);
131+
});
132+
133+
it('does not use global styles for native components', async () => {
134+
const elm = createElement('x-native', { is: Native });
135+
document.body.appendChild(elm);
136+
137+
await doubleMicrotask();
138+
expect(isActuallyNativeShadow(elm.shadowRoot)).toBe(true);
139+
expect(isClaimingToBeSyntheticShadow(elm.shadowRoot)).toBe(false);
140+
expect(getComputedStyle(elm.shadowRoot.querySelector('h1')).color).toBe(
141+
'rgb(0, 0, 0)'
142+
);
143+
});
144+
});
145+
});
146+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
<h1>Hello</h1>
3+
</template>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { LightningElement } from 'lwc';
2+
3+
export default class extends LightningElement {
4+
static shadowSupportMode = 'native';
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
h1 {
2+
opacity: 0.5;
3+
}

0 commit comments

Comments
 (0)