@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.
Files changed (74) hide show
  1. package/hmr/server/angular-root-component.d.ts +79 -0
  2. package/hmr/server/angular-root-component.js +149 -0
  3. package/hmr/server/angular-root-component.js.map +1 -0
  4. package/hmr/server/hmr-module-graph.d.ts +37 -0
  5. package/hmr/server/hmr-module-graph.js +214 -0
  6. package/hmr/server/hmr-module-graph.js.map +1 -0
  7. package/hmr/server/index.js +1 -0
  8. package/hmr/server/index.js.map +1 -1
  9. package/hmr/server/ns-rt-route.d.ts +5 -0
  10. package/hmr/server/ns-rt-route.js +35 -0
  11. package/hmr/server/ns-rt-route.js.map +1 -0
  12. package/hmr/server/require-guard.d.ts +1 -0
  13. package/hmr/server/require-guard.js +12 -0
  14. package/hmr/server/require-guard.js.map +1 -0
  15. package/hmr/server/route-helpers.d.ts +7 -0
  16. package/hmr/server/route-helpers.js +13 -0
  17. package/hmr/server/route-helpers.js.map +1 -0
  18. package/hmr/server/server-origin.d.ts +12 -0
  19. package/hmr/server/server-origin.js +66 -0
  20. package/hmr/server/server-origin.js.map +1 -0
  21. package/hmr/server/websocket-core-bridge.js +0 -11
  22. package/hmr/server/websocket-core-bridge.js.map +1 -1
  23. package/hmr/server/websocket-device-transform.d.ts +21 -0
  24. package/hmr/server/websocket-device-transform.js +1570 -0
  25. package/hmr/server/websocket-device-transform.js.map +1 -0
  26. package/hmr/server/websocket-hot-update.d.ts +51 -0
  27. package/hmr/server/websocket-hot-update.js +1160 -0
  28. package/hmr/server/websocket-hot-update.js.map +1 -0
  29. package/hmr/server/websocket-import-map-route.d.ts +15 -0
  30. package/hmr/server/websocket-import-map-route.js +44 -0
  31. package/hmr/server/websocket-import-map-route.js.map +1 -0
  32. package/hmr/server/websocket-ns-core.d.ts +21 -0
  33. package/hmr/server/websocket-ns-core.js +305 -0
  34. package/hmr/server/websocket-ns-core.js.map +1 -0
  35. package/hmr/server/websocket-ns-entry.d.ts +22 -0
  36. package/hmr/server/websocket-ns-entry.js +150 -0
  37. package/hmr/server/websocket-ns-entry.js.map +1 -0
  38. package/hmr/server/websocket-ns-m.d.ts +34 -0
  39. package/hmr/server/websocket-ns-m.js +853 -0
  40. package/hmr/server/websocket-ns-m.js.map +1 -0
  41. package/hmr/server/websocket-served-module-helpers.d.ts +1 -1
  42. package/hmr/server/websocket-served-module-helpers.js +1 -1
  43. package/hmr/server/websocket-served-module-helpers.js.map +1 -1
  44. package/hmr/server/websocket-sfc.d.ts +24 -0
  45. package/hmr/server/websocket-sfc.js +1223 -0
  46. package/hmr/server/websocket-sfc.js.map +1 -0
  47. package/hmr/server/websocket-txn.js +2 -8
  48. package/hmr/server/websocket-txn.js.map +1 -1
  49. package/hmr/server/websocket-vendor-unifier.js +2 -8
  50. package/hmr/server/websocket-vendor-unifier.js.map +1 -1
  51. package/hmr/server/websocket.d.ts +1 -44
  52. package/hmr/server/websocket.js +588 -6691
  53. package/hmr/server/websocket.js.map +1 -1
  54. package/hmr/shared/runtime/root-placeholder-view.d.ts +19 -0
  55. package/hmr/shared/runtime/root-placeholder-view.js +310 -0
  56. package/hmr/shared/runtime/root-placeholder-view.js.map +1 -0
  57. package/hmr/shared/runtime/root-placeholder.js +1 -309
  58. package/hmr/shared/runtime/root-placeholder.js.map +1 -1
  59. package/hmr/shared/vendor/manifest-collect.d.ts +32 -0
  60. package/hmr/shared/vendor/manifest-collect.js +512 -0
  61. package/hmr/shared/vendor/manifest-collect.js.map +1 -0
  62. package/hmr/shared/vendor/manifest.d.ts +1 -35
  63. package/hmr/shared/vendor/manifest.js +3 -914
  64. package/hmr/shared/vendor/manifest.js.map +1 -1
  65. package/hmr/shared/vendor/vendor-device-shim.d.ts +1 -0
  66. package/hmr/shared/vendor/vendor-device-shim.js +208 -0
  67. package/hmr/shared/vendor/vendor-device-shim.js.map +1 -0
  68. package/hmr/shared/vendor/vendor-esbuild-plugins.d.ts +16 -0
  69. package/hmr/shared/vendor/vendor-esbuild-plugins.js +203 -0
  70. package/hmr/shared/vendor/vendor-esbuild-plugins.js.map +1 -0
  71. package/package.json +1 -1
  72. package/hmr/server/websocket-vue-sfc.d.ts +0 -26
  73. package/hmr/server/websocket-vue-sfc.js +0 -1053
  74. 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