@nativescript/vite 8.0.0-alpha.1 → 8.0.0-alpha.11
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.d.ts +1 -1
- package/configuration/angular.js +486 -140
- package/configuration/angular.js.map +1 -1
- package/configuration/base.js +159 -29
- package/configuration/base.js.map +1 -1
- package/configuration/javascript.js +3 -3
- package/configuration/javascript.js.map +1 -1
- package/configuration/solid.js +27 -0
- package/configuration/solid.js.map +1 -1
- package/configuration/typescript.js +4 -4
- package/configuration/typescript.js.map +1 -1
- package/helpers/angular/angular-linker.js +38 -42
- package/helpers/angular/angular-linker.js.map +1 -1
- package/helpers/angular/inject-component-hmr-registration.d.ts +112 -0
- package/helpers/angular/inject-component-hmr-registration.js +359 -0
- package/helpers/angular/inject-component-hmr-registration.js.map +1 -0
- package/helpers/angular/inline-decorator-component-templates.d.ts +3 -0
- package/helpers/angular/inline-decorator-component-templates.js +400 -0
- package/helpers/angular/inline-decorator-component-templates.js.map +1 -0
- package/helpers/angular/shared-linker.d.ts +7 -0
- package/helpers/angular/shared-linker.js +37 -1
- package/helpers/angular/shared-linker.js.map +1 -1
- package/helpers/angular/synthesize-decorator-ctor-parameters.d.ts +1 -0
- package/helpers/angular/synthesize-decorator-ctor-parameters.js +256 -0
- package/helpers/angular/synthesize-decorator-ctor-parameters.js.map +1 -0
- package/helpers/angular/synthesize-injectable-factories.d.ts +3 -0
- package/helpers/angular/synthesize-injectable-factories.js +414 -0
- package/helpers/angular/synthesize-injectable-factories.js.map +1 -0
- package/helpers/angular/util.d.ts +1 -0
- package/helpers/angular/util.js +88 -0
- package/helpers/angular/util.js.map +1 -1
- package/helpers/commonjs-plugins.d.ts +5 -2
- package/helpers/commonjs-plugins.js +126 -0
- package/helpers/commonjs-plugins.js.map +1 -1
- package/helpers/config-as-json.js +10 -0
- package/helpers/config-as-json.js.map +1 -1
- package/helpers/esbuild-platform-resolver.js +5 -5
- package/helpers/esbuild-platform-resolver.js.map +1 -1
- package/helpers/external-configs.d.ts +9 -1
- package/helpers/external-configs.js +31 -6
- package/helpers/external-configs.js.map +1 -1
- package/helpers/global-defines.d.ts +51 -0
- package/helpers/global-defines.js +77 -0
- package/helpers/global-defines.js.map +1 -1
- package/helpers/import-meta-path.d.ts +4 -0
- package/helpers/import-meta-path.js +5 -0
- package/helpers/import-meta-path.js.map +1 -0
- package/helpers/import-specifier.d.ts +1 -0
- package/helpers/import-specifier.js +18 -0
- package/helpers/import-specifier.js.map +1 -0
- package/helpers/logging.d.ts +1 -0
- package/helpers/logging.js +63 -3
- package/helpers/logging.js.map +1 -1
- package/helpers/main-entry.d.ts +5 -2
- package/helpers/main-entry.js +375 -116
- package/helpers/main-entry.js.map +1 -1
- package/helpers/nativeclass-transform.js +8 -127
- package/helpers/nativeclass-transform.js.map +1 -1
- package/helpers/nativeclass-transformer-plugin.d.ts +19 -1
- package/helpers/nativeclass-transformer-plugin.js +318 -36
- package/helpers/nativeclass-transformer-plugin.js.map +1 -1
- package/helpers/ns-core-url.d.ts +83 -0
- package/helpers/ns-core-url.js +167 -0
- package/helpers/ns-core-url.js.map +1 -0
- package/helpers/prelink-angular.js +1 -4
- package/helpers/prelink-angular.js.map +1 -1
- package/helpers/project.d.ts +35 -0
- package/helpers/project.js +120 -2
- package/helpers/project.js.map +1 -1
- package/helpers/ts-config-paths.js +50 -2
- package/helpers/ts-config-paths.js.map +1 -1
- package/helpers/workers.d.ts +20 -19
- package/helpers/workers.js +620 -3
- package/helpers/workers.js.map +1 -1
- package/hmr/client/css-handler.js +60 -19
- package/hmr/client/css-handler.js.map +1 -1
- package/hmr/client/hmr-pending-overlay.d.ts +27 -0
- package/hmr/client/hmr-pending-overlay.js +50 -0
- package/hmr/client/hmr-pending-overlay.js.map +1 -0
- package/hmr/client/index.js +767 -24
- package/hmr/client/index.js.map +1 -1
- package/hmr/client/utils.d.ts +5 -0
- package/hmr/client/utils.js +283 -12
- package/hmr/client/utils.js.map +1 -1
- package/hmr/entry-runtime.d.ts +10 -0
- package/hmr/entry-runtime.js +330 -42
- package/hmr/entry-runtime.js.map +1 -1
- package/hmr/frameworks/angular/client/index.d.ts +3 -1
- package/hmr/frameworks/angular/client/index.js +821 -25
- package/hmr/frameworks/angular/client/index.js.map +1 -1
- package/hmr/frameworks/angular/server/linker.js +37 -6
- package/hmr/frameworks/angular/server/linker.js.map +1 -1
- package/hmr/frameworks/angular/server/strategy.js +30 -6
- package/hmr/frameworks/angular/server/strategy.js.map +1 -1
- package/hmr/frameworks/typescript/server/strategy.js +8 -2
- package/hmr/frameworks/typescript/server/strategy.js.map +1 -1
- package/hmr/frameworks/vue/client/index.js +18 -42
- package/hmr/frameworks/vue/client/index.js.map +1 -1
- package/hmr/helpers/ast-normalizer.js +22 -10
- package/hmr/helpers/ast-normalizer.js.map +1 -1
- package/hmr/helpers/cjs-named-exports.d.ts +23 -0
- package/hmr/helpers/cjs-named-exports.js +152 -0
- package/hmr/helpers/cjs-named-exports.js.map +1 -0
- package/hmr/server/constants.d.ts +1 -0
- package/hmr/server/constants.js +14 -3
- package/hmr/server/constants.js.map +1 -1
- package/hmr/server/core-sanitize.d.ts +49 -2
- package/hmr/server/core-sanitize.js +267 -24
- package/hmr/server/core-sanitize.js.map +1 -1
- package/hmr/server/import-map.d.ts +65 -0
- package/hmr/server/import-map.js +222 -0
- package/hmr/server/import-map.js.map +1 -0
- package/hmr/server/index.d.ts +2 -1
- package/hmr/server/index.js.map +1 -1
- package/hmr/server/ns-core-cjs-shape.d.ts +204 -0
- package/hmr/server/ns-core-cjs-shape.js +271 -0
- package/hmr/server/ns-core-cjs-shape.js.map +1 -0
- package/hmr/server/perf-instrumentation.d.ts +114 -0
- package/hmr/server/perf-instrumentation.js +195 -0
- package/hmr/server/perf-instrumentation.js.map +1 -0
- package/hmr/server/runtime-graph-filter.d.ts +5 -0
- package/hmr/server/runtime-graph-filter.js +21 -0
- package/hmr/server/runtime-graph-filter.js.map +1 -0
- package/hmr/server/shared-transform-request.d.ts +12 -0
- package/hmr/server/shared-transform-request.js +144 -0
- package/hmr/server/shared-transform-request.js.map +1 -0
- package/hmr/server/vite-plugin.d.ts +21 -1
- package/hmr/server/vite-plugin.js +461 -22
- package/hmr/server/vite-plugin.js.map +1 -1
- package/hmr/server/websocket-angular-entry.d.ts +2 -0
- package/hmr/server/websocket-angular-entry.js +68 -0
- package/hmr/server/websocket-angular-entry.js.map +1 -0
- package/hmr/server/websocket-angular-hot-update.d.ts +78 -0
- package/hmr/server/websocket-angular-hot-update.js +413 -0
- package/hmr/server/websocket-angular-hot-update.js.map +1 -0
- package/hmr/server/websocket-core-bridge.d.ts +21 -0
- package/hmr/server/websocket-core-bridge.js +357 -0
- package/hmr/server/websocket-core-bridge.js.map +1 -0
- package/hmr/server/websocket-graph-upsert.d.ts +21 -0
- package/hmr/server/websocket-graph-upsert.js +33 -0
- package/hmr/server/websocket-graph-upsert.js.map +1 -0
- package/hmr/server/websocket-hmr-pending.d.ts +43 -0
- package/hmr/server/websocket-hmr-pending.js +55 -0
- package/hmr/server/websocket-hmr-pending.js.map +1 -0
- package/hmr/server/websocket-module-bindings.d.ts +6 -0
- package/hmr/server/websocket-module-bindings.js +471 -0
- package/hmr/server/websocket-module-bindings.js.map +1 -0
- package/hmr/server/websocket-module-specifiers.d.ts +101 -0
- package/hmr/server/websocket-module-specifiers.js +820 -0
- package/hmr/server/websocket-module-specifiers.js.map +1 -0
- package/hmr/server/websocket-ns-m-finalize.d.ts +22 -0
- package/hmr/server/websocket-ns-m-finalize.js +88 -0
- package/hmr/server/websocket-ns-m-finalize.js.map +1 -0
- package/hmr/server/websocket-ns-m-paths.d.ts +3 -0
- package/hmr/server/websocket-ns-m-paths.js +92 -0
- package/hmr/server/websocket-ns-m-paths.js.map +1 -0
- package/hmr/server/websocket-ns-m-request.d.ts +45 -0
- package/hmr/server/websocket-ns-m-request.js +196 -0
- package/hmr/server/websocket-ns-m-request.js.map +1 -0
- package/hmr/server/websocket-runtime-compat.d.ts +19 -0
- package/hmr/server/websocket-runtime-compat.js +287 -0
- package/hmr/server/websocket-runtime-compat.js.map +1 -0
- package/hmr/server/websocket-served-module-helpers.d.ts +36 -0
- package/hmr/server/websocket-served-module-helpers.js +631 -0
- package/hmr/server/websocket-served-module-helpers.js.map +1 -0
- package/hmr/server/websocket-txn.d.ts +6 -0
- package/hmr/server/websocket-txn.js +45 -0
- package/hmr/server/websocket-txn.js.map +1 -0
- package/hmr/server/websocket-vendor-unifier.d.ts +10 -0
- package/hmr/server/websocket-vendor-unifier.js +51 -0
- package/hmr/server/websocket-vendor-unifier.js.map +1 -0
- package/hmr/server/websocket-vue-sfc.d.ts +27 -0
- package/hmr/server/websocket-vue-sfc.js +1069 -0
- package/hmr/server/websocket-vue-sfc.js.map +1 -0
- package/hmr/server/websocket.d.ts +26 -3
- package/hmr/server/websocket.js +2492 -798
- package/hmr/server/websocket.js.map +1 -1
- package/hmr/shared/package-classifier.d.ts +9 -0
- package/hmr/shared/package-classifier.js +58 -0
- package/hmr/shared/package-classifier.js.map +1 -0
- package/hmr/shared/runtime/boot-timeline.d.ts +17 -0
- package/hmr/shared/runtime/boot-timeline.js +51 -0
- package/hmr/shared/runtime/boot-timeline.js.map +1 -0
- package/hmr/shared/runtime/browser-runtime-contract.d.ts +64 -0
- package/hmr/shared/runtime/browser-runtime-contract.js +54 -0
- package/hmr/shared/runtime/browser-runtime-contract.js.map +1 -0
- package/hmr/shared/runtime/dev-overlay.d.ts +85 -0
- package/hmr/shared/runtime/dev-overlay.js +1236 -0
- package/hmr/shared/runtime/dev-overlay.js.map +1 -0
- package/hmr/shared/runtime/http-only-boot.d.ts +1 -0
- package/hmr/shared/runtime/http-only-boot.js +53 -6
- package/hmr/shared/runtime/http-only-boot.js.map +1 -1
- package/hmr/shared/runtime/module-provenance.d.ts +1 -0
- package/hmr/shared/runtime/module-provenance.js +63 -0
- package/hmr/shared/runtime/module-provenance.js.map +1 -0
- package/hmr/shared/runtime/platform-polyfills.d.ts +26 -0
- package/hmr/shared/runtime/platform-polyfills.js +122 -0
- package/hmr/shared/runtime/platform-polyfills.js.map +1 -0
- package/hmr/shared/runtime/root-placeholder.d.ts +1 -0
- package/hmr/shared/runtime/root-placeholder.js +552 -82
- package/hmr/shared/runtime/root-placeholder.js.map +1 -1
- package/hmr/shared/runtime/session-bootstrap.d.ts +1 -0
- package/hmr/shared/runtime/session-bootstrap.js +195 -0
- package/hmr/shared/runtime/session-bootstrap.js.map +1 -0
- package/hmr/shared/runtime/vendor-bootstrap.js +52 -15
- package/hmr/shared/runtime/vendor-bootstrap.js.map +1 -1
- package/hmr/shared/vendor/manifest.d.ts +37 -0
- package/hmr/shared/vendor/manifest.js +677 -57
- package/hmr/shared/vendor/manifest.js.map +1 -1
- package/hmr/shared/vendor/registry.js +104 -7
- package/hmr/shared/vendor/registry.js.map +1 -1
- package/index.d.ts +1 -0
- package/index.js +5 -0
- package/index.js.map +1 -1
- package/package.json +14 -2
- package/runtime/core-aliases-early.js +94 -67
- package/runtime/core-aliases-early.js.map +1 -1
- package/shims/solid-jsx-runtime.d.ts +7 -0
- package/shims/solid-jsx-runtime.js +17 -0
- package/shims/solid-jsx-runtime.js.map +1 -0
package/hmr/client/index.js
CHANGED
|
@@ -5,9 +5,41 @@
|
|
|
5
5
|
* Always resolve core classes and Application from the vendor realm or globalThis at runtime.
|
|
6
6
|
* The HMR client is evaluated via HTTP ESM on device; static imports would create secondary instances.
|
|
7
7
|
*/
|
|
8
|
-
import { setHMRWsUrl, getHMRWsUrl, pendingModuleFetches, deriveHttpOrigin, setHttpOriginForVite, moduleFetchCache, requestModuleFromServer, getHttpOriginForVite, normalizeSpec, hmrMetrics, graph, setGraphVersion, getGraphVersion, getCurrentApp, getRootFrame, setCurrentApp, setRootFrame, getCore } from './utils.js';
|
|
8
|
+
import { setHMRWsUrl, getHMRWsUrl, pendingModuleFetches, deriveHttpOrigin, setHttpOriginForVite, moduleFetchCache, requestModuleFromServer, getHttpOriginForVite, normalizeSpec, hmrMetrics, graph, setGraphVersion, getGraphVersion, getCurrentApp, getRootFrame, setCurrentApp, setRootFrame, getCore, hasExplicitEviction, invalidateModulesByUrls, buildEvictionUrls, emitHmrModeBannerOnce } from './utils.js';
|
|
9
9
|
import { handleCssUpdates } from './css-handler.js';
|
|
10
10
|
const VERBOSE = typeof __NS_ENV_VERBOSE__ !== 'undefined' && __NS_ENV_VERBOSE__;
|
|
11
|
+
function resolveTargetFlavor() {
|
|
12
|
+
try {
|
|
13
|
+
if (typeof __NS_TARGET_FLAVOR__ !== 'undefined' && __NS_TARGET_FLAVOR__) {
|
|
14
|
+
return __NS_TARGET_FLAVOR__;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch { }
|
|
18
|
+
try {
|
|
19
|
+
const g = globalThis;
|
|
20
|
+
if (typeof g.__NS_TARGET_FLAVOR__ === 'string' && g.__NS_TARGET_FLAVOR__) {
|
|
21
|
+
return g.__NS_TARGET_FLAVOR__;
|
|
22
|
+
}
|
|
23
|
+
if (typeof g.__NS_HMR_BROWSER_RUNTIME_TARGET_FLAVOR__ === 'string' && g.__NS_HMR_BROWSER_RUNTIME_TARGET_FLAVOR__) {
|
|
24
|
+
return g.__NS_HMR_BROWSER_RUNTIME_TARGET_FLAVOR__;
|
|
25
|
+
}
|
|
26
|
+
if (typeof g.__reboot_ng_modules__ === 'function') {
|
|
27
|
+
return 'angular';
|
|
28
|
+
}
|
|
29
|
+
if (g.__VUE_HMR_RUNTIME__ || g.__NS_HMR_VUE_SFC_REGISTRY__) {
|
|
30
|
+
return 'vue';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const TARGET_FLAVOR = resolveTargetFlavor();
|
|
37
|
+
try {
|
|
38
|
+
if (TARGET_FLAVOR && !globalThis.__NS_TARGET_FLAVOR__) {
|
|
39
|
+
globalThis.__NS_TARGET_FLAVOR__ = TARGET_FLAVOR;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch { }
|
|
11
43
|
const APP_ROOT_VIRTUAL = typeof __NS_APP_ROOT_VIRTUAL__ === 'string' && __NS_APP_ROOT_VIRTUAL__ ? __NS_APP_ROOT_VIRTUAL__ : '/src';
|
|
12
44
|
const APP_VIRTUAL_WITH_SLASH = APP_ROOT_VIRTUAL.endsWith('/') ? APP_ROOT_VIRTUAL : `${APP_ROOT_VIRTUAL}/`;
|
|
13
45
|
const APP_MAIN_ENTRY_SPEC = `${APP_VIRTUAL_WITH_SLASH}app.ts`;
|
|
@@ -29,8 +61,9 @@ function ensureCoreAliasesOnGlobalThis() {
|
|
|
29
61
|
}
|
|
30
62
|
catch { }
|
|
31
63
|
try {
|
|
32
|
-
if (A && !g.Application)
|
|
64
|
+
if (A && (!g.Application || typeof g.Application.run !== 'function' || typeof g.Application.on !== 'function' || typeof g.Application.resetRootView !== 'function')) {
|
|
33
65
|
g.Application = A;
|
|
66
|
+
}
|
|
34
67
|
}
|
|
35
68
|
catch { }
|
|
36
69
|
try {
|
|
@@ -43,12 +76,151 @@ function ensureCoreAliasesOnGlobalThis() {
|
|
|
43
76
|
}
|
|
44
77
|
// Apply once on module evaluation
|
|
45
78
|
ensureCoreAliasesOnGlobalThis();
|
|
79
|
+
function getHmrOverlayApi() {
|
|
80
|
+
try {
|
|
81
|
+
return globalThis.__NS_HMR_DEV_OVERLAY__ || null;
|
|
82
|
+
}
|
|
83
|
+
catch { }
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
function setConnectionOverlayStage(stage, detail) {
|
|
87
|
+
try {
|
|
88
|
+
const api = getHmrOverlayApi();
|
|
89
|
+
if (api && typeof api.setConnectionStage === 'function') {
|
|
90
|
+
api.setConnectionStage(stage, { detail });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
94
|
+
}
|
|
95
|
+
function hideConnectionOverlay() {
|
|
96
|
+
try {
|
|
97
|
+
const api = getHmrOverlayApi();
|
|
98
|
+
if (api && typeof api.hide === 'function') {
|
|
99
|
+
api.hide('healthy');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch { }
|
|
103
|
+
}
|
|
104
|
+
function setUpdateOverlayStage(stage, info) {
|
|
105
|
+
try {
|
|
106
|
+
const api = getHmrOverlayApi();
|
|
107
|
+
if (api && typeof api.setUpdateStage === 'function') {
|
|
108
|
+
api.setUpdateStage(stage, info);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch { }
|
|
112
|
+
}
|
|
113
|
+
// Store the listener registry on globalThis (rather than in a module-private
|
|
114
|
+
// closure) because in NativeScript the HMR client module and the user app
|
|
115
|
+
// modules can resolve to different module instances depending on how the
|
|
116
|
+
// dev runtime loads them (HTTP client URL vs. the bundled vendor realm).
|
|
117
|
+
// A module-local Set would not be shared across instances; the global one
|
|
118
|
+
// is.
|
|
119
|
+
function getNsSolidHmrListenerSet() {
|
|
120
|
+
const g = globalThis;
|
|
121
|
+
let set = g.__ns_solid_hmr_listener_set;
|
|
122
|
+
if (!set) {
|
|
123
|
+
set = new Set();
|
|
124
|
+
g.__ns_solid_hmr_listener_set = set;
|
|
125
|
+
}
|
|
126
|
+
return set;
|
|
127
|
+
}
|
|
128
|
+
function nsSolidHmrSubscribe(fn) {
|
|
129
|
+
const listeners = getNsSolidHmrListenerSet();
|
|
130
|
+
listeners.add(fn);
|
|
131
|
+
if (VERBOSE)
|
|
132
|
+
console.log('[hmr][solid] subscribe — listeners=', listeners.size);
|
|
133
|
+
return () => listeners.delete(fn);
|
|
134
|
+
}
|
|
135
|
+
function nsSolidHmrEmit(ev) {
|
|
136
|
+
const listeners = getNsSolidHmrListenerSet();
|
|
137
|
+
if (VERBOSE)
|
|
138
|
+
console.log('[hmr][solid] emit listeners=', listeners.size, 'changedFiles=', ev.changedFiles);
|
|
139
|
+
for (const fn of Array.from(listeners)) {
|
|
140
|
+
try {
|
|
141
|
+
fn(ev);
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
if (VERBOSE)
|
|
145
|
+
console.warn('[hmr][solid] listener threw', err);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const g = globalThis;
|
|
151
|
+
g.__ns_solid_hmr_subscribe = nsSolidHmrSubscribe;
|
|
152
|
+
// Eagerly create the listener set so the global exists at module load time.
|
|
153
|
+
getNsSolidHmrListenerSet();
|
|
154
|
+
if (VERBOSE)
|
|
155
|
+
console.log('[hmr][solid] HMR client loaded. global set=', typeof g.__ns_solid_hmr_subscribe, 'listenerSet=', typeof g.__ns_solid_hmr_listener_set);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
console.warn('[hmr][solid] could not install global __ns_solid_hmr_subscribe', err);
|
|
159
|
+
}
|
|
160
|
+
// Eagerly drive the HMR-applying overlay's 'received' frame as soon
|
|
161
|
+
// as the server emits `ns:hmr-pending`, BEFORE the framework-specific
|
|
162
|
+
// (`ns:angular-update` / `ns:css-updates`) payload arrives. The
|
|
163
|
+
// flavor-specific handler later walks through 'evicting' →
|
|
164
|
+
// 'reimporting' → 'rebooting' → 'complete'. Calling 'received' twice
|
|
165
|
+
// in the same cycle is safe: the overlay preserves
|
|
166
|
+
// `updateCycleStartedAt` when a 'received' frame replaces an existing
|
|
167
|
+
// 'received' frame so the minimum-visible window is still timed
|
|
168
|
+
// against the FIRST frame.
|
|
169
|
+
//
|
|
170
|
+
// Soft-fails when the overlay isn't installed (production builds,
|
|
171
|
+
// vitest, etc.) or when the user opted out via
|
|
172
|
+
// `__NS_HMR_PROGRESS_OVERLAY_ENABLED__ === false`.
|
|
173
|
+
import { applyHmrPendingFrame } from './hmr-pending-overlay.js';
|
|
174
|
+
function setHmrPendingOverlay(filePath) {
|
|
175
|
+
applyHmrPendingFrame(filePath, { getOverlay: getHmrOverlayApi });
|
|
176
|
+
}
|
|
177
|
+
let connectionOverlayTimer = null;
|
|
178
|
+
let connectionOverlayVisible = false;
|
|
179
|
+
let hasOpenedHmrSocket = false;
|
|
180
|
+
let awaitingHealthyHmrMessage = false;
|
|
181
|
+
let pendingConnectionOverlayStage = 'connecting';
|
|
182
|
+
let pendingConnectionOverlayDetail = '';
|
|
183
|
+
function clearConnectionOverlayTimer() {
|
|
184
|
+
if (connectionOverlayTimer) {
|
|
185
|
+
clearTimeout(connectionOverlayTimer);
|
|
186
|
+
connectionOverlayTimer = null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function showConnectionOverlayNow(stage, detail) {
|
|
190
|
+
pendingConnectionOverlayStage = stage;
|
|
191
|
+
pendingConnectionOverlayDetail = detail || '';
|
|
192
|
+
connectionOverlayVisible = true;
|
|
193
|
+
setConnectionOverlayStage(stage, detail);
|
|
194
|
+
}
|
|
195
|
+
function scheduleConnectionOverlay(stage, detail, delayMs = 1200) {
|
|
196
|
+
pendingConnectionOverlayStage = stage;
|
|
197
|
+
pendingConnectionOverlayDetail = detail || '';
|
|
198
|
+
clearConnectionOverlayTimer();
|
|
199
|
+
connectionOverlayTimer = setTimeout(() => {
|
|
200
|
+
showConnectionOverlayNow(pendingConnectionOverlayStage, pendingConnectionOverlayDetail);
|
|
201
|
+
}, delayMs);
|
|
202
|
+
}
|
|
203
|
+
function updateConnectionOverlay(stage, detail) {
|
|
204
|
+
pendingConnectionOverlayStage = stage;
|
|
205
|
+
pendingConnectionOverlayDetail = detail || '';
|
|
206
|
+
if (connectionOverlayVisible) {
|
|
207
|
+
showConnectionOverlayNow(stage, detail);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function markHmrConnectionHealthy() {
|
|
211
|
+
awaitingHealthyHmrMessage = false;
|
|
212
|
+
clearConnectionOverlayTimer();
|
|
213
|
+
if (connectionOverlayVisible) {
|
|
214
|
+
connectionOverlayVisible = false;
|
|
215
|
+
hideConnectionOverlay();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
46
218
|
/**
|
|
47
219
|
* Flavor hooks
|
|
48
220
|
*/
|
|
49
|
-
import { installNsVueDevShims, ensureBackWrapperInstalled, getRootForVue, loadSfcComponent, ensureVueGlobals, ensurePiniaOnApp, addSfcMapping, recordVuePayloadChanges, handleVueSfcRegistry, handleVueSfcRegistryUpdate } from '../frameworks/vue/client/index.js';
|
|
221
|
+
import { installNsVueDevShims, ensureBackWrapperInstalled, getRootForVue, loadSfcComponent, ensureVueGlobals, ensurePiniaOnApp, addSfcMapping, recordVuePayloadChanges, handleVueSfcRegistry, handleVueSfcRegistryUpdate, sfcArtifactMap } from '../frameworks/vue/client/index.js';
|
|
50
222
|
import { handleAngularHotUpdateMessage, installAngularHmrClientHooks } from '../frameworks/angular/client/index.js';
|
|
51
|
-
switch (
|
|
223
|
+
switch (TARGET_FLAVOR) {
|
|
52
224
|
case 'vue':
|
|
53
225
|
installNsVueDevShims();
|
|
54
226
|
break;
|
|
@@ -60,6 +232,10 @@ switch (__NS_TARGET_FLAVOR__) {
|
|
|
60
232
|
let initialMounted = !!globalThis.__NS_HMR_BOOT_COMPLETE__;
|
|
61
233
|
// Prevent duplicate initial-mount scheduling across rapid full-graph broadcasts and re-evaluations
|
|
62
234
|
let initialMounting = !!globalThis.__NS_HMR_INITIAL_MOUNT_IN_PROGRESS__;
|
|
235
|
+
// Track whether the first full-graph has been received. Before the full-graph,
|
|
236
|
+
// delta messages are just the server discovering modules during initial boot —
|
|
237
|
+
// NOT actual code changes. Re-imports must be gated behind this flag.
|
|
238
|
+
let hasReceivedFullGraph = false;
|
|
63
239
|
// TypeScript flavor: track registry modules and inferred main id
|
|
64
240
|
let tsModuleSet = null;
|
|
65
241
|
let tsMainId = null;
|
|
@@ -94,10 +270,14 @@ function applyFullGraph(payload) {
|
|
|
94
270
|
console.log('[hmr][graph] full graph applied version', getGraphVersion(), 'modules=', graph.size);
|
|
95
271
|
// Guarded initial mount rescue: if app hasn't replaced the placeholder shortly after graph arrives,
|
|
96
272
|
// perform a one-time mount. This waits briefly to let the app's main entry start() run first to avoid double mounts.
|
|
273
|
+
// TypeScript flavor: skip the rescue entirely. The HTTP boot will load main.ts which
|
|
274
|
+
// calls Application.run() (patched to resetRootView) — the rescue is redundant and
|
|
275
|
+
// causes a double-mount race (rescue fires at 450ms, then main.ts fires ~1s later,
|
|
276
|
+
// causing a visual flash and leaving the app in an inconsistent state).
|
|
97
277
|
try {
|
|
98
278
|
const g = globalThis;
|
|
99
279
|
const bootDone = !!g.__NS_HMR_BOOT_COMPLETE__;
|
|
100
|
-
if (!bootDone && !initialMounted && !initialMounting && !g.__NS_HMR_RESCUE_SCHEDULED__) {
|
|
280
|
+
if (!bootDone && !initialMounted && !initialMounting && !g.__NS_HMR_RESCUE_SCHEDULED__ && TARGET_FLAVOR !== 'typescript') {
|
|
101
281
|
// simple snapshot helpers
|
|
102
282
|
const getTopmost = () => {
|
|
103
283
|
try {
|
|
@@ -170,7 +350,7 @@ function applyFullGraph(payload) {
|
|
|
170
350
|
if (VERBOSE)
|
|
171
351
|
console.log('[hmr][init] placeholder persists after delay; evaluating rescue policy');
|
|
172
352
|
// Flavor-specific rescue handling
|
|
173
|
-
if (
|
|
353
|
+
if (TARGET_FLAVOR === 'typescript') {
|
|
174
354
|
// For TS apps, perform a one-time resetRootView to the conventional
|
|
175
355
|
// app root module. This mimics what Application.run would do and
|
|
176
356
|
// replaces the placeholder with the real UI without trying to
|
|
@@ -211,7 +391,7 @@ function applyFullGraph(payload) {
|
|
|
211
391
|
return;
|
|
212
392
|
}
|
|
213
393
|
let candidate = null;
|
|
214
|
-
switch (
|
|
394
|
+
switch (TARGET_FLAVOR) {
|
|
215
395
|
case 'vue': {
|
|
216
396
|
const appEntry = graph.get(APP_MAIN_ENTRY_SPEC);
|
|
217
397
|
if (appEntry && Array.isArray(appEntry.deps)) {
|
|
@@ -227,6 +407,19 @@ function applyFullGraph(payload) {
|
|
|
227
407
|
}
|
|
228
408
|
}
|
|
229
409
|
}
|
|
410
|
+
// Fallback: when the module graph is empty (Vite 7+ may not populate it
|
|
411
|
+
// before the first full-graph broadcast), check the SFC artifact registry
|
|
412
|
+
// which is populated from the ns:vue-sfc-registry message.
|
|
413
|
+
if (!candidate && sfcArtifactMap.size > 0) {
|
|
414
|
+
for (const id of sfcArtifactMap.keys()) {
|
|
415
|
+
if (/\.vue$/i.test(id)) {
|
|
416
|
+
candidate = id;
|
|
417
|
+
if (VERBOSE)
|
|
418
|
+
console.log('[hmr][init] rescue candidate from SFC registry:', id);
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
230
423
|
break;
|
|
231
424
|
}
|
|
232
425
|
}
|
|
@@ -240,7 +433,7 @@ function applyFullGraph(payload) {
|
|
|
240
433
|
(async () => {
|
|
241
434
|
try {
|
|
242
435
|
let comp = null;
|
|
243
|
-
switch (
|
|
436
|
+
switch (TARGET_FLAVOR) {
|
|
244
437
|
case 'vue':
|
|
245
438
|
comp = await loadSfcComponent(candidate, 'initial_mount_rescue');
|
|
246
439
|
break;
|
|
@@ -285,7 +478,7 @@ function applyFullGraph(payload) {
|
|
|
285
478
|
// to avoid double-mount races that can cause duplicate navigation logs.
|
|
286
479
|
if (ALLOW_INITIAL_MOUNT && !initialMounted && !bootDone && !bootInProgress && !getCurrentApp() && !getRootFrame()) {
|
|
287
480
|
let candidate = null;
|
|
288
|
-
switch (
|
|
481
|
+
switch (TARGET_FLAVOR) {
|
|
289
482
|
case 'vue': {
|
|
290
483
|
const appEntry = graph.get(APP_MAIN_ENTRY_SPEC);
|
|
291
484
|
if (appEntry && Array.isArray(appEntry.deps)) {
|
|
@@ -301,6 +494,17 @@ function applyFullGraph(payload) {
|
|
|
301
494
|
}
|
|
302
495
|
}
|
|
303
496
|
}
|
|
497
|
+
// Fallback: SFC registry (same as rescue mount above)
|
|
498
|
+
if (!candidate && sfcArtifactMap.size > 0) {
|
|
499
|
+
for (const id of sfcArtifactMap.keys()) {
|
|
500
|
+
if (/\.vue$/i.test(id)) {
|
|
501
|
+
candidate = id;
|
|
502
|
+
if (VERBOSE)
|
|
503
|
+
console.log('[hmr][init] initial mount candidate from SFC registry:', id);
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
304
508
|
break;
|
|
305
509
|
}
|
|
306
510
|
case 'typescript': {
|
|
@@ -318,7 +522,7 @@ function applyFullGraph(payload) {
|
|
|
318
522
|
(async () => {
|
|
319
523
|
try {
|
|
320
524
|
if (VERBOSE)
|
|
321
|
-
console.log('[hmr][init] mounting initial root from', candidate, 'flavor=',
|
|
525
|
+
console.log('[hmr][init] mounting initial root from', candidate, 'flavor=', TARGET_FLAVOR);
|
|
322
526
|
// Android-only: avoid racing entry-runtime reset and Activity bring-up
|
|
323
527
|
try {
|
|
324
528
|
const g = globalThis;
|
|
@@ -352,7 +556,7 @@ function applyFullGraph(payload) {
|
|
|
352
556
|
}
|
|
353
557
|
catch { }
|
|
354
558
|
let comp = null;
|
|
355
|
-
switch (
|
|
559
|
+
switch (TARGET_FLAVOR) {
|
|
356
560
|
case 'vue':
|
|
357
561
|
comp = await loadSfcComponent(candidate, 'initial_mount');
|
|
358
562
|
break;
|
|
@@ -401,7 +605,7 @@ function applyFullGraph(payload) {
|
|
|
401
605
|
})();
|
|
402
606
|
}
|
|
403
607
|
else if (VERBOSE) {
|
|
404
|
-
console.warn('[hmr][init] no component found in graph to mount initially for flavor',
|
|
608
|
+
console.warn('[hmr][init] no component found in graph to mount initially for flavor', TARGET_FLAVOR);
|
|
405
609
|
}
|
|
406
610
|
}
|
|
407
611
|
}
|
|
@@ -427,7 +631,7 @@ function applyDelta(payload) {
|
|
|
427
631
|
setGraphVersion(payload.newVersion);
|
|
428
632
|
}
|
|
429
633
|
const changed = payload.changed || [];
|
|
430
|
-
switch (
|
|
634
|
+
switch (TARGET_FLAVOR) {
|
|
431
635
|
case 'vue':
|
|
432
636
|
recordVuePayloadChanges(changed, getGraphVersion());
|
|
433
637
|
break;
|
|
@@ -445,6 +649,16 @@ function applyDelta(payload) {
|
|
|
445
649
|
console.log('[hmr][graph] delta applied newVersion', getGraphVersion(), 'changed=', (payload.changed || []).length, 'removed=', (payload.removed || []).length, 'baseVersion=', payload.baseVersion);
|
|
446
650
|
// Queue evaluation of changed modules (placeholder pipeline)
|
|
447
651
|
if (payload.changed?.length) {
|
|
652
|
+
// Gate: Before the first full-graph is received, delta messages are the server
|
|
653
|
+
// discovering modules during initial boot — NOT actual code changes. The entry-runtime
|
|
654
|
+
// already loaded all modules; re-importing them would cause duplicate Application.run(),
|
|
655
|
+
// router initialization, and modal conflicts. Only queue re-imports after the first
|
|
656
|
+
// full-graph confirms the graph is synced and subsequent deltas are real changes.
|
|
657
|
+
if (!hasReceivedFullGraph) {
|
|
658
|
+
if (VERBOSE)
|
|
659
|
+
console.log('[hmr][delta] skipping re-import queue (initial graph build, no full-graph yet)');
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
448
662
|
// HARD SUPPRESS: the very first delta (baseVersion 0) commonly includes the app main entry which is already evaluated during bootstrap.
|
|
449
663
|
// Importing it again with a cache-bust often produces a spurious module-not-found (timestamp param treated as distinct file).
|
|
450
664
|
const isInitial = payload.baseVersion === 0;
|
|
@@ -507,7 +721,7 @@ function applyDelta(payload) {
|
|
|
507
721
|
// Deterministic navigation using the current Vue app instance rather than vendor-held rootApp
|
|
508
722
|
function __nsNavigateUsingApp(comp, opts = {}) {
|
|
509
723
|
const g = globalThis;
|
|
510
|
-
switch (
|
|
724
|
+
switch (TARGET_FLAVOR) {
|
|
511
725
|
case 'vue':
|
|
512
726
|
ensureVueGlobals();
|
|
513
727
|
break;
|
|
@@ -535,7 +749,7 @@ function __nsNavigateUsingApp(comp, opts = {}) {
|
|
|
535
749
|
const existingApp = getCurrentApp();
|
|
536
750
|
const baseProvides = (existingApp && existingApp._context && existingApp._context.provides) || {};
|
|
537
751
|
const app = AppFactory(normalizeComponent(comp, comp && (comp.__name || comp.name)));
|
|
538
|
-
switch (
|
|
752
|
+
switch (TARGET_FLAVOR) {
|
|
539
753
|
case 'vue':
|
|
540
754
|
ensurePiniaOnApp(app);
|
|
541
755
|
break;
|
|
@@ -669,7 +883,42 @@ async function processQueue() {
|
|
|
669
883
|
return;
|
|
670
884
|
if (VERBOSE)
|
|
671
885
|
console.log('[hmr][queue] processing changed ids', drained);
|
|
886
|
+
// Track wall-clock so the 'complete' frame can show a meaningful
|
|
887
|
+
// total. Only the Solid + TypeScript flavors drive the overlay
|
|
888
|
+
// from here; Angular has its own flow inside
|
|
889
|
+
// `frameworks/angular/client/index.ts`.
|
|
890
|
+
const tQueueStart = Date.now();
|
|
891
|
+
const driveSolidOverlay = TARGET_FLAVOR === 'solid';
|
|
892
|
+
// Explicit eviction step.
|
|
893
|
+
//
|
|
894
|
+
// On modern runtimes the URL canonicalizer collapses any
|
|
895
|
+
// `__ns_hmr__/<tag>/` segment back to a stable cache key, so
|
|
896
|
+
// without explicit eviction the upcoming `import(url)` would
|
|
897
|
+
// resolve via V8's `g_moduleRegistry` and return the cached
|
|
898
|
+
// stale module — making the queue drain a silent no-op for
|
|
899
|
+
// every save after the first.
|
|
900
|
+
//
|
|
901
|
+
// We hand the canonical eviction URLs to the runtime first;
|
|
902
|
+
// `invalidateModulesByUrls` is a no-op on older runtimes and
|
|
903
|
+
// `requestModuleFromServer` automatically falls back to the
|
|
904
|
+
// legacy `/ns/m/__ns_hmr__/v<N>/` URL versioning path in that
|
|
905
|
+
// case. node_modules and virtual specs are filtered out by
|
|
906
|
+
// `buildEvictionUrls` so vendor modules stay hot.
|
|
907
|
+
if (driveSolidOverlay) {
|
|
908
|
+
setUpdateOverlayStage('evicting', {
|
|
909
|
+
detail: drained.length === 1 ? `Invalidating ${drained[0]}` : `Invalidating ${drained.length} modules`,
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
const evictUrls = buildEvictionUrls(drained);
|
|
913
|
+
const evicted = invalidateModulesByUrls(evictUrls);
|
|
914
|
+
if (VERBOSE)
|
|
915
|
+
console.log(`[hmr][queue] eviction count=${evictUrls.length} ok=${evicted}`);
|
|
672
916
|
// Evaluate changed modules best-effort; failures shouldn't completely break HMR.
|
|
917
|
+
if (driveSolidOverlay) {
|
|
918
|
+
setUpdateOverlayStage('reimporting', {
|
|
919
|
+
detail: drained.length === 1 ? `Re-importing ${drained[0]}` : `Re-importing ${drained.length} modules`,
|
|
920
|
+
});
|
|
921
|
+
}
|
|
673
922
|
for (const id of drained) {
|
|
674
923
|
try {
|
|
675
924
|
const spec = normalizeSpec(id);
|
|
@@ -686,10 +935,274 @@ async function processQueue() {
|
|
|
686
935
|
}
|
|
687
936
|
}
|
|
688
937
|
// After evaluating the batch, perform flavor-specific UI refresh.
|
|
689
|
-
switch (
|
|
938
|
+
switch (TARGET_FLAVOR) {
|
|
690
939
|
case 'vue':
|
|
691
940
|
// Vue SFCs are handled via the registry update path; nothing to do here.
|
|
692
941
|
break;
|
|
942
|
+
case 'solid': {
|
|
943
|
+
// Boundaries discovered in this HMR cycle (tsx files reachable
|
|
944
|
+
// via the reverse import graph from any changed file, plus route
|
|
945
|
+
// files reachable from any tsx start point). Declared at the top
|
|
946
|
+
// of the case block so the emit step below can include the
|
|
947
|
+
// complete set in the listener event — framework integrations
|
|
948
|
+
// use it to map route boundaries → fresh component references.
|
|
949
|
+
const boundaries = new Set();
|
|
950
|
+
// Solid .tsx components are self-accepting via solid-refresh's inline
|
|
951
|
+
// patchRegistry — re-importing them is sufficient. For non-component
|
|
952
|
+
// .ts utility modules, we must propagate up the import graph to find
|
|
953
|
+
// the .tsx/.jsx component boundaries and re-import those so their
|
|
954
|
+
// solid-refresh proxies pick up the new dependency values.
|
|
955
|
+
try {
|
|
956
|
+
// Build reverse index: dep id → list of importer ids
|
|
957
|
+
const reverseIndex = new Map();
|
|
958
|
+
for (const [id, mod] of graph) {
|
|
959
|
+
for (const dep of mod.deps) {
|
|
960
|
+
let arr = reverseIndex.get(dep);
|
|
961
|
+
if (!arr) {
|
|
962
|
+
arr = [];
|
|
963
|
+
reverseIndex.set(dep, arr);
|
|
964
|
+
}
|
|
965
|
+
arr.push(id);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
// Pass 1: BFS from each non-tsx changed module up to tsx/jsx
|
|
969
|
+
// boundaries. These get re-imported below so solid-refresh's
|
|
970
|
+
// inline patchRegistry runs and (best-effort) swaps the proxy
|
|
971
|
+
// signals for any components defined in those tsx boundaries.
|
|
972
|
+
for (const id of drained) {
|
|
973
|
+
if (/\.(tsx|jsx)$/i.test(id))
|
|
974
|
+
continue; // already self-accepting
|
|
975
|
+
const visited = new Set();
|
|
976
|
+
const queue = [id];
|
|
977
|
+
while (queue.length) {
|
|
978
|
+
const cur = queue.shift();
|
|
979
|
+
if (visited.has(cur))
|
|
980
|
+
continue;
|
|
981
|
+
visited.add(cur);
|
|
982
|
+
const importers = reverseIndex.get(cur);
|
|
983
|
+
if (!importers)
|
|
984
|
+
continue;
|
|
985
|
+
for (const imp of importers) {
|
|
986
|
+
if (/\.(tsx|jsx)$/i.test(imp)) {
|
|
987
|
+
boundaries.add(imp);
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
queue.push(imp);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
// Pass 2: walk further from any tsx starting point (a tsx file
|
|
996
|
+
// in `drained` OR a tsx boundary discovered in pass 1) to find
|
|
997
|
+
// route files (`/src/routes/*.{tsx,jsx}`) that transitively
|
|
998
|
+
// import them. Re-importing a route file refreshes its
|
|
999
|
+
// `Route.options.component` to the freshly-imported reference
|
|
1000
|
+
// and the existing boundary loop below patches the live router
|
|
1001
|
+
// with that fresh reference.
|
|
1002
|
+
//
|
|
1003
|
+
// This is the key fix for "edit home.tsx → save → no visual
|
|
1004
|
+
// update": the old BFS skipped tsx files in `drained` (assuming
|
|
1005
|
+
// solid-refresh's in-place proxy patch was sufficient), but in
|
|
1006
|
+
// the universal-renderer + nested-context configuration that
|
|
1007
|
+
// patch does not always propagate to the visible page tree.
|
|
1008
|
+
// Adding the route file as a boundary lets us patch
|
|
1009
|
+
// `route.options.component` directly to a fresh module export,
|
|
1010
|
+
// which the framework subscriber then passes through to the
|
|
1011
|
+
// page remount — making the cycle robust to the proxy patch
|
|
1012
|
+
// silently failing.
|
|
1013
|
+
const tsxStarts = new Set();
|
|
1014
|
+
for (const id of drained) {
|
|
1015
|
+
if (/\.(tsx|jsx)$/i.test(id))
|
|
1016
|
+
tsxStarts.add(id);
|
|
1017
|
+
}
|
|
1018
|
+
for (const b of boundaries)
|
|
1019
|
+
tsxStarts.add(b);
|
|
1020
|
+
const ROUTE_FILE_RE = /\/src\/routes\/.+\.(tsx|jsx)$/i;
|
|
1021
|
+
for (const start of tsxStarts) {
|
|
1022
|
+
const visited = new Set();
|
|
1023
|
+
const queue = [start];
|
|
1024
|
+
while (queue.length) {
|
|
1025
|
+
const cur = queue.shift();
|
|
1026
|
+
if (visited.has(cur))
|
|
1027
|
+
continue;
|
|
1028
|
+
visited.add(cur);
|
|
1029
|
+
if (cur !== start && ROUTE_FILE_RE.test(cur)) {
|
|
1030
|
+
boundaries.add(cur);
|
|
1031
|
+
}
|
|
1032
|
+
const importers = reverseIndex.get(cur);
|
|
1033
|
+
if (!importers)
|
|
1034
|
+
continue;
|
|
1035
|
+
for (const imp of importers) {
|
|
1036
|
+
queue.push(imp);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
// Re-import each boundary so solid-refresh patchRegistry fires.
|
|
1041
|
+
// For route files (TanStack Router), capture the new Route export
|
|
1042
|
+
// and patch the router's existing route with the fresh loader.
|
|
1043
|
+
let routesPatchCount = 0;
|
|
1044
|
+
let discoveredRouter = null;
|
|
1045
|
+
// Discover router: try __ns_router global (set by createNativeScriptRouter),
|
|
1046
|
+
// then scan globalThis for any router-shaped object with routesById.
|
|
1047
|
+
const findRouter = () => {
|
|
1048
|
+
if (discoveredRouter)
|
|
1049
|
+
return discoveredRouter;
|
|
1050
|
+
const g = globalThis;
|
|
1051
|
+
if (g.__ns_router?.routesById)
|
|
1052
|
+
return (discoveredRouter = g.__ns_router);
|
|
1053
|
+
// Fallback: scan common global keys for router
|
|
1054
|
+
for (const key of ['__ns_router', 'router', '__router']) {
|
|
1055
|
+
if (g[key]?.routesById && g[key]?.invalidate)
|
|
1056
|
+
return (discoveredRouter = g[key]);
|
|
1057
|
+
}
|
|
1058
|
+
return null;
|
|
1059
|
+
};
|
|
1060
|
+
// Convert boundary file path to TanStack Router fullPath.
|
|
1061
|
+
// e.g. /src/routes/posts.$postId.tsx → /posts/$postId
|
|
1062
|
+
const boundaryToFullPath = (bid) => {
|
|
1063
|
+
const m = bid.match(/\/src\/routes\/(.+)\.(tsx|jsx|ts|js)$/i);
|
|
1064
|
+
if (!m)
|
|
1065
|
+
return null;
|
|
1066
|
+
let p = m[1];
|
|
1067
|
+
// Replace dots between segments with slashes (posts.$postId → posts/$postId)
|
|
1068
|
+
p = p.replace(/\./g, '/');
|
|
1069
|
+
// Handle index files
|
|
1070
|
+
if (p === 'index')
|
|
1071
|
+
return '/';
|
|
1072
|
+
if (p.endsWith('/index'))
|
|
1073
|
+
p = p.slice(0, -6);
|
|
1074
|
+
// Strip leading - (TanStack pathless layout convention)
|
|
1075
|
+
p = p.replace(/(^|\/)-([\w])/g, '$1$2');
|
|
1076
|
+
return '/' + p;
|
|
1077
|
+
};
|
|
1078
|
+
// Find existing route by fullPath (since new Route has no id yet)
|
|
1079
|
+
const findRouteByFullPath = (router, fp) => {
|
|
1080
|
+
if (!router?.routesById)
|
|
1081
|
+
return null;
|
|
1082
|
+
for (const rid of Object.keys(router.routesById)) {
|
|
1083
|
+
const r = router.routesById[rid];
|
|
1084
|
+
if (r?.fullPath === fp)
|
|
1085
|
+
return r;
|
|
1086
|
+
}
|
|
1087
|
+
return null;
|
|
1088
|
+
};
|
|
1089
|
+
// Evict the boundary set so re-importing each .tsx
|
|
1090
|
+
// component actually picks up the new transitive
|
|
1091
|
+
// dependency code; without this V8 returns the
|
|
1092
|
+
// cached boundary module unchanged.
|
|
1093
|
+
const boundaryIds = Array.from(boundaries);
|
|
1094
|
+
const solidEvictUrls = buildEvictionUrls(boundaryIds);
|
|
1095
|
+
const solidEvicted = invalidateModulesByUrls(solidEvictUrls);
|
|
1096
|
+
if (VERBOSE)
|
|
1097
|
+
console.log(`[hmr][solid] eviction count=${solidEvictUrls.length} ok=${solidEvicted}`);
|
|
1098
|
+
for (const id of boundaries) {
|
|
1099
|
+
if (seen.has(id))
|
|
1100
|
+
continue;
|
|
1101
|
+
try {
|
|
1102
|
+
const spec = normalizeSpec(id);
|
|
1103
|
+
const url = await requestModuleFromServer(spec);
|
|
1104
|
+
if (!url)
|
|
1105
|
+
continue;
|
|
1106
|
+
if (VERBOSE)
|
|
1107
|
+
console.log('[hmr][solid] propagated to boundary', { id, url });
|
|
1108
|
+
const mod = await import(/* @vite-ignore */ url);
|
|
1109
|
+
// Patch TanStack Router route options for any module
|
|
1110
|
+
// that exports a `Route`. We patch BOTH the component
|
|
1111
|
+
// and the loader (when present); components-only routes
|
|
1112
|
+
// were previously skipped because the gate required a
|
|
1113
|
+
// loader, which left their `options.component` pointing
|
|
1114
|
+
// at the stale module's exports after HMR.
|
|
1115
|
+
try {
|
|
1116
|
+
const newRoute = mod?.Route;
|
|
1117
|
+
if (newRoute?.options) {
|
|
1118
|
+
const router = findRouter();
|
|
1119
|
+
const fullPath = boundaryToFullPath(id);
|
|
1120
|
+
if (VERBOSE)
|
|
1121
|
+
console.log('[hmr][solid][diag] route patch attempt', { id, fullPath, hasRouter: !!router, hasLoader: !!newRoute.options.loader, hasComponent: !!newRoute.options.component });
|
|
1122
|
+
const existingRoute = fullPath && router ? findRouteByFullPath(router, fullPath) : null;
|
|
1123
|
+
if (existingRoute?.options) {
|
|
1124
|
+
if (newRoute.options.loader)
|
|
1125
|
+
existingRoute.options.loader = newRoute.options.loader;
|
|
1126
|
+
if (newRoute.options.component)
|
|
1127
|
+
existingRoute.options.component = newRoute.options.component;
|
|
1128
|
+
routesPatchCount++;
|
|
1129
|
+
if (VERBOSE)
|
|
1130
|
+
console.log('[hmr][solid] patched route', existingRoute.id, 'fullPath=', fullPath);
|
|
1131
|
+
}
|
|
1132
|
+
else if (VERBOSE) {
|
|
1133
|
+
console.log('[hmr][solid] no matching route for fullPath', fullPath);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
catch (e) {
|
|
1138
|
+
if (VERBOSE)
|
|
1139
|
+
console.warn('[hmr][solid] route patch error', id, e);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
catch (e) {
|
|
1143
|
+
if (VERBOSE)
|
|
1144
|
+
console.warn('[hmr][solid] boundary re-import failed', id, e);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
// Route loaders were patched with fresh closures. The data is
|
|
1148
|
+
// correct in router.state.matches[].loaderData (confirmed via
|
|
1149
|
+
// diagnostics), but the currently visible page doesn't re-render
|
|
1150
|
+
// because the NativeScriptRouterProvider's Solid store flush only
|
|
1151
|
+
// fires through history.subscribe. TODO: find the right mechanism
|
|
1152
|
+
// to trigger a Solid reactive update for the active match stores.
|
|
1153
|
+
if (routesPatchCount > 0 && VERBOSE) {
|
|
1154
|
+
console.log('[hmr][solid] patched', routesPatchCount, 'route loaders (data correct in match state, pending UI refresh mechanism)');
|
|
1155
|
+
}
|
|
1156
|
+
if (VERBOSE) {
|
|
1157
|
+
if (boundaries.size)
|
|
1158
|
+
console.log('[hmr][solid] propagated non-component change to', boundaries.size, 'boundaries', Array.from(boundaries));
|
|
1159
|
+
console.log('[hmr][queue] Solid: modules re-imported, solid-refresh handles reactive update', drained);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
catch (e) {
|
|
1163
|
+
if (VERBOSE)
|
|
1164
|
+
console.warn('[hmr][solid] propagation failed', e);
|
|
1165
|
+
}
|
|
1166
|
+
// Notify any framework integrations (e.g.
|
|
1167
|
+
// `@nativescript/tanstack-router`) that a Solid HMR
|
|
1168
|
+
// cycle has completed. They use this signal to perform
|
|
1169
|
+
// framework-specific UI refresh (e.g. remount the active
|
|
1170
|
+
// router page) when solid-refresh's own reactive
|
|
1171
|
+
// propagation does not reach the visible tree under
|
|
1172
|
+
// the current renderer/context configuration.
|
|
1173
|
+
//
|
|
1174
|
+
// Boundaries include both the directly-changed tsx files
|
|
1175
|
+
// AND every tsx ancestor reachable via the reverse import
|
|
1176
|
+
// graph (route files in particular). The framework
|
|
1177
|
+
// listener uses the route-file boundaries to look up the
|
|
1178
|
+
// freshly-patched `route.options.component` and pass it
|
|
1179
|
+
// through to the page remount.
|
|
1180
|
+
try {
|
|
1181
|
+
const tsxChangedInDrained = drained.filter((id) => /\.(tsx|jsx)$/i.test(id));
|
|
1182
|
+
const allBoundaries = Array.from(new Set([...tsxChangedInDrained, ...boundaries]));
|
|
1183
|
+
nsSolidHmrEmit({
|
|
1184
|
+
kind: 'solid',
|
|
1185
|
+
changedFiles: drained.slice(),
|
|
1186
|
+
boundaries: allBoundaries,
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
catch (err) {
|
|
1190
|
+
if (VERBOSE)
|
|
1191
|
+
console.warn('[hmr][solid] emit failed', err);
|
|
1192
|
+
}
|
|
1193
|
+
// Tell the overlay the cycle is done. solid-refresh's
|
|
1194
|
+
// inline patchRegistry has already flushed the new
|
|
1195
|
+
// component bodies into the live tree (the `case
|
|
1196
|
+
// 'solid'` block above re-imports each .tsx
|
|
1197
|
+
// boundary), so by the time we get here the user is
|
|
1198
|
+
// already looking at the new render. The 'complete'
|
|
1199
|
+
// frame surfaces the wall-clock total and triggers
|
|
1200
|
+
// the overlay's auto-hide.
|
|
1201
|
+
setUpdateOverlayStage('complete', {
|
|
1202
|
+
detail: `Total ${Math.max(0, Date.now() - tQueueStart)}ms`,
|
|
1203
|
+
});
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
693
1206
|
case 'typescript': {
|
|
694
1207
|
// For TS apps, always reset back to the conventional app root.
|
|
695
1208
|
// This preserves the shell (Frame, ActionBar, etc.) that the app's
|
|
@@ -702,9 +1215,124 @@ async function processQueue() {
|
|
|
702
1215
|
console.warn('[hmr][queue] TS flavor: Application.resetRootView unavailable; skipping UI refresh');
|
|
703
1216
|
break;
|
|
704
1217
|
}
|
|
1218
|
+
// Re-fetch changed XML/CSS files and update the bundled module registry
|
|
1219
|
+
// so Builder.createViewFromEntry picks up fresh content.
|
|
1220
|
+
const rawAssetIds = drained.filter((id) => /\.(xml|css|scss|sass|less)$/i.test(id));
|
|
1221
|
+
if (rawAssetIds.length && typeof g.registerModule === 'function') {
|
|
1222
|
+
const origin = getHttpOriginForVite() || deriveHttpOrigin(getHMRWsUrl());
|
|
1223
|
+
if (origin) {
|
|
1224
|
+
for (const id of rawAssetIds) {
|
|
1225
|
+
try {
|
|
1226
|
+
const spec = normalizeSpec(id);
|
|
1227
|
+
// Fetch the raw file content directly from Vite's dev server.
|
|
1228
|
+
// Use the project-relative path which Vite serves as static files.
|
|
1229
|
+
const fetchUrl = origin + (spec.startsWith('/') ? spec : '/' + spec);
|
|
1230
|
+
if (VERBOSE)
|
|
1231
|
+
console.log('[hmr][queue] fetching raw asset', { id, fetchUrl });
|
|
1232
|
+
const resp = await fetch(fetchUrl);
|
|
1233
|
+
if (resp.ok) {
|
|
1234
|
+
const rawContent = await resp.text();
|
|
1235
|
+
// Register under all nickname variants the module registry uses.
|
|
1236
|
+
// The bundler context registers XML as e.g., './main-page.xml' and 'main-page.xml'
|
|
1237
|
+
const appVirtual = APP_VIRTUAL_WITH_SLASH.replace(/^\//, '');
|
|
1238
|
+
let relPath = spec.startsWith('/') ? spec.slice(1) : spec;
|
|
1239
|
+
if (relPath.startsWith(appVirtual))
|
|
1240
|
+
relPath = relPath.slice(appVirtual.length);
|
|
1241
|
+
const nicknames = ['./' + relPath, relPath];
|
|
1242
|
+
// Also add without extension for CSS
|
|
1243
|
+
const extIdx = relPath.lastIndexOf('.');
|
|
1244
|
+
if (extIdx > 0) {
|
|
1245
|
+
const baseName = relPath.slice(0, extIdx);
|
|
1246
|
+
if (!relPath.endsWith('.xml'))
|
|
1247
|
+
nicknames.push(baseName, './' + baseName);
|
|
1248
|
+
}
|
|
1249
|
+
for (const name of nicknames) {
|
|
1250
|
+
if (VERBOSE)
|
|
1251
|
+
console.log('[hmr][queue] re-registering module', name);
|
|
1252
|
+
g.registerModule(name, () => rawContent);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
else if (VERBOSE) {
|
|
1256
|
+
console.warn('[hmr][queue] raw asset fetch failed', id, resp.status);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
catch (e) {
|
|
1260
|
+
if (VERBOSE)
|
|
1261
|
+
console.warn('[hmr][queue] raw asset refresh failed for', id, e);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
// Determine if we can navigate in-place to a changed page
|
|
1267
|
+
// instead of resetting all the way back to app-root.
|
|
1268
|
+
// This keeps the user on the page they're editing for faster iteration.
|
|
1269
|
+
const changedXmlPages = drained
|
|
1270
|
+
.filter((id) => /\.xml$/i.test(id))
|
|
1271
|
+
.map((id) => {
|
|
1272
|
+
const spec = normalizeSpec(id);
|
|
1273
|
+
const appVirtual = APP_VIRTUAL_WITH_SLASH.replace(/^\//, '');
|
|
1274
|
+
let relPath = spec.startsWith('/') ? spec.slice(1) : spec;
|
|
1275
|
+
if (relPath.startsWith(appVirtual))
|
|
1276
|
+
relPath = relPath.slice(appVirtual.length);
|
|
1277
|
+
// Strip .xml extension to get the moduleName (e.g., 'pages/status-bar')
|
|
1278
|
+
return relPath.replace(/\.xml$/i, '');
|
|
1279
|
+
})
|
|
1280
|
+
.filter((m) => m && m !== 'app-root');
|
|
1281
|
+
// Resolve the topmost Frame from the bundled realm.
|
|
1282
|
+
// Frame.topmost() relies on an internal frameStack array, so we must
|
|
1283
|
+
// call it on the bundled-realm class. Multiple strategies to find it:
|
|
1284
|
+
const FrameClass = getCore('Frame') || g.Frame;
|
|
1285
|
+
let topFrame = null;
|
|
1286
|
+
// 1) Try the vendor-realm static topmost()
|
|
1287
|
+
try {
|
|
1288
|
+
topFrame = FrameClass?.topmost?.();
|
|
1289
|
+
}
|
|
1290
|
+
catch { }
|
|
1291
|
+
// 2) Try getting the root view from Application — if it's a Frame, use it
|
|
1292
|
+
if (!topFrame) {
|
|
1293
|
+
try {
|
|
1294
|
+
const rootView = App.getRootView?.() || App._rootView;
|
|
1295
|
+
if (rootView) {
|
|
1296
|
+
// rootView could be a Frame itself, or contain a Frame
|
|
1297
|
+
const isFrame = rootView.constructor?.name === 'Frame' || rootView.navigate;
|
|
1298
|
+
if (isFrame) {
|
|
1299
|
+
topFrame = rootView;
|
|
1300
|
+
}
|
|
1301
|
+
else if (rootView.getChildAt) {
|
|
1302
|
+
// Walk direct children looking for a Frame
|
|
1303
|
+
for (let i = 0; i < (rootView.getChildrenCount?.() || 0); i++) {
|
|
1304
|
+
const child = rootView.getChildAt(i);
|
|
1305
|
+
if (child?.constructor?.name === 'Frame' || child?.navigate) {
|
|
1306
|
+
topFrame = child;
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
catch { }
|
|
1314
|
+
}
|
|
705
1315
|
if (VERBOSE)
|
|
706
|
-
console.log('[hmr][queue] TS
|
|
707
|
-
|
|
1316
|
+
console.log('[hmr][queue] TS: changedXmlPages=', changedXmlPages, 'topFrame=', !!topFrame);
|
|
1317
|
+
if (changedXmlPages.length > 0 && topFrame) {
|
|
1318
|
+
// Navigate the current frame to the changed page directly.
|
|
1319
|
+
// Use the last changed XML page (most specific).
|
|
1320
|
+
const moduleName = changedXmlPages[changedXmlPages.length - 1];
|
|
1321
|
+
if (VERBOSE)
|
|
1322
|
+
console.log('[hmr][queue] TS: navigating in-place to', moduleName);
|
|
1323
|
+
try {
|
|
1324
|
+
topFrame.navigate({ moduleName, clearHistory: false, animated: false });
|
|
1325
|
+
}
|
|
1326
|
+
catch (navErr) {
|
|
1327
|
+
console.warn('[hmr][queue] TS flavor: in-place navigate failed, falling back to resetRootView', navErr);
|
|
1328
|
+
App.resetRootView({ moduleName: 'app-root' });
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
else {
|
|
1332
|
+
if (VERBOSE)
|
|
1333
|
+
console.log('[hmr][queue] TS flavor: resetRootView(app-root) after changes');
|
|
1334
|
+
App.resetRootView({ moduleName: 'app-root' });
|
|
1335
|
+
}
|
|
708
1336
|
}
|
|
709
1337
|
catch (e) {
|
|
710
1338
|
console.warn('[hmr][queue] TS flavor: resetRootView(app-root) failed', e);
|
|
@@ -752,6 +1380,11 @@ function connectHmr() {
|
|
|
752
1380
|
console.log('[hmr-client] Already connecting to HMR WebSocket, skipping');
|
|
753
1381
|
return;
|
|
754
1382
|
}
|
|
1383
|
+
try {
|
|
1384
|
+
globalThis.__NS_HMR_CLIENT_SOCKET_READY__ = false;
|
|
1385
|
+
}
|
|
1386
|
+
catch { }
|
|
1387
|
+
const overlayStage = hasOpenedHmrSocket ? 'reconnecting' : 'connecting';
|
|
755
1388
|
const baseUrl = getHMRWsUrl() || 'ws://localhost:5173/ns-hmr';
|
|
756
1389
|
const buildCandidates = (url) => {
|
|
757
1390
|
let candidates = [];
|
|
@@ -795,7 +1428,7 @@ function connectHmr() {
|
|
|
795
1428
|
if (seen.has(key))
|
|
796
1429
|
continue;
|
|
797
1430
|
seen.add(key);
|
|
798
|
-
const cand = `${p}://${host}:${port}${u.pathname || '/ns-hmr'}`;
|
|
1431
|
+
const cand = `${p}://${host}:${port}${u.pathname || '/ns-hmr'}${u.search || ''}`;
|
|
799
1432
|
candidates.push(cand);
|
|
800
1433
|
}
|
|
801
1434
|
}
|
|
@@ -811,10 +1444,19 @@ function connectHmr() {
|
|
|
811
1444
|
let idx = 0;
|
|
812
1445
|
const tryNext = () => {
|
|
813
1446
|
if (idx >= candidates.length) {
|
|
1447
|
+
showConnectionOverlayNow('offline', 'Waiting for the Vite websocket to come back.');
|
|
814
1448
|
console.warn('[hmr-client] All WS candidates failed:', candidates.join(', '));
|
|
1449
|
+
setTimeout(connectHmr, 1500);
|
|
815
1450
|
return;
|
|
816
1451
|
}
|
|
817
1452
|
const url = candidates[idx++];
|
|
1453
|
+
const connectionDetail = `${overlayStage === 'reconnecting' ? 'Retrying' : 'Opening'} ${url}`;
|
|
1454
|
+
if (connectionOverlayVisible) {
|
|
1455
|
+
updateConnectionOverlay(overlayStage, connectionDetail);
|
|
1456
|
+
}
|
|
1457
|
+
else {
|
|
1458
|
+
scheduleConnectionOverlay(overlayStage, connectionDetail);
|
|
1459
|
+
}
|
|
818
1460
|
try {
|
|
819
1461
|
if (__NS_ENV_VERBOSE__)
|
|
820
1462
|
console.log('[hmr-client] Connecting to HMR WebSocket:', url);
|
|
@@ -836,7 +1478,25 @@ function connectHmr() {
|
|
|
836
1478
|
sock.onopen = () => {
|
|
837
1479
|
opened = true;
|
|
838
1480
|
clearTimeout(timeout);
|
|
1481
|
+
clearConnectionOverlayTimer();
|
|
1482
|
+
hasOpenedHmrSocket = true;
|
|
1483
|
+
awaitingHealthyHmrMessage = true;
|
|
1484
|
+
try {
|
|
1485
|
+
globalThis.__NS_HMR_CLIENT_SOCKET_READY__ = true;
|
|
1486
|
+
}
|
|
1487
|
+
catch { }
|
|
1488
|
+
if (connectionOverlayVisible) {
|
|
1489
|
+
showConnectionOverlayNow('synchronizing', 'Connected. Synchronizing the HMR graph.');
|
|
1490
|
+
}
|
|
839
1491
|
VERBOSE && console.log('[hmr-client] Connected to HMR WebSocket');
|
|
1492
|
+
// Print the active module reload mode once on first
|
|
1493
|
+
// successful connect so the user can correlate HMR latency
|
|
1494
|
+
// with runtime capability without grepping for protocol
|
|
1495
|
+
// details. The banner is verbose-gated.
|
|
1496
|
+
try {
|
|
1497
|
+
emitHmrModeBannerOnce();
|
|
1498
|
+
}
|
|
1499
|
+
catch { }
|
|
840
1500
|
};
|
|
841
1501
|
sock.onmessage = handleHmrMessage;
|
|
842
1502
|
sock.onerror = (error) => {
|
|
@@ -845,6 +1505,10 @@ function connectHmr() {
|
|
|
845
1505
|
};
|
|
846
1506
|
sock.onclose = (ev) => {
|
|
847
1507
|
clearTimeout(timeout);
|
|
1508
|
+
try {
|
|
1509
|
+
globalThis.__NS_HMR_CLIENT_SOCKET_READY__ = false;
|
|
1510
|
+
}
|
|
1511
|
+
catch { }
|
|
848
1512
|
if (!opened) {
|
|
849
1513
|
// immediate failure during connect → try another candidate
|
|
850
1514
|
if (VERBOSE)
|
|
@@ -854,6 +1518,7 @@ function connectHmr() {
|
|
|
854
1518
|
else {
|
|
855
1519
|
if (VERBOSE)
|
|
856
1520
|
console.log('[hmr-client] WebSocket closed (code', ev?.code, '), will reconnect…');
|
|
1521
|
+
scheduleConnectionOverlay('reconnecting', 'The websocket closed. Waiting to reconnect.', 700);
|
|
857
1522
|
// try to reconnect with full candidate list again
|
|
858
1523
|
setTimeout(connectHmr, 1000);
|
|
859
1524
|
}
|
|
@@ -874,6 +1539,12 @@ async function handleHmrMessage(ev) {
|
|
|
874
1539
|
catch {
|
|
875
1540
|
return;
|
|
876
1541
|
}
|
|
1542
|
+
if (awaitingHealthyHmrMessage && msg) {
|
|
1543
|
+
markHmrConnectionHealthy();
|
|
1544
|
+
}
|
|
1545
|
+
if (VERBOSE && msg?.type) {
|
|
1546
|
+
console.log('[hmr-client] received message', msg.type);
|
|
1547
|
+
}
|
|
877
1548
|
// Notify optional app-level hook after an HMR batch is applied.
|
|
878
1549
|
function notifyAppHmrUpdate(kind, changedIds) {
|
|
879
1550
|
try {
|
|
@@ -885,6 +1556,17 @@ async function handleHmrMessage(ev) {
|
|
|
885
1556
|
catch { }
|
|
886
1557
|
}
|
|
887
1558
|
if (msg) {
|
|
1559
|
+
// `ns:hmr-pending` is a fire-and-forget UX hint emitted by the
|
|
1560
|
+
// server at the START of handleHotUpdate. We drive the
|
|
1561
|
+
// HMR-applying overlay's 'received' frame here (synchronously),
|
|
1562
|
+
// well before the authoritative payload (`ns:angular-update` /
|
|
1563
|
+
// `ns:css-updates`) lands. Skip running any other handlers —
|
|
1564
|
+
// the pending message has no module payload and intentionally
|
|
1565
|
+
// does not bump the graph version.
|
|
1566
|
+
if (msg.type === 'ns:hmr-pending' && typeof msg.path === 'string') {
|
|
1567
|
+
setHmrPendingOverlay(msg.path);
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
888
1570
|
if (msg.type === 'ns:hmr-full-graph') {
|
|
889
1571
|
// Bump a monotonic nonce so HTTP ESM imports can always be cache-busted per update.
|
|
890
1572
|
try {
|
|
@@ -896,6 +1578,34 @@ async function handleHmrMessage(ev) {
|
|
|
896
1578
|
const prevGraph = new Map(graph);
|
|
897
1579
|
setGraphVersion(Number(msg.version || getGraphVersion() || 0));
|
|
898
1580
|
applyFullGraph(msg);
|
|
1581
|
+
hasReceivedFullGraph = true;
|
|
1582
|
+
// Gate: On first boot, the entry-runtime handles all initial module loading
|
|
1583
|
+
// (with the import map already configured). Don't re-import here — the graph
|
|
1584
|
+
// is stored above for future HMR delta comparisons, but modules are already
|
|
1585
|
+
// loaded correctly via the entry-runtime boot sequence.
|
|
1586
|
+
//
|
|
1587
|
+
// Two cases to catch:
|
|
1588
|
+
// 1. Boot still in progress (__NS_HMR_BOOT_COMPLETE__ is false)
|
|
1589
|
+
// 2. Boot already finished but this is the FIRST full-graph (prevGraph was
|
|
1590
|
+
// empty). The WebSocket often connects after entry-runtime finishes, so
|
|
1591
|
+
// boot is "complete" but we still shouldn't re-import — all modules were
|
|
1592
|
+
// just loaded fresh. Only re-import on subsequent full-graphs (reconnect
|
|
1593
|
+
// scenarios) where prevGraph already has entries.
|
|
1594
|
+
if (!globalThis.__NS_HMR_BOOT_COMPLETE__) {
|
|
1595
|
+
if (VERBOSE)
|
|
1596
|
+
console.info('[hmr][full-graph] skipping initial re-import (boot in progress)');
|
|
1597
|
+
const fullIds = Array.isArray(msg.modules) ? msg.modules.map((m) => m?.id).filter(Boolean) : [];
|
|
1598
|
+
notifyAppHmrUpdate('full-graph', fullIds);
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
if (prevGraph.size === 0) {
|
|
1602
|
+
if (VERBOSE)
|
|
1603
|
+
console.info('[hmr][full-graph] skipping re-import on first graph after boot (modules already fresh)');
|
|
1604
|
+
const fullIds = Array.isArray(msg.modules) ? msg.modules.map((m) => m?.id).filter(Boolean) : [];
|
|
1605
|
+
notifyAppHmrUpdate('full-graph', fullIds);
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
// Reconnect / resync case — re-import changed modules as normal.
|
|
899
1609
|
// In some cases (e.g. server chooses full-graph resync / page reload), we won't
|
|
900
1610
|
// receive a delta queue to re-import changed TS modules. Without re-import,
|
|
901
1611
|
// HTTP ESM caching means module bodies (and side effects) won't re-run.
|
|
@@ -926,6 +1636,14 @@ async function handleHmrMessage(ev) {
|
|
|
926
1636
|
});
|
|
927
1637
|
if (toReimport.length && VERBOSE)
|
|
928
1638
|
console.log('[hmr][full-graph] inferred changed modules; re-importing', toReimport);
|
|
1639
|
+
// Evict the inferred changed set before re-importing.
|
|
1640
|
+
// See `processQueue` for the architectural rationale; the
|
|
1641
|
+
// full-graph code path is the resync fallback (server chose
|
|
1642
|
+
// not to send a delta) and shares the same V8 cache pitfall.
|
|
1643
|
+
const fgEvictUrls = buildEvictionUrls(toReimport);
|
|
1644
|
+
const fgEvicted = invalidateModulesByUrls(fgEvictUrls);
|
|
1645
|
+
if (VERBOSE)
|
|
1646
|
+
console.log(`[hmr][full-graph] eviction count=${fgEvictUrls.length} ok=${fgEvicted}`);
|
|
929
1647
|
for (const id of toReimport) {
|
|
930
1648
|
try {
|
|
931
1649
|
const spec = normalizeSpec(id);
|
|
@@ -998,8 +1716,13 @@ async function handleHmrMessage(ev) {
|
|
|
998
1716
|
notifyAppHmrUpdate('delta', deltaIds);
|
|
999
1717
|
return;
|
|
1000
1718
|
}
|
|
1001
|
-
else
|
|
1002
|
-
|
|
1719
|
+
else {
|
|
1720
|
+
if (msg.type === 'ns:angular-update' && typeof msg.version === 'number') {
|
|
1721
|
+
setGraphVersion(Number(msg.version || getGraphVersion() || 0));
|
|
1722
|
+
}
|
|
1723
|
+
if (await handleAngularHotUpdateMessage(msg, { getCore, verbose: VERBOSE })) {
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1003
1726
|
}
|
|
1004
1727
|
}
|
|
1005
1728
|
// On-demand module fetch response (Option A)
|
|
@@ -1140,7 +1863,7 @@ async function performResetRoot(newComponent) {
|
|
|
1140
1863
|
if (cachedRoot)
|
|
1141
1864
|
return cachedRoot;
|
|
1142
1865
|
try {
|
|
1143
|
-
switch (
|
|
1866
|
+
switch (TARGET_FLAVOR) {
|
|
1144
1867
|
case 'vue':
|
|
1145
1868
|
cachedRoot = getRootForVue(newComponent, state);
|
|
1146
1869
|
break;
|
|
@@ -1455,9 +2178,29 @@ export function initHmrClient(opts) {
|
|
|
1455
2178
|
}
|
|
1456
2179
|
g.__NS_HMR_CLIENT_ACTIVE__ = true;
|
|
1457
2180
|
ensureCoreAliasesOnGlobalThis();
|
|
1458
|
-
|
|
2181
|
+
// Defer WebSocket connection until boot completes to avoid native V8 crashes
|
|
2182
|
+
// caused by concurrent WebSocket message handling + HTTP fetch during early startup.
|
|
2183
|
+
// The WebSocket is only needed for HMR updates, not the initial boot sequence.
|
|
2184
|
+
if (g.__NS_HMR_BOOT_COMPLETE__) {
|
|
2185
|
+
connectHmr();
|
|
2186
|
+
}
|
|
2187
|
+
else {
|
|
2188
|
+
const waitForBoot = () => {
|
|
2189
|
+
if (globalThis.__NS_HMR_BOOT_COMPLETE__) {
|
|
2190
|
+
if (VERBOSE)
|
|
2191
|
+
console.log('[hmr-client] boot complete, connecting HMR WebSocket');
|
|
2192
|
+
connectHmr();
|
|
2193
|
+
}
|
|
2194
|
+
else {
|
|
2195
|
+
setTimeout(waitForBoot, 100);
|
|
2196
|
+
}
|
|
2197
|
+
};
|
|
2198
|
+
if (VERBOSE)
|
|
2199
|
+
console.log('[hmr-client] deferring WebSocket connection until boot completes');
|
|
2200
|
+
setTimeout(waitForBoot, 100);
|
|
2201
|
+
}
|
|
1459
2202
|
// Best-effort: install back wrapper even before first remount; original root may be captured later
|
|
1460
|
-
switch (
|
|
2203
|
+
switch (TARGET_FLAVOR) {
|
|
1461
2204
|
case 'vue':
|
|
1462
2205
|
ensureBackWrapperInstalled(performResetRoot, getCore);
|
|
1463
2206
|
break;
|