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