@nativescript/angular 21.0.1-alpha.2 → 21.0.1-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,8 +6,8 @@ import { __decorate, __param, __metadata } from 'tslib';
6
6
  import * as i1$2 from '@angular/common';
7
7
  import { LocationStrategy, XhrFactory, CommonModule, ɵNullViewportScroller as _NullViewportScroller, ViewportScroller, PlatformLocation, DOCUMENT } from '@angular/common';
8
8
  import * as i1$3 from '@angular/router';
9
- import { DefaultUrlSerializer, Router, PRIMARY_OUTLET, NavigationEnd, ChildrenOutletContexts, ActivatedRoute, RouteReuseStrategy, RouterModule, provideRouter } from '@angular/router';
10
- import { BehaviorSubject, Subject, fromEvent, defer, Observable as Observable$1 } from 'rxjs';
9
+ import { DefaultUrlSerializer, Router, PRIMARY_OUTLET, NavigationEnd, ChildrenOutletContexts, ActivatedRoute, NavigationCancel, NavigationError, NavigationStart, RouteReuseStrategy, RouterModule, provideRouter } from '@angular/router';
10
+ import { BehaviorSubject, Subject, ReplaySubject, fromEvent, defer, Observable as Observable$1 } from 'rxjs';
11
11
  import { filter, map, take, distinctUntilChanged, startWith } from 'rxjs/operators';
12
12
  import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
13
13
  import { ɵBrowserAnimationBuilder as _BrowserAnimationBuilder, AnimationBuilder } from '@angular/animations';
@@ -256,6 +256,560 @@ function resetAngularHmrCompiledComponents(core) {
256
256
  }
257
257
  }
258
258
 
259
+ /**
260
+ * Centralised dev-mode + HMR detection for `@nativescript/angular` helpers.
261
+ *
262
+ * The package ships HMR scaffolding (route tracker, route replay, modal
263
+ * preservation, compiled-component reset) that subscribes to long-lived
264
+ * router and bootstrap streams. None of that work belongs in a production
265
+ * binary — it would attach observers that never fire and keep references
266
+ * that confuse Angular's destroy logic.
267
+ *
268
+ * Every HMR helper consults {@link isAngularHmrEnabled} from its
269
+ * constructor. The check is intentionally cheap (no network, no I/O) so it
270
+ * is safe to call in dependency-injection factories and in fast paths.
271
+ *
272
+ * Detection cascade (returns the first match):
273
+ * 1. **Production build short-circuit** — `ngDevMode === false` means
274
+ * Angular built the app in production mode. We bail immediately.
275
+ * 2. **NativeScript Vite dev signal** — see
276
+ * {@link isNativeScriptViteHmrActive}. We accept either of the two
277
+ * persistent globals the NS Vite root-placeholder installer manages
278
+ * (`__NS_DEV_PLACEHOLDER_ROOT_EARLY__` during early boot,
279
+ * `__NS_HMR_BOOT_COMPLETE__` after the real app root commits) so
280
+ * services that are constructed *after* the placeholder has handed
281
+ * off — e.g. `NativeDialog` instantiated lazily when the user opens
282
+ * their first modal — still detect HMR correctly.
283
+ * 3. **Webpack HMR signal** — `globalThis.__webpack_require__` is set
284
+ * when the webpack runtime is loaded. Combined with the `ngDevMode`
285
+ * short-circuit above, its presence means "webpack dev". The
286
+ * production webpack runtime also sets the global, but `ngDevMode`
287
+ * would already be `false`, so the production case never reaches
288
+ * here.
289
+ *
290
+ * If none of these match, the caller should treat HMR as disabled and
291
+ * skip subscribing to disposal/bootstrap streams.
292
+ *
293
+ * The webpack signal lives on `globalThis` rather than `import.meta` so
294
+ * this file compiles cleanly under `--module commonjs` (the jest spec
295
+ * compiler) and under `--module esnext` (the library build).
296
+ */
297
+ function isAngularHmrEnabled() {
298
+ if (typeof ngDevMode !== 'undefined' && ngDevMode === false) {
299
+ return false;
300
+ }
301
+ return isNativeScriptViteHmrActive() || isWebpackHmrActive();
302
+ }
303
+ /**
304
+ * True when the NativeScript Vite dev HMR runtime is active. This is the
305
+ * most reliable signal that the project's `nativescript.config.ts` set
306
+ * `bundler: 'vite'` AND we are running the dev server.
307
+ *
308
+ * The NS Vite root-placeholder installer manages two persistent globals:
309
+ * - `__NS_DEV_PLACEHOLDER_ROOT_EARLY__` is set the moment the placeholder
310
+ * runs (very early, before the real app boots), then **deleted** by
311
+ * `clearPlaceholderGlobals` once `tryFinalizeBootPlaceholder` succeeds.
312
+ * - `__NS_HMR_BOOT_COMPLETE__` is set in the same finalize step and is
313
+ * **never deleted** for the lifetime of the dev session.
314
+ *
315
+ * Callers run the gamut of timing — e.g. the route tracker is constructed
316
+ * during bootstrap (early flag still set) but `NativeDialog` is typically
317
+ * instantiated lazily when the user opens their first modal (early flag
318
+ * already cleared, complete flag set). Checking either global covers both
319
+ * windows. If we only checked the early flag, every late-instantiated
320
+ * service would silently no-op and HMR features (modal preservation,
321
+ * route replay) would appear broken in development.
322
+ */
323
+ function isNativeScriptViteHmrActive() {
324
+ const g = globalThis;
325
+ return !!(g.__NS_DEV_PLACEHOLDER_ROOT_EARLY__ || g.__NS_HMR_BOOT_COMPLETE__);
326
+ }
327
+ /**
328
+ * True when the webpack runtime is loaded. The webpack runtime sets
329
+ * `__webpack_require__` on `globalThis` whenever a webpack bundle is
330
+ * executing — both in dev and prod. Callers gate on
331
+ * {@link isAngularHmrEnabled} (not this directly) so the production
332
+ * short-circuit fires first.
333
+ */
334
+ function isWebpackHmrActive() {
335
+ return typeof globalThis.__webpack_require__ === 'function';
336
+ }
337
+ /**
338
+ * True when Angular reports we are running with dev-mode flags. Useful
339
+ * for code paths that want to opt out of cost in production but don't
340
+ * care which bundler is running.
341
+ */
342
+ function isAngularDevMode() {
343
+ if (typeof ngDevMode === 'undefined') {
344
+ return true;
345
+ }
346
+ return ngDevMode !== false;
347
+ }
348
+
349
+ class NativeScriptDebug {
350
+ static { this.animationsTraceCategory = 'ns-animations'; }
351
+ static { this.rendererTraceCategory = 'ns-renderer'; }
352
+ static { this.viewUtilCategory = 'ns-view-util'; }
353
+ static { this.routerTraceCategory = 'ns-router'; }
354
+ static { this.routeReuseStrategyTraceCategory = 'ns-route-reuse-strategy'; }
355
+ static { this.listViewTraceCategory = 'ns-list-view'; }
356
+ static { this.bootstrapCategory = 'bootstrap'; }
357
+ static { this.hmrTraceCategory = 'ns-ng-hmr'; }
358
+ // TODO: migrate all usage to this - avoids extraneous method executions
359
+ static { this.enabled = Trace.isEnabled(); }
360
+ static isLogEnabled() {
361
+ return Trace.isEnabled();
362
+ }
363
+ static animationsLog(message) {
364
+ Trace.write(message, NativeScriptDebug.animationsTraceCategory);
365
+ }
366
+ static rendererLog(msg) {
367
+ Trace.write(msg, NativeScriptDebug.rendererTraceCategory);
368
+ }
369
+ static rendererError(message) {
370
+ Trace.write(message, NativeScriptDebug.rendererTraceCategory, Trace.messageType.error);
371
+ }
372
+ static viewUtilLog(msg) {
373
+ Trace.write(msg, NativeScriptDebug.viewUtilCategory);
374
+ }
375
+ static routerLog(message) {
376
+ Trace.write(message, NativeScriptDebug.routerTraceCategory);
377
+ }
378
+ static routerError(message) {
379
+ Trace.write(message, NativeScriptDebug.routerTraceCategory, Trace.messageType.error);
380
+ }
381
+ static routeReuseStrategyLog(message) {
382
+ Trace.write(message, NativeScriptDebug.routeReuseStrategyTraceCategory);
383
+ }
384
+ static styleError(message) {
385
+ Trace.write(message, Trace.categories.Style, Trace.messageType.error);
386
+ }
387
+ static listViewLog(message) {
388
+ Trace.write(message, NativeScriptDebug.listViewTraceCategory);
389
+ }
390
+ static listViewError(message) {
391
+ Trace.write(message, NativeScriptDebug.listViewTraceCategory, Trace.messageType.error);
392
+ }
393
+ static bootstrapLog(message) {
394
+ Trace.write(message, NativeScriptDebug.bootstrapCategory);
395
+ }
396
+ static bootstrapLogError(message) {
397
+ Trace.write(message, NativeScriptDebug.bootstrapCategory, Trace.messageType.error);
398
+ }
399
+ static hmrLog(message) {
400
+ Trace.write(message, NativeScriptDebug.hmrTraceCategory);
401
+ }
402
+ static hmrLogError(message) {
403
+ Trace.write(message, NativeScriptDebug.hmrTraceCategory, Trace.messageType.error);
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Fresh-class registry for HMR.
409
+ *
410
+ * After an HMR reboot, every previously imported component module is
411
+ * re-evaluated. Each `@Component()`-decorated class becomes a *new* class
412
+ * object — it shares the source name (e.g. `ResourceModalComponent`) but
413
+ * has a different identity from the class the host code captured before
414
+ * the reboot.
415
+ *
416
+ * Helpers like `NativeDialog._restoreSingleDialog` need to re-open a
417
+ * captured modal *with the new class so the visual update applies*.
418
+ * Holding onto the pre-reboot class reference reopens the modal with
419
+ * the old metadata, which manifests as "the change appears the next
420
+ * time I close and re-open the modal myself, but not when HMR auto-
421
+ * reopens it."
422
+ *
423
+ * The mechanism is:
424
+ *
425
+ * 1. The Vite plugin `ns-component-hmr-register` (in
426
+ * `@nativescript/vite/configuration/angular`) injects a
427
+ * registration call at the end of every user `.ts` file that
428
+ * defines an `@Component`-decorated class:
429
+ *
430
+ * if (typeof globalThis.__NS_HMR_REGISTER_COMPONENT__ === 'function') {
431
+ * try { globalThis.__NS_HMR_REGISTER_COMPONENT__(
432
+ * 'ResourceModalComponent', ResourceModalComponent, import.meta.url
433
+ * ); } catch {}
434
+ * }
435
+ *
436
+ * 2. This module installs `__NS_HMR_REGISTER_COMPONENT__` on
437
+ * `globalThis` so module re-evaluations after an HMR reboot
438
+ * replace the previously-registered class with the fresh one.
439
+ *
440
+ * 3. HMR helpers (modal restore, route replay) read the registry
441
+ * via {@link getFreshComponentClass} to swap in the fresh class.
442
+ *
443
+ * This avoids patching `ɵɵdefineComponent` directly, which is exported
444
+ * as an immutable ESM namespace binding from `@angular/core` — patch
445
+ * attempts silently fail (the assignment is a no-op under strict
446
+ * mode) so the registry never gets populated. With the self-
447
+ * registration approach the binding stays untouched and we don't
448
+ * depend on Angular's internal export shape.
449
+ *
450
+ * Production short-circuit: the registrar is only installed when
451
+ * {@link isAngularHmrEnabled} reports dev + (vite | webpack). In a
452
+ * production build the global hook is never assigned and the Vite
453
+ * plugin only runs in `apply: 'serve'`, so the registration calls
454
+ * never reach the runtime.
455
+ */
456
+ const REGISTRY_KEY$1 = '__NS_ANGULAR_HMR_CLASS_REGISTRY__';
457
+ const REGISTRY_META_KEY = '__NS_ANGULAR_HMR_CLASS_META__';
458
+ const REGISTRAR_HOOK = '__NS_HMR_REGISTER_COMPONENT__';
459
+ const REGISTRAR_INSTALLED_FLAG = '__NS_ANGULAR_HMR_REGISTRAR_INSTALLED__';
460
+ /**
461
+ * Diagnostic: counters that survive across HMR cycles via globalThis.
462
+ * Used to spot patterns like "the same class registered N times in a
463
+ * single cycle" or "a brand-new class object every cycle".
464
+ */
465
+ const DIAG_KEY = '__NS_HMR_DIAG__';
466
+ function getDiag() {
467
+ const slot = globalThis;
468
+ if (!slot[DIAG_KEY]) {
469
+ slot[DIAG_KEY] = {
470
+ cycle: 0,
471
+ registerCalls: 0,
472
+ classIdentities: new Map(),
473
+ classRegisterCounts: new Map(),
474
+ classIds: new WeakMap(),
475
+ classIdNext: 1,
476
+ };
477
+ }
478
+ return slot[DIAG_KEY];
479
+ }
480
+ /** Get/assign a short stable id for a class object. */
481
+ function getClassId(diag, cls) {
482
+ let id = diag.classIds.get(cls);
483
+ if (!id) {
484
+ id = `c${diag.classIdNext++}`;
485
+ diag.classIds.set(cls, id);
486
+ }
487
+ return id;
488
+ }
489
+ /**
490
+ * Class-registry HMR diagnostic.
491
+ */
492
+ function diagLog(message) {
493
+ if (!isAngularHmrEnabled())
494
+ return;
495
+ if (!NativeScriptDebug.isLogEnabled())
496
+ return;
497
+ NativeScriptDebug.hmrLog(`[class-registry] ${message}`);
498
+ }
499
+ /**
500
+ * Log helper for "must surface at module-load time" messages — fires
501
+ * for any dev-mode build (not gated on the HMR-globals check) so the
502
+ * one-shot "registrar installed" line doesn't get suppressed by a
503
+ * module-load ordering race.
504
+ */
505
+ function bootLog(message) {
506
+ if (!isAngularDevMode())
507
+ return;
508
+ if (!NativeScriptDebug.isLogEnabled())
509
+ return;
510
+ NativeScriptDebug.hmrLog(`[class-registry] ${message}`);
511
+ }
512
+ /**
513
+ * Public so callers from application.ts can bump the cycle counter when
514
+ * a new HMR reboot starts. Kept as a free function (not a class method)
515
+ * to avoid forcing more imports on the application module.
516
+ *
517
+ * Self-heal: by the time we hit cycle bump, dev/HMR globals are
518
+ * definitely set (`__NS_HMR_BOOT_COMPLETE__` was set before the first
519
+ * HMR cycle ever runs). Some module-load orderings end up with
520
+ * `installAngularHmrComponentRegistrar()` called before the early
521
+ * placeholder global was set, so the registrar would have returned
522
+ * early and never installed the hook. Re-attempting here closes that
523
+ * window — `installAngularHmrComponentRegistrar()` is idempotent.
524
+ */
525
+ function _hmrDiagBumpCycle() {
526
+ const diag = getDiag();
527
+ diag.cycle += 1;
528
+ diagLog(`---- cycle ${diag.cycle} start ----`);
529
+ installAngularHmrComponentRegistrar();
530
+ return diag.cycle;
531
+ }
532
+ /** Public for tests. */
533
+ function _hmrDiagSnapshot() {
534
+ const diag = getDiag();
535
+ return {
536
+ cycle: diag.cycle,
537
+ registerCalls: diag.registerCalls,
538
+ namesSeen: diag.classIdentities.size,
539
+ };
540
+ }
541
+ function getRegistry() {
542
+ const slot = globalThis;
543
+ let registry = slot[REGISTRY_KEY$1];
544
+ if (!registry) {
545
+ registry = new Map();
546
+ slot[REGISTRY_KEY$1] = registry;
547
+ }
548
+ return registry;
549
+ }
550
+ function getMetaRegistry() {
551
+ const slot = globalThis;
552
+ let registry = slot[REGISTRY_META_KEY];
553
+ if (!registry) {
554
+ registry = new Map();
555
+ slot[REGISTRY_META_KEY] = registry;
556
+ }
557
+ return registry;
558
+ }
559
+ /**
560
+ * Internal: write a class into the registry. Exposed for unit tests
561
+ * (which can call this directly to simulate a Vite-injected
562
+ * registration without spinning up the Vite plugin pipeline).
563
+ *
564
+ * Production callers should never need this — user code just calls
565
+ * the global `__NS_HMR_REGISTER_COMPONENT__` hook installed by
566
+ * {@link installAngularHmrComponentRegistrar}.
567
+ */
568
+ function _registerComponentForHmr(name, cls, url = '') {
569
+ if (!name || typeof name !== 'string')
570
+ return;
571
+ if (cls === undefined || cls === null)
572
+ return;
573
+ const registry = getRegistry();
574
+ const meta = getMetaRegistry();
575
+ const previous = registry.get(name);
576
+ registry.set(name, cls);
577
+ meta.set(name, { url: url || '', cycle: getDiag().cycle });
578
+ const d = getDiag();
579
+ d.registerCalls += 1;
580
+ if (typeof cls === 'object' || typeof cls === 'function') {
581
+ const classId = getClassId(d, cls);
582
+ let identitySet = d.classIdentities.get(name);
583
+ if (!identitySet) {
584
+ identitySet = new Set();
585
+ d.classIdentities.set(name, identitySet);
586
+ }
587
+ identitySet.add(cls);
588
+ d.classRegisterCounts.set(name, (d.classRegisterCounts.get(name) ?? 0) + 1);
589
+ // Only log a small set of "interesting" components to keep noise
590
+ // manageable. Verbose mode is enabled by setting
591
+ // globalThis.__NS_HMR_DIAG_VERBOSE = true (e.g. from the user
592
+ // app's main.ts) when we want all names.
593
+ const verbose = !!globalThis.__NS_HMR_DIAG_VERBOSE;
594
+ const watchPattern = globalThis.__NS_HMR_DIAG_WATCH;
595
+ const matches = watchPattern instanceof RegExp ? watchPattern.test(name) : typeof watchPattern === 'string' ? name.includes(watchPattern) : /Modal|Dialog/.test(name);
596
+ if (verbose || matches) {
597
+ diagLog(`register name=${name} classId=${classId} sameAsPrev=${previous === cls} cycle=${d.cycle} totalIdentitiesForName=${identitySet.size} registerCountForName=${d.classRegisterCounts.get(name)} url=${url || '(none)'}`);
598
+ }
599
+ }
600
+ }
601
+ /**
602
+ * Install the cross-module `__NS_HMR_REGISTER_COMPONENT__` hook on
603
+ * `globalThis`. The Vite plugin `ns-component-hmr-register` injects
604
+ * a call to this hook at the end of every user `.ts` file that
605
+ * defines an `@Component`-decorated class, so re-evaluations after
606
+ * an HMR reboot keep the registry pointed at the live class.
607
+ *
608
+ * Idempotent: calling twice is a no-op (the second call sees the
609
+ * installed flag and returns).
610
+ *
611
+ * The hook is installed unconditionally — it's a single function
612
+ * reference on globalThis with negligible cost. Production builds
613
+ * never reach this code path because the Vite plugin
614
+ * `ns-component-hmr-register` runs only with `apply: 'serve'`, so
615
+ * the hook is never *called* in production. We previously gated
616
+ * this on `isAngularHmrEnabled()` but that check depends on
617
+ * NativeScript Vite globals (`__NS_DEV_PLACEHOLDER_ROOT_EARLY__` /
618
+ * `__NS_HMR_BOOT_COMPLETE__`) that are set imperatively from
619
+ * `main-entry.ts`. Module-load ordering can put `application.ts`
620
+ * evaluation *before* those globals are set in some startup paths,
621
+ * causing the install to silently no-op while the Vite plugin
622
+ * happily emits registration calls into user `.ts` files. The end
623
+ * result was an empty registry and `getFreshComponentClass` always
624
+ * reporting `reason=no-registry`. Removing the gate eliminates
625
+ * that race entirely.
626
+ */
627
+ function installAngularHmrComponentRegistrar() {
628
+ const slot = globalThis;
629
+ if (slot[REGISTRAR_INSTALLED_FLAG]) {
630
+ return;
631
+ }
632
+ // Define the hook BEFORE marking installed so concurrent module
633
+ // initializers see the function as soon as the flag is observable.
634
+ slot[REGISTRAR_HOOK] = (name, cls, url) => {
635
+ try {
636
+ _registerComponentForHmr(name, cls, typeof url === 'string' ? url : '');
637
+ }
638
+ catch (err) {
639
+ // Registration is best-effort — never break a user module load
640
+ // because of a registration failure.
641
+ diagLog(`registrar threw for ${name}: ${err?.message ?? err}`);
642
+ }
643
+ };
644
+ slot[REGISTRAR_INSTALLED_FLAG] = true;
645
+ bootLog('installAngularHmrComponentRegistrar installed global hook __NS_HMR_REGISTER_COMPONENT__');
646
+ }
647
+ /**
648
+ * Look up the freshest registered component class for a given name, or
649
+ * `undefined` if no match. HMR helpers (e.g. dialog restore) call this
650
+ * with the captured class's `.name` to find the live class after a
651
+ * reboot. Returns `undefined` in production builds because the registrar
652
+ * is never installed there.
653
+ */
654
+ function getFreshComponentClass(name) {
655
+ if (!name)
656
+ return undefined;
657
+ const slot = globalThis;
658
+ const registry = slot[REGISTRY_KEY$1];
659
+ // Diagnostics: log every lookup with the resolved class id so the
660
+ // restore path can be cross-referenced against register emissions.
661
+ if (registry) {
662
+ const value = registry.get(name);
663
+ const diag = getDiag();
664
+ const classId = value && (typeof value === 'object' || typeof value === 'function') ? getClassId(diag, value) : '(none)';
665
+ const knownNames = registry.size;
666
+ diagLog(`getFreshComponentClass name=${name} found=${!!value} classId=${classId} registrySize=${knownNames}`);
667
+ return value;
668
+ }
669
+ diagLog(`getFreshComponentClass name=${name} found=false reason=no-registry`);
670
+ return undefined;
671
+ }
672
+ /**
673
+ * Look up the source URL (`import.meta.url`) recorded for a registered
674
+ * component. Used by HMR helpers that need to force a fresh import of
675
+ * a lazily-loaded module (e.g. modals whose static import chain doesn't
676
+ * walk the bootstrap path).
677
+ *
678
+ * Returns `undefined` if the name was never registered or if no URL was
679
+ * provided at registration time.
680
+ */
681
+ function getRegisteredComponentUrl(name) {
682
+ if (!name)
683
+ return undefined;
684
+ const slot = globalThis;
685
+ const meta = slot[REGISTRY_META_KEY];
686
+ const entry = meta?.get(name);
687
+ return entry?.url || undefined;
688
+ }
689
+ /**
690
+ * Test/debug helper: clear all registered classes. Production callers
691
+ * never need this; the registry stays empty without the registrar.
692
+ */
693
+ function clearAngularHmrClassRegistry() {
694
+ const slot = globalThis;
695
+ slot[REGISTRY_KEY$1] = undefined;
696
+ slot[REGISTRY_META_KEY] = undefined;
697
+ slot[REGISTRAR_INSTALLED_FLAG] = undefined;
698
+ slot[REGISTRAR_HOOK] = undefined;
699
+ // Also reset diag so test isolation isn't broken by counts leaking.
700
+ const diagSlot = globalThis;
701
+ diagSlot[DIAG_KEY] = undefined;
702
+ }
703
+
704
+ const REGISTRY_KEY = '__NS_HMR_EAGER_SERVICES__';
705
+ const REGISTER_KEY = '__NS_REGISTER_HMR_EAGER_SERVICE__';
706
+ /**
707
+ * Diagnostic helper.
708
+ */
709
+ function eagerDiag(message) {
710
+ const g = globalThis;
711
+ const ngDev = (typeof g.ngDevMode === 'boolean') ? g.ngDevMode : true;
712
+ const viteHmr = !!g.__NS_DEV_PLACEHOLDER_ROOT_EARLY__ || !!g.__NS_HMR_BOOT_COMPLETE__;
713
+ if (!(ngDev && viteHmr))
714
+ return;
715
+ if (!NativeScriptDebug.isLogEnabled())
716
+ return;
717
+ NativeScriptDebug.hmrLog(`[eager] ${message}`);
718
+ }
719
+ function getStore() {
720
+ return globalThis;
721
+ }
722
+ /**
723
+ * Returns the live (mutable) array of registered eager instantiators.
724
+ * Callers must not mutate it directly outside of the helpers in this
725
+ * module — use {@link registerHmrEagerInstantiator} or
726
+ * {@link clearHmrEagerInstantiators} instead.
727
+ */
728
+ function getRegisteredHmrEagerInstantiators() {
729
+ const store = getStore();
730
+ const list = store[REGISTRY_KEY];
731
+ if (!Array.isArray(list)) {
732
+ const fresh = [];
733
+ store[REGISTRY_KEY] = fresh;
734
+ return fresh;
735
+ }
736
+ return list;
737
+ }
738
+ /**
739
+ * Idempotently register an instantiator callback. Returns `true` when the
740
+ * callback was added, `false` when it was already present.
741
+ */
742
+ function registerHmrEagerInstantiator(fn) {
743
+ if (typeof fn !== 'function') {
744
+ return false;
745
+ }
746
+ const list = getRegisteredHmrEagerInstantiators();
747
+ if (list.includes(fn)) {
748
+ eagerDiag(`registerHmrEagerInstantiator dedup (already present) listSize=${list.length}`);
749
+ return false;
750
+ }
751
+ list.push(fn);
752
+ eagerDiag(`registerHmrEagerInstantiator added newSize=${list.length} fnName=${fn.name || '(anon)'}`);
753
+ return true;
754
+ }
755
+ /**
756
+ * Clear all registered instantiators. Tests use this to reset state
757
+ * between specs; production code should not call it.
758
+ */
759
+ function clearHmrEagerInstantiators() {
760
+ const list = getRegisteredHmrEagerInstantiators();
761
+ list.length = 0;
762
+ }
763
+ /**
764
+ * Install the cross-module registration entry point on `globalThis` so
765
+ * consumer modules (e.g. `dialog-services.ts`) can register without
766
+ * statically importing this file. Idempotent across multiple calls so
767
+ * application.ts can call it on every reboot without leaking state.
768
+ */
769
+ function installHmrEagerRegistrar() {
770
+ const store = getStore();
771
+ if (typeof store[REGISTER_KEY] === 'function') {
772
+ return;
773
+ }
774
+ store[REGISTER_KEY] = (fn) => {
775
+ registerHmrEagerInstantiator(fn);
776
+ };
777
+ }
778
+ /**
779
+ * Invoke every registered instantiator with the bootstrapped injector.
780
+ * Per-callback exceptions are swallowed; pass `onError` to receive them
781
+ * for logging.
782
+ */
783
+ function runHmrEagerInstantiators(injector, onError) {
784
+ if (!injector) {
785
+ eagerDiag(`runHmrEagerInstantiators called without injector — no-op`);
786
+ return;
787
+ }
788
+ const list = getRegisteredHmrEagerInstantiators();
789
+ eagerDiag(`runHmrEagerInstantiators list.length=${list.length}`);
790
+ if (list.length === 0) {
791
+ return;
792
+ }
793
+ for (let i = 0; i < list.length; i++) {
794
+ const fn = list[i];
795
+ try {
796
+ eagerDiag(`runHmrEagerInstantiators calling [${i}] ${fn.name || '(anon)'}`);
797
+ fn(injector);
798
+ }
799
+ catch (err) {
800
+ eagerDiag(`runHmrEagerInstantiators [${i}] threw: ${err?.message ?? err}`);
801
+ if (onError) {
802
+ try {
803
+ onError(err);
804
+ }
805
+ catch {
806
+ // The error reporter must not itself break the loop.
807
+ }
808
+ }
809
+ }
810
+ }
811
+ }
812
+
259
813
  class NativeScriptLoadingService {
260
814
  constructor() {
261
815
  this.mainModuleReady$ = new BehaviorSubject(false);
@@ -356,64 +910,6 @@ function clearAngularHmrRouteConfigCaches(routes) {
356
910
  return cleared;
357
911
  }
358
912
 
359
- class NativeScriptDebug {
360
- static { this.animationsTraceCategory = 'ns-animations'; }
361
- static { this.rendererTraceCategory = 'ns-renderer'; }
362
- static { this.viewUtilCategory = 'ns-view-util'; }
363
- static { this.routerTraceCategory = 'ns-router'; }
364
- static { this.routeReuseStrategyTraceCategory = 'ns-route-reuse-strategy'; }
365
- static { this.listViewTraceCategory = 'ns-list-view'; }
366
- static { this.bootstrapCategory = 'bootstrap'; }
367
- static { this.hmrTraceCategory = 'ns-ng-hmr'; }
368
- // TODO: migrate all usage to this - avoids extraneous method executions
369
- static { this.enabled = Trace.isEnabled(); }
370
- static isLogEnabled() {
371
- return Trace.isEnabled();
372
- }
373
- static animationsLog(message) {
374
- Trace.write(message, NativeScriptDebug.animationsTraceCategory);
375
- }
376
- static rendererLog(msg) {
377
- Trace.write(msg, NativeScriptDebug.rendererTraceCategory);
378
- }
379
- static rendererError(message) {
380
- Trace.write(message, NativeScriptDebug.rendererTraceCategory, Trace.messageType.error);
381
- }
382
- static viewUtilLog(msg) {
383
- Trace.write(msg, NativeScriptDebug.viewUtilCategory);
384
- }
385
- static routerLog(message) {
386
- Trace.write(message, NativeScriptDebug.routerTraceCategory);
387
- }
388
- static routerError(message) {
389
- Trace.write(message, NativeScriptDebug.routerTraceCategory, Trace.messageType.error);
390
- }
391
- static routeReuseStrategyLog(message) {
392
- Trace.write(message, NativeScriptDebug.routeReuseStrategyTraceCategory);
393
- }
394
- static styleError(message) {
395
- Trace.write(message, Trace.categories.Style, Trace.messageType.error);
396
- }
397
- static listViewLog(message) {
398
- Trace.write(message, NativeScriptDebug.listViewTraceCategory);
399
- }
400
- static listViewError(message) {
401
- Trace.write(message, NativeScriptDebug.listViewTraceCategory, Trace.messageType.error);
402
- }
403
- static bootstrapLog(message) {
404
- Trace.write(message, NativeScriptDebug.bootstrapCategory);
405
- }
406
- static bootstrapLogError(message) {
407
- Trace.write(message, NativeScriptDebug.bootstrapCategory, Trace.messageType.error);
408
- }
409
- static hmrLog(message) {
410
- Trace.write(message, NativeScriptDebug.hmrTraceCategory);
411
- }
412
- static hmrLogError(message) {
413
- Trace.write(message, NativeScriptDebug.hmrTraceCategory, Trace.messageType.error);
414
- }
415
- }
416
-
417
913
  class FrameService {
418
914
  // TODO: Add any methods that are needed to handle frame/page navigation
419
915
  getFrame() {
@@ -1457,13 +1953,43 @@ function createAngularRootTransitionGuard(globalObj = globalThis, timers = { set
1457
1953
  // This is crucial because HMR imports a fresh @angular/core with empty LView tracking
1458
1954
  // We need to use the original one that has the registered LViews
1459
1955
  rememberAngularCoreForHmr(i0, globalThis);
1956
+ // Install the cross-module HMR component registrar. The Vite plugin
1957
+ // `ns-component-hmr-register` injects a call to the global hook
1958
+ // `__NS_HMR_REGISTER_COMPONENT__` at the end of every user `.ts` file
1959
+ // that declares an `@Component`-decorated class. After an HMR reboot,
1960
+ // each re-evaluated module pushes its fresh class into the registry,
1961
+ // and HMR helpers (modal restore, route replay) read the registry via
1962
+ // `getFreshComponentClass` to re-attach to the *live* class instead of
1963
+ // a captured stale reference. Production short-circuits inside the
1964
+ // helper (the hook is never assigned).
1965
+ //
1966
+ // We install the registrar before any other module initialization can
1967
+ // reference the hook so a user module loaded synchronously alongside
1968
+ // `@nativescript/angular` always finds the function present.
1969
+ installAngularHmrComponentRegistrar();
1970
+ // Install the cross-module registration entry point used by HMR-aware
1971
+ // services (e.g. `NativeDialog`) to ask for eager construction after
1972
+ // every bootstrap. Idempotent: re-evaluations after HMR are no-ops.
1973
+ installHmrEagerRegistrar();
1460
1974
  const angularHmrGlobal = globalThis;
1461
- angularHmrGlobal.__NS_REMEMBER_ANGULAR_CORE__ = (core) => setAngularCoreForHmr(core, angularHmrGlobal);
1975
+ angularHmrGlobal.__NS_REMEMBER_ANGULAR_CORE__ = (core) => {
1976
+ setAngularCoreForHmr(core, angularHmrGlobal);
1977
+ };
1462
1978
  function disableRootViewHanding(view) {
1463
1979
  view.__disable_root_view_handling = true;
1464
1980
  }
1465
1981
  const preAngularDisposal$ = new Subject();
1466
- const postAngularBootstrap$ = new Subject();
1982
+ /**
1983
+ * Stream that emits when an Angular module finishes bootstrapping. Modeled
1984
+ * as a `ReplaySubject(1)` so consumers (e.g. `NativeDialog`) instantiated
1985
+ * lazily — *after* the bootstrap event has already fired — still receive
1986
+ * the latest event and can react. Without buffering, a service that the
1987
+ * user app injects on first need (after bootstrap) would silently miss the
1988
+ * `hotreload` notification and skip HMR-only work like restoring captured
1989
+ * modal state. The buffer size of 1 means each new HMR cycle replaces the
1990
+ * cached event so cycle N's late subscribers don't see cycle N-1's event.
1991
+ */
1992
+ const postAngularBootstrap$ = new ReplaySubject(1);
1467
1993
  /**
1468
1994
  * @deprecated
1469
1995
  */
@@ -1480,14 +2006,34 @@ if (import.meta['webpackHot']) {
1480
2006
  };
1481
2007
  }
1482
2008
  function emitModuleBootstrapEvent(ref, name, reason) {
2009
+ if (NativeScriptDebug.isLogEnabled()) {
2010
+ NativeScriptDebug.hmrLog(`emitModuleBootstrapEvent name=${name} reason=${reason}`);
2011
+ }
2012
+ // Instantiate registered HMR-aware services *before* emitting so they
2013
+ // attach their subscriptions in the same JS task and are guaranteed to
2014
+ // observe the event being emitted. `postAngularBootstrap$` is also a
2015
+ // `ReplaySubject(1)`, so a service injected later still receives the
2016
+ // buffered event — the eager pass is the fast path that lets the
2017
+ // restore work begin in the same task as bootstrap completion.
2018
+ if (name === 'main') {
2019
+ runHmrEagerInstantiators(ref.injector, (err) => NativeScriptDebug.bootstrapLogError(`HMR eager instantiator threw: ${err?.message ?? err}`));
2020
+ }
1483
2021
  postAngularBootstrap$.next({
1484
2022
  moduleType: name,
1485
2023
  reference: ref,
1486
2024
  reason,
1487
2025
  });
2026
+ if (NativeScriptDebug.isLogEnabled()) {
2027
+ NativeScriptDebug.hmrLog(`postAngularBootstrap$.next() emitted name=${name} reason=${reason}`);
2028
+ }
1488
2029
  }
1489
2030
  function destroyRef(ref, name, reason) {
1490
2031
  if (ref) {
2032
+ const refKind = ref instanceof PlatformRef ? 'PlatformRef' : ref instanceof NgModuleRef ? 'NgModuleRef' : ref instanceof ApplicationRef ? 'ApplicationRef' : '(unknown)';
2033
+ const traceEnabled = NativeScriptDebug.isLogEnabled();
2034
+ if (traceEnabled) {
2035
+ NativeScriptDebug.hmrLog(`destroyRef kind=${refKind} name=${name ?? '(none)'} reason=${reason ?? '(none)'}`);
2036
+ }
1491
2037
  if (ref instanceof PlatformRef) {
1492
2038
  preAngularDisposal$.next({
1493
2039
  moduleType: 'platform',
@@ -1503,6 +2049,9 @@ function destroyRef(ref, name, reason) {
1503
2049
  });
1504
2050
  }
1505
2051
  ref.destroy();
2052
+ if (traceEnabled) {
2053
+ NativeScriptDebug.hmrLog(`destroyRef DONE kind=${refKind} name=${name ?? '(none)'}`);
2054
+ }
1506
2055
  }
1507
2056
  }
1508
2057
  function runZoneSyncTask(fn) {
@@ -1985,9 +2534,14 @@ function runNativeScriptAngularApp(options) {
1985
2534
  disposePlatform('hotreload');
1986
2535
  };
1987
2536
  global['__reboot_ng_modules__'] = (shouldDisposePlatform = false) => {
2537
+ // Bump the global HMR cycle counter so subsequent diagnostic log
2538
+ // lines (class registry, dialog services) can be cross-referenced
2539
+ // to a specific reboot. Counter is always incremented; the trace
2540
+ // category gates whether we surface it in the console.
2541
+ const cycleNum = _hmrDiagBumpCycle();
1988
2542
  const traceEnabled = NativeScriptDebug.isLogEnabled();
1989
2543
  if (traceEnabled) {
1990
- NativeScriptDebug.hmrLog(`__reboot_ng_modules__ called shouldDisposePlatform=${shouldDisposePlatform} bootstrapId=${bootstrapId} hasMainModuleRef=${!!mainModuleRef}`);
2544
+ NativeScriptDebug.hmrLog(`__reboot_ng_modules__ called cycle=${cycleNum} shouldDisposePlatform=${shouldDisposePlatform} bootstrapId=${bootstrapId} hasMainModuleRef=${!!mainModuleRef}`);
1991
2545
  }
1992
2546
  try {
1993
2547
  global['__NS_CAPTURE_ANGULAR_HMR_ROUTE__']?.();
@@ -1995,17 +2549,17 @@ function runNativeScriptAngularApp(options) {
1995
2549
  catch { }
1996
2550
  disposeLastModules('hotreload');
1997
2551
  if (traceEnabled) {
1998
- NativeScriptDebug.hmrLog(`after disposeLastModules bootstrapId=${bootstrapId}`);
2552
+ NativeScriptDebug.hmrLog(`after disposeLastModules cycle=${cycleNum} bootstrapId=${bootstrapId}`);
1999
2553
  }
2000
2554
  if (shouldDisposePlatform) {
2001
2555
  disposePlatform('hotreload');
2002
2556
  }
2003
2557
  if (traceEnabled) {
2004
- NativeScriptDebug.hmrLog('calling bootstrapRoot');
2558
+ NativeScriptDebug.hmrLog(`calling bootstrapRoot cycle=${cycleNum}`);
2005
2559
  }
2006
2560
  bootstrapRoot('hotreload');
2007
2561
  if (traceEnabled) {
2008
- NativeScriptDebug.hmrLog(`bootstrapRoot returned bootstrapId=${bootstrapId}`);
2562
+ NativeScriptDebug.hmrLog(`bootstrapRoot returned cycle=${cycleNum} bootstrapId=${bootstrapId}`);
2009
2563
  }
2010
2564
  };
2011
2565
  if (isWebpackHot) {
@@ -5530,6 +6084,22 @@ class NativeDialogConfig {
5530
6084
  */
5531
6085
  this.closeOnNavigation = true;
5532
6086
  this.nativeOptions = {};
6087
+ /**
6088
+ * When true, this dialog will be re-opened automatically on Angular HMR
6089
+ * reboots so the user does not lose context every time a related file
6090
+ * changes. The new dialog reuses the same component class and `data` payload
6091
+ * (provided via `data`); other config such as `nativeOptions` is preserved
6092
+ * verbatim.
6093
+ *
6094
+ * The original `dialogRef.afterClosed()` subject is wired to the restored
6095
+ * dialog so consumers `await openModal(...)` resolve normally when the user
6096
+ * eventually closes the restored modal.
6097
+ *
6098
+ * Only opens via component class are restorable — `TemplateRef` openings
6099
+ * carry references that don't survive an HMR reboot and are silently
6100
+ * skipped. Has no effect outside of HMR.
6101
+ */
6102
+ this.preserveOnHmr = false;
5533
6103
  // TODO(jelbourn): add configuration for lifecycle hooks, ARIA labelling.
5534
6104
  }
5535
6105
  }
@@ -5652,6 +6222,183 @@ class NativeDialogRef {
5652
6222
  }
5653
6223
  }
5654
6224
 
6225
+ const STASH_KEY = '__NS_ANGULAR_HMR_PENDING_MODALS__';
6226
+ function getStashSlot() {
6227
+ return globalThis;
6228
+ }
6229
+ /**
6230
+ * Pure helper: filter the open dialog list down to entries that opted in via
6231
+ * `preserveOnHmr` and that we actually know how to restore (component-class
6232
+ * openings, not template openings).
6233
+ */
6234
+ function selectPreservableDialogs(openDialogs) {
6235
+ return openDialogs.filter((dialog) => isPreservable(dialog));
6236
+ }
6237
+ function isPreservable(dialog) {
6238
+ if (!dialog.config?.preserveOnHmr) {
6239
+ return false;
6240
+ }
6241
+ return typeof dialog.componentClass === 'function';
6242
+ }
6243
+ /**
6244
+ * Capture the open dialogs that opted into HMR preservation. Returns the
6245
+ * captured entries so callers can correlate counts in their logs.
6246
+ */
6247
+ function captureDialogsForHmr(openDialogs) {
6248
+ const preservable = selectPreservableDialogs(openDialogs);
6249
+ if (preservable.length === 0) {
6250
+ clearPendingHmrDialogs();
6251
+ return [];
6252
+ }
6253
+ const captures = preservable.map(({ ref, componentClass, config }) => {
6254
+ const subject = readAfterClosedSubject(ref);
6255
+ // Capture the source name now — the class reference itself becomes
6256
+ // stale after the reboot, but the name is stable across realms and
6257
+ // is what the post-reboot registry is keyed on.
6258
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6259
+ const componentName = componentClass?.name ?? '';
6260
+ return {
6261
+ // Asserted non-null in `isPreservable`.
6262
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6263
+ componentClass: componentClass,
6264
+ componentName,
6265
+ config,
6266
+ graftAfterClosed: (value) => {
6267
+ if (!subject) {
6268
+ return;
6269
+ }
6270
+ try {
6271
+ if (!subject.closed) {
6272
+ subject.next(value);
6273
+ subject.complete();
6274
+ }
6275
+ }
6276
+ catch {
6277
+ // Swallow: the subject may have completed during dispose; nothing for us to do.
6278
+ }
6279
+ },
6280
+ };
6281
+ });
6282
+ globalThis[STASH_KEY] = captures;
6283
+ return captures;
6284
+ }
6285
+ /**
6286
+ * Drain the pending captures. The caller (the new `NativeDialog`) is expected
6287
+ * to re-open each entry and graft `afterClosed` back into the original
6288
+ * subject.
6289
+ */
6290
+ function consumePendingHmrDialogs() {
6291
+ const slot = globalThis[STASH_KEY];
6292
+ if (!Array.isArray(slot)) {
6293
+ return [];
6294
+ }
6295
+ delete globalThis[STASH_KEY];
6296
+ return slot.filter((entry) => !!entry && typeof entry.componentClass === 'function');
6297
+ }
6298
+ /**
6299
+ * Remove the pending captures without restoring. Useful when a reboot happens
6300
+ * for reasons other than module replacement (e.g. platform dispose) and we
6301
+ * don't want stale modal state to leak into the next bootstrap.
6302
+ */
6303
+ function clearPendingHmrDialogs() {
6304
+ delete globalThis[STASH_KEY];
6305
+ }
6306
+ /**
6307
+ * Test/debug helper: read the current stash without consuming it.
6308
+ */
6309
+ function peekPendingHmrDialogs() {
6310
+ const slot = globalThis[STASH_KEY];
6311
+ return Array.isArray(slot) ? slot.slice() : [];
6312
+ }
6313
+ /**
6314
+ * Reach into the dialog ref's private `_afterClosed` subject. We touch the
6315
+ * private field intentionally — the ref class lives inside this package and
6316
+ * we want HMR restore to be a feature of the dialog system rather than a
6317
+ * reason to widen its public surface for everyone.
6318
+ */
6319
+ function readAfterClosedSubject(ref) {
6320
+ const candidate = ref._afterClosed;
6321
+ if (!candidate || typeof candidate !== 'object') {
6322
+ return undefined;
6323
+ }
6324
+ if (typeof candidate.next !== 'function') {
6325
+ return undefined;
6326
+ }
6327
+ return candidate;
6328
+ }
6329
+ /**
6330
+ * Used by the resume-side: if the stash references something that can no
6331
+ * longer be opened (e.g. the component class is dead post-reload), we still
6332
+ * need to release its consumers so awaited promises don't dangle forever.
6333
+ */
6334
+ function abortCapturedDialog(captured) {
6335
+ try {
6336
+ captured.graftAfterClosed(undefined);
6337
+ }
6338
+ catch {
6339
+ // Best-effort.
6340
+ }
6341
+ }
6342
+
6343
+ /**
6344
+ * Best-effort animation helpers used by the dialog HMR layer to make
6345
+ * the close + reopen round-trip feel like an in-place content refresh.
6346
+ *
6347
+ * They live in a tiny standalone module on purpose:
6348
+ *
6349
+ * - `dialog-services.ts` pulls in `@angular/core`, which Jest cannot
6350
+ * load in our spec runner without an extra ESM transform. By
6351
+ * keeping these helpers free of `@angular/core` we can unit-test
6352
+ * them in isolation (`dialog-hmr-animation.spec.ts`) while
6353
+ * `dialog-services.ts` re-exports them at the public API layer.
6354
+ * - The helpers are inherently best-effort: a missing
6355
+ * `_nativeModalRef`, a frozen `_modalAnimatedOptions` stack, or a
6356
+ * future `NativeDialogConfig` shape change must never break HMR
6357
+ * restore — we just fall back to the original animated behavior.
6358
+ */
6359
+ /**
6360
+ * Mutate the top of `parentView._modalAnimatedOptions` to `false` for
6361
+ * the given candidate so the imminent native close runs un-animated.
6362
+ *
6363
+ * iOS reads `_modalAnimatedOptions.slice(-1)[0]` when dismissing a
6364
+ * modal (see core `view-common.ts` / `view/index.ios.ts`). The
6365
+ * Angular dialog service only pushes one entry per open call, so the
6366
+ * top entry is the exact flag that controls the dismiss we're about
6367
+ * to trigger as part of the HMR root-view replacement.
6368
+ */
6369
+ function suppressNativeCloseAnimation(candidate) {
6370
+ if (!candidate.config?.preserveOnHmr) {
6371
+ return;
6372
+ }
6373
+ try {
6374
+ const modalRef = candidate.ref?._nativeModalRef;
6375
+ const parentView = modalRef?.parentView;
6376
+ const stack = parentView?._modalAnimatedOptions;
6377
+ if (Array.isArray(stack) && stack.length > 0) {
6378
+ stack[stack.length - 1] = false;
6379
+ }
6380
+ }
6381
+ catch {
6382
+ // Swallow: a missing `_nativeModalRef` / `_modalAnimatedOptions`
6383
+ // is acceptable — we just lose the no-animation optimisation.
6384
+ }
6385
+ }
6386
+ /**
6387
+ * Build a `NativeDialogConfig` clone of `original` whose
6388
+ * `nativeOptions.animated` is forced to `false`. Used when re-opening
6389
+ * a captured modal so the open animation matches the suppressed
6390
+ * close — together they make the HMR round-trip feel like a content
6391
+ * refresh instead of a close/reopen.
6392
+ */
6393
+ function buildNonAnimatedRestoreConfig(original) {
6394
+ // Clone via `Object.assign` so consumers holding the original
6395
+ // config (e.g. caching it for re-open) don't see mutations from
6396
+ // the HMR pathway.
6397
+ const cloned = Object.assign(new NativeDialogConfig(), original);
6398
+ cloned.nativeOptions = { ...(original?.nativeOptions || {}), animated: false };
6399
+ return cloned;
6400
+ }
6401
+
5655
6402
  let NativeModalRef = class NativeModalRef {
5656
6403
  constructor(_config, _injector, location) {
5657
6404
  this._config = _config;
@@ -5786,6 +6533,54 @@ NativeModalRef = __decorate([
5786
6533
  * Use of this source code is governed by an MIT-style license that can be
5787
6534
  * found in the LICENSE file at https://angular.io/license
5788
6535
  */
6536
+ /**
6537
+ * Dialog HMR lifecycle log.
6538
+ */
6539
+ function hmrDialogLog(message) {
6540
+ if (!isAngularHmrEnabled()) {
6541
+ return;
6542
+ }
6543
+ if (!NativeScriptDebug.isLogEnabled()) {
6544
+ return;
6545
+ }
6546
+ NativeScriptDebug.hmrLog(`[dialog] ${message}`);
6547
+ }
6548
+ /**
6549
+ * Lower-level dialog HMR wiring trace (module-realm count, NativeDialog
6550
+ * instance count, registry hits/misses). Distinct from `hmrDialogLog`
6551
+ * for greppability — both fan into the same Trace category so a single
6552
+ * `Trace.setCategories(NativeScriptDebug.hmrTraceCategory)` toggle
6553
+ * surfaces them all.
6554
+ */
6555
+ function hmrDialogDiag(message) {
6556
+ if (!isAngularHmrEnabled()) {
6557
+ return;
6558
+ }
6559
+ if (!NativeScriptDebug.isLogEnabled()) {
6560
+ return;
6561
+ }
6562
+ NativeScriptDebug.hmrLog(`[dialog-diag] ${message}`);
6563
+ }
6564
+ /**
6565
+ * Module-evaluation marker. Increments on every fresh evaluation of
6566
+ * `dialog-services.ts`. If we see this number rise on every HMR cycle,
6567
+ * the file is being re-evaluated (good). If it stays flat, the module
6568
+ * is being served from cache (bad — class identities won't change).
6569
+ */
6570
+ const DIALOG_MODULE_DIAG_KEY = '__NS_HMR_DIAG_DIALOG_MODULE__';
6571
+ function getDialogModuleDiag() {
6572
+ const slot = globalThis;
6573
+ if (!slot[DIALOG_MODULE_DIAG_KEY]) {
6574
+ slot[DIALOG_MODULE_DIAG_KEY] = { evals: 0, instances: 0, lastEvalAt: 0 };
6575
+ }
6576
+ return slot[DIALOG_MODULE_DIAG_KEY];
6577
+ }
6578
+ {
6579
+ const md = getDialogModuleDiag();
6580
+ md.evals += 1;
6581
+ md.lastEvalAt = Date.now();
6582
+ hmrDialogDiag(`module-eval count=${md.evals} (file=dialog-services.ts) timestamp=${md.lastEvalAt}`);
6583
+ }
5789
6584
  /** Injection token that can be used to access the data that was passed in to a dialog. */
5790
6585
  const NATIVE_DIALOG_DATA = new InjectionToken('NativeDialogData');
5791
6586
  /** Injection token that can be used to specify default dialog options. */
@@ -5799,6 +6594,14 @@ class NativeDialog {
5799
6594
  this._openDialogsAtThisLevel = [];
5800
6595
  this._afterAllClosedAtThisLevel = new Subject();
5801
6596
  this._afterOpenedAtThisLevel = new Subject();
6597
+ /**
6598
+ * Maps each open dialog ref back to the `(componentClass, config)` pair it
6599
+ * was opened with so the HMR snapshot can replay the call later. Dialogs
6600
+ * opened with a `TemplateRef` are tracked with `componentClass: undefined`
6601
+ * — the HMR layer skips them automatically.
6602
+ */
6603
+ this._openDialogMetadata = new WeakMap();
6604
+ this._hmrSubscriptions = [];
5802
6605
  // TODO (jelbourn): tighten the typing right-hand side of this expression.
5803
6606
  /**
5804
6607
  * Stream that emits when all open dialog have finished closing.
@@ -5816,6 +6619,50 @@ class NativeDialog {
5816
6619
  this._nativeModalType = NativeModalRef;
5817
6620
  this._dialogDataToken = NATIVE_DIALOG_DATA;
5818
6621
  this.locationStrategy = inject(NSLocationStrategy);
6622
+ // Bumps a global counter so we can detect duplicate or leaked
6623
+ // `NativeDialog` instances across HMR cycles. Field initialiser
6624
+ // ordering: this MUST run before `_initHmrLifecycle()` below so the
6625
+ // log line in that helper can include the assigned id.
6626
+ this._diagInstanceIdAssign = (() => {
6627
+ const md = getDialogModuleDiag();
6628
+ md.instances += 1;
6629
+ this._diagInstanceId = md.instances;
6630
+ hmrDialogDiag(`NativeDialog ctor instanceId=${md.instances} hasParentDialog=${!!this._parentDialog} moduleEvalCount=${md.evals}`);
6631
+ return null;
6632
+ })();
6633
+ // Initialise after every dependency above so the subscriptions can call
6634
+ // back into `this.open(...)` and `this.openDialogs` safely. The result is
6635
+ // unused — we just want a side-effect at construction time.
6636
+ this._hmrInitMarker = this._initHmrLifecycle();
6637
+ /**
6638
+ * Tracks whether a restore has already been scheduled for this
6639
+ * `NativeDialog` instance's lifetime. We only need to restore once
6640
+ * per HMR cycle — the rxjs `ReplaySubject(1)` for
6641
+ * `postAngularBootstrap$` delivers both the *previous* cycle's
6642
+ * cached event (replay on subscribe) **and** the *current* cycle's
6643
+ * fresh event, and the constructor stash peek can independently
6644
+ * notice pending work. Without this guard each of those triggers
6645
+ * would queue its own `setTimeout` and the logs would show two or
6646
+ * three "scheduling restore" lines per save.
6647
+ *
6648
+ * The guard is per-instance and the stash itself is the source of
6649
+ * truth: `_restorePendingDialogs` calls `consumePendingHmrDialogs()`
6650
+ * which atomically clears the stash, so even if the guard somehow
6651
+ * fired twice, only the first call would do real work.
6652
+ */
6653
+ this._restoreScheduledForThisInstance = false;
6654
+ /**
6655
+ * Per-cycle guard to keep `_restorePendingDialogs` idempotent if more
6656
+ * than one subscriber fires for the same bootstrap event. The
6657
+ * regression we have seen (`postAngularBootstrap$ → restore` logged
6658
+ * twice in the same hot reload) was caused by the `NativeDialogModule`
6659
+ * also listing `NativeDialog` in its `providers` array. That has been
6660
+ * removed, but we keep this flag as a defensive net so a future stray
6661
+ * subscription does not consume the stash twice. The flag is reset
6662
+ * after the consume so subsequent HMR cycles can run their own
6663
+ * restore.
6664
+ */
6665
+ this._restoreInFlight = false;
5819
6666
  }
5820
6667
  /** Keeps track of the currently-open dialogs. */
5821
6668
  get openDialogs() {
@@ -5825,42 +6672,363 @@ class NativeDialog {
5825
6672
  get afterOpened() {
5826
6673
  return this._parentDialog ? this._parentDialog.afterOpened : this._afterOpenedAtThisLevel;
5827
6674
  }
5828
- _getAfterAllClosed() {
5829
- const parent = this._parentDialog;
5830
- return parent ? parent._getAfterAllClosed() : this._afterAllClosedAtThisLevel;
6675
+ _getAfterAllClosed() {
6676
+ const parent = this._parentDialog;
6677
+ return parent ? parent._getAfterAllClosed() : this._afterAllClosedAtThisLevel;
6678
+ }
6679
+ open(componentOrTemplateRef, config) {
6680
+ config = _applyConfigDefaults(config, this._defaultOptions || new NativeDialogConfig());
6681
+ if (config.id && this.getDialogById(config.id) && (typeof ngDevMode === 'undefined' || ngDevMode)) {
6682
+ throw Error(`Dialog with id "${config.id}" exists already. The dialog id must be unique.`);
6683
+ }
6684
+ const dialogRef = this._attachDialogContent(componentOrTemplateRef, config);
6685
+ this.openDialogs.push(dialogRef);
6686
+ this._openDialogMetadata.set(dialogRef, {
6687
+ componentClass: componentOrTemplateRef instanceof TemplateRef ? undefined : componentOrTemplateRef,
6688
+ config,
6689
+ });
6690
+ dialogRef.afterClosed().subscribe(() => this._removeOpenDialog(dialogRef));
6691
+ this.afterOpened.next(dialogRef);
6692
+ // Notify the dialog container that the content has been attached.
6693
+ // dialogContainer._initializeWithAttachedContent();
6694
+ return dialogRef;
6695
+ }
6696
+ /**
6697
+ * Closes all of the currently-open dialogs.
6698
+ */
6699
+ closeAll() {
6700
+ this._closeDialogs(this.openDialogs);
6701
+ }
6702
+ /**
6703
+ * Finds an open dialog by its id.
6704
+ * @param id ID to use when looking up the dialog.
6705
+ */
6706
+ getDialogById(id) {
6707
+ return this.openDialogs.find((dialog) => dialog.id === id);
6708
+ }
6709
+ ngOnDestroy() {
6710
+ hmrDialogDiag(`NativeDialog ngOnDestroy instanceId=${this._diagInstanceId} openCount=${this._openDialogsAtThisLevel.length} subCount=${this._hmrSubscriptions.length}`);
6711
+ // Only close the dialogs at this level on destroy
6712
+ // since the parent service may still be active.
6713
+ this._closeDialogs(this._openDialogsAtThisLevel);
6714
+ this._afterAllClosedAtThisLevel.complete();
6715
+ this._afterOpenedAtThisLevel.complete();
6716
+ for (const sub of this._hmrSubscriptions) {
6717
+ try {
6718
+ sub.unsubscribe();
6719
+ }
6720
+ catch {
6721
+ // Best-effort: tearing down the dialog service shouldn't prevent the
6722
+ // rest of the module disposal from completing.
6723
+ }
6724
+ }
6725
+ this._hmrSubscriptions = [];
6726
+ }
6727
+ /**
6728
+ * Wires up HMR capture/restore. Only the root-level dialog manages the
6729
+ * stash so a stack of `NativeDialog` instances inside a child injector
6730
+ * doesn't fight for it.
6731
+ *
6732
+ * Production short-circuit: `isAngularHmrEnabled()` returns `false` in
6733
+ * release builds and when no NS Vite / webpack HMR runtime is present,
6734
+ * so the long-lived subscriptions below never attach in shipping apps.
6735
+ *
6736
+ * `postAngularBootstrap$` is a `ReplaySubject(1)` (see `application.ts`)
6737
+ * which means a `NativeDialog` instantiated *after* the bootstrap event
6738
+ * has already fired (typical when the user app injects `NativeDialog`
6739
+ * lazily via a service like `view.service.ts`) still receives the
6740
+ * buffered event and runs the restore path.
6741
+ */
6742
+ _initHmrLifecycle() {
6743
+ if (this._parentDialog) {
6744
+ hmrDialogDiag(`_initHmrLifecycle skipped (has parent dialog) instanceId=${this._diagInstanceId}`);
6745
+ return null;
6746
+ }
6747
+ if (!isAngularHmrEnabled()) {
6748
+ return null;
6749
+ }
6750
+ hmrDialogDiag(`_initHmrLifecycle wiring up subscriptions instanceId=${this._diagInstanceId} moduleEvalCount=${getDialogModuleDiag().evals}`);
6751
+ const dispose = preAngularDisposal$.subscribe((event) => {
6752
+ if (event.moduleType !== 'main' || event.reason !== 'hotreload') {
6753
+ return;
6754
+ }
6755
+ hmrDialogDiag(`preAngularDisposal$ fired (reason=${event.reason}) instanceId=${this._diagInstanceId}`);
6756
+ this._captureOpenDialogsForHmr();
6757
+ });
6758
+ const bootstrap = postAngularBootstrap$.subscribe((event) => {
6759
+ if (event.moduleType !== 'main' || event.reason !== 'hotreload') {
6760
+ return;
6761
+ }
6762
+ hmrDialogDiag(`postAngularBootstrap$ fired (reason=${event.reason}) instanceId=${this._diagInstanceId}`);
6763
+ this._maybeScheduleRestore(`postAngularBootstrap$ (reason=${event.reason})`);
6764
+ });
6765
+ this._hmrSubscriptions.push(dispose, bootstrap);
6766
+ // Belt-and-suspenders: even though `postAngularBootstrap$` replays
6767
+ // the last event for late subscribers, also peek the global stash
6768
+ // here. This catches the case where `NativeDialog` is instantiated
6769
+ // lazily — *after* `emitModuleBootstrapEvent` has fired and the
6770
+ // ReplaySubject's buffered event no longer matches the current
6771
+ // cycle. The `_maybeScheduleRestore` guard makes this a no-op when
6772
+ // the bootstrap subscriber already queued work.
6773
+ const pendingNow = peekPendingHmrDialogs();
6774
+ hmrDialogDiag(`_initHmrLifecycle stash peek pending=${pendingNow.length} instanceId=${this._diagInstanceId}`);
6775
+ if (pendingNow.length > 0) {
6776
+ this._maybeScheduleRestore(`stash peek on ctor: ${pendingNow.length} pending dialog(s)`);
6777
+ }
6778
+ return null;
6779
+ }
6780
+ /**
6781
+ * Schedule a restore exactly once per `NativeDialog` instance.
6782
+ *
6783
+ * The work is deferred to the next macrotask for two reasons:
6784
+ *
6785
+ * 1. `postAngularBootstrap$.next(...)` is fired from inside
6786
+ * `emitModuleBootstrapEvent`, which itself runs inside the
6787
+ * `bootstrapApplication` callback — so the call stack still
6788
+ * contains Angular's ApplicationRef bootstrap pipeline. Doing
6789
+ * `this.open(...)` synchronously re-entered Angular DI while a
6790
+ * `providedIn: 'root'` factory could still be on the resolution
6791
+ * stack, which surfaced as `NG0200: Circular dependency detected
6792
+ * for NativeDialog`. Yielding to a macrotask lets the bootstrap
6793
+ * stack fully unwind first.
6794
+ * 2. The eventual `parent.showModal(...)` cannot present onto a
6795
+ * view controller whose view is not yet in the iOS window
6796
+ * hierarchy (see `_scheduleRestoreOpenWhenReady` for the second
6797
+ * wait stage); a synchronous attempt would silently no-op
6798
+ * because iOS rejects the present without throwing.
6799
+ */
6800
+ _maybeScheduleRestore(triggerDescription) {
6801
+ if (this._restoreScheduledForThisInstance) {
6802
+ hmrDialogDiag(`_maybeScheduleRestore SKIP duplicate trigger=${triggerDescription} instanceId=${this._diagInstanceId}`);
6803
+ return;
6804
+ }
6805
+ this._restoreScheduledForThisInstance = true;
6806
+ hmrDialogLog(`scheduling restore (trigger=${triggerDescription}) instanceId=${this._diagInstanceId}`);
6807
+ setTimeout(() => {
6808
+ void this._restorePendingDialogs();
6809
+ }, 0);
6810
+ }
6811
+ _captureOpenDialogsForHmr() {
6812
+ const candidates = this._openDialogsAtThisLevel.map((ref) => {
6813
+ const meta = this._openDialogMetadata.get(ref);
6814
+ return {
6815
+ ref,
6816
+ componentClass: meta?.componentClass,
6817
+ config: meta?.config ?? new NativeDialogConfig(),
6818
+ };
6819
+ });
6820
+ hmrDialogDiag(`_captureOpenDialogsForHmr instanceId=${this._diagInstanceId} candidates=${candidates.length} (${candidates.map((c) => `${c.componentClass?.name ?? '(template)'}|preserveOnHmr=${!!c.config?.preserveOnHmr}`).join(', ')})`);
6821
+ const captured = captureDialogsForHmr(candidates);
6822
+ if (captured.length > 0) {
6823
+ // Suppress the close animation that's about to fire as part of
6824
+ // `_closeAllModalViewsInternal()` during root-view replacement.
6825
+ // Without this the user sees a slide-down + slide-up flicker
6826
+ // wrapping every HMR reboot.
6827
+ for (const candidate of candidates) {
6828
+ suppressNativeCloseAnimation(candidate);
6829
+ }
6830
+ hmrDialogLog(`captured ${captured.length} dialog(s) for HMR restore [${captured.map((c) => c.componentName).join(', ')}]`);
6831
+ }
6832
+ else if (this._openDialogsAtThisLevel.length > 0) {
6833
+ hmrDialogLog(`skipped capture: ${this._openDialogsAtThisLevel.length} open dialog(s) but none preservable`);
6834
+ }
6835
+ if (captured.length > 0 && NativeScriptDebug.isLogEnabled()) {
6836
+ NativeScriptDebug.hmrLog(`captured ${captured.length} dialog(s) for HMR restore`);
6837
+ }
5831
6838
  }
5832
- open(componentOrTemplateRef, config) {
5833
- config = _applyConfigDefaults(config, this._defaultOptions || new NativeDialogConfig());
5834
- if (config.id && this.getDialogById(config.id) && (typeof ngDevMode === 'undefined' || ngDevMode)) {
5835
- throw Error(`Dialog with id "${config.id}" exists already. The dialog id must be unique.`);
6839
+ async _restorePendingDialogs() {
6840
+ if (this._restoreInFlight) {
6841
+ hmrDialogLog('skipping restore: already in flight');
6842
+ return;
6843
+ }
6844
+ const pending = consumePendingHmrDialogs();
6845
+ if (pending.length === 0) {
6846
+ return;
6847
+ }
6848
+ this._restoreInFlight = true;
6849
+ hmrDialogLog(`restoring ${pending.length} dialog(s) after reboot [${pending.map((c) => c.componentName).join(', ')}]`);
6850
+ if (NativeScriptDebug.isLogEnabled()) {
6851
+ NativeScriptDebug.hmrLog(`restoring ${pending.length} dialog(s) after HMR reboot`);
6852
+ }
6853
+ try {
6854
+ for (const captured of pending) {
6855
+ this._restoreSingleDialog(captured);
6856
+ }
6857
+ }
6858
+ finally {
6859
+ this._restoreInFlight = false;
5836
6860
  }
5837
- const dialogRef = this._attachDialogContent(componentOrTemplateRef, config);
5838
- this.openDialogs.push(dialogRef);
5839
- dialogRef.afterClosed().subscribe(() => this._removeOpenDialog(dialogRef));
5840
- this.afterOpened.next(dialogRef);
5841
- // Notify the dialog container that the content has been attached.
5842
- // dialogContainer._initializeWithAttachedContent();
5843
- return dialogRef;
5844
6861
  }
5845
6862
  /**
5846
- * Closes all of the currently-open dialogs.
6863
+ * Resolve the freshest known class object for the captured component
6864
+ * name and re-open the dialog through the normal `open` path, but
6865
+ * only once the new root view is actually attached to the iOS window
6866
+ * hierarchy.
6867
+ *
6868
+ * Why a class lookup at all: an HMR reboot calls
6869
+ * `ɵresetCompiledComponents()` which clears each component's `ɵcmp`
6870
+ * field but **leaves the class identity unchanged**. The patched
6871
+ * `ɵɵdefineComponent` then re-registers the same class object under
6872
+ * the same source name when Angular re-renders the component. A
6873
+ * fresh-class lookup therefore returns either the same object the
6874
+ * stash captured (most common) or, on the rare occasion that the
6875
+ * source file's `@Component` decorator was re-evaluated into a brand
6876
+ * new class object (e.g. the user added `@Component(...)` to a new
6877
+ * exported symbol), the live one. Either way, a single check is
6878
+ * enough — the previous retry schedule was a no-op in 100 % of
6879
+ * observed cycles because the captured class IS the live class.
5847
6880
  */
5848
- closeAll() {
5849
- this._closeDialogs(this.openDialogs);
6881
+ _restoreSingleDialog(captured) {
6882
+ const live = getFreshComponentClass(captured.componentName);
6883
+ const componentClass = live ?? captured.componentClass;
6884
+ const usingFresh = !!live && live !== captured.componentClass;
6885
+ // Detailed diagnostic to disambiguate the three reasons
6886
+ // `usingFreshClass=false` could log:
6887
+ // 1. liveDefined=false: the patched ɵɵdefineComponent never
6888
+ // registered a class for this name in the new realm. Most
6889
+ // likely cause: the component module was not re-evaluated
6890
+ // after the eviction (file not in `evictPaths`, or runtime
6891
+ // did not actually evict it).
6892
+ // 2. liveDefined=true but live === captured: the file WAS
6893
+ // re-evaluated, but the registry still hands back the same
6894
+ // class object. This shouldn't happen post-reboot for a
6895
+ // truly fresh realm; if it does, the captured snapshot was
6896
+ // taken from the same realm that's now serving the live
6897
+ // class — i.e. the disposal path is not actually disposing
6898
+ // the old realm before the new one boots.
6899
+ // 3. liveDefined=true and live !== captured: usingFresh path,
6900
+ // the registry IS doing its job. This is the success case.
6901
+ const liveDefined = !!live;
6902
+ const sameAsCapture = liveDefined && live === captured.componentClass;
6903
+ hmrDialogDiag(`_restoreSingleDialog name=${captured.componentName} liveDefined=${liveDefined} sameAsCapture=${sameAsCapture} usingFresh=${usingFresh} capturedFnName=${captured.componentClass?.name ?? '(none)'} liveFnName=${live?.name ?? '(none)'}`);
6904
+ this._scheduleRestoreOpenWhenReady(captured, componentClass, usingFresh);
5850
6905
  }
5851
6906
  /**
5852
- * Finds an open dialog by its id.
5853
- * @param id ID to use when looking up the dialog.
6907
+ * Maximum time we'll wait for the new root view to attach to the
6908
+ * iOS window before giving up and trying the open anyway. NS Vite's
6909
+ * worst observed reboot-to-rootview-loaded gap is ~250 ms; one
6910
+ * second leaves headroom for slower devices without leaving the
6911
+ * captured dialog stuck in the stash if something genuinely goes
6912
+ * wrong.
5854
6913
  */
5855
- getDialogById(id) {
5856
- return this.openDialogs.find((dialog) => dialog.id === id);
6914
+ static { this._ROOT_VIEW_LOADED_TIMEOUT_MS = 1_000; }
6915
+ /**
6916
+ * Defer the actual `this.open(...)` call until the new root view's
6917
+ * underlying iOS `UIViewController.view` is in the window hierarchy.
6918
+ *
6919
+ * iOS silently rejects `presentViewControllerAnimatedCompletion` if
6920
+ * the parent controller's view is not yet attached to a `UIWindow`
6921
+ * (see `view/index.ios.ts::_showNativeModalView`, which logs to
6922
+ * `Trace` and returns without throwing). In dev that would surface
6923
+ * as a vanished modal with no actionable error.
6924
+ *
6925
+ * NS marks a view as `isLoaded === true` from
6926
+ * `UILayoutViewController.viewWillAppear`, which fires once iOS has
6927
+ * decided to add the view to its window. Listening for the
6928
+ * `loadedEvent` (one-shot) plus an extra macrotask gives UIKit a
6929
+ * chance to finish window attachment before we present.
6930
+ */
6931
+ _scheduleRestoreOpenWhenReady(captured, componentClass, usingFresh) {
6932
+ const rootView = Application.getRootView();
6933
+ if (rootView && rootView.isLoaded) {
6934
+ // Even when isLoaded is already true we yield once: a previous
6935
+ // capture in the same hot reload cycle may have set isLoaded on
6936
+ // the *outgoing* root view and a new root view is about to take
6937
+ // over. A single setTimeout(0) gives `setWindowContent` a chance
6938
+ // to finish swapping `win.rootViewController` before we try to
6939
+ // present on top of it.
6940
+ setTimeout(() => this._performRestoreOpen(captured, componentClass, usingFresh), 0);
6941
+ return;
6942
+ }
6943
+ if (!rootView) {
6944
+ // No root view at all yet — extremely unusual at this point in
6945
+ // the bootstrap, but protect against it by polling. We use the
6946
+ // same timeout budget as the loaded path.
6947
+ this._pollForRootView(captured, componentClass, usingFresh, Date.now());
6948
+ return;
6949
+ }
6950
+ hmrDialogLog(`restore ${captured.componentName} waiting for root view loadedEvent`);
6951
+ let settled = false;
6952
+ const onLoaded = () => {
6953
+ if (settled)
6954
+ return;
6955
+ settled = true;
6956
+ try {
6957
+ rootView.off(View.loadedEvent, onLoaded);
6958
+ }
6959
+ catch {
6960
+ // off may throw on stale view bindings; the `settled` flag
6961
+ // already prevents a double-fire.
6962
+ }
6963
+ // Defer one tick after viewWillAppear so UIKit completes the
6964
+ // actual window attachment (`view.window` is set during the
6965
+ // view-controller transition that follows viewWillAppear).
6966
+ setTimeout(() => this._performRestoreOpen(captured, componentClass, usingFresh), 0);
6967
+ };
6968
+ try {
6969
+ rootView.once(View.loadedEvent, onLoaded);
6970
+ }
6971
+ catch {
6972
+ // If the event subscription fails (ancient core builds), fall
6973
+ // back to a tiny delay — better to attempt the open and have
6974
+ // iOS log a benign trace than to leak the dialog stash.
6975
+ setTimeout(() => onLoaded(), 50);
6976
+ }
6977
+ // Bound the wait so a never-loading root view can't permanently
6978
+ // pin the captured dialog in the stash.
6979
+ setTimeout(() => {
6980
+ if (settled)
6981
+ return;
6982
+ hmrDialogLog(`restore ${captured.componentName} root view never loaded within ${NativeDialog._ROOT_VIEW_LOADED_TIMEOUT_MS}ms; attempting open anyway`);
6983
+ onLoaded();
6984
+ }, NativeDialog._ROOT_VIEW_LOADED_TIMEOUT_MS);
6985
+ }
6986
+ _pollForRootView(captured, componentClass, usingFresh, startedAt) {
6987
+ const rootView = Application.getRootView();
6988
+ if (rootView) {
6989
+ this._scheduleRestoreOpenWhenReady(captured, componentClass, usingFresh);
6990
+ return;
6991
+ }
6992
+ if (Date.now() - startedAt > NativeDialog._ROOT_VIEW_LOADED_TIMEOUT_MS) {
6993
+ hmrDialogLog(`restore ${captured.componentName} aborted: no root view after ${NativeDialog._ROOT_VIEW_LOADED_TIMEOUT_MS}ms`);
6994
+ abortCapturedDialog(captured);
6995
+ return;
6996
+ }
6997
+ setTimeout(() => this._pollForRootView(captured, componentClass, usingFresh, startedAt), 16);
5857
6998
  }
5858
- ngOnDestroy() {
5859
- // Only close the dialogs at this level on destroy
5860
- // since the parent service may still be active.
5861
- this._closeDialogs(this._openDialogsAtThisLevel);
5862
- this._afterAllClosedAtThisLevel.complete();
5863
- this._afterOpenedAtThisLevel.complete();
6999
+ _performRestoreOpen(captured, componentClass, usingFresh) {
7000
+ hmrDialogLog(`restore ${captured.componentName} usingFreshClass=${usingFresh}`);
7001
+ if (NativeScriptDebug.isLogEnabled() && usingFresh) {
7002
+ NativeScriptDebug.hmrLog(`HMR modal restore using fresh class for ${captured.componentName}`);
7003
+ }
7004
+ // Force the restored modal to open without animation so the round-
7005
+ // trip looks like an instant content refresh rather than a full
7006
+ // close-and-reopen sequence.
7007
+ const restoreConfig = buildNonAnimatedRestoreConfig(captured.config);
7008
+ try {
7009
+ const newRef = this.open(componentClass, restoreConfig);
7010
+ hmrDialogLog(`restore ${captured.componentName} → opened newRef.id=${newRef?.id ?? 'n/a'}`);
7011
+ newRef.afterClosed().subscribe({
7012
+ next: (value) => captured.graftAfterClosed(value),
7013
+ complete: () => captured.graftAfterClosed(undefined),
7014
+ });
7015
+ }
7016
+ catch (err) {
7017
+ abortCapturedDialog(captured);
7018
+ const message = err?.message ?? String(err);
7019
+ hmrDialogLog(`restore ${captured.componentName} FAILED: ${message}`);
7020
+ if (NativeScriptDebug.isLogEnabled()) {
7021
+ NativeScriptDebug.hmrLogError(`HMR modal restore failed: ${message}`);
7022
+ }
7023
+ }
7024
+ }
7025
+ /**
7026
+ * Test/debug helper: discard any captured modals without restoring them.
7027
+ * Mostly useful when projects want to opt out of restoration without
7028
+ * recompiling. Public callers should prefer `preserveOnHmr: false` instead.
7029
+ */
7030
+ static _clearPendingHmrDialogs() {
7031
+ clearPendingHmrDialogs();
5864
7032
  }
5865
7033
  /**
5866
7034
  * Attaches the user-provided component to the already-created dialog container.
@@ -5925,6 +7093,7 @@ class NativeDialog {
5925
7093
  const index = this.openDialogs.indexOf(dialogRef);
5926
7094
  if (index > -1) {
5927
7095
  this.openDialogs.splice(index, 1);
7096
+ this._openDialogMetadata.delete(dialogRef);
5928
7097
  // If all the dialogs were closed, remove/restore the `aria-hidden`
5929
7098
  // to a the siblings and emit to the `afterAllClosed` stream.
5930
7099
  if (!this.openDialogs.length) {
@@ -5961,6 +7130,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
5961
7130
  function _applyConfigDefaults(config, defaultOptions) {
5962
7131
  return { ...defaultOptions, ...config };
5963
7132
  }
7133
+ /**
7134
+ * Register `NativeDialog` with the application's HMR eager-instantiate
7135
+ * registry so the post-bootstrap pipeline forces an `injector.get()` on
7136
+ * the service in the same JS task as the bootstrap event. Without this,
7137
+ * `NativeDialog` is only constructed when something in user-app code
7138
+ * injects it (typically a wrapper service that opens modals). When that
7139
+ * injection happens lazily — on first user-driven modal open — captured
7140
+ * dialogs from a prior HMR cycle wait until the user reopens *something*
7141
+ * before the new realm even sees the stash. Eager instantiation makes
7142
+ * the restore work happen as early as possible during a hot reload while
7143
+ * staying gated to dev mode + HMR-active environments.
7144
+ *
7145
+ * Registered after the class declaration so the closure references the
7146
+ * fully-defined class rather than tripping the temporal dead zone.
7147
+ *
7148
+ * Idempotent: the registry de-dupes function references, so multiple
7149
+ * evaluations of this module across HMR cycles never accumulate stale
7150
+ * registrations.
7151
+ */
7152
+ if (isAngularHmrEnabled()) {
7153
+ const added = registerHmrEagerInstantiator((injector) => {
7154
+ try {
7155
+ const inst = injector.get(NativeDialog, null);
7156
+ hmrDialogDiag(`eager-instantiator fired NativeDialog=${inst ? `instance#${inst._diagInstanceId}` : 'null'}`);
7157
+ }
7158
+ catch (err) {
7159
+ hmrDialogDiag(`eager-instantiator threw: ${err?.message ?? err}`);
7160
+ // Some user-app providers may not include `NativeDialog` (e.g. a
7161
+ // module that doesn't depend on the dialog feature). The registry
7162
+ // contract is "best effort": failing to find the token must be a
7163
+ // silent no-op so unrelated apps aren't penalized.
7164
+ }
7165
+ });
7166
+ hmrDialogDiag(`registerHmrEagerInstantiator added=${added} (false means already present in registry)`);
7167
+ }
5964
7168
 
5965
7169
  /**
5966
7170
  * @license
@@ -6036,17 +7240,42 @@ function getClosestDialog(element, openDialogs) {
6036
7240
  return view ? openDialogs.find((dialog) => dialog.id === view['__ng_modal_id__']) : null;
6037
7241
  }
6038
7242
 
7243
+ /**
7244
+ * Convenience module that re-exports the `NativeDialogCloseDirective` for
7245
+ * template-driven `[nativeDialogClose]` usage.
7246
+ *
7247
+ * **Important**: `NativeDialog` itself is **not** listed in this module's
7248
+ * `providers` array. The service is `@Injectable({ providedIn: 'root' })`,
7249
+ * which already registers a single root-level instance and is fully
7250
+ * tree-shakeable. Listing it here as well caused Angular to treat the
7251
+ * module-level provider and the `providedIn: 'root'` factory as
7252
+ * *separate* registrations once the module was pulled into a standalone
7253
+ * app via `importProvidersFrom(NativeDialogModule, ...)`. The duplicate
7254
+ * registration triggered:
7255
+ *
7256
+ * - Two `NativeDialog` instances in the same root environment injector,
7257
+ * each subscribing to `postAngularBootstrap$`, which produced
7258
+ * duplicate restore attempts during HMR.
7259
+ * - `NG0200: Circular dependency detected for NativeDialog` while a
7260
+ * captured modal was being re-opened during HMR restore, because the
7261
+ * second resolution started while the first was still in progress.
7262
+ *
7263
+ * Removing the redundant entry collapses both providers back into a
7264
+ * single root-level instance, which is what `providedIn: 'root'`
7265
+ * documents. App authors who explicitly wire `NativeDialog` themselves
7266
+ * (e.g. in a feature module's `providers`) keep working unchanged
7267
+ * because they're targeting the same class symbol.
7268
+ */
6039
7269
  class NativeDialogModule {
6040
7270
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: NativeDialogModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
6041
7271
  static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.0.9", ngImport: i0, type: NativeDialogModule, imports: [NativeDialogCloseDirective], exports: [NativeDialogCloseDirective] }); }
6042
- static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: NativeDialogModule, providers: [NativeDialog] }); }
7272
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: NativeDialogModule }); }
6043
7273
  }
6044
7274
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: NativeDialogModule, decorators: [{
6045
7275
  type: NgModule,
6046
7276
  args: [{
6047
7277
  imports: [NativeDialogCloseDirective],
6048
7278
  exports: [NativeDialogCloseDirective],
6049
- providers: [NativeDialog],
6050
7279
  }]
6051
7280
  }] });
6052
7281
 
@@ -7925,9 +9154,37 @@ function cloneRoutesForBootstrap(routes) {
7925
9154
  const CURRENT_ROUTE_KEY = '__NS_ANGULAR_HMR_CURRENT_ROUTE__';
7926
9155
  const PENDING_START_PATH_KEY = '__NS_ANGULAR_HMR_PENDING_START_PATH__';
7927
9156
  const CAPTURE_ROUTE_KEY = '__NS_CAPTURE_ANGULAR_HMR_ROUTE__';
9157
+ // Stack of normalized URLs that mirrors Angular Router's back-stack while the
9158
+ // app is running, and is snapshotted into `PENDING_HISTORY_KEY` when an HMR
9159
+ // reboot is about to fire. After the new module bootstraps, the router replay
9160
+ // hook walks the stack to rebuild the back-stack so users keep their back
9161
+ // navigation across HMR cycles.
9162
+ const HISTORY_KEY = '__NS_ANGULAR_HMR_ROUTE_HISTORY__';
9163
+ const PENDING_HISTORY_KEY = '__NS_ANGULAR_HMR_PENDING_HISTORY__';
9164
+ // Window flag set while the new bootstrap is mid-replay of a captured route
9165
+ // stack. User-app code can consult this to skip default navigations that
9166
+ // would otherwise stomp the route the framework is restoring (e.g. a
9167
+ // bottom-nav component that defaults to its first tab on init when no
9168
+ // signal-backed selection exists).
9169
+ const RESTORING_KEY = '__NS_ANGULAR_HMR_RESTORING_ROUTE__';
9170
+ const RESTORING_TARGET_KEY = '__NS_ANGULAR_HMR_RESTORING_ROUTE_TARGET__';
7928
9171
  function getGlobalState() {
7929
9172
  return globalThis;
7930
9173
  }
9174
+ function readHistoryArray(key) {
9175
+ const g = getGlobalState();
9176
+ const raw = g[key];
9177
+ return Array.isArray(raw) ? raw.filter((entry) => typeof entry === 'string') : [];
9178
+ }
9179
+ function writeHistoryArray(key, history) {
9180
+ const g = getGlobalState();
9181
+ if (history.length > 0) {
9182
+ g[key] = history.slice();
9183
+ }
9184
+ else {
9185
+ delete g[key];
9186
+ }
9187
+ }
7931
9188
  function normalizeAngularHmrRouteUrl(value) {
7932
9189
  if (typeof value !== 'string') {
7933
9190
  return null;
@@ -7965,8 +9222,31 @@ function captureAngularHmrPendingStartPath(value, source = 'hmr-reboot') {
7965
9222
  return writeAngularHmrRouteState(value, { pending: true, source });
7966
9223
  }
7967
9224
  function readAngularHmrPendingStartPath() {
9225
+ // When a back-stack snapshot exists we boot to the bottom of the stack and
9226
+ // let `replayAngularHmrPendingForwardNavigations` walk the rest. Otherwise
9227
+ // fall back to the legacy single-URL slot so projects without history
9228
+ // tracking still land on the page they were viewing.
9229
+ const pendingHistory = readHistoryArray(PENDING_HISTORY_KEY);
9230
+ if (pendingHistory.length > 0) {
9231
+ // Open the restoring-route window so user-app default navigations
9232
+ // can step out of the framework's way until replay completes. The
9233
+ // forward-navigation walk in `NativeScriptAngularHmrRouteReplay`
9234
+ // closes the window after the final URL lands or fails. We pass
9235
+ // the deepest captured URL so consumers can compare against the
9236
+ // active router URL if they want fine-grained suppression.
9237
+ beginAngularHmrRouteRestore(pendingHistory[pendingHistory.length - 1]);
9238
+ return pendingHistory[0];
9239
+ }
7968
9240
  const g = getGlobalState();
7969
- return normalizeAngularHmrRouteUrl(g[PENDING_START_PATH_KEY]?.url ?? g[PENDING_START_PATH_KEY]) || '';
9241
+ const fallback = normalizeAngularHmrRouteUrl(g[PENDING_START_PATH_KEY]?.url ?? g[PENDING_START_PATH_KEY]) || '';
9242
+ if (fallback) {
9243
+ // Single-URL fallback path: user-app code should still suppress
9244
+ // default navigations briefly — the new bootstrap is about to
9245
+ // navigate to `fallback`, so a default tab init that fires first
9246
+ // would still stomp it.
9247
+ beginAngularHmrRouteRestore(fallback);
9248
+ }
9249
+ return fallback;
7970
9250
  }
7971
9251
  function invokeAngularHmrRouteCapture() {
7972
9252
  const g = getGlobalState();
@@ -7990,20 +9270,402 @@ function installAngularHmrRouteCaptureHook(capture) {
7990
9270
  }
7991
9271
  };
7992
9272
  }
9273
+ // ---- back-stack history primitives ------------------------------------------
9274
+ /**
9275
+ * Push a URL onto the live back-stack mirror. The mirror is collapsed when the
9276
+ * incoming URL equals the top — Angular fires multiple `NavigationEnd` events
9277
+ * for the same URL during certain `replaceUrl` scenarios and we don't want to
9278
+ * inflate the stack.
9279
+ */
9280
+ function pushAngularHmrRouteHistoryEntry(value) {
9281
+ const url = normalizeAngularHmrRouteUrl(value);
9282
+ if (!url) {
9283
+ return null;
9284
+ }
9285
+ const history = readHistoryArray(HISTORY_KEY);
9286
+ if (history.length > 0 && history[history.length - 1] === url) {
9287
+ return url;
9288
+ }
9289
+ history.push(url);
9290
+ writeHistoryArray(HISTORY_KEY, history);
9291
+ return url;
9292
+ }
9293
+ /**
9294
+ * Pop the top of the live back-stack mirror. Used when Angular reports a
9295
+ * `popstate`-triggered navigation so the mirror tracks back navigations.
9296
+ */
9297
+ function popAngularHmrRouteHistoryEntry() {
9298
+ const history = readHistoryArray(HISTORY_KEY);
9299
+ if (history.length === 0) {
9300
+ return null;
9301
+ }
9302
+ const popped = history.pop() ?? null;
9303
+ writeHistoryArray(HISTORY_KEY, history);
9304
+ return popped;
9305
+ }
9306
+ /**
9307
+ * Replace the top of the live back-stack mirror. Used when Angular reports a
9308
+ * `NavigationEnd` with `replaceUrl=true`, e.g. canonical-redirect cycles.
9309
+ */
9310
+ function replaceAngularHmrRouteHistoryTop(value) {
9311
+ const url = normalizeAngularHmrRouteUrl(value);
9312
+ if (!url) {
9313
+ return null;
9314
+ }
9315
+ const history = readHistoryArray(HISTORY_KEY);
9316
+ if (history.length === 0) {
9317
+ history.push(url);
9318
+ }
9319
+ else {
9320
+ history[history.length - 1] = url;
9321
+ }
9322
+ writeHistoryArray(HISTORY_KEY, history);
9323
+ return url;
9324
+ }
9325
+ /**
9326
+ * Read a defensive copy of the live back-stack mirror.
9327
+ */
9328
+ function readAngularHmrRouteHistory() {
9329
+ return readHistoryArray(HISTORY_KEY);
9330
+ }
9331
+ /**
9332
+ * Reset the live back-stack mirror. Used by tests and on bootstrap when the
9333
+ * router cannot replay the captured stack so we don't carry stale entries
9334
+ * forward.
9335
+ */
9336
+ function clearAngularHmrRouteHistory() {
9337
+ const g = getGlobalState();
9338
+ delete g[HISTORY_KEY];
9339
+ }
9340
+ /**
9341
+ * Snapshot the live back-stack mirror under the pending-history slot so the
9342
+ * next bootstrap can read it. Called from the HMR capture hook.
9343
+ *
9344
+ * The live mirror is cleared after the copy so the freshly bootstrapped app
9345
+ * starts from an empty back-stack. The replay walks the captured snapshot
9346
+ * via `NativeScriptAngularHmrRouteReplay` which fires `NavigationEnd` for
9347
+ * every URL it touches; the new tracker subscribes to those events and
9348
+ * naturally rebuilds the live mirror to match the snapshot. Without this
9349
+ * reset the live mirror would accumulate every URL the replay re-pushes
9350
+ * across HMR cycles, growing without bound and turning subsequent snapshots
9351
+ * into runaway forward-navigation walks (each replayed forward nav from
9352
+ * `/profile` back into `/talk` creates a fresh `TalkComponent` because
9353
+ * forward navigation never reuses the cache, so the leak shows up as
9354
+ * duplicated `Norrix is not enabled` / `BottomNavComponent Router Event:`
9355
+ * lines that double on every save).
9356
+ *
9357
+ * Returns the snapshot for diagnostics. Defensive: an empty live mirror
9358
+ * leaves the pending slot untouched so a single-page snapshot still works.
9359
+ */
9360
+ function snapshotAngularHmrRouteHistory() {
9361
+ const live = readHistoryArray(HISTORY_KEY);
9362
+ if (live.length === 0) {
9363
+ return [];
9364
+ }
9365
+ writeHistoryArray(PENDING_HISTORY_KEY, live);
9366
+ // Clear the live mirror so the next bootstrap starts from a clean slate.
9367
+ // The replay will repopulate it via the new tracker's NavigationEnd
9368
+ // subscription as it walks the captured stack.
9369
+ writeHistoryArray(HISTORY_KEY, []);
9370
+ return live.slice();
9371
+ }
9372
+ /**
9373
+ * Read the snapshotted back-stack pending replay on the new bootstrap.
9374
+ */
9375
+ function readAngularHmrPendingRouteHistory() {
9376
+ return readHistoryArray(PENDING_HISTORY_KEY);
9377
+ }
9378
+ /**
9379
+ * Read URLs to navigate forward through after the initial navigation finishes.
9380
+ * The first entry of the stack is the `START_PATH` consumed by the router; the
9381
+ * rest are forward navigations to push onto the new back-stack.
9382
+ */
9383
+ function readAngularHmrPendingForwardNavigations() {
9384
+ const pending = readHistoryArray(PENDING_HISTORY_KEY);
9385
+ if (pending.length <= 1) {
9386
+ return [];
9387
+ }
9388
+ return pending.slice(1);
9389
+ }
9390
+ /**
9391
+ * Clear the pending snapshot. The router replay calls this once it finishes
9392
+ * walking the stack so subsequent reboots start fresh.
9393
+ */
9394
+ function clearAngularHmrPendingRouteHistory() {
9395
+ const g = getGlobalState();
9396
+ delete g[PENDING_HISTORY_KEY];
9397
+ }
9398
+ // ---- restoring-route window flag --------------------------------------------
9399
+ /**
9400
+ * True while the Angular HMR layer is restoring a captured route stack
9401
+ * onto the freshly-bootstrapped router. The window opens just before
9402
+ * `START_PATH` resolves to a deep URL and closes once the router has
9403
+ * walked the entire forward navigation list (or aborted it).
9404
+ *
9405
+ * User-app code that runs default navigations on component init (e.g. a
9406
+ * bottom-nav defaulting to its first tab) can consult this flag to skip
9407
+ * its default navigation so the framework's restored route survives:
9408
+ *
9409
+ * ```ts
9410
+ * if (isAngularHmrRestoringRoute()) {
9411
+ * return; // framework is restoring a deeper route — leave it alone.
9412
+ * }
9413
+ * defaultTabNavigation();
9414
+ * ```
9415
+ *
9416
+ * Returns `false` outside of HMR or after the replay window has closed.
9417
+ * Production builds always see `false` because the framework never
9418
+ * opens the window there.
9419
+ */
9420
+ function isAngularHmrRestoringRoute() {
9421
+ const g = getGlobalState();
9422
+ return g[RESTORING_KEY] === true;
9423
+ }
9424
+ /**
9425
+ * The target route the framework is currently restoring, or `null` when
9426
+ * no replay is in progress. Useful when the consumer wants to compare
9427
+ * against the current router URL.
9428
+ */
9429
+ function getAngularHmrRestoringRoute() {
9430
+ const g = getGlobalState();
9431
+ const value = g[RESTORING_TARGET_KEY];
9432
+ return typeof value === 'string' && value ? value : null;
9433
+ }
9434
+ /**
9435
+ * Open the restoring-route window. Called by the framework when an HMR
9436
+ * bootstrap is about to navigate to a captured deep route — never call
9437
+ * this from user code.
9438
+ *
9439
+ * `targetUrl` is what the framework intends to land on; the value can
9440
+ * be read back via {@link getAngularHmrRestoringRoute}.
9441
+ */
9442
+ function beginAngularHmrRouteRestore(targetUrl) {
9443
+ const g = getGlobalState();
9444
+ g[RESTORING_KEY] = true;
9445
+ if (targetUrl) {
9446
+ g[RESTORING_TARGET_KEY] = targetUrl;
9447
+ }
9448
+ else {
9449
+ delete g[RESTORING_TARGET_KEY];
9450
+ }
9451
+ }
9452
+ /**
9453
+ * Close the restoring-route window. Called by the framework when the
9454
+ * replay finishes (NavigationEnd reached, replay aborted, or no
9455
+ * pending stack existed in the first place).
9456
+ */
9457
+ function endAngularHmrRouteRestore() {
9458
+ const g = getGlobalState();
9459
+ delete g[RESTORING_KEY];
9460
+ delete g[RESTORING_TARGET_KEY];
9461
+ }
9462
+
9463
+ /**
9464
+ * Grace period to keep `isAngularHmrRestoringRoute()` returning `true`
9465
+ * after `replayForwardNavigations()` finishes its last `navigateByUrl`.
9466
+ *
9467
+ * Why a grace period exists: NativeScript native views (TabView, BottomNavigation,
9468
+ * Frame, etc.) fire their `loaded` events asynchronously after the JS-side
9469
+ * `NavigationEnd`. User-app code wired to those events typically guards a
9470
+ * default navigation (e.g. "select first tab") with `isAngularHmrRestoringRoute()`.
9471
+ * If we close the window the instant the JS replay finishes, the loaded
9472
+ * event arrives a few hundred milliseconds later, the guard reports false,
9473
+ * and the default navigation stomps the freshly-restored route.
9474
+ *
9475
+ * 1000ms covers all the cases observed on iOS device + simulator without
9476
+ * leaving the window open long enough to interfere with genuine user
9477
+ * navigation. The fallback timeout (`fallback-timeout`) below is a safety
9478
+ * net for scenarios where this scheduled close never fires.
9479
+ */
9480
+ const REPLAY_COMPLETED_GRACE_MS = 1000;
9481
+ /**
9482
+ * Replays the back-stack snapshot captured by `NativeScriptAngularHmrRouteTracker`
9483
+ * during HMR. The router's initial navigation already lands on the bottom of
9484
+ * the stack (`stack[0]`); this service walks `stack[1..n]` so the user keeps
9485
+ * back navigation across HMR cycles.
9486
+ *
9487
+ * The replay is single-shot per bootstrap. Any failure (cancelled navigation,
9488
+ * unrouteable URL) aborts the rest of the replay so we don't fight the router
9489
+ * — the user keeps whichever subset of the stack we successfully re-pushed.
9490
+ */
9491
+ class NativeScriptAngularHmrRouteReplay {
9492
+ constructor(router) {
9493
+ this.router = router;
9494
+ if (!isAngularHmrEnabled()) {
9495
+ return;
9496
+ }
9497
+ const forwardNavigations = readAngularHmrPendingForwardNavigations();
9498
+ // The restoring window is opened by `readAngularHmrPendingStartPath()`
9499
+ // when `START_PATH` resolves to a deep route. If that path resolved
9500
+ // to nothing AND we have no forward navigations, there is nothing
9501
+ // to suppress and we must close the window if it was somehow left
9502
+ // open. Otherwise we keep it open until replay finishes.
9503
+ const restoringWindowOpen = isAngularHmrRestoringRoute();
9504
+ if (forwardNavigations.length === 0) {
9505
+ // Nothing to replay; clear the pending slot so a future navigation that
9506
+ // ends in the bootstrap window doesn't carry the snapshot forward.
9507
+ clearAngularHmrPendingRouteHistory();
9508
+ if (restoringWindowOpen) {
9509
+ // Single-URL restore (no back-stack to walk): keep the window
9510
+ // open until the initial navigation completes so user-app
9511
+ // default navigations don't fire before the framework's
9512
+ // restored URL settles. We then schedule the close with the
9513
+ // same grace period as the multi-URL replay path so async
9514
+ // native `loaded` handlers still see the flag.
9515
+ this.subscription = this.router.events
9516
+ .pipe(filter((event) => event instanceof NavigationEnd || event instanceof NavigationCancel || event instanceof NavigationError), take(1))
9517
+ .subscribe(() => this.scheduleRestoringWindowClose('initial-navigation-settled'));
9518
+ // Belt-and-braces: bootstrap can race with router init in
9519
+ // unusual cases. Close the window after a short timeout so we
9520
+ // never leave it stuck open and silently breaking default
9521
+ // navigations forever.
9522
+ this.windowFallbackTimeout = setTimeout(() => this.closeRestoringWindow('fallback-timeout'), 5000);
9523
+ }
9524
+ return;
9525
+ }
9526
+ if (NativeScriptDebug.isLogEnabled()) {
9527
+ NativeScriptDebug.hmrLog(`HMR back-stack replay queued: ${forwardNavigations.length} forward navigation(s)`);
9528
+ }
9529
+ this.subscription = this.router.events
9530
+ .pipe(filter((event) => event instanceof NavigationEnd || event instanceof NavigationCancel || event instanceof NavigationError), take(1))
9531
+ .subscribe((event) => {
9532
+ if (event instanceof NavigationEnd) {
9533
+ void this.replayForwardNavigations(forwardNavigations);
9534
+ }
9535
+ else {
9536
+ // Initial navigation never landed; replay would compound the problem.
9537
+ clearAngularHmrPendingRouteHistory();
9538
+ this.closeRestoringWindow('initial-navigation-failed');
9539
+ if (NativeScriptDebug.isLogEnabled()) {
9540
+ NativeScriptDebug.hmrLog('HMR back-stack replay skipped: initial navigation did not complete');
9541
+ }
9542
+ }
9543
+ });
9544
+ // Same belt-and-braces fallback as the single-URL path above.
9545
+ this.windowFallbackTimeout = setTimeout(() => this.closeRestoringWindow('fallback-timeout'), 10000);
9546
+ }
9547
+ ngOnDestroy() {
9548
+ this.subscription?.unsubscribe();
9549
+ if (this.windowFallbackTimeout !== undefined) {
9550
+ clearTimeout(this.windowFallbackTimeout);
9551
+ this.windowFallbackTimeout = undefined;
9552
+ }
9553
+ if (this.pendingCloseTimeout !== undefined) {
9554
+ clearTimeout(this.pendingCloseTimeout);
9555
+ this.pendingCloseTimeout = undefined;
9556
+ }
9557
+ // Defensive: never leave the restoring window open across module
9558
+ // destruction. A subsequent reboot would otherwise see it set and
9559
+ // suppress the next default navigation indefinitely.
9560
+ this.closeRestoringWindow('replay-service-destroyed');
9561
+ }
9562
+ closeRestoringWindow(reason) {
9563
+ if (this.pendingCloseTimeout !== undefined) {
9564
+ clearTimeout(this.pendingCloseTimeout);
9565
+ this.pendingCloseTimeout = undefined;
9566
+ }
9567
+ if (!isAngularHmrRestoringRoute()) {
9568
+ return;
9569
+ }
9570
+ endAngularHmrRouteRestore();
9571
+ if (this.windowFallbackTimeout !== undefined) {
9572
+ clearTimeout(this.windowFallbackTimeout);
9573
+ this.windowFallbackTimeout = undefined;
9574
+ }
9575
+ if (NativeScriptDebug.isLogEnabled()) {
9576
+ NativeScriptDebug.hmrLog(`HMR restoring-route window closed (${reason})`);
9577
+ }
9578
+ }
9579
+ /**
9580
+ * Schedule the restoring window to close after a small grace period
9581
+ * so that asynchronous user-app handlers (e.g. NativeScript native
9582
+ * `loaded` events on TabView / BottomNavigation / Frame) still observe
9583
+ * `isAngularHmrRestoringRoute() === true` and skip default navigations
9584
+ * that would otherwise stomp the freshly-restored route.
9585
+ *
9586
+ * The grace period is bounded by the existing `fallback-timeout` so
9587
+ * we never leave the flag set indefinitely even if `setTimeout` is
9588
+ * blocked by a misbehaving consumer.
9589
+ */
9590
+ scheduleRestoringWindowClose(reason) {
9591
+ if (!isAngularHmrRestoringRoute()) {
9592
+ return;
9593
+ }
9594
+ if (this.pendingCloseTimeout !== undefined) {
9595
+ clearTimeout(this.pendingCloseTimeout);
9596
+ }
9597
+ this.pendingCloseTimeout = setTimeout(() => {
9598
+ this.pendingCloseTimeout = undefined;
9599
+ this.closeRestoringWindow(reason);
9600
+ }, REPLAY_COMPLETED_GRACE_MS);
9601
+ }
9602
+ async replayForwardNavigations(urls) {
9603
+ let aborted = false;
9604
+ try {
9605
+ for (const url of urls) {
9606
+ const succeeded = await this.router.navigateByUrl(url).catch(() => false);
9607
+ if (!succeeded) {
9608
+ aborted = true;
9609
+ if (NativeScriptDebug.isLogEnabled()) {
9610
+ NativeScriptDebug.hmrLog(`HMR back-stack replay aborted at ${url}`);
9611
+ }
9612
+ return;
9613
+ }
9614
+ if (NativeScriptDebug.isLogEnabled()) {
9615
+ NativeScriptDebug.hmrLog(`HMR back-stack replay navigated to ${url}`);
9616
+ }
9617
+ }
9618
+ }
9619
+ finally {
9620
+ clearAngularHmrPendingRouteHistory();
9621
+ this.scheduleRestoringWindowClose(aborted ? 'replay-aborted' : 'replay-completed');
9622
+ }
9623
+ }
9624
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: NativeScriptAngularHmrRouteReplay, deps: [{ token: i1$3.Router }], target: i0.ɵɵFactoryTarget.Injectable }); }
9625
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: NativeScriptAngularHmrRouteReplay }); }
9626
+ }
9627
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: NativeScriptAngularHmrRouteReplay, decorators: [{
9628
+ type: Injectable
9629
+ }], ctorParameters: () => [{ type: i1$3.Router }] });
7993
9630
 
7994
9631
  class NativeScriptAngularHmrRouteTracker {
7995
9632
  constructor(router) {
7996
9633
  this.router = router;
7997
- if (!this.isHmrEnabled()) {
9634
+ // Tracks whether the current `NavigationStart..NavigationEnd` pair was kicked
9635
+ // off by a popstate (frame.goBack / NSLocationStrategy.back) so that on
9636
+ // `NavigationEnd` we can pop our mirror instead of pushing a duplicate entry.
9637
+ this.currentNavigationIsPopstate = false;
9638
+ this.currentNavigationReplaceUrl = false;
9639
+ if (!isAngularHmrEnabled()) {
7998
9640
  return;
7999
9641
  }
8000
9642
  this.disposeCaptureHook = this.installCaptureHook();
8001
9643
  this.captureCurrentRoute('bootstrap');
8002
9644
  this.subscription = this.router.events.subscribe((event) => {
9645
+ if (event instanceof NavigationStart) {
9646
+ this.currentNavigationIsPopstate = event.navigationTrigger === 'popstate';
9647
+ this.currentNavigationReplaceUrl = !!event.restoredState;
9648
+ return;
9649
+ }
8003
9650
  if (event instanceof NavigationEnd) {
8004
- writeAngularHmrRouteState(event.urlAfterRedirects || event.url, {
9651
+ const url = event.urlAfterRedirects || event.url;
9652
+ writeAngularHmrRouteState(url, {
8005
9653
  source: 'navigation-end',
8006
9654
  });
9655
+ if (this.currentNavigationIsPopstate) {
9656
+ // The user (or NSLocationStrategy.back()) walked the back-stack down
9657
+ // by one page; mirror that by dropping the top of our snapshot so a
9658
+ // subsequent HMR reboot doesn't carry the popped page back into view.
9659
+ popAngularHmrRouteHistoryEntry();
9660
+ }
9661
+ else if (this.currentNavigationReplaceUrl) {
9662
+ replaceAngularHmrRouteHistoryTop(url);
9663
+ }
9664
+ else {
9665
+ pushAngularHmrRouteHistoryEntry(url);
9666
+ }
9667
+ this.currentNavigationIsPopstate = false;
9668
+ this.currentNavigationReplaceUrl = false;
8007
9669
  }
8008
9670
  });
8009
9671
  }
@@ -8012,6 +9674,32 @@ class NativeScriptAngularHmrRouteTracker {
8012
9674
  this.disposeCaptureHook?.();
8013
9675
  }
8014
9676
  captureCurrentRoute(source) {
9677
+ if (source === 'hmr-reboot') {
9678
+ // Snapshot the live mirror first so the bootstrap can replay forward
9679
+ // navigations to rebuild the back-stack. The pending single-URL slot
9680
+ // remains useful as a fallback when the snapshot turns out to be empty
9681
+ // (e.g. bootstrap-time HMR before the first NavigationEnd).
9682
+ snapshotAngularHmrRouteHistory();
9683
+ }
9684
+ else if (source === 'bootstrap') {
9685
+ // Seed the live mirror with the current URL so the very first HMR
9686
+ // before any user navigation still has a stack of size one to snapshot.
9687
+ //
9688
+ // Skip empty / root URLs: at ENVIRONMENT_INITIALIZER time the router
9689
+ // has not run its initial navigation yet so `router.url` is "/" (or
9690
+ // an empty string). Pushing that here would seed the mirror with a
9691
+ // noise entry that becomes the bottom of the next snapshot, which in
9692
+ // turn becomes the next bootstrap's `START_PATH`. The router then
9693
+ // boots to "/" → redirects to the real default route → fires an
9694
+ // extra `NavigationEnd` that re-enters the replay path. The first
9695
+ // genuine `NavigationEnd` arrives a moment later through the event
9696
+ // subscription below and seeds the mirror with the real URL, so
9697
+ // dropping the seed here is safe.
9698
+ const seedUrl = this.router.url;
9699
+ if (seedUrl && seedUrl !== '/') {
9700
+ pushAngularHmrRouteHistoryEntry(seedUrl);
9701
+ }
9702
+ }
8015
9703
  return writeAngularHmrRouteState(this.router.url, {
8016
9704
  pending: source === 'hmr-reboot',
8017
9705
  source,
@@ -8020,10 +9708,6 @@ class NativeScriptAngularHmrRouteTracker {
8020
9708
  installCaptureHook() {
8021
9709
  return installAngularHmrRouteCaptureHook(() => this.captureCurrentRoute('hmr-reboot'));
8022
9710
  }
8023
- isHmrEnabled() {
8024
- const g = globalThis;
8025
- return !!g.__NS_DEV_PLACEHOLDER_ROOT_EARLY__ || typeof g.__reboot_ng_modules__ === 'function';
8026
- }
8027
9711
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: NativeScriptAngularHmrRouteTracker, deps: [{ token: i1$3.Router }], target: i0.ɵɵFactoryTarget.Injectable }); }
8028
9712
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: NativeScriptAngularHmrRouteTracker }); }
8029
9713
  }
@@ -8057,10 +9741,11 @@ class NativeScriptRouterModule {
8057
9741
  NSRouteReuseStrategy,
8058
9742
  { provide: RouteReuseStrategy, useExisting: NSRouteReuseStrategy },
8059
9743
  NativeScriptAngularHmrRouteTracker,
9744
+ NativeScriptAngularHmrRouteReplay,
8060
9745
  {
8061
9746
  provide: APP_BOOTSTRAP_LISTENER,
8062
9747
  multi: true,
8063
- deps: [NativeScriptAngularHmrRouteTracker],
9748
+ deps: [NativeScriptAngularHmrRouteTracker, NativeScriptAngularHmrRouteReplay],
8064
9749
  useFactory: () => () => undefined,
8065
9750
  },
8066
9751
  ],
@@ -8103,11 +9788,13 @@ function provideNativeScriptRouter(routes, ...features) {
8103
9788
  NSRouteReuseStrategy,
8104
9789
  { provide: RouteReuseStrategy, useExisting: NSRouteReuseStrategy },
8105
9790
  NativeScriptAngularHmrRouteTracker,
9791
+ NativeScriptAngularHmrRouteReplay,
8106
9792
  {
8107
9793
  provide: ENVIRONMENT_INITIALIZER,
8108
9794
  multi: true,
8109
9795
  useValue: () => {
8110
9796
  inject(NativeScriptAngularHmrRouteTracker);
9797
+ inject(NativeScriptAngularHmrRouteReplay);
8111
9798
  },
8112
9799
  },
8113
9800
  // {provide: APP_BOOTSTRAP_LISTENER, multi: true, useFactory: getBootstrapListener},
@@ -8501,5 +10188,5 @@ function provideNativeScriptNgZone(options) {
8501
10188
  * Generated bundle index. Do not edit.
8502
10189
  */
8503
10190
 
8504
- export { APP_ROOT_VIEW, ActionBarComponent, ActionBarScope, ActionItemDirective, AndroidFilterComponent, AppHostAsyncView, AppHostView, AppleFilterComponent, BasePortalOutlet, BaseValueAccessor, COMMON_PROVIDERS, CdkPortal, CdkPortalOutlet, CheckedValueAccessor, CommentNode, ComponentPortal, DEVICE, DISABLE_ROOT_VIEW_HANDLING, DateValueAccessor, DetachedLoader, DomPortal, ENABLE_REUSABE_VIEWS, EmulatedRenderer, FrameDirective, FramePageComponent, FramePageModule, FrameService, IOSFilterComponent, InjectableAnimationEngine, InvisibleNode, ItemContext, ListViewComponent, ModalDialogParams, ModalDialogService, NAMESPACE_FILTERS, NATIVESCRIPT_MODULE_PROVIDERS, NATIVESCRIPT_MODULE_STATIC_PROVIDERS, NATIVESCRIPT_ROOT_MODULE_ID, NATIVE_DIALOG_DATA, NATIVE_DIALOG_DEFAULT_OPTIONS, NSEmptyOutletComponent, NSFileSystem, NSLocationStrategy, NSRouteReuseStrategy, NSRouterLink, NSRouterLinkActive, NativeDialog, NativeDialogCloseDirective, NativeDialogConfig, NativeDialogModule, NativeDialogRef, NativeDialog as NativeDialogService, NativeModalRef, NativeScriptAnimationDriver, NativeScriptAnimationPlayer, NativeScriptAnimationsModule, NativeScriptCommonModule, NativeScriptDocument, NativeScriptDomPortalOutlet, NativeScriptFormsModule, NativeScriptHttpClientModule, NativeScriptLoadingService, NativeScriptModule, NativeScriptNgSafeEvent, NativeScriptNgZone, NativeScriptRendererFactory, NativeScriptRendererHelperService, NativeScriptRouterModule, NativeScriptSanitizer, NativescriptXhrFactory, NavigationButtonDirective, NgViewRef, NsHttpBackEnd, NsTemplatedItem, NumberValueAccessor, Outlet, PAGE_FACTORY, PREVENT_CHANGE_EVENTS_DURING_CD, PREVENT_SPECIFIC_EVENTS_DURING_CD, PageDirective, PageRoute, PageRouterOutlet, PageService, PlatformNamespaceFilter, Portal, PortalModule, RootCompositeModule, RootViewProxy, RouterExtensions, START_PATH, SelectedIndexValueAccessor, TEMPLATED_ITEMS_COMPONENT, TabViewDirective, TabViewItemDirective, TemplateKeyDirective, TemplatePortal, TextNode, TextValueAccessor, TimeValueAccessor, VisionOSFilterComponent, bootstrapApplication, createKeyframeAnimation, customFrameComponentFactory, customFrameDirectiveFactory, customPageFactory, customPageFactoryFromFrame, dashCaseToCamelCase, defaultNavOptions, defaultPageFactory, defaultPageFactoryProvider, detachViewFromParent, disableRootViewHanding, errorHandler, extractSingleViewRecursive, frameMeta, generateDetachedLoader, generateFallbackRootView, generateNativeScriptView, generateRandomId, generateRootLayoutAndProxy, getFirstNativeLikeView, getItemViewRoot, getSingleViewRecursive, getViewClass, getViewMeta, instantiateDefaultStyleNormalizer, instantiateSupportedAnimationDriver, isBlank, isContentView, isDetachedElement, isInvisibleNode, isJsObject, isKnownView, isLayout, isListLikeIterable, isPresent, isView, onAfterLivesync, onBeforeLivesync, once, platformNativeScript, platformNativeScriptDynamic, postAngularBootstrap$, preAngularDisposal$, provideLocationStrategy, provideNativeScriptHttpClient, provideNativeScriptNgZone, provideNativeScriptRouter, registerElement, registerNativeScriptViewComponents, rootRoute, runNativeScriptAngularApp, throwIfAlreadyLoaded, throwNoPortalAttachedError, throwNullPortalError, throwNullPortalOutletError, throwPortalAlreadyAttachedError, throwPortalOutletAlreadyDisposedError, throwUnknownPortalTypeError, COMPONENT_VARIABLE as ɵCOMPONENT_VARIABLE, CONTENT_ATTR as ɵCONTENT_ATTR, HOST_ATTR as ɵHOST_ATTR, NativeScriptDebug as ɵNativeScriptAngularDebug, viewUtil as ɵViewUtil, actionBarMeta as ɵactionBarMeta, elementMap as ɵelementMap, isActionItem as ɵisActionItem, isNavigationButton as ɵisNavigationButton };
10191
+ export { APP_ROOT_VIEW, ActionBarComponent, ActionBarScope, ActionItemDirective, AndroidFilterComponent, AppHostAsyncView, AppHostView, AppleFilterComponent, BasePortalOutlet, BaseValueAccessor, COMMON_PROVIDERS, CdkPortal, CdkPortalOutlet, CheckedValueAccessor, CommentNode, ComponentPortal, DEVICE, DISABLE_ROOT_VIEW_HANDLING, DateValueAccessor, DetachedLoader, DomPortal, ENABLE_REUSABE_VIEWS, EmulatedRenderer, FrameDirective, FramePageComponent, FramePageModule, FrameService, IOSFilterComponent, InjectableAnimationEngine, InvisibleNode, ItemContext, ListViewComponent, ModalDialogParams, ModalDialogService, NAMESPACE_FILTERS, NATIVESCRIPT_MODULE_PROVIDERS, NATIVESCRIPT_MODULE_STATIC_PROVIDERS, NATIVESCRIPT_ROOT_MODULE_ID, NATIVE_DIALOG_DATA, NATIVE_DIALOG_DEFAULT_OPTIONS, NSEmptyOutletComponent, NSFileSystem, NSLocationStrategy, NSRouteReuseStrategy, NSRouterLink, NSRouterLinkActive, NativeDialog, NativeDialogCloseDirective, NativeDialogConfig, NativeDialogModule, NativeDialogRef, NativeDialog as NativeDialogService, NativeModalRef, NativeScriptAnimationDriver, NativeScriptAnimationPlayer, NativeScriptAnimationsModule, NativeScriptCommonModule, NativeScriptDocument, NativeScriptDomPortalOutlet, NativeScriptFormsModule, NativeScriptHttpClientModule, NativeScriptLoadingService, NativeScriptModule, NativeScriptNgSafeEvent, NativeScriptNgZone, NativeScriptRendererFactory, NativeScriptRendererHelperService, NativeScriptRouterModule, NativeScriptSanitizer, NativescriptXhrFactory, NavigationButtonDirective, NgViewRef, NsHttpBackEnd, NsTemplatedItem, NumberValueAccessor, Outlet, PAGE_FACTORY, PREVENT_CHANGE_EVENTS_DURING_CD, PREVENT_SPECIFIC_EVENTS_DURING_CD, PageDirective, PageRoute, PageRouterOutlet, PageService, PlatformNamespaceFilter, Portal, PortalModule, RootCompositeModule, RootViewProxy, RouterExtensions, START_PATH, SelectedIndexValueAccessor, TEMPLATED_ITEMS_COMPONENT, TabViewDirective, TabViewItemDirective, TemplateKeyDirective, TemplatePortal, TextNode, TextValueAccessor, TimeValueAccessor, VisionOSFilterComponent, bootstrapApplication, createKeyframeAnimation, customFrameComponentFactory, customFrameDirectiveFactory, customPageFactory, customPageFactoryFromFrame, dashCaseToCamelCase, defaultNavOptions, defaultPageFactory, defaultPageFactoryProvider, detachViewFromParent, disableRootViewHanding, errorHandler, extractSingleViewRecursive, frameMeta, generateDetachedLoader, generateFallbackRootView, generateNativeScriptView, generateRandomId, generateRootLayoutAndProxy, getAngularHmrRestoringRoute, getFirstNativeLikeView, getItemViewRoot, getSingleViewRecursive, getViewClass, getViewMeta, instantiateDefaultStyleNormalizer, instantiateSupportedAnimationDriver, isAngularHmrRestoringRoute, isBlank, isContentView, isDetachedElement, isInvisibleNode, isJsObject, isKnownView, isLayout, isListLikeIterable, isPresent, isView, onAfterLivesync, onBeforeLivesync, once, platformNativeScript, platformNativeScriptDynamic, postAngularBootstrap$, preAngularDisposal$, provideLocationStrategy, provideNativeScriptHttpClient, provideNativeScriptNgZone, provideNativeScriptRouter, registerElement, registerNativeScriptViewComponents, rootRoute, runNativeScriptAngularApp, throwIfAlreadyLoaded, throwNoPortalAttachedError, throwNullPortalError, throwNullPortalOutletError, throwPortalAlreadyAttachedError, throwPortalOutletAlreadyDisposedError, throwUnknownPortalTypeError, COMPONENT_VARIABLE as ɵCOMPONENT_VARIABLE, CONTENT_ATTR as ɵCONTENT_ATTR, HOST_ATTR as ɵHOST_ATTR, NativeScriptDebug as ɵNativeScriptAngularDebug, viewUtil as ɵViewUtil, actionBarMeta as ɵactionBarMeta, elementMap as ɵelementMap, isActionItem as ɵisActionItem, isNavigationButton as ɵisNavigationButton };
8505
10192
  //# sourceMappingURL=nativescript-angular.mjs.map