@nativescript/vite 8.0.0-alpha.30 → 8.0.0-alpha.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/configuration/angular.js +8 -8
- package/configuration/angular.js.map +1 -1
- package/configuration/solid.js +2 -2
- package/configuration/solid.js.map +1 -1
- package/hmr/client/framework-client-strategy.d.ts +73 -0
- package/hmr/client/framework-client-strategy.js +19 -0
- package/hmr/client/framework-client-strategy.js.map +1 -0
- package/hmr/client/index.js +77 -186
- package/hmr/client/index.js.map +1 -1
- package/hmr/client/utils.js.map +1 -1
- package/hmr/entry-runtime.js.map +1 -1
- package/hmr/frameworks/angular/build/angular-linker.js.map +1 -0
- package/hmr/frameworks/angular/build/inject-component-hmr-registration.js.map +1 -0
- package/hmr/frameworks/angular/build/inject-hmr-vite-ignore.js.map +1 -0
- package/hmr/frameworks/angular/build/inline-decorator-component-templates.js.map +1 -0
- package/hmr/frameworks/angular/build/js-lexer.js.map +1 -0
- package/hmr/frameworks/angular/build/shared-linker.js.map +1 -0
- package/hmr/frameworks/angular/build/synthesize-decorator-ctor-parameters.js.map +1 -0
- package/hmr/frameworks/angular/build/synthesize-injectable-factories.js.map +1 -0
- package/hmr/frameworks/angular/build/util.js.map +1 -0
- package/hmr/frameworks/angular/client/index.js.map +1 -1
- package/hmr/frameworks/angular/client/strategy.d.ts +9 -0
- package/hmr/frameworks/angular/client/strategy.js +19 -0
- package/hmr/frameworks/angular/client/strategy.js.map +1 -0
- package/hmr/frameworks/angular/server/angular-root-component.js.map +1 -0
- package/hmr/frameworks/angular/server/strategy.js +440 -2
- package/hmr/frameworks/angular/server/strategy.js.map +1 -1
- package/hmr/{server → frameworks/angular/server}/websocket-angular-entry.js +2 -2
- package/hmr/frameworks/angular/server/websocket-angular-entry.js.map +1 -0
- package/hmr/{server → frameworks/angular/server}/websocket-angular-hot-update.d.ts +0 -11
- package/hmr/{server → frameworks/angular/server}/websocket-angular-hot-update.js +3 -80
- package/hmr/frameworks/angular/server/websocket-angular-hot-update.js.map +1 -0
- package/hmr/frameworks/solid/build/solid-jsx-deps.js.map +1 -0
- package/hmr/frameworks/solid/server/strategy.js +360 -1
- package/hmr/frameworks/solid/server/strategy.js.map +1 -1
- package/hmr/frameworks/typescript/server/strategy.js +27 -0
- package/hmr/frameworks/typescript/server/strategy.js.map +1 -1
- package/hmr/frameworks/vue/client/index.js.map +1 -1
- package/hmr/frameworks/vue/client/strategy.d.ts +7 -0
- package/hmr/frameworks/vue/client/strategy.js +83 -0
- package/hmr/frameworks/vue/client/strategy.js.map +1 -0
- package/hmr/frameworks/vue/client/vue-sfc-update-overlay.js.map +1 -0
- package/hmr/frameworks/vue/server/sfc-route-assemble.d.ts +7 -0
- package/hmr/{server/websocket-sfc.js → frameworks/vue/server/sfc-route-assemble.js} +19 -536
- package/hmr/frameworks/vue/server/sfc-route-assemble.js.map +1 -0
- package/hmr/frameworks/vue/server/sfc-route-meta.d.ts +7 -0
- package/hmr/frameworks/vue/server/sfc-route-meta.js +80 -0
- package/hmr/frameworks/vue/server/sfc-route-meta.js.map +1 -0
- package/hmr/frameworks/vue/server/sfc-route-serve.d.ts +8 -0
- package/hmr/frameworks/vue/server/sfc-route-serve.js +457 -0
- package/hmr/frameworks/vue/server/sfc-route-serve.js.map +1 -0
- package/hmr/frameworks/vue/server/sfc-route-shared.d.ts +19 -0
- package/hmr/frameworks/vue/server/sfc-route-shared.js +14 -0
- package/hmr/frameworks/vue/server/sfc-route-shared.js.map +1 -0
- package/hmr/frameworks/vue/server/strategy.js +244 -0
- package/hmr/frameworks/vue/server/strategy.js.map +1 -1
- package/hmr/frameworks/vue/server/websocket-sfc.d.ts +15 -0
- package/hmr/frameworks/vue/server/websocket-sfc.js +20 -0
- package/hmr/frameworks/vue/server/websocket-sfc.js.map +1 -0
- package/hmr/server/device-transform-helpers.d.ts +24 -0
- package/hmr/server/device-transform-helpers.js +327 -0
- package/hmr/server/device-transform-helpers.js.map +1 -0
- package/hmr/server/framework-strategy.d.ts +95 -1
- package/hmr/server/hmr-module-graph.js.map +1 -1
- package/hmr/server/import-map.d.ts +11 -2
- package/hmr/server/import-map.js +21 -54
- package/hmr/server/import-map.js.map +1 -1
- package/hmr/server/index.js +7 -17
- package/hmr/server/index.js.map +1 -1
- package/hmr/server/process-code-for-device.d.ts +15 -0
- package/hmr/server/process-code-for-device.js +654 -0
- package/hmr/server/process-code-for-device.js.map +1 -0
- package/hmr/server/rewrite-imports.d.ts +2 -0
- package/hmr/server/rewrite-imports.js +604 -0
- package/hmr/server/rewrite-imports.js.map +1 -0
- package/hmr/server/transform-cache-invalidation.d.ts +11 -0
- package/hmr/server/transform-cache-invalidation.js +84 -0
- package/hmr/server/transform-cache-invalidation.js.map +1 -0
- package/hmr/server/websocket-device-transform.d.ts +3 -21
- package/hmr/server/websocket-device-transform.js +6 -1569
- package/hmr/server/websocket-device-transform.js.map +1 -1
- package/hmr/server/websocket-hmr-pending.d.ts +2 -8
- package/hmr/server/websocket-hmr-pending.js.map +1 -1
- package/hmr/server/websocket-hot-update.d.ts +40 -14
- package/hmr/server/websocket-hot-update.js +24 -854
- package/hmr/server/websocket-hot-update.js.map +1 -1
- package/hmr/server/websocket-import-map-route.js +3 -1
- package/hmr/server/websocket-import-map-route.js.map +1 -1
- package/hmr/server/websocket-ns-core.d.ts +4 -4
- package/hmr/server/websocket-ns-core.js +3 -2
- package/hmr/server/websocket-ns-core.js.map +1 -1
- package/hmr/server/websocket-ns-entry.d.ts +0 -1
- package/hmr/server/websocket-ns-entry.js +1 -1
- package/hmr/server/websocket-ns-entry.js.map +1 -1
- package/hmr/server/websocket-ns-m.d.ts +0 -1
- package/hmr/server/websocket-ns-m.js +24 -129
- package/hmr/server/websocket-ns-m.js.map +1 -1
- package/hmr/server/websocket-vendor-unifier.d.ts +0 -1
- package/hmr/server/websocket-vendor-unifier.js +2 -1
- package/hmr/server/websocket-vendor-unifier.js.map +1 -1
- package/hmr/server/websocket.d.ts +11 -12
- package/hmr/server/websocket.js +25 -33
- package/hmr/server/websocket.js.map +1 -1
- package/hmr/shared/ns-globals.d.ts +118 -0
- package/hmr/shared/ns-globals.js +27 -0
- package/hmr/shared/ns-globals.js.map +1 -0
- package/hmr/shared/protocol.d.ts +136 -0
- package/hmr/shared/protocol.js +28 -0
- package/hmr/shared/protocol.js.map +1 -0
- package/hmr/shared/vendor/manifest-collect.d.ts +0 -28
- package/hmr/shared/vendor/manifest-collect.js +2 -2
- package/hmr/shared/vendor/manifest-collect.js.map +1 -1
- package/hmr/shared/vendor/manifest.d.ts +1 -3
- package/hmr/shared/vendor/manifest.js +1 -3
- package/hmr/shared/vendor/manifest.js.map +1 -1
- package/hmr/shared/vendor/vendor-esbuild-plugins.js +1 -1
- package/hmr/shared/vendor/vendor-esbuild-plugins.js.map +1 -1
- package/package.json +1 -1
- package/helpers/angular/angular-linker.js.map +0 -1
- package/helpers/angular/inject-component-hmr-registration.js.map +0 -1
- package/helpers/angular/inject-hmr-vite-ignore.js.map +0 -1
- package/helpers/angular/inline-decorator-component-templates.js.map +0 -1
- package/helpers/angular/js-lexer.js.map +0 -1
- package/helpers/angular/shared-linker.js.map +0 -1
- package/helpers/angular/synthesize-decorator-ctor-parameters.js.map +0 -1
- package/helpers/angular/synthesize-injectable-factories.js.map +0 -1
- package/helpers/angular/util.js.map +0 -1
- package/helpers/prelink-angular.d.ts +0 -2
- package/helpers/prelink-angular.js +0 -96
- package/helpers/prelink-angular.js.map +0 -1
- package/helpers/solid-jsx-deps.js.map +0 -1
- package/hmr/client/vue-sfc-update-overlay.js.map +0 -1
- package/hmr/server/angular-root-component.js.map +0 -1
- package/hmr/server/websocket-angular-entry.js.map +0 -1
- package/hmr/server/websocket-angular-hot-update.js.map +0 -1
- package/hmr/server/websocket-sfc.d.ts +0 -24
- package/hmr/server/websocket-sfc.js.map +0 -1
- /package/{helpers/angular → hmr/frameworks/angular/build}/angular-linker.d.ts +0 -0
- /package/{helpers/angular → hmr/frameworks/angular/build}/angular-linker.js +0 -0
- /package/{helpers/angular → hmr/frameworks/angular/build}/inject-component-hmr-registration.d.ts +0 -0
- /package/{helpers/angular → hmr/frameworks/angular/build}/inject-component-hmr-registration.js +0 -0
- /package/{helpers/angular → hmr/frameworks/angular/build}/inject-hmr-vite-ignore.d.ts +0 -0
- /package/{helpers/angular → hmr/frameworks/angular/build}/inject-hmr-vite-ignore.js +0 -0
- /package/{helpers/angular → hmr/frameworks/angular/build}/inline-decorator-component-templates.d.ts +0 -0
- /package/{helpers/angular → hmr/frameworks/angular/build}/inline-decorator-component-templates.js +0 -0
- /package/{helpers/angular → hmr/frameworks/angular/build}/js-lexer.d.ts +0 -0
- /package/{helpers/angular → hmr/frameworks/angular/build}/js-lexer.js +0 -0
- /package/{helpers/angular → hmr/frameworks/angular/build}/shared-linker.d.ts +0 -0
- /package/{helpers/angular → hmr/frameworks/angular/build}/shared-linker.js +0 -0
- /package/{helpers/angular → hmr/frameworks/angular/build}/synthesize-decorator-ctor-parameters.d.ts +0 -0
- /package/{helpers/angular → hmr/frameworks/angular/build}/synthesize-decorator-ctor-parameters.js +0 -0
- /package/{helpers/angular → hmr/frameworks/angular/build}/synthesize-injectable-factories.d.ts +0 -0
- /package/{helpers/angular → hmr/frameworks/angular/build}/synthesize-injectable-factories.js +0 -0
- /package/{helpers/angular → hmr/frameworks/angular/build}/util.d.ts +0 -0
- /package/{helpers/angular → hmr/frameworks/angular/build}/util.js +0 -0
- /package/hmr/{server → frameworks/angular/server}/angular-root-component.d.ts +0 -0
- /package/hmr/{server → frameworks/angular/server}/angular-root-component.js +0 -0
- /package/hmr/{server → frameworks/angular/server}/websocket-angular-entry.d.ts +0 -0
- /package/{helpers → hmr/frameworks/solid/build}/solid-jsx-deps.d.ts +0 -0
- /package/{helpers → hmr/frameworks/solid/build}/solid-jsx-deps.js +0 -0
- /package/hmr/{client → frameworks/vue/client}/vue-sfc-update-overlay.d.ts +0 -0
- /package/hmr/{client → frameworks/vue/client}/vue-sfc-update-overlay.js +0 -0
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
|
-
import { createHash } from 'crypto';
|
|
3
|
-
import * as PAT from './constants.js';
|
|
4
2
|
import { isRuntimeGraphExcludedPath } from './runtime-graph-filter.js';
|
|
5
3
|
import { isWithinHmrScope } from '../../helpers/hmr-scope.js';
|
|
6
|
-
import {
|
|
4
|
+
import { collectGraphUpdateModulesForHotUpdate } from '../frameworks/angular/server/websocket-angular-hot-update.js';
|
|
7
5
|
import { getAppCssState } from '../../helpers/app-css-state.js';
|
|
8
6
|
import { collectCssHotUpdatePaths } from './websocket-css-hot-update.js';
|
|
9
7
|
import { classifyHmrUpdateKind, formatHmrUpdateSummary } from './perf-instrumentation.js';
|
|
10
8
|
import { createHmrPendingMessage } from './websocket-hmr-pending.js';
|
|
11
|
-
import {
|
|
12
|
-
import { cleanCode, collectImportDependencies, processSfcCode, rewriteImports } from './websocket-device-transform.js';
|
|
13
|
-
import { isSameAngularModuleRel } from './angular-root-component.js';
|
|
9
|
+
import { getServerOrigin } from './server-origin.js';
|
|
14
10
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
11
|
+
* Shared, framework-agnostic prologue for every strategy's `handleHotUpdate`:
|
|
12
|
+
* scope gating, update-metrics setup, the `ns:hmr-pending` broadcast, awaiting the
|
|
13
|
+
* initial graph population, the common module-graph upsert, CSS hot-update +
|
|
14
|
+
* Tailwind content-CSS broadcast, and the project-scope filter. Returns `null`
|
|
15
|
+
* when one of those steps fully handles the change (every such path resolves to
|
|
16
|
+
* Vite's `undefined` result, so the caller just returns), otherwise the
|
|
17
|
+
* per-invocation {@link HotUpdatePrologueState} (`root`, `updateRel`, the live
|
|
18
|
+
* `metrics` object, the idempotent `emitSummary`) the per-flavor tail consumes.
|
|
19
|
+
* Exported so per-flavor strategies can own `prologue + their tail`.
|
|
20
20
|
*/
|
|
21
|
-
export async function
|
|
22
|
-
const { wss, moduleGraph, strategy, verbose,
|
|
21
|
+
export async function runHotUpdatePrologue(ctx, deps) {
|
|
22
|
+
const { wss, moduleGraph, strategy, verbose, getHmrSourceRootsCached, isSocketClientOpen } = deps;
|
|
23
23
|
const APP_ROOT_DIR = deps.appRootDir;
|
|
24
24
|
const graphInitialPopulationPromise = deps.getGraphInitialPopulationPromise();
|
|
25
25
|
const { file, server } = ctx;
|
|
26
26
|
if (!wss) {
|
|
27
|
-
return;
|
|
27
|
+
return null;
|
|
28
28
|
}
|
|
29
29
|
if (isRuntimeGraphExcludedPath(file)) {
|
|
30
|
-
return;
|
|
30
|
+
return null;
|
|
31
31
|
}
|
|
32
32
|
// Authoritative "what triggers HMR" gate, applied before the pending
|
|
33
33
|
// overlay broadcast below: react only to files inside the app source
|
|
@@ -36,7 +36,7 @@ export async function handleNsHotUpdate(ctx, deps) {
|
|
|
36
36
|
if (verbose) {
|
|
37
37
|
console.log(`[ns-hmr][server] ignored change (outside HMR source scope): ${file}`);
|
|
38
38
|
}
|
|
39
|
-
return;
|
|
39
|
+
return null;
|
|
40
40
|
}
|
|
41
41
|
// Always-on update timing. Captures the four phases (await,
|
|
42
42
|
// framework, broadcast, total) plus invalidated module count
|
|
@@ -135,8 +135,10 @@ export async function handleNsHotUpdate(ctx, deps) {
|
|
|
135
135
|
updateMetrics.tAfterAwait = Date.now();
|
|
136
136
|
// Graph update for this file change (wrapped to avoid aborting rest of handler)
|
|
137
137
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
// Angular HTML templates opt out via `skipDefaultGraphUpdate` — their tail
|
|
139
|
+
// re-queries the graph and drives the in-place swap / reboot itself.
|
|
140
|
+
const skipDefaultGraphUpdate = strategy.skipDefaultGraphUpdate?.(file) ?? false;
|
|
141
|
+
if (!skipDefaultGraphUpdate) {
|
|
140
142
|
const graphTargets = collectGraphUpdateModulesForHotUpdate({
|
|
141
143
|
file,
|
|
142
144
|
flavor: strategy.flavor,
|
|
@@ -169,7 +171,7 @@ export async function handleNsHotUpdate(ctx, deps) {
|
|
|
169
171
|
// needs the same contract because Solid HMR depends
|
|
170
172
|
// on the client re-fetching the just-changed module
|
|
171
173
|
// to drive `solid-refresh.patchRegistry`.
|
|
172
|
-
broadcastDelta: strategy.
|
|
174
|
+
broadcastDelta: !strategy.deferDeltaBroadcast,
|
|
173
175
|
});
|
|
174
176
|
}
|
|
175
177
|
catch (error) {
|
|
@@ -224,7 +226,7 @@ export async function handleNsHotUpdate(ctx, deps) {
|
|
|
224
226
|
if (verbose)
|
|
225
227
|
console.log(`[hmr-ws] Hot update for: ${file} → broadcast CSS paths: ${cssPaths.join(', ')}`);
|
|
226
228
|
emitHmrUpdateSummary();
|
|
227
|
-
return;
|
|
229
|
+
return null;
|
|
228
230
|
}
|
|
229
231
|
// CSS without a broadcast target (no appEntryCss
|
|
230
232
|
// configured) — fall through to the scope filter.
|
|
@@ -237,7 +239,7 @@ export async function handleNsHotUpdate(ctx, deps) {
|
|
|
237
239
|
const inApp = normalizedFile.includes(appDir);
|
|
238
240
|
const shouldIgnore = !(inSrcOrCore || inApp);
|
|
239
241
|
if (shouldIgnore)
|
|
240
|
-
return;
|
|
242
|
+
return null;
|
|
241
243
|
if (verbose)
|
|
242
244
|
console.log(`[hmr-ws] Hot update for: ${file}`);
|
|
243
245
|
// Tailwind / content-scanning CSS broadcast for non-CSS edits.
|
|
@@ -323,838 +325,6 @@ export async function handleNsHotUpdate(ctx, deps) {
|
|
|
323
325
|
console.warn('[hmr-ws] CSS content-source broadcast failed:', error);
|
|
324
326
|
}
|
|
325
327
|
}
|
|
326
|
-
|
|
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 [];
|
|
328
|
+
return { root, updateRel, metrics: updateMetrics, emitSummary: emitHmrUpdateSummary };
|
|
1159
329
|
}
|
|
1160
330
|
//# sourceMappingURL=websocket-hot-update.js.map
|