@nativescript/vite 8.0.0-alpha.4 → 8.0.0-alpha.41

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 (410) 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 +256 -30
  7. package/configuration/base.js.map +1 -1
  8. package/configuration/javascript.js +12 -90
  9. package/configuration/javascript.js.map +1 -1
  10. package/configuration/solid.js +33 -4
  11. package/configuration/solid.js.map +1 -1
  12. package/configuration/typescript.js +10 -90
  13. package/configuration/typescript.js.map +1 -1
  14. package/helpers/app-components.d.ts +2 -1
  15. package/helpers/app-components.js.map +1 -1
  16. package/helpers/app-css-state.d.ts +8 -0
  17. package/helpers/app-css-state.js +8 -0
  18. package/helpers/app-css-state.js.map +1 -0
  19. package/helpers/bundler-context.d.ts +11 -0
  20. package/helpers/bundler-context.js +71 -0
  21. package/helpers/bundler-context.js.map +1 -0
  22. package/helpers/config-as-json.js +10 -0
  23. package/helpers/config-as-json.js.map +1 -1
  24. package/helpers/css-platform-plugin.d.ts +14 -0
  25. package/helpers/css-platform-plugin.js +43 -25
  26. package/helpers/css-platform-plugin.js.map +1 -1
  27. package/helpers/dev-host.d.ts +360 -0
  28. package/helpers/dev-host.js +692 -0
  29. package/helpers/dev-host.js.map +1 -0
  30. package/helpers/dynamic-import-plugin.js +1 -1
  31. package/helpers/dynamic-import-plugin.js.map +1 -1
  32. package/helpers/esbuild-platform-resolver.js +4 -1
  33. package/helpers/esbuild-platform-resolver.js.map +1 -1
  34. package/helpers/external-configs.d.ts +10 -12
  35. package/helpers/external-configs.js +54 -35
  36. package/helpers/external-configs.js.map +1 -1
  37. package/helpers/global-defines.d.ts +128 -0
  38. package/helpers/global-defines.js +174 -11
  39. package/helpers/global-defines.js.map +1 -1
  40. package/helpers/hmr-scope.d.ts +26 -0
  41. package/helpers/hmr-scope.js +67 -0
  42. package/helpers/hmr-scope.js.map +1 -0
  43. package/helpers/init.js +0 -18
  44. package/helpers/init.js.map +1 -1
  45. package/helpers/logging.d.ts +1 -0
  46. package/helpers/logging.js +65 -4
  47. package/helpers/logging.js.map +1 -1
  48. package/helpers/main-entry.d.ts +3 -1
  49. package/helpers/main-entry.js +444 -50
  50. package/helpers/main-entry.js.map +1 -1
  51. package/helpers/nativeclass-esbuild-plugin.d.ts +2 -1
  52. package/helpers/nativeclass-esbuild-plugin.js.map +1 -1
  53. package/helpers/nativeclass-transform.js +5 -6
  54. package/helpers/nativeclass-transform.js.map +1 -1
  55. package/helpers/nativeclass-transformer-plugin.d.ts +9 -2
  56. package/helpers/nativeclass-transformer-plugin.js +157 -14
  57. package/helpers/nativeclass-transformer-plugin.js.map +1 -1
  58. package/helpers/nativescript-package-resolver.js +10 -62
  59. package/helpers/nativescript-package-resolver.js.map +1 -1
  60. package/helpers/normalize-id.d.ts +42 -0
  61. package/helpers/normalize-id.js +60 -0
  62. package/helpers/normalize-id.js.map +1 -0
  63. package/helpers/ns-core-url.d.ts +106 -0
  64. package/helpers/ns-core-url.js +225 -0
  65. package/helpers/ns-core-url.js.map +1 -0
  66. package/helpers/optimize-deps.d.ts +16 -0
  67. package/helpers/optimize-deps.js +17 -0
  68. package/helpers/optimize-deps.js.map +1 -0
  69. package/helpers/package-platform-aliases.js +34 -49
  70. package/helpers/package-platform-aliases.js.map +1 -1
  71. package/helpers/platform-types.d.ts +2 -0
  72. package/helpers/platform-types.js +2 -0
  73. package/helpers/platform-types.js.map +1 -0
  74. package/helpers/postcss-platform-config.d.ts +17 -1
  75. package/helpers/postcss-platform-config.js +20 -37
  76. package/helpers/postcss-platform-config.js.map +1 -1
  77. package/helpers/project.d.ts +35 -0
  78. package/helpers/project.js +120 -2
  79. package/helpers/project.js.map +1 -1
  80. package/helpers/resolve-main-field-platform.d.ts +20 -0
  81. package/helpers/resolve-main-field-platform.js +49 -0
  82. package/helpers/resolve-main-field-platform.js.map +1 -0
  83. package/helpers/resolver.js +17 -2
  84. package/helpers/resolver.js.map +1 -1
  85. package/helpers/theme-core-plugins.js +1 -1
  86. package/helpers/theme-core-plugins.js.map +1 -1
  87. package/helpers/ts-config-paths.d.ts +14 -0
  88. package/helpers/ts-config-paths.js +90 -9
  89. package/helpers/ts-config-paths.js.map +1 -1
  90. package/helpers/typescript-check.d.ts +2 -1
  91. package/helpers/typescript-check.js.map +1 -1
  92. package/helpers/ui-registration.d.ts +21 -0
  93. package/helpers/ui-registration.js +156 -0
  94. package/helpers/ui-registration.js.map +1 -0
  95. package/helpers/utils.js +1 -2
  96. package/helpers/utils.js.map +1 -1
  97. package/helpers/workers.d.ts +20 -19
  98. package/helpers/workers.js +624 -4
  99. package/helpers/workers.js.map +1 -1
  100. package/hmr/client/css-handler.d.ts +2 -1
  101. package/hmr/client/css-handler.js +50 -26
  102. package/hmr/client/css-handler.js.map +1 -1
  103. package/hmr/client/css-update-overlay.d.ts +18 -0
  104. package/hmr/client/css-update-overlay.js +27 -0
  105. package/hmr/client/css-update-overlay.js.map +1 -0
  106. package/hmr/client/framework-client-strategy.d.ts +79 -0
  107. package/hmr/client/framework-client-strategy.js +19 -0
  108. package/hmr/client/framework-client-strategy.js.map +1 -0
  109. package/hmr/client/hmr-pending-overlay.d.ts +13 -0
  110. package/hmr/client/hmr-pending-overlay.js +60 -0
  111. package/hmr/client/hmr-pending-overlay.js.map +1 -0
  112. package/hmr/client/index.js +820 -212
  113. package/hmr/client/index.js.map +1 -1
  114. package/hmr/client/utils.d.ts +7 -1
  115. package/hmr/client/utils.js +207 -29
  116. package/hmr/client/utils.js.map +1 -1
  117. package/hmr/entry-runtime.d.ts +2 -1
  118. package/hmr/entry-runtime.js +256 -69
  119. package/hmr/entry-runtime.js.map +1 -1
  120. package/hmr/frameworks/angular/build/angular-linker.d.ts +12 -0
  121. package/hmr/frameworks/angular/build/angular-linker.js +109 -0
  122. package/hmr/frameworks/angular/build/angular-linker.js.map +1 -0
  123. package/hmr/frameworks/angular/build/inject-component-hmr-registration.d.ts +112 -0
  124. package/hmr/frameworks/angular/build/inject-component-hmr-registration.js +291 -0
  125. package/hmr/frameworks/angular/build/inject-component-hmr-registration.js.map +1 -0
  126. package/hmr/frameworks/angular/build/inject-hmr-vite-ignore.d.ts +75 -0
  127. package/hmr/frameworks/angular/build/inject-hmr-vite-ignore.js +221 -0
  128. package/hmr/frameworks/angular/build/inject-hmr-vite-ignore.js.map +1 -0
  129. package/{helpers/angular → hmr/frameworks/angular/build}/inline-decorator-component-templates.js +1 -170
  130. package/hmr/frameworks/angular/build/inline-decorator-component-templates.js.map +1 -0
  131. package/hmr/frameworks/angular/build/js-lexer.d.ts +4 -0
  132. package/{helpers/angular/synthesize-decorator-ctor-parameters.js → hmr/frameworks/angular/build/js-lexer.js} +22 -96
  133. package/hmr/frameworks/angular/build/js-lexer.js.map +1 -0
  134. package/hmr/frameworks/angular/build/shared-linker.d.ts +39 -0
  135. package/hmr/frameworks/angular/build/shared-linker.js +128 -0
  136. package/hmr/frameworks/angular/build/shared-linker.js.map +1 -0
  137. package/hmr/frameworks/angular/build/synthesize-decorator-ctor-parameters.js +88 -0
  138. package/hmr/frameworks/angular/build/synthesize-decorator-ctor-parameters.js.map +1 -0
  139. package/{helpers/angular → hmr/frameworks/angular/build}/synthesize-injectable-factories.js +1 -174
  140. package/hmr/frameworks/angular/build/synthesize-injectable-factories.js.map +1 -0
  141. package/{helpers/angular → hmr/frameworks/angular/build}/util.d.ts +1 -0
  142. package/hmr/frameworks/angular/build/util.js +155 -0
  143. package/hmr/frameworks/angular/build/util.js.map +1 -0
  144. package/hmr/frameworks/angular/client/index.d.ts +1 -0
  145. package/hmr/frameworks/angular/client/index.js +803 -21
  146. package/hmr/frameworks/angular/client/index.js.map +1 -1
  147. package/hmr/frameworks/angular/client/strategy.d.ts +9 -0
  148. package/hmr/frameworks/angular/client/strategy.js +19 -0
  149. package/hmr/frameworks/angular/client/strategy.js.map +1 -0
  150. package/hmr/frameworks/angular/server/angular-root-component.d.ts +79 -0
  151. package/hmr/frameworks/angular/server/angular-root-component.js +149 -0
  152. package/hmr/frameworks/angular/server/angular-root-component.js.map +1 -0
  153. package/hmr/frameworks/angular/server/linker.js +1 -4
  154. package/hmr/frameworks/angular/server/linker.js.map +1 -1
  155. package/hmr/frameworks/angular/server/strategy.js +460 -12
  156. package/hmr/frameworks/angular/server/strategy.js.map +1 -1
  157. package/hmr/{server → frameworks/angular/server}/websocket-angular-entry.js +2 -2
  158. package/hmr/frameworks/angular/server/websocket-angular-entry.js.map +1 -0
  159. package/hmr/{server → frameworks/angular/server}/websocket-angular-hot-update.d.ts +17 -11
  160. package/hmr/frameworks/angular/server/websocket-angular-hot-update.js +336 -0
  161. package/hmr/frameworks/angular/server/websocket-angular-hot-update.js.map +1 -0
  162. package/hmr/frameworks/solid/build/solid-jsx-deps.d.ts +15 -0
  163. package/hmr/frameworks/solid/build/solid-jsx-deps.js +178 -0
  164. package/hmr/frameworks/solid/build/solid-jsx-deps.js.map +1 -0
  165. package/hmr/frameworks/solid/server/strategy.js +291 -16
  166. package/hmr/frameworks/solid/server/strategy.js.map +1 -1
  167. package/hmr/frameworks/typescript/server/strategy.js +38 -14
  168. package/hmr/frameworks/typescript/server/strategy.js.map +1 -1
  169. package/hmr/frameworks/vue/client/dep-propagation.d.ts +36 -0
  170. package/hmr/frameworks/vue/client/dep-propagation.js +101 -0
  171. package/hmr/frameworks/vue/client/dep-propagation.js.map +1 -0
  172. package/hmr/frameworks/vue/client/index.d.ts +8 -0
  173. package/hmr/frameworks/vue/client/index.js +56 -243
  174. package/hmr/frameworks/vue/client/index.js.map +1 -1
  175. package/hmr/frameworks/vue/client/strategy.d.ts +33 -0
  176. package/hmr/frameworks/vue/client/strategy.js +157 -0
  177. package/hmr/frameworks/vue/client/strategy.js.map +1 -0
  178. package/hmr/frameworks/vue/client/vue-sfc-update-overlay.d.ts +49 -0
  179. package/hmr/frameworks/vue/client/vue-sfc-update-overlay.js +142 -0
  180. package/hmr/frameworks/vue/client/vue-sfc-update-overlay.js.map +1 -0
  181. package/hmr/frameworks/vue/server/sfc-route-assemble.d.ts +7 -0
  182. package/hmr/frameworks/vue/server/sfc-route-assemble.js +747 -0
  183. package/hmr/frameworks/vue/server/sfc-route-assemble.js.map +1 -0
  184. package/hmr/frameworks/vue/server/sfc-route-meta.d.ts +7 -0
  185. package/hmr/frameworks/vue/server/sfc-route-meta.js +80 -0
  186. package/hmr/frameworks/vue/server/sfc-route-meta.js.map +1 -0
  187. package/hmr/frameworks/vue/server/sfc-route-serve.d.ts +8 -0
  188. package/hmr/frameworks/vue/server/sfc-route-serve.js +459 -0
  189. package/hmr/frameworks/vue/server/sfc-route-serve.js.map +1 -0
  190. package/hmr/frameworks/vue/server/sfc-route-shared.d.ts +38 -0
  191. package/hmr/frameworks/vue/server/sfc-route-shared.js +48 -0
  192. package/hmr/frameworks/vue/server/sfc-route-shared.js.map +1 -0
  193. package/hmr/frameworks/vue/server/strategy.d.ts +35 -0
  194. package/hmr/frameworks/vue/server/strategy.js +278 -1
  195. package/hmr/frameworks/vue/server/strategy.js.map +1 -1
  196. package/hmr/frameworks/vue/server/websocket-sfc.d.ts +15 -0
  197. package/hmr/frameworks/vue/server/websocket-sfc.js +20 -0
  198. package/hmr/frameworks/vue/server/websocket-sfc.js.map +1 -0
  199. package/hmr/helpers/ast-normalizer.d.ts +3 -1
  200. package/hmr/helpers/ast-normalizer.js +77 -10
  201. package/hmr/helpers/ast-normalizer.js.map +1 -1
  202. package/hmr/helpers/cjs-named-exports.d.ts +23 -0
  203. package/hmr/helpers/cjs-named-exports.js +152 -0
  204. package/hmr/helpers/cjs-named-exports.js.map +1 -0
  205. package/hmr/helpers/package-exports.d.ts +16 -0
  206. package/hmr/helpers/package-exports.js +396 -0
  207. package/hmr/helpers/package-exports.js.map +1 -0
  208. package/hmr/server/constants.js +20 -5
  209. package/hmr/server/constants.js.map +1 -1
  210. package/hmr/server/core-sanitize.d.ts +90 -7
  211. package/hmr/server/core-sanitize.js +211 -56
  212. package/hmr/server/core-sanitize.js.map +1 -1
  213. package/hmr/server/device-transform-helpers.d.ts +24 -0
  214. package/hmr/server/device-transform-helpers.js +369 -0
  215. package/hmr/server/device-transform-helpers.js.map +1 -0
  216. package/hmr/server/framework-strategy.d.ts +108 -11
  217. package/hmr/server/hmr-module-graph.d.ts +37 -0
  218. package/hmr/server/hmr-module-graph.js +214 -0
  219. package/hmr/server/hmr-module-graph.js.map +1 -0
  220. package/hmr/server/import-map.d.ts +11 -2
  221. package/hmr/server/import-map.js +95 -44
  222. package/hmr/server/import-map.js.map +1 -1
  223. package/hmr/server/index.js +7 -16
  224. package/hmr/server/index.js.map +1 -1
  225. package/hmr/server/ns-core-cjs-shape.d.ts +204 -0
  226. package/hmr/server/ns-core-cjs-shape.js +271 -0
  227. package/hmr/server/ns-core-cjs-shape.js.map +1 -0
  228. package/hmr/server/ns-rt-bridge.d.ts +51 -0
  229. package/hmr/server/ns-rt-bridge.js +131 -0
  230. package/hmr/server/ns-rt-bridge.js.map +1 -0
  231. package/hmr/server/ns-rt-route.d.ts +5 -0
  232. package/hmr/server/ns-rt-route.js +38 -0
  233. package/hmr/server/ns-rt-route.js.map +1 -0
  234. package/hmr/server/perf-instrumentation.d.ts +114 -0
  235. package/hmr/server/perf-instrumentation.js +197 -0
  236. package/hmr/server/perf-instrumentation.js.map +1 -0
  237. package/hmr/server/process-code-for-device.d.ts +14 -0
  238. package/hmr/server/process-code-for-device.js +702 -0
  239. package/hmr/server/process-code-for-device.js.map +1 -0
  240. package/hmr/server/require-guard.d.ts +1 -0
  241. package/hmr/server/require-guard.js +12 -0
  242. package/hmr/server/require-guard.js.map +1 -0
  243. package/hmr/server/rewrite-imports.d.ts +2 -0
  244. package/hmr/server/rewrite-imports.js +600 -0
  245. package/hmr/server/rewrite-imports.js.map +1 -0
  246. package/hmr/server/route-helpers.d.ts +7 -0
  247. package/hmr/server/route-helpers.js +13 -0
  248. package/hmr/server/route-helpers.js.map +1 -0
  249. package/hmr/server/server-origin.d.ts +2 -0
  250. package/hmr/server/server-origin.js +83 -0
  251. package/hmr/server/server-origin.js.map +1 -0
  252. package/hmr/server/shared-transform-request.js +13 -7
  253. package/hmr/server/shared-transform-request.js.map +1 -1
  254. package/hmr/server/transform-cache-invalidation.d.ts +37 -0
  255. package/hmr/server/transform-cache-invalidation.js +156 -0
  256. package/hmr/server/transform-cache-invalidation.js.map +1 -0
  257. package/hmr/server/vendor-bare-module-shims.d.ts +4 -0
  258. package/hmr/server/vendor-bare-module-shims.js +80 -0
  259. package/hmr/server/vendor-bare-module-shims.js.map +1 -0
  260. package/hmr/server/vite-plugin.js +72 -42
  261. package/hmr/server/vite-plugin.js.map +1 -1
  262. package/hmr/server/websocket-core-bridge.d.ts +45 -6
  263. package/hmr/server/websocket-core-bridge.js +81 -77
  264. package/hmr/server/websocket-core-bridge.js.map +1 -1
  265. package/hmr/server/websocket-css-hot-update.d.ts +33 -0
  266. package/hmr/server/websocket-css-hot-update.js +65 -0
  267. package/hmr/server/websocket-css-hot-update.js.map +1 -0
  268. package/hmr/server/websocket-device-transform.d.ts +3 -0
  269. package/hmr/server/websocket-device-transform.js +7 -0
  270. package/hmr/server/websocket-device-transform.js.map +1 -0
  271. package/hmr/server/websocket-graph-upsert.d.ts +15 -0
  272. package/hmr/server/websocket-graph-upsert.js +20 -0
  273. package/hmr/server/websocket-graph-upsert.js.map +1 -1
  274. package/hmr/server/websocket-hmr-pending.d.ts +37 -0
  275. package/hmr/server/websocket-hmr-pending.js +55 -0
  276. package/hmr/server/websocket-hmr-pending.js.map +1 -0
  277. package/hmr/server/websocket-hot-update.d.ts +77 -0
  278. package/hmr/server/websocket-hot-update.js +360 -0
  279. package/hmr/server/websocket-hot-update.js.map +1 -0
  280. package/hmr/server/websocket-import-map-route.d.ts +15 -0
  281. package/hmr/server/websocket-import-map-route.js +50 -0
  282. package/hmr/server/websocket-import-map-route.js.map +1 -0
  283. package/hmr/server/websocket-module-bindings.js +3 -3
  284. package/hmr/server/websocket-module-bindings.js.map +1 -1
  285. package/hmr/server/websocket-module-specifiers.d.ts +66 -2
  286. package/hmr/server/websocket-module-specifiers.js +203 -20
  287. package/hmr/server/websocket-module-specifiers.js.map +1 -1
  288. package/hmr/server/websocket-ns-core.d.ts +21 -0
  289. package/hmr/server/websocket-ns-core.js +311 -0
  290. package/hmr/server/websocket-ns-core.js.map +1 -0
  291. package/hmr/server/websocket-ns-entry.d.ts +21 -0
  292. package/hmr/server/websocket-ns-entry.js +164 -0
  293. package/hmr/server/websocket-ns-entry.js.map +1 -0
  294. package/hmr/server/websocket-ns-m-paths.d.ts +1 -1
  295. package/hmr/server/websocket-ns-m-paths.js +58 -13
  296. package/hmr/server/websocket-ns-m-paths.js.map +1 -1
  297. package/hmr/server/websocket-ns-m-request.d.ts +11 -1
  298. package/hmr/server/websocket-ns-m-request.js +18 -25
  299. package/hmr/server/websocket-ns-m-request.js.map +1 -1
  300. package/hmr/server/websocket-ns-m.d.ts +33 -0
  301. package/hmr/server/websocket-ns-m.js +752 -0
  302. package/hmr/server/websocket-ns-m.js.map +1 -0
  303. package/hmr/server/websocket-served-module-helpers.d.ts +82 -0
  304. package/hmr/server/websocket-served-module-helpers.js +879 -0
  305. package/hmr/server/websocket-served-module-helpers.js.map +1 -0
  306. package/hmr/server/websocket-txn.js +2 -8
  307. package/hmr/server/websocket-txn.js.map +1 -1
  308. package/hmr/server/websocket-vendor-unifier.d.ts +0 -1
  309. package/hmr/server/websocket-vendor-unifier.js +4 -9
  310. package/hmr/server/websocket-vendor-unifier.js.map +1 -1
  311. package/hmr/server/websocket.d.ts +8 -39
  312. package/hmr/server/websocket.js +514 -4030
  313. package/hmr/server/websocket.js.map +1 -1
  314. package/hmr/shared/ns-globals.d.ts +118 -0
  315. package/hmr/shared/ns-globals.js +29 -0
  316. package/hmr/shared/ns-globals.js.map +1 -0
  317. package/hmr/shared/protocol.d.ts +145 -0
  318. package/hmr/shared/protocol.js +28 -0
  319. package/hmr/shared/protocol.js.map +1 -0
  320. package/hmr/shared/runtime/boot-placeholder-ui.d.ts +69 -0
  321. package/hmr/shared/runtime/boot-placeholder-ui.js +101 -0
  322. package/hmr/shared/runtime/boot-placeholder-ui.js.map +1 -0
  323. package/hmr/shared/runtime/boot-progress.d.ts +44 -0
  324. package/hmr/shared/runtime/boot-progress.js +133 -0
  325. package/hmr/shared/runtime/boot-progress.js.map +1 -0
  326. package/hmr/shared/runtime/boot-timeline.d.ts +18 -0
  327. package/hmr/shared/runtime/boot-timeline.js +42 -0
  328. package/hmr/shared/runtime/boot-timeline.js.map +1 -0
  329. package/hmr/shared/runtime/browser-runtime-contract.js.map +1 -1
  330. package/hmr/shared/runtime/dev-overlay-snapshots.d.ts +31 -0
  331. package/hmr/shared/runtime/dev-overlay-snapshots.js +324 -0
  332. package/hmr/shared/runtime/dev-overlay-snapshots.js.map +1 -0
  333. package/hmr/shared/runtime/dev-overlay.d.ts +119 -26
  334. package/hmr/shared/runtime/dev-overlay.js +1153 -261
  335. package/hmr/shared/runtime/dev-overlay.js.map +1 -1
  336. package/hmr/shared/runtime/global-scope.d.ts +18 -0
  337. package/hmr/shared/runtime/global-scope.js +21 -0
  338. package/hmr/shared/runtime/global-scope.js.map +1 -0
  339. package/hmr/shared/runtime/hooks.js +2 -1
  340. package/hmr/shared/runtime/hooks.js.map +1 -1
  341. package/hmr/shared/runtime/http-only-boot.js +7 -6
  342. package/hmr/shared/runtime/http-only-boot.js.map +1 -1
  343. package/hmr/shared/runtime/module-provenance.js +4 -6
  344. package/hmr/shared/runtime/module-provenance.js.map +1 -1
  345. package/hmr/shared/runtime/root-placeholder-view.d.ts +19 -0
  346. package/hmr/shared/runtime/root-placeholder-view.js +311 -0
  347. package/hmr/shared/runtime/root-placeholder-view.js.map +1 -0
  348. package/hmr/shared/runtime/root-placeholder.js +371 -197
  349. package/hmr/shared/runtime/root-placeholder.js.map +1 -1
  350. package/hmr/shared/runtime/session-bootstrap.js +168 -4
  351. package/hmr/shared/runtime/session-bootstrap.js.map +1 -1
  352. package/hmr/shared/runtime/vendor-bootstrap.js +3 -10
  353. package/hmr/shared/runtime/vendor-bootstrap.js.map +1 -1
  354. package/hmr/shared/vendor/manifest-collect.d.ts +4 -0
  355. package/hmr/shared/vendor/manifest-collect.js +512 -0
  356. package/hmr/shared/vendor/manifest-collect.js.map +1 -0
  357. package/hmr/shared/vendor/manifest-loader.d.ts +2 -1
  358. package/hmr/shared/vendor/manifest-loader.js +5 -3
  359. package/hmr/shared/vendor/manifest-loader.js.map +1 -1
  360. package/hmr/shared/vendor/manifest.d.ts +1 -7
  361. package/hmr/shared/vendor/manifest.js +102 -741
  362. package/hmr/shared/vendor/manifest.js.map +1 -1
  363. package/hmr/shared/vendor/vendor-device-shim.d.ts +1 -0
  364. package/hmr/shared/vendor/vendor-device-shim.js +208 -0
  365. package/hmr/shared/vendor/vendor-device-shim.js.map +1 -0
  366. package/hmr/shared/vendor/vendor-esbuild-plugins.d.ts +16 -0
  367. package/hmr/shared/vendor/vendor-esbuild-plugins.js +203 -0
  368. package/hmr/shared/vendor/vendor-esbuild-plugins.js.map +1 -0
  369. package/hmr/vendor-bootstrap.d.ts +1 -3
  370. package/hmr/vendor-bootstrap.js +4 -6
  371. package/hmr/vendor-bootstrap.js.map +1 -1
  372. package/index.d.ts +1 -0
  373. package/index.js +5 -0
  374. package/index.js.map +1 -1
  375. package/package.json +55 -11
  376. package/runtime/core-aliases-early.js +25 -55
  377. package/runtime/core-aliases-early.js.map +1 -1
  378. package/helpers/angular/angular-linker.d.ts +0 -13
  379. package/helpers/angular/angular-linker.js +0 -194
  380. package/helpers/angular/angular-linker.js.map +0 -1
  381. package/helpers/angular/inline-decorator-component-templates.js.map +0 -1
  382. package/helpers/angular/shared-linker.d.ts +0 -11
  383. package/helpers/angular/shared-linker.js +0 -75
  384. package/helpers/angular/shared-linker.js.map +0 -1
  385. package/helpers/angular/synthesize-decorator-ctor-parameters.js.map +0 -1
  386. package/helpers/angular/synthesize-injectable-factories.js.map +0 -1
  387. package/helpers/angular/util.js +0 -67
  388. package/helpers/angular/util.js.map +0 -1
  389. package/helpers/prelink-angular.d.ts +0 -2
  390. package/helpers/prelink-angular.js +0 -117
  391. package/helpers/prelink-angular.js.map +0 -1
  392. package/hmr/server/websocket-angular-entry.js.map +0 -1
  393. package/hmr/server/websocket-angular-hot-update.js +0 -239
  394. package/hmr/server/websocket-angular-hot-update.js.map +0 -1
  395. package/hmr/server/websocket-ns-m-finalize.d.ts +0 -32
  396. package/hmr/server/websocket-ns-m-finalize.js +0 -73
  397. package/hmr/server/websocket-ns-m-finalize.js.map +0 -1
  398. package/hmr/server/websocket-runtime-compat.d.ts +0 -19
  399. package/hmr/server/websocket-runtime-compat.js +0 -286
  400. package/hmr/server/websocket-runtime-compat.js.map +0 -1
  401. package/hmr/server/websocket-vue-sfc.d.ts +0 -35
  402. package/hmr/server/websocket-vue-sfc.js +0 -1116
  403. package/hmr/server/websocket-vue-sfc.js.map +0 -1
  404. package/transformers/NativeClass/index.d.ts +0 -2
  405. package/transformers/NativeClass/index.js +0 -222
  406. package/transformers/NativeClass/index.js.map +0 -1
  407. /package/{helpers/angular → hmr/frameworks/angular/build}/inline-decorator-component-templates.d.ts +0 -0
  408. /package/{helpers/angular → hmr/frameworks/angular/build}/synthesize-decorator-ctor-parameters.d.ts +0 -0
  409. /package/{helpers/angular → hmr/frameworks/angular/build}/synthesize-injectable-factories.d.ts +0 -0
  410. /package/hmr/{server → frameworks/angular/server}/websocket-angular-entry.d.ts +0 -0
@@ -5,9 +5,11 @@
5
5
  * Always resolve core classes and Application from the vendor realm or globalThis at runtime.
6
6
  * The HMR client is evaluated via HTTP ESM on device; static imports would create secondary instances.
7
7
  */
8
- import { setHMRWsUrl, getHMRWsUrl, pendingModuleFetches, deriveHttpOrigin, setHttpOriginForVite, moduleFetchCache, requestModuleFromServer, getHttpOriginForVite, normalizeSpec, hmrMetrics, graph, setGraphVersion, getGraphVersion, getCurrentApp, getRootFrame, setCurrentApp, setRootFrame, getCore } from './utils.js';
8
+ import { setHMRWsUrl, getHMRWsUrl, pendingModuleFetches, deriveHttpOrigin, setHttpOriginForVite, moduleFetchCache, requestModuleFromServer, getHttpOriginForVite, normalizeSpec, hmrMetrics, graph, setGraphVersion, getGraphVersion, getCurrentApp, getRootFrame, setCurrentApp, setRootFrame, getCore, hasExplicitEviction, invalidateModulesByUrls, buildEvictionUrls, emitHmrModeBannerOnce, ENV_VERBOSE } from './utils.js';
9
9
  import { handleCssUpdates } from './css-handler.js';
10
- const VERBOSE = typeof __NS_ENV_VERBOSE__ !== 'undefined' && __NS_ENV_VERBOSE__;
10
+ import { buildCssApplyingDetail, buildCssAppliedDetail } from './css-update-overlay.js';
11
+ import { getGlobalScope } from '../shared/runtime/global-scope.js';
12
+ const VERBOSE = ENV_VERBOSE;
11
13
  function resolveTargetFlavor() {
12
14
  try {
13
15
  if (typeof __NS_TARGET_FLAVOR__ !== 'undefined' && __NS_TARGET_FLAVOR__) {
@@ -16,7 +18,7 @@ function resolveTargetFlavor() {
16
18
  }
17
19
  catch { }
18
20
  try {
19
- const g = globalThis;
21
+ const g = getGlobalScope();
20
22
  if (typeof g.__NS_TARGET_FLAVOR__ === 'string' && g.__NS_TARGET_FLAVOR__) {
21
23
  return g.__NS_TARGET_FLAVOR__;
22
24
  }
@@ -40,7 +42,11 @@ try {
40
42
  }
41
43
  }
42
44
  catch { }
43
- const APP_ROOT_VIRTUAL = typeof __NS_APP_ROOT_VIRTUAL__ === 'string' && __NS_APP_ROOT_VIRTUAL__ ? __NS_APP_ROOT_VIRTUAL__ : '/src';
45
+ // Define substitution does NOT reach this file (served raw from node_modules),
46
+ // so prefer the globalThis seed planted by the entry's defines-seed module —
47
+ // the '/src' literal is a last-resort default and is WRONG for 'app/'-rooted
48
+ // projects.
49
+ 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
50
  const APP_VIRTUAL_WITH_SLASH = APP_ROOT_VIRTUAL.endsWith('/') ? APP_ROOT_VIRTUAL : `${APP_ROOT_VIRTUAL}/`;
45
51
  const APP_MAIN_ENTRY_SPEC = `${APP_VIRTUAL_WITH_SLASH}app.ts`;
46
52
  // Policy: by default, let the app's own main entry mount initially; HMR client handles updates/remounts only.
@@ -101,12 +107,101 @@ function hideConnectionOverlay() {
101
107
  }
102
108
  catch { }
103
109
  }
110
+ function setUpdateOverlayStage(stage, info) {
111
+ try {
112
+ const api = getHmrOverlayApi();
113
+ if (api && typeof api.setUpdateStage === 'function') {
114
+ api.setUpdateStage(stage, info);
115
+ }
116
+ }
117
+ catch { }
118
+ }
119
+ // Store the listener registry on globalThis (rather than in a module-private
120
+ // closure) because in NativeScript the HMR client module and the user app
121
+ // modules can resolve to different module instances depending on how the
122
+ // dev runtime loads them (HTTP client URL vs. the bundled vendor realm).
123
+ // A module-local Set would not be shared across instances; the global one
124
+ // is.
125
+ function getNsSolidHmrListenerSet() {
126
+ const g = getGlobalScope();
127
+ let set = g.__ns_solid_hmr_listener_set;
128
+ if (!set) {
129
+ set = new Set();
130
+ g.__ns_solid_hmr_listener_set = set;
131
+ }
132
+ return set;
133
+ }
134
+ function nsSolidHmrSubscribe(fn) {
135
+ const listeners = getNsSolidHmrListenerSet();
136
+ listeners.add(fn);
137
+ if (VERBOSE)
138
+ console.log('[hmr][solid] subscribe — listeners=', listeners.size);
139
+ return () => listeners.delete(fn);
140
+ }
141
+ function nsSolidHmrEmit(ev) {
142
+ const listeners = getNsSolidHmrListenerSet();
143
+ if (VERBOSE)
144
+ console.log('[hmr][solid] emit listeners=', listeners.size, 'changedFiles=', ev.changedFiles);
145
+ for (const fn of Array.from(listeners)) {
146
+ try {
147
+ fn(ev);
148
+ }
149
+ catch (err) {
150
+ if (VERBOSE)
151
+ console.warn('[hmr][solid] listener threw', err);
152
+ }
153
+ }
154
+ }
155
+ try {
156
+ const g = getGlobalScope();
157
+ g.__ns_solid_hmr_subscribe = nsSolidHmrSubscribe;
158
+ // Eagerly create the listener set so the global exists at module load time.
159
+ getNsSolidHmrListenerSet();
160
+ if (VERBOSE)
161
+ console.log('[hmr][solid] HMR client loaded. global set=', typeof g.__ns_solid_hmr_subscribe, 'listenerSet=', typeof g.__ns_solid_hmr_listener_set);
162
+ }
163
+ catch (err) {
164
+ console.warn('[hmr][solid] could not install global __ns_solid_hmr_subscribe', err);
165
+ }
166
+ // Eagerly drive the HMR-applying overlay's 'received' frame as soon
167
+ // as the server emits `ns:hmr-pending`, BEFORE the framework-specific
168
+ // (`ns:angular-update` / `ns:css-updates`) payload arrives. The
169
+ // flavor-specific handler later walks through 'evicting' →
170
+ // 'reimporting' → 'rebooting' → 'complete'. Calling 'received' twice
171
+ // in the same cycle is safe: the overlay preserves
172
+ // `updateCycleStartedAt` when a 'received' frame replaces an existing
173
+ // 'received' frame so the minimum-visible window is still timed
174
+ // against the FIRST frame.
175
+ //
176
+ // Soft-fails when the overlay isn't installed (production builds,
177
+ // vitest, etc.) or when the user opted out via
178
+ // `__NS_HMR_PROGRESS_OVERLAY_ENABLED__ === false`.
179
+ import { applyHmrPendingFrame } from './hmr-pending-overlay.js';
180
+ function setHmrPendingOverlay(filePath) {
181
+ applyHmrPendingFrame(filePath, { getOverlay: getHmrOverlayApi });
182
+ }
104
183
  let connectionOverlayTimer = null;
105
184
  let connectionOverlayVisible = false;
106
185
  let hasOpenedHmrSocket = false;
107
186
  let awaitingHealthyHmrMessage = false;
108
187
  let pendingConnectionOverlayStage = 'connecting';
109
188
  let pendingConnectionOverlayDetail = '';
189
+ // The stage currently PAINTED on screen (set only when we actually show
190
+ // the overlay, not when we merely schedule it). Used to suppress the
191
+ // sub-perceptible offline↔reconnecting flicker during the reconnect
192
+ // retry loop — see the deferral in `connectHmr`/`tryNext`.
193
+ let shownConnectionOverlayStage = null;
194
+ // While the terminal 'offline' frame is showing, a fresh reconnect
195
+ // attempt that is refused near-instantly (the norm when the dev server
196
+ // is down — especially on the Android emulator, where connection-refused
197
+ // returns in ~1ms) would flip the overlay to 'reconnecting' and straight
198
+ // back to 'offline', producing a jarring 1-frame flicker. We defer the
199
+ // 'reconnecting' frame by this much so an instant failure (→ back to
200
+ // 'offline') or a success (→ 'synchronizing') cancels it before it ever
201
+ // paints; only a genuinely in-flight attempt surfaces 'reconnecting'.
202
+ // On iOS real connect attempts exceed this threshold, so the behaviour
203
+ // there is unchanged.
204
+ const RECONNECTING_OVER_OFFLINE_DELAY_MS = 500;
110
205
  function clearConnectionOverlayTimer() {
111
206
  if (connectionOverlayTimer) {
112
207
  clearTimeout(connectionOverlayTimer);
@@ -114,9 +209,14 @@ function clearConnectionOverlayTimer() {
114
209
  }
115
210
  }
116
211
  function showConnectionOverlayNow(stage, detail) {
212
+ // Painting a stage now supersedes any scheduled (deferred) stage, so
213
+ // cancel the pending timer — otherwise a deferred 'reconnecting' could
214
+ // fire moments after we settle back on 'offline' and revive the flicker.
215
+ clearConnectionOverlayTimer();
117
216
  pendingConnectionOverlayStage = stage;
118
217
  pendingConnectionOverlayDetail = detail || '';
119
218
  connectionOverlayVisible = true;
219
+ shownConnectionOverlayStage = stage;
120
220
  setConnectionOverlayStage(stage, detail);
121
221
  }
122
222
  function scheduleConnectionOverlay(stage, detail, delayMs = 1200) {
@@ -137,24 +237,25 @@ function updateConnectionOverlay(stage, detail) {
137
237
  function markHmrConnectionHealthy() {
138
238
  awaitingHealthyHmrMessage = false;
139
239
  clearConnectionOverlayTimer();
240
+ shownConnectionOverlayStage = null;
140
241
  if (connectionOverlayVisible) {
141
242
  connectionOverlayVisible = false;
142
243
  hideConnectionOverlay();
143
244
  }
144
245
  }
145
- /**
146
- * Flavor hooks
147
- */
148
- import { installNsVueDevShims, ensureBackWrapperInstalled, getRootForVue, loadSfcComponent, ensureVueGlobals, ensurePiniaOnApp, addSfcMapping, recordVuePayloadChanges, handleVueSfcRegistry, handleVueSfcRegistryUpdate, sfcArtifactMap } from '../frameworks/vue/client/index.js';
149
- import { handleAngularHotUpdateMessage, installAngularHmrClientHooks } from '../frameworks/angular/client/index.js';
150
- switch (TARGET_FLAVOR) {
151
- case 'vue':
152
- installNsVueDevShims();
153
- break;
154
- case 'angular':
155
- installAngularHmrClientHooks();
156
- break;
157
- }
246
+ let CLIENT_STRATEGY;
247
+ const CLIENT_STRATEGY_READY = TARGET_FLAVOR === 'vue' || TARGET_FLAVOR === 'angular'
248
+ ? import(`../frameworks/${TARGET_FLAVOR}/client/strategy.js`)
249
+ .then((mod) => {
250
+ CLIENT_STRATEGY = mod && mod[`${TARGET_FLAVOR}ClientStrategy`];
251
+ if (VERBOSE)
252
+ console.log('[hmr-client] client strategy loaded for flavor:', TARGET_FLAVOR);
253
+ CLIENT_STRATEGY?.install();
254
+ })
255
+ .catch((err) => {
256
+ console.warn('[hmr-client] failed to load client strategy for', TARGET_FLAVOR, err);
257
+ })
258
+ : Promise.resolve();
158
259
  // Track whether we've mounted an initial app root yet in HTTP-only boot
159
260
  let initialMounted = !!globalThis.__NS_HMR_BOOT_COMPLETE__;
160
261
  // Prevent duplicate initial-mount scheduling across rapid full-graph broadcasts and re-evaluations
@@ -172,7 +273,7 @@ let processingPromise = null;
172
273
  // Detect whether the early placeholder root is still active on screen
173
274
  function isPlaceholderActive() {
174
275
  try {
175
- const g = globalThis;
276
+ const g = getGlobalScope();
176
277
  if (g.__NS_DEV_PLACEHOLDER_ROOT_VIEW__)
177
278
  return true;
178
279
  if (g.__NS_DEV_PLACEHOLDER_ROOT_EARLY__)
@@ -202,7 +303,7 @@ function applyFullGraph(payload) {
202
303
  // causes a double-mount race (rescue fires at 450ms, then main.ts fires ~1s later,
203
304
  // causing a visual flash and leaving the app in an inconsistent state).
204
305
  try {
205
- const g = globalThis;
306
+ const g = getGlobalScope();
206
307
  const bootDone = !!g.__NS_HMR_BOOT_COMPLETE__;
207
308
  if (!bootDone && !initialMounted && !initialMounting && !g.__NS_HMR_RESCUE_SCHEDULED__ && TARGET_FLAVOR !== 'typescript') {
208
309
  // simple snapshot helpers
@@ -317,39 +418,7 @@ function applyFullGraph(payload) {
317
418
  }
318
419
  return;
319
420
  }
320
- let candidate = null;
321
- switch (TARGET_FLAVOR) {
322
- case 'vue': {
323
- const appEntry = graph.get(APP_MAIN_ENTRY_SPEC);
324
- if (appEntry && Array.isArray(appEntry.deps)) {
325
- const vueDep = appEntry.deps.find((d) => typeof d === 'string' && /\.vue$/i.test(d));
326
- if (vueDep)
327
- candidate = vueDep;
328
- }
329
- if (!candidate) {
330
- for (const id of graph.keys()) {
331
- if (/\.vue$/i.test(id)) {
332
- candidate = id;
333
- break;
334
- }
335
- }
336
- }
337
- // Fallback: when the module graph is empty (Vite 7+ may not populate it
338
- // before the first full-graph broadcast), check the SFC artifact registry
339
- // which is populated from the ns:vue-sfc-registry message.
340
- if (!candidate && sfcArtifactMap.size > 0) {
341
- for (const id of sfcArtifactMap.keys()) {
342
- if (/\.vue$/i.test(id)) {
343
- candidate = id;
344
- if (VERBOSE)
345
- console.log('[hmr][init] rescue candidate from SFC registry:', id);
346
- break;
347
- }
348
- }
349
- }
350
- break;
351
- }
352
- }
421
+ const candidate = CLIENT_STRATEGY?.selectMountCandidate?.({ graph, appMainEntrySpec: APP_MAIN_ENTRY_SPEC }) ?? null;
353
422
  if (!candidate)
354
423
  return;
355
424
  initialMounting = true;
@@ -360,10 +429,8 @@ function applyFullGraph(payload) {
360
429
  (async () => {
361
430
  try {
362
431
  let comp = null;
363
- switch (TARGET_FLAVOR) {
364
- case 'vue':
365
- comp = await loadSfcComponent(candidate, 'initial_mount_rescue');
366
- break;
432
+ if (CLIENT_STRATEGY?.loadComponentForMount) {
433
+ comp = await CLIENT_STRATEGY.loadComponentForMount(candidate, 'initial_mount_rescue');
367
434
  }
368
435
  if (!comp)
369
436
  return;
@@ -404,41 +471,11 @@ function applyFullGraph(payload) {
404
471
  // Only allow initial mount when explicitly enabled. Rely on the app's own main entry start() for the first mount
405
472
  // to avoid double-mount races that can cause duplicate navigation logs.
406
473
  if (ALLOW_INITIAL_MOUNT && !initialMounted && !bootDone && !bootInProgress && !getCurrentApp() && !getRootFrame()) {
407
- let candidate = null;
408
- switch (TARGET_FLAVOR) {
409
- case 'vue': {
410
- const appEntry = graph.get(APP_MAIN_ENTRY_SPEC);
411
- if (appEntry && Array.isArray(appEntry.deps)) {
412
- const vueDep = appEntry.deps.find((d) => typeof d === 'string' && /\.vue$/i.test(d));
413
- if (vueDep)
414
- candidate = vueDep;
415
- }
416
- if (!candidate) {
417
- for (const id of graph.keys()) {
418
- if (/\.vue$/i.test(id)) {
419
- candidate = id;
420
- break;
421
- }
422
- }
423
- }
424
- // Fallback: SFC registry (same as rescue mount above)
425
- if (!candidate && sfcArtifactMap.size > 0) {
426
- for (const id of sfcArtifactMap.keys()) {
427
- if (/\.vue$/i.test(id)) {
428
- candidate = id;
429
- if (VERBOSE)
430
- console.log('[hmr][init] initial mount candidate from SFC registry:', id);
431
- break;
432
- }
433
- }
434
- }
435
- break;
436
- }
437
- case 'typescript': {
438
- // For TS flavor, do not perform client-driven initial mount; rely on Application.run.
439
- return;
440
- }
474
+ if (TARGET_FLAVOR === 'typescript') {
475
+ // For TS flavor, do not perform client-driven initial mount; rely on Application.run.
476
+ return;
441
477
  }
478
+ const candidate = CLIENT_STRATEGY?.selectMountCandidate?.({ graph, appMainEntrySpec: APP_MAIN_ENTRY_SPEC }) ?? null;
442
479
  if (candidate) {
443
480
  // Mark initial-mount in progress (both module-local and global) BEFORE scheduling async work
444
481
  initialMounting = true;
@@ -452,7 +489,7 @@ function applyFullGraph(payload) {
452
489
  console.log('[hmr][init] mounting initial root from', candidate, 'flavor=', TARGET_FLAVOR);
453
490
  // Android-only: avoid racing entry-runtime reset and Activity bring-up
454
491
  try {
455
- const g = globalThis;
492
+ const g = getGlobalScope();
456
493
  const App = getCore('Application') || g.Application;
457
494
  const isAndroid = !!(App && App.android !== undefined);
458
495
  if (isAndroid) {
@@ -483,21 +520,19 @@ function applyFullGraph(payload) {
483
520
  }
484
521
  catch { }
485
522
  let comp = null;
486
- switch (TARGET_FLAVOR) {
487
- case 'vue':
488
- comp = await loadSfcComponent(candidate, 'initial_mount');
489
- break;
490
- case 'typescript':
491
- try {
492
- const url = await requestModuleFromServer(candidate);
493
- const mod = await import(/* @vite-ignore */ url);
494
- comp = mod && (mod.default || mod);
495
- }
496
- catch (e) {
497
- if (VERBOSE)
498
- console.warn('[hmr][init] TS initial mount failed to import', candidate, e);
499
- }
500
- break;
523
+ if (TARGET_FLAVOR === 'typescript') {
524
+ try {
525
+ const url = await requestModuleFromServer(candidate);
526
+ const mod = await import(/* @vite-ignore */ url);
527
+ comp = mod && (mod.default || mod);
528
+ }
529
+ catch (e) {
530
+ if (VERBOSE)
531
+ console.warn('[hmr][init] TS initial mount failed to import', candidate, e);
532
+ }
533
+ }
534
+ else if (CLIENT_STRATEGY?.loadComponentForMount) {
535
+ comp = await CLIENT_STRATEGY.loadComponentForMount(candidate, 'initial_mount');
501
536
  }
502
537
  if (comp) {
503
538
  const ok = await performResetRoot(comp);
@@ -558,11 +593,7 @@ function applyDelta(payload) {
558
593
  setGraphVersion(payload.newVersion);
559
594
  }
560
595
  const changed = payload.changed || [];
561
- switch (TARGET_FLAVOR) {
562
- case 'vue':
563
- recordVuePayloadChanges(changed, getGraphVersion());
564
- break;
565
- }
596
+ CLIENT_STRATEGY?.recordPayloadChanges?.(changed, getGraphVersion());
566
597
  (payload.changed || []).forEach((m) => {
567
598
  if (!m || !m.id)
568
599
  return;
@@ -629,7 +660,7 @@ function applyDelta(payload) {
629
660
  }
630
661
  if (isAppMainEntryId(id)) {
631
662
  try {
632
- const exists = globalThis.require?.(id) || globalThis.__nsGetModuleExports?.(id);
663
+ const exists = getGlobalScope().require?.(id) || globalThis.__nsGetModuleExports?.(id);
633
664
  if (!exists && VERBOSE)
634
665
  console.log(`[hmr][delta] skipping unresolved ${APP_MAIN_ENTRY_SPEC} change`);
635
666
  if (!exists)
@@ -645,14 +676,10 @@ function applyDelta(payload) {
645
676
  processQueue();
646
677
  }
647
678
  }
648
- // Deterministic navigation using the current Vue app instance rather than vendor-held rootApp
679
+ // Deterministic navigation using the current Vue app instance rather than vendor-held rootApp.
649
680
  function __nsNavigateUsingApp(comp, opts = {}) {
650
- const g = globalThis;
651
- switch (TARGET_FLAVOR) {
652
- case 'vue':
653
- ensureVueGlobals();
654
- break;
655
- }
681
+ const g = getGlobalScope();
682
+ CLIENT_STRATEGY?.beforeNavigateBuild?.();
656
683
  const AppFactory = g.createApp;
657
684
  const RootCtor = g.NSVRoot;
658
685
  if (typeof AppFactory !== 'function' || typeof RootCtor !== 'function') {
@@ -675,12 +702,14 @@ function __nsNavigateUsingApp(comp, opts = {}) {
675
702
  const buildTarget = () => {
676
703
  const existingApp = getCurrentApp();
677
704
  const baseProvides = (existingApp && existingApp._context && existingApp._context.provides) || {};
678
- const app = AppFactory(normalizeComponent(comp, comp && (comp.__name || comp.name)));
679
- switch (TARGET_FLAVOR) {
680
- case 'vue':
681
- ensurePiniaOnApp(app);
682
- break;
683
- }
705
+ // Forward `opts.props` as Vue's rootProps so `$navigateTo(Comp, { props: { } })`
706
+ // reaches the destination component. nativescript-vue's stock `$navigateTo`
707
+ // does the same via `createNativeView(target, options?.props, …)` →
708
+ // `renderer.createApp(component, props)`. Dropping props here would surface
709
+ // at the destination as `[Vue warn]: Missing required prop` and any
710
+ // required-prop component would render with `undefined` bindings.
711
+ const app = AppFactory(normalizeComponent(comp, comp && (comp.__name || comp.name)), opts && opts.props);
712
+ CLIENT_STRATEGY?.onNavAppCreated?.(app);
684
713
  try {
685
714
  const registry = g.__nsVendorRegistry;
686
715
  const req = registry?.get ? g.__nsVendorRequire || g.__nsRequire || g.require : g.__nsRequire || g.require;
@@ -775,6 +804,184 @@ try {
775
804
  globalThis.__nsNavigateUsingApp = __nsNavigateUsingApp;
776
805
  }
777
806
  catch { }
807
+ const openModalRecords = [];
808
+ let modalTrackingInstalled = false;
809
+ /**
810
+ * Map a served/graph module id (e.g. `/app/modal-page.xml`) to its app-root
811
+ * relative path (`modal-page.xml`). Single mapping point — the raw-asset
812
+ * re-registration, page-navigation targets, and modal matching all derive
813
+ * from this; keep them in sync by construction.
814
+ */
815
+ function toAppRelativePath(id) {
816
+ try {
817
+ const spec = normalizeSpec(id);
818
+ const appVirtual = APP_VIRTUAL_WITH_SLASH.replace(/^\//, '');
819
+ let relPath = spec.startsWith('/') ? spec.slice(1) : spec;
820
+ if (relPath.startsWith(appVirtual))
821
+ relPath = relPath.slice(appVirtual.length);
822
+ return relPath || null;
823
+ }
824
+ catch {
825
+ return null;
826
+ }
827
+ }
828
+ /** App-root relative module name (no extension) for page-shaped files, else null. */
829
+ function toAppModuleName(id) {
830
+ const relPath = toAppRelativePath(id);
831
+ if (!relPath || !/\.(xml|ts|js)$/i.test(relPath))
832
+ return null;
833
+ return relPath.replace(/\.(xml|ts|js)$/i, '');
834
+ }
835
+ function ensureModalTracking() {
836
+ if (modalTrackingInstalled)
837
+ return;
838
+ try {
839
+ const View = getCore('View') || getGlobalScope().View;
840
+ const proto = View?.prototype;
841
+ if (!proto || typeof proto.showModal !== 'function')
842
+ return;
843
+ const orig = proto.showModal;
844
+ if (orig.__nsHmrModalTracked) {
845
+ modalTrackingInstalled = true;
846
+ return;
847
+ }
848
+ const wrapped = function (...args) {
849
+ const result = orig.apply(this, args);
850
+ try {
851
+ if (typeof args[0] === 'string' && result) {
852
+ // Mirror core's getModalOptions arg shapes: (moduleName, options)
853
+ // or the deprecated positional form.
854
+ 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] };
855
+ const moduleName = String(args[0])
856
+ .replace(/^\.\//, '')
857
+ .replace(/\.(xml|ts|js)$/i, '');
858
+ openModalRecords.push({ moduleName, options, parent: this, modal: result });
859
+ if (VERBOSE)
860
+ console.log('[hmr][modal] tracked open modal', moduleName);
861
+ }
862
+ }
863
+ catch { }
864
+ return result;
865
+ };
866
+ wrapped.__nsHmrModalTracked = true;
867
+ proto.showModal = wrapped;
868
+ modalTrackingInstalled = true;
869
+ }
870
+ catch (e) {
871
+ if (VERBOSE)
872
+ console.warn('[hmr][modal] tracking install failed', e);
873
+ }
874
+ }
875
+ /**
876
+ * Enumerate the modals that are currently presented AND were opened by module
877
+ * name, with everything needed to re-present them.
878
+ *
879
+ * Source of truth is core's live modal stack (`_getRootModalViews()`):
880
+ * - `modal._moduleName` — set by the Builder on every createViewFromEntry
881
+ * view (longstanding, used by livesync), so available on any core.
882
+ * - `modal._modalOptions` — the original ShowModalOptions, stored by core's
883
+ * `_showNativeModalView` (newer cores). For older cores the showModal
884
+ * wrap's records (see ensureModalTracking) fill the gap.
885
+ * - `modal._modalParent` — the presenting view.
886
+ * Stale wrap records are pruned against the live stack while we're here.
887
+ */
888
+ function getOpenStringModuleModals() {
889
+ const out = [];
890
+ try {
891
+ const App = getCore('Application');
892
+ const root = App?.getRootView?.() || App?._rootView;
893
+ const stack = root?._getRootModalViews?.() || [];
894
+ // Prune wrap records whose modal is gone (keeps the fallback list small).
895
+ for (let i = openModalRecords.length - 1; i >= 0; i--) {
896
+ if (!stack.includes(openModalRecords[i].modal)) {
897
+ openModalRecords.splice(i, 1);
898
+ }
899
+ }
900
+ for (const modal of stack) {
901
+ const record = openModalRecords.find((r) => r.modal === modal);
902
+ const rawModuleName = typeof modal?._moduleName === 'string' && modal._moduleName ? modal._moduleName : record?.moduleName;
903
+ const parent = modal?._modalParent || record?.parent;
904
+ const options = modal?._modalOptions || record?.options;
905
+ if (!rawModuleName || !parent)
906
+ continue;
907
+ const moduleName = String(rawModuleName)
908
+ .replace(/^\.\//, '')
909
+ .replace(/\.(xml|ts|js)$/i, '');
910
+ out.push({ moduleName, options, parent, modal });
911
+ }
912
+ }
913
+ catch (e) {
914
+ if (VERBOSE)
915
+ console.warn('[hmr][modal] open-modal enumeration failed', e);
916
+ }
917
+ return out;
918
+ }
919
+ /**
920
+ * Close and re-present an open modal so it rebuilds from the freshly
921
+ * re-registered XML/code-behind. Core clears the modal stack synchronously on
922
+ * close but the NATIVE dismissal completes asynchronously; iOS refuses a
923
+ * present while a dismissal is in flight. Newer cores fire `closedModally` on
924
+ * the modal at exactly that completion point — preferred signal. Older cores
925
+ * fall back to polling `isLoaded` (flipped by `_tearDownUI` in the same
926
+ * completion callback).
927
+ */
928
+ async function reshowOpenModal(record) {
929
+ const { parent, modal, moduleName, options } = record;
930
+ await new Promise((resolve) => {
931
+ let settled = false;
932
+ const finish = () => {
933
+ if (!settled) {
934
+ settled = true;
935
+ resolve();
936
+ }
937
+ };
938
+ let eventArmed = false;
939
+ try {
940
+ if (typeof modal.once === 'function') {
941
+ modal.once('closedModally', finish);
942
+ eventArmed = true;
943
+ }
944
+ }
945
+ catch { }
946
+ // Poll fallback (also the safety net if the event never fires —
947
+ // e.g. an interactive-dismiss cancellation).
948
+ const deadline = Date.now() + 2000;
949
+ const poll = () => {
950
+ if (settled)
951
+ return;
952
+ let stillLoaded = false;
953
+ try {
954
+ stillLoaded = !!modal.isLoaded;
955
+ }
956
+ catch { }
957
+ if ((!eventArmed && !stillLoaded) || Date.now() > deadline) {
958
+ finish();
959
+ return;
960
+ }
961
+ setTimeout(poll, 50);
962
+ };
963
+ setTimeout(poll, 50);
964
+ try {
965
+ modal.closeModal();
966
+ }
967
+ catch (e) {
968
+ if (VERBOSE)
969
+ console.warn('[hmr][modal] close failed for', moduleName, e);
970
+ finish();
971
+ }
972
+ });
973
+ // One settle beat so the platform finishes releasing the presentation
974
+ // before the new present begins.
975
+ await new Promise((resolve) => setTimeout(resolve, 100));
976
+ try {
977
+ parent.showModal(moduleName, { ...(options || {}), animated: false });
978
+ if (VERBOSE)
979
+ console.log('[hmr][modal] re-presented', moduleName);
980
+ }
981
+ catch (e) {
982
+ console.warn('[hmr][modal] re-present failed for', moduleName, e);
983
+ }
984
+ }
778
985
  async function processQueue() {
779
986
  if (!globalThis.__NS_HMR_BOOT_COMPLETE__) {
780
987
  if (VERBOSE)
@@ -810,7 +1017,43 @@ async function processQueue() {
810
1017
  return;
811
1018
  if (VERBOSE)
812
1019
  console.log('[hmr][queue] processing changed ids', drained);
1020
+ // Track wall-clock so the 'complete' frame can show a meaningful
1021
+ // total. Only the Solid + TypeScript flavors drive the overlay
1022
+ // from here; Angular has its own flow inside
1023
+ // `frameworks/angular/client/index.ts`.
1024
+ const tQueueStart = Date.now();
1025
+ const driveSolidOverlay = TARGET_FLAVOR === 'solid';
1026
+ // Explicit eviction step.
1027
+ //
1028
+ // On modern runtimes the URL canonicalizer collapses any
1029
+ // `__ns_hmr__/<tag>/` segment back to a stable cache key, so
1030
+ // without explicit eviction the upcoming `import(url)` would
1031
+ // resolve via V8's `g_moduleRegistry` and return the cached
1032
+ // stale module — making the queue drain a silent no-op for
1033
+ // every save after the first.
1034
+ //
1035
+ // We hand the canonical eviction URLs to the runtime first;
1036
+ // `invalidateModulesByUrls` is a no-op on older runtimes and
1037
+ // `requestModuleFromServer` automatically falls back to the
1038
+ // legacy `/ns/m/__ns_hmr__/v<N>/` URL versioning path in that
1039
+ // case. node_modules and virtual specs are filtered out by
1040
+ // `buildEvictionUrls` so vendor modules stay hot.
1041
+ if (driveSolidOverlay) {
1042
+ setUpdateOverlayStage('evicting', {
1043
+ detail: drained.length === 1 ? `Invalidating ${drained[0]}` : `Invalidating ${drained.length} modules`,
1044
+ });
1045
+ }
1046
+ const evictUrls = buildEvictionUrls(drained);
1047
+ const evicted = invalidateModulesByUrls(evictUrls);
1048
+ if (VERBOSE)
1049
+ console.log(`[hmr][queue] eviction count=${evictUrls.length} ok=${evicted}`);
813
1050
  // Evaluate changed modules best-effort; failures shouldn't completely break HMR.
1051
+ if (driveSolidOverlay) {
1052
+ setUpdateOverlayStage('reimporting', {
1053
+ detail: drained.length === 1 ? `Re-importing ${drained[0]}` : `Re-importing ${drained.length} modules`,
1054
+ });
1055
+ }
1056
+ let reimportFailures = 0;
814
1057
  for (const id of drained) {
815
1058
  try {
816
1059
  const spec = normalizeSpec(id);
@@ -820,18 +1063,56 @@ async function processQueue() {
820
1063
  if (VERBOSE)
821
1064
  console.log('[hmr][queue] re-import', { id, spec, url });
822
1065
  const mod = await import(/* @vite-ignore */ url);
1066
+ // TS/XML flavor: refresh the bundler module registry with the fresh
1067
+ // exports so Builder.createViewFromEntry / loadModule('<page>')
1068
+ // resolves the NEW code-behind (tap handlers, page events) instead
1069
+ // of the stale module captured in the boot bundle. Without this,
1070
+ // XML re-renders pick up new markup but keep old behavior.
1071
+ if (TARGET_FLAVOR === 'typescript' && mod && /\.(ts|js)$/i.test(id)) {
1072
+ try {
1073
+ const g = getGlobalScope();
1074
+ const moduleName = toAppModuleName(id);
1075
+ if (moduleName && typeof g.registerModule === 'function') {
1076
+ g.registerModule(moduleName, () => mod);
1077
+ g.registerModule('./' + moduleName, () => mod);
1078
+ if (VERBOSE)
1079
+ console.log('[hmr][queue] re-registered code-behind', moduleName);
1080
+ }
1081
+ }
1082
+ catch (e) {
1083
+ if (VERBOSE)
1084
+ console.warn('[hmr][queue] code-behind re-register failed for', id, e);
1085
+ }
1086
+ }
823
1087
  }
824
1088
  catch (e) {
825
- if (VERBOSE)
826
- console.warn('[hmr][queue] re-import failed for', id, e);
1089
+ // Never silent: a failed re-import means the device may keep
1090
+ // running the previous module body, so always surface it (not
1091
+ // verbose-gated) — otherwise the overlay reports success while
1092
+ // the app runs stale code.
1093
+ reimportFailures++;
1094
+ console.warn('[hmr][queue] re-import FAILED for', id, '-', e?.message ?? e);
827
1095
  }
828
1096
  }
1097
+ if (reimportFailures > 0) {
1098
+ console.warn(`[hmr][queue] ${reimportFailures}/${drained.length} module(s) failed to re-import; the applied update may be incomplete.`);
1099
+ }
829
1100
  // After evaluating the batch, perform flavor-specific UI refresh.
830
1101
  switch (TARGET_FLAVOR) {
831
1102
  case 'vue':
832
- // Vue SFCs are handled via the registry update path; nothing to do here.
1103
+ // graph + performResetRoot + getOverlay let the Vue strategy
1104
+ // propagate non-SFC dep changes to the nearest `.vue` boundary
1105
+ // and remount it (see `propagateDepChangeToSfcBoundary`).
1106
+ await CLIENT_STRATEGY?.refreshAfterBatch?.(drained, { setUpdateOverlayStage, startedAt: tQueueStart, graph, performResetRoot, getOverlay: getHmrOverlayApi });
833
1107
  break;
834
1108
  case 'solid': {
1109
+ // Boundaries discovered in this HMR cycle (tsx files reachable
1110
+ // via the reverse import graph from any changed file, plus route
1111
+ // files reachable from any tsx start point). Declared at the top
1112
+ // of the case block so the emit step below can include the
1113
+ // complete set in the listener event — framework integrations
1114
+ // use it to map route boundaries → fresh component references.
1115
+ const boundaries = new Set();
835
1116
  // Solid .tsx components are self-accepting via solid-refresh's inline
836
1117
  // patchRegistry — re-importing them is sufficient. For non-component
837
1118
  // .ts utility modules, we must propagate up the import graph to find
@@ -850,8 +1131,10 @@ async function processQueue() {
850
1131
  arr.push(id);
851
1132
  }
852
1133
  }
853
- // BFS from each non-tsx changed module up to tsx/jsx boundaries
854
- const boundaries = new Set();
1134
+ // Pass 1: BFS from each non-tsx changed module up to tsx/jsx
1135
+ // boundaries. These get re-imported below so solid-refresh's
1136
+ // inline patchRegistry runs and (best-effort) swaps the proxy
1137
+ // signals for any components defined in those tsx boundaries.
855
1138
  for (const id of drained) {
856
1139
  if (/\.(tsx|jsx)$/i.test(id))
857
1140
  continue; // already self-accepting
@@ -875,6 +1158,51 @@ async function processQueue() {
875
1158
  }
876
1159
  }
877
1160
  }
1161
+ // Pass 2: walk further from any tsx starting point (a tsx file
1162
+ // in `drained` OR a tsx boundary discovered in pass 1) to find
1163
+ // route files (`/src/routes/*.{tsx,jsx}`) that transitively
1164
+ // import them. Re-importing a route file refreshes its
1165
+ // `Route.options.component` to the freshly-imported reference
1166
+ // and the existing boundary loop below patches the live router
1167
+ // with that fresh reference.
1168
+ //
1169
+ // This is the key fix for "edit home.tsx → save → no visual
1170
+ // update": the old BFS skipped tsx files in `drained` (assuming
1171
+ // solid-refresh's in-place proxy patch was sufficient), but in
1172
+ // the universal-renderer + nested-context configuration that
1173
+ // patch does not always propagate to the visible page tree.
1174
+ // Adding the route file as a boundary lets us patch
1175
+ // `route.options.component` directly to a fresh module export,
1176
+ // which the framework subscriber then passes through to the
1177
+ // page remount — making the cycle robust to the proxy patch
1178
+ // silently failing.
1179
+ const tsxStarts = new Set();
1180
+ for (const id of drained) {
1181
+ if (/\.(tsx|jsx)$/i.test(id))
1182
+ tsxStarts.add(id);
1183
+ }
1184
+ for (const b of boundaries)
1185
+ tsxStarts.add(b);
1186
+ const ROUTE_FILE_RE = /\/src\/routes\/.+\.(tsx|jsx)$/i;
1187
+ for (const start of tsxStarts) {
1188
+ const visited = new Set();
1189
+ const queue = [start];
1190
+ while (queue.length) {
1191
+ const cur = queue.shift();
1192
+ if (visited.has(cur))
1193
+ continue;
1194
+ visited.add(cur);
1195
+ if (cur !== start && ROUTE_FILE_RE.test(cur)) {
1196
+ boundaries.add(cur);
1197
+ }
1198
+ const importers = reverseIndex.get(cur);
1199
+ if (!importers)
1200
+ continue;
1201
+ for (const imp of importers) {
1202
+ queue.push(imp);
1203
+ }
1204
+ }
1205
+ }
878
1206
  // Re-import each boundary so solid-refresh patchRegistry fires.
879
1207
  // For route files (TanStack Router), capture the new Route export
880
1208
  // and patch the router's existing route with the fresh loader.
@@ -885,7 +1213,7 @@ async function processQueue() {
885
1213
  const findRouter = () => {
886
1214
  if (discoveredRouter)
887
1215
  return discoveredRouter;
888
- const g = globalThis;
1216
+ const g = getGlobalScope();
889
1217
  if (g.__ns_router?.routesById)
890
1218
  return (discoveredRouter = g.__ns_router);
891
1219
  // Fallback: scan common global keys for router
@@ -924,6 +1252,15 @@ async function processQueue() {
924
1252
  }
925
1253
  return null;
926
1254
  };
1255
+ // Evict the boundary set so re-importing each .tsx
1256
+ // component actually picks up the new transitive
1257
+ // dependency code; without this V8 returns the
1258
+ // cached boundary module unchanged.
1259
+ const boundaryIds = Array.from(boundaries);
1260
+ const solidEvictUrls = buildEvictionUrls(boundaryIds);
1261
+ const solidEvicted = invalidateModulesByUrls(solidEvictUrls);
1262
+ if (VERBOSE)
1263
+ console.log(`[hmr][solid] eviction count=${solidEvictUrls.length} ok=${solidEvicted}`);
927
1264
  for (const id of boundaries) {
928
1265
  if (seen.has(id))
929
1266
  continue;
@@ -935,22 +1272,28 @@ async function processQueue() {
935
1272
  if (VERBOSE)
936
1273
  console.log('[hmr][solid] propagated to boundary', { id, url });
937
1274
  const mod = await import(/* @vite-ignore */ url);
938
- // Patch TanStack Router route loaders
1275
+ // Patch TanStack Router route options for any module
1276
+ // that exports a `Route`. We patch BOTH the component
1277
+ // and the loader (when present); components-only routes
1278
+ // were previously skipped because the gate required a
1279
+ // loader, which left their `options.component` pointing
1280
+ // at the stale module's exports after HMR.
939
1281
  try {
940
1282
  const newRoute = mod?.Route;
941
- if (newRoute?.options?.loader) {
1283
+ if (newRoute?.options) {
942
1284
  const router = findRouter();
943
1285
  const fullPath = boundaryToFullPath(id);
944
1286
  if (VERBOSE)
945
- console.log('[hmr][solid][diag] route patch attempt', { id, fullPath, hasRouter: !!router, routesByIdKeys: router?.routesById ? Object.keys(router.routesById) : 'none' });
1287
+ console.log('[hmr][solid][diag] route patch attempt', { id, fullPath, hasRouter: !!router, hasLoader: !!newRoute.options.loader, hasComponent: !!newRoute.options.component });
946
1288
  const existingRoute = fullPath && router ? findRouteByFullPath(router, fullPath) : null;
947
1289
  if (existingRoute?.options) {
948
- existingRoute.options.loader = newRoute.options.loader;
1290
+ if (newRoute.options.loader)
1291
+ existingRoute.options.loader = newRoute.options.loader;
949
1292
  if (newRoute.options.component)
950
1293
  existingRoute.options.component = newRoute.options.component;
951
1294
  routesPatchCount++;
952
1295
  if (VERBOSE)
953
- console.log('[hmr][solid] patched route loader', existingRoute.id, 'fullPath=', fullPath);
1296
+ console.log('[hmr][solid] patched route', existingRoute.id, 'fullPath=', fullPath);
954
1297
  }
955
1298
  else if (VERBOSE) {
956
1299
  console.log('[hmr][solid] no matching route for fullPath', fullPath);
@@ -986,6 +1329,44 @@ async function processQueue() {
986
1329
  if (VERBOSE)
987
1330
  console.warn('[hmr][solid] propagation failed', e);
988
1331
  }
1332
+ // Notify any framework integrations (e.g.
1333
+ // `@nativescript/tanstack-router`) that a Solid HMR
1334
+ // cycle has completed. They use this signal to perform
1335
+ // framework-specific UI refresh (e.g. remount the active
1336
+ // router page) when solid-refresh's own reactive
1337
+ // propagation does not reach the visible tree under
1338
+ // the current renderer/context configuration.
1339
+ //
1340
+ // Boundaries include both the directly-changed tsx files
1341
+ // AND every tsx ancestor reachable via the reverse import
1342
+ // graph (route files in particular). The framework
1343
+ // listener uses the route-file boundaries to look up the
1344
+ // freshly-patched `route.options.component` and pass it
1345
+ // through to the page remount.
1346
+ try {
1347
+ const tsxChangedInDrained = drained.filter((id) => /\.(tsx|jsx)$/i.test(id));
1348
+ const allBoundaries = Array.from(new Set([...tsxChangedInDrained, ...boundaries]));
1349
+ nsSolidHmrEmit({
1350
+ kind: 'solid',
1351
+ changedFiles: drained.slice(),
1352
+ boundaries: allBoundaries,
1353
+ });
1354
+ }
1355
+ catch (err) {
1356
+ if (VERBOSE)
1357
+ console.warn('[hmr][solid] emit failed', err);
1358
+ }
1359
+ // Tell the overlay the cycle is done. solid-refresh's
1360
+ // inline patchRegistry has already flushed the new
1361
+ // component bodies into the live tree (the `case
1362
+ // 'solid'` block above re-imports each .tsx
1363
+ // boundary), so by the time we get here the user is
1364
+ // already looking at the new render. The 'complete'
1365
+ // frame surfaces the wall-clock total and triggers
1366
+ // the overlay's auto-hide.
1367
+ setUpdateOverlayStage('complete', {
1368
+ detail: `Total ${Math.max(0, Date.now() - tQueueStart)}ms`,
1369
+ });
989
1370
  break;
990
1371
  }
991
1372
  case 'typescript': {
@@ -993,7 +1374,7 @@ async function processQueue() {
993
1374
  // This preserves the shell (Frame, ActionBar, etc.) that the app's
994
1375
  // own bootstrapping wires up via `Application.run`.
995
1376
  try {
996
- const g = globalThis;
1377
+ const g = getGlobalScope();
997
1378
  const App = getCore('Application') || g.Application;
998
1379
  if (!App || typeof App.resetRootView !== 'function') {
999
1380
  if (VERBOSE)
@@ -1019,10 +1400,9 @@ async function processQueue() {
1019
1400
  const rawContent = await resp.text();
1020
1401
  // Register under all nickname variants the module registry uses.
1021
1402
  // The bundler context registers XML as e.g., './main-page.xml' and 'main-page.xml'
1022
- const appVirtual = APP_VIRTUAL_WITH_SLASH.replace(/^\//, '');
1023
- let relPath = spec.startsWith('/') ? spec.slice(1) : spec;
1024
- if (relPath.startsWith(appVirtual))
1025
- relPath = relPath.slice(appVirtual.length);
1403
+ const relPath = toAppRelativePath(id);
1404
+ if (!relPath)
1405
+ continue;
1026
1406
  const nicknames = ['./' + relPath, relPath];
1027
1407
  // Also add without extension for CSS
1028
1408
  const extIdx = relPath.lastIndexOf('.');
@@ -1048,21 +1428,24 @@ async function processQueue() {
1048
1428
  }
1049
1429
  }
1050
1430
  }
1431
+ // Modal-aware refresh: pages currently PRESENTED AS MODALS must be
1432
+ // closed + re-presented in place — navigating the top frame to a
1433
+ // modal's page would push it as a frame page, and resetRootView
1434
+ // would dismiss the modal entirely. State comes from core's live
1435
+ // modal stack (_moduleName/_modalOptions/_modalParent); the
1436
+ // showModal wrap only backfills options on older cores.
1437
+ ensureModalTracking();
1438
+ const openModals = getOpenStringModuleModals();
1439
+ const changedModuleNames = new Set(drained.map(toAppModuleName).filter(Boolean));
1440
+ const modalsToReshow = openModals.filter((record) => changedModuleNames.has(record.moduleName));
1441
+ const reshowModuleNames = new Set(modalsToReshow.map((record) => record.moduleName));
1051
1442
  // Determine if we can navigate in-place to a changed page
1052
1443
  // instead of resetting all the way back to app-root.
1053
1444
  // This keeps the user on the page they're editing for faster iteration.
1054
1445
  const changedXmlPages = drained
1055
1446
  .filter((id) => /\.xml$/i.test(id))
1056
- .map((id) => {
1057
- const spec = normalizeSpec(id);
1058
- const appVirtual = APP_VIRTUAL_WITH_SLASH.replace(/^\//, '');
1059
- let relPath = spec.startsWith('/') ? spec.slice(1) : spec;
1060
- if (relPath.startsWith(appVirtual))
1061
- relPath = relPath.slice(appVirtual.length);
1062
- // Strip .xml extension to get the moduleName (e.g., 'pages/status-bar')
1063
- return relPath.replace(/\.xml$/i, '');
1064
- })
1065
- .filter((m) => m && m !== 'app-root');
1447
+ .map((id) => toAppModuleName(id))
1448
+ .filter((m) => m && m !== 'app-root' && !reshowModuleNames.has(m));
1066
1449
  // Resolve the topmost Frame from the bundled realm.
1067
1450
  // Frame.topmost() relies on an internal frameStack array, so we must
1068
1451
  // call it on the bundled-realm class. Multiple strategies to find it:
@@ -1098,7 +1481,7 @@ async function processQueue() {
1098
1481
  catch { }
1099
1482
  }
1100
1483
  if (VERBOSE)
1101
- console.log('[hmr][queue] TS: changedXmlPages=', changedXmlPages, 'topFrame=', !!topFrame);
1484
+ console.log('[hmr][queue] TS: changedXmlPages=', changedXmlPages, 'topFrame=', !!topFrame, 'modalsToReshow=', modalsToReshow.length);
1102
1485
  if (changedXmlPages.length > 0 && topFrame) {
1103
1486
  // Navigate the current frame to the changed page directly.
1104
1487
  // Use the last changed XML page (most specific).
@@ -1113,15 +1496,32 @@ async function processQueue() {
1113
1496
  App.resetRootView({ moduleName: 'app-root' });
1114
1497
  }
1115
1498
  }
1116
- else {
1499
+ else if (modalsToReshow.length === 0) {
1500
+ // No frame page to refresh and no open modal owns the change —
1501
+ // fall back to a full root reset. (Skipped when an open modal is
1502
+ // being re-presented below: resetRootView would dismiss it.)
1117
1503
  if (VERBOSE)
1118
1504
  console.log('[hmr][queue] TS flavor: resetRootView(app-root) after changes');
1119
1505
  App.resetRootView({ moduleName: 'app-root' });
1120
1506
  }
1507
+ // Re-present any open modals whose XML/code-behind changed. The
1508
+ // modules were already re-registered above (raw XML assets + fresh
1509
+ // code-behind exports), so the re-presented modal rebuilds from
1510
+ // the new content while the page beneath it stays put.
1511
+ for (const record of modalsToReshow) {
1512
+ await reshowOpenModal(record);
1513
+ }
1121
1514
  }
1122
1515
  catch (e) {
1123
1516
  console.warn('[hmr][queue] TS flavor: resetRootView(app-root) failed', e);
1124
1517
  }
1518
+ // Tell the overlay the cycle is done — same as the solid path
1519
+ // above. Without this the applying overlay sticks at
1520
+ // 'received' (5%) forever even though the in-place navigate /
1521
+ // resetRootView already applied the update.
1522
+ setUpdateOverlayStage('complete', {
1523
+ detail: `Total ${Math.max(0, Date.now() - tQueueStart)}ms`,
1524
+ });
1125
1525
  break;
1126
1526
  }
1127
1527
  }
@@ -1129,11 +1529,37 @@ async function processQueue() {
1129
1529
  finally {
1130
1530
  processingQueue = false;
1131
1531
  processingPromise = null;
1532
+ // If a delta arrived mid-cycle (pushed onto changedQueue while
1533
+ // processingQueue was still true, so its processQueue() call early-
1534
+ // returned), re-drain — otherwise that save is stranded until the next
1535
+ // delta. Deferred via setTimeout to avoid synchronous re-entrancy as
1536
+ // this promise settles.
1537
+ if (changedQueue.length) {
1538
+ setTimeout(() => {
1539
+ try {
1540
+ processQueue();
1541
+ }
1542
+ catch { }
1543
+ }, 0);
1544
+ }
1132
1545
  }
1133
1546
  })();
1134
1547
  return processingPromise;
1135
1548
  }
1136
1549
  let hmrSocket = null;
1550
+ // Single reconnect timer. Overlapping close/timeout events used to each schedule
1551
+ // their own `setTimeout(connectHmr, …)`, stacking multiple pending reconnects
1552
+ // that could spawn (and leak) duplicate sockets/listeners. Route all reconnect
1553
+ // scheduling through here so only one is ever pending.
1554
+ let reconnectTimer = null;
1555
+ function scheduleReconnect(delayMs) {
1556
+ if (reconnectTimer)
1557
+ clearTimeout(reconnectTimer);
1558
+ reconnectTimer = setTimeout(() => {
1559
+ reconnectTimer = null;
1560
+ connectHmr();
1561
+ }, delayMs);
1562
+ }
1137
1563
  // Track server-announced batches for each version so we can import in-order client-side
1138
1564
  const txnClientBatches = new Map();
1139
1565
  // Public hook for NativeScript runtime to call from ImportModuleDynamicallyCallback later.
@@ -1161,10 +1587,16 @@ function connectHmr() {
1161
1587
  if (hmrSocket?.readyState === WebSocket.OPEN)
1162
1588
  return;
1163
1589
  if (hmrSocket?.readyState === WebSocket.CONNECTING) {
1164
- if (__NS_ENV_VERBOSE__)
1590
+ if (VERBOSE)
1165
1591
  console.log('[hmr-client] Already connecting to HMR WebSocket, skipping');
1166
1592
  return;
1167
1593
  }
1594
+ // A reconnect fired (or a manual connect raced one) — cancel any other pending
1595
+ // reconnect so we don't end up with overlapping connect attempts.
1596
+ if (reconnectTimer) {
1597
+ clearTimeout(reconnectTimer);
1598
+ reconnectTimer = null;
1599
+ }
1168
1600
  try {
1169
1601
  globalThis.__NS_HMR_CLIENT_SOCKET_READY__ = false;
1170
1602
  }
@@ -1172,7 +1604,7 @@ function connectHmr() {
1172
1604
  const overlayStage = hasOpenedHmrSocket ? 'reconnecting' : 'connecting';
1173
1605
  const baseUrl = getHMRWsUrl() || 'ws://localhost:5173/ns-hmr';
1174
1606
  const buildCandidates = (url) => {
1175
- let candidates = [];
1607
+ const candidates = [];
1176
1608
  try {
1177
1609
  const u = new URL(url);
1178
1610
  const proto = u.protocol === 'wss:' ? ['wss'] : ['ws'];
@@ -1180,7 +1612,7 @@ function connectHmr() {
1180
1612
  // Build ordered host candidates with preference to the active HTTP origin
1181
1613
  const orderedHosts = [];
1182
1614
  try {
1183
- const g = globalThis;
1615
+ const g = getGlobalScope();
1184
1616
  const httpOrigin = g && typeof g.__NS_HTTP_ORIGIN__ === 'string' ? g.__NS_HTTP_ORIGIN__ : undefined;
1185
1617
  if (httpOrigin) {
1186
1618
  try {
@@ -1231,19 +1663,29 @@ function connectHmr() {
1231
1663
  if (idx >= candidates.length) {
1232
1664
  showConnectionOverlayNow('offline', 'Waiting for the Vite websocket to come back.');
1233
1665
  console.warn('[hmr-client] All WS candidates failed:', candidates.join(', '));
1234
- setTimeout(connectHmr, 1500);
1666
+ scheduleReconnect(1500);
1235
1667
  return;
1236
1668
  }
1237
1669
  const url = candidates[idx++];
1238
1670
  const connectionDetail = `${overlayStage === 'reconnecting' ? 'Retrying' : 'Opening'} ${url}`;
1239
- if (connectionOverlayVisible) {
1671
+ if (overlayStage === 'reconnecting' && shownConnectionOverlayStage === 'offline') {
1672
+ // Don't flip the visible 'offline' frame to 'reconnecting' for an
1673
+ // attempt that may fail instantly — defer it so only a genuinely
1674
+ // in-flight attempt surfaces. The deferred show is cancelled by
1675
+ // showConnectionOverlayNow (offline re-show / synchronizing) the
1676
+ // moment this attempt resolves. Keeps 'offline' stable instead of
1677
+ // flickering once per retry. (Checking the PAINTED stage, not the
1678
+ // pending one, so every candidate in the loop keeps deferring.)
1679
+ scheduleConnectionOverlay(overlayStage, connectionDetail, RECONNECTING_OVER_OFFLINE_DELAY_MS);
1680
+ }
1681
+ else if (connectionOverlayVisible) {
1240
1682
  updateConnectionOverlay(overlayStage, connectionDetail);
1241
1683
  }
1242
1684
  else {
1243
1685
  scheduleConnectionOverlay(overlayStage, connectionDetail);
1244
1686
  }
1245
1687
  try {
1246
- if (__NS_ENV_VERBOSE__)
1688
+ if (VERBOSE)
1247
1689
  console.log('[hmr-client] Connecting to HMR WebSocket:', url);
1248
1690
  const sock = new WebSocket(url);
1249
1691
  hmrSocket = sock;
@@ -1273,7 +1715,16 @@ function connectHmr() {
1273
1715
  if (connectionOverlayVisible) {
1274
1716
  showConnectionOverlayNow('synchronizing', 'Connected. Synchronizing the HMR graph.');
1275
1717
  }
1276
- VERBOSE && console.log('[hmr-client] Connected to HMR WebSocket');
1718
+ if (VERBOSE)
1719
+ console.log('[hmr-client] Connected to HMR WebSocket');
1720
+ // Print the active module reload mode once on first
1721
+ // successful connect so the user can correlate HMR latency
1722
+ // with runtime capability without grepping for protocol
1723
+ // details. The banner is verbose-gated.
1724
+ try {
1725
+ emitHmrModeBannerOnce();
1726
+ }
1727
+ catch { }
1277
1728
  };
1278
1729
  sock.onmessage = handleHmrMessage;
1279
1730
  sock.onerror = (error) => {
@@ -1297,7 +1748,7 @@ function connectHmr() {
1297
1748
  console.log('[hmr-client] WebSocket closed (code', ev?.code, '), will reconnect…');
1298
1749
  scheduleConnectionOverlay('reconnecting', 'The websocket closed. Waiting to reconnect.', 700);
1299
1750
  // try to reconnect with full candidate list again
1300
- setTimeout(connectHmr, 1000);
1751
+ scheduleReconnect(1000);
1301
1752
  }
1302
1753
  };
1303
1754
  }
@@ -1333,10 +1784,26 @@ async function handleHmrMessage(ev) {
1333
1784
  catch { }
1334
1785
  }
1335
1786
  if (msg) {
1787
+ // `ns:hmr-pending` is a fire-and-forget UX hint emitted by the
1788
+ // server at the START of handleHotUpdate. We drive the
1789
+ // HMR-applying overlay's 'received' frame here (synchronously),
1790
+ // well before the authoritative payload (`ns:angular-update` /
1791
+ // `ns:css-updates`) lands. Skip running any other handlers —
1792
+ // the pending message has no module payload and intentionally
1793
+ // does not bump the graph version.
1794
+ if (msg.type === 'ns:hmr-pending' && typeof msg.path === 'string') {
1795
+ setHmrPendingOverlay(msg.path);
1796
+ return;
1797
+ }
1798
+ // The per-flavor client strategy is loaded by a dynamic import(); make
1799
+ // sure it has resolved (and `install()` has run) before any handler that
1800
+ // delegates through it. After the first message this is an already-settled
1801
+ // promise (microtask only); for Solid/TypeScript it is `Promise.resolve()`.
1802
+ await CLIENT_STRATEGY_READY;
1336
1803
  if (msg.type === 'ns:hmr-full-graph') {
1337
1804
  // Bump a monotonic nonce so HTTP ESM imports can always be cache-busted per update.
1338
1805
  try {
1339
- const g = globalThis;
1806
+ const g = getGlobalScope();
1340
1807
  g.__NS_HMR_IMPORT_NONCE__ = (typeof g.__NS_HMR_IMPORT_NONCE__ === 'number' ? g.__NS_HMR_IMPORT_NONCE__ : 0) + 1;
1341
1808
  }
1342
1809
  catch { }
@@ -1402,6 +1869,14 @@ async function handleHmrMessage(ev) {
1402
1869
  });
1403
1870
  if (toReimport.length && VERBOSE)
1404
1871
  console.log('[hmr][full-graph] inferred changed modules; re-importing', toReimport);
1872
+ // Evict the inferred changed set before re-importing.
1873
+ // See `processQueue` for the architectural rationale; the
1874
+ // full-graph code path is the resync fallback (server chose
1875
+ // not to send a delta) and shares the same V8 cache pitfall.
1876
+ const fgEvictUrls = buildEvictionUrls(toReimport);
1877
+ const fgEvicted = invalidateModulesByUrls(fgEvictUrls);
1878
+ if (VERBOSE)
1879
+ console.log(`[hmr][full-graph] eviction count=${fgEvictUrls.length} ok=${fgEvicted}`);
1405
1880
  for (const id of toReimport) {
1406
1881
  try {
1407
1882
  const spec = normalizeSpec(id);
@@ -1453,7 +1928,7 @@ async function handleHmrMessage(ev) {
1453
1928
  if (msg.type === 'ns:hmr-delta') {
1454
1929
  // Bump a monotonic nonce so HTTP ESM imports can always be cache-busted per update.
1455
1930
  try {
1456
- const g = globalThis;
1931
+ const g = getGlobalScope();
1457
1932
  g.__NS_HMR_IMPORT_NONCE__ = (typeof g.__NS_HMR_IMPORT_NONCE__ === 'number' ? g.__NS_HMR_IMPORT_NONCE__ : 0) + 1;
1458
1933
  }
1459
1934
  catch { }
@@ -1475,10 +1950,108 @@ async function handleHmrMessage(ev) {
1475
1950
  return;
1476
1951
  }
1477
1952
  else {
1953
+ // Vite custom-event dispatch.
1954
+ //
1955
+ // `server.ws.send('event-name', payload)` from any Vite plugin lands
1956
+ // on the wire as `{ type: 'custom', event: 'event-name', data: payload }`.
1957
+ // On the web, Vite's stock client owns a `customListenersMap` that
1958
+ // fires every `import.meta.hot.on('event-name', cb)` callback. We
1959
+ // don't run Vite's stock client on device — the iOS runtime owns
1960
+ // the listener registry via `__NS_DISPATCH_HOT_EVENT__` (the
1961
+ // counterpart to `import.meta.hot.on` populated by user code +
1962
+ // compiled Angular components). Forwarding `type: 'custom'` here
1963
+ // is the only thing standing between server-emitted events and
1964
+ // the listeners they were meant for.
1965
+ //
1966
+ // `angular:component-update` is the canonical example. Analog's
1967
+ // plugin sends it on `.html` / component-style edits; the
1968
+ // compiled component `.mjs` registered a listener that
1969
+ // dynamic-imports `/@ng/component?c=<id>&t=<ts>` and calls
1970
+ // `ɵɵreplaceMetadata` on the live class — swapping the template
1971
+ // definition AND walking live `LView`s to recreate matching views
1972
+ // in-place. The page stays mounted and only the changed bits
1973
+ // re-render. We MUST `return` after dispatch so the reboot path
1974
+ // (`handleAngularHotUpdateMessage` → `__reboot_ng_modules__`)
1975
+ // never runs for these updates — that's the whole point of the
1976
+ // component-replacement pipeline.
1977
+ //
1978
+ // All other custom events are forwarded but NOT short-circuited
1979
+ // (Vite spec: custom events are additive — they don't replace
1980
+ // any framework-specific handling). The reboot path falls through
1981
+ // for `ns:angular-update` (the legacy/`.ts`-edit broadcast) and
1982
+ // for any framework not yet using the in-place replacement path.
1983
+ if (msg.type === 'custom' && typeof msg.event === 'string') {
1984
+ // Dispatch every Vite "custom" event through the runtime's
1985
+ // `__NS_DISPATCH_HOT_EVENT__` bridge so `import.meta.hot.on(event, cb)`
1986
+ // callbacks fire on the device. Critical contract: this is the
1987
+ // ONLY route by which Analog's `angular:component-update` reaches
1988
+ // the compiled component's `(d) => d.id === id && Component_HmrLoad(...)`
1989
+ // listener — without it, server-side broadcasts log green
1990
+ // (`(client) hmr update`) while the device sees nothing happen.
1991
+ //
1992
+ // Diagnostic policy: log "no dispatcher" loud (boot-time rt-bridge
1993
+ // failure), and listener exceptions loud (compiled HmrLoad
1994
+ // fetch/parse error). Successful dispatches are silent — the
1995
+ // runtime's `[import.meta.hot] dispatch summary` line carries
1996
+ // the per-event match-count diagnostic.
1997
+ try {
1998
+ const dispatch = globalThis.__NS_DISPATCH_HOT_EVENT__;
1999
+ if (typeof dispatch === 'function') {
2000
+ dispatch(msg.event, msg.data);
2001
+ }
2002
+ else {
2003
+ console.warn(`[hmr-client][custom] no __NS_DISPATCH_HOT_EVENT__ available for '${msg.event}'`);
2004
+ }
2005
+ }
2006
+ catch (err) {
2007
+ console.warn('[hmr-client][custom] dispatch threw for', msg.event, err);
2008
+ }
2009
+ if (msg.event === 'angular:component-update') {
2010
+ if (VERBOSE)
2011
+ console.log('[hmr-client][custom] dispatched angular:component-update — skipping reboot path');
2012
+ // Walk the apply-progress overlay through its
2013
+ // remaining stages for the in-place template-swap
2014
+ // path. The full reboot path
2015
+ // (`handleAngularHotUpdateMessage`) drives the
2016
+ // overlay itself ('received' → 'evicting' →
2017
+ // 'reimporting' → 'rebooting' → 'complete'); the
2018
+ // in-place path bypasses that handler entirely
2019
+ // because the work happens inside Angular's
2020
+ // `ɵɵreplaceMetadata` after the runtime forwards the
2021
+ // `angular:component-update` event to the compiled
2022
+ // component's listener. Without this update the
2023
+ // overlay would freeze at 5% ('received') even
2024
+ // though the visual swap completes a few frames
2025
+ // later — exactly the "Preparing update (5%)" stuck
2026
+ // frame we have been chasing.
2027
+ //
2028
+ // We transition straight to 'reimporting' to
2029
+ // communicate that metadata is being fetched (the
2030
+ // runtime listener fires `__ns_import('/@ng/component?c=...&t=...')`),
2031
+ // then schedule 'complete' on the next macrotask so
2032
+ // the auto-hide timer kicks in. The actual
2033
+ // template swap is fire-and-forget from this point;
2034
+ // the user sees the overlay close at the same time
2035
+ // as Angular re-renders the bound text/structure.
2036
+ try {
2037
+ const filePath = typeof msg.data?.id === 'string' ? decodeURIComponent(msg.data.id).split('@')[0] : undefined;
2038
+ const detail = filePath ? `Applying template update to ${filePath}` : 'Applying template update';
2039
+ setUpdateOverlayStage('reimporting', { detail });
2040
+ setTimeout(() => {
2041
+ try {
2042
+ setUpdateOverlayStage('complete', { detail: filePath ? `Updated ${filePath}` : 'Update applied' });
2043
+ }
2044
+ catch { }
2045
+ }, 16);
2046
+ }
2047
+ catch { }
2048
+ return;
2049
+ }
2050
+ }
1478
2051
  if (msg.type === 'ns:angular-update' && typeof msg.version === 'number') {
1479
2052
  setGraphVersion(Number(msg.version || getGraphVersion() || 0));
1480
2053
  }
1481
- if (await handleAngularHotUpdateMessage(msg, { getCore, verbose: VERBOSE })) {
2054
+ if (CLIENT_STRATEGY?.handleHotUpdateMessage && (await CLIENT_STRATEGY.handleHotUpdateMessage(msg, { getCore, verbose: VERBOSE, performResetRoot, getOverlay: getHmrOverlayApi }))) {
1482
2055
  return;
1483
2056
  }
1484
2057
  }
@@ -1512,27 +2085,47 @@ async function handleHmrMessage(ev) {
1512
2085
  return;
1513
2086
  }
1514
2087
  if (msg.type === 'ns:css-updates' && Array.isArray(msg.updates)) {
2088
+ // Drive the HMR-applying overlay past the 'received' (5%) frame
2089
+ // that `ns:hmr-pending` set earlier in the cycle. Without this
2090
+ // the overlay sticks at "Preparing update" forever for CSS-only
2091
+ // edits because `handleCssUpdates` is a leaf — there's no
2092
+ // downstream module-evaluation path that would hit the queue's
2093
+ // 'complete' transition.
2094
+ const cssCount = msg.updates.length;
2095
+ try {
2096
+ setUpdateOverlayStage('reimporting', { detail: buildCssApplyingDetail(cssCount) });
2097
+ }
2098
+ catch { }
1515
2099
  try {
1516
2100
  const origin = msg.origin || getHttpOriginForVite() || deriveHttpOrigin(getHMRWsUrl());
1517
2101
  await handleCssUpdates(msg.updates, origin);
2102
+ try {
2103
+ setUpdateOverlayStage('complete', { detail: buildCssAppliedDetail(cssCount) });
2104
+ }
2105
+ catch { }
1518
2106
  return;
1519
2107
  }
1520
2108
  catch (e) {
1521
2109
  console.warn('[hmr-client] CSS updates handling failed:', e);
2110
+ try {
2111
+ setUpdateOverlayStage('complete', { detail: 'CSS update failed' });
2112
+ }
2113
+ catch { }
1522
2114
  return;
1523
2115
  }
1524
2116
  }
1525
2117
  if (msg.type === 'ns:vue-sfc-registry') {
1526
- handleVueSfcRegistry(msg);
2118
+ CLIENT_STRATEGY?.handleSfcRegistry?.(msg);
1527
2119
  return;
1528
2120
  }
1529
2121
  if (msg.type === 'ns:vue-sfc-registry-update') {
1530
2122
  if (typeof msg.version === 'number')
1531
2123
  setGraphVersion(msg.version);
1532
- const comp = await handleVueSfcRegistryUpdate(msg, getGraphVersion());
1533
- if (comp) {
1534
- await performResetRoot(comp);
1535
- }
2124
+ // `ns:hmr-pending` already set the overlay to 'received' (5%). The Vue
2125
+ // strategy walks 'evicting' → 'reimporting' → 'rebooting' → 'complete'
2126
+ // around the SFC load + reset so the toast always lands on 'complete'
2127
+ // (or a failure detail) and the auto-hide timer can dismiss it.
2128
+ await CLIENT_STRATEGY?.handleSfcRegistryUpdate?.(msg, getGraphVersion(), { getCore, verbose: VERBOSE, performResetRoot, getOverlay: getHmrOverlayApi });
1536
2129
  return;
1537
2130
  }
1538
2131
  }
@@ -1552,9 +2145,9 @@ function normalizeComponent(input, nameHint) {
1552
2145
  }
1553
2146
  // If provided a render function, wrap with defineComponent
1554
2147
  if (typeof input === 'function') {
1555
- ensureVueGlobals();
1556
- const comp = globalThis.defineComponent
1557
- ? globalThis.defineComponent({
2148
+ CLIENT_STRATEGY?.beforeNavigateBuild?.();
2149
+ const comp = getGlobalScope().defineComponent
2150
+ ? getGlobalScope().defineComponent({
1558
2151
  name: nameHint || input.name || 'AnonymousSFC',
1559
2152
  render: input,
1560
2153
  })
@@ -1563,9 +2156,9 @@ function normalizeComponent(input, nameHint) {
1563
2156
  }
1564
2157
  // If object has a render function property
1565
2158
  if (input?.render && typeof input.render === 'function') {
1566
- ensureVueGlobals();
1567
- const comp = globalThis.defineComponent
1568
- ? globalThis.defineComponent({
2159
+ CLIENT_STRATEGY?.beforeNavigateBuild?.();
2160
+ const comp = getGlobalScope().defineComponent
2161
+ ? getGlobalScope().defineComponent({
1569
2162
  name: nameHint || input.name || 'AnonymousSFC',
1570
2163
  render: input.render,
1571
2164
  })
@@ -1621,35 +2214,32 @@ async function performResetRoot(newComponent) {
1621
2214
  if (cachedRoot)
1622
2215
  return cachedRoot;
1623
2216
  try {
1624
- switch (TARGET_FLAVOR) {
1625
- case 'vue':
1626
- cachedRoot = getRootForVue(newComponent, state);
1627
- break;
1628
- case 'typescript': {
1629
- // For TS flavor, treat the component as a factory or direct NS view.
1630
- let root = null;
1631
- try {
1632
- if (typeof newComponent === 'function') {
1633
- root = newComponent();
1634
- }
1635
- else {
1636
- root = newComponent;
1637
- }
2217
+ if (CLIENT_STRATEGY?.createRoot) {
2218
+ cachedRoot = CLIENT_STRATEGY.createRoot(newComponent, state);
2219
+ }
2220
+ else if (TARGET_FLAVOR === 'typescript') {
2221
+ // For TS flavor, treat the component as a factory or direct NS view.
2222
+ let root = null;
2223
+ try {
2224
+ if (typeof newComponent === 'function') {
2225
+ root = newComponent();
1638
2226
  }
1639
- catch (e) {
1640
- console.warn('[hmr-client][ts] root factory invocation failed', e);
2227
+ else {
2228
+ root = newComponent;
1641
2229
  }
1642
- cachedRoot = root || {};
1643
- // Heuristic: if the root "looks" like a Frame, prefer frame semantics
1644
- try {
1645
- const name = String(cachedRoot?.constructor?.name || '').replace(/^_+/, '');
1646
- if (/^Frame(\$\d+)?$/.test(name)) {
1647
- rootKind = 'frame';
1648
- }
2230
+ }
2231
+ catch (e) {
2232
+ console.warn('[hmr-client][ts] root factory invocation failed', e);
2233
+ }
2234
+ cachedRoot = root || {};
2235
+ // Heuristic: if the root "looks" like a Frame, prefer frame semantics
2236
+ try {
2237
+ const name = String(cachedRoot?.constructor?.name || '').replace(/^_+/, '');
2238
+ if (/^Frame(\$\d+)?$/.test(name)) {
2239
+ rootKind = 'frame';
1649
2240
  }
1650
- catch { }
1651
- break;
1652
2241
  }
2242
+ catch { }
1653
2243
  }
1654
2244
  return cachedRoot;
1655
2245
  }
@@ -1667,7 +2257,7 @@ async function performResetRoot(newComponent) {
1667
2257
  return factory;
1668
2258
  }
1669
2259
  // Android readiness before any root changes
1670
- const App = getCore('Application') || globalThis.Application;
2260
+ const App = getCore('Application') || getGlobalScope().Application;
1671
2261
  const isAndroid = !!(App && App.android !== undefined);
1672
2262
  if (isAndroid) {
1673
2263
  const isReady = () => {
@@ -1769,7 +2359,7 @@ async function performResetRoot(newComponent) {
1769
2359
  }
1770
2360
  catch { }
1771
2361
  try {
1772
- const AppAny = getCore('Application') || globalThis.Application;
2362
+ const AppAny = getCore('Application') || getGlobalScope().Application;
1773
2363
  isIOS = !!(AppAny && AppAny.ios !== undefined);
1774
2364
  }
1775
2365
  catch { }
@@ -1779,7 +2369,7 @@ async function performResetRoot(newComponent) {
1779
2369
  // - Otherwise (subsequent HMR updates with an authoritative Frame already in place), re-use the
1780
2370
  // current app Frame and navigate to the new Page. This avoids a brief flash that can occur
1781
2371
  // when swapping the entire root view on Android. The placeholder is never involved here.
1782
- const gAnyForPolicy = globalThis;
2372
+ const gAnyForPolicy = getGlobalScope();
1783
2373
  const placeholderFrame = (() => {
1784
2374
  try {
1785
2375
  return gAnyForPolicy.__NS_DEV_PLACEHOLDER_ROOT_VIEW__ || null;
@@ -1794,7 +2384,14 @@ async function performResetRoot(newComponent) {
1794
2384
  }
1795
2385
  catch { }
1796
2386
  const isAuthoritativeFrame = !!existingAppFrame && existingAppFrame !== placeholderFrame;
1797
- if (!hadPlaceholder && !isFrameRoot && isAuthoritativeFrame && typeof existingAppFrame.navigate === 'function') {
2387
+ // Vue: skip the in-place navigate path. After `app.mount(NSVRoot)` in getRootForVue the
2388
+ // new Page already has a parent (the freshly-constructed NSVRoot), so an attempt to navigate
2389
+ // the existing app Frame to that same Page completes silently without ever rebinding the
2390
+ // page to the Frame — the screen keeps showing the previous render. resetRootView with a
2391
+ // fresh Frame correctly reparents the Page and is the proven path that produces visible
2392
+ // in-place updates for SFC HMR cycles. Non-Vue flavors keep the legacy navigate fast path.
2393
+ const allowNavigateFastPath = CLIENT_STRATEGY?.allowNavigateFastPath ?? true;
2394
+ if (allowNavigateFastPath && !hadPlaceholder && !isFrameRoot && isAuthoritativeFrame && typeof existingAppFrame.navigate === 'function') {
1798
2395
  try {
1799
2396
  const navEntry = {
1800
2397
  create: () => preparedRoot,
@@ -1813,7 +2410,7 @@ async function performResetRoot(newComponent) {
1813
2410
  console.log('[hmr-client] full root replacement via resetRootView (placeholder will be discarded)', { isFrameRoot, isIOS, hadPlaceholder });
1814
2411
  // Fallback or preferred path: resetRootView with a creator that builds a fresh Frame and navigates to the new Page
1815
2412
  try {
1816
- const App2 = getCore('Application') || globalThis.Application;
2413
+ const App2 = getCore('Application') || getGlobalScope().Application;
1817
2414
  if (!App2 || typeof App2.resetRootView !== 'function') {
1818
2415
  console.warn('[hmr-client] Application.resetRootView unavailable');
1819
2416
  return false;
@@ -1832,7 +2429,7 @@ async function performResetRoot(newComponent) {
1832
2429
  if (VERBOSE)
1833
2430
  console.warn('[hmr-client] iOS Application.window is boolean false; attempting to clear cached window');
1834
2431
  try {
1835
- const g = globalThis;
2432
+ const g = getGlobalScope();
1836
2433
  const reg = g.__nsVendorRegistry;
1837
2434
  const req = reg?.get ? g.__nsVendorRequire || g.__nsRequire || g.require : g.__nsRequire || g.require;
1838
2435
  let helpers = null;
@@ -1936,6 +2533,20 @@ export function initHmrClient(opts) {
1936
2533
  }
1937
2534
  g.__NS_HMR_CLIENT_ACTIVE__ = true;
1938
2535
  ensureCoreAliasesOnGlobalThis();
2536
+ // XML flavor: record string-module modals from the moment the client is up
2537
+ // so an already-open modal can be re-presented when its files change.
2538
+ // Installed at init (not first-update time) because the wrap can only
2539
+ // observe showModal calls made AFTER it lands. Retried briefly because
2540
+ // getCore('View') may not resolve until the vendor realm finishes booting.
2541
+ if (TARGET_FLAVOR === 'typescript') {
2542
+ const tryInstallModalTracking = (attempts) => {
2543
+ ensureModalTracking();
2544
+ if (!modalTrackingInstalled && attempts > 0) {
2545
+ setTimeout(() => tryInstallModalTracking(attempts - 1), 250);
2546
+ }
2547
+ };
2548
+ tryInstallModalTracking(40);
2549
+ }
1939
2550
  // Defer WebSocket connection until boot completes to avoid native V8 crashes
1940
2551
  // caused by concurrent WebSocket message handling + HTTP fetch during early startup.
1941
2552
  // The WebSocket is only needed for HMR updates, not the initial boot sequence.
@@ -1957,12 +2568,9 @@ export function initHmrClient(opts) {
1957
2568
  console.log('[hmr-client] deferring WebSocket connection until boot completes');
1958
2569
  setTimeout(waitForBoot, 100);
1959
2570
  }
1960
- // Best-effort: install back wrapper even before first remount; original root may be captured later
1961
- switch (TARGET_FLAVOR) {
1962
- case 'vue':
1963
- ensureBackWrapperInstalled(performResetRoot, getCore);
1964
- break;
1965
- }
2571
+ // Best-effort: install back wrapper even before first remount; original root may be captured later.
2572
+ // Deferred until the dynamically-imported strategy resolves.
2573
+ void CLIENT_STRATEGY_READY.then(() => CLIENT_STRATEGY?.installBackWrapper?.(performResetRoot, getCore));
1966
2574
  }
1967
2575
  export default function startViteHMR(opts) {
1968
2576
  if (VERBOSE)