@nativescript/vite 8.0.0-alpha.65 → 8.0.0-alpha.68
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.
- package/configuration/angular.js +78 -182
- package/configuration/angular.js.map +1 -1
- package/configuration/base.js +16 -140
- package/configuration/base.js.map +1 -1
- package/helpers/app-css-state.d.ts +20 -0
- package/helpers/app-css-state.js.map +1 -1
- package/helpers/flavor.d.ts +13 -0
- package/helpers/flavor.js +17 -0
- package/helpers/flavor.js.map +1 -1
- package/helpers/global-defines.d.ts +16 -4
- package/helpers/global-defines.js +44 -4
- package/helpers/global-defines.js.map +1 -1
- package/helpers/main-entry.js +44 -4
- package/helpers/main-entry.js.map +1 -1
- package/helpers/ns-core-external-urls.d.ts +32 -0
- package/helpers/ns-core-external-urls.js +107 -0
- package/helpers/ns-core-external-urls.js.map +1 -0
- package/helpers/typescript-check.js +2 -2
- package/helpers/typescript-check.js.map +1 -1
- package/helpers/workers.d.ts +0 -8
- package/helpers/workers.js +43 -381
- package/helpers/workers.js.map +1 -1
- package/hmr/client/framework-client-strategy.d.ts +17 -9
- package/hmr/client/framework-client-strategy.js +3 -4
- package/hmr/client/framework-client-strategy.js.map +1 -1
- package/hmr/client/hmr-pending-overlay.js +2 -21
- package/hmr/client/hmr-pending-overlay.js.map +1 -1
- package/hmr/client/index.d.ts +9 -1
- package/hmr/client/index.js +48 -2596
- package/hmr/client/index.js.map +1 -1
- package/hmr/client/overlay-driver.d.ts +10 -0
- package/hmr/client/overlay-driver.js +33 -0
- package/hmr/client/overlay-driver.js.map +1 -0
- package/hmr/client/protocol-dispatch.d.ts +8 -0
- package/hmr/client/protocol-dispatch.js +309 -0
- package/hmr/client/protocol-dispatch.js.map +1 -0
- package/hmr/client/root-reset.d.ts +15 -0
- package/hmr/client/root-reset.js +358 -0
- package/hmr/client/root-reset.js.map +1 -0
- package/hmr/client/strategy-loader.d.ts +24 -0
- package/hmr/client/strategy-loader.js +83 -0
- package/hmr/client/strategy-loader.js.map +1 -0
- package/hmr/client/update-pipeline.d.ts +27 -0
- package/hmr/client/update-pipeline.js +464 -0
- package/hmr/client/update-pipeline.js.map +1 -0
- package/hmr/client/utils.js +3 -2
- package/hmr/client/utils.js.map +1 -1
- package/hmr/client/ws-transport.d.ts +21 -0
- package/hmr/client/ws-transport.js +344 -0
- package/hmr/client/ws-transport.js.map +1 -0
- package/hmr/frameworks/angular/build/angular-linker.js +4 -0
- package/hmr/frameworks/angular/build/angular-linker.js.map +1 -1
- package/hmr/frameworks/angular/build/linker-stats.d.ts +18 -0
- package/hmr/frameworks/angular/build/linker-stats.js +29 -0
- package/hmr/frameworks/angular/build/linker-stats.js.map +1 -0
- package/hmr/frameworks/angular/build/ts-fallback-transform.d.ts +5 -0
- package/hmr/frameworks/angular/build/ts-fallback-transform.js +138 -0
- package/hmr/frameworks/angular/build/ts-fallback-transform.js.map +1 -0
- package/hmr/frameworks/angular/build/worker-url-preserve.d.ts +5 -0
- package/hmr/frameworks/angular/build/worker-url-preserve.js +140 -0
- package/hmr/frameworks/angular/build/worker-url-preserve.js.map +1 -0
- package/hmr/frameworks/angular/client/index.d.ts +3 -0
- package/hmr/frameworks/angular/client/index.js +28 -42
- package/hmr/frameworks/angular/client/index.js.map +1 -1
- package/hmr/frameworks/angular/server/ng-component-route.d.ts +43 -0
- package/hmr/frameworks/angular/server/ng-component-route.js +113 -0
- package/hmr/frameworks/angular/server/ng-component-route.js.map +1 -0
- package/hmr/frameworks/angular/server/strategy.js +15 -11
- package/hmr/frameworks/angular/server/strategy.js.map +1 -1
- package/hmr/frameworks/angular/server/websocket-angular-hot-update.d.ts +12 -0
- package/hmr/frameworks/angular/server/websocket-angular-hot-update.js +39 -0
- package/hmr/frameworks/angular/server/websocket-angular-hot-update.js.map +1 -1
- package/hmr/frameworks/react/client/strategy.d.ts +12 -0
- package/hmr/frameworks/react/client/strategy.js +27 -0
- package/hmr/frameworks/react/client/strategy.js.map +1 -0
- package/hmr/frameworks/solid/client/strategy.d.ts +7 -0
- package/hmr/frameworks/solid/client/strategy.js +391 -0
- package/hmr/frameworks/solid/client/strategy.js.map +1 -0
- package/hmr/frameworks/solid/server/strategy.js +2 -0
- package/hmr/frameworks/solid/server/strategy.js.map +1 -1
- package/hmr/frameworks/typescript/client/strategy.d.ts +7 -0
- package/hmr/frameworks/typescript/client/strategy.js +418 -0
- package/hmr/frameworks/typescript/client/strategy.js.map +1 -0
- package/hmr/frameworks/typescript/server/strategy.js +2 -0
- package/hmr/frameworks/typescript/server/strategy.js.map +1 -1
- package/hmr/frameworks/vue/client/index.d.ts +1 -1
- package/hmr/frameworks/vue/client/index.js +40 -319
- package/hmr/frameworks/vue/client/index.js.map +1 -1
- package/hmr/frameworks/vue/client/navigate-app.d.ts +3 -0
- package/hmr/frameworks/vue/client/navigate-app.js +178 -0
- package/hmr/frameworks/vue/client/navigate-app.js.map +1 -0
- package/hmr/frameworks/vue/client/strategy.d.ts +1 -1
- package/hmr/frameworks/vue/client/strategy.js +9 -14
- package/hmr/frameworks/vue/client/strategy.js.map +1 -1
- package/hmr/frameworks/vue/client/vue-sfc-update-overlay.js +2 -20
- package/hmr/frameworks/vue/client/vue-sfc-update-overlay.js.map +1 -1
- package/hmr/frameworks/vue/server/strategy.js +3 -0
- package/hmr/frameworks/vue/server/strategy.js.map +1 -1
- package/hmr/helpers/ast-normalizer.js +32 -4
- package/hmr/helpers/ast-normalizer.js.map +1 -1
- package/hmr/server/css-connect-sync.d.ts +28 -0
- package/hmr/server/css-connect-sync.js +78 -0
- package/hmr/server/css-connect-sync.js.map +1 -0
- package/hmr/server/deps-bundle.js +13 -2
- package/hmr/server/deps-bundle.js.map +1 -1
- package/hmr/server/framework-strategy.d.ts +30 -0
- package/hmr/server/process-code-for-device.js +24 -3
- package/hmr/server/process-code-for-device.js.map +1 -1
- package/hmr/server/vite-plugin.js +15 -0
- package/hmr/server/vite-plugin.js.map +1 -1
- package/hmr/server/websocket-hmr-pending.d.ts +0 -8
- package/hmr/server/websocket-hmr-pending.js +0 -22
- package/hmr/server/websocket-hmr-pending.js.map +1 -1
- package/hmr/server/websocket-hot-update.d.ts +3 -0
- package/hmr/server/websocket-hot-update.js +63 -36
- package/hmr/server/websocket-hot-update.js.map +1 -1
- package/hmr/server/websocket-module-bindings.js +23 -5
- package/hmr/server/websocket-module-bindings.js.map +1 -1
- package/hmr/server/websocket-module-specifiers.d.ts +0 -5
- package/hmr/server/websocket-module-specifiers.js +28 -29
- package/hmr/server/websocket-module-specifiers.js.map +1 -1
- package/hmr/server/websocket-ns-m-paths.d.ts +18 -0
- package/hmr/server/websocket-ns-m-paths.js +79 -0
- package/hmr/server/websocket-ns-m-paths.js.map +1 -1
- package/hmr/server/websocket-ns-m-request.d.ts +2 -1
- package/hmr/server/websocket-ns-m-request.js +3 -2
- package/hmr/server/websocket-ns-m-request.js.map +1 -1
- package/hmr/server/websocket-ns-m.js +100 -374
- package/hmr/server/websocket-ns-m.js.map +1 -1
- package/hmr/server/websocket-served-module-helpers.d.ts +7 -0
- package/hmr/server/websocket-served-module-helpers.js +93 -69
- package/hmr/server/websocket-served-module-helpers.js.map +1 -1
- package/hmr/server/websocket.d.ts +0 -1
- package/hmr/server/websocket.js +27 -2
- package/hmr/server/websocket.js.map +1 -1
- package/hmr/shared/ns-globals.d.ts +0 -1
- package/hmr/shared/ns-globals.js.map +1 -1
- package/hmr/shared/protocol.d.ts +14 -0
- package/hmr/shared/protocol.js.map +1 -1
- package/hmr/shared/runtime/dev-overlay-ios.d.ts +87 -0
- package/hmr/shared/runtime/dev-overlay-ios.js +521 -0
- package/hmr/shared/runtime/dev-overlay-ios.js.map +1 -0
- package/hmr/shared/runtime/dev-overlay-shared.d.ts +58 -0
- package/hmr/shared/runtime/dev-overlay-shared.js +116 -0
- package/hmr/shared/runtime/dev-overlay-shared.js.map +1 -0
- package/hmr/shared/runtime/dev-overlay-tree.d.ts +59 -0
- package/hmr/shared/runtime/dev-overlay-tree.js +712 -0
- package/hmr/shared/runtime/dev-overlay-tree.js.map +1 -0
- package/hmr/shared/runtime/dev-overlay.d.ts +15 -107
- package/hmr/shared/runtime/dev-overlay.js +19 -1336
- package/hmr/shared/runtime/dev-overlay.js.map +1 -1
- package/hmr/shared/runtime/launch-bridge.d.ts +9 -0
- package/hmr/shared/runtime/launch-bridge.js +98 -0
- package/hmr/shared/runtime/launch-bridge.js.map +1 -0
- package/hmr/shared/runtime/overlay-flag.d.ts +9 -0
- package/hmr/shared/runtime/overlay-flag.js +34 -0
- package/hmr/shared/runtime/overlay-flag.js.map +1 -0
- package/hmr/shared/runtime/root-placeholder.js +3 -2
- package/hmr/shared/runtime/root-placeholder.js.map +1 -1
- package/hmr/shared/runtime/session-bootstrap.js +29 -1
- package/hmr/shared/runtime/session-bootstrap.js.map +1 -1
- package/hmr/shared/runtime/vendor-resolve.d.ts +22 -0
- package/hmr/shared/runtime/vendor-resolve.js +45 -0
- package/hmr/shared/runtime/vendor-resolve.js.map +1 -0
- package/hmr/shared/vendor/manifest-collect.js +363 -3
- package/hmr/shared/vendor/manifest-collect.js.map +1 -1
- package/hmr/shared/vendor/manifest.js +5 -1
- package/hmr/shared/vendor/manifest.js.map +1 -1
- package/hmr/shared/vendor/vendor-esbuild-plugins.d.ts +31 -0
- package/hmr/shared/vendor/vendor-esbuild-plugins.js +140 -1
- package/hmr/shared/vendor/vendor-esbuild-plugins.js.map +1 -1
- package/package.json +1 -3
- package/runtime/core-aliases-early.js +3 -2
- package/runtime/core-aliases-early.js.map +1 -1
- package/helpers/module-runner-patch.d.ts +0 -3
- package/helpers/module-runner-patch.js +0 -63
- package/helpers/module-runner-patch.js.map +0 -1
- package/hmr/server/websocket-css-hot-update.d.ts +0 -33
- package/hmr/server/websocket-css-hot-update.js +0 -65
- package/hmr/server/websocket-css-hot-update.js.map +0 -1
- package/hmr/shared/runtime/http-only-boot.d.ts +0 -2
- package/hmr/shared/runtime/http-only-boot.js +0 -172
- package/hmr/shared/runtime/http-only-boot.js.map +0 -1
- package/hmr/vendor-bootstrap.d.ts +0 -1
- package/hmr/vendor-bootstrap.js +0 -30
- package/hmr/vendor-bootstrap.js.map +0 -1
- package/shims/react-reconciler-constants.d.ts +0 -14
- package/shims/react-reconciler-constants.js +0 -21
- package/shims/react-reconciler-constants.js.map +0 -1
- package/shims/react-reconciler.d.ts +0 -8
- package/shims/react-reconciler.js +0 -15
- package/shims/react-reconciler.js.map +0 -1
package/hmr/client/index.js
CHANGED
|
@@ -1,96 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* HMR client entry for NativeScript devices.
|
|
3
3
|
*
|
|
4
4
|
* Avoid importing from '@nativescript/core' and other framework modules here to prevent creating a second module realm via HTTP ESM.
|
|
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
|
+
*
|
|
8
|
+
* This entry owns boot-time side effects (core aliases, launch-notification
|
|
9
|
+
* bridge, CSS register) and wires the seams together:
|
|
10
|
+
* - `ws-transport.ts` — socket connect/reconnect + connection overlay
|
|
11
|
+
* - `protocol-dispatch.ts` — routes parsed wire messages
|
|
12
|
+
* - `update-pipeline.ts` — graph state + evict-then-reimport queue
|
|
13
|
+
* - `root-reset.ts` — root-view replacement
|
|
14
|
+
* - `strategy-loader.ts` — flavor detection + framework strategy loading
|
|
7
15
|
*/
|
|
8
|
-
import { setHMRWsUrl, getHMRWsUrl,
|
|
9
|
-
import { applyCssText
|
|
10
|
-
import { buildCssApplyingDetail, buildCssAppliedDetail } from './css-update-overlay.js';
|
|
16
|
+
import { setHMRWsUrl, getHMRWsUrl, getCore, ENV_VERBOSE } from './utils.js';
|
|
17
|
+
import { applyCssText } from './css-handler.js';
|
|
11
18
|
import { getGlobalScope } from '../shared/runtime/global-scope.js';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
19
|
+
import { installLaunchNotificationBridge } from '../shared/runtime/launch-bridge.js';
|
|
20
|
+
import { connectHmr, setHmrMessageHandler } from './ws-transport.js';
|
|
21
|
+
import { handleHmrMessage } from './protocol-dispatch.js';
|
|
22
|
+
import { ensureCoreAliasesOnGlobalThis, performResetRoot } from './root-reset.js';
|
|
23
|
+
import { CLIENT_STRATEGY_READY, getClientStrategy } from './strategy-loader.js';
|
|
14
24
|
const VERBOSE = ENV_VERBOSE;
|
|
15
|
-
|
|
25
|
+
// Apply once on module evaluation so that /ns/rt exports resolve to functions
|
|
26
|
+
// before any SFCs are evaluated during HTTP-only dev boot.
|
|
27
|
+
ensureCoreAliasesOnGlobalThis();
|
|
28
|
+
// Late-registration bridge for one-shot iOS launch notifications
|
|
29
|
+
// (see shared/runtime/launch-bridge.ts).
|
|
30
|
+
//
|
|
31
|
+
// Under a webpack (or non-HMR vite) build the app bundle evaluates BEFORE
|
|
32
|
+
// `UIApplicationMain` finishes launching, so
|
|
33
|
+
// `Application.ios.addNotificationObserver(UIApplicationDidFinishLaunchingNotification, cb)`
|
|
34
|
+
// registers in time. Under the HTTP dev session UIKit is fully launched long
|
|
35
|
+
// before the app graph evaluates — those observers register AFTER the
|
|
36
|
+
// notification fired and are never called, so apps that gate SDK init (auth,
|
|
37
|
+
// Firebase, Sentry) behind didFinishLaunching silently never initialize. The
|
|
38
|
+
// bridge wraps `addNotificationObserver` so a post-launch registration for
|
|
39
|
+
// Did/WillFinishLaunching still registers normally AND has its callback
|
|
40
|
+
// invoked on the next macrotask with a synthesized NSNotification.
|
|
41
|
+
//
|
|
42
|
+
// The DETERMINISTIC install happens in the /__ns_dev__/client bootstrap
|
|
43
|
+
// wrapper, which runs before the app entry. This module-eval attempt and the
|
|
44
|
+
// global hook are belt-and-braces for exotic boot paths — this client
|
|
45
|
+
// evaluates async and can lose the race against the entry graph, so it must
|
|
46
|
+
// never be the only wrap.
|
|
47
|
+
function tryInstallLaunchNotificationBridge(attempts, explicitApplication) {
|
|
48
|
+
let wrappedAny = false;
|
|
16
49
|
try {
|
|
17
|
-
|
|
18
|
-
return __NS_TARGET_FLAVOR__;
|
|
19
|
-
}
|
|
50
|
+
wrappedAny = installLaunchNotificationBridge(explicitApplication, [getCore('Application')]);
|
|
20
51
|
}
|
|
21
52
|
catch { }
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (typeof g.__NS_TARGET_FLAVOR__ === 'string' && g.__NS_TARGET_FLAVOR__) {
|
|
25
|
-
return g.__NS_TARGET_FLAVOR__;
|
|
26
|
-
}
|
|
27
|
-
if (typeof g.__NS_HMR_BROWSER_RUNTIME_TARGET_FLAVOR__ === 'string' && g.__NS_HMR_BROWSER_RUNTIME_TARGET_FLAVOR__) {
|
|
28
|
-
return g.__NS_HMR_BROWSER_RUNTIME_TARGET_FLAVOR__;
|
|
29
|
-
}
|
|
30
|
-
if (typeof g.__reboot_ng_modules__ === 'function') {
|
|
31
|
-
return 'angular';
|
|
32
|
-
}
|
|
33
|
-
if (g.__VUE_HMR_RUNTIME__ || g.__NS_HMR_VUE_SFC_REGISTRY__) {
|
|
34
|
-
return 'vue';
|
|
35
|
-
}
|
|
53
|
+
if (!wrappedAny && attempts > 0) {
|
|
54
|
+
setTimeout(() => tryInstallLaunchNotificationBridge(attempts - 1, explicitApplication), 100);
|
|
36
55
|
}
|
|
37
|
-
catch { }
|
|
38
|
-
return undefined;
|
|
39
56
|
}
|
|
40
|
-
|
|
41
|
-
// React reuses the generic TypeScript HMR path on BOTH server and client: it has
|
|
42
|
-
// no Fast Refresh, so a module edit drives a plain module reload / root reset
|
|
43
|
-
// (the React tree re-renders), exactly like the `typescript` flavor. The server
|
|
44
|
-
// strategy is `{ ...typescriptServerStrategy, flavor: 'react' }`; this mirrors that
|
|
45
|
-
// on the client so the `typescript`-gated update branches also run for React
|
|
46
|
-
// (otherwise a React edit is received but never applied — the overlay sticks).
|
|
47
|
-
const TS_LIKE_FLAVOR = TARGET_FLAVOR === 'typescript' || TARGET_FLAVOR === 'react';
|
|
57
|
+
tryInstallLaunchNotificationBridge(100);
|
|
48
58
|
try {
|
|
49
|
-
|
|
50
|
-
globalThis.__NS_TARGET_FLAVOR__ = TARGET_FLAVOR;
|
|
51
|
-
}
|
|
59
|
+
globalThis.__NS_DEV_INSTALL_LAUNCH_BRIDGE__ = (explicitApplication) => tryInstallLaunchNotificationBridge(100, explicitApplication);
|
|
52
60
|
}
|
|
53
61
|
catch { }
|
|
54
|
-
// Define substitution does NOT reach this file (served raw from node_modules),
|
|
55
|
-
// so prefer the globalThis seed planted by the entry's defines-seed module —
|
|
56
|
-
// the '/src' literal is a last-resort default and is WRONG for 'app/'-rooted
|
|
57
|
-
// projects.
|
|
58
|
-
const APP_ROOT_VIRTUAL = (typeof __NS_APP_ROOT_VIRTUAL__ === 'string' && __NS_APP_ROOT_VIRTUAL__) || (typeof getGlobalScope().__NS_APP_ROOT_VIRTUAL__ === 'string' && getGlobalScope().__NS_APP_ROOT_VIRTUAL__) || '/src';
|
|
59
|
-
const APP_VIRTUAL_WITH_SLASH = APP_ROOT_VIRTUAL.endsWith('/') ? APP_ROOT_VIRTUAL : `${APP_ROOT_VIRTUAL}/`;
|
|
60
|
-
const APP_MAIN_ENTRY_SPEC = `${APP_VIRTUAL_WITH_SLASH}app.ts`;
|
|
61
|
-
// Policy: by default, let the app's own main entry mount initially; HMR client handles updates/remounts only.
|
|
62
|
-
// Flip this to true via global __NS_HMR_ALLOW_INITIAL_MOUNT__ if you need the client to perform the first mount.
|
|
63
|
-
const ALLOW_INITIAL_MOUNT = !!globalThis.__NS_HMR_ALLOW_INITIAL_MOUNT__;
|
|
64
|
-
// Ensure core aliases are present on globalThis early so that /ns/rt exports resolve to functions
|
|
65
|
-
// before any SFCs are evaluated during HTTP-only dev boot.
|
|
66
|
-
function ensureCoreAliasesOnGlobalThis() {
|
|
67
|
-
const g = globalThis;
|
|
68
|
-
try {
|
|
69
|
-
const F = getCore('Frame') || g.Frame;
|
|
70
|
-
const A = getCore('Application') || g.Application;
|
|
71
|
-
const P = getCore('Page') || g.Page;
|
|
72
|
-
// Publish to globalThis if missing
|
|
73
|
-
try {
|
|
74
|
-
if (F && !g.Frame)
|
|
75
|
-
g.Frame = F;
|
|
76
|
-
}
|
|
77
|
-
catch { }
|
|
78
|
-
try {
|
|
79
|
-
if (A && (!g.Application || typeof g.Application.run !== 'function' || typeof g.Application.on !== 'function' || typeof g.Application.resetRootView !== 'function')) {
|
|
80
|
-
g.Application = A;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
catch { }
|
|
84
|
-
try {
|
|
85
|
-
if (P && !g.Page)
|
|
86
|
-
g.Page = P;
|
|
87
|
-
}
|
|
88
|
-
catch { }
|
|
89
|
-
}
|
|
90
|
-
catch { }
|
|
91
|
-
}
|
|
92
|
-
// Apply once on module evaluation
|
|
93
|
-
ensureCoreAliasesOnGlobalThis();
|
|
94
62
|
// Install the device CSS applier. CSS-bearing modules (a Vue SFC `<style>` via
|
|
95
63
|
// `/ns/asm`, an imported `.css` via `/ns/m`) call `__NS_REGISTER_CSS__(tag, css)`,
|
|
96
64
|
// or queue in `__NS_PENDING_CSS__` if they ran first; the drain below covers that.
|
|
@@ -126,2510 +94,8 @@ function installCssRegister() {
|
|
|
126
94
|
catch { }
|
|
127
95
|
}
|
|
128
96
|
installCssRegister();
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return globalThis.__NS_HMR_DEV_OVERLAY__ || null;
|
|
132
|
-
}
|
|
133
|
-
catch { }
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
function setConnectionOverlayStage(stage, detail) {
|
|
137
|
-
try {
|
|
138
|
-
const api = getHmrOverlayApi();
|
|
139
|
-
if (api && typeof api.setConnectionStage === 'function') {
|
|
140
|
-
api.setConnectionStage(stage, { detail });
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
catch { }
|
|
144
|
-
}
|
|
145
|
-
function hideConnectionOverlay() {
|
|
146
|
-
try {
|
|
147
|
-
const api = getHmrOverlayApi();
|
|
148
|
-
if (api && typeof api.hide === 'function') {
|
|
149
|
-
api.hide('healthy');
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
catch { }
|
|
153
|
-
}
|
|
154
|
-
function setUpdateOverlayStage(stage, info) {
|
|
155
|
-
try {
|
|
156
|
-
const api = getHmrOverlayApi();
|
|
157
|
-
if (api && typeof api.setUpdateStage === 'function') {
|
|
158
|
-
api.setUpdateStage(stage, info);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
catch { }
|
|
162
|
-
}
|
|
163
|
-
// Store the listener registry on globalThis (rather than in a module-private
|
|
164
|
-
// closure) because in NativeScript the HMR client module and the user app
|
|
165
|
-
// modules can resolve to different module instances depending on how the
|
|
166
|
-
// dev runtime loads them (HTTP client URL vs. the bundled vendor realm).
|
|
167
|
-
// A module-local Set would not be shared across instances; the global one
|
|
168
|
-
// is.
|
|
169
|
-
function getNsSolidHmrListenerSet() {
|
|
170
|
-
const g = getGlobalScope();
|
|
171
|
-
let set = g.__ns_solid_hmr_listener_set;
|
|
172
|
-
if (!set) {
|
|
173
|
-
set = new Set();
|
|
174
|
-
g.__ns_solid_hmr_listener_set = set;
|
|
175
|
-
}
|
|
176
|
-
return set;
|
|
177
|
-
}
|
|
178
|
-
function nsSolidHmrSubscribe(fn) {
|
|
179
|
-
const listeners = getNsSolidHmrListenerSet();
|
|
180
|
-
listeners.add(fn);
|
|
181
|
-
if (VERBOSE)
|
|
182
|
-
console.log('[hmr][solid] subscribe — listeners=', listeners.size);
|
|
183
|
-
return () => listeners.delete(fn);
|
|
184
|
-
}
|
|
185
|
-
function nsSolidHmrEmit(ev) {
|
|
186
|
-
const listeners = getNsSolidHmrListenerSet();
|
|
187
|
-
if (VERBOSE)
|
|
188
|
-
console.log('[hmr][solid] emit listeners=', listeners.size, 'changedFiles=', ev.changedFiles);
|
|
189
|
-
for (const fn of Array.from(listeners)) {
|
|
190
|
-
try {
|
|
191
|
-
fn(ev);
|
|
192
|
-
}
|
|
193
|
-
catch (err) {
|
|
194
|
-
if (VERBOSE)
|
|
195
|
-
console.warn('[hmr][solid] listener threw', err);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
try {
|
|
200
|
-
const g = getGlobalScope();
|
|
201
|
-
g.__ns_solid_hmr_subscribe = nsSolidHmrSubscribe;
|
|
202
|
-
// Eagerly create the listener set so the global exists at module load time.
|
|
203
|
-
getNsSolidHmrListenerSet();
|
|
204
|
-
if (VERBOSE)
|
|
205
|
-
console.log('[hmr][solid] HMR client loaded. global set=', typeof g.__ns_solid_hmr_subscribe, 'listenerSet=', typeof g.__ns_solid_hmr_listener_set);
|
|
206
|
-
}
|
|
207
|
-
catch (err) {
|
|
208
|
-
console.warn('[hmr][solid] could not install global __ns_solid_hmr_subscribe', err);
|
|
209
|
-
}
|
|
210
|
-
// Eagerly drive the HMR-applying overlay's 'received' frame as soon
|
|
211
|
-
// as the server emits `ns:hmr-pending`, BEFORE the framework-specific
|
|
212
|
-
// (`ns:angular-update` / `ns:css-updates`) payload arrives. The
|
|
213
|
-
// flavor-specific handler later walks through 'evicting' →
|
|
214
|
-
// 'reimporting' → 'rebooting' → 'complete'. Calling 'received' twice
|
|
215
|
-
// in the same cycle is safe: the overlay preserves
|
|
216
|
-
// `updateCycleStartedAt` when a 'received' frame replaces an existing
|
|
217
|
-
// 'received' frame so the minimum-visible window is still timed
|
|
218
|
-
// against the FIRST frame.
|
|
219
|
-
//
|
|
220
|
-
// Soft-fails when the overlay isn't installed (production builds,
|
|
221
|
-
// vitest, etc.) or when the user opted out via
|
|
222
|
-
// `__NS_HMR_PROGRESS_OVERLAY_ENABLED__ === false`.
|
|
223
|
-
import { applyHmrPendingFrame } from './hmr-pending-overlay.js';
|
|
224
|
-
function setHmrPendingOverlay(filePath) {
|
|
225
|
-
applyHmrPendingFrame(filePath, { getOverlay: getHmrOverlayApi });
|
|
226
|
-
}
|
|
227
|
-
let connectionOverlayTimer = null;
|
|
228
|
-
let connectionOverlayVisible = false;
|
|
229
|
-
let hasOpenedHmrSocket = false;
|
|
230
|
-
let awaitingHealthyHmrMessage = false;
|
|
231
|
-
let pendingConnectionOverlayStage = 'connecting';
|
|
232
|
-
let pendingConnectionOverlayDetail = '';
|
|
233
|
-
// The stage currently PAINTED on screen (set only when we actually show
|
|
234
|
-
// the overlay, not when we merely schedule it). Used to suppress the
|
|
235
|
-
// sub-perceptible offline↔reconnecting flicker during the reconnect
|
|
236
|
-
// retry loop — see the deferral in `connectHmr`/`tryNext`.
|
|
237
|
-
let shownConnectionOverlayStage = null;
|
|
238
|
-
// While the terminal 'offline' frame is showing, a fresh reconnect
|
|
239
|
-
// attempt that is refused near-instantly (the norm when the dev server
|
|
240
|
-
// is down — especially on the Android emulator, where connection-refused
|
|
241
|
-
// returns in ~1ms) would flip the overlay to 'reconnecting' and straight
|
|
242
|
-
// back to 'offline', producing a jarring 1-frame flicker. We defer the
|
|
243
|
-
// 'reconnecting' frame by this much so an instant failure (→ back to
|
|
244
|
-
// 'offline') or a success (→ 'synchronizing') cancels it before it ever
|
|
245
|
-
// paints; only a genuinely in-flight attempt surfaces 'reconnecting'.
|
|
246
|
-
// On iOS real connect attempts exceed this threshold, so the behaviour
|
|
247
|
-
// there is unchanged.
|
|
248
|
-
const RECONNECTING_OVER_OFFLINE_DELAY_MS = 500;
|
|
249
|
-
function clearConnectionOverlayTimer() {
|
|
250
|
-
if (connectionOverlayTimer) {
|
|
251
|
-
clearTimeout(connectionOverlayTimer);
|
|
252
|
-
connectionOverlayTimer = null;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
function showConnectionOverlayNow(stage, detail) {
|
|
256
|
-
// Painting a stage now supersedes any scheduled (deferred) stage, so
|
|
257
|
-
// cancel the pending timer — otherwise a deferred 'reconnecting' could
|
|
258
|
-
// fire moments after we settle back on 'offline' and revive the flicker.
|
|
259
|
-
clearConnectionOverlayTimer();
|
|
260
|
-
pendingConnectionOverlayStage = stage;
|
|
261
|
-
pendingConnectionOverlayDetail = detail || '';
|
|
262
|
-
connectionOverlayVisible = true;
|
|
263
|
-
shownConnectionOverlayStage = stage;
|
|
264
|
-
setConnectionOverlayStage(stage, detail);
|
|
265
|
-
}
|
|
266
|
-
function scheduleConnectionOverlay(stage, detail, delayMs = 1200) {
|
|
267
|
-
pendingConnectionOverlayStage = stage;
|
|
268
|
-
pendingConnectionOverlayDetail = detail || '';
|
|
269
|
-
clearConnectionOverlayTimer();
|
|
270
|
-
connectionOverlayTimer = setTimeout(() => {
|
|
271
|
-
showConnectionOverlayNow(pendingConnectionOverlayStage, pendingConnectionOverlayDetail);
|
|
272
|
-
}, delayMs);
|
|
273
|
-
}
|
|
274
|
-
function updateConnectionOverlay(stage, detail) {
|
|
275
|
-
pendingConnectionOverlayStage = stage;
|
|
276
|
-
pendingConnectionOverlayDetail = detail || '';
|
|
277
|
-
if (connectionOverlayVisible) {
|
|
278
|
-
showConnectionOverlayNow(stage, detail);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
function markHmrConnectionHealthy() {
|
|
282
|
-
awaitingHealthyHmrMessage = false;
|
|
283
|
-
clearConnectionOverlayTimer();
|
|
284
|
-
shownConnectionOverlayStage = null;
|
|
285
|
-
if (connectionOverlayVisible) {
|
|
286
|
-
connectionOverlayVisible = false;
|
|
287
|
-
hideConnectionOverlay();
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
let CLIENT_STRATEGY;
|
|
291
|
-
const CLIENT_STRATEGY_READY = TARGET_FLAVOR === 'vue' || TARGET_FLAVOR === 'angular'
|
|
292
|
-
? import(`../frameworks/${TARGET_FLAVOR}/client/strategy.js`)
|
|
293
|
-
.then((mod) => {
|
|
294
|
-
CLIENT_STRATEGY = mod && mod[`${TARGET_FLAVOR}ClientStrategy`];
|
|
295
|
-
if (VERBOSE)
|
|
296
|
-
console.log('[hmr-client] client strategy loaded for flavor:', TARGET_FLAVOR);
|
|
297
|
-
CLIENT_STRATEGY?.install();
|
|
298
|
-
})
|
|
299
|
-
.catch((err) => {
|
|
300
|
-
console.warn('[hmr-client] failed to load client strategy for', TARGET_FLAVOR, err);
|
|
301
|
-
})
|
|
302
|
-
: Promise.resolve();
|
|
303
|
-
// Track whether we've mounted an initial app root yet in HTTP-only boot
|
|
304
|
-
let initialMounted = !!globalThis.__NS_HMR_BOOT_COMPLETE__;
|
|
305
|
-
// Prevent duplicate initial-mount scheduling across rapid full-graph broadcasts and re-evaluations
|
|
306
|
-
let initialMounting = !!globalThis.__NS_HMR_INITIAL_MOUNT_IN_PROGRESS__;
|
|
307
|
-
// Track whether the first full-graph has been received. Before the full-graph,
|
|
308
|
-
// delta messages are just the server discovering modules during initial boot —
|
|
309
|
-
// NOT actual code changes. Re-imports must be gated behind this flag.
|
|
310
|
-
let hasReceivedFullGraph = false;
|
|
311
|
-
// TypeScript flavor: track registry modules and inferred main id
|
|
312
|
-
let tsModuleSet = null;
|
|
313
|
-
let tsMainId = null;
|
|
314
|
-
const changedQueue = [];
|
|
315
|
-
let processingQueue = false;
|
|
316
|
-
let processingPromise = null;
|
|
317
|
-
// Detect whether the early placeholder root is still active on screen
|
|
318
|
-
function isPlaceholderActive() {
|
|
319
|
-
try {
|
|
320
|
-
const g = getGlobalScope();
|
|
321
|
-
if (g.__NS_DEV_PLACEHOLDER_ROOT_VIEW__)
|
|
322
|
-
return true;
|
|
323
|
-
if (g.__NS_DEV_PLACEHOLDER_ROOT_EARLY__)
|
|
324
|
-
return true;
|
|
325
|
-
}
|
|
326
|
-
catch { }
|
|
327
|
-
return false;
|
|
328
|
-
}
|
|
329
|
-
// Navigation queue removed: we navigate immediately or throw with a clear error
|
|
330
|
-
// Robust navigation fallback: create a sub-app and mount it on a new Page, then navigate there.
|
|
331
|
-
// __nsNavigateWithSubApp removed: no navigation fallbacks are used
|
|
332
|
-
function applyFullGraph(payload) {
|
|
333
|
-
if (typeof payload.version !== 'number' || !Array.isArray(payload.modules))
|
|
334
|
-
return;
|
|
335
|
-
graph.clear();
|
|
336
|
-
payload.modules.forEach((m) => {
|
|
337
|
-
if (m && m.id)
|
|
338
|
-
graph.set(m.id, { id: m.id, deps: m.deps || [], hash: m.hash || '' });
|
|
339
|
-
});
|
|
340
|
-
setGraphVersion(payload.version);
|
|
341
|
-
if (VERBOSE)
|
|
342
|
-
console.log('[hmr][graph] full graph applied version', getGraphVersion(), 'modules=', graph.size);
|
|
343
|
-
// Guarded initial mount rescue: if app hasn't replaced the placeholder shortly after graph arrives,
|
|
344
|
-
// perform a one-time mount. This waits briefly to let the app's main entry start() run first to avoid double mounts.
|
|
345
|
-
// TypeScript flavor: skip the rescue entirely. The HTTP boot will load main.ts which
|
|
346
|
-
// calls Application.run() (patched to resetRootView) — the rescue is redundant and
|
|
347
|
-
// causes a double-mount race (rescue fires at 450ms, then main.ts fires ~1s later,
|
|
348
|
-
// causing a visual flash and leaving the app in an inconsistent state).
|
|
349
|
-
try {
|
|
350
|
-
const g = getGlobalScope();
|
|
351
|
-
const bootDone = !!g.__NS_HMR_BOOT_COMPLETE__;
|
|
352
|
-
if (!bootDone && !initialMounted && !initialMounting && !g.__NS_HMR_RESCUE_SCHEDULED__ && !TS_LIKE_FLAVOR) {
|
|
353
|
-
// simple snapshot helpers
|
|
354
|
-
const getTopmost = () => {
|
|
355
|
-
try {
|
|
356
|
-
const F = getCore('Frame') || g.Frame;
|
|
357
|
-
return F?.topmost?.() || null;
|
|
358
|
-
}
|
|
359
|
-
catch {
|
|
360
|
-
return null;
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
const isPlaceholderView = (v) => {
|
|
364
|
-
try {
|
|
365
|
-
if (!v)
|
|
366
|
-
return false;
|
|
367
|
-
if (v === (g.__NS_DEV_PLACEHOLDER_ROOT_VIEW__ || null))
|
|
368
|
-
return true;
|
|
369
|
-
return !!v.__ns_dev_placeholder;
|
|
370
|
-
}
|
|
371
|
-
catch {
|
|
372
|
-
return false;
|
|
373
|
-
}
|
|
374
|
-
};
|
|
375
|
-
const hasRealRoot = () => {
|
|
376
|
-
try {
|
|
377
|
-
const fr = getTopmost();
|
|
378
|
-
if (!fr)
|
|
379
|
-
return false;
|
|
380
|
-
if (isPlaceholderView(fr))
|
|
381
|
-
return false;
|
|
382
|
-
const pg = fr.currentPage || fr._currentEntry?.resolvedPage;
|
|
383
|
-
if (!pg)
|
|
384
|
-
return false;
|
|
385
|
-
if (isPlaceholderView(pg))
|
|
386
|
-
return false;
|
|
387
|
-
return true;
|
|
388
|
-
}
|
|
389
|
-
catch {
|
|
390
|
-
return false;
|
|
391
|
-
}
|
|
392
|
-
};
|
|
393
|
-
try {
|
|
394
|
-
g.__NS_HMR_RESCUE_SCHEDULED__ = true;
|
|
395
|
-
}
|
|
396
|
-
catch { }
|
|
397
|
-
setTimeout(() => {
|
|
398
|
-
try {
|
|
399
|
-
if (g.__NS_HMR_BOOT_COMPLETE__)
|
|
400
|
-
return; // app already mounted
|
|
401
|
-
if (hasRealRoot()) {
|
|
402
|
-
markDevBootComplete();
|
|
403
|
-
if (VERBOSE)
|
|
404
|
-
console.log('[hmr][init] detected real root after delay; skipping client initial mount');
|
|
405
|
-
// Detach placeholder handler if still present
|
|
406
|
-
try {
|
|
407
|
-
const restore = g.__NS_DEV_RESTORE_PLACEHOLDER__;
|
|
408
|
-
if (typeof restore === 'function')
|
|
409
|
-
restore();
|
|
410
|
-
}
|
|
411
|
-
catch { }
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
// If placeholder still active and no real root, try a one-time gentle initial mount.
|
|
415
|
-
// Vue: prefer SFCs; TypeScript: import the app main TS module and let performResetRoot handle it once.
|
|
416
|
-
const placeholderActive = isPlaceholderActive();
|
|
417
|
-
if (!placeholderActive)
|
|
418
|
-
return;
|
|
419
|
-
if (VERBOSE)
|
|
420
|
-
console.log('[hmr][init] placeholder persists after delay; evaluating rescue policy');
|
|
421
|
-
// Flavor-specific rescue handling
|
|
422
|
-
if (TS_LIKE_FLAVOR) {
|
|
423
|
-
// For TS apps, perform a one-time resetRootView to the conventional
|
|
424
|
-
// app root module. This mimics what Application.run would do and
|
|
425
|
-
// replaces the placeholder with the real UI without trying to
|
|
426
|
-
// treat app.ts as a component.
|
|
427
|
-
try {
|
|
428
|
-
const App = getCore('Application') || g.Application;
|
|
429
|
-
if (App && typeof App.resetRootView === 'function') {
|
|
430
|
-
if (VERBOSE)
|
|
431
|
-
console.log('[hmr][init] TS flavor: performing rescue resetRootView to moduleName=app-root');
|
|
432
|
-
initialMounting = true;
|
|
433
|
-
try {
|
|
434
|
-
g.__NS_HMR_INITIAL_MOUNT_IN_PROGRESS__ = true;
|
|
435
|
-
}
|
|
436
|
-
catch { }
|
|
437
|
-
App.resetRootView({ moduleName: 'app-root' });
|
|
438
|
-
initialMounted = true;
|
|
439
|
-
markDevBootComplete();
|
|
440
|
-
if (VERBOSE)
|
|
441
|
-
console.log('[hmr][init] TS rescue resetRootView complete');
|
|
442
|
-
}
|
|
443
|
-
else if (VERBOSE) {
|
|
444
|
-
console.warn('[hmr][init] TS flavor: Application.resetRootView unavailable; cannot perform rescue');
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
catch (e) {
|
|
448
|
-
console.warn('[hmr][init] TS rescue resetRootView failed', e);
|
|
449
|
-
}
|
|
450
|
-
finally {
|
|
451
|
-
initialMounting = false;
|
|
452
|
-
try {
|
|
453
|
-
g.__NS_HMR_INITIAL_MOUNT_IN_PROGRESS__ = false;
|
|
454
|
-
}
|
|
455
|
-
catch { }
|
|
456
|
-
}
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
const candidate = CLIENT_STRATEGY?.selectMountCandidate?.({ graph, appMainEntrySpec: APP_MAIN_ENTRY_SPEC }) ?? null;
|
|
460
|
-
if (!candidate)
|
|
461
|
-
return;
|
|
462
|
-
initialMounting = true;
|
|
463
|
-
try {
|
|
464
|
-
g.__NS_HMR_INITIAL_MOUNT_IN_PROGRESS__ = true;
|
|
465
|
-
}
|
|
466
|
-
catch { }
|
|
467
|
-
(async () => {
|
|
468
|
-
try {
|
|
469
|
-
let comp = null;
|
|
470
|
-
if (CLIENT_STRATEGY?.loadComponentForMount) {
|
|
471
|
-
comp = await CLIENT_STRATEGY.loadComponentForMount(candidate, 'initial_mount_rescue');
|
|
472
|
-
}
|
|
473
|
-
if (!comp)
|
|
474
|
-
return;
|
|
475
|
-
const ok = await performResetRoot(comp);
|
|
476
|
-
if (ok) {
|
|
477
|
-
initialMounted = true;
|
|
478
|
-
markDevBootComplete();
|
|
479
|
-
if (VERBOSE)
|
|
480
|
-
console.log('[hmr][init] initial mount (rescue) complete');
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
catch (e) {
|
|
484
|
-
console.warn('[hmr][init] initial mount (rescue) error', e);
|
|
485
|
-
}
|
|
486
|
-
finally {
|
|
487
|
-
initialMounting = false;
|
|
488
|
-
try {
|
|
489
|
-
g.__NS_HMR_INITIAL_MOUNT_IN_PROGRESS__ = false;
|
|
490
|
-
}
|
|
491
|
-
catch { }
|
|
492
|
-
}
|
|
493
|
-
})();
|
|
494
|
-
}
|
|
495
|
-
catch { }
|
|
496
|
-
}, 450);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
catch { }
|
|
500
|
-
// On first full graph, if we have not mounted yet, attempt an initial mount
|
|
501
|
-
// by choosing a likely root component for the active flavor and performing a resetRootView with it.
|
|
502
|
-
try {
|
|
503
|
-
// Short-circuit if boot is complete or an initial mount is already underway (across realms/evals)
|
|
504
|
-
const bootDone = !!globalThis.__NS_HMR_BOOT_COMPLETE__;
|
|
505
|
-
const bootInProgress = !!globalThis.__NS_HMR_INITIAL_MOUNT_IN_PROGRESS__ || initialMounting;
|
|
506
|
-
// Only allow initial mount when explicitly enabled. Rely on the app's own main entry start() for the first mount
|
|
507
|
-
// to avoid double-mount races that can cause duplicate navigation logs.
|
|
508
|
-
if (ALLOW_INITIAL_MOUNT && !initialMounted && !bootDone && !bootInProgress && !getCurrentApp() && !getRootFrame()) {
|
|
509
|
-
if (TS_LIKE_FLAVOR) {
|
|
510
|
-
// For TS flavor, do not perform client-driven initial mount; rely on Application.run.
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
const candidate = CLIENT_STRATEGY?.selectMountCandidate?.({ graph, appMainEntrySpec: APP_MAIN_ENTRY_SPEC }) ?? null;
|
|
514
|
-
if (candidate) {
|
|
515
|
-
// Mark initial-mount in progress (both module-local and global) BEFORE scheduling async work
|
|
516
|
-
initialMounting = true;
|
|
517
|
-
try {
|
|
518
|
-
globalThis.__NS_HMR_INITIAL_MOUNT_IN_PROGRESS__ = true;
|
|
519
|
-
}
|
|
520
|
-
catch { }
|
|
521
|
-
(async () => {
|
|
522
|
-
try {
|
|
523
|
-
if (VERBOSE)
|
|
524
|
-
console.log('[hmr][init] mounting initial root from', candidate, 'flavor=', TARGET_FLAVOR);
|
|
525
|
-
// Android-only: avoid racing entry-runtime reset and Activity bring-up
|
|
526
|
-
try {
|
|
527
|
-
const g = getGlobalScope();
|
|
528
|
-
const App = getCore('Application') || g.Application;
|
|
529
|
-
const isAndroid = !!(App && App.android !== undefined);
|
|
530
|
-
if (isAndroid) {
|
|
531
|
-
const ready = async (ms = 8000, step = 60) => {
|
|
532
|
-
const start = Date.now();
|
|
533
|
-
const ok = () => {
|
|
534
|
-
try {
|
|
535
|
-
const a = App.android;
|
|
536
|
-
const act = a && (a.foregroundActivity || a.startActivity);
|
|
537
|
-
const inProg = !!g.__NS_DEV_RESET_IN_PROGRESS__;
|
|
538
|
-
return !!act && !inProg;
|
|
539
|
-
}
|
|
540
|
-
catch {
|
|
541
|
-
return false;
|
|
542
|
-
}
|
|
543
|
-
};
|
|
544
|
-
while (Date.now() - start < ms) {
|
|
545
|
-
if (ok())
|
|
546
|
-
return true;
|
|
547
|
-
await new Promise((r) => setTimeout(r, step));
|
|
548
|
-
}
|
|
549
|
-
return ok();
|
|
550
|
-
};
|
|
551
|
-
const ok = await ready(8000, 60);
|
|
552
|
-
if (!ok && VERBOSE)
|
|
553
|
-
console.warn('[hmr][init] proceeding without explicit boot-ready signal (timeout)');
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
catch { }
|
|
557
|
-
let comp = null;
|
|
558
|
-
if (TS_LIKE_FLAVOR) {
|
|
559
|
-
try {
|
|
560
|
-
const url = await requestModuleFromServer(candidate);
|
|
561
|
-
const mod = await import(/* @vite-ignore */ url);
|
|
562
|
-
comp = mod && (mod.default || mod);
|
|
563
|
-
}
|
|
564
|
-
catch (e) {
|
|
565
|
-
if (VERBOSE)
|
|
566
|
-
console.warn('[hmr][init] TS initial mount failed to import', candidate, e);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
else if (CLIENT_STRATEGY?.loadComponentForMount) {
|
|
570
|
-
comp = await CLIENT_STRATEGY.loadComponentForMount(candidate, 'initial_mount');
|
|
571
|
-
}
|
|
572
|
-
if (comp) {
|
|
573
|
-
const ok = await performResetRoot(comp);
|
|
574
|
-
if (ok) {
|
|
575
|
-
initialMounted = true;
|
|
576
|
-
markDevBootComplete();
|
|
577
|
-
if (VERBOSE)
|
|
578
|
-
console.log('[hmr][init] initial mount complete');
|
|
579
|
-
}
|
|
580
|
-
else if (VERBOSE) {
|
|
581
|
-
console.warn('[hmr][init] initial mount did not verify; will rely on subsequent retries');
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
else if (VERBOSE) {
|
|
585
|
-
console.warn('[hmr][init] failed to load initial component from', candidate);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
catch (e) {
|
|
589
|
-
console.warn('[hmr][init] initial mount error', e);
|
|
590
|
-
}
|
|
591
|
-
finally {
|
|
592
|
-
// Clear in-progress flag regardless of outcome to allow future retries if needed
|
|
593
|
-
initialMounting = false;
|
|
594
|
-
try {
|
|
595
|
-
globalThis.__NS_HMR_INITIAL_MOUNT_IN_PROGRESS__ = false;
|
|
596
|
-
}
|
|
597
|
-
catch { }
|
|
598
|
-
}
|
|
599
|
-
})();
|
|
600
|
-
}
|
|
601
|
-
else if (VERBOSE) {
|
|
602
|
-
console.warn('[hmr][init] no component found in graph to mount initially for flavor', TARGET_FLAVOR);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
catch { }
|
|
607
|
-
}
|
|
608
|
-
function applyDelta(payload) {
|
|
609
|
-
// If versions are out of sync, request a full graph resync, but still
|
|
610
|
-
// opportunistically queue the changed ids so the client can react once
|
|
611
|
-
// the resync arrives. This is especially important for simple TS flavors
|
|
612
|
-
// where we only need a signal that "something changed" to trigger a
|
|
613
|
-
// resetRootView-driven hot update.
|
|
614
|
-
const currentVersion = getGraphVersion();
|
|
615
|
-
const versionMatches = payload.baseVersion === currentVersion;
|
|
616
|
-
if (!versionMatches) {
|
|
617
|
-
if (VERBOSE)
|
|
618
|
-
console.warn('[hmr][graph] version mismatch requesting resync', payload.baseVersion, currentVersion);
|
|
619
|
-
try {
|
|
620
|
-
hmrSocket?.send(JSON.stringify({ type: 'ns:hmr-resync-request' }));
|
|
621
|
-
}
|
|
622
|
-
catch { }
|
|
623
|
-
}
|
|
624
|
-
if (versionMatches) {
|
|
625
|
-
setGraphVersion(payload.newVersion);
|
|
626
|
-
}
|
|
627
|
-
const changed = payload.changed || [];
|
|
628
|
-
CLIENT_STRATEGY?.recordPayloadChanges?.(changed, getGraphVersion());
|
|
629
|
-
(payload.changed || []).forEach((m) => {
|
|
630
|
-
if (!m || !m.id)
|
|
631
|
-
return;
|
|
632
|
-
// Always update the local graph view with the announced module metadata
|
|
633
|
-
graph.set(m.id, { id: m.id, deps: m.deps || [], hash: m.hash || '' });
|
|
634
|
-
});
|
|
635
|
-
(payload.removed || []).forEach((r) => {
|
|
636
|
-
graph.delete(r);
|
|
637
|
-
});
|
|
638
|
-
if (VERBOSE)
|
|
639
|
-
console.log('[hmr][graph] delta applied newVersion', getGraphVersion(), 'changed=', (payload.changed || []).length, 'removed=', (payload.removed || []).length, 'baseVersion=', payload.baseVersion);
|
|
640
|
-
// Queue evaluation of changed modules (placeholder pipeline)
|
|
641
|
-
if (payload.changed?.length) {
|
|
642
|
-
// Gate: Before the first full-graph is received, delta messages are the server
|
|
643
|
-
// discovering modules during initial boot — NOT actual code changes. The entry-runtime
|
|
644
|
-
// already loaded all modules; re-importing them would cause duplicate Application.run(),
|
|
645
|
-
// router initialization, and modal conflicts. Only queue re-imports after the first
|
|
646
|
-
// full-graph confirms the graph is synced and subsequent deltas are real changes.
|
|
647
|
-
if (!hasReceivedFullGraph) {
|
|
648
|
-
if (VERBOSE)
|
|
649
|
-
console.log('[hmr][delta] skipping re-import queue (initial graph build, no full-graph yet)');
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
// HARD SUPPRESS: the very first delta (baseVersion 0) commonly includes the app main entry which is already evaluated during bootstrap.
|
|
653
|
-
// Importing it again with a cache-bust often produces a spurious module-not-found (timestamp param treated as distinct file).
|
|
654
|
-
const isInitial = payload.baseVersion === 0;
|
|
655
|
-
// Filter out virtual helper ids (e.g. '\0plugin-vue:export-helper') which we do not evaluate directly
|
|
656
|
-
const realIds = payload.changed.map((c) => c.id).filter((id) => !/^\0|^\/\0|plugin-vue:export-helper/.test(id));
|
|
657
|
-
// Proactively prefetch core/src deps for any changed .vue SFCs (before registry update triggers reset)
|
|
658
|
-
try {
|
|
659
|
-
for (const entry of payload.changed || []) {
|
|
660
|
-
if (!entry || !/\.vue$/i.test(entry.id))
|
|
661
|
-
continue;
|
|
662
|
-
const deps = Array.isArray(entry.deps) ? entry.deps : [];
|
|
663
|
-
const prefetchTargets = deps.filter((d) => /^\//.test(d) && !/node_modules/.test(d) && !/\.vue$/i.test(d));
|
|
664
|
-
if (prefetchTargets.length && VERBOSE)
|
|
665
|
-
console.log('[hmr][prefetch] SFC deps', entry.id, '->', prefetchTargets);
|
|
666
|
-
for (const dep of prefetchTargets) {
|
|
667
|
-
const norm = normalizeSpec(dep);
|
|
668
|
-
// Skip if we already wrote it (basic heuristic: moduleFetchCache has it or path exists in Documents/_ns_hmr)
|
|
669
|
-
if (moduleFetchCache.has(norm))
|
|
670
|
-
continue;
|
|
671
|
-
// Fire and forget; ensure rejection does not break HMR pipeline
|
|
672
|
-
requestModuleFromServer(norm).catch(() => { });
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
catch (e) {
|
|
677
|
-
if (VERBOSE)
|
|
678
|
-
console.warn('[hmr][prefetch] failed', e);
|
|
679
|
-
}
|
|
680
|
-
const isAppMainEntryId = (value) => value.endsWith(APP_MAIN_ENTRY_SPEC);
|
|
681
|
-
for (const id of realIds) {
|
|
682
|
-
// We now rely on SFC registry update events to trigger root resets for .vue files directly
|
|
683
|
-
if (/\.vue$/i.test(id)) {
|
|
684
|
-
if (VERBOSE)
|
|
685
|
-
console.log('[hmr][delta] skipping queue for .vue id; will handle on registry update', id);
|
|
686
|
-
continue;
|
|
687
|
-
}
|
|
688
|
-
if (isInitial && isAppMainEntryId(id)) {
|
|
689
|
-
if (VERBOSE)
|
|
690
|
-
console.log(`[hmr][delta] suppressing initial ${APP_MAIN_ENTRY_SPEC} evaluation`);
|
|
691
|
-
continue;
|
|
692
|
-
}
|
|
693
|
-
if (isAppMainEntryId(id)) {
|
|
694
|
-
try {
|
|
695
|
-
const exists = getGlobalScope().require?.(id) || globalThis.__nsGetModuleExports?.(id);
|
|
696
|
-
if (!exists && VERBOSE)
|
|
697
|
-
console.log(`[hmr][delta] skipping unresolved ${APP_MAIN_ENTRY_SPEC} change`);
|
|
698
|
-
if (!exists)
|
|
699
|
-
continue;
|
|
700
|
-
}
|
|
701
|
-
catch {
|
|
702
|
-
/* ignore */
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
changedQueue.push(id);
|
|
706
|
-
}
|
|
707
|
-
if (changedQueue.length)
|
|
708
|
-
processQueue();
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
// Deterministic navigation using the current Vue app instance rather than vendor-held rootApp.
|
|
712
|
-
function __nsNavigateUsingApp(comp, opts = {}) {
|
|
713
|
-
const g = getGlobalScope();
|
|
714
|
-
CLIENT_STRATEGY?.beforeNavigateBuild?.();
|
|
715
|
-
const AppFactory = g.createApp;
|
|
716
|
-
const RootCtor = g.NSVRoot;
|
|
717
|
-
if (typeof AppFactory !== 'function' || typeof RootCtor !== 'function') {
|
|
718
|
-
throw new Error('Vue runtime not initialized');
|
|
719
|
-
}
|
|
720
|
-
try {
|
|
721
|
-
const top = (g.Frame && g.Frame.topmost && g.Frame.topmost()) || null;
|
|
722
|
-
const ctor = top && top.constructor && top.constructor.name;
|
|
723
|
-
if (VERBOSE)
|
|
724
|
-
console.log('[app-nav] begin', {
|
|
725
|
-
hmrRealm: g.__NS_HMR_REALM__ || 'unknown',
|
|
726
|
-
rtRealm: g.__NS_RT_REALM__ || 'unknown',
|
|
727
|
-
topCtor: ctor,
|
|
728
|
-
hasTop: !!top,
|
|
729
|
-
});
|
|
730
|
-
}
|
|
731
|
-
catch { }
|
|
732
|
-
// Build a fresh Page each time the factory is invoked to avoid reusing a Page instance
|
|
733
|
-
// across fragment recreations (Android) or multiple frame attachments.
|
|
734
|
-
const buildTarget = () => {
|
|
735
|
-
const existingApp = getCurrentApp();
|
|
736
|
-
const baseProvides = (existingApp && existingApp._context && existingApp._context.provides) || {};
|
|
737
|
-
// Forward `opts.props` as Vue's rootProps so `$navigateTo(Comp, { props: { … } })`
|
|
738
|
-
// reaches the destination component. nativescript-vue's stock `$navigateTo`
|
|
739
|
-
// does the same via `createNativeView(target, options?.props, …)` →
|
|
740
|
-
// `renderer.createApp(component, props)`. Dropping props here would surface
|
|
741
|
-
// at the destination as `[Vue warn]: Missing required prop` and any
|
|
742
|
-
// required-prop component would render with `undefined` bindings.
|
|
743
|
-
const app = AppFactory(normalizeComponent(comp, comp && (comp.__name || comp.name)), opts && opts.props);
|
|
744
|
-
CLIENT_STRATEGY?.onNavAppCreated?.(app);
|
|
745
|
-
try {
|
|
746
|
-
const registry = g.__nsVendorRegistry;
|
|
747
|
-
const req = registry?.get ? g.__nsVendorRequire || g.__nsRequire || g.require : g.__nsRequire || g.require;
|
|
748
|
-
let rh = null;
|
|
749
|
-
if (registry && registry.has('nativescript-vue/dist/runtimeHelpers'))
|
|
750
|
-
rh = registry.get('nativescript-vue/dist/runtimeHelpers');
|
|
751
|
-
if (!rh && typeof req === 'function') {
|
|
752
|
-
try {
|
|
753
|
-
rh = req('nativescript-vue/dist/runtimeHelpers');
|
|
754
|
-
}
|
|
755
|
-
catch { }
|
|
756
|
-
}
|
|
757
|
-
const setRootApp = rh && (rh.setRootApp || rh.default?.setRootApp);
|
|
758
|
-
if (typeof setRootApp === 'function')
|
|
759
|
-
setRootApp(app);
|
|
760
|
-
}
|
|
761
|
-
catch { }
|
|
762
|
-
try {
|
|
763
|
-
const ctx = app?._context;
|
|
764
|
-
if (ctx) {
|
|
765
|
-
const prov = (ctx.provides || (ctx.provides = {}));
|
|
766
|
-
Object.getOwnPropertyNames(baseProvides).forEach((k) => {
|
|
767
|
-
if (!Object.prototype.hasOwnProperty.call(prov, k))
|
|
768
|
-
prov[k] = baseProvides[k];
|
|
769
|
-
});
|
|
770
|
-
Object.getOwnPropertySymbols(baseProvides).forEach((s) => {
|
|
771
|
-
if (!Object.prototype.hasOwnProperty.call(prov, s))
|
|
772
|
-
prov[s] = baseProvides[s];
|
|
773
|
-
});
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
catch { }
|
|
777
|
-
const root = new RootCtor();
|
|
778
|
-
const vm = typeof app.runWithContext === 'function' ? app.runWithContext(() => app.mount(root)) : app.mount(root);
|
|
779
|
-
setCurrentApp(app);
|
|
780
|
-
const el = vm?.$el;
|
|
781
|
-
const nativeView = el?.nativeView;
|
|
782
|
-
if (!nativeView)
|
|
783
|
-
throw new Error('navigation mount did not yield a nativeView');
|
|
784
|
-
const P = getCore('Page');
|
|
785
|
-
const ctorName = String(nativeView?.constructor?.name || '').replace(/^_+/, '');
|
|
786
|
-
if (ctorName === 'Page' || /^Page(\$\d+)?$/.test(ctorName)) {
|
|
787
|
-
return nativeView;
|
|
788
|
-
}
|
|
789
|
-
if (typeof P === 'function') {
|
|
790
|
-
const pg = new P();
|
|
791
|
-
pg.content = nativeView;
|
|
792
|
-
// Hide default ActionBar for wrapped views to avoid double bars
|
|
793
|
-
try {
|
|
794
|
-
pg.actionBarHidden = true;
|
|
795
|
-
}
|
|
796
|
-
catch { }
|
|
797
|
-
return pg;
|
|
798
|
-
}
|
|
799
|
-
return nativeView; // fallback
|
|
800
|
-
};
|
|
801
|
-
let frame = opts && opts.frame ? opts.frame : getRootFrame();
|
|
802
|
-
if (!frame) {
|
|
803
|
-
const F = getCore('Frame');
|
|
804
|
-
frame = F && typeof F.topmost === 'function' ? F.topmost() : null;
|
|
805
|
-
}
|
|
806
|
-
if (!frame) {
|
|
807
|
-
const GApp = getCore('Application') || g.Application;
|
|
808
|
-
const F = getCore('Frame');
|
|
809
|
-
if (typeof GApp?.resetRootView === 'function' && typeof F === 'function') {
|
|
810
|
-
GApp.resetRootView({
|
|
811
|
-
create: () => {
|
|
812
|
-
const fr = new F();
|
|
813
|
-
const navEntry = {
|
|
814
|
-
create: () => buildTarget(),
|
|
815
|
-
clearHistory: true,
|
|
816
|
-
animated: false,
|
|
817
|
-
};
|
|
818
|
-
try {
|
|
819
|
-
fr.navigate(navEntry);
|
|
820
|
-
}
|
|
821
|
-
catch { }
|
|
822
|
-
setRootFrame(fr);
|
|
823
|
-
return fr;
|
|
824
|
-
},
|
|
825
|
-
});
|
|
826
|
-
return undefined;
|
|
827
|
-
}
|
|
828
|
-
throw new Error('Application.resetRootView unavailable');
|
|
829
|
-
}
|
|
830
|
-
const navEntry = { create: () => buildTarget(), ...(opts || {}) };
|
|
831
|
-
frame.navigate(navEntry);
|
|
832
|
-
return undefined;
|
|
833
|
-
}
|
|
834
|
-
// Expose deterministic app navigation globally so /ns/rt can guarantee single-path navigation
|
|
835
|
-
try {
|
|
836
|
-
globalThis.__nsNavigateUsingApp = __nsNavigateUsingApp;
|
|
837
|
-
}
|
|
838
|
-
catch { }
|
|
839
|
-
const openModalRecords = [];
|
|
840
|
-
let modalTrackingInstalled = false;
|
|
841
|
-
/**
|
|
842
|
-
* Map a served/graph module id (e.g. `/app/modal-page.xml`) to its app-root
|
|
843
|
-
* relative path (`modal-page.xml`). Single mapping point — the raw-asset
|
|
844
|
-
* re-registration, page-navigation targets, and modal matching all derive
|
|
845
|
-
* from this; keep them in sync by construction.
|
|
846
|
-
*/
|
|
847
|
-
function toAppRelativePath(id) {
|
|
848
|
-
try {
|
|
849
|
-
const spec = normalizeSpec(id);
|
|
850
|
-
const appVirtual = APP_VIRTUAL_WITH_SLASH.replace(/^\//, '');
|
|
851
|
-
let relPath = spec.startsWith('/') ? spec.slice(1) : spec;
|
|
852
|
-
if (relPath.startsWith(appVirtual))
|
|
853
|
-
relPath = relPath.slice(appVirtual.length);
|
|
854
|
-
return relPath || null;
|
|
855
|
-
}
|
|
856
|
-
catch {
|
|
857
|
-
return null;
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
/** App-root relative module name (no extension) for page-shaped files, else null. */
|
|
861
|
-
function toAppModuleName(id) {
|
|
862
|
-
const relPath = toAppRelativePath(id);
|
|
863
|
-
if (!relPath || !/\.(xml|ts|js)$/i.test(relPath))
|
|
864
|
-
return null;
|
|
865
|
-
return relPath.replace(/\.(xml|ts|js)$/i, '');
|
|
866
|
-
}
|
|
867
|
-
function ensureModalTracking() {
|
|
868
|
-
if (modalTrackingInstalled)
|
|
869
|
-
return;
|
|
870
|
-
try {
|
|
871
|
-
const View = getCore('View') || getGlobalScope().View;
|
|
872
|
-
const proto = View?.prototype;
|
|
873
|
-
if (!proto || typeof proto.showModal !== 'function')
|
|
874
|
-
return;
|
|
875
|
-
const orig = proto.showModal;
|
|
876
|
-
if (orig.__nsHmrModalTracked) {
|
|
877
|
-
modalTrackingInstalled = true;
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
|
-
const wrapped = function (...args) {
|
|
881
|
-
const result = orig.apply(this, args);
|
|
882
|
-
try {
|
|
883
|
-
if (typeof args[0] === 'string' && result) {
|
|
884
|
-
// Mirror core's getModalOptions arg shapes: (moduleName, options)
|
|
885
|
-
// or the deprecated positional form.
|
|
886
|
-
const options = args.length === 2 && args[1] && typeof args[1] === 'object' ? args[1] : { context: args[1], closeCallback: args[2], fullscreen: args[3], animated: args[4], stretched: args[5] };
|
|
887
|
-
const moduleName = String(args[0])
|
|
888
|
-
.replace(/^\.\//, '')
|
|
889
|
-
.replace(/\.(xml|ts|js)$/i, '');
|
|
890
|
-
openModalRecords.push({ moduleName, options, parent: this, modal: result });
|
|
891
|
-
if (VERBOSE)
|
|
892
|
-
console.log('[hmr][modal] tracked open modal', moduleName);
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
catch { }
|
|
896
|
-
return result;
|
|
897
|
-
};
|
|
898
|
-
wrapped.__nsHmrModalTracked = true;
|
|
899
|
-
proto.showModal = wrapped;
|
|
900
|
-
modalTrackingInstalled = true;
|
|
901
|
-
}
|
|
902
|
-
catch (e) {
|
|
903
|
-
if (VERBOSE)
|
|
904
|
-
console.warn('[hmr][modal] tracking install failed', e);
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
/**
|
|
908
|
-
* Enumerate the modals that are currently presented AND were opened by module
|
|
909
|
-
* name, with everything needed to re-present them.
|
|
910
|
-
*
|
|
911
|
-
* Source of truth is core's live modal stack (`_getRootModalViews()`):
|
|
912
|
-
* - `modal._moduleName` — set by the Builder on every createViewFromEntry
|
|
913
|
-
* view (longstanding, used by livesync), so available on any core.
|
|
914
|
-
* - `modal._modalOptions` — the original ShowModalOptions, stored by core's
|
|
915
|
-
* `_showNativeModalView` (newer cores). For older cores the showModal
|
|
916
|
-
* wrap's records (see ensureModalTracking) fill the gap.
|
|
917
|
-
* - `modal._modalParent` — the presenting view.
|
|
918
|
-
* Stale wrap records are pruned against the live stack while we're here.
|
|
919
|
-
*/
|
|
920
|
-
function getOpenStringModuleModals() {
|
|
921
|
-
const out = [];
|
|
922
|
-
try {
|
|
923
|
-
const App = getCore('Application');
|
|
924
|
-
const root = App?.getRootView?.() || App?._rootView;
|
|
925
|
-
const stack = root?._getRootModalViews?.() || [];
|
|
926
|
-
// Prune wrap records whose modal is gone (keeps the fallback list small).
|
|
927
|
-
for (let i = openModalRecords.length - 1; i >= 0; i--) {
|
|
928
|
-
if (!stack.includes(openModalRecords[i].modal)) {
|
|
929
|
-
openModalRecords.splice(i, 1);
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
for (const modal of stack) {
|
|
933
|
-
const record = openModalRecords.find((r) => r.modal === modal);
|
|
934
|
-
const rawModuleName = typeof modal?._moduleName === 'string' && modal._moduleName ? modal._moduleName : record?.moduleName;
|
|
935
|
-
const parent = modal?._modalParent || record?.parent;
|
|
936
|
-
const options = modal?._modalOptions || record?.options;
|
|
937
|
-
if (!rawModuleName || !parent)
|
|
938
|
-
continue;
|
|
939
|
-
const moduleName = String(rawModuleName)
|
|
940
|
-
.replace(/^\.\//, '')
|
|
941
|
-
.replace(/\.(xml|ts|js)$/i, '');
|
|
942
|
-
out.push({ moduleName, options, parent, modal });
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
catch (e) {
|
|
946
|
-
if (VERBOSE)
|
|
947
|
-
console.warn('[hmr][modal] open-modal enumeration failed', e);
|
|
948
|
-
}
|
|
949
|
-
return out;
|
|
950
|
-
}
|
|
951
|
-
/**
|
|
952
|
-
* Close and re-present an open modal so it rebuilds from the freshly
|
|
953
|
-
* re-registered XML/code-behind. Core clears the modal stack synchronously on
|
|
954
|
-
* close but the NATIVE dismissal completes asynchronously; iOS refuses a
|
|
955
|
-
* present while a dismissal is in flight. Newer cores fire `closedModally` on
|
|
956
|
-
* the modal at exactly that completion point — preferred signal. Older cores
|
|
957
|
-
* fall back to polling `isLoaded` (flipped by `_tearDownUI` in the same
|
|
958
|
-
* completion callback).
|
|
959
|
-
*/
|
|
960
|
-
async function reshowOpenModal(record) {
|
|
961
|
-
const { parent, modal, moduleName, options } = record;
|
|
962
|
-
await new Promise((resolve) => {
|
|
963
|
-
let settled = false;
|
|
964
|
-
const finish = () => {
|
|
965
|
-
if (!settled) {
|
|
966
|
-
settled = true;
|
|
967
|
-
resolve();
|
|
968
|
-
}
|
|
969
|
-
};
|
|
970
|
-
let eventArmed = false;
|
|
971
|
-
try {
|
|
972
|
-
if (typeof modal.once === 'function') {
|
|
973
|
-
modal.once('closedModally', finish);
|
|
974
|
-
eventArmed = true;
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
catch { }
|
|
978
|
-
// Poll fallback (also the safety net if the event never fires —
|
|
979
|
-
// e.g. an interactive-dismiss cancellation).
|
|
980
|
-
const deadline = Date.now() + 2000;
|
|
981
|
-
const poll = () => {
|
|
982
|
-
if (settled)
|
|
983
|
-
return;
|
|
984
|
-
let stillLoaded = false;
|
|
985
|
-
try {
|
|
986
|
-
stillLoaded = !!modal.isLoaded;
|
|
987
|
-
}
|
|
988
|
-
catch { }
|
|
989
|
-
if ((!eventArmed && !stillLoaded) || Date.now() > deadline) {
|
|
990
|
-
finish();
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
setTimeout(poll, 50);
|
|
994
|
-
};
|
|
995
|
-
setTimeout(poll, 50);
|
|
996
|
-
try {
|
|
997
|
-
modal.closeModal();
|
|
998
|
-
}
|
|
999
|
-
catch (e) {
|
|
1000
|
-
if (VERBOSE)
|
|
1001
|
-
console.warn('[hmr][modal] close failed for', moduleName, e);
|
|
1002
|
-
finish();
|
|
1003
|
-
}
|
|
1004
|
-
});
|
|
1005
|
-
// One settle beat so the platform finishes releasing the presentation
|
|
1006
|
-
// before the new present begins.
|
|
1007
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1008
|
-
try {
|
|
1009
|
-
parent.showModal(moduleName, { ...(options || {}), animated: false });
|
|
1010
|
-
if (VERBOSE)
|
|
1011
|
-
console.log('[hmr][modal] re-presented', moduleName);
|
|
1012
|
-
}
|
|
1013
|
-
catch (e) {
|
|
1014
|
-
console.warn('[hmr][modal] re-present failed for', moduleName, e);
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
async function processQueue() {
|
|
1018
|
-
if (!globalThis.__NS_HMR_BOOT_COMPLETE__) {
|
|
1019
|
-
if (VERBOSE)
|
|
1020
|
-
console.log('[hmr][gate] deferring HMR eval until boot complete');
|
|
1021
|
-
setTimeout(() => {
|
|
1022
|
-
try {
|
|
1023
|
-
processQueue();
|
|
1024
|
-
}
|
|
1025
|
-
catch { }
|
|
1026
|
-
}, 150);
|
|
1027
|
-
return Promise.resolve();
|
|
1028
|
-
}
|
|
1029
|
-
if (processingQueue)
|
|
1030
|
-
return processingPromise || Promise.resolve();
|
|
1031
|
-
processingQueue = true;
|
|
1032
|
-
processingPromise = (async () => {
|
|
1033
|
-
try {
|
|
1034
|
-
// Simple deterministic drain of the queue. We currently focus on TS flavor
|
|
1035
|
-
// by re-importing changed modules (to refresh their HTTP ESM copies) and
|
|
1036
|
-
// then performing a root reset so the UI reflects the new code.
|
|
1037
|
-
const seen = new Set();
|
|
1038
|
-
const drained = [];
|
|
1039
|
-
while (changedQueue.length) {
|
|
1040
|
-
const id = changedQueue.shift();
|
|
1041
|
-
if (!id || typeof id !== 'string')
|
|
1042
|
-
continue;
|
|
1043
|
-
if (seen.has(id))
|
|
1044
|
-
continue;
|
|
1045
|
-
seen.add(id);
|
|
1046
|
-
drained.push(id);
|
|
1047
|
-
}
|
|
1048
|
-
if (!drained.length)
|
|
1049
|
-
return;
|
|
1050
|
-
if (VERBOSE)
|
|
1051
|
-
console.log('[hmr][queue] processing changed ids', drained);
|
|
1052
|
-
// Track wall-clock so the 'complete' frame can show a meaningful
|
|
1053
|
-
// total. Only the Solid + TypeScript flavors drive the overlay
|
|
1054
|
-
// from here; Angular has its own flow inside
|
|
1055
|
-
// `frameworks/angular/client/index.ts`.
|
|
1056
|
-
const tQueueStart = Date.now();
|
|
1057
|
-
const driveSolidOverlay = TARGET_FLAVOR === 'solid';
|
|
1058
|
-
// Explicit eviction step.
|
|
1059
|
-
//
|
|
1060
|
-
// Without explicit eviction the upcoming `import(url)` would
|
|
1061
|
-
// resolve via V8's `g_moduleRegistry` and return the cached
|
|
1062
|
-
// stale module — making the queue drain a silent no-op for
|
|
1063
|
-
// every save after the first. Eviction also arms the runtime's
|
|
1064
|
-
// bust-next-fetch nonce so the re-fetch defeats OS-level HTTP
|
|
1065
|
-
// caches; the re-import below then uses the STABLE canonical
|
|
1066
|
-
// URL (module identity IS the URL). node_modules and virtual
|
|
1067
|
-
// specs are filtered out by `buildEvictionUrls` so vendor
|
|
1068
|
-
// modules stay hot.
|
|
1069
|
-
if (driveSolidOverlay) {
|
|
1070
|
-
setUpdateOverlayStage('evicting', {
|
|
1071
|
-
detail: drained.length === 1 ? `Invalidating ${drained[0]}` : `Invalidating ${drained.length} modules`,
|
|
1072
|
-
});
|
|
1073
|
-
}
|
|
1074
|
-
const evictUrls = buildEvictionUrls(drained);
|
|
1075
|
-
const evicted = invalidateModulesByUrls(evictUrls);
|
|
1076
|
-
if (VERBOSE)
|
|
1077
|
-
console.log(`[hmr][queue] eviction count=${evictUrls.length} ok=${evicted}`);
|
|
1078
|
-
// Evaluate changed modules best-effort; failures shouldn't completely break HMR.
|
|
1079
|
-
if (driveSolidOverlay) {
|
|
1080
|
-
setUpdateOverlayStage('reimporting', {
|
|
1081
|
-
detail: drained.length === 1 ? `Re-importing ${drained[0]}` : `Re-importing ${drained.length} modules`,
|
|
1082
|
-
});
|
|
1083
|
-
}
|
|
1084
|
-
let reimportFailures = 0;
|
|
1085
|
-
for (const id of drained) {
|
|
1086
|
-
try {
|
|
1087
|
-
const spec = normalizeSpec(id);
|
|
1088
|
-
const url = await requestModuleFromServer(spec);
|
|
1089
|
-
if (!url)
|
|
1090
|
-
continue;
|
|
1091
|
-
if (VERBOSE)
|
|
1092
|
-
console.log('[hmr][queue] re-import', { id, spec, url });
|
|
1093
|
-
const mod = await import(/* @vite-ignore */ url);
|
|
1094
|
-
// TS/XML flavor: refresh the bundler module registry with the fresh
|
|
1095
|
-
// exports so Builder.createViewFromEntry / loadModule('<page>')
|
|
1096
|
-
// resolves the NEW code-behind (tap handlers, page events) instead
|
|
1097
|
-
// of the stale module captured in the boot bundle. Without this,
|
|
1098
|
-
// XML re-renders pick up new markup but keep old behavior.
|
|
1099
|
-
if (TS_LIKE_FLAVOR && mod && /\.(ts|js)$/i.test(id)) {
|
|
1100
|
-
try {
|
|
1101
|
-
const g = getGlobalScope();
|
|
1102
|
-
const moduleName = toAppModuleName(id);
|
|
1103
|
-
if (moduleName && typeof g.registerModule === 'function') {
|
|
1104
|
-
g.registerModule(moduleName, () => mod);
|
|
1105
|
-
g.registerModule('./' + moduleName, () => mod);
|
|
1106
|
-
if (VERBOSE)
|
|
1107
|
-
console.log('[hmr][queue] re-registered code-behind', moduleName);
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
catch (e) {
|
|
1111
|
-
if (VERBOSE)
|
|
1112
|
-
console.warn('[hmr][queue] code-behind re-register failed for', id, e);
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
catch (e) {
|
|
1117
|
-
// Never silent: a failed re-import means the device may keep
|
|
1118
|
-
// running the previous module body, so always surface it (not
|
|
1119
|
-
// verbose-gated) — otherwise the overlay reports success while
|
|
1120
|
-
// the app runs stale code.
|
|
1121
|
-
reimportFailures++;
|
|
1122
|
-
console.warn('[hmr][queue] re-import FAILED for', id, '-', e?.message ?? e);
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
if (reimportFailures > 0) {
|
|
1126
|
-
console.warn(`[hmr][queue] ${reimportFailures}/${drained.length} module(s) failed to re-import; the applied update may be incomplete.`);
|
|
1127
|
-
}
|
|
1128
|
-
// After evaluating the batch, perform flavor-specific UI refresh.
|
|
1129
|
-
switch (TARGET_FLAVOR) {
|
|
1130
|
-
case 'vue':
|
|
1131
|
-
// graph + performResetRoot + getOverlay let the Vue strategy
|
|
1132
|
-
// propagate non-SFC dep changes to the nearest `.vue` boundary
|
|
1133
|
-
// and remount it (see `propagateDepChangeToSfcBoundary`).
|
|
1134
|
-
await CLIENT_STRATEGY?.refreshAfterBatch?.(drained, { setUpdateOverlayStage, startedAt: tQueueStart, graph, performResetRoot, getOverlay: getHmrOverlayApi });
|
|
1135
|
-
break;
|
|
1136
|
-
case 'react': {
|
|
1137
|
-
// React has no Fast Refresh, and (like Solid) mounts a dominative
|
|
1138
|
-
// document root via `startReactApp` rather than an `app-root`
|
|
1139
|
-
// module — so the TypeScript `resetRootView({moduleName:'app-root'})`
|
|
1140
|
-
// path doesn't apply. The changed modules were already re-imported
|
|
1141
|
-
// into the device cache above; the per-page remount is driven by the
|
|
1142
|
-
// app's `__NS_HMR_ON_UPDATE__` hook (see
|
|
1143
|
-
// `@nativescript/tanstack-router/react`'s `subscribeReactHmrRemount`),
|
|
1144
|
-
// which fires right after this queue drains. Here we only mark the
|
|
1145
|
-
// overlay complete so it doesn't stick at 'received' (5%).
|
|
1146
|
-
setUpdateOverlayStage('complete', {
|
|
1147
|
-
detail: `Total ${Math.max(0, Date.now() - tQueueStart)}ms`,
|
|
1148
|
-
});
|
|
1149
|
-
break;
|
|
1150
|
-
}
|
|
1151
|
-
case 'solid': {
|
|
1152
|
-
// Boundaries discovered in this HMR cycle (tsx files reachable
|
|
1153
|
-
// via the reverse import graph from any changed file, plus route
|
|
1154
|
-
// files reachable from any tsx start point). Declared at the top
|
|
1155
|
-
// of the case block so the emit step below can include the
|
|
1156
|
-
// complete set in the listener event — framework integrations
|
|
1157
|
-
// use it to map route boundaries → fresh component references.
|
|
1158
|
-
const boundaries = new Set();
|
|
1159
|
-
// FULL reverse-import chain from every changed file up to the app
|
|
1160
|
-
// entry (all importers, transitively — not just tsx ones). Emitted
|
|
1161
|
-
// as `ev.ancestors` so app-level remount helpers can evict the
|
|
1162
|
-
// whole chain: re-importing only the root would resolve cached
|
|
1163
|
-
// intermediates and hand the fresh root a stale subtree.
|
|
1164
|
-
const ancestors = new Set();
|
|
1165
|
-
// Solid .tsx components are self-accepting via solid-refresh's inline
|
|
1166
|
-
// patchRegistry — re-importing them is sufficient. For non-component
|
|
1167
|
-
// .ts utility modules, we must propagate up the import graph to find
|
|
1168
|
-
// the .tsx/.jsx component boundaries and re-import those so their
|
|
1169
|
-
// solid-refresh proxies pick up the new dependency values.
|
|
1170
|
-
try {
|
|
1171
|
-
// Build reverse index: dep id → list of importer ids
|
|
1172
|
-
const reverseIndex = new Map();
|
|
1173
|
-
for (const [id, mod] of graph) {
|
|
1174
|
-
for (const dep of mod.deps) {
|
|
1175
|
-
let arr = reverseIndex.get(dep);
|
|
1176
|
-
if (!arr) {
|
|
1177
|
-
arr = [];
|
|
1178
|
-
reverseIndex.set(dep, arr);
|
|
1179
|
-
}
|
|
1180
|
-
arr.push(id);
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
// Pass 0: collect the FULL importer chain of every changed file
|
|
1184
|
-
// (BFS up the reverse graph, no filtering). This feeds
|
|
1185
|
-
// `ev.ancestors` for app-level remount helpers.
|
|
1186
|
-
for (const id of drained) {
|
|
1187
|
-
const queue = [id];
|
|
1188
|
-
while (queue.length) {
|
|
1189
|
-
const cur = queue.shift();
|
|
1190
|
-
const importers = reverseIndex.get(cur);
|
|
1191
|
-
if (!importers)
|
|
1192
|
-
continue;
|
|
1193
|
-
for (const imp of importers) {
|
|
1194
|
-
if (ancestors.has(imp))
|
|
1195
|
-
continue;
|
|
1196
|
-
ancestors.add(imp);
|
|
1197
|
-
queue.push(imp);
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
// Pass 1: BFS from each non-tsx changed module up to tsx/jsx
|
|
1202
|
-
// boundaries. These get re-imported below so solid-refresh's
|
|
1203
|
-
// inline patchRegistry runs and (best-effort) swaps the proxy
|
|
1204
|
-
// signals for any components defined in those tsx boundaries.
|
|
1205
|
-
for (const id of drained) {
|
|
1206
|
-
if (/\.(tsx|jsx)$/i.test(id))
|
|
1207
|
-
continue; // already self-accepting
|
|
1208
|
-
const visited = new Set();
|
|
1209
|
-
const queue = [id];
|
|
1210
|
-
while (queue.length) {
|
|
1211
|
-
const cur = queue.shift();
|
|
1212
|
-
if (visited.has(cur))
|
|
1213
|
-
continue;
|
|
1214
|
-
visited.add(cur);
|
|
1215
|
-
const importers = reverseIndex.get(cur);
|
|
1216
|
-
if (!importers)
|
|
1217
|
-
continue;
|
|
1218
|
-
for (const imp of importers) {
|
|
1219
|
-
if (/\.(tsx|jsx)$/i.test(imp)) {
|
|
1220
|
-
boundaries.add(imp);
|
|
1221
|
-
}
|
|
1222
|
-
else {
|
|
1223
|
-
queue.push(imp);
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
// Pass 2: walk further from any tsx starting point (a tsx file
|
|
1229
|
-
// in `drained` OR a tsx boundary discovered in pass 1) to find
|
|
1230
|
-
// route files (`/src/routes/*.{tsx,jsx}`) that transitively
|
|
1231
|
-
// import them. Re-importing a route file refreshes its
|
|
1232
|
-
// `Route.options.component` to the freshly-imported reference
|
|
1233
|
-
// and the existing boundary loop below patches the live router
|
|
1234
|
-
// with that fresh reference.
|
|
1235
|
-
//
|
|
1236
|
-
// This is the key fix for "edit home.tsx → save → no visual
|
|
1237
|
-
// update": the old BFS skipped tsx files in `drained` (assuming
|
|
1238
|
-
// solid-refresh's in-place proxy patch was sufficient), but in
|
|
1239
|
-
// the universal-renderer + nested-context configuration that
|
|
1240
|
-
// patch does not always propagate to the visible page tree.
|
|
1241
|
-
// Adding the route file as a boundary lets us patch
|
|
1242
|
-
// `route.options.component` directly to a fresh module export,
|
|
1243
|
-
// which the framework subscriber then passes through to the
|
|
1244
|
-
// page remount — making the cycle robust to the proxy patch
|
|
1245
|
-
// silently failing.
|
|
1246
|
-
const tsxStarts = new Set();
|
|
1247
|
-
for (const id of drained) {
|
|
1248
|
-
if (/\.(tsx|jsx)$/i.test(id))
|
|
1249
|
-
tsxStarts.add(id);
|
|
1250
|
-
}
|
|
1251
|
-
for (const b of boundaries)
|
|
1252
|
-
tsxStarts.add(b);
|
|
1253
|
-
const ROUTE_FILE_RE = /\/src\/routes\/.+\.(tsx|jsx)$/i;
|
|
1254
|
-
for (const start of tsxStarts) {
|
|
1255
|
-
const visited = new Set();
|
|
1256
|
-
const queue = [start];
|
|
1257
|
-
while (queue.length) {
|
|
1258
|
-
const cur = queue.shift();
|
|
1259
|
-
if (visited.has(cur))
|
|
1260
|
-
continue;
|
|
1261
|
-
visited.add(cur);
|
|
1262
|
-
if (cur !== start && ROUTE_FILE_RE.test(cur)) {
|
|
1263
|
-
boundaries.add(cur);
|
|
1264
|
-
}
|
|
1265
|
-
const importers = reverseIndex.get(cur);
|
|
1266
|
-
if (!importers)
|
|
1267
|
-
continue;
|
|
1268
|
-
for (const imp of importers) {
|
|
1269
|
-
queue.push(imp);
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
// Re-import each boundary so solid-refresh patchRegistry fires.
|
|
1274
|
-
// For route files (TanStack Router), capture the new Route export
|
|
1275
|
-
// and patch the router's existing route with the fresh loader.
|
|
1276
|
-
let routesPatchCount = 0;
|
|
1277
|
-
let discoveredRouter = null;
|
|
1278
|
-
// Discover router: try __ns_router global (set by createNativeScriptRouter),
|
|
1279
|
-
// then scan globalThis for any router-shaped object with routesById.
|
|
1280
|
-
const findRouter = () => {
|
|
1281
|
-
if (discoveredRouter)
|
|
1282
|
-
return discoveredRouter;
|
|
1283
|
-
const g = getGlobalScope();
|
|
1284
|
-
if (g.__ns_router?.routesById)
|
|
1285
|
-
return (discoveredRouter = g.__ns_router);
|
|
1286
|
-
// Fallback: scan common global keys for router
|
|
1287
|
-
for (const key of ['__ns_router', 'router', '__router']) {
|
|
1288
|
-
if (g[key]?.routesById && g[key]?.invalidate)
|
|
1289
|
-
return (discoveredRouter = g[key]);
|
|
1290
|
-
}
|
|
1291
|
-
return null;
|
|
1292
|
-
};
|
|
1293
|
-
// Convert boundary file path to TanStack Router fullPath.
|
|
1294
|
-
// e.g. /src/routes/posts.$postId.tsx → /posts/$postId
|
|
1295
|
-
const boundaryToFullPath = (bid) => {
|
|
1296
|
-
const m = bid.match(/\/src\/routes\/(.+)\.(tsx|jsx|ts|js)$/i);
|
|
1297
|
-
if (!m)
|
|
1298
|
-
return null;
|
|
1299
|
-
let p = m[1];
|
|
1300
|
-
// Replace dots between segments with slashes (posts.$postId → posts/$postId)
|
|
1301
|
-
p = p.replace(/\./g, '/');
|
|
1302
|
-
// Handle index files
|
|
1303
|
-
if (p === 'index')
|
|
1304
|
-
return '/';
|
|
1305
|
-
if (p.endsWith('/index'))
|
|
1306
|
-
p = p.slice(0, -6);
|
|
1307
|
-
// Strip leading - (TanStack pathless layout convention)
|
|
1308
|
-
p = p.replace(/(^|\/)-([\w])/g, '$1$2');
|
|
1309
|
-
return '/' + p;
|
|
1310
|
-
};
|
|
1311
|
-
// Find existing route by fullPath (since new Route has no id yet).
|
|
1312
|
-
// The root route (`__root__`) shares fullPath '/' with the index
|
|
1313
|
-
// route, so prefer the non-root (leaf) match — otherwise editing
|
|
1314
|
-
// `index.tsx` would clobber the root layout's component instead of
|
|
1315
|
-
// the page's. @nativescript/tanstack-router's remount uses the same
|
|
1316
|
-
// preference so patch + read target the same route.
|
|
1317
|
-
const findRouteByFullPath = (router, fp) => {
|
|
1318
|
-
if (!router?.routesById)
|
|
1319
|
-
return null;
|
|
1320
|
-
let rootMatch = null;
|
|
1321
|
-
for (const rid of Object.keys(router.routesById)) {
|
|
1322
|
-
const r = router.routesById[rid];
|
|
1323
|
-
if (r?.fullPath !== fp)
|
|
1324
|
-
continue;
|
|
1325
|
-
if (rid === '__root__') {
|
|
1326
|
-
rootMatch = rootMatch || r;
|
|
1327
|
-
continue;
|
|
1328
|
-
}
|
|
1329
|
-
return r;
|
|
1330
|
-
}
|
|
1331
|
-
return rootMatch;
|
|
1332
|
-
};
|
|
1333
|
-
// A changed route file is itself a boundary to re-import + patch — Pass 2
|
|
1334
|
-
// only adds OTHER route files that import a changed module, never the
|
|
1335
|
-
// changed route file itself. Without this, editing `about.tsx` while on
|
|
1336
|
-
// /about never refreshes the About component.
|
|
1337
|
-
for (const id of drained) {
|
|
1338
|
-
if (ROUTE_FILE_RE.test(id))
|
|
1339
|
-
boundaries.add(id);
|
|
1340
|
-
}
|
|
1341
|
-
// Evict ONLY the boundary set (changed component + route files) — NOT the
|
|
1342
|
-
// router / route-tree / app chain. Keeping those cached preserves the live
|
|
1343
|
-
// router instance and the user's current route across the re-mount; fresh
|
|
1344
|
-
// route components reach the screen via the in-place `route.options.component`
|
|
1345
|
-
// patch below, not a router rebuild (which would reset to the initial route).
|
|
1346
|
-
const boundaryIds = Array.from(boundaries);
|
|
1347
|
-
const solidEvictUrls = buildEvictionUrls(boundaryIds);
|
|
1348
|
-
const solidEvicted = invalidateModulesByUrls(solidEvictUrls);
|
|
1349
|
-
if (VERBOSE)
|
|
1350
|
-
console.log(`[hmr][solid] eviction count=${solidEvictUrls.length} ok=${solidEvicted}`);
|
|
1351
|
-
for (const id of boundaries) {
|
|
1352
|
-
// Skip already-drained modules — EXCEPT route files. The main
|
|
1353
|
-
// drain loop above re-imports drained modules but does NOT patch
|
|
1354
|
-
// `route.options.component`; a changed ROUTE file (which is in
|
|
1355
|
-
// `drained`/`seen`) must still flow through this loop so its route
|
|
1356
|
-
// gets patched with the fresh component, otherwise the router's
|
|
1357
|
-
// per-page HMR remount re-renders the stale (one-edit-behind) one.
|
|
1358
|
-
if (seen.has(id) && !/\/src\/routes\/.+\.(?:tsx|jsx)$/i.test(id))
|
|
1359
|
-
continue;
|
|
1360
|
-
// Skip the root layout (`__root`): it commonly imports a global
|
|
1361
|
-
// stylesheet (`./styles.css?url`) with no ESM `default` export on
|
|
1362
|
-
// device, so re-importing it throws and aborts the cycle. The root
|
|
1363
|
-
// layout rarely needs a component hot-swap anyway.
|
|
1364
|
-
if (/\/routes\/__root\.(?:tsx|jsx|ts|js)$/i.test(id))
|
|
1365
|
-
continue;
|
|
1366
|
-
try {
|
|
1367
|
-
const spec = normalizeSpec(id);
|
|
1368
|
-
// Re-import the canonical URL. The boundary was evicted above
|
|
1369
|
-
// (`invalidateModulesByUrls(solidEvictUrls)`), which also armed
|
|
1370
|
-
// the runtime's bust-next-fetch nonce, so this import re-fetches
|
|
1371
|
-
// the just-saved transform rather than a cached one. This is what
|
|
1372
|
-
// `route.options.component` is patched from below, and what
|
|
1373
|
-
// @nativescript/tanstack-router's per-page HMR remount reads.
|
|
1374
|
-
const url = await requestModuleFromServer(spec);
|
|
1375
|
-
if (!url)
|
|
1376
|
-
continue;
|
|
1377
|
-
if (VERBOSE)
|
|
1378
|
-
console.log('[hmr][solid] propagated to boundary', { id, url });
|
|
1379
|
-
const mod = await import(/* @vite-ignore */ url);
|
|
1380
|
-
// Patch TanStack Router route options for any module
|
|
1381
|
-
// that exports a `Route`. We patch BOTH the component
|
|
1382
|
-
// and the loader (when present); components-only routes
|
|
1383
|
-
// were previously skipped because the gate required a
|
|
1384
|
-
// loader, which left their `options.component` pointing
|
|
1385
|
-
// at the stale module's exports after HMR.
|
|
1386
|
-
try {
|
|
1387
|
-
const newRoute = mod?.Route;
|
|
1388
|
-
if (newRoute?.options) {
|
|
1389
|
-
const router = findRouter();
|
|
1390
|
-
const fullPath = boundaryToFullPath(id);
|
|
1391
|
-
if (VERBOSE)
|
|
1392
|
-
console.log('[hmr][solid][diag] route patch attempt', { id, fullPath, hasRouter: !!router, hasLoader: !!newRoute.options.loader, hasComponent: !!newRoute.options.component });
|
|
1393
|
-
const existingRoute = fullPath && router ? findRouteByFullPath(router, fullPath) : null;
|
|
1394
|
-
if (existingRoute?.options) {
|
|
1395
|
-
if (newRoute.options.loader)
|
|
1396
|
-
existingRoute.options.loader = newRoute.options.loader;
|
|
1397
|
-
if (newRoute.options.component)
|
|
1398
|
-
existingRoute.options.component = newRoute.options.component;
|
|
1399
|
-
routesPatchCount++;
|
|
1400
|
-
if (VERBOSE)
|
|
1401
|
-
console.log('[hmr][solid] patched route', existingRoute.id, 'fullPath=', fullPath);
|
|
1402
|
-
}
|
|
1403
|
-
else if (VERBOSE) {
|
|
1404
|
-
console.log('[hmr][solid] no matching route for fullPath', fullPath);
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
catch (e) {
|
|
1409
|
-
if (VERBOSE)
|
|
1410
|
-
console.warn('[hmr][solid] route patch error', id, e);
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
catch (e) {
|
|
1414
|
-
if (VERBOSE)
|
|
1415
|
-
console.warn('[hmr][solid] boundary re-import failed', id, e);
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
// Route loaders were patched with fresh closures. The data is
|
|
1419
|
-
// correct in router.state.matches[].loaderData (confirmed via
|
|
1420
|
-
// diagnostics), but the currently visible page doesn't re-render
|
|
1421
|
-
// because the NativeScriptRouterProvider's Solid store flush only
|
|
1422
|
-
// fires through history.subscribe. TODO: find the right mechanism
|
|
1423
|
-
// to trigger a Solid reactive update for the active match stores.
|
|
1424
|
-
if (routesPatchCount > 0 && VERBOSE) {
|
|
1425
|
-
console.log('[hmr][solid] patched', routesPatchCount, 'route loaders (data correct in match state, pending UI refresh mechanism)');
|
|
1426
|
-
}
|
|
1427
|
-
if (VERBOSE) {
|
|
1428
|
-
if (boundaries.size)
|
|
1429
|
-
console.log('[hmr][solid] propagated non-component change to', boundaries.size, 'boundaries', Array.from(boundaries));
|
|
1430
|
-
console.log('[hmr][queue] Solid: modules re-imported; UI refresh is driven by the framework subscriber (router per-page remount or startSolidApp re-render)', drained);
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
catch (e) {
|
|
1434
|
-
if (VERBOSE)
|
|
1435
|
-
console.warn('[hmr][solid] propagation failed', e);
|
|
1436
|
-
}
|
|
1437
|
-
// Notify any framework integrations (e.g.
|
|
1438
|
-
// `@nativescript/tanstack-router`) that a Solid HMR
|
|
1439
|
-
// cycle has completed. They use this signal to perform
|
|
1440
|
-
// framework-specific UI refresh (e.g. remount the active
|
|
1441
|
-
// router page) when solid-refresh's own reactive
|
|
1442
|
-
// propagation does not reach the visible tree under
|
|
1443
|
-
// the current renderer/context configuration.
|
|
1444
|
-
//
|
|
1445
|
-
// Boundaries include both the directly-changed tsx files
|
|
1446
|
-
// AND every tsx ancestor reachable via the reverse import
|
|
1447
|
-
// graph (route files in particular). The framework
|
|
1448
|
-
// listener uses the route-file boundaries to look up the
|
|
1449
|
-
// freshly-patched `route.options.component` and pass it
|
|
1450
|
-
// through to the page remount.
|
|
1451
|
-
try {
|
|
1452
|
-
const tsxChangedInDrained = drained.filter((id) => /\.(tsx|jsx)$/i.test(id));
|
|
1453
|
-
const allBoundaries = Array.from(new Set([...tsxChangedInDrained, ...boundaries]));
|
|
1454
|
-
nsSolidHmrEmit({
|
|
1455
|
-
kind: 'solid',
|
|
1456
|
-
changedFiles: drained.slice(),
|
|
1457
|
-
boundaries: allBoundaries,
|
|
1458
|
-
ancestors: Array.from(ancestors),
|
|
1459
|
-
});
|
|
1460
|
-
}
|
|
1461
|
-
catch (err) {
|
|
1462
|
-
if (VERBOSE)
|
|
1463
|
-
console.warn('[hmr][solid] emit failed', err);
|
|
1464
|
-
}
|
|
1465
|
-
// Tell the overlay the cycle is done. solid-refresh's
|
|
1466
|
-
// inline patchRegistry has already flushed the new
|
|
1467
|
-
// component bodies into the live tree (the `case
|
|
1468
|
-
// 'solid'` block above re-imports each .tsx
|
|
1469
|
-
// boundary), so by the time we get here the user is
|
|
1470
|
-
// already looking at the new render. The 'complete'
|
|
1471
|
-
// frame surfaces the wall-clock total and triggers
|
|
1472
|
-
// the overlay's auto-hide.
|
|
1473
|
-
setUpdateOverlayStage('complete', {
|
|
1474
|
-
detail: `Total ${Math.max(0, Date.now() - tQueueStart)}ms`,
|
|
1475
|
-
});
|
|
1476
|
-
break;
|
|
1477
|
-
}
|
|
1478
|
-
case 'typescript': {
|
|
1479
|
-
// For TS apps, always reset back to the conventional app root.
|
|
1480
|
-
// This preserves the shell (Frame, ActionBar, etc.) that the app's
|
|
1481
|
-
// own bootstrapping wires up via `Application.run`.
|
|
1482
|
-
try {
|
|
1483
|
-
const g = getGlobalScope();
|
|
1484
|
-
const App = getCore('Application') || g.Application;
|
|
1485
|
-
if (!App || typeof App.resetRootView !== 'function') {
|
|
1486
|
-
if (VERBOSE)
|
|
1487
|
-
console.warn('[hmr][queue] TS flavor: Application.resetRootView unavailable; skipping UI refresh');
|
|
1488
|
-
break;
|
|
1489
|
-
}
|
|
1490
|
-
// Re-fetch changed XML/CSS files and update the bundled module registry
|
|
1491
|
-
// so Builder.createViewFromEntry picks up fresh content.
|
|
1492
|
-
const rawAssetIds = drained.filter((id) => /\.(xml|css|scss|sass|less)$/i.test(id));
|
|
1493
|
-
if (rawAssetIds.length && typeof g.registerModule === 'function') {
|
|
1494
|
-
const origin = getHttpOriginForVite() || deriveHttpOrigin(getHMRWsUrl());
|
|
1495
|
-
if (origin) {
|
|
1496
|
-
for (const id of rawAssetIds) {
|
|
1497
|
-
try {
|
|
1498
|
-
const spec = normalizeSpec(id);
|
|
1499
|
-
// Fetch the raw file content directly from Vite's dev server.
|
|
1500
|
-
// Use the project-relative path which Vite serves as static files.
|
|
1501
|
-
const fetchUrl = origin + (spec.startsWith('/') ? spec : '/' + spec);
|
|
1502
|
-
if (VERBOSE)
|
|
1503
|
-
console.log('[hmr][queue] fetching raw asset', { id, fetchUrl });
|
|
1504
|
-
const resp = await fetch(fetchUrl);
|
|
1505
|
-
if (resp.ok) {
|
|
1506
|
-
const rawContent = await resp.text();
|
|
1507
|
-
// Register under all nickname variants the module registry uses.
|
|
1508
|
-
// The bundler context registers XML as e.g., './main-page.xml' and 'main-page.xml'
|
|
1509
|
-
const relPath = toAppRelativePath(id);
|
|
1510
|
-
if (!relPath)
|
|
1511
|
-
continue;
|
|
1512
|
-
const nicknames = ['./' + relPath, relPath];
|
|
1513
|
-
// Also add without extension for CSS
|
|
1514
|
-
const extIdx = relPath.lastIndexOf('.');
|
|
1515
|
-
if (extIdx > 0) {
|
|
1516
|
-
const baseName = relPath.slice(0, extIdx);
|
|
1517
|
-
if (!relPath.endsWith('.xml'))
|
|
1518
|
-
nicknames.push(baseName, './' + baseName);
|
|
1519
|
-
}
|
|
1520
|
-
for (const name of nicknames) {
|
|
1521
|
-
if (VERBOSE)
|
|
1522
|
-
console.log('[hmr][queue] re-registering module', name);
|
|
1523
|
-
g.registerModule(name, () => rawContent);
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
else if (VERBOSE) {
|
|
1527
|
-
console.warn('[hmr][queue] raw asset fetch failed', id, resp.status);
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
catch (e) {
|
|
1531
|
-
if (VERBOSE)
|
|
1532
|
-
console.warn('[hmr][queue] raw asset refresh failed for', id, e);
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
// Modal-aware refresh: pages currently PRESENTED AS MODALS must be
|
|
1538
|
-
// closed + re-presented in place — navigating the top frame to a
|
|
1539
|
-
// modal's page would push it as a frame page, and resetRootView
|
|
1540
|
-
// would dismiss the modal entirely. State comes from core's live
|
|
1541
|
-
// modal stack (_moduleName/_modalOptions/_modalParent); the
|
|
1542
|
-
// showModal wrap only backfills options on older cores.
|
|
1543
|
-
ensureModalTracking();
|
|
1544
|
-
const openModals = getOpenStringModuleModals();
|
|
1545
|
-
const changedModuleNames = new Set(drained.map(toAppModuleName).filter(Boolean));
|
|
1546
|
-
const modalsToReshow = openModals.filter((record) => changedModuleNames.has(record.moduleName));
|
|
1547
|
-
const reshowModuleNames = new Set(modalsToReshow.map((record) => record.moduleName));
|
|
1548
|
-
// Determine if we can navigate in-place to a changed page
|
|
1549
|
-
// instead of resetting all the way back to app-root.
|
|
1550
|
-
// This keeps the user on the page they're editing for faster iteration.
|
|
1551
|
-
const changedXmlPages = drained
|
|
1552
|
-
.filter((id) => /\.xml$/i.test(id))
|
|
1553
|
-
.map((id) => toAppModuleName(id))
|
|
1554
|
-
.filter((m) => m && m !== 'app-root' && !reshowModuleNames.has(m));
|
|
1555
|
-
// Resolve the topmost Frame from the bundled realm.
|
|
1556
|
-
// Frame.topmost() relies on an internal frameStack array, so we must
|
|
1557
|
-
// call it on the bundled-realm class. Multiple strategies to find it:
|
|
1558
|
-
const FrameClass = getCore('Frame') || g.Frame;
|
|
1559
|
-
let topFrame = null;
|
|
1560
|
-
// 1) Try the vendor-realm static topmost()
|
|
1561
|
-
try {
|
|
1562
|
-
topFrame = FrameClass?.topmost?.();
|
|
1563
|
-
}
|
|
1564
|
-
catch { }
|
|
1565
|
-
// 2) Try getting the root view from Application — if it's a Frame, use it
|
|
1566
|
-
if (!topFrame) {
|
|
1567
|
-
try {
|
|
1568
|
-
const rootView = App.getRootView?.() || App._rootView;
|
|
1569
|
-
if (rootView) {
|
|
1570
|
-
// rootView could be a Frame itself, or contain a Frame
|
|
1571
|
-
const isFrame = rootView.constructor?.name === 'Frame' || rootView.navigate;
|
|
1572
|
-
if (isFrame) {
|
|
1573
|
-
topFrame = rootView;
|
|
1574
|
-
}
|
|
1575
|
-
else if (rootView.getChildAt) {
|
|
1576
|
-
// Walk direct children looking for a Frame
|
|
1577
|
-
for (let i = 0; i < (rootView.getChildrenCount?.() || 0); i++) {
|
|
1578
|
-
const child = rootView.getChildAt(i);
|
|
1579
|
-
if (child?.constructor?.name === 'Frame' || child?.navigate) {
|
|
1580
|
-
topFrame = child;
|
|
1581
|
-
break;
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
catch { }
|
|
1588
|
-
}
|
|
1589
|
-
if (VERBOSE)
|
|
1590
|
-
console.log('[hmr][queue] TS: changedXmlPages=', changedXmlPages, 'topFrame=', !!topFrame, 'modalsToReshow=', modalsToReshow.length);
|
|
1591
|
-
if (changedXmlPages.length > 0 && topFrame) {
|
|
1592
|
-
// Navigate the current frame to the changed page directly.
|
|
1593
|
-
// Use the last changed XML page (most specific).
|
|
1594
|
-
const moduleName = changedXmlPages[changedXmlPages.length - 1];
|
|
1595
|
-
if (VERBOSE)
|
|
1596
|
-
console.log('[hmr][queue] TS: navigating in-place to', moduleName);
|
|
1597
|
-
try {
|
|
1598
|
-
topFrame.navigate({ moduleName, clearHistory: false, animated: false });
|
|
1599
|
-
}
|
|
1600
|
-
catch (navErr) {
|
|
1601
|
-
console.warn('[hmr][queue] TS flavor: in-place navigate failed, falling back to resetRootView', navErr);
|
|
1602
|
-
App.resetRootView({ moduleName: 'app-root' });
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
else if (modalsToReshow.length === 0) {
|
|
1606
|
-
// No frame page to refresh and no open modal owns the change —
|
|
1607
|
-
// fall back to a full root reset. (Skipped when an open modal is
|
|
1608
|
-
// being re-presented below: resetRootView would dismiss it.)
|
|
1609
|
-
if (VERBOSE)
|
|
1610
|
-
console.log('[hmr][queue] TS flavor: resetRootView(app-root) after changes');
|
|
1611
|
-
App.resetRootView({ moduleName: 'app-root' });
|
|
1612
|
-
}
|
|
1613
|
-
// Re-present any open modals whose XML/code-behind changed. The
|
|
1614
|
-
// modules were already re-registered above (raw XML assets + fresh
|
|
1615
|
-
// code-behind exports), so the re-presented modal rebuilds from
|
|
1616
|
-
// the new content while the page beneath it stays put.
|
|
1617
|
-
for (const record of modalsToReshow) {
|
|
1618
|
-
await reshowOpenModal(record);
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
catch (e) {
|
|
1622
|
-
console.warn('[hmr][queue] TS flavor: resetRootView(app-root) failed', e);
|
|
1623
|
-
}
|
|
1624
|
-
// Tell the overlay the cycle is done — same as the solid path
|
|
1625
|
-
// above. Without this the applying overlay sticks at
|
|
1626
|
-
// 'received' (5%) forever even though the in-place navigate /
|
|
1627
|
-
// resetRootView already applied the update.
|
|
1628
|
-
setUpdateOverlayStage('complete', {
|
|
1629
|
-
detail: `Total ${Math.max(0, Date.now() - tQueueStart)}ms`,
|
|
1630
|
-
});
|
|
1631
|
-
break;
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
finally {
|
|
1636
|
-
processingQueue = false;
|
|
1637
|
-
processingPromise = null;
|
|
1638
|
-
// If a delta arrived mid-cycle (pushed onto changedQueue while
|
|
1639
|
-
// processingQueue was still true, so its processQueue() call early-
|
|
1640
|
-
// returned), re-drain — otherwise that save is stranded until the next
|
|
1641
|
-
// delta. Deferred via setTimeout to avoid synchronous re-entrancy as
|
|
1642
|
-
// this promise settles.
|
|
1643
|
-
if (changedQueue.length) {
|
|
1644
|
-
setTimeout(() => {
|
|
1645
|
-
try {
|
|
1646
|
-
processQueue();
|
|
1647
|
-
}
|
|
1648
|
-
catch { }
|
|
1649
|
-
}, 0);
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
})();
|
|
1653
|
-
return processingPromise;
|
|
1654
|
-
}
|
|
1655
|
-
let hmrSocket = null;
|
|
1656
|
-
// Single reconnect timer. Overlapping close/timeout events used to each schedule
|
|
1657
|
-
// their own `setTimeout(connectHmr, …)`, stacking multiple pending reconnects
|
|
1658
|
-
// that could spawn (and leak) duplicate sockets/listeners. Route all reconnect
|
|
1659
|
-
// scheduling through here so only one is ever pending.
|
|
1660
|
-
let reconnectTimer = null;
|
|
1661
|
-
function scheduleReconnect(delayMs) {
|
|
1662
|
-
if (reconnectTimer)
|
|
1663
|
-
clearTimeout(reconnectTimer);
|
|
1664
|
-
reconnectTimer = setTimeout(() => {
|
|
1665
|
-
reconnectTimer = null;
|
|
1666
|
-
connectHmr();
|
|
1667
|
-
}, delayMs);
|
|
1668
|
-
}
|
|
1669
|
-
// Track server-announced batches for each version so we can import in-order client-side
|
|
1670
|
-
const txnClientBatches = new Map();
|
|
1671
|
-
// Public hook for NativeScript runtime to call from ImportModuleDynamicallyCallback later.
|
|
1672
|
-
globalThis.__nsHmrRequestModule = async function (spec) {
|
|
1673
|
-
let normalized = normalizeSpec(spec);
|
|
1674
|
-
// Remove extension for transform lookup variants (server will try .ts/.js/.mjs)
|
|
1675
|
-
normalized = normalized.replace(/\.(ts|js|tsx|jsx|mjs)$/i, '');
|
|
1676
|
-
if (!normalized.startsWith('/'))
|
|
1677
|
-
normalized = '/' + normalized;
|
|
1678
|
-
if (normalized === '@') {
|
|
1679
|
-
// Soft-handle instead of throwing to avoid breaking evaluation paths: return a safe empty module stub.
|
|
1680
|
-
try {
|
|
1681
|
-
const err = new Error('[hmr-fetch] anomalous @ spec');
|
|
1682
|
-
const stack = err.stack || '';
|
|
1683
|
-
hmrMetrics.invalidAtSpec = (hmrMetrics.invalidAtSpec || 0) + 1;
|
|
1684
|
-
console.warn('[hmr-fetch] refusing to fetch bare @ spec; returning stub; original=', spec, 'stack=\n' + stack);
|
|
1685
|
-
}
|
|
1686
|
-
catch { }
|
|
1687
|
-
return '/__invalid_at__.mjs';
|
|
1688
|
-
}
|
|
1689
|
-
const url = await requestModuleFromServer(normalized);
|
|
1690
|
-
return url;
|
|
1691
|
-
};
|
|
1692
|
-
function connectHmr() {
|
|
1693
|
-
if (hmrSocket?.readyState === WebSocket.OPEN)
|
|
1694
|
-
return;
|
|
1695
|
-
if (hmrSocket?.readyState === WebSocket.CONNECTING) {
|
|
1696
|
-
if (VERBOSE)
|
|
1697
|
-
console.log('[hmr-client] Already connecting to HMR WebSocket, skipping');
|
|
1698
|
-
return;
|
|
1699
|
-
}
|
|
1700
|
-
// A reconnect fired (or a manual connect raced one) — cancel any other pending
|
|
1701
|
-
// reconnect so we don't end up with overlapping connect attempts.
|
|
1702
|
-
if (reconnectTimer) {
|
|
1703
|
-
clearTimeout(reconnectTimer);
|
|
1704
|
-
reconnectTimer = null;
|
|
1705
|
-
}
|
|
1706
|
-
try {
|
|
1707
|
-
globalThis.__NS_HMR_CLIENT_SOCKET_READY__ = false;
|
|
1708
|
-
}
|
|
1709
|
-
catch { }
|
|
1710
|
-
const overlayStage = hasOpenedHmrSocket ? 'reconnecting' : 'connecting';
|
|
1711
|
-
const baseUrl = getHMRWsUrl() || 'ws://localhost:5173/ns-hmr';
|
|
1712
|
-
const buildCandidates = (url) => {
|
|
1713
|
-
const candidates = [];
|
|
1714
|
-
try {
|
|
1715
|
-
const u = new URL(url);
|
|
1716
|
-
const proto = u.protocol === 'wss:' ? ['wss'] : ['ws'];
|
|
1717
|
-
const defaultPort = u.port || '5173';
|
|
1718
|
-
// Build ordered host candidates with preference to the active HTTP origin
|
|
1719
|
-
const orderedHosts = [];
|
|
1720
|
-
try {
|
|
1721
|
-
const g = getGlobalScope();
|
|
1722
|
-
const httpOrigin = g && typeof g.__NS_HTTP_ORIGIN__ === 'string' ? g.__NS_HTTP_ORIGIN__ : undefined;
|
|
1723
|
-
if (httpOrigin) {
|
|
1724
|
-
try {
|
|
1725
|
-
const ho = new URL(httpOrigin);
|
|
1726
|
-
orderedHosts.push({
|
|
1727
|
-
host: ho.hostname,
|
|
1728
|
-
port: ho.port || defaultPort,
|
|
1729
|
-
});
|
|
1730
|
-
}
|
|
1731
|
-
catch { }
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
catch { }
|
|
1735
|
-
// From provided URL
|
|
1736
|
-
orderedHosts.push({ host: u.hostname, port: defaultPort });
|
|
1737
|
-
// Explicit override
|
|
1738
|
-
try {
|
|
1739
|
-
const h = globalThis.__NS_HMR_HOST;
|
|
1740
|
-
if (h)
|
|
1741
|
-
orderedHosts.push({ host: String(h), port: defaultPort });
|
|
1742
|
-
}
|
|
1743
|
-
catch { }
|
|
1744
|
-
// Common fallbacks
|
|
1745
|
-
orderedHosts.push({ host: '10.0.2.2', port: defaultPort });
|
|
1746
|
-
orderedHosts.push({ host: 'localhost', port: defaultPort });
|
|
1747
|
-
const seen = new Set();
|
|
1748
|
-
for (const p of proto) {
|
|
1749
|
-
for (const { host, port } of orderedHosts) {
|
|
1750
|
-
const key = `${host}:${port}`;
|
|
1751
|
-
if (seen.has(key))
|
|
1752
|
-
continue;
|
|
1753
|
-
seen.add(key);
|
|
1754
|
-
const cand = `${p}://${host}:${port}${u.pathname || '/ns-hmr'}${u.search || ''}`;
|
|
1755
|
-
candidates.push(cand);
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
catch {
|
|
1760
|
-
candidates.push(url);
|
|
1761
|
-
}
|
|
1762
|
-
// Deduplicate while preserving order
|
|
1763
|
-
const seen = new Set();
|
|
1764
|
-
return candidates.filter((c) => (seen.has(c) ? false : (seen.add(c), true)));
|
|
1765
|
-
};
|
|
1766
|
-
const candidates = buildCandidates(baseUrl);
|
|
1767
|
-
let idx = 0;
|
|
1768
|
-
const tryNext = () => {
|
|
1769
|
-
if (idx >= candidates.length) {
|
|
1770
|
-
showConnectionOverlayNow('offline', 'Waiting for the Vite websocket to come back.');
|
|
1771
|
-
console.warn('[hmr-client] All WS candidates failed:', candidates.join(', '));
|
|
1772
|
-
scheduleReconnect(1500);
|
|
1773
|
-
return;
|
|
1774
|
-
}
|
|
1775
|
-
const url = candidates[idx++];
|
|
1776
|
-
const connectionDetail = `${overlayStage === 'reconnecting' ? 'Retrying' : 'Opening'} ${url}`;
|
|
1777
|
-
if (overlayStage === 'reconnecting' && shownConnectionOverlayStage === 'offline') {
|
|
1778
|
-
// Don't flip the visible 'offline' frame to 'reconnecting' for an
|
|
1779
|
-
// attempt that may fail instantly — defer it so only a genuinely
|
|
1780
|
-
// in-flight attempt surfaces. The deferred show is cancelled by
|
|
1781
|
-
// showConnectionOverlayNow (offline re-show / synchronizing) the
|
|
1782
|
-
// moment this attempt resolves. Keeps 'offline' stable instead of
|
|
1783
|
-
// flickering once per retry. (Checking the PAINTED stage, not the
|
|
1784
|
-
// pending one, so every candidate in the loop keeps deferring.)
|
|
1785
|
-
scheduleConnectionOverlay(overlayStage, connectionDetail, RECONNECTING_OVER_OFFLINE_DELAY_MS);
|
|
1786
|
-
}
|
|
1787
|
-
else if (connectionOverlayVisible) {
|
|
1788
|
-
updateConnectionOverlay(overlayStage, connectionDetail);
|
|
1789
|
-
}
|
|
1790
|
-
else {
|
|
1791
|
-
scheduleConnectionOverlay(overlayStage, connectionDetail);
|
|
1792
|
-
}
|
|
1793
|
-
try {
|
|
1794
|
-
if (VERBOSE)
|
|
1795
|
-
console.log('[hmr-client] Connecting to HMR WebSocket:', url);
|
|
1796
|
-
const sock = new WebSocket(url);
|
|
1797
|
-
hmrSocket = sock;
|
|
1798
|
-
setHttpOriginForVite(deriveHttpOrigin(url));
|
|
1799
|
-
let opened = false;
|
|
1800
|
-
const timeout = setTimeout(() => {
|
|
1801
|
-
if (!opened && sock.readyState !== WebSocket.OPEN) {
|
|
1802
|
-
try {
|
|
1803
|
-
sock.close();
|
|
1804
|
-
}
|
|
1805
|
-
catch { }
|
|
1806
|
-
if (VERBOSE)
|
|
1807
|
-
console.warn('[hmr-client] WS connect timeout, trying next…');
|
|
1808
|
-
tryNext();
|
|
1809
|
-
}
|
|
1810
|
-
}, 3500);
|
|
1811
|
-
sock.onopen = () => {
|
|
1812
|
-
opened = true;
|
|
1813
|
-
clearTimeout(timeout);
|
|
1814
|
-
clearConnectionOverlayTimer();
|
|
1815
|
-
hasOpenedHmrSocket = true;
|
|
1816
|
-
awaitingHealthyHmrMessage = true;
|
|
1817
|
-
try {
|
|
1818
|
-
globalThis.__NS_HMR_CLIENT_SOCKET_READY__ = true;
|
|
1819
|
-
}
|
|
1820
|
-
catch { }
|
|
1821
|
-
// Wire the JS hot registry's server channel to this socket so
|
|
1822
|
-
// `import.meta.hot.send(event, data)` reaches Vite plugins as
|
|
1823
|
-
// `{ type: 'custom', event, data }` (Vite wire protocol).
|
|
1824
|
-
try {
|
|
1825
|
-
getNsHotRegistry().setSendToServer((event, data) => {
|
|
1826
|
-
try {
|
|
1827
|
-
if (sock.readyState === WebSocket.OPEN) {
|
|
1828
|
-
sock.send(JSON.stringify({ type: 'custom', event, data }));
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
catch { }
|
|
1832
|
-
});
|
|
1833
|
-
}
|
|
1834
|
-
catch { }
|
|
1835
|
-
if (connectionOverlayVisible) {
|
|
1836
|
-
showConnectionOverlayNow('synchronizing', 'Connected. Synchronizing the HMR graph.');
|
|
1837
|
-
}
|
|
1838
|
-
if (VERBOSE)
|
|
1839
|
-
console.log('[hmr-client] Connected to HMR WebSocket');
|
|
1840
|
-
// Print the active module reload mode once on first
|
|
1841
|
-
// successful connect so the user can correlate HMR latency
|
|
1842
|
-
// with runtime capability without grepping for protocol
|
|
1843
|
-
// details. The banner is verbose-gated.
|
|
1844
|
-
try {
|
|
1845
|
-
emitHmrModeBannerOnce();
|
|
1846
|
-
}
|
|
1847
|
-
catch { }
|
|
1848
|
-
};
|
|
1849
|
-
sock.onmessage = handleHmrMessage;
|
|
1850
|
-
sock.onerror = (error) => {
|
|
1851
|
-
clearTimeout(timeout);
|
|
1852
|
-
console.warn('[hmr-client] WebSocket error:', (error && (error.message || error)) || error);
|
|
1853
|
-
};
|
|
1854
|
-
sock.onclose = (ev) => {
|
|
1855
|
-
clearTimeout(timeout);
|
|
1856
|
-
try {
|
|
1857
|
-
globalThis.__NS_HMR_CLIENT_SOCKET_READY__ = false;
|
|
1858
|
-
}
|
|
1859
|
-
catch { }
|
|
1860
|
-
if (!opened) {
|
|
1861
|
-
// immediate failure during connect → try another candidate
|
|
1862
|
-
if (VERBOSE)
|
|
1863
|
-
console.warn('[hmr-client] WS close before open (code', ev?.code, '), trying next…');
|
|
1864
|
-
tryNext();
|
|
1865
|
-
}
|
|
1866
|
-
else {
|
|
1867
|
-
if (VERBOSE)
|
|
1868
|
-
console.log('[hmr-client] WebSocket closed (code', ev?.code, '), will reconnect…');
|
|
1869
|
-
scheduleConnectionOverlay('reconnecting', 'The websocket closed. Waiting to reconnect.', 700);
|
|
1870
|
-
// try to reconnect with full candidate list again
|
|
1871
|
-
scheduleReconnect(1000);
|
|
1872
|
-
}
|
|
1873
|
-
};
|
|
1874
|
-
}
|
|
1875
|
-
catch (e) {
|
|
1876
|
-
console.warn('[hmr-client] Failed to connect WebSocket', e);
|
|
1877
|
-
tryNext();
|
|
1878
|
-
}
|
|
1879
|
-
};
|
|
1880
|
-
tryNext();
|
|
1881
|
-
}
|
|
1882
|
-
async function handleHmrMessage(ev) {
|
|
1883
|
-
let msg;
|
|
1884
|
-
try {
|
|
1885
|
-
msg = JSON.parse(ev.data);
|
|
1886
|
-
}
|
|
1887
|
-
catch {
|
|
1888
|
-
return;
|
|
1889
|
-
}
|
|
1890
|
-
if (awaitingHealthyHmrMessage && msg) {
|
|
1891
|
-
markHmrConnectionHealthy();
|
|
1892
|
-
}
|
|
1893
|
-
if (VERBOSE && msg?.type) {
|
|
1894
|
-
console.log('[hmr-client] received message', msg.type);
|
|
1895
|
-
}
|
|
1896
|
-
// Notify optional app-level hook after an HMR batch is applied.
|
|
1897
|
-
function notifyAppHmrUpdate(kind, changedIds) {
|
|
1898
|
-
try {
|
|
1899
|
-
const hook = globalThis.__NS_HMR_ON_UPDATE__;
|
|
1900
|
-
if (typeof hook === 'function') {
|
|
1901
|
-
hook({ type: kind, version: getGraphVersion(), changedIds: changedIds || [], raw: msg });
|
|
1902
|
-
}
|
|
1903
|
-
}
|
|
1904
|
-
catch { }
|
|
1905
|
-
}
|
|
1906
|
-
if (msg) {
|
|
1907
|
-
// `ns:hmr-pending` is a fire-and-forget UX hint emitted by the
|
|
1908
|
-
// server at the START of handleHotUpdate. We drive the
|
|
1909
|
-
// HMR-applying overlay's 'received' frame here (synchronously),
|
|
1910
|
-
// well before the authoritative payload (`ns:angular-update` /
|
|
1911
|
-
// `ns:css-updates`) lands. Skip running any other handlers —
|
|
1912
|
-
// the pending message has no module payload and intentionally
|
|
1913
|
-
// does not bump the graph version.
|
|
1914
|
-
if (msg.type === 'ns:hmr-pending' && typeof msg.path === 'string') {
|
|
1915
|
-
setHmrPendingOverlay(msg.path);
|
|
1916
|
-
return;
|
|
1917
|
-
}
|
|
1918
|
-
// The per-flavor client strategy is loaded by a dynamic import(); make
|
|
1919
|
-
// sure it has resolved (and `install()` has run) before any handler that
|
|
1920
|
-
// delegates through it. After the first message this is an already-settled
|
|
1921
|
-
// promise (microtask only); for Solid/TypeScript it is `Promise.resolve()`.
|
|
1922
|
-
await CLIENT_STRATEGY_READY;
|
|
1923
|
-
if (msg.type === 'ns:hmr-full-graph') {
|
|
1924
|
-
// Bump a monotonic nonce so HTTP ESM imports can always be cache-busted per update.
|
|
1925
|
-
try {
|
|
1926
|
-
const g = getGlobalScope();
|
|
1927
|
-
g.__NS_HMR_IMPORT_NONCE__ = (typeof g.__NS_HMR_IMPORT_NONCE__ === 'number' ? g.__NS_HMR_IMPORT_NONCE__ : 0) + 1;
|
|
1928
|
-
}
|
|
1929
|
-
catch { }
|
|
1930
|
-
// Capture previous graph snapshot so we can infer which modules changed.
|
|
1931
|
-
const prevGraph = new Map(graph);
|
|
1932
|
-
setGraphVersion(Number(msg.version || getGraphVersion() || 0));
|
|
1933
|
-
applyFullGraph(msg);
|
|
1934
|
-
hasReceivedFullGraph = true;
|
|
1935
|
-
// Gate: On first boot, the entry-runtime handles all initial module loading
|
|
1936
|
-
// (with the import map already configured). Don't re-import here — the graph
|
|
1937
|
-
// is stored above for future HMR delta comparisons, but modules are already
|
|
1938
|
-
// loaded correctly via the entry-runtime boot sequence.
|
|
1939
|
-
//
|
|
1940
|
-
// Two cases to catch:
|
|
1941
|
-
// 1. Boot still in progress (__NS_HMR_BOOT_COMPLETE__ is false)
|
|
1942
|
-
// 2. Boot already finished but this is the FIRST full-graph (prevGraph was
|
|
1943
|
-
// empty). The WebSocket often connects after entry-runtime finishes, so
|
|
1944
|
-
// boot is "complete" but we still shouldn't re-import — all modules were
|
|
1945
|
-
// just loaded fresh. Only re-import on subsequent full-graphs (reconnect
|
|
1946
|
-
// scenarios) where prevGraph already has entries.
|
|
1947
|
-
if (!globalThis.__NS_HMR_BOOT_COMPLETE__) {
|
|
1948
|
-
if (VERBOSE)
|
|
1949
|
-
console.info('[hmr][full-graph] skipping initial re-import (boot in progress)');
|
|
1950
|
-
const fullIds = Array.isArray(msg.modules) ? msg.modules.map((m) => m?.id).filter(Boolean) : [];
|
|
1951
|
-
notifyAppHmrUpdate('full-graph', fullIds);
|
|
1952
|
-
return;
|
|
1953
|
-
}
|
|
1954
|
-
if (prevGraph.size === 0) {
|
|
1955
|
-
if (VERBOSE)
|
|
1956
|
-
console.info('[hmr][full-graph] skipping re-import on first graph after boot (modules already fresh)');
|
|
1957
|
-
const fullIds = Array.isArray(msg.modules) ? msg.modules.map((m) => m?.id).filter(Boolean) : [];
|
|
1958
|
-
notifyAppHmrUpdate('full-graph', fullIds);
|
|
1959
|
-
return;
|
|
1960
|
-
}
|
|
1961
|
-
// Reconnect / resync case — re-import changed modules as normal.
|
|
1962
|
-
// In some cases (e.g. server chooses full-graph resync / page reload), we won't
|
|
1963
|
-
// receive a delta queue to re-import changed TS modules. Without re-import,
|
|
1964
|
-
// HTTP ESM caching means module bodies (and side effects) won't re-run.
|
|
1965
|
-
try {
|
|
1966
|
-
const inferredChanged = [];
|
|
1967
|
-
try {
|
|
1968
|
-
for (const [id, next] of graph.entries()) {
|
|
1969
|
-
const prev = prevGraph.get(id);
|
|
1970
|
-
if (!prev || prev.hash !== next.hash)
|
|
1971
|
-
inferredChanged.push(id);
|
|
1972
|
-
}
|
|
1973
|
-
// Removed modules are also "changed" but we don't import them.
|
|
1974
|
-
}
|
|
1975
|
-
catch { }
|
|
1976
|
-
// Best-effort: only re-import real source modules (avoid .vue registry pipeline and virtual ids)
|
|
1977
|
-
const toReimport = inferredChanged.filter((id) => {
|
|
1978
|
-
if (!id || typeof id !== 'string')
|
|
1979
|
-
return false;
|
|
1980
|
-
if (/^\0|^\/\0/.test(id))
|
|
1981
|
-
return false;
|
|
1982
|
-
if (/plugin-vue:export-helper/.test(id))
|
|
1983
|
-
return false;
|
|
1984
|
-
if (/\.vue$/i.test(id))
|
|
1985
|
-
return false;
|
|
1986
|
-
if (id.endsWith(APP_MAIN_ENTRY_SPEC))
|
|
1987
|
-
return false;
|
|
1988
|
-
return true;
|
|
1989
|
-
});
|
|
1990
|
-
if (toReimport.length && VERBOSE)
|
|
1991
|
-
console.log('[hmr][full-graph] inferred changed modules; re-importing', toReimport);
|
|
1992
|
-
// Evict the inferred changed set before re-importing.
|
|
1993
|
-
// See `processQueue` for the architectural rationale; the
|
|
1994
|
-
// full-graph code path is the resync fallback (server chose
|
|
1995
|
-
// not to send a delta) and shares the same V8 cache pitfall.
|
|
1996
|
-
const fgEvictUrls = buildEvictionUrls(toReimport);
|
|
1997
|
-
const fgEvicted = invalidateModulesByUrls(fgEvictUrls);
|
|
1998
|
-
if (VERBOSE)
|
|
1999
|
-
console.log(`[hmr][full-graph] eviction count=${fgEvictUrls.length} ok=${fgEvicted}`);
|
|
2000
|
-
for (const id of toReimport) {
|
|
2001
|
-
try {
|
|
2002
|
-
const spec = normalizeSpec(id);
|
|
2003
|
-
const url = await requestModuleFromServer(spec);
|
|
2004
|
-
if (!url)
|
|
2005
|
-
continue;
|
|
2006
|
-
if (VERBOSE)
|
|
2007
|
-
console.log('[hmr][full-graph] re-import', { id, spec, url });
|
|
2008
|
-
await import(/* @vite-ignore */ url);
|
|
2009
|
-
}
|
|
2010
|
-
catch (e) {
|
|
2011
|
-
if (VERBOSE)
|
|
2012
|
-
console.warn('[hmr][full-graph] re-import failed for', id, e);
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
catch { }
|
|
2017
|
-
const fullIds = Array.isArray(msg.modules) ? msg.modules.map((m) => m?.id).filter(Boolean) : [];
|
|
2018
|
-
notifyAppHmrUpdate('full-graph', fullIds);
|
|
2019
|
-
return;
|
|
2020
|
-
}
|
|
2021
|
-
if (msg.type === 'ns:ts-module-registry') {
|
|
2022
|
-
try {
|
|
2023
|
-
const mods = Array.isArray(msg.modules) ? msg.modules.filter((m) => typeof m === 'string') : [];
|
|
2024
|
-
if (mods.length) {
|
|
2025
|
-
if (!tsModuleSet)
|
|
2026
|
-
tsModuleSet = new Set();
|
|
2027
|
-
mods.forEach((m) => tsModuleSet.add(m));
|
|
2028
|
-
// Prefer explicit app main entry if present, else first module
|
|
2029
|
-
if (mods.includes(APP_MAIN_ENTRY_SPEC)) {
|
|
2030
|
-
tsMainId = APP_MAIN_ENTRY_SPEC;
|
|
2031
|
-
}
|
|
2032
|
-
else if (!tsMainId) {
|
|
2033
|
-
tsMainId = mods[0];
|
|
2034
|
-
}
|
|
2035
|
-
if (VERBOSE)
|
|
2036
|
-
console.log('[hmr-client][ts-registry] registered TS modules', {
|
|
2037
|
-
count: tsModuleSet.size,
|
|
2038
|
-
mainId: tsMainId,
|
|
2039
|
-
});
|
|
2040
|
-
}
|
|
2041
|
-
}
|
|
2042
|
-
catch (e) {
|
|
2043
|
-
if (VERBOSE)
|
|
2044
|
-
console.warn('[hmr-client][ts-registry] failed to record', e);
|
|
2045
|
-
}
|
|
2046
|
-
return;
|
|
2047
|
-
}
|
|
2048
|
-
if (msg.type === 'ns:hmr-delta') {
|
|
2049
|
-
// Bump a monotonic nonce so HTTP ESM imports can always be cache-busted per update.
|
|
2050
|
-
try {
|
|
2051
|
-
const g = getGlobalScope();
|
|
2052
|
-
g.__NS_HMR_IMPORT_NONCE__ = (typeof g.__NS_HMR_IMPORT_NONCE__ === 'number' ? g.__NS_HMR_IMPORT_NONCE__ : 0) + 1;
|
|
2053
|
-
}
|
|
2054
|
-
catch { }
|
|
2055
|
-
try {
|
|
2056
|
-
const ids = Array.isArray(msg.changed) ? msg.changed.map((c) => c?.id).filter(Boolean) : [];
|
|
2057
|
-
if (ids.length)
|
|
2058
|
-
txnClientBatches.set(getGraphVersion(), ids);
|
|
2059
|
-
}
|
|
2060
|
-
catch { }
|
|
2061
|
-
applyDelta(msg);
|
|
2062
|
-
// Ensure queued module re-imports complete before notifying app hooks.
|
|
2063
|
-
// Otherwise app-level handlers can run against stale module bodies due to HTTP ESM caching.
|
|
2064
|
-
try {
|
|
2065
|
-
await processQueue();
|
|
2066
|
-
}
|
|
2067
|
-
catch { }
|
|
2068
|
-
const deltaIds = Array.isArray(msg.changed) ? msg.changed.map((c) => c?.id).filter(Boolean) : [];
|
|
2069
|
-
notifyAppHmrUpdate('delta', deltaIds);
|
|
2070
|
-
return;
|
|
2071
|
-
}
|
|
2072
|
-
else {
|
|
2073
|
-
// Vite custom-event dispatch.
|
|
2074
|
-
//
|
|
2075
|
-
// `server.ws.send('event-name', payload)` from any Vite plugin lands
|
|
2076
|
-
// on the wire as `{ type: 'custom', event: 'event-name', data: payload }`.
|
|
2077
|
-
// On the web, Vite's stock client owns a `customListenersMap` that
|
|
2078
|
-
// fires every `import.meta.hot.on('event-name', cb)` callback. We
|
|
2079
|
-
// don't run Vite's stock client on device — the JS hot registry
|
|
2080
|
-
// (`hot-context.ts`, installed by the bootstrap before the entry
|
|
2081
|
-
// graph) owns the listener registry populated by user code +
|
|
2082
|
-
// compiled Angular components via `import.meta.hot.on`.
|
|
2083
|
-
// Forwarding `type: 'custom'` here is the only thing standing
|
|
2084
|
-
// between server-emitted events and the listeners they were
|
|
2085
|
-
// meant for.
|
|
2086
|
-
//
|
|
2087
|
-
// `angular:component-update` is the canonical example. Analog's
|
|
2088
|
-
// plugin sends it on `.html` / component-style edits; the
|
|
2089
|
-
// compiled component `.mjs` registered a listener that
|
|
2090
|
-
// dynamic-imports `/@ng/component?c=<id>&t=<ts>` and calls
|
|
2091
|
-
// `ɵɵreplaceMetadata` on the live class — swapping the template
|
|
2092
|
-
// definition AND walking live `LView`s to recreate matching views
|
|
2093
|
-
// in-place. The page stays mounted and only the changed bits
|
|
2094
|
-
// re-render. We MUST `return` after dispatch so the reboot path
|
|
2095
|
-
// (`handleAngularHotUpdateMessage` → `__reboot_ng_modules__`)
|
|
2096
|
-
// never runs for these updates — that's the whole point of the
|
|
2097
|
-
// component-replacement pipeline.
|
|
2098
|
-
//
|
|
2099
|
-
// All other custom events are forwarded but NOT short-circuited
|
|
2100
|
-
// (Vite spec: custom events are additive — they don't replace
|
|
2101
|
-
// any framework-specific handling). The reboot path falls through
|
|
2102
|
-
// for `ns:angular-update` (the legacy/`.ts`-edit broadcast) and
|
|
2103
|
-
// for any framework not yet using the in-place replacement path.
|
|
2104
|
-
if (msg.type === 'custom' && typeof msg.event === 'string') {
|
|
2105
|
-
// Dispatch every Vite "custom" event through the JS hot
|
|
2106
|
-
// registry so `import.meta.hot.on(event, cb)` callbacks fire
|
|
2107
|
-
// on the device. Critical contract: this is the ONLY route by
|
|
2108
|
-
// which Analog's `angular:component-update` reaches the
|
|
2109
|
-
// compiled component's `(d) => d.id === id && Component_HmrLoad(...)`
|
|
2110
|
-
// listener — without it, server-side broadcasts log green
|
|
2111
|
-
// (`(client) hmr update`) while the device sees nothing happen.
|
|
2112
|
-
//
|
|
2113
|
-
// Diagnostic policy: listener exceptions log loud (compiled
|
|
2114
|
-
// HmrLoad fetch/parse error). Successful dispatches are silent.
|
|
2115
|
-
try {
|
|
2116
|
-
getNsHotRegistry().dispatchHotEvent(msg.event, msg.data);
|
|
2117
|
-
}
|
|
2118
|
-
catch (err) {
|
|
2119
|
-
console.warn('[hmr-client][custom] dispatch threw for', msg.event, err);
|
|
2120
|
-
}
|
|
2121
|
-
if (msg.event === 'angular:component-update') {
|
|
2122
|
-
if (VERBOSE)
|
|
2123
|
-
console.log('[hmr-client][custom] dispatched angular:component-update — skipping reboot path');
|
|
2124
|
-
// Walk the apply-progress overlay through its
|
|
2125
|
-
// remaining stages for the in-place template-swap
|
|
2126
|
-
// path. The full reboot path
|
|
2127
|
-
// (`handleAngularHotUpdateMessage`) drives the
|
|
2128
|
-
// overlay itself ('received' → 'evicting' →
|
|
2129
|
-
// 'reimporting' → 'rebooting' → 'complete'); the
|
|
2130
|
-
// in-place path bypasses that handler entirely
|
|
2131
|
-
// because the work happens inside Angular's
|
|
2132
|
-
// `ɵɵreplaceMetadata` after the runtime forwards the
|
|
2133
|
-
// `angular:component-update` event to the compiled
|
|
2134
|
-
// component's listener. Without this update the
|
|
2135
|
-
// overlay would freeze at 5% ('received') even
|
|
2136
|
-
// though the visual swap completes a few frames
|
|
2137
|
-
// later — exactly the "Preparing update (5%)" stuck
|
|
2138
|
-
// frame we have been chasing.
|
|
2139
|
-
//
|
|
2140
|
-
// We transition straight to 'reimporting' to
|
|
2141
|
-
// communicate that metadata is being fetched (the
|
|
2142
|
-
// runtime listener fires `__ns_import('/@ng/component?c=...&t=...')`),
|
|
2143
|
-
// then schedule 'complete' on the next macrotask so
|
|
2144
|
-
// the auto-hide timer kicks in. The actual
|
|
2145
|
-
// template swap is fire-and-forget from this point;
|
|
2146
|
-
// the user sees the overlay close at the same time
|
|
2147
|
-
// as Angular re-renders the bound text/structure.
|
|
2148
|
-
try {
|
|
2149
|
-
const filePath = typeof msg.data?.id === 'string' ? decodeURIComponent(msg.data.id).split('@')[0] : undefined;
|
|
2150
|
-
const detail = filePath ? `Applying template update to ${filePath}` : 'Applying template update';
|
|
2151
|
-
setUpdateOverlayStage('reimporting', { detail });
|
|
2152
|
-
setTimeout(() => {
|
|
2153
|
-
try {
|
|
2154
|
-
setUpdateOverlayStage('complete', { detail: filePath ? `Updated ${filePath}` : 'Update applied' });
|
|
2155
|
-
}
|
|
2156
|
-
catch { }
|
|
2157
|
-
}, 16);
|
|
2158
|
-
}
|
|
2159
|
-
catch { }
|
|
2160
|
-
return;
|
|
2161
|
-
}
|
|
2162
|
-
}
|
|
2163
|
-
if (msg.type === 'ns:angular-update' && typeof msg.version === 'number') {
|
|
2164
|
-
setGraphVersion(Number(msg.version || getGraphVersion() || 0));
|
|
2165
|
-
}
|
|
2166
|
-
if (CLIENT_STRATEGY?.handleHotUpdateMessage && (await CLIENT_STRATEGY.handleHotUpdateMessage(msg, { getCore, verbose: VERBOSE, performResetRoot, getOverlay: getHmrOverlayApi }))) {
|
|
2167
|
-
return;
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
// On-demand module fetch response (Option A)
|
|
2172
|
-
if (msg.type === 'ns:module-response' && typeof msg.requestId === 'number') {
|
|
2173
|
-
const pending = pendingModuleFetches.get(msg.requestId);
|
|
2174
|
-
if (pending) {
|
|
2175
|
-
pendingModuleFetches.delete(msg.requestId);
|
|
2176
|
-
if (msg.error) {
|
|
2177
|
-
if (VERBOSE)
|
|
2178
|
-
console.warn('[hmr-fetch] error', msg.error, 'spec=', pending.spec, 'id=', msg.requestId);
|
|
2179
|
-
pending.reject(new Error(`[hmr-fetch] ${msg.error} for ${pending.spec}`));
|
|
2180
|
-
}
|
|
2181
|
-
else {
|
|
2182
|
-
try {
|
|
2183
|
-
const origin = getHttpOriginForVite() || deriveHttpOrigin(getHMRWsUrl());
|
|
2184
|
-
if (!origin)
|
|
2185
|
-
throw new Error('no-http-origin');
|
|
2186
|
-
const url = origin + '/ns/m' + (String(pending.spec).startsWith('/') ? pending.spec : '/' + pending.spec);
|
|
2187
|
-
moduleFetchCache.set(pending.spec, url);
|
|
2188
|
-
if (VERBOSE)
|
|
2189
|
-
console.log('[hmr-fetch] resolved via URL', pending.spec, '->', url, 'id=', msg.requestId);
|
|
2190
|
-
pending.resolve(url);
|
|
2191
|
-
}
|
|
2192
|
-
catch (e) {
|
|
2193
|
-
pending.reject(e);
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
|
-
}
|
|
2197
|
-
return;
|
|
2198
|
-
}
|
|
2199
|
-
if (msg.type === 'ns:css-updates' && Array.isArray(msg.updates)) {
|
|
2200
|
-
// Drive the HMR-applying overlay past the 'received' (5%) frame
|
|
2201
|
-
// that `ns:hmr-pending` set earlier in the cycle. Without this
|
|
2202
|
-
// the overlay sticks at "Preparing update" forever for CSS-only
|
|
2203
|
-
// edits because `handleCssUpdates` is a leaf — there's no
|
|
2204
|
-
// downstream module-evaluation path that would hit the queue's
|
|
2205
|
-
// 'complete' transition.
|
|
2206
|
-
const cssCount = msg.updates.length;
|
|
2207
|
-
try {
|
|
2208
|
-
setUpdateOverlayStage('reimporting', { detail: buildCssApplyingDetail(cssCount) });
|
|
2209
|
-
}
|
|
2210
|
-
catch { }
|
|
2211
|
-
try {
|
|
2212
|
-
const origin = msg.origin || getHttpOriginForVite() || deriveHttpOrigin(getHMRWsUrl());
|
|
2213
|
-
await handleCssUpdates(msg.updates, origin);
|
|
2214
|
-
try {
|
|
2215
|
-
setUpdateOverlayStage('complete', { detail: buildCssAppliedDetail(cssCount) });
|
|
2216
|
-
}
|
|
2217
|
-
catch { }
|
|
2218
|
-
return;
|
|
2219
|
-
}
|
|
2220
|
-
catch (e) {
|
|
2221
|
-
console.warn('[hmr-client] CSS updates handling failed:', e);
|
|
2222
|
-
try {
|
|
2223
|
-
setUpdateOverlayStage('complete', { detail: 'CSS update failed' });
|
|
2224
|
-
}
|
|
2225
|
-
catch { }
|
|
2226
|
-
return;
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
if (msg.type === 'ns:vue-sfc-registry') {
|
|
2230
|
-
CLIENT_STRATEGY?.handleSfcRegistry?.(msg);
|
|
2231
|
-
return;
|
|
2232
|
-
}
|
|
2233
|
-
if (msg.type === 'ns:vue-sfc-registry-update') {
|
|
2234
|
-
if (typeof msg.version === 'number')
|
|
2235
|
-
setGraphVersion(msg.version);
|
|
2236
|
-
// `ns:hmr-pending` already set the overlay to 'received' (5%). The Vue
|
|
2237
|
-
// strategy walks 'evicting' → 'reimporting' → 'rebooting' → 'complete'
|
|
2238
|
-
// around the SFC load + reset so the toast always lands on 'complete'
|
|
2239
|
-
// (or a failure detail) and the auto-hide timer can dismiss it.
|
|
2240
|
-
await CLIENT_STRATEGY?.handleSfcRegistryUpdate?.(msg, getGraphVersion(), { getCore, verbose: VERBOSE, performResetRoot, getOverlay: getHmrOverlayApi });
|
|
2241
|
-
return;
|
|
2242
|
-
}
|
|
2243
|
-
}
|
|
2244
|
-
function normalizeComponent(input, nameHint) {
|
|
2245
|
-
try {
|
|
2246
|
-
if (!input)
|
|
2247
|
-
return null;
|
|
2248
|
-
// Unwrap module namespace with default
|
|
2249
|
-
if (input && typeof input === 'object' && 'default' in input) {
|
|
2250
|
-
const d = input.default;
|
|
2251
|
-
if (d)
|
|
2252
|
-
return normalizeComponent(d, nameHint);
|
|
2253
|
-
}
|
|
2254
|
-
// If already a component-like object with render/setup/template, return as-is
|
|
2255
|
-
if (typeof input === 'object' && (input.render || input.setup || input.template || input.__isVue)) {
|
|
2256
|
-
return input;
|
|
2257
|
-
}
|
|
2258
|
-
// If provided a render function, wrap with defineComponent
|
|
2259
|
-
if (typeof input === 'function') {
|
|
2260
|
-
CLIENT_STRATEGY?.beforeNavigateBuild?.();
|
|
2261
|
-
const comp = getGlobalScope().defineComponent
|
|
2262
|
-
? getGlobalScope().defineComponent({
|
|
2263
|
-
name: nameHint || input.name || 'AnonymousSFC',
|
|
2264
|
-
render: input,
|
|
2265
|
-
})
|
|
2266
|
-
: { name: nameHint || input.name || 'AnonymousSFC', render: input };
|
|
2267
|
-
return comp;
|
|
2268
|
-
}
|
|
2269
|
-
// If object has a render function property
|
|
2270
|
-
if (input?.render && typeof input.render === 'function') {
|
|
2271
|
-
CLIENT_STRATEGY?.beforeNavigateBuild?.();
|
|
2272
|
-
const comp = getGlobalScope().defineComponent
|
|
2273
|
-
? getGlobalScope().defineComponent({
|
|
2274
|
-
name: nameHint || input.name || 'AnonymousSFC',
|
|
2275
|
-
render: input.render,
|
|
2276
|
-
})
|
|
2277
|
-
: {
|
|
2278
|
-
name: nameHint || input.name || 'AnonymousSFC',
|
|
2279
|
-
render: input.render,
|
|
2280
|
-
};
|
|
2281
|
-
return comp;
|
|
2282
|
-
}
|
|
2283
|
-
}
|
|
2284
|
-
catch { }
|
|
2285
|
-
return input;
|
|
2286
|
-
}
|
|
2287
|
-
async function performResetRoot(newComponent) {
|
|
2288
|
-
const tStart = Date.now();
|
|
2289
|
-
// Guard against re-entrant or rapid successive resets
|
|
2290
|
-
try {
|
|
2291
|
-
globalThis.__NS_DEV_RESET_IN_PROGRESS__ = true;
|
|
2292
|
-
}
|
|
2293
|
-
catch { }
|
|
2294
|
-
try {
|
|
2295
|
-
ensureCoreAliasesOnGlobalThis();
|
|
2296
|
-
}
|
|
2297
|
-
catch { }
|
|
2298
|
-
if (VERBOSE) {
|
|
2299
|
-
console.log('[hmr-client] Single-path: replace current root Page');
|
|
2300
|
-
console.log('[hmr-client] Component details:', {
|
|
2301
|
-
componentName: newComponent?.__name,
|
|
2302
|
-
componentFile: newComponent?.__file,
|
|
2303
|
-
hasRender: !!newComponent?.render,
|
|
2304
|
-
hasSetup: !!newComponent?.setup,
|
|
2305
|
-
componentKeys: Object.keys(newComponent || {}),
|
|
2306
|
-
});
|
|
2307
|
-
}
|
|
2308
|
-
function createRootFactory() {
|
|
2309
|
-
let rootKind = 'page';
|
|
2310
|
-
let cachedRoot = undefined;
|
|
2311
|
-
const state = {
|
|
2312
|
-
getRootKind: () => rootKind,
|
|
2313
|
-
setRootKind: (k) => {
|
|
2314
|
-
rootKind = k;
|
|
2315
|
-
},
|
|
2316
|
-
getCachedRoot: () => cachedRoot,
|
|
2317
|
-
setCachedRoot: (r) => {
|
|
2318
|
-
cachedRoot = r;
|
|
2319
|
-
},
|
|
2320
|
-
};
|
|
2321
|
-
const factory = {
|
|
2322
|
-
get kind() {
|
|
2323
|
-
return rootKind;
|
|
2324
|
-
},
|
|
2325
|
-
create: () => {
|
|
2326
|
-
if (cachedRoot)
|
|
2327
|
-
return cachedRoot;
|
|
2328
|
-
try {
|
|
2329
|
-
if (CLIENT_STRATEGY?.createRoot) {
|
|
2330
|
-
cachedRoot = CLIENT_STRATEGY.createRoot(newComponent, state);
|
|
2331
|
-
}
|
|
2332
|
-
else if (TS_LIKE_FLAVOR) {
|
|
2333
|
-
// For TS flavor, treat the component as a factory or direct NS view.
|
|
2334
|
-
let root = null;
|
|
2335
|
-
try {
|
|
2336
|
-
if (typeof newComponent === 'function') {
|
|
2337
|
-
root = newComponent();
|
|
2338
|
-
}
|
|
2339
|
-
else {
|
|
2340
|
-
root = newComponent;
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
catch (e) {
|
|
2344
|
-
console.warn('[hmr-client][ts] root factory invocation failed', e);
|
|
2345
|
-
}
|
|
2346
|
-
cachedRoot = root || {};
|
|
2347
|
-
// Heuristic: if the root "looks" like a Frame, prefer frame semantics
|
|
2348
|
-
try {
|
|
2349
|
-
const name = String(cachedRoot?.constructor?.name || '').replace(/^_+/, '');
|
|
2350
|
-
if (/^Frame(\$\d+)?$/.test(name)) {
|
|
2351
|
-
rootKind = 'frame';
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
catch { }
|
|
2355
|
-
}
|
|
2356
|
-
return cachedRoot;
|
|
2357
|
-
}
|
|
2358
|
-
catch (e) {
|
|
2359
|
-
console.warn('[hmr-client] createRootFactory failed', e);
|
|
2360
|
-
if (VERBOSE)
|
|
2361
|
-
console.warn('[hmr-client] [createRoot] stack', e?.stack);
|
|
2362
|
-
const GPage = getCore('Page');
|
|
2363
|
-
rootKind = 'page';
|
|
2364
|
-
cachedRoot = GPage ? new GPage() : {};
|
|
2365
|
-
return cachedRoot;
|
|
2366
|
-
}
|
|
2367
|
-
},
|
|
2368
|
-
};
|
|
2369
|
-
return factory;
|
|
2370
|
-
}
|
|
2371
|
-
// Android readiness before any root changes
|
|
2372
|
-
const App = getCore('Application') || getGlobalScope().Application;
|
|
2373
|
-
const isAndroid = !!(App && App.android !== undefined);
|
|
2374
|
-
if (isAndroid) {
|
|
2375
|
-
const isReady = () => {
|
|
2376
|
-
try {
|
|
2377
|
-
const a = App.android;
|
|
2378
|
-
const act = a && (a.foregroundActivity || a.startActivity);
|
|
2379
|
-
return !!act;
|
|
2380
|
-
}
|
|
2381
|
-
catch {
|
|
2382
|
-
return false;
|
|
2383
|
-
}
|
|
2384
|
-
};
|
|
2385
|
-
const waitFor = async (pred, ms = 6000, step = 60) => {
|
|
2386
|
-
const start = Date.now();
|
|
2387
|
-
while (Date.now() - start < ms) {
|
|
2388
|
-
if (pred())
|
|
2389
|
-
return true;
|
|
2390
|
-
await new Promise((r) => setTimeout(r, step));
|
|
2391
|
-
}
|
|
2392
|
-
return pred();
|
|
2393
|
-
};
|
|
2394
|
-
const withEvents = async (ms = 6000) => {
|
|
2395
|
-
try {
|
|
2396
|
-
const a = App.android;
|
|
2397
|
-
if (!a || typeof a.on !== 'function' || typeof a.off !== 'function') {
|
|
2398
|
-
if (VERBOSE)
|
|
2399
|
-
console.log('[hmr-client] [android] events API not available; polling');
|
|
2400
|
-
return await waitFor(isReady, ms, 60);
|
|
2401
|
-
}
|
|
2402
|
-
if (isReady()) {
|
|
2403
|
-
if (VERBOSE)
|
|
2404
|
-
console.log('[hmr-client] [android] activity already ready');
|
|
2405
|
-
return true;
|
|
2406
|
-
}
|
|
2407
|
-
return await new Promise((resolve) => {
|
|
2408
|
-
let done = false;
|
|
2409
|
-
const tWait = Date.now();
|
|
2410
|
-
const finish = (ok) => {
|
|
2411
|
-
if (done)
|
|
2412
|
-
return;
|
|
2413
|
-
done = true;
|
|
2414
|
-
try {
|
|
2415
|
-
a.off(a.activityStartedEvent || 'activityStarted', onAny);
|
|
2416
|
-
a.off(a.activityResumedEvent || 'activityResumed', onAny);
|
|
2417
|
-
}
|
|
2418
|
-
catch { }
|
|
2419
|
-
if (VERBOSE)
|
|
2420
|
-
console.log('[hmr-client] [android] readiness result', {
|
|
2421
|
-
ok,
|
|
2422
|
-
elapsedMs: Date.now() - tWait,
|
|
2423
|
-
});
|
|
2424
|
-
resolve(ok);
|
|
2425
|
-
};
|
|
2426
|
-
const onAny = (evt) => {
|
|
2427
|
-
if (VERBOSE)
|
|
2428
|
-
console.log('[hmr-client] [android] activity event', {
|
|
2429
|
-
type: evt?.eventName || 'unknown',
|
|
2430
|
-
});
|
|
2431
|
-
if (isReady())
|
|
2432
|
-
finish(true);
|
|
2433
|
-
};
|
|
2434
|
-
try {
|
|
2435
|
-
a.on(a.activityStartedEvent || 'activityStarted', onAny);
|
|
2436
|
-
a.on(a.activityResumedEvent || 'activityResumed', onAny);
|
|
2437
|
-
if (VERBOSE)
|
|
2438
|
-
console.log('[hmr-client] [android] waiting for activity events…');
|
|
2439
|
-
}
|
|
2440
|
-
catch {
|
|
2441
|
-
return resolve(waitFor(isReady, ms, 60));
|
|
2442
|
-
}
|
|
2443
|
-
setTimeout(() => finish(isReady()), ms);
|
|
2444
|
-
});
|
|
2445
|
-
}
|
|
2446
|
-
catch {
|
|
2447
|
-
return await waitFor(isReady, ms, 60);
|
|
2448
|
-
}
|
|
2449
|
-
};
|
|
2450
|
-
const ok = await withEvents(6000);
|
|
2451
|
-
if (!ok && VERBOSE)
|
|
2452
|
-
console.warn('[hmr-client] [android] activity not ready; proceeding with best-effort');
|
|
2453
|
-
}
|
|
2454
|
-
// Create the new root first to determine whether it is a Page or a Frame
|
|
2455
|
-
const rootFactory = createRootFactory();
|
|
2456
|
-
const preparedRoot = rootFactory.create();
|
|
2457
|
-
const isFrameRoot = (() => {
|
|
2458
|
-
try {
|
|
2459
|
-
const name = String(preparedRoot?.constructor?.name || '').replace(/^_+/, '');
|
|
2460
|
-
return /^Frame(\$\d+)?$/.test(name);
|
|
2461
|
-
}
|
|
2462
|
-
catch {
|
|
2463
|
-
return false;
|
|
2464
|
-
}
|
|
2465
|
-
})();
|
|
2466
|
-
// Prefer resetRootView on initial boot (placeholder present) or on iOS to avoid early createRootView races
|
|
2467
|
-
let hadPlaceholder = false;
|
|
2468
|
-
let isIOS = false;
|
|
2469
|
-
try {
|
|
2470
|
-
hadPlaceholder = !!globalThis.__NS_DEV_PLACEHOLDER_ROOT_EARLY__;
|
|
2471
|
-
}
|
|
2472
|
-
catch { }
|
|
2473
|
-
try {
|
|
2474
|
-
const AppAny = getCore('Application') || getGlobalScope().Application;
|
|
2475
|
-
isIOS = !!(AppAny && AppAny.ios !== undefined);
|
|
2476
|
-
}
|
|
2477
|
-
catch { }
|
|
2478
|
-
// Preferred policy:
|
|
2479
|
-
// - If this is the very first mount (placeholder present), or if the new root is a Frame,
|
|
2480
|
-
// perform a full root replacement via resetRootView so we never couple with the placeholder.
|
|
2481
|
-
// - Otherwise (subsequent HMR updates with an authoritative Frame already in place), re-use the
|
|
2482
|
-
// current app Frame and navigate to the new Page. This avoids a brief flash that can occur
|
|
2483
|
-
// when swapping the entire root view on Android. The placeholder is never involved here.
|
|
2484
|
-
const gAnyForPolicy = getGlobalScope();
|
|
2485
|
-
const placeholderFrame = (() => {
|
|
2486
|
-
try {
|
|
2487
|
-
return gAnyForPolicy.__NS_DEV_PLACEHOLDER_ROOT_VIEW__ || null;
|
|
2488
|
-
}
|
|
2489
|
-
catch {
|
|
2490
|
-
return null;
|
|
2491
|
-
}
|
|
2492
|
-
})();
|
|
2493
|
-
let existingAppFrame = null;
|
|
2494
|
-
try {
|
|
2495
|
-
existingAppFrame = getRootFrame() || getCore('Frame')?.topmost?.() || null;
|
|
2496
|
-
}
|
|
2497
|
-
catch { }
|
|
2498
|
-
const isAuthoritativeFrame = !!existingAppFrame && existingAppFrame !== placeholderFrame;
|
|
2499
|
-
// Vue: skip the in-place navigate path. After `app.mount(NSVRoot)` in getRootForVue the
|
|
2500
|
-
// new Page already has a parent (the freshly-constructed NSVRoot), so an attempt to navigate
|
|
2501
|
-
// the existing app Frame to that same Page completes silently without ever rebinding the
|
|
2502
|
-
// page to the Frame — the screen keeps showing the previous render. resetRootView with a
|
|
2503
|
-
// fresh Frame correctly reparents the Page and is the proven path that produces visible
|
|
2504
|
-
// in-place updates for SFC HMR cycles. Non-Vue flavors keep the legacy navigate fast path.
|
|
2505
|
-
const allowNavigateFastPath = CLIENT_STRATEGY?.allowNavigateFastPath ?? true;
|
|
2506
|
-
if (allowNavigateFastPath && !hadPlaceholder && !isFrameRoot && isAuthoritativeFrame && typeof existingAppFrame.navigate === 'function') {
|
|
2507
|
-
try {
|
|
2508
|
-
const navEntry = {
|
|
2509
|
-
create: () => preparedRoot,
|
|
2510
|
-
clearHistory: true,
|
|
2511
|
-
animated: false,
|
|
2512
|
-
};
|
|
2513
|
-
existingAppFrame.navigate(navEntry);
|
|
2514
|
-
setRootFrame(existingAppFrame);
|
|
2515
|
-
return true;
|
|
2516
|
-
}
|
|
2517
|
-
catch (e) {
|
|
2518
|
-
console.warn('[hmr-client] authoritative navigate fallback to resetRootView due to error', e);
|
|
2519
|
-
}
|
|
2520
|
-
}
|
|
2521
|
-
if (VERBOSE)
|
|
2522
|
-
console.log('[hmr-client] full root replacement via resetRootView (placeholder will be discarded)', { isFrameRoot, isIOS, hadPlaceholder });
|
|
2523
|
-
// Fallback or preferred path: resetRootView with a creator that builds a fresh Frame and navigates to the new Page
|
|
2524
|
-
try {
|
|
2525
|
-
const App2 = getCore('Application') || getGlobalScope().Application;
|
|
2526
|
-
if (!App2 || typeof App2.resetRootView !== 'function') {
|
|
2527
|
-
console.warn('[hmr-client] Application.resetRootView unavailable');
|
|
2528
|
-
return false;
|
|
2529
|
-
}
|
|
2530
|
-
// iOS-specific: if the cached window was left in an invalid state (boolean false), clear it so
|
|
2531
|
-
// setWindowContent can recreate a proper UIWindow. This can occur in rare early-boot races.
|
|
2532
|
-
try {
|
|
2533
|
-
const isIos = !!(App2 && App2.ios !== undefined);
|
|
2534
|
-
if (isIos) {
|
|
2535
|
-
let win = undefined;
|
|
2536
|
-
try {
|
|
2537
|
-
win = App2.window;
|
|
2538
|
-
}
|
|
2539
|
-
catch { }
|
|
2540
|
-
if (win === false) {
|
|
2541
|
-
if (VERBOSE)
|
|
2542
|
-
console.warn('[hmr-client] iOS Application.window is boolean false; attempting to clear cached window');
|
|
2543
|
-
try {
|
|
2544
|
-
const g = getGlobalScope();
|
|
2545
|
-
const reg = g.__nsVendorRegistry;
|
|
2546
|
-
const req = reg?.get ? g.__nsVendorRequire || g.__nsRequire || g.require : g.__nsRequire || g.require;
|
|
2547
|
-
let helpers = null;
|
|
2548
|
-
// Try vendor registry first
|
|
2549
|
-
if (reg && reg.has('@nativescript/core/application/helpers-common')) {
|
|
2550
|
-
helpers = reg.get('@nativescript/core/application/helpers-common');
|
|
2551
|
-
}
|
|
2552
|
-
// Fallback to device require
|
|
2553
|
-
if (!helpers && typeof req === 'function') {
|
|
2554
|
-
try {
|
|
2555
|
-
helpers = req('@nativescript/core/application/helpers-common');
|
|
2556
|
-
}
|
|
2557
|
-
catch { }
|
|
2558
|
-
}
|
|
2559
|
-
const setter = helpers && (helpers.setiOSWindow || helpers.default?.setiOSWindow);
|
|
2560
|
-
if (typeof setter === 'function') {
|
|
2561
|
-
try {
|
|
2562
|
-
setter(undefined);
|
|
2563
|
-
if (VERBOSE)
|
|
2564
|
-
console.log('[hmr-client] cleared cached iOS window via helpers-common');
|
|
2565
|
-
}
|
|
2566
|
-
catch { }
|
|
2567
|
-
}
|
|
2568
|
-
}
|
|
2569
|
-
catch { }
|
|
2570
|
-
}
|
|
2571
|
-
}
|
|
2572
|
-
}
|
|
2573
|
-
catch { }
|
|
2574
|
-
// Use the previously created preparedRoot
|
|
2575
|
-
const entry = {
|
|
2576
|
-
create: () => {
|
|
2577
|
-
if (!isFrameRoot) {
|
|
2578
|
-
const F = getCore('Frame');
|
|
2579
|
-
const fr = new F();
|
|
2580
|
-
if (VERBOSE)
|
|
2581
|
-
console.log('[hmr-client] resetRootView:create new Frame', {
|
|
2582
|
-
type: fr?.constructor?.name,
|
|
2583
|
-
});
|
|
2584
|
-
const navEntry = {
|
|
2585
|
-
create: () => preparedRoot,
|
|
2586
|
-
clearHistory: true,
|
|
2587
|
-
animated: false,
|
|
2588
|
-
};
|
|
2589
|
-
try {
|
|
2590
|
-
fr.navigate(navEntry);
|
|
2591
|
-
}
|
|
2592
|
-
catch (e) {
|
|
2593
|
-
console.warn('[hmr-client] resetRootView:create navigate failed', e);
|
|
2594
|
-
}
|
|
2595
|
-
setRootFrame(fr);
|
|
2596
|
-
return fr;
|
|
2597
|
-
}
|
|
2598
|
-
else {
|
|
2599
|
-
const fr = preparedRoot;
|
|
2600
|
-
setRootFrame(fr);
|
|
2601
|
-
return fr;
|
|
2602
|
-
}
|
|
2603
|
-
},
|
|
2604
|
-
};
|
|
2605
|
-
// Always use an entry with a create() function to avoid cross‑realm instanceof checks on Android.
|
|
2606
|
-
App2.resetRootView(entry);
|
|
2607
|
-
// After authoritative reset, it's safe to detach the early placeholder launch handler
|
|
2608
|
-
try {
|
|
2609
|
-
const restore = globalThis.__NS_DEV_RESTORE_PLACEHOLDER__;
|
|
2610
|
-
if (typeof restore === 'function') {
|
|
2611
|
-
restore();
|
|
2612
|
-
}
|
|
2613
|
-
}
|
|
2614
|
-
catch { }
|
|
2615
|
-
return true;
|
|
2616
|
-
}
|
|
2617
|
-
catch (e) {
|
|
2618
|
-
console.warn('[hmr-client] resetRootView failed', e);
|
|
2619
|
-
try {
|
|
2620
|
-
if (VERBOSE)
|
|
2621
|
-
console.warn('[hmr-client] resetRootView stack', e?.stack);
|
|
2622
|
-
}
|
|
2623
|
-
catch { }
|
|
2624
|
-
return false;
|
|
2625
|
-
}
|
|
2626
|
-
finally {
|
|
2627
|
-
try {
|
|
2628
|
-
globalThis.__NS_DEV_RESET_IN_PROGRESS__ = false;
|
|
2629
|
-
}
|
|
2630
|
-
catch { }
|
|
2631
|
-
}
|
|
2632
|
-
}
|
|
97
|
+
// Route incoming socket messages through the protocol dispatcher.
|
|
98
|
+
setHmrMessageHandler(handleHmrMessage);
|
|
2633
99
|
export function initHmrClient(opts) {
|
|
2634
100
|
if (opts?.wsUrl) {
|
|
2635
101
|
setHMRWsUrl(opts.wsUrl);
|
|
@@ -2645,20 +111,6 @@ export function initHmrClient(opts) {
|
|
|
2645
111
|
}
|
|
2646
112
|
g.__NS_HMR_CLIENT_ACTIVE__ = true;
|
|
2647
113
|
ensureCoreAliasesOnGlobalThis();
|
|
2648
|
-
// XML flavor: record string-module modals from the moment the client is up
|
|
2649
|
-
// so an already-open modal can be re-presented when its files change.
|
|
2650
|
-
// Installed at init (not first-update time) because the wrap can only
|
|
2651
|
-
// observe showModal calls made AFTER it lands. Retried briefly because
|
|
2652
|
-
// getCore('View') may not resolve until the vendor realm finishes booting.
|
|
2653
|
-
if (TS_LIKE_FLAVOR) {
|
|
2654
|
-
const tryInstallModalTracking = (attempts) => {
|
|
2655
|
-
ensureModalTracking();
|
|
2656
|
-
if (!modalTrackingInstalled && attempts > 0) {
|
|
2657
|
-
setTimeout(() => tryInstallModalTracking(attempts - 1), 250);
|
|
2658
|
-
}
|
|
2659
|
-
};
|
|
2660
|
-
tryInstallModalTracking(40);
|
|
2661
|
-
}
|
|
2662
114
|
// Defer WebSocket connection until boot completes to avoid native V8 crashes
|
|
2663
115
|
// caused by concurrent WebSocket message handling + HTTP fetch during early startup.
|
|
2664
116
|
// The WebSocket is only needed for HMR updates, not the initial boot sequence.
|
|
@@ -2682,7 +134,7 @@ export function initHmrClient(opts) {
|
|
|
2682
134
|
}
|
|
2683
135
|
// Best-effort: install back wrapper even before first remount; original root may be captured later.
|
|
2684
136
|
// Deferred until the dynamically-imported strategy resolves.
|
|
2685
|
-
void CLIENT_STRATEGY_READY.then(() =>
|
|
137
|
+
void CLIENT_STRATEGY_READY.then(() => getClientStrategy()?.installBackWrapper?.(performResetRoot, getCore));
|
|
2686
138
|
}
|
|
2687
139
|
export default function startViteHMR(opts) {
|
|
2688
140
|
if (VERBOSE)
|