@nativescript/vite 8.0.0-alpha.5 → 8.0.0-alpha.6

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.
Files changed (57) hide show
  1. package/helpers/global-defines.d.ts +55 -0
  2. package/helpers/global-defines.js +81 -0
  3. package/helpers/global-defines.js.map +1 -1
  4. package/helpers/logging.d.ts +1 -0
  5. package/helpers/logging.js +36 -3
  6. package/helpers/logging.js.map +1 -1
  7. package/hmr/client/hmr-pending-overlay.d.ts +27 -0
  8. package/hmr/client/hmr-pending-overlay.js +50 -0
  9. package/hmr/client/hmr-pending-overlay.js.map +1 -0
  10. package/hmr/client/index.js +72 -1
  11. package/hmr/client/index.js.map +1 -1
  12. package/hmr/client/utils.d.ts +5 -0
  13. package/hmr/client/utils.js +153 -15
  14. package/hmr/client/utils.js.map +1 -1
  15. package/hmr/entry-runtime.js +95 -31
  16. package/hmr/entry-runtime.js.map +1 -1
  17. package/hmr/frameworks/angular/client/index.d.ts +1 -0
  18. package/hmr/frameworks/angular/client/index.js +424 -11
  19. package/hmr/frameworks/angular/client/index.js.map +1 -1
  20. package/hmr/server/perf-instrumentation.d.ts +118 -0
  21. package/hmr/server/perf-instrumentation.js +198 -0
  22. package/hmr/server/perf-instrumentation.js.map +1 -0
  23. package/hmr/server/shared-transform-request.js +12 -5
  24. package/hmr/server/shared-transform-request.js.map +1 -1
  25. package/hmr/server/websocket-angular-hot-update.d.ts +16 -0
  26. package/hmr/server/websocket-angular-hot-update.js +163 -1
  27. package/hmr/server/websocket-angular-hot-update.js.map +1 -1
  28. package/hmr/server/websocket-graph-upsert.d.ts +15 -0
  29. package/hmr/server/websocket-graph-upsert.js +20 -0
  30. package/hmr/server/websocket-graph-upsert.js.map +1 -1
  31. package/hmr/server/websocket-hmr-pending.d.ts +43 -0
  32. package/hmr/server/websocket-hmr-pending.js +55 -0
  33. package/hmr/server/websocket-hmr-pending.js.map +1 -0
  34. package/hmr/server/websocket-ns-m-finalize.js +1 -1
  35. package/hmr/server/websocket-ns-m-finalize.js.map +1 -1
  36. package/hmr/server/websocket-ns-m-paths.d.ts +1 -1
  37. package/hmr/server/websocket-ns-m-paths.js +59 -13
  38. package/hmr/server/websocket-ns-m-paths.js.map +1 -1
  39. package/hmr/server/websocket-ns-m-request.js +1 -16
  40. package/hmr/server/websocket-ns-m-request.js.map +1 -1
  41. package/hmr/server/websocket-runtime-compat.js.map +1 -1
  42. package/hmr/server/websocket-served-module-helpers.js +42 -18
  43. package/hmr/server/websocket-served-module-helpers.js.map +1 -1
  44. package/hmr/server/websocket-vue-sfc.js +3 -6
  45. package/hmr/server/websocket-vue-sfc.js.map +1 -1
  46. package/hmr/server/websocket.d.ts +4 -4
  47. package/hmr/server/websocket.js +614 -177
  48. package/hmr/server/websocket.js.map +1 -1
  49. package/hmr/shared/runtime/boot-timeline.d.ts +17 -0
  50. package/hmr/shared/runtime/boot-timeline.js +54 -0
  51. package/hmr/shared/runtime/boot-timeline.js.map +1 -0
  52. package/hmr/shared/runtime/dev-overlay.d.ts +49 -2
  53. package/hmr/shared/runtime/dev-overlay.js +587 -12
  54. package/hmr/shared/runtime/dev-overlay.js.map +1 -1
  55. package/hmr/shared/runtime/session-bootstrap.js +49 -0
  56. package/hmr/shared/runtime/session-bootstrap.js.map +1 -1
  57. package/package.json +1 -1
@@ -21,10 +21,25 @@ function getRuntimeState() {
21
21
  snapshot: { ...DEFAULT_SNAPSHOT },
22
22
  bootRefs: null,
23
23
  liveRefs: null,
24
+ iosRefs: null,
25
+ iosBuildFailed: false,
24
26
  verbose: false,
27
+ updateAutoHideTimer: null,
28
+ updateCycleStartedAt: 0,
25
29
  };
26
30
  }
27
- return g.__NS_HMR_DEV_OVERLAY_STATE__;
31
+ const state = g.__NS_HMR_DEV_OVERLAY_STATE__;
32
+ // Backfill newer fields for legacy state objects (e.g. after hot reload)
33
+ // so we never observe an undefined iosRefs/iosBuildFailed at runtime.
34
+ if (typeof state.iosRefs === 'undefined')
35
+ state.iosRefs = null;
36
+ if (typeof state.iosBuildFailed === 'undefined')
37
+ state.iosBuildFailed = false;
38
+ if (typeof state.updateAutoHideTimer === 'undefined')
39
+ state.updateAutoHideTimer = null;
40
+ if (typeof state.updateCycleStartedAt !== 'number')
41
+ state.updateCycleStartedAt = 0;
42
+ return state;
28
43
  }
29
44
  function describeAttempt(info) {
30
45
  const attempt = Number(info?.attempt || 0);
@@ -241,7 +256,7 @@ export function createConnectionOverlaySnapshot(stage, info) {
241
256
  mode: 'connection',
242
257
  badge: 'OFFLINE',
243
258
  title: 'Vite dev server offline',
244
- phase: 'Idle...',
259
+ phase: 'Please check your terminal.',
245
260
  detail: 'The websocket and HTTP HMR path are both unavailable right now.',
246
261
  progress: null,
247
262
  busy: true,
@@ -256,6 +271,89 @@ export function createConnectionOverlaySnapshot(stage, info) {
256
271
  progress: typeof info?.progress === 'number' || info?.progress === null ? info.progress : base.progress,
257
272
  };
258
273
  }
274
+ // Round-eleven.3 (alpha.62) — Snapshot factory for the HMR-applying
275
+ // overlay. Each stage owns a fixed phase string, badge, and progress
276
+ // %. We pick the percentages so users see continuous forward motion:
277
+ // the cheap stages (mutex acquire, eviction call) advance fast; the
278
+ // long tail (entry re-import + Angular reboot) sits at 60→90 so the
279
+ // bar keeps moving even when the V8 ESM walk dominates wall time.
280
+ //
281
+ // The 'complete' stage holds for a brief moment (the API auto-hides
282
+ // it via setUpdateStage) so the user gets visual closure ("the update
283
+ // landed") without staring at a frozen overlay; tone stays 'success'
284
+ // throughout so the colour scheme never flickers between phases.
285
+ const HMR_UPDATE_TITLE = 'HMR update applying...';
286
+ const HMR_UPDATE_DONE_TITLE = 'HMR update applied';
287
+ export function createUpdateOverlaySnapshot(stage, info) {
288
+ const phaseInfo = {
289
+ received: {
290
+ visible: true,
291
+ mode: 'update',
292
+ badge: 'HMR',
293
+ title: HMR_UPDATE_TITLE,
294
+ phase: 'Preparing update',
295
+ detail: '',
296
+ progress: 5,
297
+ busy: true,
298
+ blocking: false,
299
+ tone: 'success',
300
+ },
301
+ evicting: {
302
+ visible: true,
303
+ mode: 'update',
304
+ badge: 'HMR',
305
+ title: HMR_UPDATE_TITLE,
306
+ phase: 'Invalidating module cache',
307
+ detail: '',
308
+ progress: 30,
309
+ busy: true,
310
+ blocking: false,
311
+ tone: 'success',
312
+ },
313
+ reimporting: {
314
+ visible: true,
315
+ mode: 'update',
316
+ badge: 'HMR',
317
+ title: HMR_UPDATE_TITLE,
318
+ phase: 'Re-importing entry',
319
+ detail: '',
320
+ progress: 60,
321
+ busy: true,
322
+ blocking: false,
323
+ tone: 'success',
324
+ },
325
+ rebooting: {
326
+ visible: true,
327
+ mode: 'update',
328
+ badge: 'HMR',
329
+ title: HMR_UPDATE_TITLE,
330
+ phase: 'Rebooting Angular',
331
+ detail: '',
332
+ progress: 90,
333
+ busy: true,
334
+ blocking: false,
335
+ tone: 'success',
336
+ },
337
+ complete: {
338
+ visible: true,
339
+ mode: 'update',
340
+ badge: 'HMR',
341
+ title: HMR_UPDATE_DONE_TITLE,
342
+ phase: 'Update applied',
343
+ detail: '',
344
+ progress: 100,
345
+ busy: false,
346
+ blocking: false,
347
+ tone: 'success',
348
+ },
349
+ };
350
+ const base = phaseInfo[stage];
351
+ return {
352
+ ...base,
353
+ detail: info?.detail || base.detail,
354
+ progress: typeof info?.progress === 'number' || info?.progress === null ? info.progress : base.progress,
355
+ };
356
+ }
259
357
  function resolveCoreExport(name) {
260
358
  const g = getOverlayGlobal();
261
359
  try {
@@ -584,17 +682,35 @@ function applySnapshotToLiveRefs(refs, snapshot) {
584
682
  if (!refs) {
585
683
  return;
586
684
  }
587
- const visible = snapshot.visible && snapshot.mode === 'connection';
685
+ // Round-eleven.3 'update' mode shares the live (in-tree) overlay
686
+ // chrome with 'connection'. Both render a centered panel inside the
687
+ // page; only the colours and text change with the snapshot's tone.
688
+ const visible = snapshot.visible && (snapshot.mode === 'connection' || snapshot.mode === 'update');
588
689
  refs.overlay.visibility = visible ? 'visible' : 'collapse';
589
- refs.overlay.backgroundColor = asColor(snapshot.tone === 'error' ? '#b4181068' : '#a1771683');
690
+ // Backdrop tints by tone:
691
+ // error → red wash (matches existing UX)
692
+ // success → richer green wash; the previous 18% alpha was so
693
+ // subtle on light app backgrounds that users couldn't
694
+ // tell the overlay had even fired. 50% alpha keeps the
695
+ // underlying app legible while making the apply event
696
+ // unmistakable during the (often <300ms) cycle.
697
+ // default → warm orange (existing connection-overlay look)
698
+ const overlayBg = snapshot.tone === 'error' ? '#b4181068' : snapshot.tone === 'success' ? '#1f883d80' : '#a1771683';
699
+ refs.overlay.backgroundColor = asColor(overlayBg);
590
700
  refs.titleLabel.text = snapshot.title;
591
- refs.titleLabel.color = asColor(snapshot.tone === 'error' ? '#b41810e6' : '#563e3fb1');
701
+ const textColor = snapshot.tone === 'error' ? '#b41810e6' : snapshot.tone === 'success' ? '#0e6e2fff' : '#563e3fb1';
702
+ refs.titleLabel.color = asColor(textColor);
592
703
  refs.statusLabel.text = formatStatusText(snapshot);
593
- refs.statusLabel.color = asColor(snapshot.tone === 'error' ? '#b41810e6' : '#563e3fb1');
704
+ refs.statusLabel.color = asColor(textColor);
594
705
  try {
595
706
  const panel = refs.titleLabel.parent;
596
707
  if (panel) {
597
- panel.backgroundColor = asColor('#FFFFFFFF');
708
+ // Slightly richer green-tinted panel for HMR-apply so the
709
+ // title/status text reads at a glance against the brighter
710
+ // backdrop wash. White panel for connection/error keeps
711
+ // existing UX intact.
712
+ const panelBg = snapshot.tone === 'success' ? '#E6F8E9FF' : '#FFFFFFFF';
713
+ panel.backgroundColor = asColor(panelBg);
598
714
  panel.opacity = 1;
599
715
  panel.padding = 16;
600
716
  try {
@@ -605,6 +721,310 @@ function applySnapshotToLiveRefs(refs, snapshot) {
605
721
  }
606
722
  catch { }
607
723
  }
724
+ // pure helpers for iOS window promotion. Factored out so the layout
725
+ // math and window-level selection stay unit-testable without booting a
726
+ // simulator. See `dev-overlay.spec.ts`.
727
+ /**
728
+ * Returns the UIWindow level we use for the live/connection overlay. We lift
729
+ * above `UIWindowLevelAlert` so system alerts (and any app-presented modal)
730
+ * stack underneath. When the platform does not expose `UIWindowLevelAlert`
731
+ * we fall back to the documented constant value (2000).
732
+ */
733
+ export function computeIosOverlayWindowLevel(baseAlert) {
734
+ if (typeof baseAlert === 'number' && Number.isFinite(baseAlert)) {
735
+ return baseAlert + 1;
736
+ }
737
+ return 2000 + 1;
738
+ }
739
+ /**
740
+ * Layout math for the live overlay when it runs inside its own UIWindow.
741
+ * Pure, deterministic and independent of UIKit so we can verify the rules
742
+ * (max panel width, centered placement, safe-area clamping, sane defaults)
743
+ * from tests.
744
+ */
745
+ export function computeIosOverlayLayout(input) {
746
+ const viewWidth = Math.max(0, Number(input.viewWidth) || 0);
747
+ const viewHeight = Math.max(0, Number(input.viewHeight) || 0);
748
+ const safeInsets = {
749
+ top: Math.max(0, Number(input.safeInsets?.top ?? 0) || 0),
750
+ bottom: Math.max(0, Number(input.safeInsets?.bottom ?? 0) || 0),
751
+ left: Math.max(0, Number(input.safeInsets?.left ?? 0) || 0),
752
+ right: Math.max(0, Number(input.safeInsets?.right ?? 0) || 0),
753
+ };
754
+ const titleHeight = Math.max(0, Number(input.titleHeight) || 0);
755
+ const statusHeight = Math.max(0, Number(input.statusHeight) || 0);
756
+ const horizontalMargin = Math.max(0, Number(input.horizontalMargin ?? 24));
757
+ const maxPanelWidth = Math.max(0, Number(input.maxPanelWidth ?? 340));
758
+ const panelPadding = Math.max(0, Number(input.panelPadding ?? 16));
759
+ const interLabelSpacing = Math.max(0, Number(input.interLabelSpacing ?? 10));
760
+ const minTopInset = Math.max(0, Number(input.minTopInset ?? 20));
761
+ const available = Math.max(0, viewWidth - 2 * horizontalMargin - safeInsets.left - safeInsets.right);
762
+ const panelWidth = Math.min(maxPanelWidth, available);
763
+ const innerWidth = Math.max(0, panelWidth - 2 * panelPadding);
764
+ const spacing = titleHeight > 0 && statusHeight > 0 ? interLabelSpacing : 0;
765
+ const panelHeight = panelPadding * 2 + titleHeight + spacing + statusHeight;
766
+ const panelX = Math.max(0, (viewWidth - panelWidth) / 2);
767
+ // Center vertically, but never cross the top safe-area inset (notch/Dynamic Island).
768
+ const centered = (viewHeight - panelHeight) / 2;
769
+ const panelY = Math.max(safeInsets.top + minTopInset, centered);
770
+ return {
771
+ backdrop: { x: 0, y: 0, width: viewWidth, height: viewHeight },
772
+ panel: { x: panelX, y: panelY, width: panelWidth, height: panelHeight },
773
+ title: { x: panelPadding, y: panelPadding, width: innerWidth, height: titleHeight },
774
+ status: {
775
+ x: panelPadding,
776
+ y: panelPadding + titleHeight + spacing,
777
+ width: innerWidth,
778
+ height: statusHeight,
779
+ },
780
+ };
781
+ }
782
+ /**
783
+ * Returns the iOS UIKit symbols we rely on if we're running on an iOS runtime
784
+ * with the metadata bridge available. Returns null on Android, web, or in
785
+ * tests so callers can gracefully fall back to the in-tree overlay.
786
+ */
787
+ function getIosOverlayHost() {
788
+ const g = getOverlayGlobal();
789
+ if (typeof g.UIWindow === 'undefined' || typeof g.UIApplication === 'undefined' || typeof g.UIViewController === 'undefined' || typeof g.UIView === 'undefined' || typeof g.UILabel === 'undefined' || typeof g.UIColor === 'undefined' || typeof g.UIFont === 'undefined' || typeof g.UIScreen === 'undefined') {
790
+ return null;
791
+ }
792
+ return {
793
+ UIWindow: g.UIWindow,
794
+ UIViewController: g.UIViewController,
795
+ UIView: g.UIView,
796
+ UILabel: g.UILabel,
797
+ UIColor: g.UIColor,
798
+ UIFont: g.UIFont,
799
+ UIApplication: g.UIApplication,
800
+ UIScreen: g.UIScreen,
801
+ UIWindowLevelAlert: typeof g.UIWindowLevelAlert === 'number' ? g.UIWindowLevelAlert : undefined,
802
+ };
803
+ }
804
+ /**
805
+ * Walks UIApplication.sharedApplication windows and returns the first active
806
+ * UIWindowScene we can locate. On iOS 13+ every UIWindow is attached to a
807
+ * scene, and we must initialise our overlay window the same way or the OS
808
+ * will silently refuse to render it. Returns null when no scene is found
809
+ * (older iOS versions or non-UI environments).
810
+ */
811
+ function findActiveWindowScene(host) {
812
+ try {
813
+ const app = host.UIApplication.sharedApplication;
814
+ const windows = app?.windows;
815
+ if (!windows || typeof windows.count !== 'number')
816
+ return null;
817
+ for (let i = 0; i < windows.count; i++) {
818
+ const w = windows.objectAtIndex(i);
819
+ const scene = w && w.windowScene;
820
+ if (scene)
821
+ return scene;
822
+ }
823
+ }
824
+ catch { }
825
+ return null;
826
+ }
827
+ function buildIosOverlayRefs(state) {
828
+ const host = getIosOverlayHost();
829
+ if (!host)
830
+ return null;
831
+ // Without a scene we can't build a modern UIWindow that actually renders.
832
+ // Fall back to the in-tree overlay rather than show nothing.
833
+ const scene = findActiveWindowScene(host);
834
+ if (!scene) {
835
+ if (state.verbose) {
836
+ try {
837
+ console.info('[ns-hmr-overlay] no active UIWindowScene; skipping iOS overlay promotion');
838
+ }
839
+ catch { }
840
+ }
841
+ return null;
842
+ }
843
+ try {
844
+ const { UIWindow, UIViewController, UIView, UILabel, UIColor, UIFont } = host;
845
+ const window = UIWindow.alloc().initWithWindowScene(scene);
846
+ window.windowLevel = computeIosOverlayWindowLevel(host.UIWindowLevelAlert ?? null);
847
+ window.backgroundColor = UIColor.clearColor;
848
+ window.hidden = true;
849
+ const controller = UIViewController.new();
850
+ controller.view.backgroundColor = UIColor.clearColor;
851
+ window.rootViewController = controller;
852
+ // UIViewAutoresizing bit masks. We mirror the UIKit constants here to
853
+ // avoid depending on symbols the metadata bridge does not always
854
+ // expose as top-level globals.
855
+ const FLEXIBLE_LEFT_MARGIN = 1 << 0;
856
+ const FLEXIBLE_WIDTH = 1 << 1;
857
+ const FLEXIBLE_RIGHT_MARGIN = 1 << 2;
858
+ const FLEXIBLE_TOP_MARGIN = 1 << 3;
859
+ const FLEXIBLE_HEIGHT = 1 << 4;
860
+ const FLEXIBLE_BOTTOM_MARGIN = 1 << 5;
861
+ const backdrop = UIView.new();
862
+ backdrop.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0, 0, 0, 0.35);
863
+ backdrop.autoresizingMask = FLEXIBLE_WIDTH | FLEXIBLE_HEIGHT;
864
+ controller.view.addSubview(backdrop);
865
+ const panel = UIView.new();
866
+ panel.backgroundColor = UIColor.whiteColor;
867
+ panel.autoresizingMask = FLEXIBLE_LEFT_MARGIN | FLEXIBLE_RIGHT_MARGIN | FLEXIBLE_TOP_MARGIN | FLEXIBLE_BOTTOM_MARGIN;
868
+ try {
869
+ panel.layer.cornerRadius = 14;
870
+ panel.layer.masksToBounds = true;
871
+ }
872
+ catch { }
873
+ controller.view.addSubview(panel);
874
+ const titleLabel = UILabel.new();
875
+ titleLabel.numberOfLines = 0;
876
+ titleLabel.textAlignment = 1; // NSTextAlignmentCenter
877
+ titleLabel.font = UIFont.boldSystemFontOfSize(16);
878
+ titleLabel.textColor = UIColor.blackColor;
879
+ panel.addSubview(titleLabel);
880
+ const statusLabel = UILabel.new();
881
+ statusLabel.numberOfLines = 0;
882
+ statusLabel.textAlignment = 1;
883
+ statusLabel.font = UIFont.systemFontOfSize(13);
884
+ statusLabel.textColor = UIColor.darkGrayColor;
885
+ panel.addSubview(statusLabel);
886
+ return { window, controller, backdrop, panel, titleLabel, statusLabel };
887
+ }
888
+ catch (err) {
889
+ try {
890
+ console.warn('[ns-hmr-overlay] iOS overlay construction failed:', err?.message || err);
891
+ }
892
+ catch { }
893
+ return null;
894
+ }
895
+ }
896
+ function ensureIosOverlayRefs(state) {
897
+ if (state.iosRefs)
898
+ return state.iosRefs;
899
+ if (state.iosBuildFailed)
900
+ return null;
901
+ const built = buildIosOverlayRefs(state);
902
+ if (built) {
903
+ state.iosRefs = built;
904
+ }
905
+ else {
906
+ // Remember failure so we don't hammer construction on every snapshot
907
+ // update — the in-tree path will take over for this session.
908
+ state.iosBuildFailed = true;
909
+ }
910
+ return state.iosRefs;
911
+ }
912
+ function layoutIosOverlayRefs(refs) {
913
+ try {
914
+ const bounds = refs.controller.view.bounds;
915
+ const viewWidth = Number(bounds?.size?.width) || 0;
916
+ const viewHeight = Number(bounds?.size?.height) || 0;
917
+ const raw = refs.controller.view.safeAreaInsets;
918
+ const safeInsets = raw
919
+ ? {
920
+ top: Number(raw.top) || 0,
921
+ bottom: Number(raw.bottom) || 0,
922
+ left: Number(raw.left) || 0,
923
+ right: Number(raw.right) || 0,
924
+ }
925
+ : { top: 0, bottom: 0, left: 0, right: 0 };
926
+ // Ask UIKit what the labels want given the panel inner width. We use a
927
+ // generous height bound so nothing clips on long reconnect strings.
928
+ const panelPadding = 16;
929
+ const horizontalMargin = 24;
930
+ const maxPanelWidth = 340;
931
+ const innerWidth = Math.max(0, Math.min(maxPanelWidth, viewWidth - 2 * horizontalMargin - safeInsets.left - safeInsets.right) - 2 * panelPadding);
932
+ const titleFit = refs.titleLabel.sizeThatFits({ width: innerWidth, height: 10000 }) || { height: 0 };
933
+ const statusFit = refs.statusLabel.sizeThatFits({ width: innerWidth, height: 10000 }) || { height: 0 };
934
+ const layout = computeIosOverlayLayout({
935
+ viewWidth,
936
+ viewHeight,
937
+ safeInsets,
938
+ titleHeight: Number(titleFit.height) || 0,
939
+ statusHeight: Number(statusFit.height) || 0,
940
+ maxPanelWidth,
941
+ horizontalMargin,
942
+ panelPadding,
943
+ });
944
+ const toCgRect = (rect) => ({
945
+ origin: { x: rect.x, y: rect.y },
946
+ size: { width: rect.width, height: rect.height },
947
+ });
948
+ refs.backdrop.frame = toCgRect(layout.backdrop);
949
+ refs.panel.frame = toCgRect(layout.panel);
950
+ refs.titleLabel.frame = toCgRect(layout.title);
951
+ refs.statusLabel.frame = toCgRect(layout.status);
952
+ }
953
+ catch (err) {
954
+ try {
955
+ console.warn('[ns-hmr-overlay] iOS overlay layout failed:', err?.message || err);
956
+ }
957
+ catch { }
958
+ }
959
+ }
960
+ function applySnapshotToIosRefs(refs, snapshot) {
961
+ if (!refs)
962
+ return false;
963
+ try {
964
+ // Round-eleven.3 — 'update' mode rides the same dedicated
965
+ // UIWindow as 'connection' so the HMR apply overlay always
966
+ // stacks above modals/sheets/system alerts. The window is
967
+ // constructed lazily (ensureIosOverlayRefs) and reused for the
968
+ // lifetime of the dev session.
969
+ const visible = snapshot.visible && (snapshot.mode === 'connection' || snapshot.mode === 'update');
970
+ refs.window.hidden = !visible;
971
+ if (!visible)
972
+ return true;
973
+ refs.titleLabel.text = snapshot.title || '';
974
+ refs.statusLabel.text = formatStatusText(snapshot);
975
+ const host = getIosOverlayHost();
976
+ if (host) {
977
+ const { UIColor } = host;
978
+ const isError = snapshot.tone === 'error';
979
+ const isSuccess = snapshot.tone === 'success';
980
+ try {
981
+ if (isError) {
982
+ // Red panel + dark red text (existing UX).
983
+ refs.panel.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(1, 0.96, 0.96, 1);
984
+ refs.titleLabel.textColor = UIColor.colorWithRedGreenBlueAlpha(0.7, 0.1, 0.06, 1);
985
+ refs.statusLabel.textColor = UIColor.colorWithRedGreenBlueAlpha(0.7, 0.1, 0.06, 0.9);
986
+ // Slightly stronger dimming on errors; users need to
987
+ // notice these.
988
+ refs.backdrop.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0, 0, 0, 0.35);
989
+ }
990
+ else if (isSuccess) {
991
+ // Slightly more saturated green panel + dark-green
992
+ // text. The previous 0.94/0.99/0.95 background was
993
+ // nearly indistinguishable from white on most
994
+ // devices; this bump keeps long detail strings
995
+ // readable while making the apply event obviously
996
+ // "happening right now".
997
+ refs.panel.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0.9, 0.97, 0.91, 1);
998
+ refs.titleLabel.textColor = UIColor.colorWithRedGreenBlueAlpha(0.05, 0.43, 0.18, 1);
999
+ refs.statusLabel.textColor = UIColor.colorWithRedGreenBlueAlpha(0.05, 0.43, 0.18, 1);
1000
+ // Bumped from 0.12 to 0.28. The 0.12 wash was so
1001
+ // faint on bright app backgrounds that the overlay
1002
+ // was effectively invisible during a fast cycle.
1003
+ // 0.28 still keeps the app readable underneath but
1004
+ // makes the HMR event visually unmistakable.
1005
+ refs.backdrop.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0, 0.15, 0.05, 0.28);
1006
+ }
1007
+ else {
1008
+ // Default (info / warn) — existing connection look.
1009
+ refs.panel.backgroundColor = UIColor.whiteColor;
1010
+ refs.titleLabel.textColor = UIColor.blackColor;
1011
+ refs.statusLabel.textColor = UIColor.darkGrayColor;
1012
+ refs.backdrop.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0, 0, 0, 0.35);
1013
+ }
1014
+ }
1015
+ catch { }
1016
+ }
1017
+ layoutIosOverlayRefs(refs);
1018
+ return true;
1019
+ }
1020
+ catch (err) {
1021
+ try {
1022
+ console.warn('[ns-hmr-overlay] iOS overlay apply failed:', err?.message || err);
1023
+ }
1024
+ catch { }
1025
+ return false;
1026
+ }
1027
+ }
608
1028
  function applyRuntimeSnapshot(snapshot) {
609
1029
  const state = getRuntimeState();
610
1030
  state.snapshot = snapshot;
@@ -613,15 +1033,110 @@ function applyRuntimeSnapshot(snapshot) {
613
1033
  updateBootStatusLabel(snapshot);
614
1034
  }
615
1035
  applySnapshotToBootRefs(state.bootRefs, snapshot);
616
- if (snapshot.visible && snapshot.mode === 'connection') {
617
- const liveRefs = ensureLiveOverlayRefs(snapshot);
618
- applySnapshotToLiveRefs(liveRefs, snapshot);
1036
+ // prefer the dedicated UIWindow
1037
+ // path so the live/update overlays always stack on top of modals,
1038
+ // sheets, and other windows. Fall back to the in-tree overlay when
1039
+ // iOS APIs aren't available (Android, tests, or when scene
1040
+ // construction fails).
1041
+ let handledByIos = false;
1042
+ // Both 'connection' and 'update' use the small-panel surface
1043
+ // (UIWindow on iOS, in-tree overlay everywhere else). 'boot' uses
1044
+ // the placeholder root via applySnapshotToBootRefs above; 'hidden'
1045
+ // hides everything.
1046
+ const wantsOverlay = snapshot.visible && (snapshot.mode === 'connection' || snapshot.mode === 'update');
1047
+ if (getIosOverlayHost()) {
1048
+ if (wantsOverlay) {
1049
+ const iosRefs = ensureIosOverlayRefs(state);
1050
+ handledByIos = applySnapshotToIosRefs(iosRefs, snapshot);
1051
+ }
1052
+ else if (state.iosRefs) {
1053
+ handledByIos = applySnapshotToIosRefs(state.iosRefs, snapshot);
1054
+ }
619
1055
  }
620
- else {
621
- applySnapshotToLiveRefs(state.liveRefs, snapshot);
1056
+ if (!handledByIos) {
1057
+ if (wantsOverlay) {
1058
+ const liveRefs = ensureLiveOverlayRefs(snapshot);
1059
+ applySnapshotToLiveRefs(liveRefs, snapshot);
1060
+ }
1061
+ else {
1062
+ applySnapshotToLiveRefs(state.liveRefs, snapshot);
1063
+ }
622
1064
  }
623
1065
  return state.snapshot;
624
1066
  }
1067
+ // Round-eleven.3 — How long the 'complete' frame stays on screen
1068
+ // before we auto-hide. The original 350ms was too tight: many HMR
1069
+ // cycles complete in 50–250ms, so the *total* overlay lifetime
1070
+ // (received → complete + 350ms) was often under 500ms, which is
1071
+ // faster than the human eye can comfortably register. 600ms gives
1072
+ // the user time to read the "Total Xms" line and confirm visually
1073
+ // that something happened.
1074
+ const UPDATE_AUTO_HIDE_MS = 600;
1075
+ // Round-eleven.3 (alpha.62 follow-up) — Minimum perceptible duration
1076
+ // for an entire update overlay cycle (from 'received' to hide). If
1077
+ // the cycle finished in 50ms (e.g., a tiny HTML edit on a warm
1078
+ // cache), we still hold for ~MIN_VISIBLE_MS total before hiding so
1079
+ // the overlay is actually seen. Combined with UPDATE_AUTO_HIDE_MS,
1080
+ // the *effective* hold-after-complete = max(UPDATE_AUTO_HIDE_MS,
1081
+ // MIN_VISIBLE_MS - elapsed-since-received).
1082
+ const UPDATE_MIN_VISIBLE_MS = 800;
1083
+ function clearUpdateAutoHideTimer(state) {
1084
+ if (state.updateAutoHideTimer) {
1085
+ try {
1086
+ clearTimeout(state.updateAutoHideTimer);
1087
+ }
1088
+ catch { }
1089
+ state.updateAutoHideTimer = null;
1090
+ }
1091
+ }
1092
+ function scheduleUpdateAutoHide(state) {
1093
+ clearUpdateAutoHideTimer(state);
1094
+ // Compute how much longer we need to hold the overlay so that the
1095
+ // total cycle visibility is at least UPDATE_MIN_VISIBLE_MS. For
1096
+ // fast cycles (50ms reboot) this stretches the hide; for slow
1097
+ // cycles (>UPDATE_MIN_VISIBLE_MS) it falls back to the standard
1098
+ // UPDATE_AUTO_HIDE_MS so we don't truncate the celebratory hold.
1099
+ const startedAt = state.updateCycleStartedAt || 0;
1100
+ const elapsed = startedAt > 0 ? Math.max(0, Date.now() - startedAt) : 0;
1101
+ const minRemainder = elapsed > 0 ? Math.max(0, UPDATE_MIN_VISIBLE_MS - elapsed) : UPDATE_MIN_VISIBLE_MS;
1102
+ const holdMs = Math.max(UPDATE_AUTO_HIDE_MS, minRemainder);
1103
+ try {
1104
+ state.updateAutoHideTimer = setTimeout(() => {
1105
+ state.updateAutoHideTimer = null;
1106
+ // Critical: only auto-hide if we're still on the 'complete'
1107
+ // frame. If a new HMR cycle has rotated the snapshot back
1108
+ // to 'update' / 'received' (e.g., user saved twice in
1109
+ // quick succession), the new cycle owns the overlay and
1110
+ // our timer must not steal it.
1111
+ const current = state.snapshot;
1112
+ if (current.mode === 'update' && current.tone === 'success' && current.progress === 100) {
1113
+ state.updateCycleStartedAt = 0;
1114
+ applyRuntimeSnapshot({ ...DEFAULT_SNAPSHOT });
1115
+ }
1116
+ }, holdMs);
1117
+ }
1118
+ catch {
1119
+ // setTimeout missing (extremely rare; some test envs). Fall
1120
+ // back to immediate hide so we never leave the overlay visible
1121
+ // forever after a 'complete'.
1122
+ state.updateCycleStartedAt = 0;
1123
+ applyRuntimeSnapshot({ ...DEFAULT_SNAPSHOT });
1124
+ }
1125
+ }
1126
+ function logUpdateStageTransition(state, stage, info) {
1127
+ if (!state.verbose)
1128
+ return;
1129
+ try {
1130
+ const detail = info?.detail || '';
1131
+ const progress = typeof info?.progress === 'number' ? info.progress : null;
1132
+ const progressTag = progress !== null ? ` (${Math.round(progress)}%)` : '';
1133
+ // Single-line breadcrumb so a developer can correlate
1134
+ // overlay frames with the [ns-hmr][angular] timing log when
1135
+ // debugging "I don't see the overlay" reports.
1136
+ console.info(`[ns-hmr-overlay] update stage=${stage}${progressTag}${detail ? ` detail=${detail}` : ''}`);
1137
+ }
1138
+ catch { }
1139
+ }
625
1140
  function createOverlayApi() {
626
1141
  return {
627
1142
  ensureBootPage(verbose) {
@@ -637,12 +1152,64 @@ function createOverlayApi() {
637
1152
  return state.bootRefs?.page || null;
638
1153
  },
639
1154
  setBootStage(stage, info) {
1155
+ // A boot transition cancels any pending HMR auto-hide so
1156
+ // the boot phase always wins.
1157
+ const state = getRuntimeState();
1158
+ clearUpdateAutoHideTimer(state);
1159
+ state.updateCycleStartedAt = 0;
640
1160
  return applyRuntimeSnapshot(createBootOverlaySnapshot(stage, info));
641
1161
  },
642
1162
  setConnectionStage(stage, info) {
1163
+ const state = getRuntimeState();
1164
+ clearUpdateAutoHideTimer(state);
1165
+ state.updateCycleStartedAt = 0;
643
1166
  return applyRuntimeSnapshot(createConnectionOverlaySnapshot(stage, info));
644
1167
  },
1168
+ setUpdateStage(stage, info) {
1169
+ const state = getRuntimeState();
1170
+ // Each new in-progress stage cancels any pending auto-hide
1171
+ // from a previous cycle. Without this, two saves in quick
1172
+ // succession could see cycle-2's progress overlay yanked
1173
+ // off by cycle-1's already-scheduled hide.
1174
+ clearUpdateAutoHideTimer(state);
1175
+ // Stamp the cycle start on 'received', but distinguish
1176
+ // between two cases:
1177
+ //
1178
+ // (a) Re-assertion of the SAME cycle (e.g., the server
1179
+ // emits both `ns:hmr-pending` AND `ns:angular-update`,
1180
+ // both of which call `setUpdateStage('received')`).
1181
+ // We must PRESERVE the original timestamp so the
1182
+ // minimum-visible-window math measures the FIRST
1183
+ // 'received' the user actually saw.
1184
+ //
1185
+ // (b) Genuinely-new cycle starting either from a hidden
1186
+ // overlay OR while the previous cycle is still on
1187
+ // its 'complete' frame (pre auto-hide). In both
1188
+ // sub-cases we MUST stamp a fresh start so the
1189
+ // new cycle's auto-hide math is sane.
1190
+ //
1191
+ // We treat the previous snapshot as "in-progress for the
1192
+ // same cycle" iff mode==='update' AND progress!==100.
1193
+ // 'complete' frames are a sign that the cycle finished;
1194
+ // any subsequent 'received' is a NEW cycle.
1195
+ if (stage === 'received') {
1196
+ const prev = state.snapshot;
1197
+ const isMidCycleReassertion = prev.mode === 'update' && prev.progress !== 100;
1198
+ if (!isMidCycleReassertion) {
1199
+ state.updateCycleStartedAt = Date.now();
1200
+ }
1201
+ }
1202
+ logUpdateStageTransition(state, stage, info);
1203
+ const snapshot = applyRuntimeSnapshot(createUpdateOverlaySnapshot(stage, info));
1204
+ if (stage === 'complete') {
1205
+ scheduleUpdateAutoHide(state);
1206
+ }
1207
+ return snapshot;
1208
+ },
645
1209
  hide() {
1210
+ const state = getRuntimeState();
1211
+ clearUpdateAutoHideTimer(state);
1212
+ state.updateCycleStartedAt = 0;
646
1213
  applyRuntimeSnapshot({ ...DEFAULT_SNAPSHOT });
647
1214
  },
648
1215
  getSnapshot() {
@@ -668,6 +1235,14 @@ export function setHmrBootStage(stage, info) {
668
1235
  export function setHmrConnectionStage(stage, info) {
669
1236
  return ensureHmrDevOverlayRuntimeInstalled().setConnectionStage(stage, info);
670
1237
  }
1238
+ // Round-eleven.3 — Public entry point for driving the HMR-applying
1239
+ // overlay. Callers walk through stages (received → evicting →
1240
+ // reimporting → rebooting → complete); 'complete' auto-hides after a
1241
+ // short interval. Soft-fails (no-op) if the runtime overlay was never
1242
+ // installed (e.g., production builds, test environments).
1243
+ export function setHmrUpdateStage(stage, info) {
1244
+ return ensureHmrDevOverlayRuntimeInstalled().setUpdateStage(stage, info);
1245
+ }
671
1246
  export function hideHmrDevOverlay(reason) {
672
1247
  void reason;
673
1248
  ensureHmrDevOverlayRuntimeInstalled().hide(reason);