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

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 (424) hide show
  1. package/README.md +51 -11
  2. package/bin/cli.cjs +8 -14
  3. package/configuration/angular.d.ts +34 -1
  4. package/configuration/angular.js +376 -165
  5. package/configuration/angular.js.map +1 -1
  6. package/configuration/base.js +241 -51
  7. package/configuration/base.js.map +1 -1
  8. package/configuration/javascript.js +14 -93
  9. package/configuration/javascript.js.map +1 -1
  10. package/configuration/react.js +41 -73
  11. package/configuration/react.js.map +1 -1
  12. package/configuration/solid.js +51 -6
  13. package/configuration/solid.js.map +1 -1
  14. package/configuration/typescript.js +12 -93
  15. package/configuration/typescript.js.map +1 -1
  16. package/helpers/app-components.d.ts +2 -1
  17. package/helpers/app-components.js.map +1 -1
  18. package/helpers/app-css-state.d.ts +8 -0
  19. package/helpers/app-css-state.js +8 -0
  20. package/helpers/app-css-state.js.map +1 -0
  21. package/helpers/bundler-context.d.ts +11 -0
  22. package/helpers/bundler-context.js +71 -0
  23. package/helpers/bundler-context.js.map +1 -0
  24. package/helpers/cli-flags.d.ts +12 -0
  25. package/helpers/cli-flags.js +34 -0
  26. package/helpers/cli-flags.js.map +1 -1
  27. package/helpers/css-platform-plugin.d.ts +14 -0
  28. package/helpers/css-platform-plugin.js +43 -25
  29. package/helpers/css-platform-plugin.js.map +1 -1
  30. package/helpers/dev-host.d.ts +360 -0
  31. package/helpers/dev-host.js +694 -0
  32. package/helpers/dev-host.js.map +1 -0
  33. package/helpers/dynamic-import-plugin.js +1 -1
  34. package/helpers/dynamic-import-plugin.js.map +1 -1
  35. package/helpers/esbuild-platform-resolver.js +4 -1
  36. package/helpers/esbuild-platform-resolver.js.map +1 -1
  37. package/helpers/external-configs.d.ts +10 -12
  38. package/helpers/external-configs.js +58 -35
  39. package/helpers/external-configs.js.map +1 -1
  40. package/helpers/global-defines.d.ts +142 -0
  41. package/helpers/global-defines.js +213 -11
  42. package/helpers/global-defines.js.map +1 -1
  43. package/helpers/hmr-scope.d.ts +26 -0
  44. package/helpers/hmr-scope.js +67 -0
  45. package/helpers/hmr-scope.js.map +1 -0
  46. package/helpers/init.js +0 -18
  47. package/helpers/init.js.map +1 -1
  48. package/helpers/logging.d.ts +1 -0
  49. package/helpers/logging.js +65 -4
  50. package/helpers/logging.js.map +1 -1
  51. package/helpers/main-entry.d.ts +3 -1
  52. package/helpers/main-entry.js +301 -53
  53. package/helpers/main-entry.js.map +1 -1
  54. package/helpers/nativeclass-esbuild-plugin.d.ts +2 -1
  55. package/helpers/nativeclass-esbuild-plugin.js.map +1 -1
  56. package/helpers/nativeclass-transform.js +5 -6
  57. package/helpers/nativeclass-transform.js.map +1 -1
  58. package/helpers/nativeclass-transformer-plugin.d.ts +9 -2
  59. package/helpers/nativeclass-transformer-plugin.js +157 -14
  60. package/helpers/nativeclass-transformer-plugin.js.map +1 -1
  61. package/helpers/nativescript-package-resolver.js +10 -62
  62. package/helpers/nativescript-package-resolver.js.map +1 -1
  63. package/helpers/normalize-id.d.ts +42 -0
  64. package/helpers/normalize-id.js +60 -0
  65. package/helpers/normalize-id.js.map +1 -0
  66. package/helpers/ns-core-url.d.ts +29 -7
  67. package/helpers/ns-core-url.js +69 -12
  68. package/helpers/ns-core-url.js.map +1 -1
  69. package/helpers/optimize-deps.d.ts +16 -0
  70. package/helpers/optimize-deps.js +17 -0
  71. package/helpers/optimize-deps.js.map +1 -0
  72. package/helpers/package-platform-aliases.js +34 -49
  73. package/helpers/package-platform-aliases.js.map +1 -1
  74. package/helpers/platform-types.d.ts +2 -0
  75. package/helpers/platform-types.js +2 -0
  76. package/helpers/platform-types.js.map +1 -0
  77. package/helpers/postcss-platform-config.d.ts +17 -1
  78. package/helpers/postcss-platform-config.js +20 -37
  79. package/helpers/postcss-platform-config.js.map +1 -1
  80. package/helpers/project.d.ts +35 -0
  81. package/helpers/project.js +120 -2
  82. package/helpers/project.js.map +1 -1
  83. package/helpers/resolve-main-field-platform.d.ts +20 -0
  84. package/helpers/resolve-main-field-platform.js +49 -0
  85. package/helpers/resolve-main-field-platform.js.map +1 -0
  86. package/helpers/resolver.js +17 -2
  87. package/helpers/resolver.js.map +1 -1
  88. package/helpers/theme-core-plugins.js +1 -1
  89. package/helpers/theme-core-plugins.js.map +1 -1
  90. package/helpers/ts-config-paths.d.ts +14 -0
  91. package/helpers/ts-config-paths.js +90 -9
  92. package/helpers/ts-config-paths.js.map +1 -1
  93. package/helpers/typescript-check.d.ts +2 -1
  94. package/helpers/typescript-check.js +2 -2
  95. package/helpers/typescript-check.js.map +1 -1
  96. package/helpers/ui-registration.d.ts +21 -0
  97. package/helpers/ui-registration.js +156 -0
  98. package/helpers/ui-registration.js.map +1 -0
  99. package/helpers/utils.d.ts +9 -0
  100. package/helpers/utils.js +22 -2
  101. package/helpers/utils.js.map +1 -1
  102. package/helpers/workers.d.ts +20 -19
  103. package/helpers/workers.js +624 -4
  104. package/helpers/workers.js.map +1 -1
  105. package/hmr/client/css-handler.d.ts +2 -1
  106. package/hmr/client/css-handler.js +50 -26
  107. package/hmr/client/css-handler.js.map +1 -1
  108. package/hmr/client/css-update-overlay.d.ts +18 -0
  109. package/hmr/client/css-update-overlay.js +27 -0
  110. package/hmr/client/css-update-overlay.js.map +1 -0
  111. package/hmr/client/framework-client-strategy.d.ts +79 -0
  112. package/hmr/client/framework-client-strategy.js +19 -0
  113. package/hmr/client/framework-client-strategy.js.map +1 -0
  114. package/hmr/client/hmr-pending-overlay.d.ts +13 -0
  115. package/hmr/client/hmr-pending-overlay.js +60 -0
  116. package/hmr/client/hmr-pending-overlay.js.map +1 -0
  117. package/hmr/client/index.js +891 -220
  118. package/hmr/client/index.js.map +1 -1
  119. package/hmr/client/utils.d.ts +7 -1
  120. package/hmr/client/utils.js +207 -29
  121. package/hmr/client/utils.js.map +1 -1
  122. package/hmr/entry-runtime.d.ts +2 -1
  123. package/hmr/entry-runtime.js +256 -69
  124. package/hmr/entry-runtime.js.map +1 -1
  125. package/hmr/frameworks/angular/build/angular-linker.d.ts +12 -0
  126. package/hmr/frameworks/angular/build/angular-linker.js +109 -0
  127. package/hmr/frameworks/angular/build/angular-linker.js.map +1 -0
  128. package/hmr/frameworks/angular/build/inject-component-hmr-registration.d.ts +112 -0
  129. package/hmr/frameworks/angular/build/inject-component-hmr-registration.js +291 -0
  130. package/hmr/frameworks/angular/build/inject-component-hmr-registration.js.map +1 -0
  131. package/hmr/frameworks/angular/build/inject-hmr-vite-ignore.d.ts +75 -0
  132. package/hmr/frameworks/angular/build/inject-hmr-vite-ignore.js +221 -0
  133. package/hmr/frameworks/angular/build/inject-hmr-vite-ignore.js.map +1 -0
  134. package/{helpers/angular → hmr/frameworks/angular/build}/inline-decorator-component-templates.js +1 -170
  135. package/hmr/frameworks/angular/build/inline-decorator-component-templates.js.map +1 -0
  136. package/hmr/frameworks/angular/build/js-lexer.d.ts +4 -0
  137. package/{helpers/angular/synthesize-decorator-ctor-parameters.js → hmr/frameworks/angular/build/js-lexer.js} +22 -96
  138. package/hmr/frameworks/angular/build/js-lexer.js.map +1 -0
  139. package/hmr/frameworks/angular/build/shared-linker.d.ts +39 -0
  140. package/hmr/frameworks/angular/build/shared-linker.js +128 -0
  141. package/hmr/frameworks/angular/build/shared-linker.js.map +1 -0
  142. package/hmr/frameworks/angular/build/synthesize-decorator-ctor-parameters.js +88 -0
  143. package/hmr/frameworks/angular/build/synthesize-decorator-ctor-parameters.js.map +1 -0
  144. package/{helpers/angular → hmr/frameworks/angular/build}/synthesize-injectable-factories.js +1 -174
  145. package/hmr/frameworks/angular/build/synthesize-injectable-factories.js.map +1 -0
  146. package/{helpers/angular → hmr/frameworks/angular/build}/util.d.ts +1 -0
  147. package/hmr/frameworks/angular/build/util.js +155 -0
  148. package/hmr/frameworks/angular/build/util.js.map +1 -0
  149. package/hmr/frameworks/angular/client/index.d.ts +1 -0
  150. package/hmr/frameworks/angular/client/index.js +859 -21
  151. package/hmr/frameworks/angular/client/index.js.map +1 -1
  152. package/hmr/frameworks/angular/client/strategy.d.ts +9 -0
  153. package/hmr/frameworks/angular/client/strategy.js +19 -0
  154. package/hmr/frameworks/angular/client/strategy.js.map +1 -0
  155. package/hmr/frameworks/angular/server/angular-root-component.d.ts +79 -0
  156. package/hmr/frameworks/angular/server/angular-root-component.js +149 -0
  157. package/hmr/frameworks/angular/server/angular-root-component.js.map +1 -0
  158. package/hmr/frameworks/angular/server/linker.js +1 -4
  159. package/hmr/frameworks/angular/server/linker.js.map +1 -1
  160. package/hmr/frameworks/angular/server/strategy.js +460 -12
  161. package/hmr/frameworks/angular/server/strategy.js.map +1 -1
  162. package/hmr/{server → frameworks/angular/server}/websocket-angular-entry.js +2 -2
  163. package/hmr/frameworks/angular/server/websocket-angular-entry.js.map +1 -0
  164. package/hmr/{server → frameworks/angular/server}/websocket-angular-hot-update.d.ts +17 -11
  165. package/hmr/frameworks/angular/server/websocket-angular-hot-update.js +336 -0
  166. package/hmr/frameworks/angular/server/websocket-angular-hot-update.js.map +1 -0
  167. package/hmr/frameworks/react/server/strategy.d.ts +2 -0
  168. package/hmr/frameworks/react/server/strategy.js +150 -0
  169. package/hmr/frameworks/react/server/strategy.js.map +1 -0
  170. package/hmr/frameworks/solid/build/solid-jsx-deps.d.ts +15 -0
  171. package/hmr/frameworks/solid/build/solid-jsx-deps.js +178 -0
  172. package/hmr/frameworks/solid/build/solid-jsx-deps.js.map +1 -0
  173. package/hmr/frameworks/solid/client/app-runtime.d.ts +54 -0
  174. package/hmr/frameworks/solid/client/app-runtime.js +184 -0
  175. package/hmr/frameworks/solid/client/app-runtime.js.map +1 -0
  176. package/hmr/frameworks/solid/server/strategy.js +291 -16
  177. package/hmr/frameworks/solid/server/strategy.js.map +1 -1
  178. package/hmr/frameworks/typescript/server/strategy.js +38 -14
  179. package/hmr/frameworks/typescript/server/strategy.js.map +1 -1
  180. package/hmr/frameworks/vue/client/dep-propagation.d.ts +36 -0
  181. package/hmr/frameworks/vue/client/dep-propagation.js +101 -0
  182. package/hmr/frameworks/vue/client/dep-propagation.js.map +1 -0
  183. package/hmr/frameworks/vue/client/index.d.ts +8 -0
  184. package/hmr/frameworks/vue/client/index.js +56 -243
  185. package/hmr/frameworks/vue/client/index.js.map +1 -1
  186. package/hmr/frameworks/vue/client/strategy.d.ts +33 -0
  187. package/hmr/frameworks/vue/client/strategy.js +157 -0
  188. package/hmr/frameworks/vue/client/strategy.js.map +1 -0
  189. package/hmr/frameworks/vue/client/vue-sfc-update-overlay.d.ts +49 -0
  190. package/hmr/frameworks/vue/client/vue-sfc-update-overlay.js +142 -0
  191. package/hmr/frameworks/vue/client/vue-sfc-update-overlay.js.map +1 -0
  192. package/hmr/frameworks/vue/server/sfc-route-assemble.d.ts +7 -0
  193. package/hmr/frameworks/vue/server/sfc-route-assemble.js +747 -0
  194. package/hmr/frameworks/vue/server/sfc-route-assemble.js.map +1 -0
  195. package/hmr/frameworks/vue/server/sfc-route-meta.d.ts +7 -0
  196. package/hmr/frameworks/vue/server/sfc-route-meta.js +80 -0
  197. package/hmr/frameworks/vue/server/sfc-route-meta.js.map +1 -0
  198. package/hmr/frameworks/vue/server/sfc-route-serve.d.ts +8 -0
  199. package/hmr/frameworks/vue/server/sfc-route-serve.js +459 -0
  200. package/hmr/frameworks/vue/server/sfc-route-serve.js.map +1 -0
  201. package/hmr/frameworks/vue/server/sfc-route-shared.d.ts +38 -0
  202. package/hmr/frameworks/vue/server/sfc-route-shared.js +48 -0
  203. package/hmr/frameworks/vue/server/sfc-route-shared.js.map +1 -0
  204. package/hmr/frameworks/vue/server/strategy.d.ts +35 -0
  205. package/hmr/frameworks/vue/server/strategy.js +278 -1
  206. package/hmr/frameworks/vue/server/strategy.js.map +1 -1
  207. package/hmr/frameworks/vue/server/websocket-sfc.d.ts +15 -0
  208. package/hmr/frameworks/vue/server/websocket-sfc.js +20 -0
  209. package/hmr/frameworks/vue/server/websocket-sfc.js.map +1 -0
  210. package/hmr/helpers/ast-normalizer.d.ts +3 -1
  211. package/hmr/helpers/ast-normalizer.js +77 -10
  212. package/hmr/helpers/ast-normalizer.js.map +1 -1
  213. package/hmr/helpers/cjs-named-exports.d.ts +23 -0
  214. package/hmr/helpers/cjs-named-exports.js +152 -0
  215. package/hmr/helpers/cjs-named-exports.js.map +1 -0
  216. package/hmr/helpers/package-exports.d.ts +16 -0
  217. package/hmr/helpers/package-exports.js +396 -0
  218. package/hmr/helpers/package-exports.js.map +1 -0
  219. package/hmr/server/constants.js +20 -5
  220. package/hmr/server/constants.js.map +1 -1
  221. package/hmr/server/core-sanitize.d.ts +86 -7
  222. package/hmr/server/core-sanitize.js +170 -45
  223. package/hmr/server/core-sanitize.js.map +1 -1
  224. package/hmr/server/device-transform-helpers.d.ts +24 -0
  225. package/hmr/server/device-transform-helpers.js +408 -0
  226. package/hmr/server/device-transform-helpers.js.map +1 -0
  227. package/hmr/server/framework-strategy.d.ts +108 -11
  228. package/hmr/server/hmr-module-graph.d.ts +37 -0
  229. package/hmr/server/hmr-module-graph.js +214 -0
  230. package/hmr/server/hmr-module-graph.js.map +1 -0
  231. package/hmr/server/import-map.d.ts +11 -2
  232. package/hmr/server/import-map.js +92 -45
  233. package/hmr/server/import-map.js.map +1 -1
  234. package/hmr/server/index.js +7 -16
  235. package/hmr/server/index.js.map +1 -1
  236. package/hmr/server/ns-core-cjs-shape.d.ts +2 -4
  237. package/hmr/server/ns-core-cjs-shape.js +4 -6
  238. package/hmr/server/ns-core-cjs-shape.js.map +1 -1
  239. package/hmr/server/ns-rt-bridge.d.ts +51 -0
  240. package/hmr/server/ns-rt-bridge.js +131 -0
  241. package/hmr/server/ns-rt-bridge.js.map +1 -0
  242. package/hmr/server/ns-rt-route.d.ts +5 -0
  243. package/hmr/server/ns-rt-route.js +38 -0
  244. package/hmr/server/ns-rt-route.js.map +1 -0
  245. package/hmr/server/perf-instrumentation.d.ts +114 -0
  246. package/hmr/server/perf-instrumentation.js +197 -0
  247. package/hmr/server/perf-instrumentation.js.map +1 -0
  248. package/hmr/server/process-code-for-device.d.ts +14 -0
  249. package/hmr/server/process-code-for-device.js +699 -0
  250. package/hmr/server/process-code-for-device.js.map +1 -0
  251. package/hmr/server/require-guard.d.ts +1 -0
  252. package/hmr/server/require-guard.js +12 -0
  253. package/hmr/server/require-guard.js.map +1 -0
  254. package/hmr/server/rewrite-imports.d.ts +2 -0
  255. package/hmr/server/rewrite-imports.js +630 -0
  256. package/hmr/server/rewrite-imports.js.map +1 -0
  257. package/hmr/server/route-helpers.d.ts +7 -0
  258. package/hmr/server/route-helpers.js +13 -0
  259. package/hmr/server/route-helpers.js.map +1 -0
  260. package/hmr/server/server-origin.d.ts +2 -0
  261. package/hmr/server/server-origin.js +83 -0
  262. package/hmr/server/server-origin.js.map +1 -0
  263. package/hmr/server/shared-transform-request.js +13 -7
  264. package/hmr/server/shared-transform-request.js.map +1 -1
  265. package/hmr/server/transform-cache-invalidation.d.ts +37 -0
  266. package/hmr/server/transform-cache-invalidation.js +156 -0
  267. package/hmr/server/transform-cache-invalidation.js.map +1 -0
  268. package/hmr/server/vendor-bare-module-shims.d.ts +4 -0
  269. package/hmr/server/vendor-bare-module-shims.js +80 -0
  270. package/hmr/server/vendor-bare-module-shims.js.map +1 -0
  271. package/hmr/server/vite-plugin.js +78 -42
  272. package/hmr/server/vite-plugin.js.map +1 -1
  273. package/hmr/server/websocket-core-bridge.d.ts +45 -4
  274. package/hmr/server/websocket-core-bridge.js +50 -48
  275. package/hmr/server/websocket-core-bridge.js.map +1 -1
  276. package/hmr/server/websocket-css-hot-update.d.ts +33 -0
  277. package/hmr/server/websocket-css-hot-update.js +65 -0
  278. package/hmr/server/websocket-css-hot-update.js.map +1 -0
  279. package/hmr/server/websocket-device-transform.d.ts +3 -0
  280. package/hmr/server/websocket-device-transform.js +7 -0
  281. package/hmr/server/websocket-device-transform.js.map +1 -0
  282. package/hmr/server/websocket-graph-upsert.d.ts +15 -0
  283. package/hmr/server/websocket-graph-upsert.js +20 -0
  284. package/hmr/server/websocket-graph-upsert.js.map +1 -1
  285. package/hmr/server/websocket-hmr-pending.d.ts +37 -0
  286. package/hmr/server/websocket-hmr-pending.js +55 -0
  287. package/hmr/server/websocket-hmr-pending.js.map +1 -0
  288. package/hmr/server/websocket-hot-update.d.ts +77 -0
  289. package/hmr/server/websocket-hot-update.js +360 -0
  290. package/hmr/server/websocket-hot-update.js.map +1 -0
  291. package/hmr/server/websocket-import-map-route.d.ts +15 -0
  292. package/hmr/server/websocket-import-map-route.js +50 -0
  293. package/hmr/server/websocket-import-map-route.js.map +1 -0
  294. package/hmr/server/websocket-module-bindings.js +3 -3
  295. package/hmr/server/websocket-module-bindings.js.map +1 -1
  296. package/hmr/server/websocket-module-specifiers.d.ts +66 -2
  297. package/hmr/server/websocket-module-specifiers.js +191 -20
  298. package/hmr/server/websocket-module-specifiers.js.map +1 -1
  299. package/hmr/server/websocket-ns-core.d.ts +21 -0
  300. package/hmr/server/websocket-ns-core.js +311 -0
  301. package/hmr/server/websocket-ns-core.js.map +1 -0
  302. package/hmr/server/websocket-ns-entry.d.ts +21 -0
  303. package/hmr/server/websocket-ns-entry.js +164 -0
  304. package/hmr/server/websocket-ns-entry.js.map +1 -0
  305. package/hmr/server/websocket-ns-m-paths.d.ts +1 -1
  306. package/hmr/server/websocket-ns-m-paths.js +58 -13
  307. package/hmr/server/websocket-ns-m-paths.js.map +1 -1
  308. package/hmr/server/websocket-ns-m-request.d.ts +11 -1
  309. package/hmr/server/websocket-ns-m-request.js +18 -25
  310. package/hmr/server/websocket-ns-m-request.js.map +1 -1
  311. package/hmr/server/websocket-ns-m.d.ts +33 -0
  312. package/hmr/server/websocket-ns-m.js +752 -0
  313. package/hmr/server/websocket-ns-m.js.map +1 -0
  314. package/hmr/server/websocket-served-module-helpers.d.ts +49 -3
  315. package/hmr/server/websocket-served-module-helpers.js +403 -87
  316. package/hmr/server/websocket-served-module-helpers.js.map +1 -1
  317. package/hmr/server/websocket-txn.js +2 -8
  318. package/hmr/server/websocket-txn.js.map +1 -1
  319. package/hmr/server/websocket-vendor-unifier.d.ts +0 -1
  320. package/hmr/server/websocket-vendor-unifier.js +4 -9
  321. package/hmr/server/websocket-vendor-unifier.js.map +1 -1
  322. package/hmr/server/websocket.d.ts +8 -39
  323. package/hmr/server/websocket.js +608 -6300
  324. package/hmr/server/websocket.js.map +1 -1
  325. package/hmr/shared/ns-globals.d.ts +118 -0
  326. package/hmr/shared/ns-globals.js +29 -0
  327. package/hmr/shared/ns-globals.js.map +1 -0
  328. package/hmr/shared/protocol.d.ts +145 -0
  329. package/hmr/shared/protocol.js +28 -0
  330. package/hmr/shared/protocol.js.map +1 -0
  331. package/hmr/shared/runtime/boot-placeholder-ui.d.ts +69 -0
  332. package/hmr/shared/runtime/boot-placeholder-ui.js +101 -0
  333. package/hmr/shared/runtime/boot-placeholder-ui.js.map +1 -0
  334. package/hmr/shared/runtime/boot-progress.d.ts +44 -0
  335. package/hmr/shared/runtime/boot-progress.js +133 -0
  336. package/hmr/shared/runtime/boot-progress.js.map +1 -0
  337. package/hmr/shared/runtime/boot-timeline.d.ts +18 -0
  338. package/hmr/shared/runtime/boot-timeline.js +42 -0
  339. package/hmr/shared/runtime/boot-timeline.js.map +1 -0
  340. package/hmr/shared/runtime/browser-runtime-contract.js.map +1 -1
  341. package/hmr/shared/runtime/dev-overlay-snapshots.d.ts +31 -0
  342. package/hmr/shared/runtime/dev-overlay-snapshots.js +324 -0
  343. package/hmr/shared/runtime/dev-overlay-snapshots.js.map +1 -0
  344. package/hmr/shared/runtime/dev-overlay.d.ts +119 -26
  345. package/hmr/shared/runtime/dev-overlay.js +1196 -262
  346. package/hmr/shared/runtime/dev-overlay.js.map +1 -1
  347. package/hmr/shared/runtime/global-scope.d.ts +18 -0
  348. package/hmr/shared/runtime/global-scope.js +21 -0
  349. package/hmr/shared/runtime/global-scope.js.map +1 -0
  350. package/hmr/shared/runtime/hooks.js +2 -1
  351. package/hmr/shared/runtime/hooks.js.map +1 -1
  352. package/hmr/shared/runtime/http-only-boot.js +7 -6
  353. package/hmr/shared/runtime/http-only-boot.js.map +1 -1
  354. package/hmr/shared/runtime/module-provenance.js +4 -6
  355. package/hmr/shared/runtime/module-provenance.js.map +1 -1
  356. package/hmr/shared/runtime/root-placeholder-view.d.ts +19 -0
  357. package/hmr/shared/runtime/root-placeholder-view.js +311 -0
  358. package/hmr/shared/runtime/root-placeholder-view.js.map +1 -0
  359. package/hmr/shared/runtime/root-placeholder.js +393 -200
  360. package/hmr/shared/runtime/root-placeholder.js.map +1 -1
  361. package/hmr/shared/runtime/session-bootstrap.js +168 -4
  362. package/hmr/shared/runtime/session-bootstrap.js.map +1 -1
  363. package/hmr/shared/runtime/vendor-bootstrap.js +3 -10
  364. package/hmr/shared/runtime/vendor-bootstrap.js.map +1 -1
  365. package/hmr/shared/vendor/manifest-collect.d.ts +4 -0
  366. package/hmr/shared/vendor/manifest-collect.js +549 -0
  367. package/hmr/shared/vendor/manifest-collect.js.map +1 -0
  368. package/hmr/shared/vendor/manifest-loader.d.ts +2 -1
  369. package/hmr/shared/vendor/manifest-loader.js +5 -3
  370. package/hmr/shared/vendor/manifest-loader.js.map +1 -1
  371. package/hmr/shared/vendor/manifest.d.ts +1 -7
  372. package/hmr/shared/vendor/manifest.js +84 -819
  373. package/hmr/shared/vendor/manifest.js.map +1 -1
  374. package/hmr/shared/vendor/vendor-device-shim.d.ts +1 -0
  375. package/hmr/shared/vendor/vendor-device-shim.js +208 -0
  376. package/hmr/shared/vendor/vendor-device-shim.js.map +1 -0
  377. package/hmr/shared/vendor/vendor-esbuild-plugins.d.ts +38 -0
  378. package/hmr/shared/vendor/vendor-esbuild-plugins.js +296 -0
  379. package/hmr/shared/vendor/vendor-esbuild-plugins.js.map +1 -0
  380. package/hmr/vendor-bootstrap.d.ts +1 -3
  381. package/hmr/vendor-bootstrap.js +4 -6
  382. package/hmr/vendor-bootstrap.js.map +1 -1
  383. package/index.d.ts +1 -0
  384. package/index.js +5 -0
  385. package/index.js.map +1 -1
  386. package/package.json +61 -11
  387. package/runtime/core-aliases-early.js +25 -55
  388. package/runtime/core-aliases-early.js.map +1 -1
  389. package/shims/react-jsx-runtime.d.ts +4 -0
  390. package/shims/react-jsx-runtime.js +61 -0
  391. package/shims/react-jsx-runtime.js.map +1 -0
  392. package/helpers/angular/angular-linker.d.ts +0 -13
  393. package/helpers/angular/angular-linker.js +0 -194
  394. package/helpers/angular/angular-linker.js.map +0 -1
  395. package/helpers/angular/inline-decorator-component-templates.js.map +0 -1
  396. package/helpers/angular/shared-linker.d.ts +0 -11
  397. package/helpers/angular/shared-linker.js +0 -75
  398. package/helpers/angular/shared-linker.js.map +0 -1
  399. package/helpers/angular/synthesize-decorator-ctor-parameters.js.map +0 -1
  400. package/helpers/angular/synthesize-injectable-factories.js.map +0 -1
  401. package/helpers/angular/util.js +0 -67
  402. package/helpers/angular/util.js.map +0 -1
  403. package/helpers/prelink-angular.d.ts +0 -2
  404. package/helpers/prelink-angular.js +0 -117
  405. package/helpers/prelink-angular.js.map +0 -1
  406. package/hmr/server/websocket-angular-entry.js.map +0 -1
  407. package/hmr/server/websocket-angular-hot-update.js +0 -239
  408. package/hmr/server/websocket-angular-hot-update.js.map +0 -1
  409. package/hmr/server/websocket-ns-m-finalize.d.ts +0 -22
  410. package/hmr/server/websocket-ns-m-finalize.js +0 -88
  411. package/hmr/server/websocket-ns-m-finalize.js.map +0 -1
  412. package/hmr/server/websocket-runtime-compat.d.ts +0 -19
  413. package/hmr/server/websocket-runtime-compat.js +0 -286
  414. package/hmr/server/websocket-runtime-compat.js.map +0 -1
  415. package/hmr/server/websocket-vue-sfc.d.ts +0 -27
  416. package/hmr/server/websocket-vue-sfc.js +0 -1117
  417. package/hmr/server/websocket-vue-sfc.js.map +0 -1
  418. package/transformers/NativeClass/index.d.ts +0 -2
  419. package/transformers/NativeClass/index.js +0 -222
  420. package/transformers/NativeClass/index.js.map +0 -1
  421. /package/{helpers/angular → hmr/frameworks/angular/build}/inline-decorator-component-templates.d.ts +0 -0
  422. /package/{helpers/angular → hmr/frameworks/angular/build}/synthesize-decorator-ctor-parameters.d.ts +0 -0
  423. /package/{helpers/angular → hmr/frameworks/angular/build}/synthesize-injectable-factories.d.ts +0 -0
  424. /package/hmr/{server → frameworks/angular/server}/websocket-angular-entry.d.ts +0 -0
@@ -5,9 +5,11 @@
5
5
  * Always resolve core classes and Application from the vendor realm or globalThis at runtime.
6
6
  * The HMR client is evaluated via HTTP ESM on device; static imports would create secondary instances.
7
7
  */
8
- import { setHMRWsUrl, getHMRWsUrl, pendingModuleFetches, deriveHttpOrigin, setHttpOriginForVite, moduleFetchCache, requestModuleFromServer, getHttpOriginForVite, normalizeSpec, hmrMetrics, graph, setGraphVersion, getGraphVersion, getCurrentApp, getRootFrame, setCurrentApp, setRootFrame, getCore } from './utils.js';
8
+ import { setHMRWsUrl, getHMRWsUrl, pendingModuleFetches, deriveHttpOrigin, setHttpOriginForVite, moduleFetchCache, requestModuleFromServer, getHttpOriginForVite, normalizeSpec, hmrMetrics, graph, setGraphVersion, getGraphVersion, getCurrentApp, getRootFrame, setCurrentApp, setRootFrame, getCore, hasExplicitEviction, invalidateModulesByUrls, buildEvictionUrls, emitHmrModeBannerOnce, ENV_VERBOSE } from './utils.js';
9
9
  import { handleCssUpdates } from './css-handler.js';
10
- const VERBOSE = typeof __NS_ENV_VERBOSE__ !== 'undefined' && __NS_ENV_VERBOSE__;
10
+ import { buildCssApplyingDetail, buildCssAppliedDetail } from './css-update-overlay.js';
11
+ import { getGlobalScope } from '../shared/runtime/global-scope.js';
12
+ const VERBOSE = ENV_VERBOSE;
11
13
  function resolveTargetFlavor() {
12
14
  try {
13
15
  if (typeof __NS_TARGET_FLAVOR__ !== 'undefined' && __NS_TARGET_FLAVOR__) {
@@ -16,7 +18,7 @@ function resolveTargetFlavor() {
16
18
  }
17
19
  catch { }
18
20
  try {
19
- const g = globalThis;
21
+ const g = getGlobalScope();
20
22
  if (typeof g.__NS_TARGET_FLAVOR__ === 'string' && g.__NS_TARGET_FLAVOR__) {
21
23
  return g.__NS_TARGET_FLAVOR__;
22
24
  }
@@ -34,13 +36,24 @@ function resolveTargetFlavor() {
34
36
  return undefined;
35
37
  }
36
38
  const TARGET_FLAVOR = resolveTargetFlavor();
39
+ // React reuses the generic TypeScript HMR path on BOTH server and client: it has
40
+ // no Fast Refresh, so a module edit drives a plain module reload / root reset
41
+ // (the React tree re-renders), exactly like the `typescript` flavor. The server
42
+ // strategy is `{ ...typescriptServerStrategy, flavor: 'react' }`; this mirrors that
43
+ // on the client so the `typescript`-gated update branches also run for React
44
+ // (otherwise a React edit is received but never applied — the overlay sticks).
45
+ const TS_LIKE_FLAVOR = TARGET_FLAVOR === 'typescript' || TARGET_FLAVOR === 'react';
37
46
  try {
38
47
  if (TARGET_FLAVOR && !globalThis.__NS_TARGET_FLAVOR__) {
39
48
  globalThis.__NS_TARGET_FLAVOR__ = TARGET_FLAVOR;
40
49
  }
41
50
  }
42
51
  catch { }
43
- const APP_ROOT_VIRTUAL = typeof __NS_APP_ROOT_VIRTUAL__ === 'string' && __NS_APP_ROOT_VIRTUAL__ ? __NS_APP_ROOT_VIRTUAL__ : '/src';
52
+ // Define substitution does NOT reach this file (served raw from node_modules),
53
+ // so prefer the globalThis seed planted by the entry's defines-seed module —
54
+ // the '/src' literal is a last-resort default and is WRONG for 'app/'-rooted
55
+ // projects.
56
+ const APP_ROOT_VIRTUAL = (typeof __NS_APP_ROOT_VIRTUAL__ === 'string' && __NS_APP_ROOT_VIRTUAL__) || (typeof getGlobalScope().__NS_APP_ROOT_VIRTUAL__ === 'string' && getGlobalScope().__NS_APP_ROOT_VIRTUAL__) || '/src';
44
57
  const APP_VIRTUAL_WITH_SLASH = APP_ROOT_VIRTUAL.endsWith('/') ? APP_ROOT_VIRTUAL : `${APP_ROOT_VIRTUAL}/`;
45
58
  const APP_MAIN_ENTRY_SPEC = `${APP_VIRTUAL_WITH_SLASH}app.ts`;
46
59
  // Policy: by default, let the app's own main entry mount initially; HMR client handles updates/remounts only.
@@ -101,12 +114,101 @@ function hideConnectionOverlay() {
101
114
  }
102
115
  catch { }
103
116
  }
117
+ function setUpdateOverlayStage(stage, info) {
118
+ try {
119
+ const api = getHmrOverlayApi();
120
+ if (api && typeof api.setUpdateStage === 'function') {
121
+ api.setUpdateStage(stage, info);
122
+ }
123
+ }
124
+ catch { }
125
+ }
126
+ // Store the listener registry on globalThis (rather than in a module-private
127
+ // closure) because in NativeScript the HMR client module and the user app
128
+ // modules can resolve to different module instances depending on how the
129
+ // dev runtime loads them (HTTP client URL vs. the bundled vendor realm).
130
+ // A module-local Set would not be shared across instances; the global one
131
+ // is.
132
+ function getNsSolidHmrListenerSet() {
133
+ const g = getGlobalScope();
134
+ let set = g.__ns_solid_hmr_listener_set;
135
+ if (!set) {
136
+ set = new Set();
137
+ g.__ns_solid_hmr_listener_set = set;
138
+ }
139
+ return set;
140
+ }
141
+ function nsSolidHmrSubscribe(fn) {
142
+ const listeners = getNsSolidHmrListenerSet();
143
+ listeners.add(fn);
144
+ if (VERBOSE)
145
+ console.log('[hmr][solid] subscribe — listeners=', listeners.size);
146
+ return () => listeners.delete(fn);
147
+ }
148
+ function nsSolidHmrEmit(ev) {
149
+ const listeners = getNsSolidHmrListenerSet();
150
+ if (VERBOSE)
151
+ console.log('[hmr][solid] emit listeners=', listeners.size, 'changedFiles=', ev.changedFiles);
152
+ for (const fn of Array.from(listeners)) {
153
+ try {
154
+ fn(ev);
155
+ }
156
+ catch (err) {
157
+ if (VERBOSE)
158
+ console.warn('[hmr][solid] listener threw', err);
159
+ }
160
+ }
161
+ }
162
+ try {
163
+ const g = getGlobalScope();
164
+ g.__ns_solid_hmr_subscribe = nsSolidHmrSubscribe;
165
+ // Eagerly create the listener set so the global exists at module load time.
166
+ getNsSolidHmrListenerSet();
167
+ if (VERBOSE)
168
+ console.log('[hmr][solid] HMR client loaded. global set=', typeof g.__ns_solid_hmr_subscribe, 'listenerSet=', typeof g.__ns_solid_hmr_listener_set);
169
+ }
170
+ catch (err) {
171
+ console.warn('[hmr][solid] could not install global __ns_solid_hmr_subscribe', err);
172
+ }
173
+ // Eagerly drive the HMR-applying overlay's 'received' frame as soon
174
+ // as the server emits `ns:hmr-pending`, BEFORE the framework-specific
175
+ // (`ns:angular-update` / `ns:css-updates`) payload arrives. The
176
+ // flavor-specific handler later walks through 'evicting' →
177
+ // 'reimporting' → 'rebooting' → 'complete'. Calling 'received' twice
178
+ // in the same cycle is safe: the overlay preserves
179
+ // `updateCycleStartedAt` when a 'received' frame replaces an existing
180
+ // 'received' frame so the minimum-visible window is still timed
181
+ // against the FIRST frame.
182
+ //
183
+ // Soft-fails when the overlay isn't installed (production builds,
184
+ // vitest, etc.) or when the user opted out via
185
+ // `__NS_HMR_PROGRESS_OVERLAY_ENABLED__ === false`.
186
+ import { applyHmrPendingFrame } from './hmr-pending-overlay.js';
187
+ function setHmrPendingOverlay(filePath) {
188
+ applyHmrPendingFrame(filePath, { getOverlay: getHmrOverlayApi });
189
+ }
104
190
  let connectionOverlayTimer = null;
105
191
  let connectionOverlayVisible = false;
106
192
  let hasOpenedHmrSocket = false;
107
193
  let awaitingHealthyHmrMessage = false;
108
194
  let pendingConnectionOverlayStage = 'connecting';
109
195
  let pendingConnectionOverlayDetail = '';
196
+ // The stage currently PAINTED on screen (set only when we actually show
197
+ // the overlay, not when we merely schedule it). Used to suppress the
198
+ // sub-perceptible offline↔reconnecting flicker during the reconnect
199
+ // retry loop — see the deferral in `connectHmr`/`tryNext`.
200
+ let shownConnectionOverlayStage = null;
201
+ // While the terminal 'offline' frame is showing, a fresh reconnect
202
+ // attempt that is refused near-instantly (the norm when the dev server
203
+ // is down — especially on the Android emulator, where connection-refused
204
+ // returns in ~1ms) would flip the overlay to 'reconnecting' and straight
205
+ // back to 'offline', producing a jarring 1-frame flicker. We defer the
206
+ // 'reconnecting' frame by this much so an instant failure (→ back to
207
+ // 'offline') or a success (→ 'synchronizing') cancels it before it ever
208
+ // paints; only a genuinely in-flight attempt surfaces 'reconnecting'.
209
+ // On iOS real connect attempts exceed this threshold, so the behaviour
210
+ // there is unchanged.
211
+ const RECONNECTING_OVER_OFFLINE_DELAY_MS = 500;
110
212
  function clearConnectionOverlayTimer() {
111
213
  if (connectionOverlayTimer) {
112
214
  clearTimeout(connectionOverlayTimer);
@@ -114,9 +216,14 @@ function clearConnectionOverlayTimer() {
114
216
  }
115
217
  }
116
218
  function showConnectionOverlayNow(stage, detail) {
219
+ // Painting a stage now supersedes any scheduled (deferred) stage, so
220
+ // cancel the pending timer — otherwise a deferred 'reconnecting' could
221
+ // fire moments after we settle back on 'offline' and revive the flicker.
222
+ clearConnectionOverlayTimer();
117
223
  pendingConnectionOverlayStage = stage;
118
224
  pendingConnectionOverlayDetail = detail || '';
119
225
  connectionOverlayVisible = true;
226
+ shownConnectionOverlayStage = stage;
120
227
  setConnectionOverlayStage(stage, detail);
121
228
  }
122
229
  function scheduleConnectionOverlay(stage, detail, delayMs = 1200) {
@@ -137,24 +244,25 @@ function updateConnectionOverlay(stage, detail) {
137
244
  function markHmrConnectionHealthy() {
138
245
  awaitingHealthyHmrMessage = false;
139
246
  clearConnectionOverlayTimer();
247
+ shownConnectionOverlayStage = null;
140
248
  if (connectionOverlayVisible) {
141
249
  connectionOverlayVisible = false;
142
250
  hideConnectionOverlay();
143
251
  }
144
252
  }
145
- /**
146
- * Flavor hooks
147
- */
148
- import { installNsVueDevShims, ensureBackWrapperInstalled, getRootForVue, loadSfcComponent, ensureVueGlobals, ensurePiniaOnApp, addSfcMapping, recordVuePayloadChanges, handleVueSfcRegistry, handleVueSfcRegistryUpdate, sfcArtifactMap } from '../frameworks/vue/client/index.js';
149
- import { handleAngularHotUpdateMessage, installAngularHmrClientHooks } from '../frameworks/angular/client/index.js';
150
- switch (TARGET_FLAVOR) {
151
- case 'vue':
152
- installNsVueDevShims();
153
- break;
154
- case 'angular':
155
- installAngularHmrClientHooks();
156
- break;
157
- }
253
+ let CLIENT_STRATEGY;
254
+ const CLIENT_STRATEGY_READY = TARGET_FLAVOR === 'vue' || TARGET_FLAVOR === 'angular'
255
+ ? import(`../frameworks/${TARGET_FLAVOR}/client/strategy.js`)
256
+ .then((mod) => {
257
+ CLIENT_STRATEGY = mod && mod[`${TARGET_FLAVOR}ClientStrategy`];
258
+ if (VERBOSE)
259
+ console.log('[hmr-client] client strategy loaded for flavor:', TARGET_FLAVOR);
260
+ CLIENT_STRATEGY?.install();
261
+ })
262
+ .catch((err) => {
263
+ console.warn('[hmr-client] failed to load client strategy for', TARGET_FLAVOR, err);
264
+ })
265
+ : Promise.resolve();
158
266
  // Track whether we've mounted an initial app root yet in HTTP-only boot
159
267
  let initialMounted = !!globalThis.__NS_HMR_BOOT_COMPLETE__;
160
268
  // Prevent duplicate initial-mount scheduling across rapid full-graph broadcasts and re-evaluations
@@ -172,7 +280,7 @@ let processingPromise = null;
172
280
  // Detect whether the early placeholder root is still active on screen
173
281
  function isPlaceholderActive() {
174
282
  try {
175
- const g = globalThis;
283
+ const g = getGlobalScope();
176
284
  if (g.__NS_DEV_PLACEHOLDER_ROOT_VIEW__)
177
285
  return true;
178
286
  if (g.__NS_DEV_PLACEHOLDER_ROOT_EARLY__)
@@ -202,9 +310,9 @@ function applyFullGraph(payload) {
202
310
  // causes a double-mount race (rescue fires at 450ms, then main.ts fires ~1s later,
203
311
  // causing a visual flash and leaving the app in an inconsistent state).
204
312
  try {
205
- const g = globalThis;
313
+ const g = getGlobalScope();
206
314
  const bootDone = !!g.__NS_HMR_BOOT_COMPLETE__;
207
- if (!bootDone && !initialMounted && !initialMounting && !g.__NS_HMR_RESCUE_SCHEDULED__ && TARGET_FLAVOR !== 'typescript') {
315
+ if (!bootDone && !initialMounted && !initialMounting && !g.__NS_HMR_RESCUE_SCHEDULED__ && !TS_LIKE_FLAVOR) {
208
316
  // simple snapshot helpers
209
317
  const getTopmost = () => {
210
318
  try {
@@ -277,7 +385,7 @@ function applyFullGraph(payload) {
277
385
  if (VERBOSE)
278
386
  console.log('[hmr][init] placeholder persists after delay; evaluating rescue policy');
279
387
  // Flavor-specific rescue handling
280
- if (TARGET_FLAVOR === 'typescript') {
388
+ if (TS_LIKE_FLAVOR) {
281
389
  // For TS apps, perform a one-time resetRootView to the conventional
282
390
  // app root module. This mimics what Application.run would do and
283
391
  // replaces the placeholder with the real UI without trying to
@@ -317,39 +425,7 @@ function applyFullGraph(payload) {
317
425
  }
318
426
  return;
319
427
  }
320
- let candidate = null;
321
- switch (TARGET_FLAVOR) {
322
- case 'vue': {
323
- const appEntry = graph.get(APP_MAIN_ENTRY_SPEC);
324
- if (appEntry && Array.isArray(appEntry.deps)) {
325
- const vueDep = appEntry.deps.find((d) => typeof d === 'string' && /\.vue$/i.test(d));
326
- if (vueDep)
327
- candidate = vueDep;
328
- }
329
- if (!candidate) {
330
- for (const id of graph.keys()) {
331
- if (/\.vue$/i.test(id)) {
332
- candidate = id;
333
- break;
334
- }
335
- }
336
- }
337
- // Fallback: when the module graph is empty (Vite 7+ may not populate it
338
- // before the first full-graph broadcast), check the SFC artifact registry
339
- // which is populated from the ns:vue-sfc-registry message.
340
- if (!candidate && sfcArtifactMap.size > 0) {
341
- for (const id of sfcArtifactMap.keys()) {
342
- if (/\.vue$/i.test(id)) {
343
- candidate = id;
344
- if (VERBOSE)
345
- console.log('[hmr][init] rescue candidate from SFC registry:', id);
346
- break;
347
- }
348
- }
349
- }
350
- break;
351
- }
352
- }
428
+ const candidate = CLIENT_STRATEGY?.selectMountCandidate?.({ graph, appMainEntrySpec: APP_MAIN_ENTRY_SPEC }) ?? null;
353
429
  if (!candidate)
354
430
  return;
355
431
  initialMounting = true;
@@ -360,10 +436,8 @@ function applyFullGraph(payload) {
360
436
  (async () => {
361
437
  try {
362
438
  let comp = null;
363
- switch (TARGET_FLAVOR) {
364
- case 'vue':
365
- comp = await loadSfcComponent(candidate, 'initial_mount_rescue');
366
- break;
439
+ if (CLIENT_STRATEGY?.loadComponentForMount) {
440
+ comp = await CLIENT_STRATEGY.loadComponentForMount(candidate, 'initial_mount_rescue');
367
441
  }
368
442
  if (!comp)
369
443
  return;
@@ -404,41 +478,11 @@ function applyFullGraph(payload) {
404
478
  // Only allow initial mount when explicitly enabled. Rely on the app's own main entry start() for the first mount
405
479
  // to avoid double-mount races that can cause duplicate navigation logs.
406
480
  if (ALLOW_INITIAL_MOUNT && !initialMounted && !bootDone && !bootInProgress && !getCurrentApp() && !getRootFrame()) {
407
- let candidate = null;
408
- switch (TARGET_FLAVOR) {
409
- case 'vue': {
410
- const appEntry = graph.get(APP_MAIN_ENTRY_SPEC);
411
- if (appEntry && Array.isArray(appEntry.deps)) {
412
- const vueDep = appEntry.deps.find((d) => typeof d === 'string' && /\.vue$/i.test(d));
413
- if (vueDep)
414
- candidate = vueDep;
415
- }
416
- if (!candidate) {
417
- for (const id of graph.keys()) {
418
- if (/\.vue$/i.test(id)) {
419
- candidate = id;
420
- break;
421
- }
422
- }
423
- }
424
- // Fallback: SFC registry (same as rescue mount above)
425
- if (!candidate && sfcArtifactMap.size > 0) {
426
- for (const id of sfcArtifactMap.keys()) {
427
- if (/\.vue$/i.test(id)) {
428
- candidate = id;
429
- if (VERBOSE)
430
- console.log('[hmr][init] initial mount candidate from SFC registry:', id);
431
- break;
432
- }
433
- }
434
- }
435
- break;
436
- }
437
- case 'typescript': {
438
- // For TS flavor, do not perform client-driven initial mount; rely on Application.run.
439
- return;
440
- }
481
+ if (TS_LIKE_FLAVOR) {
482
+ // For TS flavor, do not perform client-driven initial mount; rely on Application.run.
483
+ return;
441
484
  }
485
+ const candidate = CLIENT_STRATEGY?.selectMountCandidate?.({ graph, appMainEntrySpec: APP_MAIN_ENTRY_SPEC }) ?? null;
442
486
  if (candidate) {
443
487
  // Mark initial-mount in progress (both module-local and global) BEFORE scheduling async work
444
488
  initialMounting = true;
@@ -452,7 +496,7 @@ function applyFullGraph(payload) {
452
496
  console.log('[hmr][init] mounting initial root from', candidate, 'flavor=', TARGET_FLAVOR);
453
497
  // Android-only: avoid racing entry-runtime reset and Activity bring-up
454
498
  try {
455
- const g = globalThis;
499
+ const g = getGlobalScope();
456
500
  const App = getCore('Application') || g.Application;
457
501
  const isAndroid = !!(App && App.android !== undefined);
458
502
  if (isAndroid) {
@@ -483,21 +527,19 @@ function applyFullGraph(payload) {
483
527
  }
484
528
  catch { }
485
529
  let comp = null;
486
- switch (TARGET_FLAVOR) {
487
- case 'vue':
488
- comp = await loadSfcComponent(candidate, 'initial_mount');
489
- break;
490
- case 'typescript':
491
- try {
492
- const url = await requestModuleFromServer(candidate);
493
- const mod = await import(/* @vite-ignore */ url);
494
- comp = mod && (mod.default || mod);
495
- }
496
- catch (e) {
497
- if (VERBOSE)
498
- console.warn('[hmr][init] TS initial mount failed to import', candidate, e);
499
- }
500
- break;
530
+ if (TS_LIKE_FLAVOR) {
531
+ try {
532
+ const url = await requestModuleFromServer(candidate);
533
+ const mod = await import(/* @vite-ignore */ url);
534
+ comp = mod && (mod.default || mod);
535
+ }
536
+ catch (e) {
537
+ if (VERBOSE)
538
+ console.warn('[hmr][init] TS initial mount failed to import', candidate, e);
539
+ }
540
+ }
541
+ else if (CLIENT_STRATEGY?.loadComponentForMount) {
542
+ comp = await CLIENT_STRATEGY.loadComponentForMount(candidate, 'initial_mount');
501
543
  }
502
544
  if (comp) {
503
545
  const ok = await performResetRoot(comp);
@@ -558,11 +600,7 @@ function applyDelta(payload) {
558
600
  setGraphVersion(payload.newVersion);
559
601
  }
560
602
  const changed = payload.changed || [];
561
- switch (TARGET_FLAVOR) {
562
- case 'vue':
563
- recordVuePayloadChanges(changed, getGraphVersion());
564
- break;
565
- }
603
+ CLIENT_STRATEGY?.recordPayloadChanges?.(changed, getGraphVersion());
566
604
  (payload.changed || []).forEach((m) => {
567
605
  if (!m || !m.id)
568
606
  return;
@@ -629,7 +667,7 @@ function applyDelta(payload) {
629
667
  }
630
668
  if (isAppMainEntryId(id)) {
631
669
  try {
632
- const exists = globalThis.require?.(id) || globalThis.__nsGetModuleExports?.(id);
670
+ const exists = getGlobalScope().require?.(id) || globalThis.__nsGetModuleExports?.(id);
633
671
  if (!exists && VERBOSE)
634
672
  console.log(`[hmr][delta] skipping unresolved ${APP_MAIN_ENTRY_SPEC} change`);
635
673
  if (!exists)
@@ -645,14 +683,10 @@ function applyDelta(payload) {
645
683
  processQueue();
646
684
  }
647
685
  }
648
- // Deterministic navigation using the current Vue app instance rather than vendor-held rootApp
686
+ // Deterministic navigation using the current Vue app instance rather than vendor-held rootApp.
649
687
  function __nsNavigateUsingApp(comp, opts = {}) {
650
- const g = globalThis;
651
- switch (TARGET_FLAVOR) {
652
- case 'vue':
653
- ensureVueGlobals();
654
- break;
655
- }
688
+ const g = getGlobalScope();
689
+ CLIENT_STRATEGY?.beforeNavigateBuild?.();
656
690
  const AppFactory = g.createApp;
657
691
  const RootCtor = g.NSVRoot;
658
692
  if (typeof AppFactory !== 'function' || typeof RootCtor !== 'function') {
@@ -675,12 +709,14 @@ function __nsNavigateUsingApp(comp, opts = {}) {
675
709
  const buildTarget = () => {
676
710
  const existingApp = getCurrentApp();
677
711
  const baseProvides = (existingApp && existingApp._context && existingApp._context.provides) || {};
678
- const app = AppFactory(normalizeComponent(comp, comp && (comp.__name || comp.name)));
679
- switch (TARGET_FLAVOR) {
680
- case 'vue':
681
- ensurePiniaOnApp(app);
682
- break;
683
- }
712
+ // Forward `opts.props` as Vue's rootProps so `$navigateTo(Comp, { props: { } })`
713
+ // reaches the destination component. nativescript-vue's stock `$navigateTo`
714
+ // does the same via `createNativeView(target, options?.props, …)` →
715
+ // `renderer.createApp(component, props)`. Dropping props here would surface
716
+ // at the destination as `[Vue warn]: Missing required prop` and any
717
+ // required-prop component would render with `undefined` bindings.
718
+ const app = AppFactory(normalizeComponent(comp, comp && (comp.__name || comp.name)), opts && opts.props);
719
+ CLIENT_STRATEGY?.onNavAppCreated?.(app);
684
720
  try {
685
721
  const registry = g.__nsVendorRegistry;
686
722
  const req = registry?.get ? g.__nsVendorRequire || g.__nsRequire || g.require : g.__nsRequire || g.require;
@@ -775,6 +811,184 @@ try {
775
811
  globalThis.__nsNavigateUsingApp = __nsNavigateUsingApp;
776
812
  }
777
813
  catch { }
814
+ const openModalRecords = [];
815
+ let modalTrackingInstalled = false;
816
+ /**
817
+ * Map a served/graph module id (e.g. `/app/modal-page.xml`) to its app-root
818
+ * relative path (`modal-page.xml`). Single mapping point — the raw-asset
819
+ * re-registration, page-navigation targets, and modal matching all derive
820
+ * from this; keep them in sync by construction.
821
+ */
822
+ function toAppRelativePath(id) {
823
+ try {
824
+ const spec = normalizeSpec(id);
825
+ const appVirtual = APP_VIRTUAL_WITH_SLASH.replace(/^\//, '');
826
+ let relPath = spec.startsWith('/') ? spec.slice(1) : spec;
827
+ if (relPath.startsWith(appVirtual))
828
+ relPath = relPath.slice(appVirtual.length);
829
+ return relPath || null;
830
+ }
831
+ catch {
832
+ return null;
833
+ }
834
+ }
835
+ /** App-root relative module name (no extension) for page-shaped files, else null. */
836
+ function toAppModuleName(id) {
837
+ const relPath = toAppRelativePath(id);
838
+ if (!relPath || !/\.(xml|ts|js)$/i.test(relPath))
839
+ return null;
840
+ return relPath.replace(/\.(xml|ts|js)$/i, '');
841
+ }
842
+ function ensureModalTracking() {
843
+ if (modalTrackingInstalled)
844
+ return;
845
+ try {
846
+ const View = getCore('View') || getGlobalScope().View;
847
+ const proto = View?.prototype;
848
+ if (!proto || typeof proto.showModal !== 'function')
849
+ return;
850
+ const orig = proto.showModal;
851
+ if (orig.__nsHmrModalTracked) {
852
+ modalTrackingInstalled = true;
853
+ return;
854
+ }
855
+ const wrapped = function (...args) {
856
+ const result = orig.apply(this, args);
857
+ try {
858
+ if (typeof args[0] === 'string' && result) {
859
+ // Mirror core's getModalOptions arg shapes: (moduleName, options)
860
+ // or the deprecated positional form.
861
+ const options = args.length === 2 && args[1] && typeof args[1] === 'object' ? args[1] : { context: args[1], closeCallback: args[2], fullscreen: args[3], animated: args[4], stretched: args[5] };
862
+ const moduleName = String(args[0])
863
+ .replace(/^\.\//, '')
864
+ .replace(/\.(xml|ts|js)$/i, '');
865
+ openModalRecords.push({ moduleName, options, parent: this, modal: result });
866
+ if (VERBOSE)
867
+ console.log('[hmr][modal] tracked open modal', moduleName);
868
+ }
869
+ }
870
+ catch { }
871
+ return result;
872
+ };
873
+ wrapped.__nsHmrModalTracked = true;
874
+ proto.showModal = wrapped;
875
+ modalTrackingInstalled = true;
876
+ }
877
+ catch (e) {
878
+ if (VERBOSE)
879
+ console.warn('[hmr][modal] tracking install failed', e);
880
+ }
881
+ }
882
+ /**
883
+ * Enumerate the modals that are currently presented AND were opened by module
884
+ * name, with everything needed to re-present them.
885
+ *
886
+ * Source of truth is core's live modal stack (`_getRootModalViews()`):
887
+ * - `modal._moduleName` — set by the Builder on every createViewFromEntry
888
+ * view (longstanding, used by livesync), so available on any core.
889
+ * - `modal._modalOptions` — the original ShowModalOptions, stored by core's
890
+ * `_showNativeModalView` (newer cores). For older cores the showModal
891
+ * wrap's records (see ensureModalTracking) fill the gap.
892
+ * - `modal._modalParent` — the presenting view.
893
+ * Stale wrap records are pruned against the live stack while we're here.
894
+ */
895
+ function getOpenStringModuleModals() {
896
+ const out = [];
897
+ try {
898
+ const App = getCore('Application');
899
+ const root = App?.getRootView?.() || App?._rootView;
900
+ const stack = root?._getRootModalViews?.() || [];
901
+ // Prune wrap records whose modal is gone (keeps the fallback list small).
902
+ for (let i = openModalRecords.length - 1; i >= 0; i--) {
903
+ if (!stack.includes(openModalRecords[i].modal)) {
904
+ openModalRecords.splice(i, 1);
905
+ }
906
+ }
907
+ for (const modal of stack) {
908
+ const record = openModalRecords.find((r) => r.modal === modal);
909
+ const rawModuleName = typeof modal?._moduleName === 'string' && modal._moduleName ? modal._moduleName : record?.moduleName;
910
+ const parent = modal?._modalParent || record?.parent;
911
+ const options = modal?._modalOptions || record?.options;
912
+ if (!rawModuleName || !parent)
913
+ continue;
914
+ const moduleName = String(rawModuleName)
915
+ .replace(/^\.\//, '')
916
+ .replace(/\.(xml|ts|js)$/i, '');
917
+ out.push({ moduleName, options, parent, modal });
918
+ }
919
+ }
920
+ catch (e) {
921
+ if (VERBOSE)
922
+ console.warn('[hmr][modal] open-modal enumeration failed', e);
923
+ }
924
+ return out;
925
+ }
926
+ /**
927
+ * Close and re-present an open modal so it rebuilds from the freshly
928
+ * re-registered XML/code-behind. Core clears the modal stack synchronously on
929
+ * close but the NATIVE dismissal completes asynchronously; iOS refuses a
930
+ * present while a dismissal is in flight. Newer cores fire `closedModally` on
931
+ * the modal at exactly that completion point — preferred signal. Older cores
932
+ * fall back to polling `isLoaded` (flipped by `_tearDownUI` in the same
933
+ * completion callback).
934
+ */
935
+ async function reshowOpenModal(record) {
936
+ const { parent, modal, moduleName, options } = record;
937
+ await new Promise((resolve) => {
938
+ let settled = false;
939
+ const finish = () => {
940
+ if (!settled) {
941
+ settled = true;
942
+ resolve();
943
+ }
944
+ };
945
+ let eventArmed = false;
946
+ try {
947
+ if (typeof modal.once === 'function') {
948
+ modal.once('closedModally', finish);
949
+ eventArmed = true;
950
+ }
951
+ }
952
+ catch { }
953
+ // Poll fallback (also the safety net if the event never fires —
954
+ // e.g. an interactive-dismiss cancellation).
955
+ const deadline = Date.now() + 2000;
956
+ const poll = () => {
957
+ if (settled)
958
+ return;
959
+ let stillLoaded = false;
960
+ try {
961
+ stillLoaded = !!modal.isLoaded;
962
+ }
963
+ catch { }
964
+ if ((!eventArmed && !stillLoaded) || Date.now() > deadline) {
965
+ finish();
966
+ return;
967
+ }
968
+ setTimeout(poll, 50);
969
+ };
970
+ setTimeout(poll, 50);
971
+ try {
972
+ modal.closeModal();
973
+ }
974
+ catch (e) {
975
+ if (VERBOSE)
976
+ console.warn('[hmr][modal] close failed for', moduleName, e);
977
+ finish();
978
+ }
979
+ });
980
+ // One settle beat so the platform finishes releasing the presentation
981
+ // before the new present begins.
982
+ await new Promise((resolve) => setTimeout(resolve, 100));
983
+ try {
984
+ parent.showModal(moduleName, { ...(options || {}), animated: false });
985
+ if (VERBOSE)
986
+ console.log('[hmr][modal] re-presented', moduleName);
987
+ }
988
+ catch (e) {
989
+ console.warn('[hmr][modal] re-present failed for', moduleName, e);
990
+ }
991
+ }
778
992
  async function processQueue() {
779
993
  if (!globalThis.__NS_HMR_BOOT_COMPLETE__) {
780
994
  if (VERBOSE)
@@ -810,7 +1024,43 @@ async function processQueue() {
810
1024
  return;
811
1025
  if (VERBOSE)
812
1026
  console.log('[hmr][queue] processing changed ids', drained);
1027
+ // Track wall-clock so the 'complete' frame can show a meaningful
1028
+ // total. Only the Solid + TypeScript flavors drive the overlay
1029
+ // from here; Angular has its own flow inside
1030
+ // `frameworks/angular/client/index.ts`.
1031
+ const tQueueStart = Date.now();
1032
+ const driveSolidOverlay = TARGET_FLAVOR === 'solid';
1033
+ // Explicit eviction step.
1034
+ //
1035
+ // On modern runtimes the URL canonicalizer collapses any
1036
+ // `__ns_hmr__/<tag>/` segment back to a stable cache key, so
1037
+ // without explicit eviction the upcoming `import(url)` would
1038
+ // resolve via V8's `g_moduleRegistry` and return the cached
1039
+ // stale module — making the queue drain a silent no-op for
1040
+ // every save after the first.
1041
+ //
1042
+ // We hand the canonical eviction URLs to the runtime first;
1043
+ // `invalidateModulesByUrls` is a no-op on older runtimes and
1044
+ // `requestModuleFromServer` automatically falls back to the
1045
+ // legacy `/ns/m/__ns_hmr__/v<N>/` URL versioning path in that
1046
+ // case. node_modules and virtual specs are filtered out by
1047
+ // `buildEvictionUrls` so vendor modules stay hot.
1048
+ if (driveSolidOverlay) {
1049
+ setUpdateOverlayStage('evicting', {
1050
+ detail: drained.length === 1 ? `Invalidating ${drained[0]}` : `Invalidating ${drained.length} modules`,
1051
+ });
1052
+ }
1053
+ const evictUrls = buildEvictionUrls(drained);
1054
+ const evicted = invalidateModulesByUrls(evictUrls);
1055
+ if (VERBOSE)
1056
+ console.log(`[hmr][queue] eviction count=${evictUrls.length} ok=${evicted}`);
813
1057
  // Evaluate changed modules best-effort; failures shouldn't completely break HMR.
1058
+ if (driveSolidOverlay) {
1059
+ setUpdateOverlayStage('reimporting', {
1060
+ detail: drained.length === 1 ? `Re-importing ${drained[0]}` : `Re-importing ${drained.length} modules`,
1061
+ });
1062
+ }
1063
+ let reimportFailures = 0;
814
1064
  for (const id of drained) {
815
1065
  try {
816
1066
  const spec = normalizeSpec(id);
@@ -820,18 +1070,71 @@ async function processQueue() {
820
1070
  if (VERBOSE)
821
1071
  console.log('[hmr][queue] re-import', { id, spec, url });
822
1072
  const mod = await import(/* @vite-ignore */ url);
1073
+ // TS/XML flavor: refresh the bundler module registry with the fresh
1074
+ // exports so Builder.createViewFromEntry / loadModule('<page>')
1075
+ // resolves the NEW code-behind (tap handlers, page events) instead
1076
+ // of the stale module captured in the boot bundle. Without this,
1077
+ // XML re-renders pick up new markup but keep old behavior.
1078
+ if (TS_LIKE_FLAVOR && mod && /\.(ts|js)$/i.test(id)) {
1079
+ try {
1080
+ const g = getGlobalScope();
1081
+ const moduleName = toAppModuleName(id);
1082
+ if (moduleName && typeof g.registerModule === 'function') {
1083
+ g.registerModule(moduleName, () => mod);
1084
+ g.registerModule('./' + moduleName, () => mod);
1085
+ if (VERBOSE)
1086
+ console.log('[hmr][queue] re-registered code-behind', moduleName);
1087
+ }
1088
+ }
1089
+ catch (e) {
1090
+ if (VERBOSE)
1091
+ console.warn('[hmr][queue] code-behind re-register failed for', id, e);
1092
+ }
1093
+ }
823
1094
  }
824
1095
  catch (e) {
825
- if (VERBOSE)
826
- console.warn('[hmr][queue] re-import failed for', id, e);
1096
+ // Never silent: a failed re-import means the device may keep
1097
+ // running the previous module body, so always surface it (not
1098
+ // verbose-gated) — otherwise the overlay reports success while
1099
+ // the app runs stale code.
1100
+ reimportFailures++;
1101
+ console.warn('[hmr][queue] re-import FAILED for', id, '-', e?.message ?? e);
827
1102
  }
828
1103
  }
1104
+ if (reimportFailures > 0) {
1105
+ console.warn(`[hmr][queue] ${reimportFailures}/${drained.length} module(s) failed to re-import; the applied update may be incomplete.`);
1106
+ }
829
1107
  // After evaluating the batch, perform flavor-specific UI refresh.
830
1108
  switch (TARGET_FLAVOR) {
831
1109
  case 'vue':
832
- // Vue SFCs are handled via the registry update path; nothing to do here.
1110
+ // graph + performResetRoot + getOverlay let the Vue strategy
1111
+ // propagate non-SFC dep changes to the nearest `.vue` boundary
1112
+ // and remount it (see `propagateDepChangeToSfcBoundary`).
1113
+ await CLIENT_STRATEGY?.refreshAfterBatch?.(drained, { setUpdateOverlayStage, startedAt: tQueueStart, graph, performResetRoot, getOverlay: getHmrOverlayApi });
1114
+ break;
1115
+ case 'react': {
1116
+ // React has no Fast Refresh, and (like Solid) mounts a dominative
1117
+ // document root via `startReactApp` rather than an `app-root`
1118
+ // module — so the TypeScript `resetRootView({moduleName:'app-root'})`
1119
+ // path doesn't apply. The changed modules were already re-imported
1120
+ // into the device cache above; the per-page remount is driven by the
1121
+ // app's `__NS_HMR_ON_UPDATE__` hook (see
1122
+ // `@nativescript/tanstack-router/react`'s `subscribeReactHmrRemount`),
1123
+ // which fires right after this queue drains. Here we only mark the
1124
+ // overlay complete so it doesn't stick at 'received' (5%).
1125
+ setUpdateOverlayStage('complete', {
1126
+ detail: `Total ${Math.max(0, Date.now() - tQueueStart)}ms`,
1127
+ });
833
1128
  break;
1129
+ }
834
1130
  case 'solid': {
1131
+ // Boundaries discovered in this HMR cycle (tsx files reachable
1132
+ // via the reverse import graph from any changed file, plus route
1133
+ // files reachable from any tsx start point). Declared at the top
1134
+ // of the case block so the emit step below can include the
1135
+ // complete set in the listener event — framework integrations
1136
+ // use it to map route boundaries → fresh component references.
1137
+ const boundaries = new Set();
835
1138
  // Solid .tsx components are self-accepting via solid-refresh's inline
836
1139
  // patchRegistry — re-importing them is sufficient. For non-component
837
1140
  // .ts utility modules, we must propagate up the import graph to find
@@ -850,8 +1153,10 @@ async function processQueue() {
850
1153
  arr.push(id);
851
1154
  }
852
1155
  }
853
- // BFS from each non-tsx changed module up to tsx/jsx boundaries
854
- const boundaries = new Set();
1156
+ // Pass 1: BFS from each non-tsx changed module up to tsx/jsx
1157
+ // boundaries. These get re-imported below so solid-refresh's
1158
+ // inline patchRegistry runs and (best-effort) swaps the proxy
1159
+ // signals for any components defined in those tsx boundaries.
855
1160
  for (const id of drained) {
856
1161
  if (/\.(tsx|jsx)$/i.test(id))
857
1162
  continue; // already self-accepting
@@ -875,6 +1180,51 @@ async function processQueue() {
875
1180
  }
876
1181
  }
877
1182
  }
1183
+ // Pass 2: walk further from any tsx starting point (a tsx file
1184
+ // in `drained` OR a tsx boundary discovered in pass 1) to find
1185
+ // route files (`/src/routes/*.{tsx,jsx}`) that transitively
1186
+ // import them. Re-importing a route file refreshes its
1187
+ // `Route.options.component` to the freshly-imported reference
1188
+ // and the existing boundary loop below patches the live router
1189
+ // with that fresh reference.
1190
+ //
1191
+ // This is the key fix for "edit home.tsx → save → no visual
1192
+ // update": the old BFS skipped tsx files in `drained` (assuming
1193
+ // solid-refresh's in-place proxy patch was sufficient), but in
1194
+ // the universal-renderer + nested-context configuration that
1195
+ // patch does not always propagate to the visible page tree.
1196
+ // Adding the route file as a boundary lets us patch
1197
+ // `route.options.component` directly to a fresh module export,
1198
+ // which the framework subscriber then passes through to the
1199
+ // page remount — making the cycle robust to the proxy patch
1200
+ // silently failing.
1201
+ const tsxStarts = new Set();
1202
+ for (const id of drained) {
1203
+ if (/\.(tsx|jsx)$/i.test(id))
1204
+ tsxStarts.add(id);
1205
+ }
1206
+ for (const b of boundaries)
1207
+ tsxStarts.add(b);
1208
+ const ROUTE_FILE_RE = /\/src\/routes\/.+\.(tsx|jsx)$/i;
1209
+ for (const start of tsxStarts) {
1210
+ const visited = new Set();
1211
+ const queue = [start];
1212
+ while (queue.length) {
1213
+ const cur = queue.shift();
1214
+ if (visited.has(cur))
1215
+ continue;
1216
+ visited.add(cur);
1217
+ if (cur !== start && ROUTE_FILE_RE.test(cur)) {
1218
+ boundaries.add(cur);
1219
+ }
1220
+ const importers = reverseIndex.get(cur);
1221
+ if (!importers)
1222
+ continue;
1223
+ for (const imp of importers) {
1224
+ queue.push(imp);
1225
+ }
1226
+ }
1227
+ }
878
1228
  // Re-import each boundary so solid-refresh patchRegistry fires.
879
1229
  // For route files (TanStack Router), capture the new Route export
880
1230
  // and patch the router's existing route with the fresh loader.
@@ -885,7 +1235,7 @@ async function processQueue() {
885
1235
  const findRouter = () => {
886
1236
  if (discoveredRouter)
887
1237
  return discoveredRouter;
888
- const g = globalThis;
1238
+ const g = getGlobalScope();
889
1239
  if (g.__ns_router?.routesById)
890
1240
  return (discoveredRouter = g.__ns_router);
891
1241
  // Fallback: scan common global keys for router
@@ -913,44 +1263,100 @@ async function processQueue() {
913
1263
  p = p.replace(/(^|\/)-([\w])/g, '$1$2');
914
1264
  return '/' + p;
915
1265
  };
916
- // Find existing route by fullPath (since new Route has no id yet)
1266
+ // Find existing route by fullPath (since new Route has no id yet).
1267
+ // The root route (`__root__`) shares fullPath '/' with the index
1268
+ // route, so prefer the non-root (leaf) match — otherwise editing
1269
+ // `index.tsx` would clobber the root layout's component instead of
1270
+ // the page's. @nativescript/tanstack-router's remount uses the same
1271
+ // preference so patch + read target the same route.
917
1272
  const findRouteByFullPath = (router, fp) => {
918
1273
  if (!router?.routesById)
919
1274
  return null;
1275
+ let rootMatch = null;
920
1276
  for (const rid of Object.keys(router.routesById)) {
921
1277
  const r = router.routesById[rid];
922
- if (r?.fullPath === fp)
923
- return r;
1278
+ if (r?.fullPath !== fp)
1279
+ continue;
1280
+ if (rid === '__root__') {
1281
+ rootMatch = rootMatch || r;
1282
+ continue;
1283
+ }
1284
+ return r;
924
1285
  }
925
- return null;
1286
+ return rootMatch;
926
1287
  };
1288
+ // A changed route file is itself a boundary to re-import + patch — Pass 2
1289
+ // only adds OTHER route files that import a changed module, never the
1290
+ // changed route file itself. Without this, editing `about.tsx` while on
1291
+ // /about never refreshes the About component.
1292
+ for (const id of drained) {
1293
+ if (ROUTE_FILE_RE.test(id))
1294
+ boundaries.add(id);
1295
+ }
1296
+ // Evict ONLY the boundary set (changed component + route files) — NOT the
1297
+ // router / route-tree / app chain. Keeping those cached preserves the live
1298
+ // router instance and the user's current route across the re-mount; fresh
1299
+ // route components reach the screen via the in-place `route.options.component`
1300
+ // patch below, not a router rebuild (which would reset to the initial route).
1301
+ const boundaryIds = Array.from(boundaries);
1302
+ const solidEvictUrls = buildEvictionUrls(boundaryIds);
1303
+ const solidEvicted = invalidateModulesByUrls(solidEvictUrls);
1304
+ if (VERBOSE)
1305
+ console.log(`[hmr][solid] eviction count=${solidEvictUrls.length} ok=${solidEvicted}`);
927
1306
  for (const id of boundaries) {
928
- if (seen.has(id))
1307
+ // Skip already-drained modules — EXCEPT route files. The main
1308
+ // drain loop above re-imports drained modules but does NOT patch
1309
+ // `route.options.component`; a changed ROUTE file (which is in
1310
+ // `drained`/`seen`) must still flow through this loop so its route
1311
+ // gets patched with the fresh component, otherwise the router's
1312
+ // per-page HMR remount re-renders the stale (one-edit-behind) one.
1313
+ if (seen.has(id) && !/\/src\/routes\/.+\.(?:tsx|jsx)$/i.test(id))
1314
+ continue;
1315
+ // Skip the root layout (`__root`): it commonly imports a global
1316
+ // stylesheet (`./styles.css?url`) with no ESM `default` export on
1317
+ // device, so re-importing it throws and aborts the cycle. The root
1318
+ // layout rarely needs a component hot-swap anyway.
1319
+ if (/\/routes\/__root\.(?:tsx|jsx|ts|js)$/i.test(id))
929
1320
  continue;
930
1321
  try {
931
1322
  const spec = normalizeSpec(id);
932
- const url = await requestModuleFromServer(spec);
1323
+ // Re-import via an entry-tagged URL so the dev server serves a
1324
+ // freshly-transformed module rather than a cached (one-edit-behind)
1325
+ // transform. This is what `route.options.component` is patched
1326
+ // from below, and what @nativescript/tanstack-router's per-page
1327
+ // HMR remount reads — it MUST be the just-saved code, not the
1328
+ // previous save. `requestModuleFromServer` (used elsewhere) can
1329
+ // return the prior transform within the cache window.
1330
+ const ftOrigin = getHttpOriginForVite() || deriveHttpOrigin(getHMRWsUrl());
1331
+ const ftNonce = `${getGraphVersion()}_${Date.now()}`;
1332
+ const url = ftOrigin ? ftOrigin + '/ns/m/__ns_hmr__/entry-' + ftNonce + (spec.startsWith('/') ? spec : '/' + spec) + '?t=' + ftNonce : await requestModuleFromServer(spec);
933
1333
  if (!url)
934
1334
  continue;
935
1335
  if (VERBOSE)
936
1336
  console.log('[hmr][solid] propagated to boundary', { id, url });
937
1337
  const mod = await import(/* @vite-ignore */ url);
938
- // Patch TanStack Router route loaders
1338
+ // Patch TanStack Router route options for any module
1339
+ // that exports a `Route`. We patch BOTH the component
1340
+ // and the loader (when present); components-only routes
1341
+ // were previously skipped because the gate required a
1342
+ // loader, which left their `options.component` pointing
1343
+ // at the stale module's exports after HMR.
939
1344
  try {
940
1345
  const newRoute = mod?.Route;
941
- if (newRoute?.options?.loader) {
1346
+ if (newRoute?.options) {
942
1347
  const router = findRouter();
943
1348
  const fullPath = boundaryToFullPath(id);
944
1349
  if (VERBOSE)
945
- console.log('[hmr][solid][diag] route patch attempt', { id, fullPath, hasRouter: !!router, routesByIdKeys: router?.routesById ? Object.keys(router.routesById) : 'none' });
1350
+ console.log('[hmr][solid][diag] route patch attempt', { id, fullPath, hasRouter: !!router, hasLoader: !!newRoute.options.loader, hasComponent: !!newRoute.options.component });
946
1351
  const existingRoute = fullPath && router ? findRouteByFullPath(router, fullPath) : null;
947
1352
  if (existingRoute?.options) {
948
- existingRoute.options.loader = newRoute.options.loader;
1353
+ if (newRoute.options.loader)
1354
+ existingRoute.options.loader = newRoute.options.loader;
949
1355
  if (newRoute.options.component)
950
1356
  existingRoute.options.component = newRoute.options.component;
951
1357
  routesPatchCount++;
952
1358
  if (VERBOSE)
953
- console.log('[hmr][solid] patched route loader', existingRoute.id, 'fullPath=', fullPath);
1359
+ console.log('[hmr][solid] patched route', existingRoute.id, 'fullPath=', fullPath);
954
1360
  }
955
1361
  else if (VERBOSE) {
956
1362
  console.log('[hmr][solid] no matching route for fullPath', fullPath);
@@ -986,6 +1392,44 @@ async function processQueue() {
986
1392
  if (VERBOSE)
987
1393
  console.warn('[hmr][solid] propagation failed', e);
988
1394
  }
1395
+ // Notify any framework integrations (e.g.
1396
+ // `@nativescript/tanstack-router`) that a Solid HMR
1397
+ // cycle has completed. They use this signal to perform
1398
+ // framework-specific UI refresh (e.g. remount the active
1399
+ // router page) when solid-refresh's own reactive
1400
+ // propagation does not reach the visible tree under
1401
+ // the current renderer/context configuration.
1402
+ //
1403
+ // Boundaries include both the directly-changed tsx files
1404
+ // AND every tsx ancestor reachable via the reverse import
1405
+ // graph (route files in particular). The framework
1406
+ // listener uses the route-file boundaries to look up the
1407
+ // freshly-patched `route.options.component` and pass it
1408
+ // through to the page remount.
1409
+ try {
1410
+ const tsxChangedInDrained = drained.filter((id) => /\.(tsx|jsx)$/i.test(id));
1411
+ const allBoundaries = Array.from(new Set([...tsxChangedInDrained, ...boundaries]));
1412
+ nsSolidHmrEmit({
1413
+ kind: 'solid',
1414
+ changedFiles: drained.slice(),
1415
+ boundaries: allBoundaries,
1416
+ });
1417
+ }
1418
+ catch (err) {
1419
+ if (VERBOSE)
1420
+ console.warn('[hmr][solid] emit failed', err);
1421
+ }
1422
+ // Tell the overlay the cycle is done. solid-refresh's
1423
+ // inline patchRegistry has already flushed the new
1424
+ // component bodies into the live tree (the `case
1425
+ // 'solid'` block above re-imports each .tsx
1426
+ // boundary), so by the time we get here the user is
1427
+ // already looking at the new render. The 'complete'
1428
+ // frame surfaces the wall-clock total and triggers
1429
+ // the overlay's auto-hide.
1430
+ setUpdateOverlayStage('complete', {
1431
+ detail: `Total ${Math.max(0, Date.now() - tQueueStart)}ms`,
1432
+ });
989
1433
  break;
990
1434
  }
991
1435
  case 'typescript': {
@@ -993,7 +1437,7 @@ async function processQueue() {
993
1437
  // This preserves the shell (Frame, ActionBar, etc.) that the app's
994
1438
  // own bootstrapping wires up via `Application.run`.
995
1439
  try {
996
- const g = globalThis;
1440
+ const g = getGlobalScope();
997
1441
  const App = getCore('Application') || g.Application;
998
1442
  if (!App || typeof App.resetRootView !== 'function') {
999
1443
  if (VERBOSE)
@@ -1019,10 +1463,9 @@ async function processQueue() {
1019
1463
  const rawContent = await resp.text();
1020
1464
  // Register under all nickname variants the module registry uses.
1021
1465
  // The bundler context registers XML as e.g., './main-page.xml' and 'main-page.xml'
1022
- const appVirtual = APP_VIRTUAL_WITH_SLASH.replace(/^\//, '');
1023
- let relPath = spec.startsWith('/') ? spec.slice(1) : spec;
1024
- if (relPath.startsWith(appVirtual))
1025
- relPath = relPath.slice(appVirtual.length);
1466
+ const relPath = toAppRelativePath(id);
1467
+ if (!relPath)
1468
+ continue;
1026
1469
  const nicknames = ['./' + relPath, relPath];
1027
1470
  // Also add without extension for CSS
1028
1471
  const extIdx = relPath.lastIndexOf('.');
@@ -1048,21 +1491,24 @@ async function processQueue() {
1048
1491
  }
1049
1492
  }
1050
1493
  }
1494
+ // Modal-aware refresh: pages currently PRESENTED AS MODALS must be
1495
+ // closed + re-presented in place — navigating the top frame to a
1496
+ // modal's page would push it as a frame page, and resetRootView
1497
+ // would dismiss the modal entirely. State comes from core's live
1498
+ // modal stack (_moduleName/_modalOptions/_modalParent); the
1499
+ // showModal wrap only backfills options on older cores.
1500
+ ensureModalTracking();
1501
+ const openModals = getOpenStringModuleModals();
1502
+ const changedModuleNames = new Set(drained.map(toAppModuleName).filter(Boolean));
1503
+ const modalsToReshow = openModals.filter((record) => changedModuleNames.has(record.moduleName));
1504
+ const reshowModuleNames = new Set(modalsToReshow.map((record) => record.moduleName));
1051
1505
  // Determine if we can navigate in-place to a changed page
1052
1506
  // instead of resetting all the way back to app-root.
1053
1507
  // This keeps the user on the page they're editing for faster iteration.
1054
1508
  const changedXmlPages = drained
1055
1509
  .filter((id) => /\.xml$/i.test(id))
1056
- .map((id) => {
1057
- const spec = normalizeSpec(id);
1058
- const appVirtual = APP_VIRTUAL_WITH_SLASH.replace(/^\//, '');
1059
- let relPath = spec.startsWith('/') ? spec.slice(1) : spec;
1060
- if (relPath.startsWith(appVirtual))
1061
- relPath = relPath.slice(appVirtual.length);
1062
- // Strip .xml extension to get the moduleName (e.g., 'pages/status-bar')
1063
- return relPath.replace(/\.xml$/i, '');
1064
- })
1065
- .filter((m) => m && m !== 'app-root');
1510
+ .map((id) => toAppModuleName(id))
1511
+ .filter((m) => m && m !== 'app-root' && !reshowModuleNames.has(m));
1066
1512
  // Resolve the topmost Frame from the bundled realm.
1067
1513
  // Frame.topmost() relies on an internal frameStack array, so we must
1068
1514
  // call it on the bundled-realm class. Multiple strategies to find it:
@@ -1098,7 +1544,7 @@ async function processQueue() {
1098
1544
  catch { }
1099
1545
  }
1100
1546
  if (VERBOSE)
1101
- console.log('[hmr][queue] TS: changedXmlPages=', changedXmlPages, 'topFrame=', !!topFrame);
1547
+ console.log('[hmr][queue] TS: changedXmlPages=', changedXmlPages, 'topFrame=', !!topFrame, 'modalsToReshow=', modalsToReshow.length);
1102
1548
  if (changedXmlPages.length > 0 && topFrame) {
1103
1549
  // Navigate the current frame to the changed page directly.
1104
1550
  // Use the last changed XML page (most specific).
@@ -1113,15 +1559,32 @@ async function processQueue() {
1113
1559
  App.resetRootView({ moduleName: 'app-root' });
1114
1560
  }
1115
1561
  }
1116
- else {
1562
+ else if (modalsToReshow.length === 0) {
1563
+ // No frame page to refresh and no open modal owns the change —
1564
+ // fall back to a full root reset. (Skipped when an open modal is
1565
+ // being re-presented below: resetRootView would dismiss it.)
1117
1566
  if (VERBOSE)
1118
1567
  console.log('[hmr][queue] TS flavor: resetRootView(app-root) after changes');
1119
1568
  App.resetRootView({ moduleName: 'app-root' });
1120
1569
  }
1570
+ // Re-present any open modals whose XML/code-behind changed. The
1571
+ // modules were already re-registered above (raw XML assets + fresh
1572
+ // code-behind exports), so the re-presented modal rebuilds from
1573
+ // the new content while the page beneath it stays put.
1574
+ for (const record of modalsToReshow) {
1575
+ await reshowOpenModal(record);
1576
+ }
1121
1577
  }
1122
1578
  catch (e) {
1123
1579
  console.warn('[hmr][queue] TS flavor: resetRootView(app-root) failed', e);
1124
1580
  }
1581
+ // Tell the overlay the cycle is done — same as the solid path
1582
+ // above. Without this the applying overlay sticks at
1583
+ // 'received' (5%) forever even though the in-place navigate /
1584
+ // resetRootView already applied the update.
1585
+ setUpdateOverlayStage('complete', {
1586
+ detail: `Total ${Math.max(0, Date.now() - tQueueStart)}ms`,
1587
+ });
1125
1588
  break;
1126
1589
  }
1127
1590
  }
@@ -1129,11 +1592,37 @@ async function processQueue() {
1129
1592
  finally {
1130
1593
  processingQueue = false;
1131
1594
  processingPromise = null;
1595
+ // If a delta arrived mid-cycle (pushed onto changedQueue while
1596
+ // processingQueue was still true, so its processQueue() call early-
1597
+ // returned), re-drain — otherwise that save is stranded until the next
1598
+ // delta. Deferred via setTimeout to avoid synchronous re-entrancy as
1599
+ // this promise settles.
1600
+ if (changedQueue.length) {
1601
+ setTimeout(() => {
1602
+ try {
1603
+ processQueue();
1604
+ }
1605
+ catch { }
1606
+ }, 0);
1607
+ }
1132
1608
  }
1133
1609
  })();
1134
1610
  return processingPromise;
1135
1611
  }
1136
1612
  let hmrSocket = null;
1613
+ // Single reconnect timer. Overlapping close/timeout events used to each schedule
1614
+ // their own `setTimeout(connectHmr, …)`, stacking multiple pending reconnects
1615
+ // that could spawn (and leak) duplicate sockets/listeners. Route all reconnect
1616
+ // scheduling through here so only one is ever pending.
1617
+ let reconnectTimer = null;
1618
+ function scheduleReconnect(delayMs) {
1619
+ if (reconnectTimer)
1620
+ clearTimeout(reconnectTimer);
1621
+ reconnectTimer = setTimeout(() => {
1622
+ reconnectTimer = null;
1623
+ connectHmr();
1624
+ }, delayMs);
1625
+ }
1137
1626
  // Track server-announced batches for each version so we can import in-order client-side
1138
1627
  const txnClientBatches = new Map();
1139
1628
  // Public hook for NativeScript runtime to call from ImportModuleDynamicallyCallback later.
@@ -1161,10 +1650,16 @@ function connectHmr() {
1161
1650
  if (hmrSocket?.readyState === WebSocket.OPEN)
1162
1651
  return;
1163
1652
  if (hmrSocket?.readyState === WebSocket.CONNECTING) {
1164
- if (__NS_ENV_VERBOSE__)
1653
+ if (VERBOSE)
1165
1654
  console.log('[hmr-client] Already connecting to HMR WebSocket, skipping');
1166
1655
  return;
1167
1656
  }
1657
+ // A reconnect fired (or a manual connect raced one) — cancel any other pending
1658
+ // reconnect so we don't end up with overlapping connect attempts.
1659
+ if (reconnectTimer) {
1660
+ clearTimeout(reconnectTimer);
1661
+ reconnectTimer = null;
1662
+ }
1168
1663
  try {
1169
1664
  globalThis.__NS_HMR_CLIENT_SOCKET_READY__ = false;
1170
1665
  }
@@ -1172,7 +1667,7 @@ function connectHmr() {
1172
1667
  const overlayStage = hasOpenedHmrSocket ? 'reconnecting' : 'connecting';
1173
1668
  const baseUrl = getHMRWsUrl() || 'ws://localhost:5173/ns-hmr';
1174
1669
  const buildCandidates = (url) => {
1175
- let candidates = [];
1670
+ const candidates = [];
1176
1671
  try {
1177
1672
  const u = new URL(url);
1178
1673
  const proto = u.protocol === 'wss:' ? ['wss'] : ['ws'];
@@ -1180,7 +1675,7 @@ function connectHmr() {
1180
1675
  // Build ordered host candidates with preference to the active HTTP origin
1181
1676
  const orderedHosts = [];
1182
1677
  try {
1183
- const g = globalThis;
1678
+ const g = getGlobalScope();
1184
1679
  const httpOrigin = g && typeof g.__NS_HTTP_ORIGIN__ === 'string' ? g.__NS_HTTP_ORIGIN__ : undefined;
1185
1680
  if (httpOrigin) {
1186
1681
  try {
@@ -1231,19 +1726,29 @@ function connectHmr() {
1231
1726
  if (idx >= candidates.length) {
1232
1727
  showConnectionOverlayNow('offline', 'Waiting for the Vite websocket to come back.');
1233
1728
  console.warn('[hmr-client] All WS candidates failed:', candidates.join(', '));
1234
- setTimeout(connectHmr, 1500);
1729
+ scheduleReconnect(1500);
1235
1730
  return;
1236
1731
  }
1237
1732
  const url = candidates[idx++];
1238
1733
  const connectionDetail = `${overlayStage === 'reconnecting' ? 'Retrying' : 'Opening'} ${url}`;
1239
- if (connectionOverlayVisible) {
1734
+ if (overlayStage === 'reconnecting' && shownConnectionOverlayStage === 'offline') {
1735
+ // Don't flip the visible 'offline' frame to 'reconnecting' for an
1736
+ // attempt that may fail instantly — defer it so only a genuinely
1737
+ // in-flight attempt surfaces. The deferred show is cancelled by
1738
+ // showConnectionOverlayNow (offline re-show / synchronizing) the
1739
+ // moment this attempt resolves. Keeps 'offline' stable instead of
1740
+ // flickering once per retry. (Checking the PAINTED stage, not the
1741
+ // pending one, so every candidate in the loop keeps deferring.)
1742
+ scheduleConnectionOverlay(overlayStage, connectionDetail, RECONNECTING_OVER_OFFLINE_DELAY_MS);
1743
+ }
1744
+ else if (connectionOverlayVisible) {
1240
1745
  updateConnectionOverlay(overlayStage, connectionDetail);
1241
1746
  }
1242
1747
  else {
1243
1748
  scheduleConnectionOverlay(overlayStage, connectionDetail);
1244
1749
  }
1245
1750
  try {
1246
- if (__NS_ENV_VERBOSE__)
1751
+ if (VERBOSE)
1247
1752
  console.log('[hmr-client] Connecting to HMR WebSocket:', url);
1248
1753
  const sock = new WebSocket(url);
1249
1754
  hmrSocket = sock;
@@ -1273,7 +1778,16 @@ function connectHmr() {
1273
1778
  if (connectionOverlayVisible) {
1274
1779
  showConnectionOverlayNow('synchronizing', 'Connected. Synchronizing the HMR graph.');
1275
1780
  }
1276
- VERBOSE && console.log('[hmr-client] Connected to HMR WebSocket');
1781
+ if (VERBOSE)
1782
+ console.log('[hmr-client] Connected to HMR WebSocket');
1783
+ // Print the active module reload mode once on first
1784
+ // successful connect so the user can correlate HMR latency
1785
+ // with runtime capability without grepping for protocol
1786
+ // details. The banner is verbose-gated.
1787
+ try {
1788
+ emitHmrModeBannerOnce();
1789
+ }
1790
+ catch { }
1277
1791
  };
1278
1792
  sock.onmessage = handleHmrMessage;
1279
1793
  sock.onerror = (error) => {
@@ -1297,7 +1811,7 @@ function connectHmr() {
1297
1811
  console.log('[hmr-client] WebSocket closed (code', ev?.code, '), will reconnect…');
1298
1812
  scheduleConnectionOverlay('reconnecting', 'The websocket closed. Waiting to reconnect.', 700);
1299
1813
  // try to reconnect with full candidate list again
1300
- setTimeout(connectHmr, 1000);
1814
+ scheduleReconnect(1000);
1301
1815
  }
1302
1816
  };
1303
1817
  }
@@ -1333,10 +1847,26 @@ async function handleHmrMessage(ev) {
1333
1847
  catch { }
1334
1848
  }
1335
1849
  if (msg) {
1850
+ // `ns:hmr-pending` is a fire-and-forget UX hint emitted by the
1851
+ // server at the START of handleHotUpdate. We drive the
1852
+ // HMR-applying overlay's 'received' frame here (synchronously),
1853
+ // well before the authoritative payload (`ns:angular-update` /
1854
+ // `ns:css-updates`) lands. Skip running any other handlers —
1855
+ // the pending message has no module payload and intentionally
1856
+ // does not bump the graph version.
1857
+ if (msg.type === 'ns:hmr-pending' && typeof msg.path === 'string') {
1858
+ setHmrPendingOverlay(msg.path);
1859
+ return;
1860
+ }
1861
+ // The per-flavor client strategy is loaded by a dynamic import(); make
1862
+ // sure it has resolved (and `install()` has run) before any handler that
1863
+ // delegates through it. After the first message this is an already-settled
1864
+ // promise (microtask only); for Solid/TypeScript it is `Promise.resolve()`.
1865
+ await CLIENT_STRATEGY_READY;
1336
1866
  if (msg.type === 'ns:hmr-full-graph') {
1337
1867
  // Bump a monotonic nonce so HTTP ESM imports can always be cache-busted per update.
1338
1868
  try {
1339
- const g = globalThis;
1869
+ const g = getGlobalScope();
1340
1870
  g.__NS_HMR_IMPORT_NONCE__ = (typeof g.__NS_HMR_IMPORT_NONCE__ === 'number' ? g.__NS_HMR_IMPORT_NONCE__ : 0) + 1;
1341
1871
  }
1342
1872
  catch { }
@@ -1402,6 +1932,14 @@ async function handleHmrMessage(ev) {
1402
1932
  });
1403
1933
  if (toReimport.length && VERBOSE)
1404
1934
  console.log('[hmr][full-graph] inferred changed modules; re-importing', toReimport);
1935
+ // Evict the inferred changed set before re-importing.
1936
+ // See `processQueue` for the architectural rationale; the
1937
+ // full-graph code path is the resync fallback (server chose
1938
+ // not to send a delta) and shares the same V8 cache pitfall.
1939
+ const fgEvictUrls = buildEvictionUrls(toReimport);
1940
+ const fgEvicted = invalidateModulesByUrls(fgEvictUrls);
1941
+ if (VERBOSE)
1942
+ console.log(`[hmr][full-graph] eviction count=${fgEvictUrls.length} ok=${fgEvicted}`);
1405
1943
  for (const id of toReimport) {
1406
1944
  try {
1407
1945
  const spec = normalizeSpec(id);
@@ -1453,7 +1991,7 @@ async function handleHmrMessage(ev) {
1453
1991
  if (msg.type === 'ns:hmr-delta') {
1454
1992
  // Bump a monotonic nonce so HTTP ESM imports can always be cache-busted per update.
1455
1993
  try {
1456
- const g = globalThis;
1994
+ const g = getGlobalScope();
1457
1995
  g.__NS_HMR_IMPORT_NONCE__ = (typeof g.__NS_HMR_IMPORT_NONCE__ === 'number' ? g.__NS_HMR_IMPORT_NONCE__ : 0) + 1;
1458
1996
  }
1459
1997
  catch { }
@@ -1475,10 +2013,108 @@ async function handleHmrMessage(ev) {
1475
2013
  return;
1476
2014
  }
1477
2015
  else {
2016
+ // Vite custom-event dispatch.
2017
+ //
2018
+ // `server.ws.send('event-name', payload)` from any Vite plugin lands
2019
+ // on the wire as `{ type: 'custom', event: 'event-name', data: payload }`.
2020
+ // On the web, Vite's stock client owns a `customListenersMap` that
2021
+ // fires every `import.meta.hot.on('event-name', cb)` callback. We
2022
+ // don't run Vite's stock client on device — the iOS runtime owns
2023
+ // the listener registry via `__NS_DISPATCH_HOT_EVENT__` (the
2024
+ // counterpart to `import.meta.hot.on` populated by user code +
2025
+ // compiled Angular components). Forwarding `type: 'custom'` here
2026
+ // is the only thing standing between server-emitted events and
2027
+ // the listeners they were meant for.
2028
+ //
2029
+ // `angular:component-update` is the canonical example. Analog's
2030
+ // plugin sends it on `.html` / component-style edits; the
2031
+ // compiled component `.mjs` registered a listener that
2032
+ // dynamic-imports `/@ng/component?c=<id>&t=<ts>` and calls
2033
+ // `ɵɵreplaceMetadata` on the live class — swapping the template
2034
+ // definition AND walking live `LView`s to recreate matching views
2035
+ // in-place. The page stays mounted and only the changed bits
2036
+ // re-render. We MUST `return` after dispatch so the reboot path
2037
+ // (`handleAngularHotUpdateMessage` → `__reboot_ng_modules__`)
2038
+ // never runs for these updates — that's the whole point of the
2039
+ // component-replacement pipeline.
2040
+ //
2041
+ // All other custom events are forwarded but NOT short-circuited
2042
+ // (Vite spec: custom events are additive — they don't replace
2043
+ // any framework-specific handling). The reboot path falls through
2044
+ // for `ns:angular-update` (the legacy/`.ts`-edit broadcast) and
2045
+ // for any framework not yet using the in-place replacement path.
2046
+ if (msg.type === 'custom' && typeof msg.event === 'string') {
2047
+ // Dispatch every Vite "custom" event through the runtime's
2048
+ // `__NS_DISPATCH_HOT_EVENT__` bridge so `import.meta.hot.on(event, cb)`
2049
+ // callbacks fire on the device. Critical contract: this is the
2050
+ // ONLY route by which Analog's `angular:component-update` reaches
2051
+ // the compiled component's `(d) => d.id === id && Component_HmrLoad(...)`
2052
+ // listener — without it, server-side broadcasts log green
2053
+ // (`(client) hmr update`) while the device sees nothing happen.
2054
+ //
2055
+ // Diagnostic policy: log "no dispatcher" loud (boot-time rt-bridge
2056
+ // failure), and listener exceptions loud (compiled HmrLoad
2057
+ // fetch/parse error). Successful dispatches are silent — the
2058
+ // runtime's `[import.meta.hot] dispatch summary` line carries
2059
+ // the per-event match-count diagnostic.
2060
+ try {
2061
+ const dispatch = globalThis.__NS_DISPATCH_HOT_EVENT__;
2062
+ if (typeof dispatch === 'function') {
2063
+ dispatch(msg.event, msg.data);
2064
+ }
2065
+ else {
2066
+ console.warn(`[hmr-client][custom] no __NS_DISPATCH_HOT_EVENT__ available for '${msg.event}'`);
2067
+ }
2068
+ }
2069
+ catch (err) {
2070
+ console.warn('[hmr-client][custom] dispatch threw for', msg.event, err);
2071
+ }
2072
+ if (msg.event === 'angular:component-update') {
2073
+ if (VERBOSE)
2074
+ console.log('[hmr-client][custom] dispatched angular:component-update — skipping reboot path');
2075
+ // Walk the apply-progress overlay through its
2076
+ // remaining stages for the in-place template-swap
2077
+ // path. The full reboot path
2078
+ // (`handleAngularHotUpdateMessage`) drives the
2079
+ // overlay itself ('received' → 'evicting' →
2080
+ // 'reimporting' → 'rebooting' → 'complete'); the
2081
+ // in-place path bypasses that handler entirely
2082
+ // because the work happens inside Angular's
2083
+ // `ɵɵreplaceMetadata` after the runtime forwards the
2084
+ // `angular:component-update` event to the compiled
2085
+ // component's listener. Without this update the
2086
+ // overlay would freeze at 5% ('received') even
2087
+ // though the visual swap completes a few frames
2088
+ // later — exactly the "Preparing update (5%)" stuck
2089
+ // frame we have been chasing.
2090
+ //
2091
+ // We transition straight to 'reimporting' to
2092
+ // communicate that metadata is being fetched (the
2093
+ // runtime listener fires `__ns_import('/@ng/component?c=...&t=...')`),
2094
+ // then schedule 'complete' on the next macrotask so
2095
+ // the auto-hide timer kicks in. The actual
2096
+ // template swap is fire-and-forget from this point;
2097
+ // the user sees the overlay close at the same time
2098
+ // as Angular re-renders the bound text/structure.
2099
+ try {
2100
+ const filePath = typeof msg.data?.id === 'string' ? decodeURIComponent(msg.data.id).split('@')[0] : undefined;
2101
+ const detail = filePath ? `Applying template update to ${filePath}` : 'Applying template update';
2102
+ setUpdateOverlayStage('reimporting', { detail });
2103
+ setTimeout(() => {
2104
+ try {
2105
+ setUpdateOverlayStage('complete', { detail: filePath ? `Updated ${filePath}` : 'Update applied' });
2106
+ }
2107
+ catch { }
2108
+ }, 16);
2109
+ }
2110
+ catch { }
2111
+ return;
2112
+ }
2113
+ }
1478
2114
  if (msg.type === 'ns:angular-update' && typeof msg.version === 'number') {
1479
2115
  setGraphVersion(Number(msg.version || getGraphVersion() || 0));
1480
2116
  }
1481
- if (await handleAngularHotUpdateMessage(msg, { getCore, verbose: VERBOSE })) {
2117
+ if (CLIENT_STRATEGY?.handleHotUpdateMessage && (await CLIENT_STRATEGY.handleHotUpdateMessage(msg, { getCore, verbose: VERBOSE, performResetRoot, getOverlay: getHmrOverlayApi }))) {
1482
2118
  return;
1483
2119
  }
1484
2120
  }
@@ -1512,27 +2148,47 @@ async function handleHmrMessage(ev) {
1512
2148
  return;
1513
2149
  }
1514
2150
  if (msg.type === 'ns:css-updates' && Array.isArray(msg.updates)) {
2151
+ // Drive the HMR-applying overlay past the 'received' (5%) frame
2152
+ // that `ns:hmr-pending` set earlier in the cycle. Without this
2153
+ // the overlay sticks at "Preparing update" forever for CSS-only
2154
+ // edits because `handleCssUpdates` is a leaf — there's no
2155
+ // downstream module-evaluation path that would hit the queue's
2156
+ // 'complete' transition.
2157
+ const cssCount = msg.updates.length;
2158
+ try {
2159
+ setUpdateOverlayStage('reimporting', { detail: buildCssApplyingDetail(cssCount) });
2160
+ }
2161
+ catch { }
1515
2162
  try {
1516
2163
  const origin = msg.origin || getHttpOriginForVite() || deriveHttpOrigin(getHMRWsUrl());
1517
2164
  await handleCssUpdates(msg.updates, origin);
2165
+ try {
2166
+ setUpdateOverlayStage('complete', { detail: buildCssAppliedDetail(cssCount) });
2167
+ }
2168
+ catch { }
1518
2169
  return;
1519
2170
  }
1520
2171
  catch (e) {
1521
2172
  console.warn('[hmr-client] CSS updates handling failed:', e);
2173
+ try {
2174
+ setUpdateOverlayStage('complete', { detail: 'CSS update failed' });
2175
+ }
2176
+ catch { }
1522
2177
  return;
1523
2178
  }
1524
2179
  }
1525
2180
  if (msg.type === 'ns:vue-sfc-registry') {
1526
- handleVueSfcRegistry(msg);
2181
+ CLIENT_STRATEGY?.handleSfcRegistry?.(msg);
1527
2182
  return;
1528
2183
  }
1529
2184
  if (msg.type === 'ns:vue-sfc-registry-update') {
1530
2185
  if (typeof msg.version === 'number')
1531
2186
  setGraphVersion(msg.version);
1532
- const comp = await handleVueSfcRegistryUpdate(msg, getGraphVersion());
1533
- if (comp) {
1534
- await performResetRoot(comp);
1535
- }
2187
+ // `ns:hmr-pending` already set the overlay to 'received' (5%). The Vue
2188
+ // strategy walks 'evicting' → 'reimporting' → 'rebooting' → 'complete'
2189
+ // around the SFC load + reset so the toast always lands on 'complete'
2190
+ // (or a failure detail) and the auto-hide timer can dismiss it.
2191
+ await CLIENT_STRATEGY?.handleSfcRegistryUpdate?.(msg, getGraphVersion(), { getCore, verbose: VERBOSE, performResetRoot, getOverlay: getHmrOverlayApi });
1536
2192
  return;
1537
2193
  }
1538
2194
  }
@@ -1552,9 +2208,9 @@ function normalizeComponent(input, nameHint) {
1552
2208
  }
1553
2209
  // If provided a render function, wrap with defineComponent
1554
2210
  if (typeof input === 'function') {
1555
- ensureVueGlobals();
1556
- const comp = globalThis.defineComponent
1557
- ? globalThis.defineComponent({
2211
+ CLIENT_STRATEGY?.beforeNavigateBuild?.();
2212
+ const comp = getGlobalScope().defineComponent
2213
+ ? getGlobalScope().defineComponent({
1558
2214
  name: nameHint || input.name || 'AnonymousSFC',
1559
2215
  render: input,
1560
2216
  })
@@ -1563,9 +2219,9 @@ function normalizeComponent(input, nameHint) {
1563
2219
  }
1564
2220
  // If object has a render function property
1565
2221
  if (input?.render && typeof input.render === 'function') {
1566
- ensureVueGlobals();
1567
- const comp = globalThis.defineComponent
1568
- ? globalThis.defineComponent({
2222
+ CLIENT_STRATEGY?.beforeNavigateBuild?.();
2223
+ const comp = getGlobalScope().defineComponent
2224
+ ? getGlobalScope().defineComponent({
1569
2225
  name: nameHint || input.name || 'AnonymousSFC',
1570
2226
  render: input.render,
1571
2227
  })
@@ -1621,35 +2277,32 @@ async function performResetRoot(newComponent) {
1621
2277
  if (cachedRoot)
1622
2278
  return cachedRoot;
1623
2279
  try {
1624
- switch (TARGET_FLAVOR) {
1625
- case 'vue':
1626
- cachedRoot = getRootForVue(newComponent, state);
1627
- break;
1628
- case 'typescript': {
1629
- // For TS flavor, treat the component as a factory or direct NS view.
1630
- let root = null;
1631
- try {
1632
- if (typeof newComponent === 'function') {
1633
- root = newComponent();
1634
- }
1635
- else {
1636
- root = newComponent;
1637
- }
2280
+ if (CLIENT_STRATEGY?.createRoot) {
2281
+ cachedRoot = CLIENT_STRATEGY.createRoot(newComponent, state);
2282
+ }
2283
+ else if (TS_LIKE_FLAVOR) {
2284
+ // For TS flavor, treat the component as a factory or direct NS view.
2285
+ let root = null;
2286
+ try {
2287
+ if (typeof newComponent === 'function') {
2288
+ root = newComponent();
1638
2289
  }
1639
- catch (e) {
1640
- console.warn('[hmr-client][ts] root factory invocation failed', e);
2290
+ else {
2291
+ root = newComponent;
1641
2292
  }
1642
- cachedRoot = root || {};
1643
- // Heuristic: if the root "looks" like a Frame, prefer frame semantics
1644
- try {
1645
- const name = String(cachedRoot?.constructor?.name || '').replace(/^_+/, '');
1646
- if (/^Frame(\$\d+)?$/.test(name)) {
1647
- rootKind = 'frame';
1648
- }
2293
+ }
2294
+ catch (e) {
2295
+ console.warn('[hmr-client][ts] root factory invocation failed', e);
2296
+ }
2297
+ cachedRoot = root || {};
2298
+ // Heuristic: if the root "looks" like a Frame, prefer frame semantics
2299
+ try {
2300
+ const name = String(cachedRoot?.constructor?.name || '').replace(/^_+/, '');
2301
+ if (/^Frame(\$\d+)?$/.test(name)) {
2302
+ rootKind = 'frame';
1649
2303
  }
1650
- catch { }
1651
- break;
1652
2304
  }
2305
+ catch { }
1653
2306
  }
1654
2307
  return cachedRoot;
1655
2308
  }
@@ -1667,7 +2320,7 @@ async function performResetRoot(newComponent) {
1667
2320
  return factory;
1668
2321
  }
1669
2322
  // Android readiness before any root changes
1670
- const App = getCore('Application') || globalThis.Application;
2323
+ const App = getCore('Application') || getGlobalScope().Application;
1671
2324
  const isAndroid = !!(App && App.android !== undefined);
1672
2325
  if (isAndroid) {
1673
2326
  const isReady = () => {
@@ -1769,7 +2422,7 @@ async function performResetRoot(newComponent) {
1769
2422
  }
1770
2423
  catch { }
1771
2424
  try {
1772
- const AppAny = getCore('Application') || globalThis.Application;
2425
+ const AppAny = getCore('Application') || getGlobalScope().Application;
1773
2426
  isIOS = !!(AppAny && AppAny.ios !== undefined);
1774
2427
  }
1775
2428
  catch { }
@@ -1779,7 +2432,7 @@ async function performResetRoot(newComponent) {
1779
2432
  // - Otherwise (subsequent HMR updates with an authoritative Frame already in place), re-use the
1780
2433
  // current app Frame and navigate to the new Page. This avoids a brief flash that can occur
1781
2434
  // when swapping the entire root view on Android. The placeholder is never involved here.
1782
- const gAnyForPolicy = globalThis;
2435
+ const gAnyForPolicy = getGlobalScope();
1783
2436
  const placeholderFrame = (() => {
1784
2437
  try {
1785
2438
  return gAnyForPolicy.__NS_DEV_PLACEHOLDER_ROOT_VIEW__ || null;
@@ -1794,7 +2447,14 @@ async function performResetRoot(newComponent) {
1794
2447
  }
1795
2448
  catch { }
1796
2449
  const isAuthoritativeFrame = !!existingAppFrame && existingAppFrame !== placeholderFrame;
1797
- if (!hadPlaceholder && !isFrameRoot && isAuthoritativeFrame && typeof existingAppFrame.navigate === 'function') {
2450
+ // Vue: skip the in-place navigate path. After `app.mount(NSVRoot)` in getRootForVue the
2451
+ // new Page already has a parent (the freshly-constructed NSVRoot), so an attempt to navigate
2452
+ // the existing app Frame to that same Page completes silently without ever rebinding the
2453
+ // page to the Frame — the screen keeps showing the previous render. resetRootView with a
2454
+ // fresh Frame correctly reparents the Page and is the proven path that produces visible
2455
+ // in-place updates for SFC HMR cycles. Non-Vue flavors keep the legacy navigate fast path.
2456
+ const allowNavigateFastPath = CLIENT_STRATEGY?.allowNavigateFastPath ?? true;
2457
+ if (allowNavigateFastPath && !hadPlaceholder && !isFrameRoot && isAuthoritativeFrame && typeof existingAppFrame.navigate === 'function') {
1798
2458
  try {
1799
2459
  const navEntry = {
1800
2460
  create: () => preparedRoot,
@@ -1813,7 +2473,7 @@ async function performResetRoot(newComponent) {
1813
2473
  console.log('[hmr-client] full root replacement via resetRootView (placeholder will be discarded)', { isFrameRoot, isIOS, hadPlaceholder });
1814
2474
  // Fallback or preferred path: resetRootView with a creator that builds a fresh Frame and navigates to the new Page
1815
2475
  try {
1816
- const App2 = getCore('Application') || globalThis.Application;
2476
+ const App2 = getCore('Application') || getGlobalScope().Application;
1817
2477
  if (!App2 || typeof App2.resetRootView !== 'function') {
1818
2478
  console.warn('[hmr-client] Application.resetRootView unavailable');
1819
2479
  return false;
@@ -1832,7 +2492,7 @@ async function performResetRoot(newComponent) {
1832
2492
  if (VERBOSE)
1833
2493
  console.warn('[hmr-client] iOS Application.window is boolean false; attempting to clear cached window');
1834
2494
  try {
1835
- const g = globalThis;
2495
+ const g = getGlobalScope();
1836
2496
  const reg = g.__nsVendorRegistry;
1837
2497
  const req = reg?.get ? g.__nsVendorRequire || g.__nsRequire || g.require : g.__nsRequire || g.require;
1838
2498
  let helpers = null;
@@ -1936,6 +2596,20 @@ export function initHmrClient(opts) {
1936
2596
  }
1937
2597
  g.__NS_HMR_CLIENT_ACTIVE__ = true;
1938
2598
  ensureCoreAliasesOnGlobalThis();
2599
+ // XML flavor: record string-module modals from the moment the client is up
2600
+ // so an already-open modal can be re-presented when its files change.
2601
+ // Installed at init (not first-update time) because the wrap can only
2602
+ // observe showModal calls made AFTER it lands. Retried briefly because
2603
+ // getCore('View') may not resolve until the vendor realm finishes booting.
2604
+ if (TS_LIKE_FLAVOR) {
2605
+ const tryInstallModalTracking = (attempts) => {
2606
+ ensureModalTracking();
2607
+ if (!modalTrackingInstalled && attempts > 0) {
2608
+ setTimeout(() => tryInstallModalTracking(attempts - 1), 250);
2609
+ }
2610
+ };
2611
+ tryInstallModalTracking(40);
2612
+ }
1939
2613
  // Defer WebSocket connection until boot completes to avoid native V8 crashes
1940
2614
  // caused by concurrent WebSocket message handling + HTTP fetch during early startup.
1941
2615
  // The WebSocket is only needed for HMR updates, not the initial boot sequence.
@@ -1957,12 +2631,9 @@ export function initHmrClient(opts) {
1957
2631
  console.log('[hmr-client] deferring WebSocket connection until boot completes');
1958
2632
  setTimeout(waitForBoot, 100);
1959
2633
  }
1960
- // Best-effort: install back wrapper even before first remount; original root may be captured later
1961
- switch (TARGET_FLAVOR) {
1962
- case 'vue':
1963
- ensureBackWrapperInstalled(performResetRoot, getCore);
1964
- break;
1965
- }
2634
+ // Best-effort: install back wrapper even before first remount; original root may be captured later.
2635
+ // Deferred until the dynamically-imported strategy resolves.
2636
+ void CLIENT_STRATEGY_READY.then(() => CLIENT_STRATEGY?.installBackWrapper?.(performResetRoot, getCore));
1966
2637
  }
1967
2638
  export default function startViteHMR(opts) {
1968
2639
  if (VERBOSE)