Skip to content

Commit 570c646

Browse files
committed
fix(runtime): re-inject styles removed from DOM by external frameworks (#6637)
Track style elements in rootAppliedStyles Map and verify DOM presence before skipping re-injection. Frameworks like SvelteKit may remove Stencil-injected <style> elements from <head> during hydration. - Change RootAppliedStyleMap from Set<string> to Map<string, HTMLStyleElement | null> - Verify tracked element parentNode before treating style as applied - null value indicates constructable stylesheet (cannot be externally removed) - Add regression test for style re-attachment after removal
1 parent 02f91b3 commit 570c646

File tree

3 files changed

+59
-15
lines changed

3 files changed

+59
-15
lines changed

src/declarations/stencil-private.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1927,7 +1927,7 @@ export interface PlatformRuntime {
19271927

19281928
export type StyleMap = Map<string, CSSStyleSheet | string>;
19291929

1930-
export type RootAppliedStyleMap = WeakMap<Element, Set<string>>;
1930+
export type RootAppliedStyleMap = WeakMap<Element | ShadowRoot, Map<string, HTMLStyleElement | null>>;
19311931

19321932
export interface ScreenshotConnector {
19331933
initBuild(opts: ScreenshotConnectorOptions): Promise<void>;

src/runtime/styles.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,22 +73,30 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
7373
let appliedStyles = rootAppliedStyles.get(styleContainerNode);
7474
let styleElm;
7575
if (!appliedStyles) {
76-
rootAppliedStyles.set(styleContainerNode, (appliedStyles = new Set()));
76+
rootAppliedStyles.set(styleContainerNode, (appliedStyles = new Map()));
7777
}
7878

79-
// Check if style element already exists (for HMR updates)
80-
// For shadow DOM components, directly update their dedicated style element
81-
// For scoped components, check if they have their own HMR-created style element
82-
const existingStyleElm: HTMLStyleElement =
83-
(BUILD.hydrateClientSide || BUILD.hotModuleReplacement) &&
84-
styleContainerNode.querySelector(`[${HYDRATED_STYLE_ID}="${scopeId}"]`);
79+
// Check if tracked element is still in the DOM (fixes #6637)
80+
const trackedElm = appliedStyles.get(scopeId);
81+
if (trackedElm !== undefined) {
82+
if (trackedElm === null || trackedElm.parentNode === styleContainerNode) {
83+
return scopeId;
84+
}
85+
appliedStyles.delete(scopeId);
86+
}
87+
88+
const existingStyleElm: HTMLStyleElement | undefined =
89+
((BUILD.hydrateClientSide || BUILD.hotModuleReplacement) &&
90+
styleContainerNode.querySelector(`[${HYDRATED_STYLE_ID}="${scopeId}"]`)) ||
91+
undefined;
8592

8693
if (existingStyleElm) {
87-
// Update existing style element (for hydration or HMR)
8894
existingStyleElm.textContent = style;
89-
} else if (!appliedStyles.has(scopeId)) {
95+
appliedStyles.set(scopeId, existingStyleElm);
96+
} else {
9097
styleElm = win.document.createElement('style');
9198
styleElm.textContent = style;
99+
let appliedStyleElm = styleElm;
92100

93101
// Apply CSP nonce to the style tag if it exists
94102
const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(win.document);
@@ -165,6 +173,7 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
165173
const existingStyleContainer: HTMLStyleElement = styleContainerNode.querySelector('style');
166174
if (existingStyleContainer && !BUILD.hotModuleReplacement) {
167175
existingStyleContainer.textContent = style + existingStyleContainer.textContent;
176+
appliedStyleElm = existingStyleContainer;
168177
} else {
169178
(styleContainerNode as HTMLElement).prepend(styleElm);
170179
}
@@ -186,14 +195,12 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
186195
styleElm.textContent += SLOT_FB_CSS;
187196
}
188197

189-
if (appliedStyles) {
190-
appliedStyles.add(scopeId);
191-
}
198+
appliedStyles.set(scopeId, appliedStyleElm);
192199
}
193200
} else if (BUILD.constructableCSS) {
194201
let appliedStyles = rootAppliedStyles.get(styleContainerNode);
195202
if (!appliedStyles) {
196-
rootAppliedStyles.set(styleContainerNode, (appliedStyles = new Set()));
203+
rootAppliedStyles.set(styleContainerNode, (appliedStyles = new Map()));
197204
}
198205
if (!appliedStyles.has(scopeId)) {
199206
/**
@@ -220,7 +227,7 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
220227
styleContainerNode.adoptedStyleSheets = [...styleContainerNode.adoptedStyleSheets, stylesheet];
221228
}
222229

223-
appliedStyles.add(scopeId);
230+
appliedStyles.set(scopeId, null);
224231

225232
// Remove SSR style element from shadow root now that adoptedStyleSheets is in use
226233
// Only remove from shadow roots, not from document head (for scoped components)

src/runtime/test/style.spec.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,43 @@ describe('style', () => {
5656
);
5757
});
5858

59+
it('re-attaches a removed style element when the component is rendered again', async () => {
60+
@Component({
61+
tag: 'cmp-a',
62+
styles: `
63+
cmp-a {
64+
color: red;
65+
}
66+
`,
67+
})
68+
class CmpA {
69+
render() {
70+
return `innertext`;
71+
}
72+
}
73+
74+
const page = await newSpecPage({
75+
components: [CmpA],
76+
html: `<cmp-a></cmp-a>`,
77+
attachStyles: true,
78+
});
79+
80+
const findCmpStyle = () =>
81+
Array.from(page.doc.head.querySelectorAll('style')).find((styleElm) => styleElm.textContent?.includes('color: red'));
82+
83+
const initialStyleElm = findCmpStyle();
84+
expect(initialStyleElm).toBeDefined();
85+
86+
initialStyleElm!.remove();
87+
expect(findCmpStyle()).toBeUndefined();
88+
89+
await page.setContent(`<cmp-a></cmp-a>`);
90+
91+
const reattachedStyleElm = findCmpStyle();
92+
expect(reattachedStyleElm).toBeDefined();
93+
expect(reattachedStyleElm!.isConnected).toBe(true);
94+
});
95+
5996
describe('mode', () => {
6097
it('md mode', async () => {
6198
setMode(() => 'md');

0 commit comments

Comments
 (0)