@@ -15,8 +15,51 @@ import { queryNonceMetaTagContent } from '../utils/query-nonce-meta-tag-content'
1515import { createTime } from './profile' ;
1616import { HYDRATED_STYLE_ID , NODE_TYPE , SLOT_FB_CSS } from './runtime-constants' ;
1717
18+ /**
19+ * Tracks which style scopeIds have been applied to each container, along with
20+ * a reference to the associated `<style>` DOM element (or `null` for styles
21+ * applied via constructable stylesheets, which are immune to external removal).
22+ *
23+ * Using a `Map<string, HTMLStyleElement | null>` instead of a plain `Set<string>`
24+ * lets us verify that a tracked `<style>` element is still in the DOM before
25+ * treating it as "applied". This fixes the issue where an external framework
26+ * (e.g. SvelteKit's head management) removes Stencil's `<style>` elements but
27+ * the in-memory cache still considers them applied.
28+ */
1829export const rootAppliedStyles : d . RootAppliedStyleMap = /*@__PURE__ */ new WeakMap ( ) ;
1930
31+ /**
32+ * Tracks shared `<style>` containers in shadow roots for the fallback case
33+ * where constructable stylesheets are not supported and multiple scoped
34+ * components merge their styles into a single `<style>` element.
35+ *
36+ * This is a separate WeakMap because the mapping is per-shadow-root (not
37+ * per-scopeId) and the shared container needs to be re-inserted as a whole
38+ * if removed externally, preserving all merged styles.
39+ */
40+ export const rootAppliedSharedStyleContainers : WeakMap < ShadowRoot , HTMLStyleElement > = /*@__PURE__ */ new WeakMap ( ) ;
41+
42+ type ConstructableStylesheetWindow = Pick < typeof globalThis , 'CSSStyleSheet' > ;
43+
44+ /**
45+ * Build the full style text for a component, including slot fallback CSS
46+ * (`slot-fb{display:contents}`) when the component uses slot relocation.
47+ */
48+ const getStyleText = ( cmpMeta : d . ComponentRuntimeMeta , style : string ) =>
49+ cmpMeta . $flags$ & CMP_FLAGS . hasSlotRelocation ? style + SLOT_FB_CSS : style ;
50+
51+ /**
52+ * Create a constructable stylesheet scoped to a shadow root's window context.
53+ * Falls back to the platform window if the owner document has no `defaultView`
54+ * (e.g. detached documents).
55+ */
56+ export const createShadowRootConstructableStylesheet = ( shadowRoot : ShadowRoot , styleText : string ) => {
57+ const currentWindow = ( shadowRoot . ownerDocument . defaultView ?? win ) as unknown as ConstructableStylesheetWindow ;
58+ const stylesheet = new currentWindow . CSSStyleSheet ( ) ;
59+ stylesheet . replaceSync ( styleText ) ;
60+ return stylesheet ;
61+ } ;
62+
2063/**
2164 * Register the styles for a component by creating a stylesheet and then
2265 * registering it under the component's scope ID in a `WeakMap` for later use.
@@ -69,26 +112,61 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
69112
70113 if ( style ) {
71114 if ( typeof style === 'string' ) {
72- styleContainerNode = styleContainerNode . head || ( styleContainerNode as HTMLElement ) ;
73- let appliedStyles = rootAppliedStyles . get ( styleContainerNode ) ;
74- let styleElm ;
115+ const styleText = getStyleText ( cmpMeta , style ) ;
116+ const appliedStyleContainer = ( styleContainerNode . head || styleContainerNode ) as Element | ShadowRoot ;
117+ let appliedStyles = rootAppliedStyles . get ( appliedStyleContainer ) ;
118+
75119 if ( ! appliedStyles ) {
76- rootAppliedStyles . set ( styleContainerNode , ( appliedStyles = new Set ( ) ) ) ;
120+ rootAppliedStyles . set ( appliedStyleContainer , ( appliedStyles = new Map ( ) ) ) ;
121+ }
122+
123+ // Check if this scopeId has a tracked style element that is still
124+ // in its expected container. Using parentNode instead of isConnected
125+ // because the element may be in a shadow root whose host is not yet
126+ // connected to the document (e.g. during initial render).
127+ const trackedElm = appliedStyles . get ( scopeId ) ;
128+ if ( trackedElm !== undefined ) {
129+ if ( trackedElm === null ) {
130+ // Applied via constructable stylesheet — immune to external DOM removal
131+ return scopeId ;
132+ }
133+ if ( trackedElm . parentNode === appliedStyleContainer ) {
134+ // Style element is still in the expected container — update content during HMR only
135+ if ( BUILD . hotModuleReplacement && trackedElm . textContent !== styleText ) {
136+ trackedElm . textContent = styleText ;
137+ }
138+ return scopeId ;
139+ }
140+ // Style element was removed from the DOM by an external framework
141+ // (e.g. SvelteKit head management). Remove stale tracking so we re-apply.
142+ appliedStyles . delete ( scopeId ) ;
77143 }
78144
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 } "]` ) ;
145+ // For shadow roots without constructable stylesheets, check if a shared
146+ // style container was removed and needs to be re-inserted
147+ if ( 'host' in appliedStyleContainer ) {
148+ const sharedContainer = rootAppliedSharedStyleContainers . get ( appliedStyleContainer as ShadowRoot ) ;
149+ if ( sharedContainer && sharedContainer . parentNode !== appliedStyleContainer ) {
150+ appliedStyleContainer . insertBefore ( sharedContainer , appliedStyleContainer . firstChild ) ;
151+ appliedStyles . set ( scopeId , sharedContainer ) ;
152+ return scopeId ;
153+ }
154+ }
155+
156+ // Check for existing hydrated or HMR style element already in the DOM
157+ const existingStyleElm =
158+ ( ( ( BUILD . hydrateClientSide || BUILD . hotModuleReplacement ) &&
159+ appliedStyleContainer . querySelector < HTMLStyleElement > ( `[${ HYDRATED_STYLE_ID } ="${ scopeId } "]` ) ) ||
160+ undefined ) ;
85161
86162 if ( existingStyleElm ) {
87163 // Update existing style element (for hydration or HMR)
88- existingStyleElm . textContent = style ;
89- } else if ( ! appliedStyles . has ( scopeId ) ) {
90- styleElm = win . document . createElement ( 'style' ) ;
91- styleElm . textContent = style ;
164+ existingStyleElm . textContent = styleText ;
165+ appliedStyles . set ( scopeId , existingStyleElm ) ;
166+ } else {
167+ const styleElm = win . document . createElement ( 'style' ) ;
168+ styleElm . textContent = styleText ;
169+ let trackedStyleElm : HTMLStyleElement | null = styleElm ;
92170
93171 // Apply CSP nonce to the style tag if it exists
94172 const nonce = plt . $nonce$ ?? queryNonceMetaTagContent ( win . document ) ;
@@ -109,21 +187,21 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
109187 * attach styles at the end of the head tag if we render scoped components
110188 */
111189 if ( ! ( cmpMeta . $flags$ & CMP_FLAGS . shadowDomEncapsulation ) ) {
112- if ( styleContainerNode . nodeName === 'HEAD' ) {
190+ if ( appliedStyleContainer . nodeName === 'HEAD' ) {
113191 /**
114192 * if the page contains preconnect links, we want to insert the styles
115193 * after the last preconnect link to ensure the styles are preloaded
116194 */
117- const preconnectLinks = styleContainerNode . querySelectorAll ( 'link[rel=preconnect]' ) ;
195+ const preconnectLinks = appliedStyleContainer . querySelectorAll ( 'link[rel=preconnect]' ) ;
118196 const referenceNode =
119197 preconnectLinks . length > 0
120198 ? preconnectLinks [ preconnectLinks . length - 1 ] . nextSibling
121- : styleContainerNode . querySelector ( 'style' ) ;
122- ( styleContainerNode as HTMLElement ) . insertBefore (
199+ : appliedStyleContainer . querySelector ( 'style' ) ;
200+ ( appliedStyleContainer as HTMLElement ) . insertBefore (
123201 styleElm ,
124- referenceNode ?. parentNode === styleContainerNode ? referenceNode : null ,
202+ referenceNode ?. parentNode === appliedStyleContainer ? referenceNode : null ,
125203 ) ;
126- } else if ( 'host' in styleContainerNode ) {
204+ } else if ( 'host' in appliedStyleContainer ) {
127205 if ( supportsConstructableStylesheets ) {
128206 /**
129207 * If a scoped component is used within a shadow root then turn the styles into a
@@ -135,19 +213,22 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
135213 * Note: constructable stylesheets can't be shared between windows,
136214 * we need to create a new one for the current window if necessary
137215 */
138- const currentWindow = styleContainerNode . defaultView ?? styleContainerNode . ownerDocument . defaultView ;
139- const stylesheet = new currentWindow . CSSStyleSheet ( ) ;
140- stylesheet . replaceSync ( style ) ;
216+ const stylesheet = createShadowRootConstructableStylesheet (
217+ appliedStyleContainer as ShadowRoot ,
218+ styleText ,
219+ ) ;
141220
142221 /**
143222 * > If the array needs to be modified, use in-place mutations like push().
144223 * https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets
145224 */
146225 if ( supportsMutableAdoptedStyleSheets ) {
147- styleContainerNode . adoptedStyleSheets . unshift ( stylesheet ) ;
226+ appliedStyleContainer . adoptedStyleSheets . unshift ( stylesheet ) ;
148227 } else {
149- styleContainerNode . adoptedStyleSheets = [ stylesheet , ...styleContainerNode . adoptedStyleSheets ] ;
228+ appliedStyleContainer . adoptedStyleSheets = [ stylesheet , ...appliedStyleContainer . adoptedStyleSheets ] ;
150229 }
230+
231+ trackedStyleElm = null ;
151232 } else {
152233 /**
153234 * If a scoped component is used within a shadow root and constructable stylesheets are
@@ -162,38 +243,34 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
162243 * During HMR, create separate style elements for scoped components so they can be
163244 * updated independently without affecting other components' styles.
164245 */
165- const existingStyleContainer : HTMLStyleElement = styleContainerNode . querySelector ( 'style' ) ;
246+ const existingStyleContainer = appliedStyleContainer . querySelector < HTMLStyleElement > ( 'style' ) ;
166247 if ( existingStyleContainer && ! BUILD . hotModuleReplacement ) {
167- existingStyleContainer . textContent = style + existingStyleContainer . textContent ;
248+ existingStyleContainer . textContent = styleText + existingStyleContainer . textContent ;
249+ existingStyleContainer . removeAttribute ( HYDRATED_STYLE_ID ) ;
250+ rootAppliedSharedStyleContainers . set ( appliedStyleContainer as ShadowRoot , existingStyleContainer ) ;
251+ trackedStyleElm = existingStyleContainer ;
168252 } else {
169- ( styleContainerNode as HTMLElement ) . prepend ( styleElm ) ;
253+ appliedStyleContainer . insertBefore ( styleElm , appliedStyleContainer . firstChild ) ;
170254 }
171255 }
172256 } else {
173- styleContainerNode . append ( styleElm ) ;
257+ appliedStyleContainer . append ( styleElm ) ;
174258 }
175259 }
176260
177261 /**
178262 * attach styles at the beginning of a shadow root node if we render shadow components
179263 */
180264 if ( cmpMeta . $flags$ & CMP_FLAGS . shadowDomEncapsulation ) {
181- styleContainerNode . insertBefore ( styleElm , null ) ;
265+ appliedStyleContainer . insertBefore ( styleElm , null ) ;
182266 }
183267
184- // Add styles for `slot-fb` elements if we're using slots outside the Shadow DOM
185- if ( cmpMeta . $flags$ & CMP_FLAGS . hasSlotRelocation ) {
186- styleElm . textContent += SLOT_FB_CSS ;
187- }
188-
189- if ( appliedStyles ) {
190- appliedStyles . add ( scopeId ) ;
191- }
268+ appliedStyles . set ( scopeId , trackedStyleElm ) ;
192269 }
193270 } else if ( BUILD . constructableCSS ) {
194271 let appliedStyles = rootAppliedStyles . get ( styleContainerNode ) ;
195272 if ( ! appliedStyles ) {
196- rootAppliedStyles . set ( styleContainerNode , ( appliedStyles = new Set ( ) ) ) ;
273+ rootAppliedStyles . set ( styleContainerNode , ( appliedStyles = new Map ( ) ) ) ;
197274 }
198275 if ( ! appliedStyles . has ( scopeId ) ) {
199276 /**
@@ -220,12 +297,13 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
220297 styleContainerNode . adoptedStyleSheets = [ ...styleContainerNode . adoptedStyleSheets , stylesheet ] ;
221298 }
222299
223- appliedStyles . add ( scopeId ) ;
300+ appliedStyles . set ( scopeId , null ) ;
224301
225302 // Remove SSR style element from shadow root now that adoptedStyleSheets is in use
226303 // Only remove from shadow roots, not from document head (for scoped components)
227304 if ( BUILD . hydrateClientSide && 'host' in styleContainerNode ) {
228- const ssrStyleElm = styleContainerNode . querySelector ( `[${ HYDRATED_STYLE_ID } ="${ scopeId } "]` ) ;
305+ const shadowRoot = styleContainerNode as ShadowRoot ;
306+ const ssrStyleElm = shadowRoot . querySelector < HTMLStyleElement > ( `[${ HYDRATED_STYLE_ID } ="${ scopeId } "]` ) ;
229307 if ( ssrStyleElm ) {
230308 writeTask ( ( ) => ssrStyleElm . remove ( ) ) ;
231309 }
0 commit comments