Skip to content

Commit c98184e

Browse files
committed
feat(angular): improve vite hmr runtime wiring
1 parent 6b34841 commit c98184e

5 files changed

Lines changed: 182 additions & 99 deletions

File tree

packages/angular/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@nativescript/angular",
3-
"version": "21.0.0",
3+
"version": "21.0.1-alpha.5",
44
"homepage": "https://nativescript.org/",
55
"repository": {
66
"type": "git",

packages/angular/src/lib/application.ts

Lines changed: 89 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,12 @@ export function runNativeScriptAngularApp<T, K>(options: AppRunOptions<T, K>) {
278278
}
279279
const view = ref.injector.get(APP_ROOT_VIEW) as AppHostView | View;
280280
const newRoot = view instanceof AppHostView ? view.content : view;
281-
console.log('[ng-hmr] setRootView: view from injector:', view?.constructor?.name, 'newRoot:', newRoot?.constructor?.name);
281+
console.log(
282+
'[ng-hmr] setRootView: view from injector:',
283+
view?.constructor?.name,
284+
'newRoot:',
285+
newRoot?.constructor?.name,
286+
);
282287
console.log('[ng-hmr] setRootView: launchEventDone:', launchEventDone, 'embedded:', options.embedded);
283288
if (NativeScriptDebug.isLogEnabled()) {
284289
NativeScriptDebug.bootstrapLog(`Setting RootView to ${newRoot}`);
@@ -340,67 +345,92 @@ export function runNativeScriptAngularApp<T, K>(options: AppRunOptions<T, K>) {
340345
ref.destroy();
341346
return;
342347
}
343-
mainModuleRef = ref;
344-
345-
// Expose ApplicationRef for HMR to trigger change detection
346-
// Check for ApplicationRef by duck-typing since instanceof can fail across module realms
347-
const refAny = ref as any;
348-
const isAppRef = refAny && typeof refAny.tick === 'function' && Array.isArray(refAny.components);
349-
console.log('[ng-hmr] ref type check: isAppRef=', isAppRef, 'has tick=', typeof refAny?.tick === 'function', 'has components=', Array.isArray(refAny?.components));
350-
351-
if (isAppRef) {
352-
global['__NS_ANGULAR_APP_REF__'] = ref;
353-
// Mark boot complete for the HMR system
354-
global['__NS_HMR_BOOT_COMPLETE__'] = true;
355-
356-
// Register bootstrapped components for HMR lookup
357-
if (!global['__NS_ANGULAR_COMPONENTS__']) {
358-
global['__NS_ANGULAR_COMPONENTS__'] = {};
348+
349+
// When Zone.js is active and we're outside the Angular zone (which
350+
// happens in HMR mode — the Promise .then() runs in the root zone),
351+
// wrap the completion handler inside NgZone.run() so that:
352+
// 1. resetRootView + component initialization happens inside the Angular zone
353+
// 2. ngrx effects, store dispatches, and signal-triggered actions run inside NgZone
354+
// 3. strictActionWithinNgZone checks pass for initial actions
355+
// In zoneless apps (no Zone.js), skip the wrapping entirely.
356+
const useZoneWrap = typeof Zone !== 'undefined' && !NgZone.isInAngularZone();
357+
const runInZone = (fn: () => void) => {
358+
if (useZoneWrap) {
359+
ref.injector.get(NgZone).run(fn);
360+
} else {
361+
fn();
359362
}
360-
// Get the component class from the first bootstrapped component
361-
console.log('[ng-hmr] ApplicationRef components count:', refAny.components?.length);
362-
if (refAny.components && refAny.components.length > 0) {
363-
const componentRef = refAny.components[0];
364-
console.log('[ng-hmr] componentRef:', componentRef?.constructor?.name);
365-
console.log('[ng-hmr] componentRef.componentType:', componentRef?.componentType?.name);
366-
367-
// For Angular 17+ standalone components, the component type is on componentRef.componentType
368-
// For older Angular, try componentRef.instance.constructor
369-
let componentType = componentRef?.componentType;
370-
if (!componentType && componentRef?.instance) {
371-
componentType = componentRef.instance.constructor;
363+
};
364+
runInZone(() => {
365+
mainModuleRef = ref;
366+
367+
// Expose ApplicationRef for HMR to trigger change detection
368+
// Check for ApplicationRef by duck-typing since instanceof can fail across module realms
369+
const refAny = ref as any;
370+
const isAppRef = refAny && typeof refAny.tick === 'function' && Array.isArray(refAny.components);
371+
console.log(
372+
'[ng-hmr] ref type check: isAppRef=',
373+
isAppRef,
374+
'has tick=',
375+
typeof refAny?.tick === 'function',
376+
'has components=',
377+
Array.isArray(refAny?.components),
378+
);
379+
380+
if (isAppRef) {
381+
global['__NS_ANGULAR_APP_REF__'] = ref;
382+
// Mark boot complete for the HMR system
383+
global['__NS_HMR_BOOT_COMPLETE__'] = true;
384+
385+
// Register bootstrapped components for HMR lookup
386+
if (!global['__NS_ANGULAR_COMPONENTS__']) {
387+
global['__NS_ANGULAR_COMPONENTS__'] = {};
372388
}
373-
374-
if (componentType && componentType.name) {
375-
global['__NS_ANGULAR_COMPONENTS__'][componentType.name] = componentType;
376-
console.log('[ng-hmr] Registered component for HMR:', componentType.name);
389+
// Get the component class from the first bootstrapped component
390+
console.log('[ng-hmr] ApplicationRef components count:', refAny.components?.length);
391+
if (refAny.components && refAny.components.length > 0) {
392+
const componentRef = refAny.components[0];
393+
console.log('[ng-hmr] componentRef:', componentRef?.constructor?.name);
394+
console.log('[ng-hmr] componentRef.componentType:', componentRef?.componentType?.name);
395+
396+
// For Angular 17+ standalone components, the component type is on componentRef.componentType
397+
// For older Angular, try componentRef.instance.constructor
398+
let componentType = componentRef?.componentType;
399+
if (!componentType && componentRef?.instance) {
400+
componentType = componentRef.instance.constructor;
401+
}
402+
403+
if (componentType && componentType.name) {
404+
global['__NS_ANGULAR_COMPONENTS__'][componentType.name] = componentType;
405+
console.log('[ng-hmr] Registered component for HMR:', componentType.name);
406+
} else {
407+
console.log('[ng-hmr] Could not get componentType name');
408+
}
377409
} else {
378-
console.log('[ng-hmr] Could not get componentType name');
410+
console.log('[ng-hmr] No components in ApplicationRef');
379411
}
380412
} else {
381-
console.log('[ng-hmr] No components in ApplicationRef');
382-
}
383-
} else {
384-
const appRef = ref.injector.get(ApplicationRef, null);
385-
if (appRef) {
386-
global['__NS_ANGULAR_APP_REF__'] = appRef;
387-
// Mark boot complete for the HMR system
388-
global['__NS_HMR_BOOT_COMPLETE__'] = true;
413+
const appRef = ref.injector.get(ApplicationRef, null);
414+
if (appRef) {
415+
global['__NS_ANGULAR_APP_REF__'] = appRef;
416+
// Mark boot complete for the HMR system
417+
global['__NS_HMR_BOOT_COMPLETE__'] = true;
418+
}
389419
}
390-
}
391420

392-
(isAppRef ? refAny.components[0] : ref).onDestroy(
393-
() => (mainModuleRef = mainModuleRef === ref ? null : mainModuleRef),
394-
);
395-
updatePlatformRef(ref, reason);
396-
const styleTag = ref.injector.get(NATIVESCRIPT_ROOT_MODULE_ID);
397-
(isAppRef ? refAny.components[0] : ref).onDestroy(() => {
398-
removeTaggedAdditionalCSS(styleTag);
421+
(isAppRef ? refAny.components[0] : ref).onDestroy(
422+
() => (mainModuleRef = mainModuleRef === ref ? null : mainModuleRef),
423+
);
424+
updatePlatformRef(ref, reason);
425+
const styleTag = ref.injector.get(NATIVESCRIPT_ROOT_MODULE_ID);
426+
(isAppRef ? refAny.components[0] : ref).onDestroy(() => {
427+
removeTaggedAdditionalCSS(styleTag);
428+
});
429+
bootstrapped = true;
430+
onMainBootstrap();
431+
emitModuleBootstrapEvent(ref, 'main', reason);
432+
// bootstrapped component: (ref as any)._bootstrapComponents[0];
399433
});
400-
bootstrapped = true;
401-
onMainBootstrap();
402-
emitModuleBootstrapEvent(ref, 'main', reason);
403-
// bootstrapped component: (ref as any)._bootstrapComponents[0];
404434
},
405435
(err) => {
406436
bootstrapped = true;
@@ -544,7 +574,11 @@ export function runNativeScriptAngularApp<T, K>(options: AppRunOptions<T, K>) {
544574

545575
// Detect HMR environment (webpack or Vite)
546576
const isWebpackHot = !!import.meta['webpackHot'];
547-
const isViteHot = !!import.meta['hot'];
577+
// import.meta.hot is available when code goes through Vite's transform pipeline.
578+
// When @nativescript/angular is pre-bundled in the vendor (esbuild), import.meta.hot
579+
// won't exist. Fall back to the global placeholder flag that the NativeScript Vite
580+
// HMR runtime sets during dev boot.
581+
const isViteHot = !!import.meta['hot'] || !!(globalThis as any).__NS_DEV_PLACEHOLDER_ROOT_EARLY__;
548582
const isHotReloadEnabled = isWebpackHot || isViteHot;
549583

550584
// Always expose HMR globals for both webpack and Vite HMR support
Lines changed: 79 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,86 @@
1-
import { AbsoluteLayout, ActivityIndicator, Button, ContentView, DatePicker, DockLayout, FlexboxLayout, FormattedString, Frame, GridLayout, HtmlView, Image, Label, ListPicker, ListView, Page, Placeholder, Progress, ProxyViewContainer, Repeater, RootLayout, ScrollView, SearchBar, SegmentedBar, SegmentedBarItem, Slider, Span, SplitView, StackLayout, Switch, TabView, TextField, TextView, TimePicker, WebView, WrapLayout } from '@nativescript/core';
1+
import {
2+
AbsoluteLayout,
3+
ActivityIndicator,
4+
Button,
5+
ContentView,
6+
DatePicker,
7+
DockLayout,
8+
FlexboxLayout,
9+
FormattedString,
10+
Frame,
11+
GridLayout,
12+
HtmlView,
13+
Image,
14+
Label,
15+
ListPicker,
16+
ListView,
17+
Page,
18+
Placeholder,
19+
Progress,
20+
ProxyViewContainer,
21+
Repeater,
22+
RootLayout,
23+
ScrollView,
24+
SearchBar,
25+
SegmentedBar,
26+
SegmentedBarItem,
27+
Slider,
28+
Span,
29+
SplitView,
30+
StackLayout,
31+
Switch,
32+
TabView,
33+
TextField,
34+
TextView,
35+
TimePicker,
36+
WebView,
37+
WrapLayout,
38+
} from '@nativescript/core';
239
import { formattedStringMeta, frameMeta, textBaseMeta } from './metas';
340
import { registerElement } from './registry';
441

542
// Register default NativeScript components
643
// Note: ActionBar related components are registerd together with action-bar directives.
744
export function registerNativeScriptViewComponents() {
8-
if (!(<any>global).__ngRegisteredViews) {
9-
(<any>global).__ngRegisteredViews = true;
10-
registerElement('AbsoluteLayout', () => AbsoluteLayout);
11-
registerElement('ActivityIndicator', () => ActivityIndicator);
12-
registerElement('Button', () => Button, textBaseMeta);
13-
registerElement('ContentView', () => ContentView);
14-
registerElement('DatePicker', () => DatePicker);
15-
registerElement('DockLayout', () => DockLayout);
16-
registerElement('Frame', () => Frame, frameMeta);
17-
registerElement('GridLayout', () => GridLayout);
18-
registerElement('HtmlView', () => HtmlView);
19-
registerElement('Image', () => Image);
20-
// Parse5 changes <Image> tags to <img>. WTF!
21-
registerElement('img', () => Image);
22-
registerElement('Label', () => Label, textBaseMeta);
23-
registerElement('ListPicker', () => ListPicker);
24-
registerElement('ListView', () => ListView);
25-
registerElement('Page', () => Page);
26-
registerElement('Placeholder', () => Placeholder);
27-
registerElement('Progress', () => Progress);
28-
registerElement('ProxyViewContainer', () => ProxyViewContainer);
29-
registerElement('Repeater', () => Repeater);
30-
registerElement('RootLayout', () => RootLayout);
31-
registerElement('ScrollView', () => ScrollView);
32-
registerElement('SearchBar', () => SearchBar);
33-
registerElement('SegmentedBar', () => SegmentedBar);
34-
registerElement('SegmentedBarItem', () => SegmentedBarItem);
35-
registerElement('Slider', () => Slider);
36-
registerElement('SplitView', () => SplitView);
37-
registerElement('StackLayout', () => StackLayout);
38-
registerElement('FlexboxLayout', () => FlexboxLayout);
39-
registerElement('Switch', () => Switch);
40-
registerElement('TabView', () => TabView);
41-
registerElement('TextField', () => TextField, textBaseMeta);
42-
registerElement('TextView', () => TextView, textBaseMeta);
43-
registerElement('TimePicker', () => TimePicker);
44-
registerElement('WebView', () => WebView);
45-
registerElement('WrapLayout', () => WrapLayout);
46-
registerElement('FormattedString', () => FormattedString, formattedStringMeta);
47-
registerElement('Span', () => Span);
48-
}
45+
// No guard needed — registerElement calls Map.set which is idempotent.
46+
// The old `elementMap.size > 0` guard could falsely skip registration
47+
// in Vite HMR mode when elements were registered by a prior boot phase.
48+
registerElement('AbsoluteLayout', () => AbsoluteLayout);
49+
registerElement('ActivityIndicator', () => ActivityIndicator);
50+
registerElement('Button', () => Button, textBaseMeta);
51+
registerElement('ContentView', () => ContentView);
52+
registerElement('DatePicker', () => DatePicker);
53+
registerElement('DockLayout', () => DockLayout);
54+
registerElement('Frame', () => Frame, frameMeta);
55+
registerElement('GridLayout', () => GridLayout);
56+
registerElement('HtmlView', () => HtmlView);
57+
registerElement('Image', () => Image);
58+
// Parse5 changes <Image> tags to <img>. WTF!
59+
registerElement('img', () => Image);
60+
registerElement('Label', () => Label, textBaseMeta);
61+
registerElement('ListPicker', () => ListPicker);
62+
registerElement('ListView', () => ListView);
63+
registerElement('Page', () => Page);
64+
registerElement('Placeholder', () => Placeholder);
65+
registerElement('Progress', () => Progress);
66+
registerElement('ProxyViewContainer', () => ProxyViewContainer);
67+
registerElement('Repeater', () => Repeater);
68+
registerElement('RootLayout', () => RootLayout);
69+
registerElement('ScrollView', () => ScrollView);
70+
registerElement('SearchBar', () => SearchBar);
71+
registerElement('SegmentedBar', () => SegmentedBar);
72+
registerElement('SegmentedBarItem', () => SegmentedBarItem);
73+
registerElement('Slider', () => Slider);
74+
registerElement('SplitView', () => SplitView);
75+
registerElement('StackLayout', () => StackLayout);
76+
registerElement('FlexboxLayout', () => FlexboxLayout);
77+
registerElement('Switch', () => Switch);
78+
registerElement('TabView', () => TabView);
79+
registerElement('TextField', () => TextField, textBaseMeta);
80+
registerElement('TextView', () => TextView, textBaseMeta);
81+
registerElement('TimePicker', () => TimePicker);
82+
registerElement('WebView', () => WebView);
83+
registerElement('WrapLayout', () => WrapLayout);
84+
registerElement('FormattedString', () => FormattedString, formattedStringMeta);
85+
registerElement('Span', () => Span);
4986
}

packages/angular/src/lib/element-registry/registry.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { ViewClassMeta } from '../views/view-types';
44

55
export type ViewResolver = () => any;
66

7-
export const elementMap = new Map<string, { resolver: ViewResolver; meta?: ViewClassMeta }>();
7+
// Use a global elementMap so the vendor bundle and HTTP-loaded module instances
8+
// share the same element registry during Vite HMR (where two copies of
9+
// @nativescript/angular can coexist in separate module realms).
10+
export const elementMap: Map<string, { resolver: ViewResolver; meta?: ViewClassMeta }> =
11+
(globalThis as any).__NS_NG_ELEMENT_MAP__ || ((globalThis as any).__NS_NG_ELEMENT_MAP__ = new Map());
812
const camelCaseSplit = /([a-z0-9])([A-Z])/g;
913
const defaultViewMeta: ViewClassMeta = { skipAddToDom: false };
1014

packages/angular/src/lib/platform-nativescript.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { Color, GridLayout } from '@nativescript/core';
2222
import { defaultPageFactory, PAGE_FACTORY } from './tokens';
2323
import { AppLaunchView } from './application';
2424
import { NATIVESCRIPT_MODULE_PROVIDERS, NATIVESCRIPT_MODULE_STATIC_PROVIDERS } from './nativescript';
25+
import { registerNativeScriptViewComponents } from './element-registry';
2526

2627
export const defaultPageFactoryProvider = { provide: PAGE_FACTORY, useValue: defaultPageFactory };
2728
export class NativeScriptSanitizer extends Sanitizer {
@@ -151,13 +152,20 @@ function createProvidersConfig(options?: ApplicationConfig) {
151152
}
152153

153154
export function bootstrapApplication(rootComponent: Type<any>, options?: ApplicationConfig) {
155+
// Ensure NativeScript view components are registered in this module instance's
156+
// element registry. During Vite HMR, the vendor bundle and HTTP-loaded modules
157+
// may have separate module instances of @nativescript/angular, each with their
158+
// own elementMap. Without this call, the HTTP instance's elementMap would be
159+
// empty and the renderer would throw "No known component for element ...".
160+
registerNativeScriptViewComponents();
154161
return ɵinternalCreateApplication({
155162
rootComponent: rootComponent,
156163
...createProvidersConfig(options),
157164
});
158165
}
159166

160167
export function createApplication(options?: ApplicationConfig) {
168+
registerNativeScriptViewComponents();
161169
return ɵinternalCreateApplication(createProvidersConfig(options));
162170
}
163171

0 commit comments

Comments
 (0)