@nativescript/vite 8.0.0-alpha.10 → 8.0.0-alpha.12

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.
@@ -101,6 +101,62 @@ function hideConnectionOverlay() {
101
101
  }
102
102
  catch { }
103
103
  }
104
+ function setUpdateOverlayStage(stage, info) {
105
+ try {
106
+ const api = getHmrOverlayApi();
107
+ if (api && typeof api.setUpdateStage === 'function') {
108
+ api.setUpdateStage(stage, info);
109
+ }
110
+ }
111
+ catch { }
112
+ }
113
+ // Store the listener registry on globalThis (rather than in a module-private
114
+ // closure) because in NativeScript the HMR client module and the user app
115
+ // modules can resolve to different module instances depending on how the
116
+ // dev runtime loads them (HTTP client URL vs. the bundled vendor realm).
117
+ // A module-local Set would not be shared across instances; the global one
118
+ // is.
119
+ function getNsSolidHmrListenerSet() {
120
+ const g = globalThis;
121
+ let set = g.__ns_solid_hmr_listener_set;
122
+ if (!set) {
123
+ set = new Set();
124
+ g.__ns_solid_hmr_listener_set = set;
125
+ }
126
+ return set;
127
+ }
128
+ function nsSolidHmrSubscribe(fn) {
129
+ const listeners = getNsSolidHmrListenerSet();
130
+ listeners.add(fn);
131
+ if (VERBOSE)
132
+ console.log('[hmr][solid] subscribe — listeners=', listeners.size);
133
+ return () => listeners.delete(fn);
134
+ }
135
+ function nsSolidHmrEmit(ev) {
136
+ const listeners = getNsSolidHmrListenerSet();
137
+ if (VERBOSE)
138
+ console.log('[hmr][solid] emit listeners=', listeners.size, 'changedFiles=', ev.changedFiles);
139
+ for (const fn of Array.from(listeners)) {
140
+ try {
141
+ fn(ev);
142
+ }
143
+ catch (err) {
144
+ if (VERBOSE)
145
+ console.warn('[hmr][solid] listener threw', err);
146
+ }
147
+ }
148
+ }
149
+ try {
150
+ const g = globalThis;
151
+ g.__ns_solid_hmr_subscribe = nsSolidHmrSubscribe;
152
+ // Eagerly create the listener set so the global exists at module load time.
153
+ getNsSolidHmrListenerSet();
154
+ if (VERBOSE)
155
+ console.log('[hmr][solid] HMR client loaded. global set=', typeof g.__ns_solid_hmr_subscribe, 'listenerSet=', typeof g.__ns_solid_hmr_listener_set);
156
+ }
157
+ catch (err) {
158
+ console.warn('[hmr][solid] could not install global __ns_solid_hmr_subscribe', err);
159
+ }
104
160
  // Eagerly drive the HMR-applying overlay's 'received' frame as soon
105
161
  // as the server emits `ns:hmr-pending`, BEFORE the framework-specific
106
162
  // (`ns:angular-update` / `ns:css-updates`) payload arrives. The
@@ -827,6 +883,12 @@ async function processQueue() {
827
883
  return;
828
884
  if (VERBOSE)
829
885
  console.log('[hmr][queue] processing changed ids', drained);
886
+ // Track wall-clock so the 'complete' frame can show a meaningful
887
+ // total. Only the Solid + TypeScript flavors drive the overlay
888
+ // from here; Angular has its own flow inside
889
+ // `frameworks/angular/client/index.ts`.
890
+ const tQueueStart = Date.now();
891
+ const driveSolidOverlay = TARGET_FLAVOR === 'solid';
830
892
  // Explicit eviction step.
831
893
  //
832
894
  // On modern runtimes the URL canonicalizer collapses any
@@ -842,11 +904,21 @@ async function processQueue() {
842
904
  // legacy `/ns/m/__ns_hmr__/v<N>/` URL versioning path in that
843
905
  // case. node_modules and virtual specs are filtered out by
844
906
  // `buildEvictionUrls` so vendor modules stay hot.
907
+ if (driveSolidOverlay) {
908
+ setUpdateOverlayStage('evicting', {
909
+ detail: drained.length === 1 ? `Invalidating ${drained[0]}` : `Invalidating ${drained.length} modules`,
910
+ });
911
+ }
845
912
  const evictUrls = buildEvictionUrls(drained);
846
913
  const evicted = invalidateModulesByUrls(evictUrls);
847
914
  if (VERBOSE)
848
915
  console.log(`[hmr][queue] eviction count=${evictUrls.length} ok=${evicted}`);
849
916
  // Evaluate changed modules best-effort; failures shouldn't completely break HMR.
917
+ if (driveSolidOverlay) {
918
+ setUpdateOverlayStage('reimporting', {
919
+ detail: drained.length === 1 ? `Re-importing ${drained[0]}` : `Re-importing ${drained.length} modules`,
920
+ });
921
+ }
850
922
  for (const id of drained) {
851
923
  try {
852
924
  const spec = normalizeSpec(id);
@@ -868,6 +940,13 @@ async function processQueue() {
868
940
  // Vue SFCs are handled via the registry update path; nothing to do here.
869
941
  break;
870
942
  case 'solid': {
943
+ // Boundaries discovered in this HMR cycle (tsx files reachable
944
+ // via the reverse import graph from any changed file, plus route
945
+ // files reachable from any tsx start point). Declared at the top
946
+ // of the case block so the emit step below can include the
947
+ // complete set in the listener event — framework integrations
948
+ // use it to map route boundaries → fresh component references.
949
+ const boundaries = new Set();
871
950
  // Solid .tsx components are self-accepting via solid-refresh's inline
872
951
  // patchRegistry — re-importing them is sufficient. For non-component
873
952
  // .ts utility modules, we must propagate up the import graph to find
@@ -886,8 +965,10 @@ async function processQueue() {
886
965
  arr.push(id);
887
966
  }
888
967
  }
889
- // BFS from each non-tsx changed module up to tsx/jsx boundaries
890
- const boundaries = new Set();
968
+ // Pass 1: BFS from each non-tsx changed module up to tsx/jsx
969
+ // boundaries. These get re-imported below so solid-refresh's
970
+ // inline patchRegistry runs and (best-effort) swaps the proxy
971
+ // signals for any components defined in those tsx boundaries.
891
972
  for (const id of drained) {
892
973
  if (/\.(tsx|jsx)$/i.test(id))
893
974
  continue; // already self-accepting
@@ -911,6 +992,51 @@ async function processQueue() {
911
992
  }
912
993
  }
913
994
  }
995
+ // Pass 2: walk further from any tsx starting point (a tsx file
996
+ // in `drained` OR a tsx boundary discovered in pass 1) to find
997
+ // route files (`/src/routes/*.{tsx,jsx}`) that transitively
998
+ // import them. Re-importing a route file refreshes its
999
+ // `Route.options.component` to the freshly-imported reference
1000
+ // and the existing boundary loop below patches the live router
1001
+ // with that fresh reference.
1002
+ //
1003
+ // This is the key fix for "edit home.tsx → save → no visual
1004
+ // update": the old BFS skipped tsx files in `drained` (assuming
1005
+ // solid-refresh's in-place proxy patch was sufficient), but in
1006
+ // the universal-renderer + nested-context configuration that
1007
+ // patch does not always propagate to the visible page tree.
1008
+ // Adding the route file as a boundary lets us patch
1009
+ // `route.options.component` directly to a fresh module export,
1010
+ // which the framework subscriber then passes through to the
1011
+ // page remount — making the cycle robust to the proxy patch
1012
+ // silently failing.
1013
+ const tsxStarts = new Set();
1014
+ for (const id of drained) {
1015
+ if (/\.(tsx|jsx)$/i.test(id))
1016
+ tsxStarts.add(id);
1017
+ }
1018
+ for (const b of boundaries)
1019
+ tsxStarts.add(b);
1020
+ const ROUTE_FILE_RE = /\/src\/routes\/.+\.(tsx|jsx)$/i;
1021
+ for (const start of tsxStarts) {
1022
+ const visited = new Set();
1023
+ const queue = [start];
1024
+ while (queue.length) {
1025
+ const cur = queue.shift();
1026
+ if (visited.has(cur))
1027
+ continue;
1028
+ visited.add(cur);
1029
+ if (cur !== start && ROUTE_FILE_RE.test(cur)) {
1030
+ boundaries.add(cur);
1031
+ }
1032
+ const importers = reverseIndex.get(cur);
1033
+ if (!importers)
1034
+ continue;
1035
+ for (const imp of importers) {
1036
+ queue.push(imp);
1037
+ }
1038
+ }
1039
+ }
914
1040
  // Re-import each boundary so solid-refresh patchRegistry fires.
915
1041
  // For route files (TanStack Router), capture the new Route export
916
1042
  // and patch the router's existing route with the fresh loader.
@@ -980,22 +1106,28 @@ async function processQueue() {
980
1106
  if (VERBOSE)
981
1107
  console.log('[hmr][solid] propagated to boundary', { id, url });
982
1108
  const mod = await import(/* @vite-ignore */ url);
983
- // Patch TanStack Router route loaders
1109
+ // Patch TanStack Router route options for any module
1110
+ // that exports a `Route`. We patch BOTH the component
1111
+ // and the loader (when present); components-only routes
1112
+ // were previously skipped because the gate required a
1113
+ // loader, which left their `options.component` pointing
1114
+ // at the stale module's exports after HMR.
984
1115
  try {
985
1116
  const newRoute = mod?.Route;
986
- if (newRoute?.options?.loader) {
1117
+ if (newRoute?.options) {
987
1118
  const router = findRouter();
988
1119
  const fullPath = boundaryToFullPath(id);
989
1120
  if (VERBOSE)
990
- console.log('[hmr][solid][diag] route patch attempt', { id, fullPath, hasRouter: !!router, routesByIdKeys: router?.routesById ? Object.keys(router.routesById) : 'none' });
1121
+ console.log('[hmr][solid][diag] route patch attempt', { id, fullPath, hasRouter: !!router, hasLoader: !!newRoute.options.loader, hasComponent: !!newRoute.options.component });
991
1122
  const existingRoute = fullPath && router ? findRouteByFullPath(router, fullPath) : null;
992
1123
  if (existingRoute?.options) {
993
- existingRoute.options.loader = newRoute.options.loader;
1124
+ if (newRoute.options.loader)
1125
+ existingRoute.options.loader = newRoute.options.loader;
994
1126
  if (newRoute.options.component)
995
1127
  existingRoute.options.component = newRoute.options.component;
996
1128
  routesPatchCount++;
997
1129
  if (VERBOSE)
998
- console.log('[hmr][solid] patched route loader', existingRoute.id, 'fullPath=', fullPath);
1130
+ console.log('[hmr][solid] patched route', existingRoute.id, 'fullPath=', fullPath);
999
1131
  }
1000
1132
  else if (VERBOSE) {
1001
1133
  console.log('[hmr][solid] no matching route for fullPath', fullPath);
@@ -1031,6 +1163,44 @@ async function processQueue() {
1031
1163
  if (VERBOSE)
1032
1164
  console.warn('[hmr][solid] propagation failed', e);
1033
1165
  }
1166
+ // Notify any framework integrations (e.g.
1167
+ // `@nativescript/tanstack-router`) that a Solid HMR
1168
+ // cycle has completed. They use this signal to perform
1169
+ // framework-specific UI refresh (e.g. remount the active
1170
+ // router page) when solid-refresh's own reactive
1171
+ // propagation does not reach the visible tree under
1172
+ // the current renderer/context configuration.
1173
+ //
1174
+ // Boundaries include both the directly-changed tsx files
1175
+ // AND every tsx ancestor reachable via the reverse import
1176
+ // graph (route files in particular). The framework
1177
+ // listener uses the route-file boundaries to look up the
1178
+ // freshly-patched `route.options.component` and pass it
1179
+ // through to the page remount.
1180
+ try {
1181
+ const tsxChangedInDrained = drained.filter((id) => /\.(tsx|jsx)$/i.test(id));
1182
+ const allBoundaries = Array.from(new Set([...tsxChangedInDrained, ...boundaries]));
1183
+ nsSolidHmrEmit({
1184
+ kind: 'solid',
1185
+ changedFiles: drained.slice(),
1186
+ boundaries: allBoundaries,
1187
+ });
1188
+ }
1189
+ catch (err) {
1190
+ if (VERBOSE)
1191
+ console.warn('[hmr][solid] emit failed', err);
1192
+ }
1193
+ // Tell the overlay the cycle is done. solid-refresh's
1194
+ // inline patchRegistry has already flushed the new
1195
+ // component bodies into the live tree (the `case
1196
+ // 'solid'` block above re-imports each .tsx
1197
+ // boundary), so by the time we get here the user is
1198
+ // already looking at the new render. The 'complete'
1199
+ // frame surfaces the wall-clock total and triggers
1200
+ // the overlay's auto-hide.
1201
+ setUpdateOverlayStage('complete', {
1202
+ detail: `Total ${Math.max(0, Date.now() - tQueueStart)}ms`,
1203
+ });
1034
1204
  break;
1035
1205
  }
1036
1206
  case 'typescript': {
@@ -1547,6 +1717,104 @@ async function handleHmrMessage(ev) {
1547
1717
  return;
1548
1718
  }
1549
1719
  else {
1720
+ // Vite custom-event dispatch.
1721
+ //
1722
+ // `server.ws.send('event-name', payload)` from any Vite plugin lands
1723
+ // on the wire as `{ type: 'custom', event: 'event-name', data: payload }`.
1724
+ // On the web, Vite's stock client owns a `customListenersMap` that
1725
+ // fires every `import.meta.hot.on('event-name', cb)` callback. We
1726
+ // don't run Vite's stock client on device — the iOS runtime owns
1727
+ // the listener registry via `__NS_DISPATCH_HOT_EVENT__` (the
1728
+ // counterpart to `import.meta.hot.on` populated by user code +
1729
+ // compiled Angular components). Forwarding `type: 'custom'` here
1730
+ // is the only thing standing between server-emitted events and
1731
+ // the listeners they were meant for.
1732
+ //
1733
+ // `angular:component-update` is the canonical example. Analog's
1734
+ // plugin sends it on `.html` / component-style edits; the
1735
+ // compiled component `.mjs` registered a listener that
1736
+ // dynamic-imports `/@ng/component?c=<id>&t=<ts>` and calls
1737
+ // `ɵɵreplaceMetadata` on the live class — swapping the template
1738
+ // definition AND walking live `LView`s to recreate matching views
1739
+ // in-place. The page stays mounted and only the changed bits
1740
+ // re-render. We MUST `return` after dispatch so the reboot path
1741
+ // (`handleAngularHotUpdateMessage` → `__reboot_ng_modules__`)
1742
+ // never runs for these updates — that's the whole point of the
1743
+ // component-replacement pipeline.
1744
+ //
1745
+ // All other custom events are forwarded but NOT short-circuited
1746
+ // (Vite spec: custom events are additive — they don't replace
1747
+ // any framework-specific handling). The reboot path falls through
1748
+ // for `ns:angular-update` (the legacy/`.ts`-edit broadcast) and
1749
+ // for any framework not yet using the in-place replacement path.
1750
+ if (msg.type === 'custom' && typeof msg.event === 'string') {
1751
+ // Dispatch every Vite "custom" event through the runtime's
1752
+ // `__NS_DISPATCH_HOT_EVENT__` bridge so `import.meta.hot.on(event, cb)`
1753
+ // callbacks fire on the device. Critical contract: this is the
1754
+ // ONLY route by which Analog's `angular:component-update` reaches
1755
+ // the compiled component's `(d) => d.id === id && Component_HmrLoad(...)`
1756
+ // listener — without it, server-side broadcasts log green
1757
+ // (`(client) hmr update`) while the device sees nothing happen.
1758
+ //
1759
+ // Diagnostic policy: log "no dispatcher" loud (boot-time rt-bridge
1760
+ // failure), and listener exceptions loud (compiled HmrLoad
1761
+ // fetch/parse error). Successful dispatches are silent — the
1762
+ // runtime's `[import.meta.hot] dispatch summary` line carries
1763
+ // the per-event match-count diagnostic.
1764
+ try {
1765
+ const dispatch = globalThis.__NS_DISPATCH_HOT_EVENT__;
1766
+ if (typeof dispatch === 'function') {
1767
+ dispatch(msg.event, msg.data);
1768
+ }
1769
+ else {
1770
+ console.warn(`[hmr-client][custom] no __NS_DISPATCH_HOT_EVENT__ available for '${msg.event}'`);
1771
+ }
1772
+ }
1773
+ catch (err) {
1774
+ console.warn('[hmr-client][custom] dispatch threw for', msg.event, err);
1775
+ }
1776
+ if (msg.event === 'angular:component-update') {
1777
+ if (VERBOSE)
1778
+ console.log('[hmr-client][custom] dispatched angular:component-update — skipping reboot path');
1779
+ // Walk the apply-progress overlay through its
1780
+ // remaining stages for the in-place template-swap
1781
+ // path. The full reboot path
1782
+ // (`handleAngularHotUpdateMessage`) drives the
1783
+ // overlay itself ('received' → 'evicting' →
1784
+ // 'reimporting' → 'rebooting' → 'complete'); the
1785
+ // in-place path bypasses that handler entirely
1786
+ // because the work happens inside Angular's
1787
+ // `ɵɵreplaceMetadata` after the runtime forwards the
1788
+ // `angular:component-update` event to the compiled
1789
+ // component's listener. Without this update the
1790
+ // overlay would freeze at 5% ('received') even
1791
+ // though the visual swap completes a few frames
1792
+ // later — exactly the "Preparing update (5%)" stuck
1793
+ // frame we have been chasing.
1794
+ //
1795
+ // We transition straight to 'reimporting' to
1796
+ // communicate that metadata is being fetched (the
1797
+ // runtime listener fires `__ns_import('/@ng/component?c=...&t=...')`),
1798
+ // then schedule 'complete' on the next macrotask so
1799
+ // the auto-hide timer kicks in. The actual
1800
+ // template swap is fire-and-forget from this point;
1801
+ // the user sees the overlay close at the same time
1802
+ // as Angular re-renders the bound text/structure.
1803
+ try {
1804
+ const filePath = typeof msg.data?.id === 'string' ? decodeURIComponent(msg.data.id).split('@')[0] : undefined;
1805
+ const detail = filePath ? `Applying template update to ${filePath}` : 'Applying template update';
1806
+ setUpdateOverlayStage('reimporting', { detail });
1807
+ setTimeout(() => {
1808
+ try {
1809
+ setUpdateOverlayStage('complete', { detail: filePath ? `Updated ${filePath}` : 'Update applied' });
1810
+ }
1811
+ catch { }
1812
+ }, 16);
1813
+ }
1814
+ catch { }
1815
+ return;
1816
+ }
1817
+ }
1550
1818
  if (msg.type === 'ns:angular-update' && typeof msg.version === 'number') {
1551
1819
  setGraphVersion(Number(msg.version || getGraphVersion() || 0));
1552
1820
  }