@nativescript/vite 8.0.0-alpha.29 → 8.0.0-alpha.30
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/hmr/server/angular-root-component.d.ts +79 -0
- package/hmr/server/angular-root-component.js +149 -0
- package/hmr/server/angular-root-component.js.map +1 -0
- package/hmr/server/hmr-module-graph.d.ts +37 -0
- package/hmr/server/hmr-module-graph.js +214 -0
- package/hmr/server/hmr-module-graph.js.map +1 -0
- package/hmr/server/index.js +1 -0
- package/hmr/server/index.js.map +1 -1
- package/hmr/server/ns-rt-route.d.ts +5 -0
- package/hmr/server/ns-rt-route.js +35 -0
- package/hmr/server/ns-rt-route.js.map +1 -0
- package/hmr/server/require-guard.d.ts +1 -0
- package/hmr/server/require-guard.js +12 -0
- package/hmr/server/require-guard.js.map +1 -0
- package/hmr/server/route-helpers.d.ts +7 -0
- package/hmr/server/route-helpers.js +13 -0
- package/hmr/server/route-helpers.js.map +1 -0
- package/hmr/server/server-origin.d.ts +12 -0
- package/hmr/server/server-origin.js +66 -0
- package/hmr/server/server-origin.js.map +1 -0
- package/hmr/server/websocket-core-bridge.js +0 -11
- package/hmr/server/websocket-core-bridge.js.map +1 -1
- package/hmr/server/websocket-device-transform.d.ts +21 -0
- package/hmr/server/websocket-device-transform.js +1570 -0
- package/hmr/server/websocket-device-transform.js.map +1 -0
- package/hmr/server/websocket-hot-update.d.ts +51 -0
- package/hmr/server/websocket-hot-update.js +1160 -0
- package/hmr/server/websocket-hot-update.js.map +1 -0
- package/hmr/server/websocket-import-map-route.d.ts +15 -0
- package/hmr/server/websocket-import-map-route.js +44 -0
- package/hmr/server/websocket-import-map-route.js.map +1 -0
- package/hmr/server/websocket-ns-core.d.ts +21 -0
- package/hmr/server/websocket-ns-core.js +305 -0
- package/hmr/server/websocket-ns-core.js.map +1 -0
- package/hmr/server/websocket-ns-entry.d.ts +22 -0
- package/hmr/server/websocket-ns-entry.js +150 -0
- package/hmr/server/websocket-ns-entry.js.map +1 -0
- package/hmr/server/websocket-ns-m.d.ts +34 -0
- package/hmr/server/websocket-ns-m.js +853 -0
- package/hmr/server/websocket-ns-m.js.map +1 -0
- package/hmr/server/websocket-served-module-helpers.d.ts +1 -1
- package/hmr/server/websocket-served-module-helpers.js +1 -1
- package/hmr/server/websocket-served-module-helpers.js.map +1 -1
- package/hmr/server/websocket-sfc.d.ts +24 -0
- package/hmr/server/websocket-sfc.js +1223 -0
- package/hmr/server/websocket-sfc.js.map +1 -0
- package/hmr/server/websocket-txn.js +2 -8
- package/hmr/server/websocket-txn.js.map +1 -1
- package/hmr/server/websocket-vendor-unifier.js +2 -8
- package/hmr/server/websocket-vendor-unifier.js.map +1 -1
- package/hmr/server/websocket.d.ts +1 -44
- package/hmr/server/websocket.js +588 -6691
- package/hmr/server/websocket.js.map +1 -1
- package/hmr/shared/runtime/root-placeholder-view.d.ts +19 -0
- package/hmr/shared/runtime/root-placeholder-view.js +310 -0
- package/hmr/shared/runtime/root-placeholder-view.js.map +1 -0
- package/hmr/shared/runtime/root-placeholder.js +1 -309
- package/hmr/shared/runtime/root-placeholder.js.map +1 -1
- package/hmr/shared/vendor/manifest-collect.d.ts +32 -0
- package/hmr/shared/vendor/manifest-collect.js +512 -0
- package/hmr/shared/vendor/manifest-collect.js.map +1 -0
- package/hmr/shared/vendor/manifest.d.ts +1 -35
- package/hmr/shared/vendor/manifest.js +3 -914
- package/hmr/shared/vendor/manifest.js.map +1 -1
- package/hmr/shared/vendor/vendor-device-shim.d.ts +1 -0
- package/hmr/shared/vendor/vendor-device-shim.js +208 -0
- package/hmr/shared/vendor/vendor-device-shim.js.map +1 -0
- package/hmr/shared/vendor/vendor-esbuild-plugins.d.ts +16 -0
- package/hmr/shared/vendor/vendor-esbuild-plugins.js +203 -0
- package/hmr/shared/vendor/vendor-esbuild-plugins.js.map +1 -0
- package/package.json +1 -1
- package/hmr/server/websocket-vue-sfc.d.ts +0 -26
- package/hmr/server/websocket-vue-sfc.js +0 -1053
- package/hmr/server/websocket-vue-sfc.js.map +0 -1
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import * as PAT from './constants.js';
|
|
4
|
+
import { isRuntimeGraphExcludedPath } from './runtime-graph-filter.js';
|
|
5
|
+
import { isWithinHmrScope } from '../../helpers/hmr-scope.js';
|
|
6
|
+
import { canonicalizeTransformRequestCacheKey, collectAngularEvictionUrls, collectAngularHotUpdateRoots, collectAngularTransformCacheInvalidationUrls, collectAngularTransitiveImportersForInvalidation, collectGraphUpdateModulesForHotUpdate, shouldInvalidateAngularTransitiveImporters, shouldSuppressDefaultViteHotUpdate } from './websocket-angular-hot-update.js';
|
|
7
|
+
import { getAppCssState } from '../../helpers/app-css-state.js';
|
|
8
|
+
import { collectCssHotUpdatePaths } from './websocket-css-hot-update.js';
|
|
9
|
+
import { classifyHmrUpdateKind, formatHmrUpdateSummary } from './perf-instrumentation.js';
|
|
10
|
+
import { createHmrPendingMessage } from './websocket-hmr-pending.js';
|
|
11
|
+
import { isCoreGlobalsReference, isNativeScriptCoreModule, isNativeScriptPluginModule, resolveVendorFromCandidate } from './websocket-module-specifiers.js';
|
|
12
|
+
import { cleanCode, collectImportDependencies, processSfcCode, rewriteImports } from './websocket-device-transform.js';
|
|
13
|
+
import { isSameAngularModuleRel } from './angular-root-component.js';
|
|
14
|
+
/**
|
|
15
|
+
* The NativeScript `handleHotUpdate` hook, extracted verbatim from
|
|
16
|
+
* `createHmrWebSocketPlugin`. Receives the live Vite {@link HmrContext} plus an
|
|
17
|
+
* injected {@link NsHotUpdateContext}. The early `const` block re-binds every
|
|
18
|
+
* injected dependency to the original closure-local names so the (large) body
|
|
19
|
+
* below is a faithful, behaviour-preserving move.
|
|
20
|
+
*/
|
|
21
|
+
export async function handleNsHotUpdate(ctx, deps) {
|
|
22
|
+
const { wss, moduleGraph, strategy, verbose, sfcFileMap, depFileMap, sharedTransformRequest, getServerOrigin, getHmrSourceRootsCached, getBootstrapEntryRelPath, isSocketClientOpen, getHmrSocketRole, shouldRemapImport, rememberAngularReloadSuppression, getRootComponentIdentity } = deps;
|
|
23
|
+
const APP_ROOT_DIR = deps.appRootDir;
|
|
24
|
+
const graphInitialPopulationPromise = deps.getGraphInitialPopulationPromise();
|
|
25
|
+
const { file, server } = ctx;
|
|
26
|
+
if (!wss) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (isRuntimeGraphExcludedPath(file)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Authoritative "what triggers HMR" gate, applied before the pending
|
|
33
|
+
// overlay broadcast below: react only to files inside the app source
|
|
34
|
+
// dir (`appPath`) or a tsconfig-configured shared library.
|
|
35
|
+
if (!isWithinHmrScope(file, getHmrSourceRootsCached())) {
|
|
36
|
+
if (verbose) {
|
|
37
|
+
console.log(`[ns-hmr][server] ignored change (outside HMR source scope): ${file}`);
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Always-on update timing. Captures the four phases (await,
|
|
42
|
+
// framework, broadcast, total) plus invalidated module count
|
|
43
|
+
// and recipient count. Emitted at the end of this function via
|
|
44
|
+
// `emitHmrUpdateSummary()`. Single line, always-on so a
|
|
45
|
+
// 6-second `.ts` save is immediately visible without flipping
|
|
46
|
+
// verbose.
|
|
47
|
+
const updateRoot = server.config.root || process.cwd();
|
|
48
|
+
const updateRel = (() => {
|
|
49
|
+
try {
|
|
50
|
+
return '/' + path.posix.normalize(path.relative(updateRoot, file)).split(path.sep).join('/');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return file;
|
|
54
|
+
}
|
|
55
|
+
})();
|
|
56
|
+
const updateMetrics = {
|
|
57
|
+
file: updateRel,
|
|
58
|
+
kind: classifyHmrUpdateKind(file),
|
|
59
|
+
t0: Date.now(),
|
|
60
|
+
tAfterAwait: 0,
|
|
61
|
+
tAfterFramework: 0,
|
|
62
|
+
tEnd: 0,
|
|
63
|
+
invalidated: 0,
|
|
64
|
+
recipients: 0,
|
|
65
|
+
// Narrowing diagnostic — populated by the angular branch when
|
|
66
|
+
// the changed file is `.ts`, otherwise remains undefined and is
|
|
67
|
+
// omitted from the summary line entirely.
|
|
68
|
+
narrowed: undefined,
|
|
69
|
+
emitted: false,
|
|
70
|
+
};
|
|
71
|
+
// Broadcast a "pending" notification at the very start of
|
|
72
|
+
// handleHotUpdate so the client can show the HMR-applying
|
|
73
|
+
// overlay BEFORE we spend time on graph updates / transforms /
|
|
74
|
+
// dependency analysis (typically 7–200ms on a warm cache).
|
|
75
|
+
// Without this, the overlay only appears at `ns:angular-update`
|
|
76
|
+
// broadcast time and the user perceives a "delayed" reaction
|
|
77
|
+
// to their save.
|
|
78
|
+
//
|
|
79
|
+
// Fire-and-forget: a failed pending broadcast must never
|
|
80
|
+
// hold up the actual update. The client treats receipt of
|
|
81
|
+
// `ns:angular-update` (or `ns:css-updates`) as authoritative;
|
|
82
|
+
// the pending message is purely a UX hint.
|
|
83
|
+
try {
|
|
84
|
+
const pendingPayload = JSON.stringify(createHmrPendingMessage({
|
|
85
|
+
origin: getServerOrigin(server),
|
|
86
|
+
path: updateMetrics.file,
|
|
87
|
+
kind: updateMetrics.kind,
|
|
88
|
+
timestamp: updateMetrics.t0,
|
|
89
|
+
}));
|
|
90
|
+
wss.clients.forEach((client) => {
|
|
91
|
+
if (isSocketClientOpen(client)) {
|
|
92
|
+
try {
|
|
93
|
+
client.send(pendingPayload);
|
|
94
|
+
}
|
|
95
|
+
catch { }
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch { }
|
|
100
|
+
const emitHmrUpdateSummary = () => {
|
|
101
|
+
if (updateMetrics.emitted)
|
|
102
|
+
return;
|
|
103
|
+
updateMetrics.emitted = true;
|
|
104
|
+
updateMetrics.tEnd = Date.now();
|
|
105
|
+
try {
|
|
106
|
+
const awaitMs = (updateMetrics.tAfterAwait || updateMetrics.t0) - updateMetrics.t0;
|
|
107
|
+
const frameworkMs = (updateMetrics.tAfterFramework || updateMetrics.tAfterAwait || updateMetrics.t0) - (updateMetrics.tAfterAwait || updateMetrics.t0);
|
|
108
|
+
const broadcastMs = updateMetrics.tEnd - (updateMetrics.tAfterFramework || updateMetrics.tAfterAwait || updateMetrics.t0);
|
|
109
|
+
const totalMs = updateMetrics.tEnd - updateMetrics.t0;
|
|
110
|
+
console.info(formatHmrUpdateSummary({
|
|
111
|
+
file: updateMetrics.file,
|
|
112
|
+
kind: updateMetrics.kind,
|
|
113
|
+
awaitMs,
|
|
114
|
+
frameworkMs,
|
|
115
|
+
broadcastMs,
|
|
116
|
+
totalMs,
|
|
117
|
+
invalidated: updateMetrics.invalidated,
|
|
118
|
+
recipients: updateMetrics.recipients,
|
|
119
|
+
narrowed: updateMetrics.narrowed,
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
catch { }
|
|
123
|
+
};
|
|
124
|
+
// The first /ns/m request kicks off populateInitialGraph in the
|
|
125
|
+
// background. If an HMR update races in before that walk
|
|
126
|
+
// completes, we'd lose transitive-importer data. Await
|
|
127
|
+
// completion here so the delta computation below always sees a
|
|
128
|
+
// populated graph.
|
|
129
|
+
if (graphInitialPopulationPromise) {
|
|
130
|
+
try {
|
|
131
|
+
await graphInitialPopulationPromise;
|
|
132
|
+
}
|
|
133
|
+
catch { }
|
|
134
|
+
}
|
|
135
|
+
updateMetrics.tAfterAwait = Date.now();
|
|
136
|
+
// Graph update for this file change (wrapped to avoid aborting rest of handler)
|
|
137
|
+
try {
|
|
138
|
+
const skipAngularHtmlGraphUpdate = strategy.flavor === 'angular' && /\.(html|htm)$/i.test(file);
|
|
139
|
+
if (!skipAngularHtmlGraphUpdate) {
|
|
140
|
+
const graphTargets = collectGraphUpdateModulesForHotUpdate({
|
|
141
|
+
file,
|
|
142
|
+
flavor: strategy.flavor,
|
|
143
|
+
modules: ctx.modules,
|
|
144
|
+
getModuleById: (id) => server.moduleGraph.getModuleById(id),
|
|
145
|
+
verbose,
|
|
146
|
+
});
|
|
147
|
+
for (const mod of graphTargets) {
|
|
148
|
+
if (!mod?.id)
|
|
149
|
+
continue;
|
|
150
|
+
try {
|
|
151
|
+
const deps = Array.from(mod.importedModules || [])
|
|
152
|
+
.map((m) => (m.id || '').replace(/\?.*$/, ''))
|
|
153
|
+
.filter(Boolean);
|
|
154
|
+
const transformed = await server.transformRequest(mod.id);
|
|
155
|
+
const code = transformed?.code || '';
|
|
156
|
+
moduleGraph.upsert((mod.id || '').replace(/\?.*$/, ''), code, deps, {
|
|
157
|
+
emitDeltaOnInsert: true,
|
|
158
|
+
// Defer the delta broadcast until AFTER the framework
|
|
159
|
+
// hot-update handler has had a chance to invalidate the
|
|
160
|
+
// shared transform-request cache + Vite's moduleGraph
|
|
161
|
+
// for the changed file and its transitive importers.
|
|
162
|
+
// Otherwise the client races: it receives the delta
|
|
163
|
+
// (eviction + re-import via tagged URL) before the
|
|
164
|
+
// server has purged its caches, and the re-import is
|
|
165
|
+
// served from cache → V8 evaluates the previous save's
|
|
166
|
+
// transformed code → patchRegistry runs against an
|
|
167
|
+
// unchanged source → the visible page is "one save
|
|
168
|
+
// behind". Angular has always taken this path; Solid
|
|
169
|
+
// needs the same contract because Solid HMR depends
|
|
170
|
+
// on the client re-fetching the just-changed module
|
|
171
|
+
// to drive `solid-refresh.patchRegistry`.
|
|
172
|
+
broadcastDelta: strategy.flavor !== 'angular' && strategy.flavor !== 'solid',
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (verbose)
|
|
177
|
+
console.warn('[hmr-ws][v2] failed graph update target', mod.id, error);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
if (verbose)
|
|
184
|
+
console.warn('[hmr-ws][v2] failed graph update', e);
|
|
185
|
+
}
|
|
186
|
+
const root = server.config.root || process.cwd();
|
|
187
|
+
// CSS hot-update — handled BEFORE the project-scope filter
|
|
188
|
+
// because workspace `@import` deps live outside `<root>/`.
|
|
189
|
+
// The helper maps in-scope edits to their own path and
|
|
190
|
+
// out-of-scope edits to `app.css` (Vite re-runs PostCSS
|
|
191
|
+
// through the `@import` chain on the next fetch).
|
|
192
|
+
if (file.endsWith('.css')) {
|
|
193
|
+
const cssPaths = collectCssHotUpdatePaths({
|
|
194
|
+
file,
|
|
195
|
+
root,
|
|
196
|
+
appRootDir: APP_ROOT_DIR,
|
|
197
|
+
appEntryCss: path.resolve(root, APP_ROOT_DIR, 'app.css'),
|
|
198
|
+
});
|
|
199
|
+
if (cssPaths.length > 0) {
|
|
200
|
+
updateMetrics.tAfterFramework = Date.now();
|
|
201
|
+
try {
|
|
202
|
+
const origin = getServerOrigin(server);
|
|
203
|
+
const timestamp = Date.now();
|
|
204
|
+
const msg = {
|
|
205
|
+
type: 'ns:css-updates',
|
|
206
|
+
origin,
|
|
207
|
+
updates: cssPaths.map((cssPath) => ({
|
|
208
|
+
type: 'css-update',
|
|
209
|
+
path: cssPath,
|
|
210
|
+
acceptedPath: cssPath,
|
|
211
|
+
timestamp,
|
|
212
|
+
})),
|
|
213
|
+
};
|
|
214
|
+
wss.clients.forEach((client) => {
|
|
215
|
+
if (isSocketClientOpen(client)) {
|
|
216
|
+
client.send(JSON.stringify(msg));
|
|
217
|
+
updateMetrics.recipients += 1;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
console.warn('[hmr-ws] CSS update failed:', error);
|
|
223
|
+
}
|
|
224
|
+
if (verbose)
|
|
225
|
+
console.log(`[hmr-ws] Hot update for: ${file} → broadcast CSS paths: ${cssPaths.join(', ')}`);
|
|
226
|
+
emitHmrUpdateSummary();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
// CSS without a broadcast target (no appEntryCss
|
|
230
|
+
// configured) — fall through to the scope filter.
|
|
231
|
+
}
|
|
232
|
+
const srcDir = `${root}/src`;
|
|
233
|
+
const coreDir = `${root}/core`;
|
|
234
|
+
const appDir = `${root}/${APP_ROOT_DIR}`;
|
|
235
|
+
const normalizedFile = file.split(path.sep).join('/');
|
|
236
|
+
const inSrcOrCore = normalizedFile.includes(srcDir) || normalizedFile.includes(coreDir);
|
|
237
|
+
const inApp = normalizedFile.includes(appDir);
|
|
238
|
+
const shouldIgnore = !(inSrcOrCore || inApp);
|
|
239
|
+
if (shouldIgnore)
|
|
240
|
+
return;
|
|
241
|
+
if (verbose)
|
|
242
|
+
console.log(`[hmr-ws] Hot update for: ${file}`);
|
|
243
|
+
// Tailwind / content-scanning CSS broadcast for non-CSS edits.
|
|
244
|
+
//
|
|
245
|
+
// Background: when a `.html` template or `.ts` file scanned
|
|
246
|
+
// by Tailwind's `content` config gets a brand-new utility
|
|
247
|
+
// class (e.g. `pt-6` that was never used in the codebase
|
|
248
|
+
// before), the booted CSS bundle doesn't contain a rule for
|
|
249
|
+
// it. The Angular template HMR swaps the markup, the view
|
|
250
|
+
// re-renders, the class lookup misses, and the layout
|
|
251
|
+
// regresses to its default.
|
|
252
|
+
//
|
|
253
|
+
// In a "normal" Vite setup, the `vite:css` plugin consumes
|
|
254
|
+
// each PostCSS `dependency` message via `addWatchFile`, and
|
|
255
|
+
// `vite:css-analysis` later registers each watched file as
|
|
256
|
+
// an importer of the CSS module. A content-file edit then
|
|
257
|
+
// invalidates the CSS module through the moduleGraph and
|
|
258
|
+
// `ctx.modules`/`mod.importers` would surface it.
|
|
259
|
+
//
|
|
260
|
+
// NS HMR breaks that chain: `app.css` is loaded via a
|
|
261
|
+
// virtual module (`virtual:ns-app-css`) whose `load` hook
|
|
262
|
+
// calls `preprocessCSS(...)` and emits a JS module — the
|
|
263
|
+
// CSS itself is never a moduleGraph node, so the importer
|
|
264
|
+
// chain never forms. `ctx.modules` for the html edit only
|
|
265
|
+
// contains the html-as-Angular-template module with the
|
|
266
|
+
// component `.ts` as its importer.
|
|
267
|
+
//
|
|
268
|
+
// To bridge that gap, `mainEntryPlugin` stores the set of
|
|
269
|
+
// `preprocessCSS` deps for `app.css` on the server as
|
|
270
|
+
// `__nsAppCssDeps` (refreshed when `app.css` /
|
|
271
|
+
// `tailwind.config.*` change, or when files are added /
|
|
272
|
+
// removed). If the changed file is in that set, we
|
|
273
|
+
// broadcast a `ns:css-updates` for `app.css` so the device
|
|
274
|
+
// fetches fresh CSS through `?direct=1` and Vite re-runs
|
|
275
|
+
// PostCSS+Tailwind — picking up the new utility class.
|
|
276
|
+
//
|
|
277
|
+
// This MUST run before the framework branches because
|
|
278
|
+
// several of them return early (notably the Angular HTML
|
|
279
|
+
// live-reload path), and the broadcast must land alongside
|
|
280
|
+
// the framework's own template-update payload.
|
|
281
|
+
if (!file.endsWith('.css')) {
|
|
282
|
+
try {
|
|
283
|
+
const appCssState = getAppCssState(server);
|
|
284
|
+
const deps = appCssState?.deps;
|
|
285
|
+
const appCssPath = appCssState?.path;
|
|
286
|
+
if (deps && appCssPath) {
|
|
287
|
+
const normalizedFile = path.resolve(file).replace(/\\/g, '/');
|
|
288
|
+
if (deps.has(normalizedFile)) {
|
|
289
|
+
const rootPosix = root.replace(/\\/g, '/').replace(/\/$/, '');
|
|
290
|
+
const relRaw = path.posix.normalize(path.posix.relative(rootPosix, appCssPath));
|
|
291
|
+
const appCssRel = relRaw && relRaw !== '.' && !relRaw.startsWith('..') ? (relRaw.startsWith('/') ? relRaw : `/${relRaw}`) : null;
|
|
292
|
+
if (appCssRel) {
|
|
293
|
+
const origin = getServerOrigin(server);
|
|
294
|
+
const timestamp = Date.now();
|
|
295
|
+
const msg = {
|
|
296
|
+
type: 'ns:css-updates',
|
|
297
|
+
origin,
|
|
298
|
+
updates: [
|
|
299
|
+
{
|
|
300
|
+
type: 'css-update',
|
|
301
|
+
path: appCssRel,
|
|
302
|
+
acceptedPath: appCssRel,
|
|
303
|
+
timestamp,
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
wss.clients.forEach((client) => {
|
|
308
|
+
if (isSocketClientOpen(client)) {
|
|
309
|
+
try {
|
|
310
|
+
client.send(JSON.stringify(msg));
|
|
311
|
+
updateMetrics.recipients += 1;
|
|
312
|
+
}
|
|
313
|
+
catch { }
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
if (verbose)
|
|
317
|
+
console.info(`[ns-hmr][server] Tailwind/PostCSS content-file edit (${path.basename(file)}) broadcast ${appCssRel}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
console.warn('[hmr-ws] CSS content-source broadcast failed:', error);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Framework-specific hot update handling
|
|
327
|
+
if (strategy.flavor === 'angular') {
|
|
328
|
+
// For Angular, react to component TS or external template HTML changes under /src
|
|
329
|
+
const isHtml = file.endsWith('.html');
|
|
330
|
+
const isTs = file.endsWith('.ts');
|
|
331
|
+
// Web-style template HMR opt-in: when the user enables Angular's
|
|
332
|
+
// `liveReload` (Analog's flag, mirrored from `--hmr` in
|
|
333
|
+
// `configuration/angular.ts`), `.html` edits are owned by
|
|
334
|
+
// Analog's `handleHotUpdate` which sends
|
|
335
|
+
// `server.ws.send('angular:component-update', { id, timestamp })`.
|
|
336
|
+
// The runtime listener registered in each compiled component
|
|
337
|
+
// `.mjs` then dynamic-imports `/@ng/component?c=<id>&t=<ts>` and
|
|
338
|
+
// calls `ɵɵreplaceMetadata` on the live class — swapping the
|
|
339
|
+
// template definition AND walking live `LView`s to recreate
|
|
340
|
+
// matching views in-place. NO Angular reboot, NO route navigation.
|
|
341
|
+
//
|
|
342
|
+
// The NS reboot path (`ns:angular-update` → `__reboot_ng_modules__`)
|
|
343
|
+
// must be SKIPPED for HTML edits when this is on; otherwise both
|
|
344
|
+
// fire, the reboot wins, and we lose the in-place swap. The
|
|
345
|
+
// reboot path stays intact for `.ts` edits — those genuinely
|
|
346
|
+
// change module-level code (services, route configs, NgModule
|
|
347
|
+
// providers) that Angular's `ɵɵreplaceMetadata` can't reach.
|
|
348
|
+
//
|
|
349
|
+
// We detect "live reload mode is on" by checking that the
|
|
350
|
+
// `analogjs-live-reload-plugin` registered itself with the
|
|
351
|
+
// dev server. That plugin only exists when `liveReload: true`
|
|
352
|
+
// was passed to `angular()` in `configuration/angular.ts`,
|
|
353
|
+
// which gates on `hmrActive`. So this check is a clean
|
|
354
|
+
// boolean: true iff the in-place pipeline is wired up.
|
|
355
|
+
const angularLiveReloadActive = (server.config?.plugins ?? []).some((plugin) => plugin?.name === 'analogjs-live-reload-plugin');
|
|
356
|
+
// Root-component edits must NOT take Analog's in-place
|
|
357
|
+
// `ɵɵreplaceMetadata` path: the root component hosts the
|
|
358
|
+
// navigation `Frame` via `<page-router-outlet>`, and replacing
|
|
359
|
+
// its metadata recreates the root view without re-navigating,
|
|
360
|
+
// leaving a permanent white screen. We route the edit to the
|
|
361
|
+
// reboot broadcast below instead (which re-bootstraps and
|
|
362
|
+
// replays route state). The companion guard in the websocket
|
|
363
|
+
// bridge drops the in-place `angular:component-update` event for
|
|
364
|
+
// the root so the two paths don't race. `.ts` root edits already
|
|
365
|
+
// fall through to the reboot path; this only re-routes `.html`.
|
|
366
|
+
const rootComponent = getRootComponentIdentity();
|
|
367
|
+
// `isSameAngularModuleRel` normalizes separators + leading slash internally,
|
|
368
|
+
// so the raw project-relative path can be passed straight through.
|
|
369
|
+
const isRootComponentEdit = !!rootComponent && isSameAngularModuleRel(rootComponent.moduleRel, path.relative(root, file));
|
|
370
|
+
if (isHtml && angularLiveReloadActive && !isRootComponentEdit) {
|
|
371
|
+
updateMetrics.tAfterFramework = Date.now();
|
|
372
|
+
if (verbose) {
|
|
373
|
+
const rel = '/' + path.relative(root, file).split(path.sep).join('/');
|
|
374
|
+
console.info(`[ns-hmr][server] HTML edit handed off to Analog component-update path; skipping ns:angular-update broadcast (file=${rel})`);
|
|
375
|
+
}
|
|
376
|
+
// Re-query the moduleGraph for this file AFTER awaiting
|
|
377
|
+
// `graphInitialPopulationPromise` (done at the top of
|
|
378
|
+
// `handleHotUpdate`) and return the freshly-discovered
|
|
379
|
+
// modules so they propagate to Analog's `handleHotUpdate`
|
|
380
|
+
// in the same chain.
|
|
381
|
+
//
|
|
382
|
+
// Vite v8 builds the initial `mixedHmrContext.modules`
|
|
383
|
+
// from `mixedModuleGraph.getModulesByFile(file)` BEFORE
|
|
384
|
+
// any plugin runs. On the very first save after a cold
|
|
385
|
+
// dev-server start, the moduleGraph for the changed
|
|
386
|
+
// `.html` template has not yet been populated — that
|
|
387
|
+
// population happens lazily via `populateInitialGraph`
|
|
388
|
+
// → `transformRequest` → Analog's `transform` hook →
|
|
389
|
+
// `addWatchFile(htmlFile)` → `vite:import-analysis`
|
|
390
|
+
// consumes `_addedImports` and finally calls
|
|
391
|
+
// `moduleGraph.updateModuleInfo` which registers the
|
|
392
|
+
// `html → component.ts` importer relationship in
|
|
393
|
+
// `fileToModulesMap`. All of that work races against the
|
|
394
|
+
// file-watcher event for the `.html` edit, and the
|
|
395
|
+
// watcher event almost always wins — so `ctx.modules`
|
|
396
|
+
// arrives as `[]` even though the component is fully
|
|
397
|
+
// compiled and ready to receive an in-place template
|
|
398
|
+
// swap.
|
|
399
|
+
//
|
|
400
|
+
// Returning `undefined` here would propagate that empty
|
|
401
|
+
// `ctx.modules` to the next plugin (Analog's handler),
|
|
402
|
+
// which iterates with `ctx.modules.forEach(mod => mod
|
|
403
|
+
// .importers.forEach(imp => …))` — a no-op when
|
|
404
|
+
// `ctx.modules` is empty. Analog never broadcasts
|
|
405
|
+
// `angular:component-update`, never marks anything
|
|
406
|
+
// self-accepting, and Vite falls back to a `full-reload`
|
|
407
|
+
// payload that the device runtime cannot honor (NS apps
|
|
408
|
+
// don't have a browser-style page reload). The
|
|
409
|
+
// user-visible symptom is exactly the "first save logs
|
|
410
|
+
// `(client) page reload` and the simulator gets stuck
|
|
411
|
+
// on the HMR-applying overlay forever" failure we hit
|
|
412
|
+
// before this re-query was added.
|
|
413
|
+
//
|
|
414
|
+
// Since we already `await graphInitialPopulationPromise`
|
|
415
|
+
// at the top of this function, by this point the
|
|
416
|
+
// moduleGraph IS populated (every component file in
|
|
417
|
+
// `src/` has been transformed and `addWatchFile` has
|
|
418
|
+
// been consumed by `import-analysis`). A fresh
|
|
419
|
+
// `getModulesByFile(file)` call now returns the template
|
|
420
|
+
// module with the importing component's module in
|
|
421
|
+
// `.importers`. Returning that array overwrites
|
|
422
|
+
// `mixedHmrContext.modules` so Analog's handler — which
|
|
423
|
+
// runs RIGHT AFTER us in the same chain — sees the
|
|
424
|
+
// populated importer graph, identifies the component
|
|
425
|
+
// class via `classNames.get(imp.id)`, and broadcasts
|
|
426
|
+
// `angular:component-update` for `ɵɵreplaceMetadata`.
|
|
427
|
+
//
|
|
428
|
+
// We still skip the reboot path (`ns:angular-update`)
|
|
429
|
+
// for HTML edits — control never reaches the
|
|
430
|
+
// reboot-broadcast block below because of the `return`
|
|
431
|
+
// here. The default-Vite-full-reload suppression is now
|
|
432
|
+
// Analog's responsibility: it marks the changed module
|
|
433
|
+
// self-accepting, which tells Vite the update is
|
|
434
|
+
// handled and prevents the fallback.
|
|
435
|
+
let resolvedModules = ctx.modules;
|
|
436
|
+
try {
|
|
437
|
+
const fresh = server.moduleGraph?.getModulesByFile?.(file);
|
|
438
|
+
if (fresh && fresh.size > 0) {
|
|
439
|
+
resolvedModules = [...fresh];
|
|
440
|
+
if (verbose) {
|
|
441
|
+
console.info(`[ns-hmr][server] re-queried modules after graph population: count=${resolvedModules.length} (was ${ctx.modules?.length ?? 0})`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
catch (refetchErr) {
|
|
446
|
+
if (verbose) {
|
|
447
|
+
console.warn('[ns-hmr][server] failed to re-query moduleGraph for html update', refetchErr);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
emitHmrUpdateSummary();
|
|
451
|
+
return resolvedModules;
|
|
452
|
+
}
|
|
453
|
+
const angularHotUpdateRoots = collectAngularHotUpdateRoots({
|
|
454
|
+
file,
|
|
455
|
+
modules: ctx.modules,
|
|
456
|
+
getModuleById: (id) => server.moduleGraph.getModuleById(id),
|
|
457
|
+
getModulesByFile: (targetFile) => server.moduleGraph.getModulesByFile?.(targetFile),
|
|
458
|
+
});
|
|
459
|
+
if (verbose) {
|
|
460
|
+
console.info(`[ns-hmr][server] hot-update file=${file} isHtml=${isHtml} isTs=${isTs} ctxModules=${Array.from(ctx.modules || []).length} hotUpdateRoots=${angularHotUpdateRoots.length} (${angularHotUpdateRoots
|
|
461
|
+
.map((m) => m?.id ?? '(none)')
|
|
462
|
+
.slice(0, 8)
|
|
463
|
+
.join(', ')}${angularHotUpdateRoots.length > 8 ? ', …' : ''})`);
|
|
464
|
+
}
|
|
465
|
+
if (!(isHtml || isTs))
|
|
466
|
+
return;
|
|
467
|
+
updateMetrics.invalidated += angularHotUpdateRoots.length;
|
|
468
|
+
if (angularHotUpdateRoots.length) {
|
|
469
|
+
for (const mod of angularHotUpdateRoots) {
|
|
470
|
+
try {
|
|
471
|
+
server.moduleGraph.invalidateModule(mod);
|
|
472
|
+
}
|
|
473
|
+
catch (invalidationError) {
|
|
474
|
+
if (verbose) {
|
|
475
|
+
console.warn('[hmr-ws][angular] hot-update root invalidation failed', mod?.id, invalidationError);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (verbose) {
|
|
480
|
+
console.log('[hmr-ws][angular] invalidated hot-update root modules:', angularHotUpdateRoots.length);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const angularTransitiveInvalidationRoots = (angularHotUpdateRoots.length ? angularHotUpdateRoots : ctx.modules);
|
|
484
|
+
// Read the source for `.ts/.tsx/.js/.jsx` edits so
|
|
485
|
+
// `shouldInvalidateAngularTransitiveImporters` can
|
|
486
|
+
// distinguish leaf modules (constants/utils) from real
|
|
487
|
+
// Angular files. If `ctx.read()` throws (file deleted, race
|
|
488
|
+
// against the watcher), `angularChangedSource` stays
|
|
489
|
+
// undefined and we fall back to the conservative "always
|
|
490
|
+
// invalidate transitively" behavior.
|
|
491
|
+
let angularChangedSource;
|
|
492
|
+
if (isTs) {
|
|
493
|
+
try {
|
|
494
|
+
angularChangedSource = await ctx.read();
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
angularChangedSource = undefined;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const angularNeedsTransitive = shouldInvalidateAngularTransitiveImporters({
|
|
501
|
+
flavor: strategy.flavor,
|
|
502
|
+
file,
|
|
503
|
+
source: angularChangedSource,
|
|
504
|
+
});
|
|
505
|
+
// Surface the narrowing decision on every `.ts` Angular hot
|
|
506
|
+
// update (HTML routes always invalidate transitively and
|
|
507
|
+
// aren't subject to narrowing, so we leave them as
|
|
508
|
+
// `undefined` — the field is omitted from the summary line).
|
|
509
|
+
// The boolean is the inverse of `angularNeedsTransitive`
|
|
510
|
+
// because "needs transitive" is the broad (un-narrowed)
|
|
511
|
+
// behavior.
|
|
512
|
+
if (isTs) {
|
|
513
|
+
updateMetrics.narrowed = !angularNeedsTransitive;
|
|
514
|
+
}
|
|
515
|
+
// Stable URL + Explicit Invalidation:
|
|
516
|
+
//
|
|
517
|
+
// Compute the transitive importer closure ONCE here and reuse
|
|
518
|
+
// it for (a) `server.moduleGraph.invalidateModule` (so Vite's
|
|
519
|
+
// transform pipeline re-runs on next request), (b) the shared
|
|
520
|
+
// transform-request cache, and (c) the runtime eviction set
|
|
521
|
+
// we broadcast in `ns:angular-update`. Consolidating this
|
|
522
|
+
// removes a redundant graph walk and guarantees the three
|
|
523
|
+
// consumers see the exact same set of importers (otherwise a
|
|
524
|
+
// late module-graph mutation between calls could leave an
|
|
525
|
+
// asymmetric narrowed/broad mix).
|
|
526
|
+
//
|
|
527
|
+
// We separate Vite-transform narrowing from runtime eviction:
|
|
528
|
+
// `angularNeedsTransitive` answers the question "does the
|
|
529
|
+
// changed file's symbol shape change such that importers
|
|
530
|
+
// must be re-transformed by Vite?". The runtime, however,
|
|
531
|
+
// has a stricter requirement: ESM live bindings only refresh
|
|
532
|
+
// if the importing module re-evaluates inside V8. A
|
|
533
|
+
// constants file with no Angular decorator does NOT need a
|
|
534
|
+
// Vite re-transform of its importers (their compiled JS is
|
|
535
|
+
// identical), but its importers still hold stale bindings to
|
|
536
|
+
// the OLD constants Module record. After eviction + re-import
|
|
537
|
+
// of `main.ts`, V8 sees the cached importers, returns them
|
|
538
|
+
// unchanged, and they continue to read the OLD values. The
|
|
539
|
+
// user-visible symptom: HMR completes successfully, logs are
|
|
540
|
+
// clean, but the simulator does not reflect the change.
|
|
541
|
+
//
|
|
542
|
+
// The fix: ALWAYS compute the transitive importer closure
|
|
543
|
+
// for runtime eviction. Only skip Vite's
|
|
544
|
+
// `moduleGraph.invalidate` + transform-cache purge when
|
|
545
|
+
// `angularNeedsTransitive` is false — those are the genuine
|
|
546
|
+
// narrowing wins (saves re-transform work on the server).
|
|
547
|
+
// The eviction set always includes importers so V8 re-fetches
|
|
548
|
+
// and re-binds them.
|
|
549
|
+
if (verbose) {
|
|
550
|
+
console.info(`[ns-hmr][server] angularNeedsTransitive=${angularNeedsTransitive} (file=${path.basename(file)})`);
|
|
551
|
+
}
|
|
552
|
+
let transitiveImporters = [];
|
|
553
|
+
try {
|
|
554
|
+
transitiveImporters = collectAngularTransitiveImportersForInvalidation({
|
|
555
|
+
modules: angularTransitiveInvalidationRoots,
|
|
556
|
+
isExcluded: (id) => id.includes('/node_modules/'),
|
|
557
|
+
maxDepth: 16,
|
|
558
|
+
});
|
|
559
|
+
if (verbose) {
|
|
560
|
+
console.info(`[ns-hmr][server] transitiveImporters count=${transitiveImporters.length} firstN=`, transitiveImporters.slice(0, 16).map((m) => m?.id ?? '(none)'));
|
|
561
|
+
}
|
|
562
|
+
if (angularNeedsTransitive) {
|
|
563
|
+
updateMetrics.invalidated += transitiveImporters.length;
|
|
564
|
+
for (const mod of transitiveImporters) {
|
|
565
|
+
try {
|
|
566
|
+
server.moduleGraph.invalidateModule(mod);
|
|
567
|
+
}
|
|
568
|
+
catch (invalidationError) {
|
|
569
|
+
if (verbose) {
|
|
570
|
+
console.warn('[hmr-ws][angular] transitive importer invalidation failed', mod?.id, invalidationError);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (verbose && transitiveImporters.length) {
|
|
575
|
+
console.log('[hmr-ws][angular] invalidated transitive importers:', transitiveImporters.length);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
else if (isTs && typeof angularChangedSource === 'string') {
|
|
579
|
+
// Surfacing this log unconditionally lets the user
|
|
580
|
+
// immediately confirm whether narrowing fired for a
|
|
581
|
+
// given `.ts` edit (the summary line below still
|
|
582
|
+
// emits `narrowed=yes`/`no`, but having both makes
|
|
583
|
+
// the decision easier to spot in noisy logs and lets
|
|
584
|
+
// the user diff scenarios without flipping
|
|
585
|
+
// `NS_HMR_VERBOSE=true`).
|
|
586
|
+
//
|
|
587
|
+
// Narrowing means "skip Vite re-transform" (the
|
|
588
|
+
// importers still get evicted from the V8 module
|
|
589
|
+
// registry so live bindings refresh). The importer
|
|
590
|
+
// count is appended so the distinction is visible.
|
|
591
|
+
if (verbose && transitiveImporters.length) {
|
|
592
|
+
console.log(`[hmr-ws][angular] narrowed transitive invalidation (no @Component/@Directive/@Pipe/@Injectable/@NgModule): ${updateRel} — Vite transform skipped, runtime eviction includes ${transitiveImporters.length} importer(s)`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
if (verbose)
|
|
598
|
+
console.warn('[hmr-ws][angular] transitive importer collection failed', error);
|
|
599
|
+
}
|
|
600
|
+
try {
|
|
601
|
+
// Purge shared transform cache for the changed file +
|
|
602
|
+
// hot-update roots unconditionally (their transform
|
|
603
|
+
// output IS different now). Transitive importers are
|
|
604
|
+
// only purged when narrowing decides their output may
|
|
605
|
+
// have changed; otherwise their cached transforms are
|
|
606
|
+
// still valid (compiled JS is identical even though the
|
|
607
|
+
// runtime must re-evaluate them to refresh ESM bindings).
|
|
608
|
+
const transformCacheInvalidationUrls = new Set(collectAngularTransformCacheInvalidationUrls({
|
|
609
|
+
file,
|
|
610
|
+
isTs,
|
|
611
|
+
hotUpdateRoots: angularHotUpdateRoots,
|
|
612
|
+
transitiveImporters: angularNeedsTransitive ? transitiveImporters : [],
|
|
613
|
+
projectRoot: server.config.root || process.cwd(),
|
|
614
|
+
}));
|
|
615
|
+
if (transformCacheInvalidationUrls.size) {
|
|
616
|
+
sharedTransformRequest.invalidateMany(transformCacheInvalidationUrls);
|
|
617
|
+
if (verbose) {
|
|
618
|
+
console.log('[hmr-ws][angular] purged shared transform cache entries:', transformCacheInvalidationUrls.size);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
if (verbose)
|
|
624
|
+
console.warn('[hmr-ws][angular] shared transform cache purge failed', error);
|
|
625
|
+
}
|
|
626
|
+
updateMetrics.tAfterFramework = Date.now();
|
|
627
|
+
try {
|
|
628
|
+
const root = server.config.root || process.cwd();
|
|
629
|
+
const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
|
|
630
|
+
rememberAngularReloadSuppression(root, file);
|
|
631
|
+
const origin = getServerOrigin(server);
|
|
632
|
+
const bootstrapEntryRel = getBootstrapEntryRelPath();
|
|
633
|
+
// Stable URL + Explicit Invalidation:
|
|
634
|
+
//
|
|
635
|
+
// `evictPaths` is the canonical list of `/ns/m/<rel>` URLs
|
|
636
|
+
// the runtime must drop from `g_moduleRegistry` before
|
|
637
|
+
// re-importing `importerEntry`. Older versions of the
|
|
638
|
+
// server signaled invalidation by bumping a global
|
|
639
|
+
// `graphVersion` counter and embedding it in every URL —
|
|
640
|
+
// but V8 keys the module registry by full URL, so a v1 →
|
|
641
|
+
// v2 bump effectively flushed the entire dependency
|
|
642
|
+
// graph from the cache and forced the runtime to
|
|
643
|
+
// re-fetch + re-eval every transitively-imported module
|
|
644
|
+
// on each save (~3s HMR cycles, dominated by Vite's
|
|
645
|
+
// single-threaded transform pipeline). The new model:
|
|
646
|
+
//
|
|
647
|
+
// 1. URLs are stable: `/ns/m/<rel>` everywhere, no `vN`.
|
|
648
|
+
// 2. The server walks the inverse-dependency closure and
|
|
649
|
+
// sends only the modules that actually need to be
|
|
650
|
+
// re-evaluated (typically O(1) for component edits,
|
|
651
|
+
// or the changed file + entry for narrowed edits).
|
|
652
|
+
// 3. The client calls `__nsInvalidateModules(evictPaths)`
|
|
653
|
+
// and re-imports `importerEntry`, which causes V8 to
|
|
654
|
+
// refetch ONLY those modules. Everything else stays
|
|
655
|
+
// hot in the registry.
|
|
656
|
+
//
|
|
657
|
+
// Invariants enforced by `collectAngularEvictionUrls`:
|
|
658
|
+
// - Always includes the changed file (so the new source
|
|
659
|
+
// is fetched).
|
|
660
|
+
// - Always includes `importerEntry` (so re-import
|
|
661
|
+
// re-evaluates).
|
|
662
|
+
// - Excludes node_modules (vendor packages are stable).
|
|
663
|
+
// - Excludes virtual / runtime-graph-excluded ids.
|
|
664
|
+
// - Origin-prefixed: `http://host:port/ns/m/<rel>`.
|
|
665
|
+
let evictPaths = [];
|
|
666
|
+
try {
|
|
667
|
+
evictPaths = collectAngularEvictionUrls({
|
|
668
|
+
file,
|
|
669
|
+
hotUpdateRoots: angularHotUpdateRoots,
|
|
670
|
+
transitiveImporters,
|
|
671
|
+
projectRoot: root,
|
|
672
|
+
origin,
|
|
673
|
+
bootstrapEntry: bootstrapEntryRel,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
catch (error) {
|
|
677
|
+
if (verbose) {
|
|
678
|
+
console.warn('[ns-hmr][server] eviction set computation failed', error);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (verbose) {
|
|
682
|
+
try {
|
|
683
|
+
const tsRel = rel.replace(/\.(html|htm)$/i, '.ts');
|
|
684
|
+
const jsRel = rel.replace(/\.(html|htm)$/i, '.js');
|
|
685
|
+
const containsRelatedTs = evictPaths.some((u) => u.endsWith(tsRel));
|
|
686
|
+
const containsRelatedJs = evictPaths.some((u) => u.endsWith(jsRel));
|
|
687
|
+
const sample = evictPaths.slice(0, 32);
|
|
688
|
+
console.info(`[ns-hmr][server] evict-set count=${evictPaths.length} importerEntry=${bootstrapEntryRel ?? '(none)'} containsRelatedTs=${containsRelatedTs} containsRelatedJs=${containsRelatedJs} firstN=`, sample);
|
|
689
|
+
if (evictPaths.length > sample.length) {
|
|
690
|
+
console.info(`[ns-hmr][server] evict-set hidden=${evictPaths.length - sample.length} (showed first ${sample.length})`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
catch { }
|
|
694
|
+
}
|
|
695
|
+
const msg = {
|
|
696
|
+
type: 'ns:angular-update',
|
|
697
|
+
origin,
|
|
698
|
+
path: rel,
|
|
699
|
+
version: moduleGraph.version,
|
|
700
|
+
timestamp: Date.now(),
|
|
701
|
+
evictPaths,
|
|
702
|
+
importerEntry: bootstrapEntryRel,
|
|
703
|
+
};
|
|
704
|
+
if (verbose) {
|
|
705
|
+
console.log('[hmr-ws][angular] broadcasting update', Array.from(wss.clients || []).map((client) => ({
|
|
706
|
+
role: getHmrSocketRole(client),
|
|
707
|
+
readyState: client.readyState,
|
|
708
|
+
openState: client.OPEN,
|
|
709
|
+
})));
|
|
710
|
+
}
|
|
711
|
+
wss.clients.forEach((client) => {
|
|
712
|
+
if (isSocketClientOpen(client)) {
|
|
713
|
+
client.send(JSON.stringify(msg));
|
|
714
|
+
updateMetrics.recipients += 1;
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
catch (error) {
|
|
719
|
+
console.warn('[hmr-ws][angular] update failed:', error);
|
|
720
|
+
}
|
|
721
|
+
emitHmrUpdateSummary();
|
|
722
|
+
if (shouldSuppressDefaultViteHotUpdate({ flavor: strategy.flavor, file })) {
|
|
723
|
+
return [];
|
|
724
|
+
}
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
// TypeScript flavor: emit generic graph delta for app XML/TS/style changes
|
|
728
|
+
if (strategy.flavor === 'typescript') {
|
|
729
|
+
updateMetrics.tAfterFramework = Date.now();
|
|
730
|
+
try {
|
|
731
|
+
const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
|
|
732
|
+
if (verbose)
|
|
733
|
+
console.log('[hmr-ws][ts] app file hot update', { file, rel });
|
|
734
|
+
// Treat the changed file itself as a graph module with no deps. We only
|
|
735
|
+
// care that its hash/identity changes so the client sees a delta and can
|
|
736
|
+
// perform a TS root reset. Code is not used for execution here.
|
|
737
|
+
moduleGraph.upsert(rel, '', [], { emitDeltaOnInsert: true });
|
|
738
|
+
}
|
|
739
|
+
catch (e) {
|
|
740
|
+
if (verbose)
|
|
741
|
+
console.warn('[hmr-ws][ts] failed to emit delta for', file, e);
|
|
742
|
+
}
|
|
743
|
+
emitHmrUpdateSummary();
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
// Solid flavor: emit graph delta for app TSX/TS/JSX file changes.
|
|
747
|
+
// The common graph-update block above (moduleGraph lookup) may have
|
|
748
|
+
// already emitted a delta if the file was in Vite's module graph.
|
|
749
|
+
// This handler ensures a delta is emitted even if the module wasn't
|
|
750
|
+
// found (e.g. new file, or moduleGraph mismatch), and provides
|
|
751
|
+
// Solid-specific logging. The client-side processQueue handles
|
|
752
|
+
// propagation from non-component .ts files to .tsx component boundaries.
|
|
753
|
+
if (strategy.flavor === 'solid') {
|
|
754
|
+
const isSolidFile = /\.(tsx?|jsx?)$/i.test(file);
|
|
755
|
+
if (!isSolidFile)
|
|
756
|
+
return;
|
|
757
|
+
updateMetrics.tAfterFramework = Date.now();
|
|
758
|
+
try {
|
|
759
|
+
const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
|
|
760
|
+
if (verbose)
|
|
761
|
+
console.log('[hmr-ws][solid] app file hot update', { file, rel });
|
|
762
|
+
// If the common block already upserted (hash changed), this will
|
|
763
|
+
// detect unchanged hash and no-op. If the common block missed it
|
|
764
|
+
// (module not in Vite's graph), this forces the delta emission.
|
|
765
|
+
const normalizedId = moduleGraph.normalizeGraphId(rel);
|
|
766
|
+
const existing = moduleGraph.get(normalizedId);
|
|
767
|
+
if (!existing) {
|
|
768
|
+
// Module not in graph yet — force upsert with timestamp-based
|
|
769
|
+
// hash so the client sees a change.
|
|
770
|
+
moduleGraph.upsert(rel, `/* solid-hmr ${Date.now()} */`, [], { emitDeltaOnInsert: true });
|
|
771
|
+
}
|
|
772
|
+
// Log what we're sending so devs can trace the flow on the server side.
|
|
773
|
+
if (verbose) {
|
|
774
|
+
const gm = moduleGraph.get(normalizedId);
|
|
775
|
+
console.log('[hmr-ws][solid] delta module', { id: gm?.id, hash: gm?.hash });
|
|
776
|
+
}
|
|
777
|
+
// Purge the shared transform-request cache AND Vite's own
|
|
778
|
+
// moduleGraph transformResult cache for the changed file
|
|
779
|
+
// AND every transitive importer.
|
|
780
|
+
//
|
|
781
|
+
// Why this matters for Solid HMR specifically:
|
|
782
|
+
// - The HMR client evicts V8's module cache for the
|
|
783
|
+
// canonical /ns/m/<path> URL and re-imports the module.
|
|
784
|
+
// - The dev server resolves /ns/m/* by calling
|
|
785
|
+
// `sharedTransformRequest(...)`, which has a 60s TTL on
|
|
786
|
+
// transform results to amortize cost across HMR
|
|
787
|
+
// cycles. The shared cache wraps `server.transformRequest`,
|
|
788
|
+
// which itself caches the compiled output on each
|
|
789
|
+
// `ModuleNode.transformResult`. Both layers must be
|
|
790
|
+
// invalidated, or the re-import resolves to whatever
|
|
791
|
+
// the previous save populated.
|
|
792
|
+
// - Without invalidation at *both* layers, the second
|
|
793
|
+
// save of a file within the cache window returns the
|
|
794
|
+
// FIRST save's transform — V8 evaluates stale code,
|
|
795
|
+
// `solid-refresh.patchRegistry` runs against an
|
|
796
|
+
// unchanged source body, and the visible page picks
|
|
797
|
+
// up the previous save's edit instead of the current
|
|
798
|
+
// one (the "one-save-behind" symptom users reported).
|
|
799
|
+
//
|
|
800
|
+
// Critically, transitive importers must also be invalidated
|
|
801
|
+
// because TanStack file-based routing (and similar frameworks)
|
|
802
|
+
// use route files that statically import their components.
|
|
803
|
+
// When `home.tsx` changes, `routes/index.tsx`'s transform
|
|
804
|
+
// output references the imported home module identity. Even
|
|
805
|
+
// though the route file's source bytes did not change, its
|
|
806
|
+
// *resolved* import target has — and its cached transform
|
|
807
|
+
// might still encode the previous resolution. Forcing a
|
|
808
|
+
// fresh transform of the importer guarantees the route
|
|
809
|
+
// file's `import Home from ...` re-resolves against the
|
|
810
|
+
// freshly evaluated home module on V8 side.
|
|
811
|
+
//
|
|
812
|
+
// The Angular path performs the equivalent purge via
|
|
813
|
+
// `collectAngularTransformCacheInvalidationUrls` /
|
|
814
|
+
// `sharedTransformRequest.invalidateMany`. We replicate
|
|
815
|
+
// that contract for Solid here. The transitive walk is
|
|
816
|
+
// bounded the same way (max depth 16, node_modules /
|
|
817
|
+
// virtual ids excluded) so vendor packages stay hot.
|
|
818
|
+
try {
|
|
819
|
+
const projectRoot = server.config.root || process.cwd();
|
|
820
|
+
const cacheInvalidationUrls = new Set();
|
|
821
|
+
const addCacheKey = (rawId) => {
|
|
822
|
+
const id = String(rawId || '');
|
|
823
|
+
if (!id)
|
|
824
|
+
return;
|
|
825
|
+
const cacheKey = canonicalizeTransformRequestCacheKey(id, projectRoot);
|
|
826
|
+
cacheInvalidationUrls.add(cacheKey);
|
|
827
|
+
const noQuery = cacheKey.replace(/\?.*$/, '');
|
|
828
|
+
const stripped = noQuery.replace(/\.(?:[mc]?[jt]sx?)$/i, '');
|
|
829
|
+
if (stripped !== noQuery) {
|
|
830
|
+
cacheInvalidationUrls.add(stripped);
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
addCacheKey(file);
|
|
834
|
+
const rootModules = server.moduleGraph.getModulesByFile?.(file);
|
|
835
|
+
const transitiveImporters = collectAngularTransitiveImportersForInvalidation({
|
|
836
|
+
modules: rootModules ? Array.from(rootModules) : [],
|
|
837
|
+
isExcluded: (id) => id.includes('/node_modules/') || isRuntimeGraphExcludedPath(id),
|
|
838
|
+
maxDepth: 16,
|
|
839
|
+
});
|
|
840
|
+
// Invalidate Vite's moduleGraph for the changed file +
|
|
841
|
+
// every transitive importer so `server.transformRequest`
|
|
842
|
+
// re-runs the transform pipeline instead of returning
|
|
843
|
+
// the cached `ModuleNode.transformResult`. We call
|
|
844
|
+
// `onFileChange` (Vite's authoritative file-changed
|
|
845
|
+
// signal — walks all module variants including `?v=`,
|
|
846
|
+
// `?import`, `?t=`) AND per-module `invalidateModule`
|
|
847
|
+
// for transitive importers (which onFileChange
|
|
848
|
+
// doesn't reach).
|
|
849
|
+
try {
|
|
850
|
+
server.moduleGraph.onFileChange(file);
|
|
851
|
+
}
|
|
852
|
+
catch { }
|
|
853
|
+
if (rootModules) {
|
|
854
|
+
for (const mod of rootModules) {
|
|
855
|
+
try {
|
|
856
|
+
server.moduleGraph.invalidateModule(mod);
|
|
857
|
+
}
|
|
858
|
+
catch { }
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
for (const mod of transitiveImporters) {
|
|
862
|
+
addCacheKey(mod?.id);
|
|
863
|
+
try {
|
|
864
|
+
server.moduleGraph.invalidateModule(mod);
|
|
865
|
+
}
|
|
866
|
+
catch { }
|
|
867
|
+
}
|
|
868
|
+
if (cacheInvalidationUrls.size && sharedTransformRequest) {
|
|
869
|
+
sharedTransformRequest.invalidateMany(cacheInvalidationUrls);
|
|
870
|
+
if (verbose) {
|
|
871
|
+
console.log('[hmr-ws][solid] purged shared transform cache entries:', cacheInvalidationUrls.size, 'transitiveImporters=', transitiveImporters.length);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
// Sledgehammer: nuke EVERY entry in sharedTransformRequest's
|
|
875
|
+
// result cache. The targeted `invalidateMany` above only
|
|
876
|
+
// clears keys we know about. The `/ns/m/` handler iterates
|
|
877
|
+
// a long list of candidate extensions (`.ts`, `.js`, `.tsx`,
|
|
878
|
+
// `.jsx`, `.mjs`, `.mts`, `.cts`, `.vue`, `index.*`) and
|
|
879
|
+
// EACH candidate is a separate cache key. If a previous
|
|
880
|
+
// serve populated cache for `/src/components/home.js` (via
|
|
881
|
+
// extension fallback that resolves to `home.tsx`), our
|
|
882
|
+
// targeted invalidate misses it and iOS HITs the stale
|
|
883
|
+
// entry — serving the previous save's transformed code.
|
|
884
|
+
try {
|
|
885
|
+
sharedTransformRequest.clear();
|
|
886
|
+
}
|
|
887
|
+
catch { }
|
|
888
|
+
}
|
|
889
|
+
catch (e) {
|
|
890
|
+
if (verbose)
|
|
891
|
+
console.warn('[hmr-ws][solid] transform cache invalidation failed', e);
|
|
892
|
+
}
|
|
893
|
+
// Re-run the transform AFTER all caches are invalidated, then
|
|
894
|
+
// re-upsert the graph so the broadcast hash matches the freshly-
|
|
895
|
+
// transformed content. The common upsert block above ran
|
|
896
|
+
// `server.transformRequest` BEFORE invalidation — at that
|
|
897
|
+
// moment Vite's auto-invalidate hadn't fired yet (it runs after
|
|
898
|
+
// `plugin.handleHotUpdate`), so the result it cached was the
|
|
899
|
+
// previous save's. Without this re-transform, the broadcast
|
|
900
|
+
// carries a stale hash and iOS evaluates the previous save's
|
|
901
|
+
// bytes ("one save behind").
|
|
902
|
+
//
|
|
903
|
+
// We pre-populate the cache for every extension variant Vite's
|
|
904
|
+
// /ns/m/ handler might try, so the first request from iOS hits
|
|
905
|
+
// fresh data regardless of which candidate it resolves first.
|
|
906
|
+
try {
|
|
907
|
+
const ext = file.match(/\.(?:[mc]?[jt]sx?)$/i)?.[0] || '';
|
|
908
|
+
const baseSpec = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
|
|
909
|
+
const baseNoExt = ext ? baseSpec.replace(/\.(?:[mc]?[jt]sx?)$/i, '') : baseSpec;
|
|
910
|
+
const candidates = Array.from(new Set([baseSpec, baseNoExt, baseNoExt + '.ts', baseNoExt + '.tsx', baseNoExt + '.js', baseNoExt + '.jsx', baseNoExt + '.mjs', baseNoExt + '.mts', baseNoExt + '.cts', file]));
|
|
911
|
+
let freshCode = '';
|
|
912
|
+
for (const cand of candidates) {
|
|
913
|
+
try {
|
|
914
|
+
const fresh = await sharedTransformRequest(cand, 30000);
|
|
915
|
+
if (fresh?.code && !freshCode)
|
|
916
|
+
freshCode = fresh.code;
|
|
917
|
+
}
|
|
918
|
+
catch { }
|
|
919
|
+
}
|
|
920
|
+
if (freshCode) {
|
|
921
|
+
const existingGm = moduleGraph.get(normalizedId);
|
|
922
|
+
const existingDeps = existingGm?.deps || [];
|
|
923
|
+
moduleGraph.upsert(normalizedId, freshCode, existingDeps, {
|
|
924
|
+
broadcastDelta: false,
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
catch (e) {
|
|
929
|
+
if (verbose)
|
|
930
|
+
console.warn('[hmr-ws][solid] post-invalidation re-transform failed', e);
|
|
931
|
+
}
|
|
932
|
+
// Broadcast the (now-fresh) delta. Suppressing this in the
|
|
933
|
+
// common upsert block (`broadcastDelta: strategy.flavor
|
|
934
|
+
// !== 'solid'`) and emitting it here ensures the client's
|
|
935
|
+
// eviction + re-import doesn't race the server's cache
|
|
936
|
+
// invalidation.
|
|
937
|
+
try {
|
|
938
|
+
const gm = moduleGraph.get(normalizedId);
|
|
939
|
+
if (gm) {
|
|
940
|
+
moduleGraph.emitDelta([gm], []);
|
|
941
|
+
if (verbose) {
|
|
942
|
+
console.log('[hmr-ws][solid] broadcast delta after cache invalidation', { id: gm.id, hash: gm.hash });
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
catch (e) {
|
|
947
|
+
if (verbose)
|
|
948
|
+
console.warn('[hmr-ws][solid] post-invalidation broadcast failed', e);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
catch (e) {
|
|
952
|
+
if (verbose)
|
|
953
|
+
console.warn('[hmr-ws][solid] failed to handle hot update for', file, e);
|
|
954
|
+
}
|
|
955
|
+
emitHmrUpdateSummary();
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
// Handle .vue file updates
|
|
959
|
+
if (!file.endsWith('.vue')) {
|
|
960
|
+
if (verbose)
|
|
961
|
+
console.log('[hmr-ws] Not a .vue file, skipping');
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
if (verbose)
|
|
965
|
+
console.log('[hmr-ws] Processing .vue file update...');
|
|
966
|
+
try {
|
|
967
|
+
const root = server.config.root || process.cwd();
|
|
968
|
+
let rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
|
|
969
|
+
// Transform the .vue file
|
|
970
|
+
const transformed = await server.transformRequest(rel);
|
|
971
|
+
if (!transformed?.code)
|
|
972
|
+
return;
|
|
973
|
+
let code = transformed.code;
|
|
974
|
+
// Clean and process
|
|
975
|
+
code = cleanCode(code, strategy);
|
|
976
|
+
// Process dependencies
|
|
977
|
+
const visitedPaths = new Set();
|
|
978
|
+
const importerDir = path.posix.dirname(rel);
|
|
979
|
+
// Collect dependencies from this file
|
|
980
|
+
const deps = new Set();
|
|
981
|
+
const collectDeps = (pattern) => {
|
|
982
|
+
let match;
|
|
983
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
984
|
+
const spec = match[2];
|
|
985
|
+
if (!spec || PAT.VUE_FILE_PATTERN.test(spec) || !shouldRemapImport(spec)) {
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
let key;
|
|
989
|
+
if (spec.startsWith('/')) {
|
|
990
|
+
key = spec;
|
|
991
|
+
}
|
|
992
|
+
else if (spec.startsWith('./') || spec.startsWith('../')) {
|
|
993
|
+
key = path.posix.normalize(path.posix.join(importerDir, spec));
|
|
994
|
+
if (!key.startsWith('/'))
|
|
995
|
+
key = '/' + key;
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
key = key.replace(PAT.QUERY_PATTERN, '');
|
|
1001
|
+
deps.add(key);
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
collectDeps(PAT.IMPORT_PATTERN_1);
|
|
1005
|
+
collectDeps(PAT.IMPORT_PATTERN_2);
|
|
1006
|
+
collectDeps(PAT.EXPORT_PATTERN);
|
|
1007
|
+
collectDeps(PAT.IMPORT_PATTERN_3);
|
|
1008
|
+
// CRITICAL: Collect .vue file imports separately
|
|
1009
|
+
// Use matchAll() to avoid regex state issues
|
|
1010
|
+
const vueDeps = new Set();
|
|
1011
|
+
const vueImportMatches = [...code.matchAll(PAT.IMPORT_PATTERN_1), ...code.matchAll(PAT.VUE_FILE_IMPORT)];
|
|
1012
|
+
for (const match of vueImportMatches) {
|
|
1013
|
+
const spec = match[2];
|
|
1014
|
+
if (!spec || !PAT.VUE_FILE_PATTERN.test(spec)) {
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
let key;
|
|
1018
|
+
if (spec.startsWith('/')) {
|
|
1019
|
+
key = spec.replace(PAT.QUERY_PATTERN, '');
|
|
1020
|
+
}
|
|
1021
|
+
else if (spec.startsWith('./') || spec.startsWith('../')) {
|
|
1022
|
+
key = path.posix.normalize(path.posix.join(importerDir, spec.replace(PAT.QUERY_PATTERN, '')));
|
|
1023
|
+
if (!key.startsWith('/'))
|
|
1024
|
+
key = '/' + key;
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
// Ensure this .vue file is registered in sfcFileMap
|
|
1030
|
+
if (!sfcFileMap.has(key)) {
|
|
1031
|
+
const hash = createHash('md5').update(key).digest('hex').slice(0, 8);
|
|
1032
|
+
sfcFileMap.set(key, `sfc-${hash}.mjs`);
|
|
1033
|
+
if (verbose) {
|
|
1034
|
+
console.log(`[hmr-ws] Registered .vue import: ${key} → sfc-${hash}.mjs`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
// Add to vueDeps for separate processing
|
|
1038
|
+
vueDeps.add(key);
|
|
1039
|
+
}
|
|
1040
|
+
// Process .vue dependencies (they stay as sfc-*.mjs imports)
|
|
1041
|
+
for (const vueDep of vueDeps) {
|
|
1042
|
+
await strategy.processFile({
|
|
1043
|
+
filePath: vueDep,
|
|
1044
|
+
server,
|
|
1045
|
+
sfcFileMap,
|
|
1046
|
+
depFileMap,
|
|
1047
|
+
visitedPaths,
|
|
1048
|
+
wss,
|
|
1049
|
+
verbose,
|
|
1050
|
+
helpers: {
|
|
1051
|
+
cleanCode: (code) => cleanCode(code, strategy),
|
|
1052
|
+
collectImportDependencies,
|
|
1053
|
+
isCoreGlobalsReference,
|
|
1054
|
+
isNativeScriptCoreModule,
|
|
1055
|
+
isNativeScriptPluginModule,
|
|
1056
|
+
resolveVendorFromCandidate,
|
|
1057
|
+
createHash: (value) => createHash('md5').update(value).digest('hex'),
|
|
1058
|
+
},
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
// Process with consistent SFC processor (removes non-.vue imports)
|
|
1062
|
+
code = processSfcCode(code);
|
|
1063
|
+
// Rewrite ONLY .vue imports (everything else is now inlined)
|
|
1064
|
+
const projectRoot = server.config.root || process.cwd();
|
|
1065
|
+
code = rewriteImports(code, rel, sfcFileMap, depFileMap, projectRoot, verbose, undefined);
|
|
1066
|
+
moduleGraph.upsert(rel, code, [...deps, ...vueDeps]);
|
|
1067
|
+
// Add HMR runtime prelude (CRITICAL for runtime)
|
|
1068
|
+
const hmrPrelude = `
|
|
1069
|
+
// Embedded HMR Runtime for NativeScript runtime
|
|
1070
|
+
const createHotContext = (id) => ({
|
|
1071
|
+
on: (event, handler) => {
|
|
1072
|
+
if (!globalThis.__NS_HMR_HANDLERS__) globalThis.__NS_HMR_HANDLERS__ = new Map();
|
|
1073
|
+
if (!globalThis.__NS_HMR_HANDLERS__.has(id)) globalThis.__NS_HMR_HANDLERS__.set(id, []);
|
|
1074
|
+
globalThis.__NS_HMR_HANDLERS__.get(id).push({ event, handler });
|
|
1075
|
+
},
|
|
1076
|
+
accept: (handler) => {
|
|
1077
|
+
if (!globalThis.__NS_HMR_ACCEPTS__) globalThis.__NS_HMR_ACCEPTS__ = new Map();
|
|
1078
|
+
globalThis.__NS_HMR_ACCEPTS__.set(id, handler);
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
if (typeof import.meta === 'undefined') {
|
|
1083
|
+
globalThis.importMeta = { hot: null };
|
|
1084
|
+
} else if (!import.meta.hot) {
|
|
1085
|
+
import.meta.hot = null;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const __vite__createHotContext = createHotContext;
|
|
1089
|
+
|
|
1090
|
+
if (typeof __VUE_HMR_RUNTIME__ === 'undefined') {
|
|
1091
|
+
globalThis.__VUE_HMR_RUNTIME__ = {
|
|
1092
|
+
createRecord: () => true,
|
|
1093
|
+
reload: () => {},
|
|
1094
|
+
rerender: () => {},
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Install a lightweight guard to capture require('http(s)://...') attempts with stack traces
|
|
1099
|
+
(() => {
|
|
1100
|
+
try {
|
|
1101
|
+
const g = globalThis;
|
|
1102
|
+
if (g.__NS_REQUIRE_GUARD_INSTALLED__) return;
|
|
1103
|
+
const makeGuard = (orig, label) => function () {
|
|
1104
|
+
try {
|
|
1105
|
+
const spec = arguments[0];
|
|
1106
|
+
if (typeof spec === 'string' && /^(?:https?:)\/\//.test(spec)) {
|
|
1107
|
+
const err = new Error('[ns-hmr][require-guard] require of URL: ' + spec + ' via ' + label);
|
|
1108
|
+
const stack = err.stack || '';
|
|
1109
|
+
console.error(err.message + '\n' + stack);
|
|
1110
|
+
try { g.__NS_REQUIRE_GUARD_LAST__ = { spec, stack, label, ts: Date.now() }; } catch {}
|
|
1111
|
+
}
|
|
1112
|
+
} catch {}
|
|
1113
|
+
return orig.apply(this, arguments);
|
|
1114
|
+
};
|
|
1115
|
+
if (typeof g.require === 'function' && !g.require.__NS_REQ_GUARDED__) {
|
|
1116
|
+
const orig = g.require; g.require = makeGuard(orig, 'require'); g.require.__NS_REQ_GUARDED__ = true;
|
|
1117
|
+
}
|
|
1118
|
+
if (typeof g.__nsRequire === 'function' && !g.__nsRequire.__NS_REQ_GUARDED__) {
|
|
1119
|
+
const orig = g.__nsRequire; g.__nsRequire = makeGuard(orig, '__nsRequire'); g.__nsRequire.__NS_REQ_GUARDED__ = true;
|
|
1120
|
+
}
|
|
1121
|
+
g.__NS_REQUIRE_GUARD_INSTALLED__ = true;
|
|
1122
|
+
} catch {}
|
|
1123
|
+
})();
|
|
1124
|
+
`;
|
|
1125
|
+
code = hmrPrelude + '\n' + code;
|
|
1126
|
+
// Update SFC registry
|
|
1127
|
+
const hash = createHash('md5').update(rel).digest('hex').slice(0, 8);
|
|
1128
|
+
const fileName = sfcFileMap.get(rel) || `sfc-${hash}.mjs`;
|
|
1129
|
+
sfcFileMap.set(rel, fileName);
|
|
1130
|
+
const ts = Date.now();
|
|
1131
|
+
// FIRST: Send mapping-only registry update (no code)
|
|
1132
|
+
const registryUpdateMsg = {
|
|
1133
|
+
type: 'ns:vue-sfc-registry-update',
|
|
1134
|
+
path: rel,
|
|
1135
|
+
fileName,
|
|
1136
|
+
ts,
|
|
1137
|
+
version: moduleGraph.version,
|
|
1138
|
+
};
|
|
1139
|
+
wss.clients.forEach((client) => {
|
|
1140
|
+
if (isSocketClientOpen(client)) {
|
|
1141
|
+
client.send(JSON.stringify(registryUpdateMsg));
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
// HTTP-only mode: the device loads SFC artifacts and their dependencies via
|
|
1145
|
+
// HTTP endpoints on demand, so the WS channel stays metadata-only (just the
|
|
1146
|
+
// registry update above). No code-push, dependency harvest, or legacy dynamic
|
|
1147
|
+
// module message is emitted here.
|
|
1148
|
+
}
|
|
1149
|
+
catch (error) {
|
|
1150
|
+
console.warn('[hmr-ws] HMR update failed:', error);
|
|
1151
|
+
console.error(error);
|
|
1152
|
+
}
|
|
1153
|
+
// Vue path emits update summary at the end of the function so
|
|
1154
|
+
// every framework branch gets exactly one log line. Idempotent
|
|
1155
|
+
// — if any branch already emitted, this is a no-op.
|
|
1156
|
+
emitHmrUpdateSummary();
|
|
1157
|
+
// CRITICAL: Return empty array to prevent Vite's default HMR
|
|
1158
|
+
return [];
|
|
1159
|
+
}
|
|
1160
|
+
//# sourceMappingURL=websocket-hot-update.js.map
|