@@ -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,58 @@ 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
124+ const trackedElm = appliedStyles . get ( scopeId ) ;
125+ if ( trackedElm !== undefined ) {
126+ if ( trackedElm === null ) {
127+ // Applied via constructable stylesheet — immune to external DOM removal
128+ return scopeId ;
129+ }
130+ if ( trackedElm . isConnected ) {
131+ // Style element is still in the DOM — update content during HMR only
132+ if ( BUILD . hotModuleReplacement && trackedElm . textContent !== styleText ) {
133+ trackedElm . textContent = styleText ;
134+ }
135+ return scopeId ;
136+ }
137+ // Style element was removed from the DOM by an external framework
138+ // (e.g. SvelteKit head management). Remove stale tracking so we re-apply.
139+ appliedStyles . delete ( scopeId ) ;
77140 }
78141
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 } "]` ) ;
142+ // For shadow roots without constructable stylesheets, check if a shared
143+ // style container was removed and needs to be re-inserted
144+ if ( 'host' in appliedStyleContainer ) {
145+ const sharedContainer = rootAppliedSharedStyleContainers . get ( appliedStyleContainer as ShadowRoot ) ;
146+ if ( sharedContainer && ! sharedContainer . isConnected ) {
147+ appliedStyleContainer . insertBefore ( sharedContainer , appliedStyleContainer . firstChild ) ;
148+ appliedStyles . set ( scopeId , sharedContainer ) ;
149+ return scopeId ;
150+ }
151+ }
152+
153+ // Check for existing hydrated or HMR style element already in the DOM
154+ const existingStyleElm =
155+ ( ( ( BUILD . hydrateClientSide || BUILD . hotModuleReplacement ) &&
156+ appliedStyleContainer . querySelector < HTMLStyleElement > ( `[${ HYDRATED_STYLE_ID } ="${ scopeId } "]` ) ) ||
157+ undefined ) ;
85158
86159 if ( existingStyleElm ) {
87160 // 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 ;
161+ existingStyleElm . textContent = styleText ;
162+ appliedStyles . set ( scopeId , existingStyleElm ) ;
163+ } else {
164+ const styleElm = win . document . createElement ( 'style' ) ;
165+ styleElm . textContent = styleText ;
166+ let trackedStyleElm : HTMLStyleElement | null = styleElm ;
92167
93168 // Apply CSP nonce to the style tag if it exists
94169 const nonce = plt . $nonce$ ?? queryNonceMetaTagContent ( win . document ) ;
@@ -109,21 +184,21 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
109184 * attach styles at the end of the head tag if we render scoped components
110185 */
111186 if ( ! ( cmpMeta . $flags$ & CMP_FLAGS . shadowDomEncapsulation ) ) {
112- if ( styleContainerNode . nodeName === 'HEAD' ) {
187+ if ( appliedStyleContainer . nodeName === 'HEAD' ) {
113188 /**
114189 * if the page contains preconnect links, we want to insert the styles
115190 * after the last preconnect link to ensure the styles are preloaded
116191 */
117- const preconnectLinks = styleContainerNode . querySelectorAll ( 'link[rel=preconnect]' ) ;
192+ const preconnectLinks = appliedStyleContainer . querySelectorAll ( 'link[rel=preconnect]' ) ;
118193 const referenceNode =
119194 preconnectLinks . length > 0
120195 ? preconnectLinks [ preconnectLinks . length - 1 ] . nextSibling
121- : styleContainerNode . querySelector ( 'style' ) ;
122- ( styleContainerNode as HTMLElement ) . insertBefore (
196+ : appliedStyleContainer . querySelector ( 'style' ) ;
197+ ( appliedStyleContainer as HTMLElement ) . insertBefore (
123198 styleElm ,
124- referenceNode ?. parentNode === styleContainerNode ? referenceNode : null ,
199+ referenceNode ?. parentNode === appliedStyleContainer ? referenceNode : null ,
125200 ) ;
126- } else if ( 'host' in styleContainerNode ) {
201+ } else if ( 'host' in appliedStyleContainer ) {
127202 if ( supportsConstructableStylesheets ) {
128203 /**
129204 * If a scoped component is used within a shadow root then turn the styles into a
@@ -135,19 +210,22 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
135210 * Note: constructable stylesheets can't be shared between windows,
136211 * we need to create a new one for the current window if necessary
137212 */
138- const currentWindow = styleContainerNode . defaultView ?? styleContainerNode . ownerDocument . defaultView ;
139- const stylesheet = new currentWindow . CSSStyleSheet ( ) ;
140- stylesheet . replaceSync ( style ) ;
213+ const stylesheet = createShadowRootConstructableStylesheet (
214+ appliedStyleContainer as ShadowRoot ,
215+ styleText ,
216+ ) ;
141217
142218 /**
143219 * > If the array needs to be modified, use in-place mutations like push().
144220 * https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets
145221 */
146222 if ( supportsMutableAdoptedStyleSheets ) {
147- styleContainerNode . adoptedStyleSheets . unshift ( stylesheet ) ;
223+ appliedStyleContainer . adoptedStyleSheets . unshift ( stylesheet ) ;
148224 } else {
149- styleContainerNode . adoptedStyleSheets = [ stylesheet , ...styleContainerNode . adoptedStyleSheets ] ;
225+ appliedStyleContainer . adoptedStyleSheets = [ stylesheet , ...appliedStyleContainer . adoptedStyleSheets ] ;
150226 }
227+
228+ trackedStyleElm = null ;
151229 } else {
152230 /**
153231 * If a scoped component is used within a shadow root and constructable stylesheets are
@@ -162,38 +240,34 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
162240 * During HMR, create separate style elements for scoped components so they can be
163241 * updated independently without affecting other components' styles.
164242 */
165- const existingStyleContainer : HTMLStyleElement = styleContainerNode . querySelector ( 'style' ) ;
243+ const existingStyleContainer = appliedStyleContainer . querySelector < HTMLStyleElement > ( 'style' ) ;
166244 if ( existingStyleContainer && ! BUILD . hotModuleReplacement ) {
167- existingStyleContainer . textContent = style + existingStyleContainer . textContent ;
245+ existingStyleContainer . textContent = styleText + existingStyleContainer . textContent ;
246+ existingStyleContainer . removeAttribute ( HYDRATED_STYLE_ID ) ;
247+ rootAppliedSharedStyleContainers . set ( appliedStyleContainer as ShadowRoot , existingStyleContainer ) ;
248+ trackedStyleElm = existingStyleContainer ;
168249 } else {
169- ( styleContainerNode as HTMLElement ) . prepend ( styleElm ) ;
250+ appliedStyleContainer . insertBefore ( styleElm , appliedStyleContainer . firstChild ) ;
170251 }
171252 }
172253 } else {
173- styleContainerNode . append ( styleElm ) ;
254+ appliedStyleContainer . append ( styleElm ) ;
174255 }
175256 }
176257
177258 /**
178259 * attach styles at the beginning of a shadow root node if we render shadow components
179260 */
180261 if ( cmpMeta . $flags$ & CMP_FLAGS . shadowDomEncapsulation ) {
181- styleContainerNode . insertBefore ( styleElm , null ) ;
262+ appliedStyleContainer . insertBefore ( styleElm , null ) ;
182263 }
183264
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- }
265+ appliedStyles . set ( scopeId , trackedStyleElm ) ;
192266 }
193267 } else if ( BUILD . constructableCSS ) {
194268 let appliedStyles = rootAppliedStyles . get ( styleContainerNode ) ;
195269 if ( ! appliedStyles ) {
196- rootAppliedStyles . set ( styleContainerNode , ( appliedStyles = new Set ( ) ) ) ;
270+ rootAppliedStyles . set ( styleContainerNode , ( appliedStyles = new Map ( ) ) ) ;
197271 }
198272 if ( ! appliedStyles . has ( scopeId ) ) {
199273 /**
@@ -220,12 +294,13 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
220294 styleContainerNode . adoptedStyleSheets = [ ...styleContainerNode . adoptedStyleSheets , stylesheet ] ;
221295 }
222296
223- appliedStyles . add ( scopeId ) ;
297+ appliedStyles . set ( scopeId , null ) ;
224298
225299 // Remove SSR style element from shadow root now that adoptedStyleSheets is in use
226300 // Only remove from shadow roots, not from document head (for scoped components)
227301 if ( BUILD . hydrateClientSide && 'host' in styleContainerNode ) {
228- const ssrStyleElm = styleContainerNode . querySelector ( `[${ HYDRATED_STYLE_ID } ="${ scopeId } "]` ) ;
302+ const shadowRoot = styleContainerNode as ShadowRoot ;
303+ const ssrStyleElm = shadowRoot . querySelector < HTMLStyleElement > ( `[${ HYDRATED_STYLE_ID } ="${ scopeId } "]` ) ;
229304 if ( ssrStyleElm ) {
230305 writeTask ( ( ) => ssrStyleElm . remove ( ) ) ;
231306 }
0 commit comments