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

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 (463) 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 +244 -53
  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 +4 -2
  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 +100 -13
  41. package/helpers/global-defines.js +149 -28
  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.js +29 -1
  49. package/helpers/logging.js.map +1 -1
  50. package/helpers/main-entry.d.ts +4 -1
  51. package/helpers/main-entry.js +368 -53
  52. package/helpers/main-entry.js.map +1 -1
  53. package/helpers/nativeclass-esbuild-plugin.d.ts +2 -1
  54. package/helpers/nativeclass-esbuild-plugin.js.map +1 -1
  55. package/helpers/nativeclass-transform.js +5 -6
  56. package/helpers/nativeclass-transform.js.map +1 -1
  57. package/helpers/nativeclass-transformer-plugin.d.ts +9 -2
  58. package/helpers/nativeclass-transformer-plugin.js +157 -14
  59. package/helpers/nativeclass-transformer-plugin.js.map +1 -1
  60. package/helpers/nativescript-package-resolver.js +10 -62
  61. package/helpers/nativescript-package-resolver.js.map +1 -1
  62. package/helpers/normalize-id.d.ts +42 -0
  63. package/helpers/normalize-id.js +60 -0
  64. package/helpers/normalize-id.js.map +1 -0
  65. package/helpers/ns-core-url.d.ts +29 -7
  66. package/helpers/ns-core-url.js +69 -12
  67. package/helpers/ns-core-url.js.map +1 -1
  68. package/helpers/optimize-deps.d.ts +16 -0
  69. package/helpers/optimize-deps.js +17 -0
  70. package/helpers/optimize-deps.js.map +1 -0
  71. package/helpers/package-platform-aliases.js +34 -49
  72. package/helpers/package-platform-aliases.js.map +1 -1
  73. package/helpers/platform-types.d.ts +2 -0
  74. package/helpers/platform-types.js +2 -0
  75. package/helpers/platform-types.js.map +1 -0
  76. package/helpers/postcss-platform-config.d.ts +17 -1
  77. package/helpers/postcss-platform-config.js +20 -37
  78. package/helpers/postcss-platform-config.js.map +1 -1
  79. package/helpers/project.d.ts +35 -0
  80. package/helpers/project.js +120 -2
  81. package/helpers/project.js.map +1 -1
  82. package/helpers/resolve-main-field-platform.d.ts +20 -0
  83. package/helpers/resolve-main-field-platform.js +49 -0
  84. package/helpers/resolve-main-field-platform.js.map +1 -0
  85. package/helpers/resolver.js +17 -2
  86. package/helpers/resolver.js.map +1 -1
  87. package/helpers/theme-core-plugins.js +1 -1
  88. package/helpers/theme-core-plugins.js.map +1 -1
  89. package/helpers/ts-config-paths.d.ts +32 -0
  90. package/helpers/ts-config-paths.js +156 -27
  91. package/helpers/ts-config-paths.js.map +1 -1
  92. package/helpers/typescript-check.d.ts +2 -1
  93. package/helpers/typescript-check.js +2 -2
  94. package/helpers/typescript-check.js.map +1 -1
  95. package/helpers/ui-registration.d.ts +21 -0
  96. package/helpers/ui-registration.js +156 -0
  97. package/helpers/ui-registration.js.map +1 -0
  98. package/helpers/utils.d.ts +9 -0
  99. package/helpers/utils.js +22 -2
  100. package/helpers/utils.js.map +1 -1
  101. package/helpers/vite-package-version.d.ts +7 -0
  102. package/helpers/vite-package-version.js +44 -0
  103. package/helpers/vite-package-version.js.map +1 -0
  104. package/helpers/workers.d.ts +20 -19
  105. package/helpers/workers.js +624 -4
  106. package/helpers/workers.js.map +1 -1
  107. package/hmr/client/css-handler.d.ts +2 -1
  108. package/hmr/client/css-handler.js +50 -26
  109. package/hmr/client/css-handler.js.map +1 -1
  110. package/hmr/client/css-update-overlay.d.ts +18 -0
  111. package/hmr/client/css-update-overlay.js +27 -0
  112. package/hmr/client/css-update-overlay.js.map +1 -0
  113. package/hmr/client/framework-client-strategy.d.ts +79 -0
  114. package/hmr/client/framework-client-strategy.js +19 -0
  115. package/hmr/client/framework-client-strategy.js.map +1 -0
  116. package/hmr/client/hmr-pending-overlay.d.ts +0 -14
  117. package/hmr/client/hmr-pending-overlay.js +14 -4
  118. package/hmr/client/hmr-pending-overlay.js.map +1 -1
  119. package/hmr/client/hot-context.d.ts +67 -0
  120. package/hmr/client/hot-context.js +284 -0
  121. package/hmr/client/hot-context.js.map +1 -0
  122. package/hmr/client/index.js +922 -273
  123. package/hmr/client/index.js.map +1 -1
  124. package/hmr/client/utils.d.ts +2 -1
  125. package/hmr/client/utils.js +111 -99
  126. package/hmr/client/utils.js.map +1 -1
  127. package/hmr/entry-runtime.d.ts +2 -1
  128. package/hmr/entry-runtime.js +271 -69
  129. package/hmr/entry-runtime.js.map +1 -1
  130. package/hmr/frameworks/angular/build/angular-linker.d.ts +12 -0
  131. package/hmr/frameworks/angular/build/angular-linker.js +109 -0
  132. package/hmr/frameworks/angular/build/angular-linker.js.map +1 -0
  133. package/hmr/frameworks/angular/build/inject-component-hmr-registration.d.ts +112 -0
  134. package/hmr/frameworks/angular/build/inject-component-hmr-registration.js +291 -0
  135. package/hmr/frameworks/angular/build/inject-component-hmr-registration.js.map +1 -0
  136. package/hmr/frameworks/angular/build/inject-hmr-vite-ignore.d.ts +75 -0
  137. package/hmr/frameworks/angular/build/inject-hmr-vite-ignore.js +221 -0
  138. package/hmr/frameworks/angular/build/inject-hmr-vite-ignore.js.map +1 -0
  139. package/{helpers/angular → hmr/frameworks/angular/build}/inline-decorator-component-templates.js +1 -170
  140. package/hmr/frameworks/angular/build/inline-decorator-component-templates.js.map +1 -0
  141. package/hmr/frameworks/angular/build/js-lexer.d.ts +4 -0
  142. package/{helpers/angular/synthesize-decorator-ctor-parameters.js → hmr/frameworks/angular/build/js-lexer.js} +22 -96
  143. package/hmr/frameworks/angular/build/js-lexer.js.map +1 -0
  144. package/hmr/frameworks/angular/build/shared-linker.d.ts +39 -0
  145. package/hmr/frameworks/angular/build/shared-linker.js +128 -0
  146. package/hmr/frameworks/angular/build/shared-linker.js.map +1 -0
  147. package/hmr/frameworks/angular/build/synthesize-decorator-ctor-parameters.js +88 -0
  148. package/hmr/frameworks/angular/build/synthesize-decorator-ctor-parameters.js.map +1 -0
  149. package/{helpers/angular → hmr/frameworks/angular/build}/synthesize-injectable-factories.js +1 -174
  150. package/hmr/frameworks/angular/build/synthesize-injectable-factories.js.map +1 -0
  151. package/{helpers/angular → hmr/frameworks/angular/build}/util.d.ts +1 -0
  152. package/hmr/frameworks/angular/build/util.js +155 -0
  153. package/hmr/frameworks/angular/build/util.js.map +1 -0
  154. package/hmr/frameworks/angular/client/index.js +542 -175
  155. package/hmr/frameworks/angular/client/index.js.map +1 -1
  156. package/hmr/frameworks/angular/client/strategy.d.ts +9 -0
  157. package/hmr/frameworks/angular/client/strategy.js +19 -0
  158. package/hmr/frameworks/angular/client/strategy.js.map +1 -0
  159. package/hmr/frameworks/angular/server/angular-root-component.d.ts +79 -0
  160. package/hmr/frameworks/angular/server/angular-root-component.js +149 -0
  161. package/hmr/frameworks/angular/server/angular-root-component.js.map +1 -0
  162. package/hmr/frameworks/angular/server/linker.js +1 -4
  163. package/hmr/frameworks/angular/server/linker.js.map +1 -1
  164. package/hmr/frameworks/angular/server/strategy.js +458 -12
  165. package/hmr/frameworks/angular/server/strategy.js.map +1 -1
  166. package/hmr/{server → frameworks/angular/server}/websocket-angular-entry.js +2 -2
  167. package/hmr/frameworks/angular/server/websocket-angular-entry.js.map +1 -0
  168. package/hmr/{server → frameworks/angular/server}/websocket-angular-hot-update.d.ts +14 -11
  169. package/hmr/{server → frameworks/angular/server}/websocket-angular-hot-update.js +108 -111
  170. package/hmr/frameworks/angular/server/websocket-angular-hot-update.js.map +1 -0
  171. package/hmr/frameworks/react/server/strategy.d.ts +2 -0
  172. package/hmr/frameworks/react/server/strategy.js +230 -0
  173. package/hmr/frameworks/react/server/strategy.js.map +1 -0
  174. package/hmr/frameworks/solid/build/solid-jsx-deps.d.ts +15 -0
  175. package/hmr/frameworks/solid/build/solid-jsx-deps.js +178 -0
  176. package/hmr/frameworks/solid/build/solid-jsx-deps.js.map +1 -0
  177. package/hmr/frameworks/solid/client/app-runtime.d.ts +68 -0
  178. package/hmr/frameworks/solid/client/app-runtime.js +246 -0
  179. package/hmr/frameworks/solid/client/app-runtime.js.map +1 -0
  180. package/hmr/frameworks/solid/server/strategy.js +291 -16
  181. package/hmr/frameworks/solid/server/strategy.js.map +1 -1
  182. package/hmr/frameworks/typescript/server/strategy.js +38 -14
  183. package/hmr/frameworks/typescript/server/strategy.js.map +1 -1
  184. package/hmr/frameworks/vue/client/dep-propagation.d.ts +44 -0
  185. package/hmr/frameworks/vue/client/dep-propagation.js +148 -0
  186. package/hmr/frameworks/vue/client/dep-propagation.js.map +1 -0
  187. package/hmr/frameworks/vue/client/index.d.ts +9 -0
  188. package/hmr/frameworks/vue/client/index.js +251 -247
  189. package/hmr/frameworks/vue/client/index.js.map +1 -1
  190. package/hmr/frameworks/vue/client/strategy.d.ts +33 -0
  191. package/hmr/frameworks/vue/client/strategy.js +157 -0
  192. package/hmr/frameworks/vue/client/strategy.js.map +1 -0
  193. package/hmr/frameworks/vue/client/vue-sfc-update-overlay.d.ts +49 -0
  194. package/hmr/frameworks/vue/client/vue-sfc-update-overlay.js +142 -0
  195. package/hmr/frameworks/vue/client/vue-sfc-update-overlay.js.map +1 -0
  196. package/hmr/frameworks/vue/server/compiler.d.ts +3 -1
  197. package/hmr/frameworks/vue/server/compiler.js +3 -1
  198. package/hmr/frameworks/vue/server/compiler.js.map +1 -1
  199. package/hmr/frameworks/vue/server/sfc-route-assemble.d.ts +7 -0
  200. package/hmr/frameworks/vue/server/sfc-route-assemble.js +823 -0
  201. package/hmr/frameworks/vue/server/sfc-route-assemble.js.map +1 -0
  202. package/hmr/frameworks/vue/server/sfc-route-meta.d.ts +7 -0
  203. package/hmr/frameworks/vue/server/sfc-route-meta.js +80 -0
  204. package/hmr/frameworks/vue/server/sfc-route-meta.js.map +1 -0
  205. package/hmr/frameworks/vue/server/sfc-route-serve.d.ts +8 -0
  206. package/hmr/frameworks/vue/server/sfc-route-serve.js +462 -0
  207. package/hmr/frameworks/vue/server/sfc-route-serve.js.map +1 -0
  208. package/hmr/frameworks/vue/server/sfc-route-shared.d.ts +45 -0
  209. package/hmr/frameworks/vue/server/sfc-route-shared.js +105 -0
  210. package/hmr/frameworks/vue/server/sfc-route-shared.js.map +1 -0
  211. package/hmr/frameworks/vue/server/strategy.d.ts +35 -0
  212. package/hmr/frameworks/vue/server/strategy.js +286 -7
  213. package/hmr/frameworks/vue/server/strategy.js.map +1 -1
  214. package/hmr/frameworks/vue/server/websocket-sfc.d.ts +15 -0
  215. package/hmr/frameworks/vue/server/websocket-sfc.js +20 -0
  216. package/hmr/frameworks/vue/server/websocket-sfc.js.map +1 -0
  217. package/hmr/helpers/ast-normalizer.d.ts +3 -1
  218. package/hmr/helpers/ast-normalizer.js +77 -10
  219. package/hmr/helpers/ast-normalizer.js.map +1 -1
  220. package/hmr/helpers/cjs-named-exports.d.ts +23 -0
  221. package/hmr/helpers/cjs-named-exports.js +152 -0
  222. package/hmr/helpers/cjs-named-exports.js.map +1 -0
  223. package/hmr/helpers/package-exports.d.ts +16 -0
  224. package/hmr/helpers/package-exports.js +396 -0
  225. package/hmr/helpers/package-exports.js.map +1 -0
  226. package/hmr/helpers/vendor-rewrite.d.ts +1 -1
  227. package/hmr/helpers/vendor-rewrite.js +5 -3
  228. package/hmr/helpers/vendor-rewrite.js.map +1 -1
  229. package/hmr/server/boot-archive-route.d.ts +40 -0
  230. package/hmr/server/boot-archive-route.js +154 -0
  231. package/hmr/server/boot-archive-route.js.map +1 -0
  232. package/hmr/server/boot-closure-route.d.ts +49 -0
  233. package/hmr/server/boot-closure-route.js +144 -0
  234. package/hmr/server/boot-closure-route.js.map +1 -0
  235. package/hmr/server/boot-recording.d.ts +58 -0
  236. package/hmr/server/boot-recording.js +228 -0
  237. package/hmr/server/boot-recording.js.map +1 -0
  238. package/hmr/server/constants.js +20 -5
  239. package/hmr/server/constants.js.map +1 -1
  240. package/hmr/server/core-bundle.d.ts +140 -0
  241. package/hmr/server/core-bundle.js +614 -0
  242. package/hmr/server/core-bundle.js.map +1 -0
  243. package/hmr/server/core-sanitize.d.ts +86 -7
  244. package/hmr/server/core-sanitize.js +170 -45
  245. package/hmr/server/core-sanitize.js.map +1 -1
  246. package/hmr/server/css-device-module.d.ts +28 -0
  247. package/hmr/server/css-device-module.js +44 -0
  248. package/hmr/server/css-device-module.js.map +1 -0
  249. package/hmr/server/deps-bundle.d.ts +218 -0
  250. package/hmr/server/deps-bundle.js +984 -0
  251. package/hmr/server/deps-bundle.js.map +1 -0
  252. package/hmr/server/device-transform-helpers.d.ts +24 -0
  253. package/hmr/server/device-transform-helpers.js +408 -0
  254. package/hmr/server/device-transform-helpers.js.map +1 -0
  255. package/hmr/server/framework-strategy.d.ts +113 -11
  256. package/hmr/server/hmr-module-graph.d.ts +37 -0
  257. package/hmr/server/hmr-module-graph.js +215 -0
  258. package/hmr/server/hmr-module-graph.js.map +1 -0
  259. package/hmr/server/import-map.d.ts +13 -4
  260. package/hmr/server/import-map.js +94 -47
  261. package/hmr/server/import-map.js.map +1 -1
  262. package/hmr/server/index.js +7 -16
  263. package/hmr/server/index.js.map +1 -1
  264. package/hmr/server/ns-core-cjs-shape.d.ts +2 -4
  265. package/hmr/server/ns-core-cjs-shape.js +4 -6
  266. package/hmr/server/ns-core-cjs-shape.js.map +1 -1
  267. package/hmr/server/ns-m-response-memo.d.ts +10 -0
  268. package/hmr/server/ns-m-response-memo.js +33 -0
  269. package/hmr/server/ns-m-response-memo.js.map +1 -0
  270. package/hmr/server/ns-rt-bridge.d.ts +51 -0
  271. package/hmr/server/ns-rt-bridge.js +131 -0
  272. package/hmr/server/ns-rt-bridge.js.map +1 -0
  273. package/hmr/server/ns-rt-route.d.ts +5 -0
  274. package/hmr/server/ns-rt-route.js +39 -0
  275. package/hmr/server/ns-rt-route.js.map +1 -0
  276. package/hmr/server/perf-instrumentation.d.ts +6 -10
  277. package/hmr/server/perf-instrumentation.js +5 -6
  278. package/hmr/server/perf-instrumentation.js.map +1 -1
  279. package/hmr/server/process-code-for-device.d.ts +30 -0
  280. package/hmr/server/process-code-for-device.js +791 -0
  281. package/hmr/server/process-code-for-device.js.map +1 -0
  282. package/hmr/server/require-guard.d.ts +1 -0
  283. package/hmr/server/require-guard.js +12 -0
  284. package/hmr/server/require-guard.js.map +1 -0
  285. package/hmr/server/rewrite-imports.d.ts +2 -0
  286. package/hmr/server/rewrite-imports.js +630 -0
  287. package/hmr/server/rewrite-imports.js.map +1 -0
  288. package/hmr/server/route-helpers.d.ts +7 -0
  289. package/hmr/server/route-helpers.js +13 -0
  290. package/hmr/server/route-helpers.js.map +1 -0
  291. package/hmr/server/server-origin.d.ts +2 -0
  292. package/hmr/server/server-origin.js +83 -0
  293. package/hmr/server/server-origin.js.map +1 -0
  294. package/hmr/server/shared-transform-request.js +1 -2
  295. package/hmr/server/shared-transform-request.js.map +1 -1
  296. package/hmr/server/transform-cache-invalidation.d.ts +37 -0
  297. package/hmr/server/transform-cache-invalidation.js +156 -0
  298. package/hmr/server/transform-cache-invalidation.js.map +1 -0
  299. package/hmr/server/vendor-bare-module-shims.d.ts +4 -0
  300. package/hmr/server/vendor-bare-module-shims.js +80 -0
  301. package/hmr/server/vendor-bare-module-shims.js.map +1 -0
  302. package/hmr/server/vite-plugin.d.ts +1 -0
  303. package/hmr/server/vite-plugin.js +180 -54
  304. package/hmr/server/vite-plugin.js.map +1 -1
  305. package/hmr/server/websocket-core-bridge.d.ts +51 -4
  306. package/hmr/server/websocket-core-bridge.js +62 -54
  307. package/hmr/server/websocket-core-bridge.js.map +1 -1
  308. package/hmr/server/websocket-css-hot-update.d.ts +33 -0
  309. package/hmr/server/websocket-css-hot-update.js +65 -0
  310. package/hmr/server/websocket-css-hot-update.js.map +1 -0
  311. package/hmr/server/websocket-device-transform.d.ts +3 -0
  312. package/hmr/server/websocket-device-transform.js +7 -0
  313. package/hmr/server/websocket-device-transform.js.map +1 -0
  314. package/hmr/server/websocket-hmr-pending.d.ts +3 -9
  315. package/hmr/server/websocket-hmr-pending.js +1 -1
  316. package/hmr/server/websocket-hmr-pending.js.map +1 -1
  317. package/hmr/server/websocket-hot-update.d.ts +77 -0
  318. package/hmr/server/websocket-hot-update.js +360 -0
  319. package/hmr/server/websocket-hot-update.js.map +1 -0
  320. package/hmr/server/websocket-import-map-route.d.ts +15 -0
  321. package/hmr/server/websocket-import-map-route.js +50 -0
  322. package/hmr/server/websocket-import-map-route.js.map +1 -0
  323. package/hmr/server/websocket-module-bindings.js +3 -3
  324. package/hmr/server/websocket-module-bindings.js.map +1 -1
  325. package/hmr/server/websocket-module-specifiers.d.ts +66 -2
  326. package/hmr/server/websocket-module-specifiers.js +191 -20
  327. package/hmr/server/websocket-module-specifiers.js.map +1 -1
  328. package/hmr/server/websocket-ns-core.d.ts +34 -0
  329. package/hmr/server/websocket-ns-core.js +414 -0
  330. package/hmr/server/websocket-ns-core.js.map +1 -0
  331. package/hmr/server/websocket-ns-entry.d.ts +21 -0
  332. package/hmr/server/websocket-ns-entry.js +164 -0
  333. package/hmr/server/websocket-ns-entry.js.map +1 -0
  334. package/hmr/server/websocket-ns-m-paths.d.ts +1 -3
  335. package/hmr/server/websocket-ns-m-paths.js +25 -82
  336. package/hmr/server/websocket-ns-m-paths.js.map +1 -1
  337. package/hmr/server/websocket-ns-m-request.d.ts +11 -1
  338. package/hmr/server/websocket-ns-m-request.js +17 -9
  339. package/hmr/server/websocket-ns-m-request.js.map +1 -1
  340. package/hmr/server/websocket-ns-m.d.ts +52 -0
  341. package/hmr/server/websocket-ns-m.js +824 -0
  342. package/hmr/server/websocket-ns-m.js.map +1 -0
  343. package/hmr/server/websocket-served-module-helpers.d.ts +50 -4
  344. package/hmr/server/websocket-served-module-helpers.js +384 -86
  345. package/hmr/server/websocket-served-module-helpers.js.map +1 -1
  346. package/hmr/server/websocket-txn.js +6 -9
  347. package/hmr/server/websocket-txn.js.map +1 -1
  348. package/hmr/server/websocket-vendor-unifier.d.ts +0 -1
  349. package/hmr/server/websocket-vendor-unifier.js +4 -9
  350. package/hmr/server/websocket-vendor-unifier.js.map +1 -1
  351. package/hmr/server/websocket.d.ts +7 -39
  352. package/hmr/server/websocket.js +643 -6716
  353. package/hmr/server/websocket.js.map +1 -1
  354. package/hmr/shared/ns-globals.d.ts +108 -0
  355. package/hmr/shared/ns-globals.js +28 -0
  356. package/hmr/shared/ns-globals.js.map +1 -0
  357. package/hmr/shared/protocol.d.ts +145 -0
  358. package/hmr/shared/protocol.js +28 -0
  359. package/hmr/shared/protocol.js.map +1 -0
  360. package/hmr/shared/runtime/boot-archive-format.d.ts +16 -0
  361. package/hmr/shared/runtime/boot-archive-format.js +31 -0
  362. package/hmr/shared/runtime/boot-archive-format.js.map +1 -0
  363. package/hmr/shared/runtime/boot-complete.d.ts +14 -0
  364. package/hmr/shared/runtime/boot-complete.js +32 -0
  365. package/hmr/shared/runtime/boot-complete.js.map +1 -0
  366. package/hmr/shared/runtime/boot-placeholder-ui.d.ts +69 -0
  367. package/hmr/shared/runtime/boot-placeholder-ui.js +101 -0
  368. package/hmr/shared/runtime/boot-placeholder-ui.js.map +1 -0
  369. package/hmr/shared/runtime/boot-progress.d.ts +44 -0
  370. package/hmr/shared/runtime/boot-progress.js +134 -0
  371. package/hmr/shared/runtime/boot-progress.js.map +1 -0
  372. package/hmr/shared/runtime/boot-timeline.d.ts +2 -1
  373. package/hmr/shared/runtime/boot-timeline.js +9 -22
  374. package/hmr/shared/runtime/boot-timeline.js.map +1 -1
  375. package/hmr/shared/runtime/browser-runtime-contract.d.ts +53 -22
  376. package/hmr/shared/runtime/browser-runtime-contract.js +6 -9
  377. package/hmr/shared/runtime/browser-runtime-contract.js.map +1 -1
  378. package/hmr/shared/runtime/dev-overlay-snapshots.d.ts +31 -0
  379. package/hmr/shared/runtime/dev-overlay-snapshots.js +324 -0
  380. package/hmr/shared/runtime/dev-overlay-snapshots.js.map +1 -0
  381. package/hmr/shared/runtime/dev-overlay.d.ts +76 -30
  382. package/hmr/shared/runtime/dev-overlay.js +767 -408
  383. package/hmr/shared/runtime/dev-overlay.js.map +1 -1
  384. package/hmr/shared/runtime/global-scope.d.ts +18 -0
  385. package/hmr/shared/runtime/global-scope.js +21 -0
  386. package/hmr/shared/runtime/global-scope.js.map +1 -0
  387. package/hmr/shared/runtime/hooks.js +2 -1
  388. package/hmr/shared/runtime/hooks.js.map +1 -1
  389. package/hmr/shared/runtime/http-only-boot.js +7 -6
  390. package/hmr/shared/runtime/http-only-boot.js.map +1 -1
  391. package/hmr/shared/runtime/module-provenance.js +4 -6
  392. package/hmr/shared/runtime/module-provenance.js.map +1 -1
  393. package/hmr/shared/runtime/root-placeholder-view.d.ts +19 -0
  394. package/hmr/shared/runtime/root-placeholder-view.js +311 -0
  395. package/hmr/shared/runtime/root-placeholder-view.js.map +1 -0
  396. package/hmr/shared/runtime/root-placeholder.js +398 -204
  397. package/hmr/shared/runtime/root-placeholder.js.map +1 -1
  398. package/hmr/shared/runtime/session-bootstrap.js +255 -50
  399. package/hmr/shared/runtime/session-bootstrap.js.map +1 -1
  400. package/hmr/shared/runtime/vendor-bootstrap.js +3 -10
  401. package/hmr/shared/runtime/vendor-bootstrap.js.map +1 -1
  402. package/hmr/shared/vendor/manifest-collect.d.ts +4 -0
  403. package/hmr/shared/vendor/manifest-collect.js +549 -0
  404. package/hmr/shared/vendor/manifest-collect.js.map +1 -0
  405. package/hmr/shared/vendor/manifest-loader.d.ts +2 -1
  406. package/hmr/shared/vendor/manifest-loader.js +5 -3
  407. package/hmr/shared/vendor/manifest-loader.js.map +1 -1
  408. package/hmr/shared/vendor/manifest.d.ts +1 -7
  409. package/hmr/shared/vendor/manifest.js +125 -825
  410. package/hmr/shared/vendor/manifest.js.map +1 -1
  411. package/hmr/shared/vendor/registry.d.ts +2 -0
  412. package/hmr/shared/vendor/registry.js +14 -0
  413. package/hmr/shared/vendor/registry.js.map +1 -1
  414. package/hmr/shared/vendor/vendor-device-shim.d.ts +1 -0
  415. package/hmr/shared/vendor/vendor-device-shim.js +208 -0
  416. package/hmr/shared/vendor/vendor-device-shim.js.map +1 -0
  417. package/hmr/shared/vendor/vendor-esbuild-plugins.d.ts +38 -0
  418. package/hmr/shared/vendor/vendor-esbuild-plugins.js +296 -0
  419. package/hmr/shared/vendor/vendor-esbuild-plugins.js.map +1 -0
  420. package/hmr/vendor-bootstrap.d.ts +1 -3
  421. package/hmr/vendor-bootstrap.js +4 -6
  422. package/hmr/vendor-bootstrap.js.map +1 -1
  423. package/index.d.ts +1 -0
  424. package/index.js +5 -0
  425. package/index.js.map +1 -1
  426. package/package.json +65 -11
  427. package/runtime/core-aliases-early.js +25 -55
  428. package/runtime/core-aliases-early.js.map +1 -1
  429. package/shims/react-jsx-runtime.d.ts +4 -0
  430. package/shims/react-jsx-runtime.js +61 -0
  431. package/shims/react-jsx-runtime.js.map +1 -0
  432. package/helpers/angular/angular-linker.d.ts +0 -13
  433. package/helpers/angular/angular-linker.js +0 -194
  434. package/helpers/angular/angular-linker.js.map +0 -1
  435. package/helpers/angular/inline-decorator-component-templates.js.map +0 -1
  436. package/helpers/angular/shared-linker.d.ts +0 -11
  437. package/helpers/angular/shared-linker.js +0 -75
  438. package/helpers/angular/shared-linker.js.map +0 -1
  439. package/helpers/angular/synthesize-decorator-ctor-parameters.js.map +0 -1
  440. package/helpers/angular/synthesize-injectable-factories.js.map +0 -1
  441. package/helpers/angular/util.js +0 -67
  442. package/helpers/angular/util.js.map +0 -1
  443. package/helpers/prelink-angular.d.ts +0 -2
  444. package/helpers/prelink-angular.js +0 -117
  445. package/helpers/prelink-angular.js.map +0 -1
  446. package/hmr/server/websocket-angular-entry.js.map +0 -1
  447. package/hmr/server/websocket-angular-hot-update.js.map +0 -1
  448. package/hmr/server/websocket-ns-m-finalize.d.ts +0 -22
  449. package/hmr/server/websocket-ns-m-finalize.js +0 -88
  450. package/hmr/server/websocket-ns-m-finalize.js.map +0 -1
  451. package/hmr/server/websocket-runtime-compat.d.ts +0 -19
  452. package/hmr/server/websocket-runtime-compat.js +0 -286
  453. package/hmr/server/websocket-runtime-compat.js.map +0 -1
  454. package/hmr/server/websocket-vue-sfc.d.ts +0 -27
  455. package/hmr/server/websocket-vue-sfc.js +0 -1114
  456. package/hmr/server/websocket-vue-sfc.js.map +0 -1
  457. package/transformers/NativeClass/index.d.ts +0 -2
  458. package/transformers/NativeClass/index.js +0 -222
  459. package/transformers/NativeClass/index.js.map +0 -1
  460. /package/{helpers/angular → hmr/frameworks/angular/build}/inline-decorator-component-templates.d.ts +0 -0
  461. /package/{helpers/angular → hmr/frameworks/angular/build}/synthesize-decorator-ctor-parameters.d.ts +0 -0
  462. /package/{helpers/angular → hmr/frameworks/angular/build}/synthesize-injectable-factories.d.ts +0 -0
  463. /package/hmr/{server → frameworks/angular/server}/websocket-angular-entry.d.ts +0 -0
@@ -5,9 +5,13 @@
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, hasExplicitEviction, invalidateModulesByUrls, buildEvictionUrls, emitHmrModeBannerOnce } from './utils.js';
9
- import { handleCssUpdates } from './css-handler.js';
10
- const VERBOSE = typeof __NS_ENV_VERBOSE__ !== 'undefined' && __NS_ENV_VERBOSE__;
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
+ import { applyCssText, handleCssUpdates } from './css-handler.js';
10
+ import { buildCssApplyingDetail, buildCssAppliedDetail } from './css-update-overlay.js';
11
+ import { getGlobalScope } from '../shared/runtime/global-scope.js';
12
+ import { markDevBootComplete } from '../shared/runtime/boot-complete.js';
13
+ import { getNsHotRegistry } from './hot-context.js';
14
+ const VERBOSE = ENV_VERBOSE;
11
15
  function resolveTargetFlavor() {
12
16
  try {
13
17
  if (typeof __NS_TARGET_FLAVOR__ !== 'undefined' && __NS_TARGET_FLAVOR__) {
@@ -16,7 +20,7 @@ function resolveTargetFlavor() {
16
20
  }
17
21
  catch { }
18
22
  try {
19
- const g = globalThis;
23
+ const g = getGlobalScope();
20
24
  if (typeof g.__NS_TARGET_FLAVOR__ === 'string' && g.__NS_TARGET_FLAVOR__) {
21
25
  return g.__NS_TARGET_FLAVOR__;
22
26
  }
@@ -34,13 +38,24 @@ function resolveTargetFlavor() {
34
38
  return undefined;
35
39
  }
36
40
  const TARGET_FLAVOR = resolveTargetFlavor();
41
+ // React reuses the generic TypeScript HMR path on BOTH server and client: it has
42
+ // no Fast Refresh, so a module edit drives a plain module reload / root reset
43
+ // (the React tree re-renders), exactly like the `typescript` flavor. The server
44
+ // strategy is `{ ...typescriptServerStrategy, flavor: 'react' }`; this mirrors that
45
+ // on the client so the `typescript`-gated update branches also run for React
46
+ // (otherwise a React edit is received but never applied — the overlay sticks).
47
+ const TS_LIKE_FLAVOR = TARGET_FLAVOR === 'typescript' || TARGET_FLAVOR === 'react';
37
48
  try {
38
49
  if (TARGET_FLAVOR && !globalThis.__NS_TARGET_FLAVOR__) {
39
50
  globalThis.__NS_TARGET_FLAVOR__ = TARGET_FLAVOR;
40
51
  }
41
52
  }
42
53
  catch { }
43
- const APP_ROOT_VIRTUAL = typeof __NS_APP_ROOT_VIRTUAL__ === 'string' && __NS_APP_ROOT_VIRTUAL__ ? __NS_APP_ROOT_VIRTUAL__ : '/src';
54
+ // Define substitution does NOT reach this file (served raw from node_modules),
55
+ // so prefer the globalThis seed planted by the entry's defines-seed module —
56
+ // the '/src' literal is a last-resort default and is WRONG for 'app/'-rooted
57
+ // projects.
58
+ 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
59
  const APP_VIRTUAL_WITH_SLASH = APP_ROOT_VIRTUAL.endsWith('/') ? APP_ROOT_VIRTUAL : `${APP_ROOT_VIRTUAL}/`;
45
60
  const APP_MAIN_ENTRY_SPEC = `${APP_VIRTUAL_WITH_SLASH}app.ts`;
46
61
  // Policy: by default, let the app's own main entry mount initially; HMR client handles updates/remounts only.
@@ -76,6 +91,41 @@ function ensureCoreAliasesOnGlobalThis() {
76
91
  }
77
92
  // Apply once on module evaluation
78
93
  ensureCoreAliasesOnGlobalThis();
94
+ // Install the device CSS applier. CSS-bearing modules (a Vue SFC `<style>` via
95
+ // `/ns/asm`, an imported `.css` via `/ns/m`) call `__NS_REGISTER_CSS__(tag, css)`,
96
+ // or queue in `__NS_PENDING_CSS__` if they ran first; the drain below covers that.
97
+ // Routes through the shared `applyCssText` (tagged remove+add + root restyle),
98
+ // the same path app.css and live `.css` edits use.
99
+ function installCssRegister() {
100
+ const g = getGlobalScope();
101
+ if (typeof g.__NS_REGISTER_CSS__ === 'function') {
102
+ return;
103
+ }
104
+ const apply = (tag, cssText) => {
105
+ if (typeof cssText !== 'string' || !cssText.length) {
106
+ return;
107
+ }
108
+ try {
109
+ applyCssText(cssText, tag);
110
+ }
111
+ catch (cssErr) {
112
+ if (VERBOSE)
113
+ console.warn('[ns-hmr] CSS register/apply failed for', tag, cssErr?.message || cssErr);
114
+ }
115
+ };
116
+ g.__NS_REGISTER_CSS__ = apply;
117
+ try {
118
+ const pending = g.__NS_PENDING_CSS__;
119
+ if (pending && typeof pending === 'object') {
120
+ for (const tag of Object.keys(pending)) {
121
+ apply(tag, pending[tag]);
122
+ }
123
+ g.__NS_PENDING_CSS__ = null;
124
+ }
125
+ }
126
+ catch { }
127
+ }
128
+ installCssRegister();
79
129
  function getHmrOverlayApi() {
80
130
  try {
81
131
  return globalThis.__NS_HMR_DEV_OVERLAY__ || null;
@@ -101,15 +151,71 @@ function hideConnectionOverlay() {
101
151
  }
102
152
  catch { }
103
153
  }
104
- // alpha.62 follow-up — Eagerly drive the HMR-applying overlay's
105
- // 'received' frame as soon as the server emits `ns:hmr-pending`,
106
- // BEFORE the framework-specific (`ns:angular-update` /
107
- // `ns:css-updates`) payload arrives. The flavor-specific handler
108
- // later walks through 'evicting' → 'reimporting' → 'rebooting' →
109
- // 'complete'. Calling 'received' twice in the same cycle is safe:
110
- // the overlay preserves `updateCycleStartedAt` when a 'received'
111
- // frame replaces an existing 'received' frame so the
112
- // minimum-visible window is still timed against the FIRST frame.
154
+ function setUpdateOverlayStage(stage, info) {
155
+ try {
156
+ const api = getHmrOverlayApi();
157
+ if (api && typeof api.setUpdateStage === 'function') {
158
+ api.setUpdateStage(stage, info);
159
+ }
160
+ }
161
+ catch { }
162
+ }
163
+ // Store the listener registry on globalThis (rather than in a module-private
164
+ // closure) because in NativeScript the HMR client module and the user app
165
+ // modules can resolve to different module instances depending on how the
166
+ // dev runtime loads them (HTTP client URL vs. the bundled vendor realm).
167
+ // A module-local Set would not be shared across instances; the global one
168
+ // is.
169
+ function getNsSolidHmrListenerSet() {
170
+ const g = getGlobalScope();
171
+ let set = g.__ns_solid_hmr_listener_set;
172
+ if (!set) {
173
+ set = new Set();
174
+ g.__ns_solid_hmr_listener_set = set;
175
+ }
176
+ return set;
177
+ }
178
+ function nsSolidHmrSubscribe(fn) {
179
+ const listeners = getNsSolidHmrListenerSet();
180
+ listeners.add(fn);
181
+ if (VERBOSE)
182
+ console.log('[hmr][solid] subscribe — listeners=', listeners.size);
183
+ return () => listeners.delete(fn);
184
+ }
185
+ function nsSolidHmrEmit(ev) {
186
+ const listeners = getNsSolidHmrListenerSet();
187
+ if (VERBOSE)
188
+ console.log('[hmr][solid] emit listeners=', listeners.size, 'changedFiles=', ev.changedFiles);
189
+ for (const fn of Array.from(listeners)) {
190
+ try {
191
+ fn(ev);
192
+ }
193
+ catch (err) {
194
+ if (VERBOSE)
195
+ console.warn('[hmr][solid] listener threw', err);
196
+ }
197
+ }
198
+ }
199
+ try {
200
+ const g = getGlobalScope();
201
+ g.__ns_solid_hmr_subscribe = nsSolidHmrSubscribe;
202
+ // Eagerly create the listener set so the global exists at module load time.
203
+ getNsSolidHmrListenerSet();
204
+ if (VERBOSE)
205
+ console.log('[hmr][solid] HMR client loaded. global set=', typeof g.__ns_solid_hmr_subscribe, 'listenerSet=', typeof g.__ns_solid_hmr_listener_set);
206
+ }
207
+ catch (err) {
208
+ console.warn('[hmr][solid] could not install global __ns_solid_hmr_subscribe', err);
209
+ }
210
+ // Eagerly drive the HMR-applying overlay's 'received' frame as soon
211
+ // as the server emits `ns:hmr-pending`, BEFORE the framework-specific
212
+ // (`ns:angular-update` / `ns:css-updates`) payload arrives. The
213
+ // flavor-specific handler later walks through 'evicting' →
214
+ // 'reimporting' → 'rebooting' → 'complete'. Calling 'received' twice
215
+ // in the same cycle is safe: the overlay preserves
216
+ // `updateCycleStartedAt` when a 'received' frame replaces an existing
217
+ // 'received' frame so the minimum-visible window is still timed
218
+ // against the FIRST frame.
113
219
  //
114
220
  // Soft-fails when the overlay isn't installed (production builds,
115
221
  // vitest, etc.) or when the user opted out via
@@ -124,6 +230,22 @@ let hasOpenedHmrSocket = false;
124
230
  let awaitingHealthyHmrMessage = false;
125
231
  let pendingConnectionOverlayStage = 'connecting';
126
232
  let pendingConnectionOverlayDetail = '';
233
+ // The stage currently PAINTED on screen (set only when we actually show
234
+ // the overlay, not when we merely schedule it). Used to suppress the
235
+ // sub-perceptible offline↔reconnecting flicker during the reconnect
236
+ // retry loop — see the deferral in `connectHmr`/`tryNext`.
237
+ let shownConnectionOverlayStage = null;
238
+ // While the terminal 'offline' frame is showing, a fresh reconnect
239
+ // attempt that is refused near-instantly (the norm when the dev server
240
+ // is down — especially on the Android emulator, where connection-refused
241
+ // returns in ~1ms) would flip the overlay to 'reconnecting' and straight
242
+ // back to 'offline', producing a jarring 1-frame flicker. We defer the
243
+ // 'reconnecting' frame by this much so an instant failure (→ back to
244
+ // 'offline') or a success (→ 'synchronizing') cancels it before it ever
245
+ // paints; only a genuinely in-flight attempt surfaces 'reconnecting'.
246
+ // On iOS real connect attempts exceed this threshold, so the behaviour
247
+ // there is unchanged.
248
+ const RECONNECTING_OVER_OFFLINE_DELAY_MS = 500;
127
249
  function clearConnectionOverlayTimer() {
128
250
  if (connectionOverlayTimer) {
129
251
  clearTimeout(connectionOverlayTimer);
@@ -131,9 +253,14 @@ function clearConnectionOverlayTimer() {
131
253
  }
132
254
  }
133
255
  function showConnectionOverlayNow(stage, detail) {
256
+ // Painting a stage now supersedes any scheduled (deferred) stage, so
257
+ // cancel the pending timer — otherwise a deferred 'reconnecting' could
258
+ // fire moments after we settle back on 'offline' and revive the flicker.
259
+ clearConnectionOverlayTimer();
134
260
  pendingConnectionOverlayStage = stage;
135
261
  pendingConnectionOverlayDetail = detail || '';
136
262
  connectionOverlayVisible = true;
263
+ shownConnectionOverlayStage = stage;
137
264
  setConnectionOverlayStage(stage, detail);
138
265
  }
139
266
  function scheduleConnectionOverlay(stage, detail, delayMs = 1200) {
@@ -154,24 +281,25 @@ function updateConnectionOverlay(stage, detail) {
154
281
  function markHmrConnectionHealthy() {
155
282
  awaitingHealthyHmrMessage = false;
156
283
  clearConnectionOverlayTimer();
284
+ shownConnectionOverlayStage = null;
157
285
  if (connectionOverlayVisible) {
158
286
  connectionOverlayVisible = false;
159
287
  hideConnectionOverlay();
160
288
  }
161
289
  }
162
- /**
163
- * Flavor hooks
164
- */
165
- import { installNsVueDevShims, ensureBackWrapperInstalled, getRootForVue, loadSfcComponent, ensureVueGlobals, ensurePiniaOnApp, addSfcMapping, recordVuePayloadChanges, handleVueSfcRegistry, handleVueSfcRegistryUpdate, sfcArtifactMap } from '../frameworks/vue/client/index.js';
166
- import { handleAngularHotUpdateMessage, installAngularHmrClientHooks } from '../frameworks/angular/client/index.js';
167
- switch (TARGET_FLAVOR) {
168
- case 'vue':
169
- installNsVueDevShims();
170
- break;
171
- case 'angular':
172
- installAngularHmrClientHooks();
173
- break;
174
- }
290
+ let CLIENT_STRATEGY;
291
+ const CLIENT_STRATEGY_READY = TARGET_FLAVOR === 'vue' || TARGET_FLAVOR === 'angular'
292
+ ? import(`../frameworks/${TARGET_FLAVOR}/client/strategy.js`)
293
+ .then((mod) => {
294
+ CLIENT_STRATEGY = mod && mod[`${TARGET_FLAVOR}ClientStrategy`];
295
+ if (VERBOSE)
296
+ console.log('[hmr-client] client strategy loaded for flavor:', TARGET_FLAVOR);
297
+ CLIENT_STRATEGY?.install();
298
+ })
299
+ .catch((err) => {
300
+ console.warn('[hmr-client] failed to load client strategy for', TARGET_FLAVOR, err);
301
+ })
302
+ : Promise.resolve();
175
303
  // Track whether we've mounted an initial app root yet in HTTP-only boot
176
304
  let initialMounted = !!globalThis.__NS_HMR_BOOT_COMPLETE__;
177
305
  // Prevent duplicate initial-mount scheduling across rapid full-graph broadcasts and re-evaluations
@@ -189,7 +317,7 @@ let processingPromise = null;
189
317
  // Detect whether the early placeholder root is still active on screen
190
318
  function isPlaceholderActive() {
191
319
  try {
192
- const g = globalThis;
320
+ const g = getGlobalScope();
193
321
  if (g.__NS_DEV_PLACEHOLDER_ROOT_VIEW__)
194
322
  return true;
195
323
  if (g.__NS_DEV_PLACEHOLDER_ROOT_EARLY__)
@@ -219,9 +347,9 @@ function applyFullGraph(payload) {
219
347
  // causes a double-mount race (rescue fires at 450ms, then main.ts fires ~1s later,
220
348
  // causing a visual flash and leaving the app in an inconsistent state).
221
349
  try {
222
- const g = globalThis;
350
+ const g = getGlobalScope();
223
351
  const bootDone = !!g.__NS_HMR_BOOT_COMPLETE__;
224
- if (!bootDone && !initialMounted && !initialMounting && !g.__NS_HMR_RESCUE_SCHEDULED__ && TARGET_FLAVOR !== 'typescript') {
352
+ if (!bootDone && !initialMounted && !initialMounting && !g.__NS_HMR_RESCUE_SCHEDULED__ && !TS_LIKE_FLAVOR) {
225
353
  // simple snapshot helpers
226
354
  const getTopmost = () => {
227
355
  try {
@@ -271,10 +399,7 @@ function applyFullGraph(payload) {
271
399
  if (g.__NS_HMR_BOOT_COMPLETE__)
272
400
  return; // app already mounted
273
401
  if (hasRealRoot()) {
274
- try {
275
- g.__NS_HMR_BOOT_COMPLETE__ = true;
276
- }
277
- catch { }
402
+ markDevBootComplete();
278
403
  if (VERBOSE)
279
404
  console.log('[hmr][init] detected real root after delay; skipping client initial mount');
280
405
  // Detach placeholder handler if still present
@@ -294,7 +419,7 @@ function applyFullGraph(payload) {
294
419
  if (VERBOSE)
295
420
  console.log('[hmr][init] placeholder persists after delay; evaluating rescue policy');
296
421
  // Flavor-specific rescue handling
297
- if (TARGET_FLAVOR === 'typescript') {
422
+ if (TS_LIKE_FLAVOR) {
298
423
  // For TS apps, perform a one-time resetRootView to the conventional
299
424
  // app root module. This mimics what Application.run would do and
300
425
  // replaces the placeholder with the real UI without trying to
@@ -311,10 +436,7 @@ function applyFullGraph(payload) {
311
436
  catch { }
312
437
  App.resetRootView({ moduleName: 'app-root' });
313
438
  initialMounted = true;
314
- try {
315
- g.__NS_HMR_BOOT_COMPLETE__ = true;
316
- }
317
- catch { }
439
+ markDevBootComplete();
318
440
  if (VERBOSE)
319
441
  console.log('[hmr][init] TS rescue resetRootView complete');
320
442
  }
@@ -334,39 +456,7 @@ function applyFullGraph(payload) {
334
456
  }
335
457
  return;
336
458
  }
337
- let candidate = null;
338
- switch (TARGET_FLAVOR) {
339
- case 'vue': {
340
- const appEntry = graph.get(APP_MAIN_ENTRY_SPEC);
341
- if (appEntry && Array.isArray(appEntry.deps)) {
342
- const vueDep = appEntry.deps.find((d) => typeof d === 'string' && /\.vue$/i.test(d));
343
- if (vueDep)
344
- candidate = vueDep;
345
- }
346
- if (!candidate) {
347
- for (const id of graph.keys()) {
348
- if (/\.vue$/i.test(id)) {
349
- candidate = id;
350
- break;
351
- }
352
- }
353
- }
354
- // Fallback: when the module graph is empty (Vite 7+ may not populate it
355
- // before the first full-graph broadcast), check the SFC artifact registry
356
- // which is populated from the ns:vue-sfc-registry message.
357
- if (!candidate && sfcArtifactMap.size > 0) {
358
- for (const id of sfcArtifactMap.keys()) {
359
- if (/\.vue$/i.test(id)) {
360
- candidate = id;
361
- if (VERBOSE)
362
- console.log('[hmr][init] rescue candidate from SFC registry:', id);
363
- break;
364
- }
365
- }
366
- }
367
- break;
368
- }
369
- }
459
+ const candidate = CLIENT_STRATEGY?.selectMountCandidate?.({ graph, appMainEntrySpec: APP_MAIN_ENTRY_SPEC }) ?? null;
370
460
  if (!candidate)
371
461
  return;
372
462
  initialMounting = true;
@@ -377,20 +467,15 @@ function applyFullGraph(payload) {
377
467
  (async () => {
378
468
  try {
379
469
  let comp = null;
380
- switch (TARGET_FLAVOR) {
381
- case 'vue':
382
- comp = await loadSfcComponent(candidate, 'initial_mount_rescue');
383
- break;
470
+ if (CLIENT_STRATEGY?.loadComponentForMount) {
471
+ comp = await CLIENT_STRATEGY.loadComponentForMount(candidate, 'initial_mount_rescue');
384
472
  }
385
473
  if (!comp)
386
474
  return;
387
475
  const ok = await performResetRoot(comp);
388
476
  if (ok) {
389
477
  initialMounted = true;
390
- try {
391
- g.__NS_HMR_BOOT_COMPLETE__ = true;
392
- }
393
- catch { }
478
+ markDevBootComplete();
394
479
  if (VERBOSE)
395
480
  console.log('[hmr][init] initial mount (rescue) complete');
396
481
  }
@@ -421,41 +506,11 @@ function applyFullGraph(payload) {
421
506
  // Only allow initial mount when explicitly enabled. Rely on the app's own main entry start() for the first mount
422
507
  // to avoid double-mount races that can cause duplicate navigation logs.
423
508
  if (ALLOW_INITIAL_MOUNT && !initialMounted && !bootDone && !bootInProgress && !getCurrentApp() && !getRootFrame()) {
424
- let candidate = null;
425
- switch (TARGET_FLAVOR) {
426
- case 'vue': {
427
- const appEntry = graph.get(APP_MAIN_ENTRY_SPEC);
428
- if (appEntry && Array.isArray(appEntry.deps)) {
429
- const vueDep = appEntry.deps.find((d) => typeof d === 'string' && /\.vue$/i.test(d));
430
- if (vueDep)
431
- candidate = vueDep;
432
- }
433
- if (!candidate) {
434
- for (const id of graph.keys()) {
435
- if (/\.vue$/i.test(id)) {
436
- candidate = id;
437
- break;
438
- }
439
- }
440
- }
441
- // Fallback: SFC registry (same as rescue mount above)
442
- if (!candidate && sfcArtifactMap.size > 0) {
443
- for (const id of sfcArtifactMap.keys()) {
444
- if (/\.vue$/i.test(id)) {
445
- candidate = id;
446
- if (VERBOSE)
447
- console.log('[hmr][init] initial mount candidate from SFC registry:', id);
448
- break;
449
- }
450
- }
451
- }
452
- break;
453
- }
454
- case 'typescript': {
455
- // For TS flavor, do not perform client-driven initial mount; rely on Application.run.
456
- return;
457
- }
509
+ if (TS_LIKE_FLAVOR) {
510
+ // For TS flavor, do not perform client-driven initial mount; rely on Application.run.
511
+ return;
458
512
  }
513
+ const candidate = CLIENT_STRATEGY?.selectMountCandidate?.({ graph, appMainEntrySpec: APP_MAIN_ENTRY_SPEC }) ?? null;
459
514
  if (candidate) {
460
515
  // Mark initial-mount in progress (both module-local and global) BEFORE scheduling async work
461
516
  initialMounting = true;
@@ -469,7 +524,7 @@ function applyFullGraph(payload) {
469
524
  console.log('[hmr][init] mounting initial root from', candidate, 'flavor=', TARGET_FLAVOR);
470
525
  // Android-only: avoid racing entry-runtime reset and Activity bring-up
471
526
  try {
472
- const g = globalThis;
527
+ const g = getGlobalScope();
473
528
  const App = getCore('Application') || g.Application;
474
529
  const isAndroid = !!(App && App.android !== undefined);
475
530
  if (isAndroid) {
@@ -500,30 +555,25 @@ function applyFullGraph(payload) {
500
555
  }
501
556
  catch { }
502
557
  let comp = null;
503
- switch (TARGET_FLAVOR) {
504
- case 'vue':
505
- comp = await loadSfcComponent(candidate, 'initial_mount');
506
- break;
507
- case 'typescript':
508
- try {
509
- const url = await requestModuleFromServer(candidate);
510
- const mod = await import(/* @vite-ignore */ url);
511
- comp = mod && (mod.default || mod);
512
- }
513
- catch (e) {
514
- if (VERBOSE)
515
- console.warn('[hmr][init] TS initial mount failed to import', candidate, e);
516
- }
517
- break;
558
+ if (TS_LIKE_FLAVOR) {
559
+ try {
560
+ const url = await requestModuleFromServer(candidate);
561
+ const mod = await import(/* @vite-ignore */ url);
562
+ comp = mod && (mod.default || mod);
563
+ }
564
+ catch (e) {
565
+ if (VERBOSE)
566
+ console.warn('[hmr][init] TS initial mount failed to import', candidate, e);
567
+ }
568
+ }
569
+ else if (CLIENT_STRATEGY?.loadComponentForMount) {
570
+ comp = await CLIENT_STRATEGY.loadComponentForMount(candidate, 'initial_mount');
518
571
  }
519
572
  if (comp) {
520
573
  const ok = await performResetRoot(comp);
521
574
  if (ok) {
522
575
  initialMounted = true;
523
- try {
524
- globalThis.__NS_HMR_BOOT_COMPLETE__ = true;
525
- }
526
- catch { }
576
+ markDevBootComplete();
527
577
  if (VERBOSE)
528
578
  console.log('[hmr][init] initial mount complete');
529
579
  }
@@ -575,11 +625,7 @@ function applyDelta(payload) {
575
625
  setGraphVersion(payload.newVersion);
576
626
  }
577
627
  const changed = payload.changed || [];
578
- switch (TARGET_FLAVOR) {
579
- case 'vue':
580
- recordVuePayloadChanges(changed, getGraphVersion());
581
- break;
582
- }
628
+ CLIENT_STRATEGY?.recordPayloadChanges?.(changed, getGraphVersion());
583
629
  (payload.changed || []).forEach((m) => {
584
630
  if (!m || !m.id)
585
631
  return;
@@ -646,7 +692,7 @@ function applyDelta(payload) {
646
692
  }
647
693
  if (isAppMainEntryId(id)) {
648
694
  try {
649
- const exists = globalThis.require?.(id) || globalThis.__nsGetModuleExports?.(id);
695
+ const exists = getGlobalScope().require?.(id) || globalThis.__nsGetModuleExports?.(id);
650
696
  if (!exists && VERBOSE)
651
697
  console.log(`[hmr][delta] skipping unresolved ${APP_MAIN_ENTRY_SPEC} change`);
652
698
  if (!exists)
@@ -662,14 +708,10 @@ function applyDelta(payload) {
662
708
  processQueue();
663
709
  }
664
710
  }
665
- // Deterministic navigation using the current Vue app instance rather than vendor-held rootApp
711
+ // Deterministic navigation using the current Vue app instance rather than vendor-held rootApp.
666
712
  function __nsNavigateUsingApp(comp, opts = {}) {
667
- const g = globalThis;
668
- switch (TARGET_FLAVOR) {
669
- case 'vue':
670
- ensureVueGlobals();
671
- break;
672
- }
713
+ const g = getGlobalScope();
714
+ CLIENT_STRATEGY?.beforeNavigateBuild?.();
673
715
  const AppFactory = g.createApp;
674
716
  const RootCtor = g.NSVRoot;
675
717
  if (typeof AppFactory !== 'function' || typeof RootCtor !== 'function') {
@@ -692,12 +734,14 @@ function __nsNavigateUsingApp(comp, opts = {}) {
692
734
  const buildTarget = () => {
693
735
  const existingApp = getCurrentApp();
694
736
  const baseProvides = (existingApp && existingApp._context && existingApp._context.provides) || {};
695
- const app = AppFactory(normalizeComponent(comp, comp && (comp.__name || comp.name)));
696
- switch (TARGET_FLAVOR) {
697
- case 'vue':
698
- ensurePiniaOnApp(app);
699
- break;
700
- }
737
+ // Forward `opts.props` as Vue's rootProps so `$navigateTo(Comp, { props: { … } })`
738
+ // reaches the destination component. nativescript-vue's stock `$navigateTo`
739
+ // does the same via `createNativeView(target, options?.props, …)` →
740
+ // `renderer.createApp(component, props)`. Dropping props here would surface
741
+ // at the destination as `[Vue warn]: Missing required prop` and any
742
+ // required-prop component would render with `undefined` bindings.
743
+ const app = AppFactory(normalizeComponent(comp, comp && (comp.__name || comp.name)), opts && opts.props);
744
+ CLIENT_STRATEGY?.onNavAppCreated?.(app);
701
745
  try {
702
746
  const registry = g.__nsVendorRegistry;
703
747
  const req = registry?.get ? g.__nsVendorRequire || g.__nsRequire || g.require : g.__nsRequire || g.require;
@@ -792,6 +836,184 @@ try {
792
836
  globalThis.__nsNavigateUsingApp = __nsNavigateUsingApp;
793
837
  }
794
838
  catch { }
839
+ const openModalRecords = [];
840
+ let modalTrackingInstalled = false;
841
+ /**
842
+ * Map a served/graph module id (e.g. `/app/modal-page.xml`) to its app-root
843
+ * relative path (`modal-page.xml`). Single mapping point — the raw-asset
844
+ * re-registration, page-navigation targets, and modal matching all derive
845
+ * from this; keep them in sync by construction.
846
+ */
847
+ function toAppRelativePath(id) {
848
+ try {
849
+ const spec = normalizeSpec(id);
850
+ const appVirtual = APP_VIRTUAL_WITH_SLASH.replace(/^\//, '');
851
+ let relPath = spec.startsWith('/') ? spec.slice(1) : spec;
852
+ if (relPath.startsWith(appVirtual))
853
+ relPath = relPath.slice(appVirtual.length);
854
+ return relPath || null;
855
+ }
856
+ catch {
857
+ return null;
858
+ }
859
+ }
860
+ /** App-root relative module name (no extension) for page-shaped files, else null. */
861
+ function toAppModuleName(id) {
862
+ const relPath = toAppRelativePath(id);
863
+ if (!relPath || !/\.(xml|ts|js)$/i.test(relPath))
864
+ return null;
865
+ return relPath.replace(/\.(xml|ts|js)$/i, '');
866
+ }
867
+ function ensureModalTracking() {
868
+ if (modalTrackingInstalled)
869
+ return;
870
+ try {
871
+ const View = getCore('View') || getGlobalScope().View;
872
+ const proto = View?.prototype;
873
+ if (!proto || typeof proto.showModal !== 'function')
874
+ return;
875
+ const orig = proto.showModal;
876
+ if (orig.__nsHmrModalTracked) {
877
+ modalTrackingInstalled = true;
878
+ return;
879
+ }
880
+ const wrapped = function (...args) {
881
+ const result = orig.apply(this, args);
882
+ try {
883
+ if (typeof args[0] === 'string' && result) {
884
+ // Mirror core's getModalOptions arg shapes: (moduleName, options)
885
+ // or the deprecated positional form.
886
+ 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] };
887
+ const moduleName = String(args[0])
888
+ .replace(/^\.\//, '')
889
+ .replace(/\.(xml|ts|js)$/i, '');
890
+ openModalRecords.push({ moduleName, options, parent: this, modal: result });
891
+ if (VERBOSE)
892
+ console.log('[hmr][modal] tracked open modal', moduleName);
893
+ }
894
+ }
895
+ catch { }
896
+ return result;
897
+ };
898
+ wrapped.__nsHmrModalTracked = true;
899
+ proto.showModal = wrapped;
900
+ modalTrackingInstalled = true;
901
+ }
902
+ catch (e) {
903
+ if (VERBOSE)
904
+ console.warn('[hmr][modal] tracking install failed', e);
905
+ }
906
+ }
907
+ /**
908
+ * Enumerate the modals that are currently presented AND were opened by module
909
+ * name, with everything needed to re-present them.
910
+ *
911
+ * Source of truth is core's live modal stack (`_getRootModalViews()`):
912
+ * - `modal._moduleName` — set by the Builder on every createViewFromEntry
913
+ * view (longstanding, used by livesync), so available on any core.
914
+ * - `modal._modalOptions` — the original ShowModalOptions, stored by core's
915
+ * `_showNativeModalView` (newer cores). For older cores the showModal
916
+ * wrap's records (see ensureModalTracking) fill the gap.
917
+ * - `modal._modalParent` — the presenting view.
918
+ * Stale wrap records are pruned against the live stack while we're here.
919
+ */
920
+ function getOpenStringModuleModals() {
921
+ const out = [];
922
+ try {
923
+ const App = getCore('Application');
924
+ const root = App?.getRootView?.() || App?._rootView;
925
+ const stack = root?._getRootModalViews?.() || [];
926
+ // Prune wrap records whose modal is gone (keeps the fallback list small).
927
+ for (let i = openModalRecords.length - 1; i >= 0; i--) {
928
+ if (!stack.includes(openModalRecords[i].modal)) {
929
+ openModalRecords.splice(i, 1);
930
+ }
931
+ }
932
+ for (const modal of stack) {
933
+ const record = openModalRecords.find((r) => r.modal === modal);
934
+ const rawModuleName = typeof modal?._moduleName === 'string' && modal._moduleName ? modal._moduleName : record?.moduleName;
935
+ const parent = modal?._modalParent || record?.parent;
936
+ const options = modal?._modalOptions || record?.options;
937
+ if (!rawModuleName || !parent)
938
+ continue;
939
+ const moduleName = String(rawModuleName)
940
+ .replace(/^\.\//, '')
941
+ .replace(/\.(xml|ts|js)$/i, '');
942
+ out.push({ moduleName, options, parent, modal });
943
+ }
944
+ }
945
+ catch (e) {
946
+ if (VERBOSE)
947
+ console.warn('[hmr][modal] open-modal enumeration failed', e);
948
+ }
949
+ return out;
950
+ }
951
+ /**
952
+ * Close and re-present an open modal so it rebuilds from the freshly
953
+ * re-registered XML/code-behind. Core clears the modal stack synchronously on
954
+ * close but the NATIVE dismissal completes asynchronously; iOS refuses a
955
+ * present while a dismissal is in flight. Newer cores fire `closedModally` on
956
+ * the modal at exactly that completion point — preferred signal. Older cores
957
+ * fall back to polling `isLoaded` (flipped by `_tearDownUI` in the same
958
+ * completion callback).
959
+ */
960
+ async function reshowOpenModal(record) {
961
+ const { parent, modal, moduleName, options } = record;
962
+ await new Promise((resolve) => {
963
+ let settled = false;
964
+ const finish = () => {
965
+ if (!settled) {
966
+ settled = true;
967
+ resolve();
968
+ }
969
+ };
970
+ let eventArmed = false;
971
+ try {
972
+ if (typeof modal.once === 'function') {
973
+ modal.once('closedModally', finish);
974
+ eventArmed = true;
975
+ }
976
+ }
977
+ catch { }
978
+ // Poll fallback (also the safety net if the event never fires —
979
+ // e.g. an interactive-dismiss cancellation).
980
+ const deadline = Date.now() + 2000;
981
+ const poll = () => {
982
+ if (settled)
983
+ return;
984
+ let stillLoaded = false;
985
+ try {
986
+ stillLoaded = !!modal.isLoaded;
987
+ }
988
+ catch { }
989
+ if ((!eventArmed && !stillLoaded) || Date.now() > deadline) {
990
+ finish();
991
+ return;
992
+ }
993
+ setTimeout(poll, 50);
994
+ };
995
+ setTimeout(poll, 50);
996
+ try {
997
+ modal.closeModal();
998
+ }
999
+ catch (e) {
1000
+ if (VERBOSE)
1001
+ console.warn('[hmr][modal] close failed for', moduleName, e);
1002
+ finish();
1003
+ }
1004
+ });
1005
+ // One settle beat so the platform finishes releasing the presentation
1006
+ // before the new present begins.
1007
+ await new Promise((resolve) => setTimeout(resolve, 100));
1008
+ try {
1009
+ parent.showModal(moduleName, { ...(options || {}), animated: false });
1010
+ if (VERBOSE)
1011
+ console.log('[hmr][modal] re-presented', moduleName);
1012
+ }
1013
+ catch (e) {
1014
+ console.warn('[hmr][modal] re-present failed for', moduleName, e);
1015
+ }
1016
+ }
795
1017
  async function processQueue() {
796
1018
  if (!globalThis.__NS_HMR_BOOT_COMPLETE__) {
797
1019
  if (VERBOSE)
@@ -827,26 +1049,39 @@ async function processQueue() {
827
1049
  return;
828
1050
  if (VERBOSE)
829
1051
  console.log('[hmr][queue] processing changed ids', drained);
830
- // alpha.59 — Explicit eviction step.
1052
+ // Track wall-clock so the 'complete' frame can show a meaningful
1053
+ // total. Only the Solid + TypeScript flavors drive the overlay
1054
+ // from here; Angular has its own flow inside
1055
+ // `frameworks/angular/client/index.ts`.
1056
+ const tQueueStart = Date.now();
1057
+ const driveSolidOverlay = TARGET_FLAVOR === 'solid';
1058
+ // Explicit eviction step.
831
1059
  //
832
- // On alpha.59+ runtimes the URL canonicalizer collapses any
833
- // `__ns_hmr__/<tag>/` segment back to a stable cache key, so
834
- // without explicit eviction the upcoming `import(url)` would
1060
+ // Without explicit eviction the upcoming `import(url)` would
835
1061
  // resolve via V8's `g_moduleRegistry` and return the cached
836
1062
  // stale module — making the queue drain a silent no-op for
837
- // every save after the first.
838
- //
839
- // We hand the canonical eviction URLs to the runtime first;
840
- // `invalidateModulesByUrls` is a no-op on older runtimes and
841
- // `requestModuleFromServer` automatically falls back to the
842
- // legacy `/ns/m/__ns_hmr__/v<N>/` URL versioning path in that
843
- // case. node_modules and virtual specs are filtered out by
844
- // `buildEvictionUrls` so vendor modules stay hot.
1063
+ // every save after the first. Eviction also arms the runtime's
1064
+ // bust-next-fetch nonce so the re-fetch defeats OS-level HTTP
1065
+ // caches; the re-import below then uses the STABLE canonical
1066
+ // URL (module identity IS the URL). node_modules and virtual
1067
+ // specs are filtered out by `buildEvictionUrls` so vendor
1068
+ // modules stay hot.
1069
+ if (driveSolidOverlay) {
1070
+ setUpdateOverlayStage('evicting', {
1071
+ detail: drained.length === 1 ? `Invalidating ${drained[0]}` : `Invalidating ${drained.length} modules`,
1072
+ });
1073
+ }
845
1074
  const evictUrls = buildEvictionUrls(drained);
846
1075
  const evicted = invalidateModulesByUrls(evictUrls);
847
1076
  if (VERBOSE)
848
1077
  console.log(`[hmr][queue] eviction count=${evictUrls.length} ok=${evicted}`);
849
1078
  // Evaluate changed modules best-effort; failures shouldn't completely break HMR.
1079
+ if (driveSolidOverlay) {
1080
+ setUpdateOverlayStage('reimporting', {
1081
+ detail: drained.length === 1 ? `Re-importing ${drained[0]}` : `Re-importing ${drained.length} modules`,
1082
+ });
1083
+ }
1084
+ let reimportFailures = 0;
850
1085
  for (const id of drained) {
851
1086
  try {
852
1087
  const spec = normalizeSpec(id);
@@ -856,18 +1091,77 @@ async function processQueue() {
856
1091
  if (VERBOSE)
857
1092
  console.log('[hmr][queue] re-import', { id, spec, url });
858
1093
  const mod = await import(/* @vite-ignore */ url);
1094
+ // TS/XML flavor: refresh the bundler module registry with the fresh
1095
+ // exports so Builder.createViewFromEntry / loadModule('<page>')
1096
+ // resolves the NEW code-behind (tap handlers, page events) instead
1097
+ // of the stale module captured in the boot bundle. Without this,
1098
+ // XML re-renders pick up new markup but keep old behavior.
1099
+ if (TS_LIKE_FLAVOR && mod && /\.(ts|js)$/i.test(id)) {
1100
+ try {
1101
+ const g = getGlobalScope();
1102
+ const moduleName = toAppModuleName(id);
1103
+ if (moduleName && typeof g.registerModule === 'function') {
1104
+ g.registerModule(moduleName, () => mod);
1105
+ g.registerModule('./' + moduleName, () => mod);
1106
+ if (VERBOSE)
1107
+ console.log('[hmr][queue] re-registered code-behind', moduleName);
1108
+ }
1109
+ }
1110
+ catch (e) {
1111
+ if (VERBOSE)
1112
+ console.warn('[hmr][queue] code-behind re-register failed for', id, e);
1113
+ }
1114
+ }
859
1115
  }
860
1116
  catch (e) {
861
- if (VERBOSE)
862
- console.warn('[hmr][queue] re-import failed for', id, e);
1117
+ // Never silent: a failed re-import means the device may keep
1118
+ // running the previous module body, so always surface it (not
1119
+ // verbose-gated) — otherwise the overlay reports success while
1120
+ // the app runs stale code.
1121
+ reimportFailures++;
1122
+ console.warn('[hmr][queue] re-import FAILED for', id, '-', e?.message ?? e);
863
1123
  }
864
1124
  }
1125
+ if (reimportFailures > 0) {
1126
+ console.warn(`[hmr][queue] ${reimportFailures}/${drained.length} module(s) failed to re-import; the applied update may be incomplete.`);
1127
+ }
865
1128
  // After evaluating the batch, perform flavor-specific UI refresh.
866
1129
  switch (TARGET_FLAVOR) {
867
1130
  case 'vue':
868
- // Vue SFCs are handled via the registry update path; nothing to do here.
1131
+ // graph + performResetRoot + getOverlay let the Vue strategy
1132
+ // propagate non-SFC dep changes to the nearest `.vue` boundary
1133
+ // and remount it (see `propagateDepChangeToSfcBoundary`).
1134
+ await CLIENT_STRATEGY?.refreshAfterBatch?.(drained, { setUpdateOverlayStage, startedAt: tQueueStart, graph, performResetRoot, getOverlay: getHmrOverlayApi });
1135
+ break;
1136
+ case 'react': {
1137
+ // React has no Fast Refresh, and (like Solid) mounts a dominative
1138
+ // document root via `startReactApp` rather than an `app-root`
1139
+ // module — so the TypeScript `resetRootView({moduleName:'app-root'})`
1140
+ // path doesn't apply. The changed modules were already re-imported
1141
+ // into the device cache above; the per-page remount is driven by the
1142
+ // app's `__NS_HMR_ON_UPDATE__` hook (see
1143
+ // `@nativescript/tanstack-router/react`'s `subscribeReactHmrRemount`),
1144
+ // which fires right after this queue drains. Here we only mark the
1145
+ // overlay complete so it doesn't stick at 'received' (5%).
1146
+ setUpdateOverlayStage('complete', {
1147
+ detail: `Total ${Math.max(0, Date.now() - tQueueStart)}ms`,
1148
+ });
869
1149
  break;
1150
+ }
870
1151
  case 'solid': {
1152
+ // Boundaries discovered in this HMR cycle (tsx files reachable
1153
+ // via the reverse import graph from any changed file, plus route
1154
+ // files reachable from any tsx start point). Declared at the top
1155
+ // of the case block so the emit step below can include the
1156
+ // complete set in the listener event — framework integrations
1157
+ // use it to map route boundaries → fresh component references.
1158
+ const boundaries = new Set();
1159
+ // FULL reverse-import chain from every changed file up to the app
1160
+ // entry (all importers, transitively — not just tsx ones). Emitted
1161
+ // as `ev.ancestors` so app-level remount helpers can evict the
1162
+ // whole chain: re-importing only the root would resolve cached
1163
+ // intermediates and hand the fresh root a stale subtree.
1164
+ const ancestors = new Set();
871
1165
  // Solid .tsx components are self-accepting via solid-refresh's inline
872
1166
  // patchRegistry — re-importing them is sufficient. For non-component
873
1167
  // .ts utility modules, we must propagate up the import graph to find
@@ -886,8 +1180,28 @@ async function processQueue() {
886
1180
  arr.push(id);
887
1181
  }
888
1182
  }
889
- // BFS from each non-tsx changed module up to tsx/jsx boundaries
890
- const boundaries = new Set();
1183
+ // Pass 0: collect the FULL importer chain of every changed file
1184
+ // (BFS up the reverse graph, no filtering). This feeds
1185
+ // `ev.ancestors` for app-level remount helpers.
1186
+ for (const id of drained) {
1187
+ const queue = [id];
1188
+ while (queue.length) {
1189
+ const cur = queue.shift();
1190
+ const importers = reverseIndex.get(cur);
1191
+ if (!importers)
1192
+ continue;
1193
+ for (const imp of importers) {
1194
+ if (ancestors.has(imp))
1195
+ continue;
1196
+ ancestors.add(imp);
1197
+ queue.push(imp);
1198
+ }
1199
+ }
1200
+ }
1201
+ // Pass 1: BFS from each non-tsx changed module up to tsx/jsx
1202
+ // boundaries. These get re-imported below so solid-refresh's
1203
+ // inline patchRegistry runs and (best-effort) swaps the proxy
1204
+ // signals for any components defined in those tsx boundaries.
891
1205
  for (const id of drained) {
892
1206
  if (/\.(tsx|jsx)$/i.test(id))
893
1207
  continue; // already self-accepting
@@ -911,6 +1225,51 @@ async function processQueue() {
911
1225
  }
912
1226
  }
913
1227
  }
1228
+ // Pass 2: walk further from any tsx starting point (a tsx file
1229
+ // in `drained` OR a tsx boundary discovered in pass 1) to find
1230
+ // route files (`/src/routes/*.{tsx,jsx}`) that transitively
1231
+ // import them. Re-importing a route file refreshes its
1232
+ // `Route.options.component` to the freshly-imported reference
1233
+ // and the existing boundary loop below patches the live router
1234
+ // with that fresh reference.
1235
+ //
1236
+ // This is the key fix for "edit home.tsx → save → no visual
1237
+ // update": the old BFS skipped tsx files in `drained` (assuming
1238
+ // solid-refresh's in-place proxy patch was sufficient), but in
1239
+ // the universal-renderer + nested-context configuration that
1240
+ // patch does not always propagate to the visible page tree.
1241
+ // Adding the route file as a boundary lets us patch
1242
+ // `route.options.component` directly to a fresh module export,
1243
+ // which the framework subscriber then passes through to the
1244
+ // page remount — making the cycle robust to the proxy patch
1245
+ // silently failing.
1246
+ const tsxStarts = new Set();
1247
+ for (const id of drained) {
1248
+ if (/\.(tsx|jsx)$/i.test(id))
1249
+ tsxStarts.add(id);
1250
+ }
1251
+ for (const b of boundaries)
1252
+ tsxStarts.add(b);
1253
+ const ROUTE_FILE_RE = /\/src\/routes\/.+\.(tsx|jsx)$/i;
1254
+ for (const start of tsxStarts) {
1255
+ const visited = new Set();
1256
+ const queue = [start];
1257
+ while (queue.length) {
1258
+ const cur = queue.shift();
1259
+ if (visited.has(cur))
1260
+ continue;
1261
+ visited.add(cur);
1262
+ if (cur !== start && ROUTE_FILE_RE.test(cur)) {
1263
+ boundaries.add(cur);
1264
+ }
1265
+ const importers = reverseIndex.get(cur);
1266
+ if (!importers)
1267
+ continue;
1268
+ for (const imp of importers) {
1269
+ queue.push(imp);
1270
+ }
1271
+ }
1272
+ }
914
1273
  // Re-import each boundary so solid-refresh patchRegistry fires.
915
1274
  // For route files (TanStack Router), capture the new Route export
916
1275
  // and patch the router's existing route with the fresh loader.
@@ -921,7 +1280,7 @@ async function processQueue() {
921
1280
  const findRouter = () => {
922
1281
  if (discoveredRouter)
923
1282
  return discoveredRouter;
924
- const g = globalThis;
1283
+ const g = getGlobalScope();
925
1284
  if (g.__ns_router?.routesById)
926
1285
  return (discoveredRouter = g.__ns_router);
927
1286
  // Fallback: scan common global keys for router
@@ -949,53 +1308,97 @@ async function processQueue() {
949
1308
  p = p.replace(/(^|\/)-([\w])/g, '$1$2');
950
1309
  return '/' + p;
951
1310
  };
952
- // Find existing route by fullPath (since new Route has no id yet)
1311
+ // Find existing route by fullPath (since new Route has no id yet).
1312
+ // The root route (`__root__`) shares fullPath '/' with the index
1313
+ // route, so prefer the non-root (leaf) match — otherwise editing
1314
+ // `index.tsx` would clobber the root layout's component instead of
1315
+ // the page's. @nativescript/tanstack-router's remount uses the same
1316
+ // preference so patch + read target the same route.
953
1317
  const findRouteByFullPath = (router, fp) => {
954
1318
  if (!router?.routesById)
955
1319
  return null;
1320
+ let rootMatch = null;
956
1321
  for (const rid of Object.keys(router.routesById)) {
957
1322
  const r = router.routesById[rid];
958
- if (r?.fullPath === fp)
959
- return r;
1323
+ if (r?.fullPath !== fp)
1324
+ continue;
1325
+ if (rid === '__root__') {
1326
+ rootMatch = rootMatch || r;
1327
+ continue;
1328
+ }
1329
+ return r;
960
1330
  }
961
- return null;
1331
+ return rootMatch;
962
1332
  };
963
- // alpha.59 — Evict the boundary set so re-importing
964
- // each .tsx component actually picks up the new
965
- // transitive dependency code; without this V8 returns
966
- // the cached boundary module unchanged.
1333
+ // A changed route file is itself a boundary to re-import + patch — Pass 2
1334
+ // only adds OTHER route files that import a changed module, never the
1335
+ // changed route file itself. Without this, editing `about.tsx` while on
1336
+ // /about never refreshes the About component.
1337
+ for (const id of drained) {
1338
+ if (ROUTE_FILE_RE.test(id))
1339
+ boundaries.add(id);
1340
+ }
1341
+ // Evict ONLY the boundary set (changed component + route files) — NOT the
1342
+ // router / route-tree / app chain. Keeping those cached preserves the live
1343
+ // router instance and the user's current route across the re-mount; fresh
1344
+ // route components reach the screen via the in-place `route.options.component`
1345
+ // patch below, not a router rebuild (which would reset to the initial route).
967
1346
  const boundaryIds = Array.from(boundaries);
968
1347
  const solidEvictUrls = buildEvictionUrls(boundaryIds);
969
1348
  const solidEvicted = invalidateModulesByUrls(solidEvictUrls);
970
1349
  if (VERBOSE)
971
1350
  console.log(`[hmr][solid] eviction count=${solidEvictUrls.length} ok=${solidEvicted}`);
972
1351
  for (const id of boundaries) {
973
- if (seen.has(id))
1352
+ // Skip already-drained modules — EXCEPT route files. The main
1353
+ // drain loop above re-imports drained modules but does NOT patch
1354
+ // `route.options.component`; a changed ROUTE file (which is in
1355
+ // `drained`/`seen`) must still flow through this loop so its route
1356
+ // gets patched with the fresh component, otherwise the router's
1357
+ // per-page HMR remount re-renders the stale (one-edit-behind) one.
1358
+ if (seen.has(id) && !/\/src\/routes\/.+\.(?:tsx|jsx)$/i.test(id))
1359
+ continue;
1360
+ // Skip the root layout (`__root`): it commonly imports a global
1361
+ // stylesheet (`./styles.css?url`) with no ESM `default` export on
1362
+ // device, so re-importing it throws and aborts the cycle. The root
1363
+ // layout rarely needs a component hot-swap anyway.
1364
+ if (/\/routes\/__root\.(?:tsx|jsx|ts|js)$/i.test(id))
974
1365
  continue;
975
1366
  try {
976
1367
  const spec = normalizeSpec(id);
1368
+ // Re-import the canonical URL. The boundary was evicted above
1369
+ // (`invalidateModulesByUrls(solidEvictUrls)`), which also armed
1370
+ // the runtime's bust-next-fetch nonce, so this import re-fetches
1371
+ // the just-saved transform rather than a cached one. This is what
1372
+ // `route.options.component` is patched from below, and what
1373
+ // @nativescript/tanstack-router's per-page HMR remount reads.
977
1374
  const url = await requestModuleFromServer(spec);
978
1375
  if (!url)
979
1376
  continue;
980
1377
  if (VERBOSE)
981
1378
  console.log('[hmr][solid] propagated to boundary', { id, url });
982
1379
  const mod = await import(/* @vite-ignore */ url);
983
- // Patch TanStack Router route loaders
1380
+ // Patch TanStack Router route options for any module
1381
+ // that exports a `Route`. We patch BOTH the component
1382
+ // and the loader (when present); components-only routes
1383
+ // were previously skipped because the gate required a
1384
+ // loader, which left their `options.component` pointing
1385
+ // at the stale module's exports after HMR.
984
1386
  try {
985
1387
  const newRoute = mod?.Route;
986
- if (newRoute?.options?.loader) {
1388
+ if (newRoute?.options) {
987
1389
  const router = findRouter();
988
1390
  const fullPath = boundaryToFullPath(id);
989
1391
  if (VERBOSE)
990
- console.log('[hmr][solid][diag] route patch attempt', { id, fullPath, hasRouter: !!router, routesByIdKeys: router?.routesById ? Object.keys(router.routesById) : 'none' });
1392
+ console.log('[hmr][solid][diag] route patch attempt', { id, fullPath, hasRouter: !!router, hasLoader: !!newRoute.options.loader, hasComponent: !!newRoute.options.component });
991
1393
  const existingRoute = fullPath && router ? findRouteByFullPath(router, fullPath) : null;
992
1394
  if (existingRoute?.options) {
993
- existingRoute.options.loader = newRoute.options.loader;
1395
+ if (newRoute.options.loader)
1396
+ existingRoute.options.loader = newRoute.options.loader;
994
1397
  if (newRoute.options.component)
995
1398
  existingRoute.options.component = newRoute.options.component;
996
1399
  routesPatchCount++;
997
1400
  if (VERBOSE)
998
- console.log('[hmr][solid] patched route loader', existingRoute.id, 'fullPath=', fullPath);
1401
+ console.log('[hmr][solid] patched route', existingRoute.id, 'fullPath=', fullPath);
999
1402
  }
1000
1403
  else if (VERBOSE) {
1001
1404
  console.log('[hmr][solid] no matching route for fullPath', fullPath);
@@ -1024,13 +1427,52 @@ async function processQueue() {
1024
1427
  if (VERBOSE) {
1025
1428
  if (boundaries.size)
1026
1429
  console.log('[hmr][solid] propagated non-component change to', boundaries.size, 'boundaries', Array.from(boundaries));
1027
- console.log('[hmr][queue] Solid: modules re-imported, solid-refresh handles reactive update', drained);
1430
+ console.log('[hmr][queue] Solid: modules re-imported; UI refresh is driven by the framework subscriber (router per-page remount or startSolidApp re-render)', drained);
1028
1431
  }
1029
1432
  }
1030
1433
  catch (e) {
1031
1434
  if (VERBOSE)
1032
1435
  console.warn('[hmr][solid] propagation failed', e);
1033
1436
  }
1437
+ // Notify any framework integrations (e.g.
1438
+ // `@nativescript/tanstack-router`) that a Solid HMR
1439
+ // cycle has completed. They use this signal to perform
1440
+ // framework-specific UI refresh (e.g. remount the active
1441
+ // router page) when solid-refresh's own reactive
1442
+ // propagation does not reach the visible tree under
1443
+ // the current renderer/context configuration.
1444
+ //
1445
+ // Boundaries include both the directly-changed tsx files
1446
+ // AND every tsx ancestor reachable via the reverse import
1447
+ // graph (route files in particular). The framework
1448
+ // listener uses the route-file boundaries to look up the
1449
+ // freshly-patched `route.options.component` and pass it
1450
+ // through to the page remount.
1451
+ try {
1452
+ const tsxChangedInDrained = drained.filter((id) => /\.(tsx|jsx)$/i.test(id));
1453
+ const allBoundaries = Array.from(new Set([...tsxChangedInDrained, ...boundaries]));
1454
+ nsSolidHmrEmit({
1455
+ kind: 'solid',
1456
+ changedFiles: drained.slice(),
1457
+ boundaries: allBoundaries,
1458
+ ancestors: Array.from(ancestors),
1459
+ });
1460
+ }
1461
+ catch (err) {
1462
+ if (VERBOSE)
1463
+ console.warn('[hmr][solid] emit failed', err);
1464
+ }
1465
+ // Tell the overlay the cycle is done. solid-refresh's
1466
+ // inline patchRegistry has already flushed the new
1467
+ // component bodies into the live tree (the `case
1468
+ // 'solid'` block above re-imports each .tsx
1469
+ // boundary), so by the time we get here the user is
1470
+ // already looking at the new render. The 'complete'
1471
+ // frame surfaces the wall-clock total and triggers
1472
+ // the overlay's auto-hide.
1473
+ setUpdateOverlayStage('complete', {
1474
+ detail: `Total ${Math.max(0, Date.now() - tQueueStart)}ms`,
1475
+ });
1034
1476
  break;
1035
1477
  }
1036
1478
  case 'typescript': {
@@ -1038,7 +1480,7 @@ async function processQueue() {
1038
1480
  // This preserves the shell (Frame, ActionBar, etc.) that the app's
1039
1481
  // own bootstrapping wires up via `Application.run`.
1040
1482
  try {
1041
- const g = globalThis;
1483
+ const g = getGlobalScope();
1042
1484
  const App = getCore('Application') || g.Application;
1043
1485
  if (!App || typeof App.resetRootView !== 'function') {
1044
1486
  if (VERBOSE)
@@ -1064,10 +1506,9 @@ async function processQueue() {
1064
1506
  const rawContent = await resp.text();
1065
1507
  // Register under all nickname variants the module registry uses.
1066
1508
  // The bundler context registers XML as e.g., './main-page.xml' and 'main-page.xml'
1067
- const appVirtual = APP_VIRTUAL_WITH_SLASH.replace(/^\//, '');
1068
- let relPath = spec.startsWith('/') ? spec.slice(1) : spec;
1069
- if (relPath.startsWith(appVirtual))
1070
- relPath = relPath.slice(appVirtual.length);
1509
+ const relPath = toAppRelativePath(id);
1510
+ if (!relPath)
1511
+ continue;
1071
1512
  const nicknames = ['./' + relPath, relPath];
1072
1513
  // Also add without extension for CSS
1073
1514
  const extIdx = relPath.lastIndexOf('.');
@@ -1093,21 +1534,24 @@ async function processQueue() {
1093
1534
  }
1094
1535
  }
1095
1536
  }
1537
+ // Modal-aware refresh: pages currently PRESENTED AS MODALS must be
1538
+ // closed + re-presented in place — navigating the top frame to a
1539
+ // modal's page would push it as a frame page, and resetRootView
1540
+ // would dismiss the modal entirely. State comes from core's live
1541
+ // modal stack (_moduleName/_modalOptions/_modalParent); the
1542
+ // showModal wrap only backfills options on older cores.
1543
+ ensureModalTracking();
1544
+ const openModals = getOpenStringModuleModals();
1545
+ const changedModuleNames = new Set(drained.map(toAppModuleName).filter(Boolean));
1546
+ const modalsToReshow = openModals.filter((record) => changedModuleNames.has(record.moduleName));
1547
+ const reshowModuleNames = new Set(modalsToReshow.map((record) => record.moduleName));
1096
1548
  // Determine if we can navigate in-place to a changed page
1097
1549
  // instead of resetting all the way back to app-root.
1098
1550
  // This keeps the user on the page they're editing for faster iteration.
1099
1551
  const changedXmlPages = drained
1100
1552
  .filter((id) => /\.xml$/i.test(id))
1101
- .map((id) => {
1102
- const spec = normalizeSpec(id);
1103
- const appVirtual = APP_VIRTUAL_WITH_SLASH.replace(/^\//, '');
1104
- let relPath = spec.startsWith('/') ? spec.slice(1) : spec;
1105
- if (relPath.startsWith(appVirtual))
1106
- relPath = relPath.slice(appVirtual.length);
1107
- // Strip .xml extension to get the moduleName (e.g., 'pages/status-bar')
1108
- return relPath.replace(/\.xml$/i, '');
1109
- })
1110
- .filter((m) => m && m !== 'app-root');
1553
+ .map((id) => toAppModuleName(id))
1554
+ .filter((m) => m && m !== 'app-root' && !reshowModuleNames.has(m));
1111
1555
  // Resolve the topmost Frame from the bundled realm.
1112
1556
  // Frame.topmost() relies on an internal frameStack array, so we must
1113
1557
  // call it on the bundled-realm class. Multiple strategies to find it:
@@ -1143,7 +1587,7 @@ async function processQueue() {
1143
1587
  catch { }
1144
1588
  }
1145
1589
  if (VERBOSE)
1146
- console.log('[hmr][queue] TS: changedXmlPages=', changedXmlPages, 'topFrame=', !!topFrame);
1590
+ console.log('[hmr][queue] TS: changedXmlPages=', changedXmlPages, 'topFrame=', !!topFrame, 'modalsToReshow=', modalsToReshow.length);
1147
1591
  if (changedXmlPages.length > 0 && topFrame) {
1148
1592
  // Navigate the current frame to the changed page directly.
1149
1593
  // Use the last changed XML page (most specific).
@@ -1158,15 +1602,32 @@ async function processQueue() {
1158
1602
  App.resetRootView({ moduleName: 'app-root' });
1159
1603
  }
1160
1604
  }
1161
- else {
1605
+ else if (modalsToReshow.length === 0) {
1606
+ // No frame page to refresh and no open modal owns the change —
1607
+ // fall back to a full root reset. (Skipped when an open modal is
1608
+ // being re-presented below: resetRootView would dismiss it.)
1162
1609
  if (VERBOSE)
1163
1610
  console.log('[hmr][queue] TS flavor: resetRootView(app-root) after changes');
1164
1611
  App.resetRootView({ moduleName: 'app-root' });
1165
1612
  }
1613
+ // Re-present any open modals whose XML/code-behind changed. The
1614
+ // modules were already re-registered above (raw XML assets + fresh
1615
+ // code-behind exports), so the re-presented modal rebuilds from
1616
+ // the new content while the page beneath it stays put.
1617
+ for (const record of modalsToReshow) {
1618
+ await reshowOpenModal(record);
1619
+ }
1166
1620
  }
1167
1621
  catch (e) {
1168
1622
  console.warn('[hmr][queue] TS flavor: resetRootView(app-root) failed', e);
1169
1623
  }
1624
+ // Tell the overlay the cycle is done — same as the solid path
1625
+ // above. Without this the applying overlay sticks at
1626
+ // 'received' (5%) forever even though the in-place navigate /
1627
+ // resetRootView already applied the update.
1628
+ setUpdateOverlayStage('complete', {
1629
+ detail: `Total ${Math.max(0, Date.now() - tQueueStart)}ms`,
1630
+ });
1170
1631
  break;
1171
1632
  }
1172
1633
  }
@@ -1174,11 +1635,37 @@ async function processQueue() {
1174
1635
  finally {
1175
1636
  processingQueue = false;
1176
1637
  processingPromise = null;
1638
+ // If a delta arrived mid-cycle (pushed onto changedQueue while
1639
+ // processingQueue was still true, so its processQueue() call early-
1640
+ // returned), re-drain — otherwise that save is stranded until the next
1641
+ // delta. Deferred via setTimeout to avoid synchronous re-entrancy as
1642
+ // this promise settles.
1643
+ if (changedQueue.length) {
1644
+ setTimeout(() => {
1645
+ try {
1646
+ processQueue();
1647
+ }
1648
+ catch { }
1649
+ }, 0);
1650
+ }
1177
1651
  }
1178
1652
  })();
1179
1653
  return processingPromise;
1180
1654
  }
1181
1655
  let hmrSocket = null;
1656
+ // Single reconnect timer. Overlapping close/timeout events used to each schedule
1657
+ // their own `setTimeout(connectHmr, …)`, stacking multiple pending reconnects
1658
+ // that could spawn (and leak) duplicate sockets/listeners. Route all reconnect
1659
+ // scheduling through here so only one is ever pending.
1660
+ let reconnectTimer = null;
1661
+ function scheduleReconnect(delayMs) {
1662
+ if (reconnectTimer)
1663
+ clearTimeout(reconnectTimer);
1664
+ reconnectTimer = setTimeout(() => {
1665
+ reconnectTimer = null;
1666
+ connectHmr();
1667
+ }, delayMs);
1668
+ }
1182
1669
  // Track server-announced batches for each version so we can import in-order client-side
1183
1670
  const txnClientBatches = new Map();
1184
1671
  // Public hook for NativeScript runtime to call from ImportModuleDynamicallyCallback later.
@@ -1206,10 +1693,16 @@ function connectHmr() {
1206
1693
  if (hmrSocket?.readyState === WebSocket.OPEN)
1207
1694
  return;
1208
1695
  if (hmrSocket?.readyState === WebSocket.CONNECTING) {
1209
- if (__NS_ENV_VERBOSE__)
1696
+ if (VERBOSE)
1210
1697
  console.log('[hmr-client] Already connecting to HMR WebSocket, skipping');
1211
1698
  return;
1212
1699
  }
1700
+ // A reconnect fired (or a manual connect raced one) — cancel any other pending
1701
+ // reconnect so we don't end up with overlapping connect attempts.
1702
+ if (reconnectTimer) {
1703
+ clearTimeout(reconnectTimer);
1704
+ reconnectTimer = null;
1705
+ }
1213
1706
  try {
1214
1707
  globalThis.__NS_HMR_CLIENT_SOCKET_READY__ = false;
1215
1708
  }
@@ -1217,7 +1710,7 @@ function connectHmr() {
1217
1710
  const overlayStage = hasOpenedHmrSocket ? 'reconnecting' : 'connecting';
1218
1711
  const baseUrl = getHMRWsUrl() || 'ws://localhost:5173/ns-hmr';
1219
1712
  const buildCandidates = (url) => {
1220
- let candidates = [];
1713
+ const candidates = [];
1221
1714
  try {
1222
1715
  const u = new URL(url);
1223
1716
  const proto = u.protocol === 'wss:' ? ['wss'] : ['ws'];
@@ -1225,7 +1718,7 @@ function connectHmr() {
1225
1718
  // Build ordered host candidates with preference to the active HTTP origin
1226
1719
  const orderedHosts = [];
1227
1720
  try {
1228
- const g = globalThis;
1721
+ const g = getGlobalScope();
1229
1722
  const httpOrigin = g && typeof g.__NS_HTTP_ORIGIN__ === 'string' ? g.__NS_HTTP_ORIGIN__ : undefined;
1230
1723
  if (httpOrigin) {
1231
1724
  try {
@@ -1276,19 +1769,29 @@ function connectHmr() {
1276
1769
  if (idx >= candidates.length) {
1277
1770
  showConnectionOverlayNow('offline', 'Waiting for the Vite websocket to come back.');
1278
1771
  console.warn('[hmr-client] All WS candidates failed:', candidates.join(', '));
1279
- setTimeout(connectHmr, 1500);
1772
+ scheduleReconnect(1500);
1280
1773
  return;
1281
1774
  }
1282
1775
  const url = candidates[idx++];
1283
1776
  const connectionDetail = `${overlayStage === 'reconnecting' ? 'Retrying' : 'Opening'} ${url}`;
1284
- if (connectionOverlayVisible) {
1777
+ if (overlayStage === 'reconnecting' && shownConnectionOverlayStage === 'offline') {
1778
+ // Don't flip the visible 'offline' frame to 'reconnecting' for an
1779
+ // attempt that may fail instantly — defer it so only a genuinely
1780
+ // in-flight attempt surfaces. The deferred show is cancelled by
1781
+ // showConnectionOverlayNow (offline re-show / synchronizing) the
1782
+ // moment this attempt resolves. Keeps 'offline' stable instead of
1783
+ // flickering once per retry. (Checking the PAINTED stage, not the
1784
+ // pending one, so every candidate in the loop keeps deferring.)
1785
+ scheduleConnectionOverlay(overlayStage, connectionDetail, RECONNECTING_OVER_OFFLINE_DELAY_MS);
1786
+ }
1787
+ else if (connectionOverlayVisible) {
1285
1788
  updateConnectionOverlay(overlayStage, connectionDetail);
1286
1789
  }
1287
1790
  else {
1288
1791
  scheduleConnectionOverlay(overlayStage, connectionDetail);
1289
1792
  }
1290
1793
  try {
1291
- if (__NS_ENV_VERBOSE__)
1794
+ if (VERBOSE)
1292
1795
  console.log('[hmr-client] Connecting to HMR WebSocket:', url);
1293
1796
  const sock = new WebSocket(url);
1294
1797
  hmrSocket = sock;
@@ -1315,13 +1818,29 @@ function connectHmr() {
1315
1818
  globalThis.__NS_HMR_CLIENT_SOCKET_READY__ = true;
1316
1819
  }
1317
1820
  catch { }
1821
+ // Wire the JS hot registry's server channel to this socket so
1822
+ // `import.meta.hot.send(event, data)` reaches Vite plugins as
1823
+ // `{ type: 'custom', event, data }` (Vite wire protocol).
1824
+ try {
1825
+ getNsHotRegistry().setSendToServer((event, data) => {
1826
+ try {
1827
+ if (sock.readyState === WebSocket.OPEN) {
1828
+ sock.send(JSON.stringify({ type: 'custom', event, data }));
1829
+ }
1830
+ }
1831
+ catch { }
1832
+ });
1833
+ }
1834
+ catch { }
1318
1835
  if (connectionOverlayVisible) {
1319
1836
  showConnectionOverlayNow('synchronizing', 'Connected. Synchronizing the HMR graph.');
1320
1837
  }
1321
- VERBOSE && console.log('[hmr-client] Connected to HMR WebSocket');
1322
- // alpha.59 — Print the active module reload mode once on first
1323
- // successful connect so the user can correlate HMR latency with
1324
- // runtime capability without grepping for protocol details.
1838
+ if (VERBOSE)
1839
+ console.log('[hmr-client] Connected to HMR WebSocket');
1840
+ // Print the active module reload mode once on first
1841
+ // successful connect so the user can correlate HMR latency
1842
+ // with runtime capability without grepping for protocol
1843
+ // details. The banner is verbose-gated.
1325
1844
  try {
1326
1845
  emitHmrModeBannerOnce();
1327
1846
  }
@@ -1349,7 +1868,7 @@ function connectHmr() {
1349
1868
  console.log('[hmr-client] WebSocket closed (code', ev?.code, '), will reconnect…');
1350
1869
  scheduleConnectionOverlay('reconnecting', 'The websocket closed. Waiting to reconnect.', 700);
1351
1870
  // try to reconnect with full candidate list again
1352
- setTimeout(connectHmr, 1000);
1871
+ scheduleReconnect(1000);
1353
1872
  }
1354
1873
  };
1355
1874
  }
@@ -1385,21 +1904,26 @@ async function handleHmrMessage(ev) {
1385
1904
  catch { }
1386
1905
  }
1387
1906
  if (msg) {
1388
- // alpha.62 follow-up — `ns:hmr-pending` is a fire-and-forget
1389
- // UX hint emitted by the server at the START of handleHotUpdate.
1390
- // We drive the HMR-applying overlay's 'received' frame here
1391
- // (synchronously), well before the authoritative payload
1392
- // (`ns:angular-update` / `ns:css-updates`) lands. Skip running
1393
- // any other handlers — the pending message has no module
1394
- // payload and intentionally does not bump the graph version.
1907
+ // `ns:hmr-pending` is a fire-and-forget UX hint emitted by the
1908
+ // server at the START of handleHotUpdate. We drive the
1909
+ // HMR-applying overlay's 'received' frame here (synchronously),
1910
+ // well before the authoritative payload (`ns:angular-update` /
1911
+ // `ns:css-updates`) lands. Skip running any other handlers —
1912
+ // the pending message has no module payload and intentionally
1913
+ // does not bump the graph version.
1395
1914
  if (msg.type === 'ns:hmr-pending' && typeof msg.path === 'string') {
1396
1915
  setHmrPendingOverlay(msg.path);
1397
1916
  return;
1398
1917
  }
1918
+ // The per-flavor client strategy is loaded by a dynamic import(); make
1919
+ // sure it has resolved (and `install()` has run) before any handler that
1920
+ // delegates through it. After the first message this is an already-settled
1921
+ // promise (microtask only); for Solid/TypeScript it is `Promise.resolve()`.
1922
+ await CLIENT_STRATEGY_READY;
1399
1923
  if (msg.type === 'ns:hmr-full-graph') {
1400
1924
  // Bump a monotonic nonce so HTTP ESM imports can always be cache-busted per update.
1401
1925
  try {
1402
- const g = globalThis;
1926
+ const g = getGlobalScope();
1403
1927
  g.__NS_HMR_IMPORT_NONCE__ = (typeof g.__NS_HMR_IMPORT_NONCE__ === 'number' ? g.__NS_HMR_IMPORT_NONCE__ : 0) + 1;
1404
1928
  }
1405
1929
  catch { }
@@ -1465,7 +1989,7 @@ async function handleHmrMessage(ev) {
1465
1989
  });
1466
1990
  if (toReimport.length && VERBOSE)
1467
1991
  console.log('[hmr][full-graph] inferred changed modules; re-importing', toReimport);
1468
- // alpha.59 — Evict the inferred changed set before re-importing.
1992
+ // Evict the inferred changed set before re-importing.
1469
1993
  // See `processQueue` for the architectural rationale; the
1470
1994
  // full-graph code path is the resync fallback (server chose
1471
1995
  // not to send a delta) and shares the same V8 cache pitfall.
@@ -1524,7 +2048,7 @@ async function handleHmrMessage(ev) {
1524
2048
  if (msg.type === 'ns:hmr-delta') {
1525
2049
  // Bump a monotonic nonce so HTTP ESM imports can always be cache-busted per update.
1526
2050
  try {
1527
- const g = globalThis;
2051
+ const g = getGlobalScope();
1528
2052
  g.__NS_HMR_IMPORT_NONCE__ = (typeof g.__NS_HMR_IMPORT_NONCE__ === 'number' ? g.__NS_HMR_IMPORT_NONCE__ : 0) + 1;
1529
2053
  }
1530
2054
  catch { }
@@ -1546,10 +2070,100 @@ async function handleHmrMessage(ev) {
1546
2070
  return;
1547
2071
  }
1548
2072
  else {
2073
+ // Vite custom-event dispatch.
2074
+ //
2075
+ // `server.ws.send('event-name', payload)` from any Vite plugin lands
2076
+ // on the wire as `{ type: 'custom', event: 'event-name', data: payload }`.
2077
+ // On the web, Vite's stock client owns a `customListenersMap` that
2078
+ // fires every `import.meta.hot.on('event-name', cb)` callback. We
2079
+ // don't run Vite's stock client on device — the JS hot registry
2080
+ // (`hot-context.ts`, installed by the bootstrap before the entry
2081
+ // graph) owns the listener registry populated by user code +
2082
+ // compiled Angular components via `import.meta.hot.on`.
2083
+ // Forwarding `type: 'custom'` here is the only thing standing
2084
+ // between server-emitted events and the listeners they were
2085
+ // meant for.
2086
+ //
2087
+ // `angular:component-update` is the canonical example. Analog's
2088
+ // plugin sends it on `.html` / component-style edits; the
2089
+ // compiled component `.mjs` registered a listener that
2090
+ // dynamic-imports `/@ng/component?c=<id>&t=<ts>` and calls
2091
+ // `ɵɵreplaceMetadata` on the live class — swapping the template
2092
+ // definition AND walking live `LView`s to recreate matching views
2093
+ // in-place. The page stays mounted and only the changed bits
2094
+ // re-render. We MUST `return` after dispatch so the reboot path
2095
+ // (`handleAngularHotUpdateMessage` → `__reboot_ng_modules__`)
2096
+ // never runs for these updates — that's the whole point of the
2097
+ // component-replacement pipeline.
2098
+ //
2099
+ // All other custom events are forwarded but NOT short-circuited
2100
+ // (Vite spec: custom events are additive — they don't replace
2101
+ // any framework-specific handling). The reboot path falls through
2102
+ // for `ns:angular-update` (the legacy/`.ts`-edit broadcast) and
2103
+ // for any framework not yet using the in-place replacement path.
2104
+ if (msg.type === 'custom' && typeof msg.event === 'string') {
2105
+ // Dispatch every Vite "custom" event through the JS hot
2106
+ // registry so `import.meta.hot.on(event, cb)` callbacks fire
2107
+ // on the device. Critical contract: this is the ONLY route by
2108
+ // which Analog's `angular:component-update` reaches the
2109
+ // compiled component's `(d) => d.id === id && Component_HmrLoad(...)`
2110
+ // listener — without it, server-side broadcasts log green
2111
+ // (`(client) hmr update`) while the device sees nothing happen.
2112
+ //
2113
+ // Diagnostic policy: listener exceptions log loud (compiled
2114
+ // HmrLoad fetch/parse error). Successful dispatches are silent.
2115
+ try {
2116
+ getNsHotRegistry().dispatchHotEvent(msg.event, msg.data);
2117
+ }
2118
+ catch (err) {
2119
+ console.warn('[hmr-client][custom] dispatch threw for', msg.event, err);
2120
+ }
2121
+ if (msg.event === 'angular:component-update') {
2122
+ if (VERBOSE)
2123
+ console.log('[hmr-client][custom] dispatched angular:component-update — skipping reboot path');
2124
+ // Walk the apply-progress overlay through its
2125
+ // remaining stages for the in-place template-swap
2126
+ // path. The full reboot path
2127
+ // (`handleAngularHotUpdateMessage`) drives the
2128
+ // overlay itself ('received' → 'evicting' →
2129
+ // 'reimporting' → 'rebooting' → 'complete'); the
2130
+ // in-place path bypasses that handler entirely
2131
+ // because the work happens inside Angular's
2132
+ // `ɵɵreplaceMetadata` after the runtime forwards the
2133
+ // `angular:component-update` event to the compiled
2134
+ // component's listener. Without this update the
2135
+ // overlay would freeze at 5% ('received') even
2136
+ // though the visual swap completes a few frames
2137
+ // later — exactly the "Preparing update (5%)" stuck
2138
+ // frame we have been chasing.
2139
+ //
2140
+ // We transition straight to 'reimporting' to
2141
+ // communicate that metadata is being fetched (the
2142
+ // runtime listener fires `__ns_import('/@ng/component?c=...&t=...')`),
2143
+ // then schedule 'complete' on the next macrotask so
2144
+ // the auto-hide timer kicks in. The actual
2145
+ // template swap is fire-and-forget from this point;
2146
+ // the user sees the overlay close at the same time
2147
+ // as Angular re-renders the bound text/structure.
2148
+ try {
2149
+ const filePath = typeof msg.data?.id === 'string' ? decodeURIComponent(msg.data.id).split('@')[0] : undefined;
2150
+ const detail = filePath ? `Applying template update to ${filePath}` : 'Applying template update';
2151
+ setUpdateOverlayStage('reimporting', { detail });
2152
+ setTimeout(() => {
2153
+ try {
2154
+ setUpdateOverlayStage('complete', { detail: filePath ? `Updated ${filePath}` : 'Update applied' });
2155
+ }
2156
+ catch { }
2157
+ }, 16);
2158
+ }
2159
+ catch { }
2160
+ return;
2161
+ }
2162
+ }
1549
2163
  if (msg.type === 'ns:angular-update' && typeof msg.version === 'number') {
1550
2164
  setGraphVersion(Number(msg.version || getGraphVersion() || 0));
1551
2165
  }
1552
- if (await handleAngularHotUpdateMessage(msg, { getCore, verbose: VERBOSE })) {
2166
+ if (CLIENT_STRATEGY?.handleHotUpdateMessage && (await CLIENT_STRATEGY.handleHotUpdateMessage(msg, { getCore, verbose: VERBOSE, performResetRoot, getOverlay: getHmrOverlayApi }))) {
1553
2167
  return;
1554
2168
  }
1555
2169
  }
@@ -1583,27 +2197,47 @@ async function handleHmrMessage(ev) {
1583
2197
  return;
1584
2198
  }
1585
2199
  if (msg.type === 'ns:css-updates' && Array.isArray(msg.updates)) {
2200
+ // Drive the HMR-applying overlay past the 'received' (5%) frame
2201
+ // that `ns:hmr-pending` set earlier in the cycle. Without this
2202
+ // the overlay sticks at "Preparing update" forever for CSS-only
2203
+ // edits because `handleCssUpdates` is a leaf — there's no
2204
+ // downstream module-evaluation path that would hit the queue's
2205
+ // 'complete' transition.
2206
+ const cssCount = msg.updates.length;
2207
+ try {
2208
+ setUpdateOverlayStage('reimporting', { detail: buildCssApplyingDetail(cssCount) });
2209
+ }
2210
+ catch { }
1586
2211
  try {
1587
2212
  const origin = msg.origin || getHttpOriginForVite() || deriveHttpOrigin(getHMRWsUrl());
1588
2213
  await handleCssUpdates(msg.updates, origin);
2214
+ try {
2215
+ setUpdateOverlayStage('complete', { detail: buildCssAppliedDetail(cssCount) });
2216
+ }
2217
+ catch { }
1589
2218
  return;
1590
2219
  }
1591
2220
  catch (e) {
1592
2221
  console.warn('[hmr-client] CSS updates handling failed:', e);
2222
+ try {
2223
+ setUpdateOverlayStage('complete', { detail: 'CSS update failed' });
2224
+ }
2225
+ catch { }
1593
2226
  return;
1594
2227
  }
1595
2228
  }
1596
2229
  if (msg.type === 'ns:vue-sfc-registry') {
1597
- handleVueSfcRegistry(msg);
2230
+ CLIENT_STRATEGY?.handleSfcRegistry?.(msg);
1598
2231
  return;
1599
2232
  }
1600
2233
  if (msg.type === 'ns:vue-sfc-registry-update') {
1601
2234
  if (typeof msg.version === 'number')
1602
2235
  setGraphVersion(msg.version);
1603
- const comp = await handleVueSfcRegistryUpdate(msg, getGraphVersion());
1604
- if (comp) {
1605
- await performResetRoot(comp);
1606
- }
2236
+ // `ns:hmr-pending` already set the overlay to 'received' (5%). The Vue
2237
+ // strategy walks 'evicting' → 'reimporting' → 'rebooting' → 'complete'
2238
+ // around the SFC load + reset so the toast always lands on 'complete'
2239
+ // (or a failure detail) and the auto-hide timer can dismiss it.
2240
+ await CLIENT_STRATEGY?.handleSfcRegistryUpdate?.(msg, getGraphVersion(), { getCore, verbose: VERBOSE, performResetRoot, getOverlay: getHmrOverlayApi });
1607
2241
  return;
1608
2242
  }
1609
2243
  }
@@ -1623,9 +2257,9 @@ function normalizeComponent(input, nameHint) {
1623
2257
  }
1624
2258
  // If provided a render function, wrap with defineComponent
1625
2259
  if (typeof input === 'function') {
1626
- ensureVueGlobals();
1627
- const comp = globalThis.defineComponent
1628
- ? globalThis.defineComponent({
2260
+ CLIENT_STRATEGY?.beforeNavigateBuild?.();
2261
+ const comp = getGlobalScope().defineComponent
2262
+ ? getGlobalScope().defineComponent({
1629
2263
  name: nameHint || input.name || 'AnonymousSFC',
1630
2264
  render: input,
1631
2265
  })
@@ -1634,9 +2268,9 @@ function normalizeComponent(input, nameHint) {
1634
2268
  }
1635
2269
  // If object has a render function property
1636
2270
  if (input?.render && typeof input.render === 'function') {
1637
- ensureVueGlobals();
1638
- const comp = globalThis.defineComponent
1639
- ? globalThis.defineComponent({
2271
+ CLIENT_STRATEGY?.beforeNavigateBuild?.();
2272
+ const comp = getGlobalScope().defineComponent
2273
+ ? getGlobalScope().defineComponent({
1640
2274
  name: nameHint || input.name || 'AnonymousSFC',
1641
2275
  render: input.render,
1642
2276
  })
@@ -1692,35 +2326,32 @@ async function performResetRoot(newComponent) {
1692
2326
  if (cachedRoot)
1693
2327
  return cachedRoot;
1694
2328
  try {
1695
- switch (TARGET_FLAVOR) {
1696
- case 'vue':
1697
- cachedRoot = getRootForVue(newComponent, state);
1698
- break;
1699
- case 'typescript': {
1700
- // For TS flavor, treat the component as a factory or direct NS view.
1701
- let root = null;
1702
- try {
1703
- if (typeof newComponent === 'function') {
1704
- root = newComponent();
1705
- }
1706
- else {
1707
- root = newComponent;
1708
- }
2329
+ if (CLIENT_STRATEGY?.createRoot) {
2330
+ cachedRoot = CLIENT_STRATEGY.createRoot(newComponent, state);
2331
+ }
2332
+ else if (TS_LIKE_FLAVOR) {
2333
+ // For TS flavor, treat the component as a factory or direct NS view.
2334
+ let root = null;
2335
+ try {
2336
+ if (typeof newComponent === 'function') {
2337
+ root = newComponent();
1709
2338
  }
1710
- catch (e) {
1711
- console.warn('[hmr-client][ts] root factory invocation failed', e);
2339
+ else {
2340
+ root = newComponent;
1712
2341
  }
1713
- cachedRoot = root || {};
1714
- // Heuristic: if the root "looks" like a Frame, prefer frame semantics
1715
- try {
1716
- const name = String(cachedRoot?.constructor?.name || '').replace(/^_+/, '');
1717
- if (/^Frame(\$\d+)?$/.test(name)) {
1718
- rootKind = 'frame';
1719
- }
2342
+ }
2343
+ catch (e) {
2344
+ console.warn('[hmr-client][ts] root factory invocation failed', e);
2345
+ }
2346
+ cachedRoot = root || {};
2347
+ // Heuristic: if the root "looks" like a Frame, prefer frame semantics
2348
+ try {
2349
+ const name = String(cachedRoot?.constructor?.name || '').replace(/^_+/, '');
2350
+ if (/^Frame(\$\d+)?$/.test(name)) {
2351
+ rootKind = 'frame';
1720
2352
  }
1721
- catch { }
1722
- break;
1723
2353
  }
2354
+ catch { }
1724
2355
  }
1725
2356
  return cachedRoot;
1726
2357
  }
@@ -1738,7 +2369,7 @@ async function performResetRoot(newComponent) {
1738
2369
  return factory;
1739
2370
  }
1740
2371
  // Android readiness before any root changes
1741
- const App = getCore('Application') || globalThis.Application;
2372
+ const App = getCore('Application') || getGlobalScope().Application;
1742
2373
  const isAndroid = !!(App && App.android !== undefined);
1743
2374
  if (isAndroid) {
1744
2375
  const isReady = () => {
@@ -1840,7 +2471,7 @@ async function performResetRoot(newComponent) {
1840
2471
  }
1841
2472
  catch { }
1842
2473
  try {
1843
- const AppAny = getCore('Application') || globalThis.Application;
2474
+ const AppAny = getCore('Application') || getGlobalScope().Application;
1844
2475
  isIOS = !!(AppAny && AppAny.ios !== undefined);
1845
2476
  }
1846
2477
  catch { }
@@ -1850,7 +2481,7 @@ async function performResetRoot(newComponent) {
1850
2481
  // - Otherwise (subsequent HMR updates with an authoritative Frame already in place), re-use the
1851
2482
  // current app Frame and navigate to the new Page. This avoids a brief flash that can occur
1852
2483
  // when swapping the entire root view on Android. The placeholder is never involved here.
1853
- const gAnyForPolicy = globalThis;
2484
+ const gAnyForPolicy = getGlobalScope();
1854
2485
  const placeholderFrame = (() => {
1855
2486
  try {
1856
2487
  return gAnyForPolicy.__NS_DEV_PLACEHOLDER_ROOT_VIEW__ || null;
@@ -1865,7 +2496,14 @@ async function performResetRoot(newComponent) {
1865
2496
  }
1866
2497
  catch { }
1867
2498
  const isAuthoritativeFrame = !!existingAppFrame && existingAppFrame !== placeholderFrame;
1868
- if (!hadPlaceholder && !isFrameRoot && isAuthoritativeFrame && typeof existingAppFrame.navigate === 'function') {
2499
+ // Vue: skip the in-place navigate path. After `app.mount(NSVRoot)` in getRootForVue the
2500
+ // new Page already has a parent (the freshly-constructed NSVRoot), so an attempt to navigate
2501
+ // the existing app Frame to that same Page completes silently without ever rebinding the
2502
+ // page to the Frame — the screen keeps showing the previous render. resetRootView with a
2503
+ // fresh Frame correctly reparents the Page and is the proven path that produces visible
2504
+ // in-place updates for SFC HMR cycles. Non-Vue flavors keep the legacy navigate fast path.
2505
+ const allowNavigateFastPath = CLIENT_STRATEGY?.allowNavigateFastPath ?? true;
2506
+ if (allowNavigateFastPath && !hadPlaceholder && !isFrameRoot && isAuthoritativeFrame && typeof existingAppFrame.navigate === 'function') {
1869
2507
  try {
1870
2508
  const navEntry = {
1871
2509
  create: () => preparedRoot,
@@ -1884,7 +2522,7 @@ async function performResetRoot(newComponent) {
1884
2522
  console.log('[hmr-client] full root replacement via resetRootView (placeholder will be discarded)', { isFrameRoot, isIOS, hadPlaceholder });
1885
2523
  // Fallback or preferred path: resetRootView with a creator that builds a fresh Frame and navigates to the new Page
1886
2524
  try {
1887
- const App2 = getCore('Application') || globalThis.Application;
2525
+ const App2 = getCore('Application') || getGlobalScope().Application;
1888
2526
  if (!App2 || typeof App2.resetRootView !== 'function') {
1889
2527
  console.warn('[hmr-client] Application.resetRootView unavailable');
1890
2528
  return false;
@@ -1903,7 +2541,7 @@ async function performResetRoot(newComponent) {
1903
2541
  if (VERBOSE)
1904
2542
  console.warn('[hmr-client] iOS Application.window is boolean false; attempting to clear cached window');
1905
2543
  try {
1906
- const g = globalThis;
2544
+ const g = getGlobalScope();
1907
2545
  const reg = g.__nsVendorRegistry;
1908
2546
  const req = reg?.get ? g.__nsVendorRequire || g.__nsRequire || g.require : g.__nsRequire || g.require;
1909
2547
  let helpers = null;
@@ -2007,6 +2645,20 @@ export function initHmrClient(opts) {
2007
2645
  }
2008
2646
  g.__NS_HMR_CLIENT_ACTIVE__ = true;
2009
2647
  ensureCoreAliasesOnGlobalThis();
2648
+ // XML flavor: record string-module modals from the moment the client is up
2649
+ // so an already-open modal can be re-presented when its files change.
2650
+ // Installed at init (not first-update time) because the wrap can only
2651
+ // observe showModal calls made AFTER it lands. Retried briefly because
2652
+ // getCore('View') may not resolve until the vendor realm finishes booting.
2653
+ if (TS_LIKE_FLAVOR) {
2654
+ const tryInstallModalTracking = (attempts) => {
2655
+ ensureModalTracking();
2656
+ if (!modalTrackingInstalled && attempts > 0) {
2657
+ setTimeout(() => tryInstallModalTracking(attempts - 1), 250);
2658
+ }
2659
+ };
2660
+ tryInstallModalTracking(40);
2661
+ }
2010
2662
  // Defer WebSocket connection until boot completes to avoid native V8 crashes
2011
2663
  // caused by concurrent WebSocket message handling + HTTP fetch during early startup.
2012
2664
  // The WebSocket is only needed for HMR updates, not the initial boot sequence.
@@ -2028,12 +2680,9 @@ export function initHmrClient(opts) {
2028
2680
  console.log('[hmr-client] deferring WebSocket connection until boot completes');
2029
2681
  setTimeout(waitForBoot, 100);
2030
2682
  }
2031
- // Best-effort: install back wrapper even before first remount; original root may be captured later
2032
- switch (TARGET_FLAVOR) {
2033
- case 'vue':
2034
- ensureBackWrapperInstalled(performResetRoot, getCore);
2035
- break;
2036
- }
2683
+ // Best-effort: install back wrapper even before first remount; original root may be captured later.
2684
+ // Deferred until the dynamically-imported strategy resolves.
2685
+ void CLIENT_STRATEGY_READY.then(() => CLIENT_STRATEGY?.installBackWrapper?.(performResetRoot, getCore));
2037
2686
  }
2038
2687
  export default function startViteHMR(opts) {
2039
2688
  if (VERBOSE)