Skip to content

Commit a6b5c75

Browse files
committed
feat(core): add HMR hooks for XML errors, module registry, and modals
Persist the module registry across realms, surface XML build failures to dev tooling, and emit closedModally with retained modal options so HMR can safely re-present.
1 parent c6e6442 commit a6b5c75

5 files changed

Lines changed: 100 additions & 6 deletions

File tree

packages/core/application/application.ios.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -321,8 +321,8 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
321321
return this._rootView;
322322
}
323323

324-
resetRootView(view?: View) {
325-
super.resetRootView(view);
324+
resetRootView(entry?: NavigationEntry | string) {
325+
super.resetRootView(entry);
326326
this.setWindowContent();
327327
}
328328

@@ -331,6 +331,42 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
331331
this.started = true;
332332

333333
if (this.nativeApp) {
334+
// During Vite HMR dev boot, the placeholder has already started
335+
// the app lifecycle. A second run() with an entry should replace
336+
// the placeholder root, NOT present a modal via runAsEmbeddedApp.
337+
// The HTTP ESM realm creates a separate @nativescript/core instance,
338+
// so JS-level patching of Application.run can't intercept this call.
339+
// Detect HMR mode via the placeholder's global flag and use
340+
// setWindowContent on the PRIMARY Application singleton (bundled realm)
341+
// which has the actual window and root view hierarchy.
342+
const g = globalThis as any;
343+
if (g.__NS_DEV_PLACEHOLDER_ROOT_VIEW__ || g.__NS_DEV_PLACEHOLDER_ROOT_EARLY__) {
344+
if (entry) {
345+
// Defer to next run loop tick so resetRootView executes outside the
346+
// HTTP ESM import context. Direct calls can fail with
347+
// "ReferenceError: __COMMONJS__ is not defined" because the JS
348+
// execution stack is in the HTTP realm when Builder loads modules.
349+
const resolvedEntry = typeof entry === 'string' ? { moduleName: entry } : entry;
350+
setTimeout(() => {
351+
try {
352+
const primaryApp = g.Application;
353+
if (primaryApp && typeof primaryApp.resetRootView === 'function') {
354+
primaryApp.resetRootView(resolvedEntry);
355+
}
356+
} catch (e) {
357+
if (__DEV__) console.warn('[app-ios] deferred resetRootView failed:', e);
358+
}
359+
delete g.__NS_DEV_PLACEHOLDER_ROOT_VIEW__;
360+
delete g.__NS_DEV_PLACEHOLDER_ROOT_EARLY__;
361+
}, 0);
362+
} else {
363+
// Framework (e.g. Angular) calls run() with no entry — it manages
364+
// root views itself via resetRootView(). No-op to avoid presenting
365+
// a modal via runAsEmbeddedApp or throwing "Main entry is missing".
366+
if (__DEV__) console.info('[app-ios] run() called with no entry during HMR placeholder; framework manages root view');
367+
}
368+
return;
369+
}
334370
this.runAsEmbeddedApp();
335371
} else {
336372
this.runAsMainApp();
@@ -1044,7 +1080,13 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
10441080

10451081
// During initial scene startup we must wait for launch to be notified first.
10461082
// Some frameworks provide root content from launch handlers.
1047-
if (this.hasLaunched()) {
1083+
// Guard: skip setWindowContent when no main entry is configured yet.
1084+
// During Vite HMR dev boot, the placeholder calls Application.run() with
1085+
// no entry; the real entry is set later when the HTTP-loaded main module
1086+
// calls Application.run({ moduleName: ... }). Without this guard the
1087+
// scene handler would throw "Main entry is missing" and leave the window
1088+
// in a broken state (root view reset but no replacement created).
1089+
if (this.hasLaunched() && this.getMainEntry()) {
10481090
this.setWindowContent();
10491091
}
10501092
}
@@ -1388,6 +1430,7 @@ global.__onLiveSyncCore = function (context?: ModuleContext) {
13881430
};
13891431

13901432
export * from './application-common';
1433+
export * from './application-interfaces';
13911434
export const Application = iosApp;
13921435
export const AndroidApplication = undefined;
13931436

packages/core/globals/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,10 @@ global.Experimental = function (target: Object, key?: string | symbol, descripto
242242
return target;
243243
}
244244
};
245-
const modules: Map<string, { moduleId: string; loader: ModuleLoader }> = new Map<string, { moduleId: string; loader: ModuleLoader }>();
245+
// Persist the module registry Map on globalThis so that if this file is
246+
// re-evaluated in a secondary realm (HTTP ESM during Vite HMR), the
247+
// existing modules registered by the primary bundle are preserved.
248+
const modules: Map<string, { moduleId: string; loader: ModuleLoader }> = (global as any).__nsModuleRegistry || ((global as any).__nsModuleRegistry = new Map<string, { moduleId: string; loader: ModuleLoader }>());
246249

247250
// console.log(`globals/index, __COMMONJS__:`, __COMMONJS__);
248251
global.loadModule = function loadModule(name: string): any {

packages/core/ui/builder/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,14 @@ export namespace xml2ui {
376376

377377
export function SourceErrorFormat(uri): ErrorFormatter {
378378
return (e: Error, p: xml.Position) => {
379-
console.error(uri);
379+
console.error(uri, e?.message ?? e);
380+
// Stash for dev tooling: callers up the stack frequently swallow this
381+
// error (e.g. deferred Application.run paths), leaving nothing but the
382+
// console line above. The NativeScript Vite dev overlay reads this to
383+
// surface the failure when the app root never commits during boot.
384+
try {
385+
(globalThis as any).__NS_LAST_XML_ERROR__ = { uri, message: (e && e.message) || String(e), time: Date.now() };
386+
} catch {}
380387
const source = p ? new Source(uri, p.line, p.column) : new Source(uri, -1, -1);
381388
e = new SourceError(e, source, 'Building UI from XML.');
382389

packages/core/ui/core/view/index.d.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { GestureTypes, GesturesObserver, TouchAnimationOptions, VisionHoverOptio
77
import { ShadowCSSValues } from '../../styling/css-shadow';
88
import { LinearGradient } from '../../styling/linear-gradient';
99
import { InheritedProperty, Property } from '../properties';
10-
import { ViewBase } from '../view-base';
10+
import { ShowModalOptions, ViewBase } from '../view-base';
1111
import { GlassEffectType, ViewCommon } from './view-common';
1212
import type { Point, ShownModallyData, Size } from './view-interfaces';
1313

@@ -71,6 +71,15 @@ export abstract class View extends ViewCommon {
7171
*/
7272
public static shownModallyEvent: string;
7373

74+
/**
75+
* String value used when hooking to closedModally event. Fired on the
76+
* modal view once its native dismissal has fully completed (after the
77+
* close callback and UI teardown).
78+
*
79+
* @nsEvent {EventData} closedModally
80+
*/
81+
public static closedModallyEvent: string;
82+
7483
/**
7584
* String value used when hooking to accessibilityBlur event.
7685
*
@@ -997,6 +1006,13 @@ export abstract class View extends ViewCommon {
9971006
* @private
9981007
*/
9991008
_modalParent?: View;
1009+
/**
1010+
* The ShowModalOptions this view is currently presented with (set while
1011+
* shown modally, cleared on close). Lets tooling re-present the modal
1012+
* with its original options.
1013+
* @private
1014+
*/
1015+
_modalOptions?: ShowModalOptions;
10001016
/**
10011017
* @private
10021018
*/

packages/core/ui/core/view/view-common.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ export abstract class ViewCommon extends ViewBase {
9393
public static layoutChangedEvent = 'layoutChanged';
9494
public static shownModallyEvent = 'shownModally';
9595
public static showingModallyEvent = 'showingModally';
96+
/**
97+
* Fired on the modal view once its native dismissal has fully completed
98+
* (after the close callback and UI teardown). Unlike `closeCallback` —
99+
* which is captured per-show — any observer can listen for this, e.g.
100+
* tooling that needs to re-present a modal (dev-time HMR).
101+
*/
102+
public static closedModallyEvent = 'closedModally';
96103
public static accessibilityBlurEvent = accessibilityBlurEvent;
97104
public static accessibilityFocusEvent = accessibilityFocusEvent;
98105
public static accessibilityFocusChangedEvent = accessibilityFocusChangedEvent;
@@ -134,6 +141,13 @@ export abstract class ViewCommon extends ViewBase {
134141
protected _closeModalCallback: Function;
135142
public _manager: any;
136143
public _modalParent?: ViewCommon;
144+
/**
145+
* The ShowModalOptions this view is currently presented with (set in
146+
* _showNativeModalView, cleared on close). Lets tooling re-present the
147+
* modal with its original options — e.g. dev-time HMR re-show — which
148+
* are otherwise only captured inside the close-callback closure.
149+
*/
150+
public _modalOptions?: ShowModalOptions;
137151
private _modalContext: any;
138152
private _modal: ViewCommon;
139153

@@ -475,6 +489,7 @@ export abstract class ViewCommon extends ViewBase {
475489
this.style.fontScaleInternal = getFontScale();
476490
this._modalParent = parent;
477491
this._modalContext = options.context;
492+
this._modalOptions = options;
478493
this._closeModalCallback = (...originalArgs) => {
479494
const cleanupModalViews = () => {
480495
const modalIndex = _rootModalViews.indexOf(this);
@@ -484,6 +499,7 @@ export abstract class ViewCommon extends ViewBase {
484499

485500
this._modalParent = null;
486501
this._modalContext = null;
502+
this._modalOptions = null;
487503
this._closeModalCallback = null;
488504
this._dialogClosed();
489505
parent._modal = null;
@@ -506,6 +522,15 @@ export abstract class ViewCommon extends ViewBase {
506522
}
507523

508524
this._tearDownUI(true);
525+
526+
// Native dismissal fully completed (this callback runs in the
527+
// platform dismissal completion) — observable counterpart to the
528+
// per-show closeCallback. Used by dev tooling (HMR modal
529+
// re-present) to know exactly when a new present is safe.
530+
this.notify(<EventData>{
531+
eventName: ViewCommon.closedModallyEvent,
532+
object: this,
533+
});
509534
}
510535
};
511536

0 commit comments

Comments
 (0)