@@ -106,6 +106,12 @@ function modifiesDom() {
106106
107107export class NativeScriptRendererFactory implements RendererFactory2 {
108108 private componentRenderers = new Map < string , Renderer2 > ( ) ;
109+ // Signature of the styles last applied for each component `type.id`. Used to
110+ // detect a `styleUrls`/`styles` change across an `replaceMetadata` HMR
111+ // update so the cached renderer can re-apply the new (scoped) styles - the
112+ // renderer cache below otherwise short-circuits `addStyles`, so a component
113+ // style edit would never take effect without a full re-bootstrap.
114+ private componentStyleSignatures = new Map < string , string > ( ) ;
109115 private defaultRenderer : Renderer2 ;
110116 // backwards compatibility with RadListView
111117 private rootView = inject ( APP_ROOT_VIEW ) ;
@@ -151,6 +157,25 @@ export class NativeScriptRendererFactory implements RendererFactory2 {
151157 renderer . applyToHost ( hostElement ) ;
152158 }
153159
160+ // HMR: a component `styleUrls`/`styles` edit recompiles the component
161+ // metadata and `replaceMetadata` recreates its views, which re-enters
162+ // `createRenderer` with the SAME `type.id` but NEW `type.styles`. The
163+ // cache hit above would otherwise return the renderer whose one-time
164+ // `addStyles` already ran with the OLD styles, so the change would never
165+ // render. When the style signature changed, re-apply: emulated styles
166+ // are re-scoped + re-added (same selector/specificity -> later wins);
167+ // None-encapsulation styles are re-added globally. Both keep the shared
168+ // `rootModuleID` tag so module teardown still removes them.
169+ const styleSignature = this . styleSignature ( type . styles ) ;
170+ if ( this . componentStyleSignatures . get ( type . id ) !== styleSignature ) {
171+ this . componentStyleSignatures . set ( type . id , styleSignature ) ;
172+ if ( renderer instanceof EmulatedRenderer ) {
173+ renderer . reapplyStyles ( type . styles ) ;
174+ } else {
175+ this . reapplyGlobalStyles ( type . styles ) ;
176+ }
177+ }
178+
154179 return renderer ;
155180 }
156181
@@ -165,8 +190,31 @@ export class NativeScriptRendererFactory implements RendererFactory2 {
165190 }
166191
167192 this . componentRenderers . set ( type . id , renderer ) ;
193+ this . componentStyleSignatures . set ( type . id , this . styleSignature ( type . styles ) ) ;
168194 return renderer ;
169195 }
196+
197+ // Stable signature of a component's styles, used to detect HMR style edits.
198+ private styleSignature ( styles : ( string | any [ ] ) [ ] ) : string {
199+ try {
200+ return ( styles || [ ] ) . map ( ( s ) => s . toString ( ) ) . join ( "\n" ) ;
201+ } catch {
202+ return '' ;
203+ }
204+ }
205+
206+ // Re-apply ViewEncapsulation.None component styles (global, unscoped) on an
207+ // HMR style edit and re-trigger styling on the live view tree.
208+ private reapplyGlobalStyles ( styles : ( string | any [ ] ) [ ] ) : void {
209+ try {
210+ styles . map ( ( s ) => s . toString ( ) ) . forEach ( ( v ) => addStyleToCss ( v , this . rootModuleID ) ) ;
211+ Application . getRootView ( ) ?. _onCssStateChange ( ) ;
212+ } catch ( err ) {
213+ if ( NativeScriptDebug . enabled ) {
214+ NativeScriptDebug . rendererLog ( `reapplyGlobalStyles failed: ${ err } ` ) ;
215+ }
216+ }
217+ }
170218 begin ( ) {
171219 if ( __APPLE__ && this . wrapCdInTransaction ) {
172220 if ( this . cdDepth > 0 ) {
@@ -484,17 +532,40 @@ const addScopedStyleToCss = profile(
484532export class EmulatedRenderer extends NativeScriptRenderer {
485533 private contentAttr : string ;
486534 private hostAttr : string ;
535+ private componentId : string ;
487536 private rootModuleId = inject ( NATIVESCRIPT_ROOT_MODULE_ID ) ;
488537
489538 constructor ( component : RendererType2 , rootView : View ) {
490539 super ( rootView ) ;
491540
492541 const componentId = component . id . replace ( ATTR_SANITIZER , '_' ) ;
542+ this . componentId = componentId ;
493543 this . contentAttr = replaceNgAttribute ( CONTENT_ATTR , componentId ) ;
494544 this . hostAttr = replaceNgAttribute ( HOST_ATTR , componentId ) ;
495545 this . addStyles ( component . styles , componentId ) ;
496546 }
497547
548+ /**
549+ * Re-apply this component's emulated-scoped styles after an HMR
550+ * `styleUrls`/`styles` edit. The renderer is cached per component type id
551+ * (see `NativeScriptRendererFactory.createRenderer`), so the constructor's
552+ * one-time `addStyles` never re-runs on `replaceMetadata` - without this
553+ * the new styles never reach the device. The freshly-compiled rules are
554+ * re-scoped to this renderer's component id (so the existing views, which
555+ * carry that `_ngcontent` attribute, match) and re-added under the same
556+ * `rootModuleId` tag; since they share the previous rules' selector and
557+ * specificity, the later-added values win. We then re-trigger styling on
558+ * the live view tree so the change paints without a re-bootstrap.
559+ */
560+ reapplyStyles ( styles : ( string | any [ ] ) [ ] ) : void {
561+ this . addStyles ( styles , this . componentId ) ;
562+ try {
563+ Application . getRootView ( ) ?. _onCssStateChange ( ) ;
564+ } catch {
565+ // best-effort restyle; never let an HMR style re-apply throw
566+ }
567+ }
568+
498569 applyToHost ( view : NgView ) {
499570 super . setAttribute ( view , this . hostAttr , '' ) ;
500571 }
0 commit comments