@nativescript/vite 8.0.0-alpha.5 → 8.0.0-alpha.6
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/helpers/global-defines.d.ts +55 -0
- package/helpers/global-defines.js +81 -0
- package/helpers/global-defines.js.map +1 -1
- package/helpers/logging.d.ts +1 -0
- package/helpers/logging.js +36 -3
- package/helpers/logging.js.map +1 -1
- package/hmr/client/hmr-pending-overlay.d.ts +27 -0
- package/hmr/client/hmr-pending-overlay.js +50 -0
- package/hmr/client/hmr-pending-overlay.js.map +1 -0
- package/hmr/client/index.js +72 -1
- package/hmr/client/index.js.map +1 -1
- package/hmr/client/utils.d.ts +5 -0
- package/hmr/client/utils.js +153 -15
- package/hmr/client/utils.js.map +1 -1
- package/hmr/entry-runtime.js +95 -31
- package/hmr/entry-runtime.js.map +1 -1
- package/hmr/frameworks/angular/client/index.d.ts +1 -0
- package/hmr/frameworks/angular/client/index.js +424 -11
- package/hmr/frameworks/angular/client/index.js.map +1 -1
- package/hmr/server/perf-instrumentation.d.ts +118 -0
- package/hmr/server/perf-instrumentation.js +198 -0
- package/hmr/server/perf-instrumentation.js.map +1 -0
- package/hmr/server/shared-transform-request.js +12 -5
- package/hmr/server/shared-transform-request.js.map +1 -1
- package/hmr/server/websocket-angular-hot-update.d.ts +16 -0
- package/hmr/server/websocket-angular-hot-update.js +163 -1
- package/hmr/server/websocket-angular-hot-update.js.map +1 -1
- package/hmr/server/websocket-graph-upsert.d.ts +15 -0
- package/hmr/server/websocket-graph-upsert.js +20 -0
- package/hmr/server/websocket-graph-upsert.js.map +1 -1
- package/hmr/server/websocket-hmr-pending.d.ts +43 -0
- package/hmr/server/websocket-hmr-pending.js +55 -0
- package/hmr/server/websocket-hmr-pending.js.map +1 -0
- package/hmr/server/websocket-ns-m-finalize.js +1 -1
- package/hmr/server/websocket-ns-m-finalize.js.map +1 -1
- package/hmr/server/websocket-ns-m-paths.d.ts +1 -1
- package/hmr/server/websocket-ns-m-paths.js +59 -13
- package/hmr/server/websocket-ns-m-paths.js.map +1 -1
- package/hmr/server/websocket-ns-m-request.js +1 -16
- package/hmr/server/websocket-ns-m-request.js.map +1 -1
- package/hmr/server/websocket-runtime-compat.js.map +1 -1
- package/hmr/server/websocket-served-module-helpers.js +42 -18
- package/hmr/server/websocket-served-module-helpers.js.map +1 -1
- package/hmr/server/websocket-vue-sfc.js +3 -6
- package/hmr/server/websocket-vue-sfc.js.map +1 -1
- package/hmr/server/websocket.d.ts +4 -4
- package/hmr/server/websocket.js +614 -177
- package/hmr/server/websocket.js.map +1 -1
- package/hmr/shared/runtime/boot-timeline.d.ts +17 -0
- package/hmr/shared/runtime/boot-timeline.js +54 -0
- package/hmr/shared/runtime/boot-timeline.js.map +1 -0
- package/hmr/shared/runtime/dev-overlay.d.ts +49 -2
- package/hmr/shared/runtime/dev-overlay.js +587 -12
- package/hmr/shared/runtime/dev-overlay.js.map +1 -1
- package/hmr/shared/runtime/session-bootstrap.js +49 -0
- package/hmr/shared/runtime/session-bootstrap.js.map +1 -1
- package/package.json +1 -1
package/hmr/server/websocket.js
CHANGED
|
@@ -35,17 +35,25 @@ import { getCliFlags } from '../../helpers/cli-flags.js';
|
|
|
35
35
|
import { normalizeCoreSub as normalizeCoreSubCanonical } from '../../helpers/ns-core-url.js';
|
|
36
36
|
import { isRuntimeGraphExcludedPath, matchesRuntimeGraphModuleId, normalizeRuntimeGraphPath, shouldIncludeRuntimeGraphFile, shouldSkipRuntimeGraphDirectoryName } from './runtime-graph-filter.js';
|
|
37
37
|
import { resolveAngularCoreHmrImportSource, rewriteAngularEntryRegisterOnly } from './websocket-angular-entry.js';
|
|
38
|
-
import { canonicalizeTransformRequestCacheKey, collectAngularHotUpdateRoots, collectAngularTransformCacheInvalidationUrls, collectAngularTransitiveImportersForInvalidation, collectGraphUpdateModulesForHotUpdate, normalizeHotReloadMatchPath, shouldInvalidateAngularTransitiveImporters, shouldSuppressDefaultViteHotUpdate, shouldSuppressViteFullReloadPayload } from './websocket-angular-hot-update.js';
|
|
39
|
-
import { classifyGraphUpsert, shouldBroadcastGraphUpsertDelta } from './websocket-graph-upsert.js';
|
|
38
|
+
import { angularSourceHasSemanticDecorator, canonicalizeTransformRequestCacheKey, collectAngularEvictionUrls, collectAngularHotUpdateRoots, collectAngularTransformCacheInvalidationUrls, collectAngularTransitiveImportersForInvalidation, collectGraphUpdateModulesForHotUpdate, normalizeHotReloadMatchPath, shouldInvalidateAngularTransitiveImporters, shouldSuppressDefaultViteHotUpdate, shouldSuppressViteFullReloadPayload } from './websocket-angular-hot-update.js';
|
|
39
|
+
import { classifyGraphUpsert, shouldBroadcastGraphUpsertDelta, shouldBumpGraphVersion } from './websocket-graph-upsert.js';
|
|
40
|
+
import { classifyBootRoute, classifyHmrUpdateKind, createColdBootRequestCounter, formatHmrUpdateSummary, formatPopulateInitialGraphSummary, formatServerStartupBanner } from './perf-instrumentation.js';
|
|
41
|
+
import { createHmrPendingMessage } from './websocket-hmr-pending.js';
|
|
40
42
|
import { extractVitePrebundleId, filterExistingNodeModulesTransformCandidates, getBlockedDeviceNodeModulesReason, getFlattenedManifestMap, isCoreGlobalsReference, isEsmFrameworkPackageSpecifier, isLikelyNativeScriptPluginSpecifier, isLikelyNativeScriptRuntimePluginSpecifier, isNativeScriptCoreModule, isNativeScriptPluginModule, normalizeNativeScriptCoreSpecifier, normalizeNodeModulesSpecifier, resolveCandidateFilePath, resolveInternalRuntimePluginBareSpecifier, resolveNodeModulesPackageBoundary, resolveVendorFromCandidate, resolveVendorRouting, shouldPreserveBareRuntimePluginSubpathImport, stripDecoratedServePrefixes, tryReadRawExplicitJavaScriptModule, viteDepsPathToBareSpecifier, } from './websocket-module-specifiers.js';
|
|
41
43
|
import { ensureNativeScriptModuleBindings, getProcessCodeResolvedSpecifierOverrides } from './websocket-module-bindings.js';
|
|
42
44
|
import { collectStaticExportNamesFromFile, collectStaticExportOriginsFromFile, ensureVersionedCoreImports, extractDirectExportedNames, normalizeCoreExportOriginsForRuntime, parseCoreBridgeRequest, resolveRuntimeCoreModulePath } from './websocket-core-bridge.js';
|
|
43
45
|
import { createSharedTransformRequestRunner } from './shared-transform-request.js';
|
|
46
|
+
import { formatNsMHmrServeTag, getNumericServeVersionTag, rewriteNsMImportPathForHmr } from './websocket-ns-m-paths.js';
|
|
47
|
+
import { ensureDynamicHmrImportHelper } from './websocket-served-module-helpers.js';
|
|
44
48
|
export { ensureNativeScriptModuleBindings, getProcessCodeResolvedSpecifierOverrides } from './websocket-module-bindings.js';
|
|
45
49
|
export { stripDecoratedServePrefixes, tryReadRawExplicitJavaScriptModule } from './websocket-module-specifiers.js';
|
|
46
50
|
export { collectStaticExportNamesFromFile, collectStaticExportOriginsFromFile, ensureVersionedCoreImports, normalizeCoreExportOriginsForRuntime, parseCoreBridgeRequest } from './websocket-core-bridge.js';
|
|
47
51
|
export { rewriteAngularEntryRegisterOnly } from './websocket-angular-entry.js';
|
|
48
|
-
export
|
|
52
|
+
// Re-export the canonical URL rewriter from `websocket-ns-m-paths.js` so the
|
|
53
|
+
// existing test suites (which import from `./websocket.js`) keep working
|
|
54
|
+
// without churn while the implementation lives in a focused module.
|
|
55
|
+
export { formatNsMHmrServeTag, rewriteNsMImportPathForHmr } from './websocket-ns-m-paths.js';
|
|
56
|
+
export { angularSourceHasSemanticDecorator, canonicalizeTransformRequestCacheKey, collectAngularEvictionUrls, collectAngularHotUpdateRoots, collectAngularTransformCacheInvalidationUrls, collectAngularTransitiveImportersForInvalidation, collectGraphUpdateModulesForHotUpdate, createSharedTransformRequestRunner, normalizeHotReloadMatchPath, shouldInvalidateAngularTransitiveImporters, shouldSuppressDefaultViteHotUpdate, shouldSuppressViteFullReloadPayload, classifyGraphUpsert, shouldBroadcastGraphUpsertDelta, shouldBumpGraphVersion };
|
|
49
57
|
const pluginTransformTypescript = (() => {
|
|
50
58
|
const requireFromHere = createRequire(import.meta.url);
|
|
51
59
|
const loaded = requireFromHere('@babel/plugin-transform-typescript');
|
|
@@ -72,6 +80,34 @@ const APP_VIRTUAL_PREFIX = getProjectAppVirtualPath();
|
|
|
72
80
|
const APP_VIRTUAL_WITH_SLASH = `${APP_VIRTUAL_PREFIX}/`;
|
|
73
81
|
const DEFAULT_MAIN_ENTRY = getProjectAppRelativePath('app.ts');
|
|
74
82
|
const DEFAULT_MAIN_ENTRY_VIRTUAL = getProjectAppVirtualPath('app.ts');
|
|
83
|
+
// alpha.59 — Stable URL + Explicit Invalidation:
|
|
84
|
+
// Memoized resolver for the project bootstrap entry as a posix
|
|
85
|
+
// project-relative path (e.g. `/src/main.ts`). This mirrors the resolution
|
|
86
|
+
// the cold-boot wrapper performs (`getPackageJson().main` →
|
|
87
|
+
// project-relative under `/<APP_ROOT_DIR>/`) so the eviction set for HMR
|
|
88
|
+
// always lines up with the URL the runtime actually re-imports. Resolved
|
|
89
|
+
// at first call and cached: `package.json` is read at startup and never
|
|
90
|
+
// changes during a dev session, so it's safe to memoize.
|
|
91
|
+
let __ns_bootstrap_entry_rel_cached = null;
|
|
92
|
+
function getBootstrapEntryRelPath() {
|
|
93
|
+
if (__ns_bootstrap_entry_rel_cached)
|
|
94
|
+
return __ns_bootstrap_entry_rel_cached;
|
|
95
|
+
let entry = DEFAULT_MAIN_ENTRY_VIRTUAL;
|
|
96
|
+
try {
|
|
97
|
+
const pkg = getPackageJson();
|
|
98
|
+
const main = (pkg && pkg.main) || DEFAULT_MAIN_ENTRY;
|
|
99
|
+
const abs = getProjectFilePath(main).replace(/\\/g, '/');
|
|
100
|
+
const marker = `/${APP_ROOT_DIR}/`;
|
|
101
|
+
const idx = abs.indexOf(marker);
|
|
102
|
+
entry = idx >= 0 ? abs.substring(idx) : DEFAULT_MAIN_ENTRY_VIRTUAL;
|
|
103
|
+
}
|
|
104
|
+
catch { }
|
|
105
|
+
if (!entry.startsWith('/')) {
|
|
106
|
+
entry = '/' + entry;
|
|
107
|
+
}
|
|
108
|
+
__ns_bootstrap_entry_rel_cached = entry;
|
|
109
|
+
return entry;
|
|
110
|
+
}
|
|
75
111
|
const STRATEGY_REGISTRY = new Map([
|
|
76
112
|
['vue', vueServerStrategy],
|
|
77
113
|
['angular', angularServerStrategy],
|
|
@@ -318,44 +354,12 @@ function ensureGuardPlainDynamicImports(code, origin) {
|
|
|
318
354
|
return code;
|
|
319
355
|
}
|
|
320
356
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const helper = 'const __nsDynamicHmrImport = (spec) => {\n' +
|
|
328
|
-
" const __nsm = '/ns' + '/m';\n" +
|
|
329
|
-
" const __nsBootPrefix = typeof import.meta !== 'undefined' && import.meta && typeof import.meta.url === 'string' && import.meta.url.includes('/__ns_boot__/b1/') ? '/__ns_boot__/b1' : '';\n" +
|
|
330
|
-
" const __nsImporterTagMatch = typeof import.meta !== 'undefined' && import.meta && typeof import.meta.url === 'string' ? import.meta.url.match(/\\/__ns_hmr__\\/([^/]+)\\//) : null;\n" +
|
|
331
|
-
" const __nsImporterTag = __nsImporterTagMatch && __nsImporterTagMatch[1] ? decodeURIComponent(__nsImporterTagMatch[1]) : '';\n" +
|
|
332
|
-
" try { if (!spec || spec === '@') { return import(new URL(__nsm + '/__invalid_at__.mjs', import.meta.url).href); } } catch {}\n" +
|
|
333
|
-
' try {\n' +
|
|
334
|
-
" if (typeof spec === 'string' && spec.startsWith(__nsm + '/')) {\n" +
|
|
335
|
-
' const g = globalThis;\n' +
|
|
336
|
-
" const graphVersion = typeof g.__NS_HMR_GRAPH_VERSION__ === 'number' ? g.__NS_HMR_GRAPH_VERSION__ : 0;\n" +
|
|
337
|
-
" const nonce = typeof g.__NS_HMR_IMPORT_NONCE__ === 'number' ? g.__NS_HMR_IMPORT_NONCE__ : 0;\n" +
|
|
338
|
-
" const __nsActiveBootPrefix = graphVersion || nonce ? '' : __nsBootPrefix;\n" +
|
|
339
|
-
" if (spec.includes('/__ns_hmr__/')) {\n" +
|
|
340
|
-
" const __preservedSpec = !nonce && __nsBootPrefix && spec.startsWith(__nsm + '/__ns_hmr__/') && !spec.includes('/node_modules/') ? __nsm + __nsBootPrefix + spec.slice(__nsm.length) : spec;\n" +
|
|
341
|
-
' return import(new URL(__preservedSpec, import.meta.url).href);\n' +
|
|
342
|
-
' }\n' +
|
|
343
|
-
" if (spec.startsWith(__nsm + '/node_modules/')) { return import(new URL(spec, import.meta.url).href); }\n" +
|
|
344
|
-
" const tag = nonce ? `n${nonce}` : (graphVersion ? `v${graphVersion}` : (__nsImporterTag || 'live'));\n" +
|
|
345
|
-
" const nextPath = __nsm + __nsActiveBootPrefix + '/__ns_hmr__/' + encodeURIComponent(tag) + spec.slice(__nsm.length);\n" +
|
|
346
|
-
" const origin = typeof g.__NS_HTTP_ORIGIN__ === 'string' && /^https?:\\/\\//.test(g.__NS_HTTP_ORIGIN__) ? g.__NS_HTTP_ORIGIN__ : '';\n" +
|
|
347
|
-
' return import(origin ? origin + nextPath : new URL(nextPath, import.meta.url).href);\n' +
|
|
348
|
-
' }\n' +
|
|
349
|
-
' } catch {}\n' +
|
|
350
|
-
' return import(spec);\n' +
|
|
351
|
-
'};\n';
|
|
352
|
-
return helper + code;
|
|
353
|
-
}
|
|
354
|
-
catch {
|
|
355
|
-
return code;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
async function expandStarExports(code, server, projectRoot, verbose) {
|
|
357
|
+
// alpha.59 — `ensureDynamicHmrImportHelper` was previously duplicated
|
|
358
|
+
// here. Single source of truth now lives in
|
|
359
|
+
// `./websocket-served-module-helpers.js`. See that file for the
|
|
360
|
+
// architectural rationale and the current (much smaller) helper
|
|
361
|
+
// implementation.
|
|
362
|
+
async function expandStarExports(code, server, projectRoot, verbose, sharedTransformer) {
|
|
359
363
|
const STAR_RE = /^[ \t]*(export\s+\*\s+from\s+["'])([^"']+)(["'];?)[ \t]*$/gm;
|
|
360
364
|
let match;
|
|
361
365
|
const replacements = [];
|
|
@@ -367,25 +371,41 @@ async function expandStarExports(code, server, projectRoot, verbose) {
|
|
|
367
371
|
}
|
|
368
372
|
if (!replacements.length)
|
|
369
373
|
return code;
|
|
370
|
-
|
|
374
|
+
// Pull target URLs through the shared runner when it's available so each
|
|
375
|
+
// node_modules path shares the 60s TTL cache with the main /ns/m pipeline
|
|
376
|
+
// and respects the global concurrency gate. Fan them out in parallel —
|
|
377
|
+
// this block used to be a serial `for await` loop, which dominated cold
|
|
378
|
+
// boot on apps with dozens of star-re-exports.
|
|
379
|
+
const transformer = sharedTransformer ?? ((url) => server.transformRequest(url));
|
|
380
|
+
const resolved = await Promise.all(replacements.map(async (rep) => {
|
|
371
381
|
try {
|
|
372
382
|
let vitePath = rep.url.replace(/^https?:\/\/[^/]+/, '');
|
|
373
383
|
vitePath = vitePath.replace(/^\/ns\/m\//, '/');
|
|
374
384
|
vitePath = vitePath.replace(/^\/__ns_boot__\/[^/]+/, '');
|
|
375
385
|
vitePath = vitePath.replace(/\/__ns_hmr__\/[^/]+/, '');
|
|
376
|
-
const result = await
|
|
386
|
+
const result = await transformer(vitePath);
|
|
377
387
|
if (!result?.code)
|
|
378
|
-
|
|
388
|
+
return null;
|
|
379
389
|
const names = extractExportedNames(result.code);
|
|
380
390
|
if (!names.length)
|
|
381
|
-
|
|
382
|
-
const explicit = `export { ${names.join(', ')} } from ${JSON.stringify(rep.url)};`;
|
|
383
|
-
code = code.replace(rep.full, explicit);
|
|
391
|
+
return null;
|
|
384
392
|
if (verbose) {
|
|
385
|
-
|
|
393
|
+
try {
|
|
394
|
+
console.log(`[ns/m] expanded export* -> ${names.length} names from ${vitePath}`);
|
|
395
|
+
}
|
|
396
|
+
catch { }
|
|
386
397
|
}
|
|
398
|
+
return { rep, names };
|
|
387
399
|
}
|
|
388
|
-
catch {
|
|
400
|
+
catch {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
}));
|
|
404
|
+
for (const entry of resolved) {
|
|
405
|
+
if (!entry)
|
|
406
|
+
continue;
|
|
407
|
+
const explicit = `export { ${entry.names.join(', ')} } from ${JSON.stringify(entry.rep.url)};`;
|
|
408
|
+
code = code.replace(entry.rep.full, explicit);
|
|
389
409
|
}
|
|
390
410
|
return code;
|
|
391
411
|
}
|
|
@@ -894,52 +914,14 @@ function toNodeModulesHttpModuleId(importPath) {
|
|
|
894
914
|
}
|
|
895
915
|
return `/ns/m/node_modules/${nodeModulesSpecifier}`;
|
|
896
916
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
}
|
|
906
|
-
if (/^\d+$/.test(raw)) {
|
|
907
|
-
return `v${raw}`;
|
|
908
|
-
}
|
|
909
|
-
return raw;
|
|
910
|
-
};
|
|
911
|
-
if (!p || !p.startsWith('/ns/m/')) {
|
|
912
|
-
return p;
|
|
913
|
-
}
|
|
914
|
-
const canonicalNodeModulesPath = p.replace(/^\/ns\/m\/__ns_boot__\/b1\/__ns_hmr__\/[^/]+\/node_modules\//, '/ns/m/node_modules/').replace(/^\/ns\/m\/__ns_hmr__\/[^/]+\/node_modules\//, '/ns/m/node_modules/');
|
|
915
|
-
if (canonicalNodeModulesPath.startsWith('/ns/m/node_modules/')) {
|
|
916
|
-
return canonicalNodeModulesPath;
|
|
917
|
-
}
|
|
918
|
-
if (canonicalNodeModulesPath.startsWith('/ns/m/__ns_boot__/')) {
|
|
919
|
-
return canonicalNodeModulesPath;
|
|
920
|
-
}
|
|
921
|
-
if (canonicalNodeModulesPath.startsWith('/ns/m/__ns_hmr__/')) {
|
|
922
|
-
return bootTaggedRequest ? `/ns/m/__ns_boot__/b1${canonicalNodeModulesPath.slice('/ns/m'.length)}` : canonicalNodeModulesPath;
|
|
923
|
-
}
|
|
924
|
-
const tag = toHmrServeTag(ver);
|
|
925
|
-
const hmrPrefix = `/ns/m/__ns_hmr__/${tag}`;
|
|
926
|
-
const bootHmrPrefix = `/ns/m/__ns_boot__/b1/__ns_hmr__/${tag}`;
|
|
927
|
-
return (bootTaggedRequest ? bootHmrPrefix : hmrPrefix) + canonicalNodeModulesPath.slice('/ns/m'.length);
|
|
928
|
-
}
|
|
929
|
-
function getNumericServeVersionTag(tag, fallback) {
|
|
930
|
-
const raw = String(tag || '').trim();
|
|
931
|
-
if (!raw) {
|
|
932
|
-
return fallback;
|
|
933
|
-
}
|
|
934
|
-
const versionMatch = raw.match(/^v(\d+)$/);
|
|
935
|
-
if (versionMatch?.[1]) {
|
|
936
|
-
return Number(versionMatch[1]);
|
|
937
|
-
}
|
|
938
|
-
if (/^\d+$/.test(raw)) {
|
|
939
|
-
return Number(raw);
|
|
940
|
-
}
|
|
941
|
-
return fallback;
|
|
942
|
-
}
|
|
917
|
+
// alpha.59 — `rewriteNsMImportPathForHmr` and `getNumericServeVersionTag`
|
|
918
|
+
// previously lived here as duplicates of the implementations in
|
|
919
|
+
// `./websocket-ns-m-paths.js`. The path rewriter is part of the
|
|
920
|
+
// "Stable URL + Explicit Invalidation" architecture (see
|
|
921
|
+
// HMR_STABLE_URL_INVALIDATION_PLAN.md) and must be a single source of
|
|
922
|
+
// truth so the canonicalization rules can't drift between the two files.
|
|
923
|
+
// They are imported above and re-exported below for tests / external
|
|
924
|
+
// callers that historically reached them through this module.
|
|
943
925
|
function normalizeAbsoluteFilesystemImport(spec, importerPath, projectRoot) {
|
|
944
926
|
if (!spec || typeof spec !== 'string') {
|
|
945
927
|
return null;
|
|
@@ -2331,10 +2313,21 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2331
2313
|
let registrySent = false;
|
|
2332
2314
|
let vendorBootstrapDone = false;
|
|
2333
2315
|
let pluginRoot;
|
|
2334
|
-
|
|
2316
|
+
// graphVersion starts at 1 so the very first /ns/m response uses a stable
|
|
2317
|
+
// `v1` URL tag (see dynamic-import helper at lines 398-432). Keeping it
|
|
2318
|
+
// stable during cold boot prevents double-loads when the graph fills up
|
|
2319
|
+
// lazily as modules are served.
|
|
2320
|
+
let graphVersion = 1;
|
|
2335
2321
|
// Transactional HMR batches: map graphVersion -> ordered list of changed ids for that version
|
|
2336
2322
|
const txnBatches = new Map();
|
|
2337
2323
|
const graph = new Map();
|
|
2324
|
+
// Tracks the background initial-graph population so handleHotUpdate can
|
|
2325
|
+
// await completion before computing delta roots for the first HMR event.
|
|
2326
|
+
let graphInitialPopulationPromise = null;
|
|
2327
|
+
// Cold-boot /ns/m request counter — populated the first time a /ns/m
|
|
2328
|
+
// request arrives, finalized when the request window goes idle.
|
|
2329
|
+
// See Shared across requests so a single counter spans the whole cold boot.
|
|
2330
|
+
let coldBootCounter = null;
|
|
2338
2331
|
function rememberAngularReloadSuppression(root, file, ttlMs = 3000) {
|
|
2339
2332
|
const absPath = normalizeHotReloadMatchPath(file);
|
|
2340
2333
|
const relPath = normalizeHotReloadMatchPath(file, root);
|
|
@@ -2506,12 +2499,17 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2506
2499
|
const classification = classifyGraphUpsert(existing, hash, normDeps);
|
|
2507
2500
|
if (classification === 'unchanged')
|
|
2508
2501
|
return existing;
|
|
2509
|
-
|
|
2502
|
+
// Version bumps are only meaningful for live edits — serve-time graph
|
|
2503
|
+
// warm-ups and the initial bulk walk should leave graphVersion stable.
|
|
2504
|
+
const bumpVersion = shouldBumpGraphVersion(classification, options?.bumpVersion !== false);
|
|
2505
|
+
if (bumpVersion) {
|
|
2506
|
+
graphVersion++;
|
|
2507
|
+
}
|
|
2510
2508
|
const gm = { id, deps: normDeps, hash };
|
|
2511
2509
|
graph.set(id, gm);
|
|
2512
2510
|
if (verbose) {
|
|
2513
2511
|
try {
|
|
2514
|
-
console.log('[hmr-ws][graph] upsert', { id, deps: normDeps, hash, graphVersion, classification });
|
|
2512
|
+
console.log('[hmr-ws][graph] upsert', { id, deps: normDeps, hash, graphVersion, classification, bumpVersion });
|
|
2515
2513
|
console.log('[hmr-ws][graph] size', graph.size);
|
|
2516
2514
|
}
|
|
2517
2515
|
catch { }
|
|
@@ -2539,6 +2537,8 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2539
2537
|
async function populateInitialGraph(server) {
|
|
2540
2538
|
if (graph.size)
|
|
2541
2539
|
return; // already populated
|
|
2540
|
+
const tStart = Date.now();
|
|
2541
|
+
const versionAtStart = graphVersion;
|
|
2542
2542
|
const root = server.config.root || process.cwd();
|
|
2543
2543
|
// Avoid direct require in ESM build: lazily obtain fs & path via createRequire or dynamic import
|
|
2544
2544
|
let fs;
|
|
@@ -2554,6 +2554,18 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2554
2554
|
fs = await import('fs');
|
|
2555
2555
|
pathMod = await import('path');
|
|
2556
2556
|
}
|
|
2557
|
+
// Route every bulk transform through `sharedTransformRequest` when it's
|
|
2558
|
+
// already been wired up — this way the background walk shares the 60s
|
|
2559
|
+
// TTL cache with live /ns/m requests, so the device sees cached results
|
|
2560
|
+
// for any file the walker already visited. The fallback keeps the
|
|
2561
|
+
// walker working during server tests where the shared runner isn't
|
|
2562
|
+
// constructed yet.
|
|
2563
|
+
const bulkTransform = (rel) => {
|
|
2564
|
+
if (sharedTransformRequest) {
|
|
2565
|
+
return sharedTransformRequest(rel);
|
|
2566
|
+
}
|
|
2567
|
+
return server.transformRequest(rel);
|
|
2568
|
+
};
|
|
2557
2569
|
async function walk(dir) {
|
|
2558
2570
|
for (const name of fs.readdirSync(dir)) {
|
|
2559
2571
|
if (name === 'node_modules' || name.startsWith('.') || shouldSkipRuntimeGraphDirectoryName(name))
|
|
@@ -2568,7 +2580,7 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2568
2580
|
const rel = '/' + pathMod.relative(root, full).split(pathMod.sep).join('/');
|
|
2569
2581
|
// Transform via Vite to gather deps (ignore failures)
|
|
2570
2582
|
try {
|
|
2571
|
-
const transformed = await
|
|
2583
|
+
const transformed = await bulkTransform(rel);
|
|
2572
2584
|
const code = transformed?.code || '';
|
|
2573
2585
|
const deps = [];
|
|
2574
2586
|
// fallback to import relationships via moduleGraph
|
|
@@ -2579,7 +2591,10 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2579
2591
|
deps.push(m.id.split('?')[0]);
|
|
2580
2592
|
}
|
|
2581
2593
|
}
|
|
2582
|
-
|
|
2594
|
+
// bumpVersion: false — the initial walk is a bulk load, not a live
|
|
2595
|
+
// edit. Keeping graphVersion stable during cold boot avoids double
|
|
2596
|
+
// cache-key drift described in Track 1.3 of the HMR plan.
|
|
2597
|
+
upsertGraphModule(rel, code, deps, { bumpVersion: false });
|
|
2583
2598
|
}
|
|
2584
2599
|
catch { }
|
|
2585
2600
|
}
|
|
@@ -2592,6 +2607,36 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2592
2607
|
await walk(pathMod.join(root, 'src'));
|
|
2593
2608
|
}
|
|
2594
2609
|
catch { }
|
|
2610
|
+
// Diagnostic summary: Always-on so slow
|
|
2611
|
+
// cold-boot walks surface even when verbose is off. A `bumpedVersion=no`
|
|
2612
|
+
// result is the happy path (Track 1.3); `yes` indicates a regression.
|
|
2613
|
+
try {
|
|
2614
|
+
console.info(formatPopulateInitialGraphSummary({
|
|
2615
|
+
moduleCount: graph.size,
|
|
2616
|
+
durationMs: Date.now() - tStart,
|
|
2617
|
+
graphVersion,
|
|
2618
|
+
bumpedVersion: graphVersion !== versionAtStart,
|
|
2619
|
+
}));
|
|
2620
|
+
}
|
|
2621
|
+
catch { }
|
|
2622
|
+
}
|
|
2623
|
+
// Kick off `populateInitialGraph` in the background (non-awaited) so /ns/m
|
|
2624
|
+
// responses are never blocked on a full tree walk. Returns the shared
|
|
2625
|
+
// promise so hot-update code paths can await completion before computing
|
|
2626
|
+
// delta roots for the first HMR event.
|
|
2627
|
+
function ensureInitialGraphPopulationStarted(server) {
|
|
2628
|
+
if (graphInitialPopulationPromise) {
|
|
2629
|
+
return graphInitialPopulationPromise;
|
|
2630
|
+
}
|
|
2631
|
+
if (graph.size) {
|
|
2632
|
+
graphInitialPopulationPromise = Promise.resolve();
|
|
2633
|
+
return graphInitialPopulationPromise;
|
|
2634
|
+
}
|
|
2635
|
+
graphInitialPopulationPromise = populateInitialGraph(server).catch((error) => {
|
|
2636
|
+
if (verbose)
|
|
2637
|
+
console.warn('[hmr-ws][graph] background initial population failed', error);
|
|
2638
|
+
});
|
|
2639
|
+
return graphInitialPopulationPromise;
|
|
2595
2640
|
}
|
|
2596
2641
|
return {
|
|
2597
2642
|
name: 'nativescript-hmr-websocket',
|
|
@@ -2620,12 +2665,22 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2620
2665
|
return originalSend(payload, ...rest);
|
|
2621
2666
|
});
|
|
2622
2667
|
}
|
|
2623
|
-
//
|
|
2624
|
-
//
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2668
|
+
// Transform concurrency. Historically we defaulted to 1 to avoid
|
|
2669
|
+
// race conditions during HTTP HMR startup, but the shared runner
|
|
2670
|
+
// already has per-URL coalescing and an async-cached result map,
|
|
2671
|
+
// so higher fan-out is safe and dramatically reduces cold-boot
|
|
2672
|
+
// time (Track 1.4 in HMR_CORE_REALM_DETERMINISTIC_PLAN.md). We cap
|
|
2673
|
+
// at 8 by default to match typical dev machines and respect Vite's
|
|
2674
|
+
// internal worker pool limits. Override via the env var when needed.
|
|
2675
|
+
const configuredTransformConcurrency = Number.parseInt(process.env.NS_VITE_HMR_TRANSFORM_CONCURRENCY || '', 10);
|
|
2676
|
+
const transformConcurrency = Number.isFinite(configuredTransformConcurrency) && configuredTransformConcurrency > 0 ? configuredTransformConcurrency : 8;
|
|
2677
|
+
// Keep transformed code cached for longer across HMR updates so that
|
|
2678
|
+
// unchanged neighbours of an edited file don't re-run through the
|
|
2679
|
+
// Angular/TypeScript/Vite transform pipeline. The HMR flow
|
|
2680
|
+
// explicitly invalidates affected URLs, so a longer TTL is safe.
|
|
2681
|
+
// See Track 2.2 in HMR_CORE_REALM_DETERMINISTIC_PLAN.md.
|
|
2682
|
+
const configuredTransformCacheMs = Number.parseInt(process.env.NS_VITE_HMR_TRANSFORM_CACHE_MS || '', 10);
|
|
2683
|
+
const transformCacheMs = Number.isFinite(configuredTransformCacheMs) && configuredTransformCacheMs >= 0 ? configuredTransformCacheMs : 60000;
|
|
2629
2684
|
sharedTransformRequest = createSharedTransformRequestRunner((url) => server.transformRequest(url), (url, timeoutMs) => {
|
|
2630
2685
|
try {
|
|
2631
2686
|
console.warn('[ns:m] slow transformRequest for', url, '(>' + timeoutMs + 'ms)');
|
|
@@ -2636,6 +2691,120 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2636
2691
|
resultCacheTtlMs: transformCacheMs,
|
|
2637
2692
|
getResultCacheKey: (url) => canonicalizeTransformRequestCacheKey(url, pluginRoot || process.cwd()),
|
|
2638
2693
|
});
|
|
2694
|
+
// Always-on startup banner — prints once per dev server process so
|
|
2695
|
+
// anyone investigating perf can immediately see which build is live
|
|
2696
|
+
// and what knobs are active. See HMR_CORE_REALM_DETERMINISTIC_PLAN.md
|
|
2697
|
+
// ("Track 1 + 2 — diagnostic instrumentation").
|
|
2698
|
+
try {
|
|
2699
|
+
let pkgVersion = 'unknown';
|
|
2700
|
+
try {
|
|
2701
|
+
const req = createRequire(import.meta.url);
|
|
2702
|
+
const pkg = req('@nativescript/vite/package.json');
|
|
2703
|
+
if (pkg && typeof pkg.version === 'string')
|
|
2704
|
+
pkgVersion = pkg.version;
|
|
2705
|
+
}
|
|
2706
|
+
catch {
|
|
2707
|
+
// `@nativescript/vite/package.json` is not always exported; fall
|
|
2708
|
+
// back to reading the file from disk next to this module.
|
|
2709
|
+
try {
|
|
2710
|
+
const here = new URL(import.meta.url).pathname;
|
|
2711
|
+
const pkgPath = path.resolve(path.dirname(here), '..', '..', 'package.json');
|
|
2712
|
+
if (existsSync(pkgPath)) {
|
|
2713
|
+
const parsed = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
2714
|
+
if (parsed && typeof parsed.version === 'string')
|
|
2715
|
+
pkgVersion = parsed.version;
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
catch { }
|
|
2719
|
+
}
|
|
2720
|
+
console.info(formatServerStartupBanner({
|
|
2721
|
+
version: pkgVersion,
|
|
2722
|
+
transformConcurrency,
|
|
2723
|
+
transformCacheMs,
|
|
2724
|
+
lazyInitialGraph: true,
|
|
2725
|
+
graphVersion,
|
|
2726
|
+
}));
|
|
2727
|
+
}
|
|
2728
|
+
catch { }
|
|
2729
|
+
// Always-on cold-boot request trace. Runs in front of every other
|
|
2730
|
+
// middleware so it catches all NS dev routes (/ns/m/*, /ns/rt/*,
|
|
2731
|
+
// /ns/core/*, /__ns_boot__/*, etc.) with a single hook. Closes
|
|
2732
|
+
// itself after an idle window so HMR edits don't get rolled into
|
|
2733
|
+
// the cold-boot numbers. The idle window is generous by default
|
|
2734
|
+
// (5s) because V8's HTTP ESM resolver pauses between dep levels
|
|
2735
|
+
// while parsing — a too-tight window was closing after the first
|
|
2736
|
+
// wave and under-reporting boot by 100x. Override via
|
|
2737
|
+
// NS_VITE_HMR_BOOT_TRACE_IDLE_MS when profiling something tricky.
|
|
2738
|
+
// See HMR_CORE_REALM_DETERMINISTIC_PLAN.md ("Track 1 + 2 — round
|
|
2739
|
+
// three, 2026-04").
|
|
2740
|
+
try {
|
|
2741
|
+
const configuredIdleMs = Number.parseInt(process.env.NS_VITE_HMR_BOOT_TRACE_IDLE_MS || '', 10);
|
|
2742
|
+
const idleWindowMs = Number.isFinite(configuredIdleMs) && configuredIdleMs > 0 ? configuredIdleMs : 5000;
|
|
2743
|
+
const configuredSummaryEvery = Number.parseInt(process.env.NS_VITE_HMR_BOOT_TRACE_PROGRESS_EVERY || '', 10);
|
|
2744
|
+
const summaryEvery = Number.isFinite(configuredSummaryEvery) && configuredSummaryEvery >= 0 ? configuredSummaryEvery : 25;
|
|
2745
|
+
if (!coldBootCounter) {
|
|
2746
|
+
coldBootCounter = createColdBootRequestCounter({
|
|
2747
|
+
summaryEvery,
|
|
2748
|
+
idleWindowMs,
|
|
2749
|
+
log: (line) => {
|
|
2750
|
+
try {
|
|
2751
|
+
console.info(line);
|
|
2752
|
+
}
|
|
2753
|
+
catch { }
|
|
2754
|
+
},
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
catch { }
|
|
2759
|
+
server.middlewares.use((req, res, next) => {
|
|
2760
|
+
try {
|
|
2761
|
+
const urlObj = new URL(req.url || '', 'http://localhost');
|
|
2762
|
+
const route = classifyBootRoute(urlObj.pathname);
|
|
2763
|
+
if (route === 'other')
|
|
2764
|
+
return next();
|
|
2765
|
+
if (!coldBootCounter)
|
|
2766
|
+
return next();
|
|
2767
|
+
const handle = coldBootCounter.record(urlObj.pathname);
|
|
2768
|
+
const finishOnce = () => {
|
|
2769
|
+
try {
|
|
2770
|
+
handle.finish();
|
|
2771
|
+
}
|
|
2772
|
+
catch { }
|
|
2773
|
+
};
|
|
2774
|
+
try {
|
|
2775
|
+
res.once('finish', finishOnce);
|
|
2776
|
+
res.once('close', finishOnce);
|
|
2777
|
+
}
|
|
2778
|
+
catch { }
|
|
2779
|
+
}
|
|
2780
|
+
catch { }
|
|
2781
|
+
next();
|
|
2782
|
+
});
|
|
2783
|
+
// Give `populateInitialGraph` a head start. Previously this only
|
|
2784
|
+
// kicked off on the first /ns/m hit, which meant populate was
|
|
2785
|
+
// competing with the device for the same 8 transform slots
|
|
2786
|
+
// throughout the first 4-5 seconds of cold boot. Starting at
|
|
2787
|
+
// `configureServer` time gives populate the full app build/launch
|
|
2788
|
+
// window (typically 2-3s on simulator) as a head start, so more
|
|
2789
|
+
// of its work lands before the device even connects. Disable via
|
|
2790
|
+
// NS_VITE_HMR_DISABLE_POPULATE=1 when profiling whether populate
|
|
2791
|
+
// is helping or hurting a specific app. See
|
|
2792
|
+
// HMR_CORE_REALM_DETERMINISTIC_PLAN.md ("Track 1 + 2 — round
|
|
2793
|
+
// three").
|
|
2794
|
+
try {
|
|
2795
|
+
const disablePopulate = process.env.NS_VITE_HMR_DISABLE_POPULATE === '1' || process.env.NS_VITE_HMR_DISABLE_POPULATE === 'true';
|
|
2796
|
+
if (disablePopulate) {
|
|
2797
|
+
if (verbose)
|
|
2798
|
+
console.info('[hmr-ws][populate] disabled via NS_VITE_HMR_DISABLE_POPULATE');
|
|
2799
|
+
// Short-circuit: mark as resolved so /ns/m never schedules it and
|
|
2800
|
+
// HMR still works (handleHotUpdate just has no pre-warmed graph).
|
|
2801
|
+
graphInitialPopulationPromise = Promise.resolve();
|
|
2802
|
+
}
|
|
2803
|
+
else {
|
|
2804
|
+
ensureInitialGraphPopulationStarted(server);
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
catch { }
|
|
2639
2808
|
// Attempt early vendor manifest bootstrap once per server.
|
|
2640
2809
|
if (!vendorBootstrapDone) {
|
|
2641
2810
|
vendorBootstrapDone = true;
|
|
@@ -2910,25 +3079,23 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2910
3079
|
const urlObj = new URL(req.url || '', 'http://localhost');
|
|
2911
3080
|
if (!urlObj.pathname.startsWith('/ns/m'))
|
|
2912
3081
|
return next();
|
|
2913
|
-
//
|
|
2914
|
-
// non-zero
|
|
2915
|
-
//
|
|
2916
|
-
//
|
|
2917
|
-
//
|
|
2918
|
-
//
|
|
2919
|
-
//
|
|
2920
|
-
//
|
|
2921
|
-
//
|
|
2922
|
-
//
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
}
|
|
2931
|
-
}
|
|
3082
|
+
// Previously we awaited `populateInitialGraph(server)` here so
|
|
3083
|
+
// graphVersion would be non-zero for the first /ns/m request.
|
|
3084
|
+
// That gave deterministic URL tags but blocked the cold boot on a
|
|
3085
|
+
// full src/ tree walk (hundreds of transformRequest calls, 3-6s).
|
|
3086
|
+
//
|
|
3087
|
+
// Track 1.3: graphVersion now starts at 1 and stays stable during
|
|
3088
|
+
// cold boot (see `upsertGraphModule`'s bumpVersion option and the
|
|
3089
|
+
// inline comment at the graphVersion declaration). We kick off the
|
|
3090
|
+
// initial population in the background so it doesn't block the first
|
|
3091
|
+
// response. `handleHotUpdate` awaits the same promise so the first
|
|
3092
|
+
// HMR event still sees a fully populated graph.
|
|
3093
|
+
ensureInitialGraphPopulationStarted(server);
|
|
3094
|
+
// Cold-boot counter is now hooked via the leading boot-trace
|
|
3095
|
+
// middleware (see `configureServer` — it records the request
|
|
3096
|
+
// and tracks finish() via res.on('close'/'finish')). This
|
|
3097
|
+
// handler used to record here but that missed the
|
|
3098
|
+
// round-trip timing and didn't track per-route breakdowns.
|
|
2932
3099
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
2933
3100
|
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
2934
3101
|
// Disable caching for dev ESM endpoints to avoid device-side stale module reuse
|
|
@@ -3207,7 +3374,9 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
3207
3374
|
}
|
|
3208
3375
|
catch { }
|
|
3209
3376
|
}
|
|
3210
|
-
|
|
3377
|
+
// Serve-time warm-up: no live edit happened, so don't bump
|
|
3378
|
+
// graphVersion. See Track 1.3 in HMR_CORE_REALM_DETERMINISTIC_PLAN.md.
|
|
3379
|
+
upsertGraphModule(id, code, deps, { bumpVersion: false });
|
|
3211
3380
|
}
|
|
3212
3381
|
}
|
|
3213
3382
|
}
|
|
@@ -3357,7 +3526,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3357
3526
|
// misses re-exported names). By expanding to `export { a, b } from "url"`,
|
|
3358
3527
|
// the engine sees explicit named exports and resolves them correctly.
|
|
3359
3528
|
try {
|
|
3360
|
-
code = await expandStarExports(code, server, server.config?.root || process.cwd(), verbose);
|
|
3529
|
+
code = await expandStarExports(code, server, server.config?.root || process.cwd(), verbose, sharedTransformRequest);
|
|
3361
3530
|
}
|
|
3362
3531
|
catch (e) {
|
|
3363
3532
|
if (verbose)
|
|
@@ -3430,45 +3599,62 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3430
3599
|
}
|
|
3431
3600
|
}
|
|
3432
3601
|
catch { }
|
|
3602
|
+
// alpha.59 — `/ns/rt` and `/ns/core` URL versioning.
|
|
3603
|
+
//
|
|
3604
|
+
// Pre-alpha.59 these URLs were emitted as `/ns/rt/<ver>`
|
|
3605
|
+
// and `/ns/core/<ver>` so V8's HTTP module cache would see
|
|
3606
|
+
// a fresh URL on every save. The runtime canonicalizer
|
|
3607
|
+
// (HMRSupport.mm `CanonicalizeHttpUrlKey`) collapses these
|
|
3608
|
+
// version segments to the bare `/ns/rt` and `/ns/core`
|
|
3609
|
+
// keys before lookup, so V8 actually saw a single cache
|
|
3610
|
+
// entry — but the server was doing extra work to inject a
|
|
3611
|
+
// version segment that the runtime then immediately
|
|
3612
|
+
// stripped. Now that alpha.59 has explicit eviction (and
|
|
3613
|
+
// these bridge endpoints don't change at HMR time
|
|
3614
|
+
// anyway), the version segment is purely vestigial.
|
|
3615
|
+
//
|
|
3616
|
+
// Rather than rip the helpers out (which would touch
|
|
3617
|
+
// every ensureVersionedImports caller and risk bumping
|
|
3618
|
+
// older runtimes), we keep them but pass `verNum=0`. The
|
|
3619
|
+
// helpers still normalize URL shape (strip the absolute
|
|
3620
|
+
// origin prefix when present) but emit a stable
|
|
3621
|
+
// `/ns/rt/0` / `/ns/core/0` URL — which collapses to
|
|
3622
|
+
// `/ns/rt` / `/ns/core` in the runtime.
|
|
3433
3623
|
try {
|
|
3434
|
-
const verNum =
|
|
3624
|
+
const verNum = 0;
|
|
3435
3625
|
code = ensureVersionedRtImports(code, getServerOrigin(server), verNum);
|
|
3436
3626
|
code = ACTIVE_STRATEGY.ensureVersionedImports(code, getServerOrigin(server), verNum);
|
|
3437
3627
|
code = ensureVersionedCoreImports(code, getServerOrigin(server), verNum);
|
|
3438
3628
|
}
|
|
3439
3629
|
catch { }
|
|
3440
|
-
//
|
|
3441
|
-
//
|
|
3442
|
-
//
|
|
3630
|
+
// alpha.59 — `/ns/m` URL finalize step.
|
|
3631
|
+
//
|
|
3632
|
+
// `rewriteNsMImportPathForHmr` (Phase 3a) is now a
|
|
3633
|
+
// canonicalizer: it strips legacy `__ns_hmr__/<tag>/`
|
|
3634
|
+
// segments and adds `__ns_boot__/b1/` only for boot-tagged
|
|
3635
|
+
// requests. The `ver` parameter is preserved on the
|
|
3636
|
+
// signature for API compatibility but is ignored for app
|
|
3637
|
+
// modules (cache busting is driven by
|
|
3638
|
+
// `__nsInvalidateModules`, not URL versioning). We pass
|
|
3639
|
+
// `'v0'` as a stable placeholder — the canonicalizer
|
|
3640
|
+
// emits the same URL regardless of this value, but a
|
|
3641
|
+
// constant placeholder makes the contract explicit.
|
|
3642
|
+
//
|
|
3643
|
+
// SFC URLs (line below, `/ns/sfc/${verTag}/...`) still
|
|
3644
|
+
// embed a version because the Vue SFC pathway does not
|
|
3645
|
+
// yet have an eviction protocol. The runtime canonicalizer
|
|
3646
|
+
// does NOT strip `/ns/sfc/<ver>/`, so Vue users still see
|
|
3647
|
+
// per-save SFC re-fetches — that's a known follow-up
|
|
3648
|
+
// (HMR_STABLE_URL_INVALIDATION_PLAN.md "Vue Follow-up").
|
|
3443
3649
|
try {
|
|
3444
|
-
const
|
|
3445
|
-
const
|
|
3446
|
-
|
|
3447
|
-
// 'live' is the client dynamic-import helper's fallback when
|
|
3448
|
-
// globalThis.__NS_HMR_GRAPH_VERSION__ has not yet been set (i.e. before the
|
|
3449
|
-
// full HMR client takes over). If we propagate 'live' into child import
|
|
3450
|
-
// URLs, a file loaded through a 'live'-tagged parent ends up at
|
|
3451
|
-
// /ns/m/__ns_hmr__/live/... in the iOS V8 module cache while a later
|
|
3452
|
-
// dyn-import at v${N} loads the same file at /ns/m/__ns_hmr__/v${N}/...
|
|
3453
|
-
// — two distinct cache entries, two module realms, two Angular class
|
|
3454
|
-
// identities per @Component, and NG0912 selector collisions. Replace
|
|
3455
|
-
// 'live' with the current graph version so every child resolves to one
|
|
3456
|
-
// canonical URL regardless of the parent's request tag.
|
|
3457
|
-
if (raw === 'live' && gv > 0) {
|
|
3458
|
-
return `v${gv}`;
|
|
3459
|
-
}
|
|
3460
|
-
if (raw) {
|
|
3461
|
-
if (raw === 'live' || /^n\d+$/i.test(raw) || /^v[^/]+$/i.test(raw)) {
|
|
3462
|
-
return raw;
|
|
3463
|
-
}
|
|
3464
|
-
if (/^\d+$/.test(raw)) {
|
|
3465
|
-
return `v${raw}`;
|
|
3466
|
-
}
|
|
3467
|
-
}
|
|
3468
|
-
return `v${String(graphVersion || 0)}`;
|
|
3650
|
+
const verTag = (() => {
|
|
3651
|
+
const numeric = getNumericServeVersionTag(forcedVer, Number(graphVersion || 0));
|
|
3652
|
+
return numeric > 0 ? `v${numeric}` : 'v0';
|
|
3469
3653
|
})();
|
|
3470
3654
|
const origin = getServerOrigin(server);
|
|
3471
|
-
const rewritePath = (p) => rewriteNsMImportPathForHmr(p,
|
|
3655
|
+
const rewritePath = (p) => rewriteNsMImportPathForHmr(p, 'v0', bootTaggedRequest);
|
|
3656
|
+
// /ns/m URL forms — all collapse to canonical stable
|
|
3657
|
+
// URLs via the Phase 3a rewriter.
|
|
3472
3658
|
// 1) Static imports: import ... from "/ns/m/..."
|
|
3473
3659
|
code = code.replace(/(from\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
|
|
3474
3660
|
// 2) Side-effect imports: import "/ns/m/..."
|
|
@@ -3479,14 +3665,14 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3479
3665
|
code = code.replace(/(new\s+URL\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*,\s*import\.meta\.url\s*\))/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
|
|
3480
3666
|
// 5) __ns_import(new URL('/ns/m/...', import.meta.url).href)
|
|
3481
3667
|
code = code.replace(/(new\s+URL\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*,\s*import\.meta\.url\s*\)\.href)/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
|
|
3482
|
-
// 6) Force absolute HTTP for new URL('/ns/m/...', import.meta.url).href →
|
|
3668
|
+
// 6) Force absolute HTTP for new URL('/ns/m/...', import.meta.url).href → canonical stable URL.
|
|
3483
3669
|
try {
|
|
3484
3670
|
code = code.replace(/new\s+URL\(\s*["'](\/ns\/m\/[^"'?]+)(?:\?[^"']*)?["']\s*,\s*import\.meta\.url\s*\)\.href/g, (_m, p1) => `${JSON.stringify(`${origin}${rewritePath(p1)}`)}`);
|
|
3485
3671
|
}
|
|
3486
3672
|
catch { }
|
|
3487
|
-
// 7)
|
|
3673
|
+
// 7) SFC URLs (Vue) — still versioned. See header comment.
|
|
3488
3674
|
try {
|
|
3489
|
-
code = code.replace(/new\s+URL\(\s*["']\/ns\/sfc(\/[^"'?]+)(?:\?[^"']*)?["']\s*,\s*import\.meta\.url\s*\)\.href/g, (_m, p1) => `${JSON.stringify(`${origin}/ns/sfc/${
|
|
3675
|
+
code = code.replace(/new\s+URL\(\s*["']\/ns\/sfc(\/[^"'?]+)(?:\?[^"']*)?["']\s*,\s*import\.meta\.url\s*\)\.href/g, (_m, p1) => `${JSON.stringify(`${origin}/ns/sfc/${verTag}${p1}`)}`);
|
|
3490
3676
|
}
|
|
3491
3677
|
catch { }
|
|
3492
3678
|
}
|
|
@@ -5868,6 +6054,103 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
5868
6054
|
if (isRuntimeGraphExcludedPath(file)) {
|
|
5869
6055
|
return;
|
|
5870
6056
|
}
|
|
6057
|
+
// Always-on update timing — see HMR_CORE_REALM_DETERMINISTIC_PLAN.md
|
|
6058
|
+
// ("Track 2 — round one, 2026-04: HMR update metrics"). Captures
|
|
6059
|
+
// the four phases (await, framework, broadcast, total) plus
|
|
6060
|
+
// invalidated module count and recipient count. Emitted at the
|
|
6061
|
+
// end of this function via `emitHmrUpdateSummary()`. Single line,
|
|
6062
|
+
// always-on so a 6-second `.ts` save is immediately visible
|
|
6063
|
+
// without flipping verbose.
|
|
6064
|
+
const updateRoot = server.config.root || process.cwd();
|
|
6065
|
+
const updateRel = (() => {
|
|
6066
|
+
try {
|
|
6067
|
+
return '/' + path.posix.normalize(path.relative(updateRoot, file)).split(path.sep).join('/');
|
|
6068
|
+
}
|
|
6069
|
+
catch {
|
|
6070
|
+
return file;
|
|
6071
|
+
}
|
|
6072
|
+
})();
|
|
6073
|
+
const updateMetrics = {
|
|
6074
|
+
file: updateRel,
|
|
6075
|
+
kind: classifyHmrUpdateKind(file),
|
|
6076
|
+
t0: Date.now(),
|
|
6077
|
+
tAfterAwait: 0,
|
|
6078
|
+
tAfterFramework: 0,
|
|
6079
|
+
tEnd: 0,
|
|
6080
|
+
invalidated: 0,
|
|
6081
|
+
recipients: 0,
|
|
6082
|
+
// Round-eight diagnostic — populated by the angular branch when
|
|
6083
|
+
// the changed file is `.ts`, otherwise remains undefined and is
|
|
6084
|
+
// omitted from the summary line entirely. See
|
|
6085
|
+
// HMR_CORE_REALM_DETERMINISTIC_PLAN.md ("Round-eight — surface
|
|
6086
|
+
// narrowing decision").
|
|
6087
|
+
narrowed: undefined,
|
|
6088
|
+
emitted: false,
|
|
6089
|
+
};
|
|
6090
|
+
// alpha.62 follow-up — broadcast a "pending" notification at
|
|
6091
|
+
// the very start of handleHotUpdate so the client can show
|
|
6092
|
+
// the HMR-applying overlay BEFORE we spend time on graph
|
|
6093
|
+
// updates / transforms / dependency analysis (typically
|
|
6094
|
+
// 7–200ms on a warm cache). Without this, the overlay only
|
|
6095
|
+
// appears at `ns:angular-update` broadcast time and the
|
|
6096
|
+
// user perceives a "delayed" reaction to their save.
|
|
6097
|
+
//
|
|
6098
|
+
// Fire-and-forget: a failed pending broadcast must never
|
|
6099
|
+
// hold up the actual update. The client treats receipt of
|
|
6100
|
+
// `ns:angular-update` (or `ns:css-updates`) as authoritative;
|
|
6101
|
+
// the pending message is purely a UX hint.
|
|
6102
|
+
try {
|
|
6103
|
+
const pendingPayload = JSON.stringify(createHmrPendingMessage({
|
|
6104
|
+
origin: getServerOrigin(server),
|
|
6105
|
+
path: updateMetrics.file,
|
|
6106
|
+
kind: updateMetrics.kind,
|
|
6107
|
+
timestamp: updateMetrics.t0,
|
|
6108
|
+
}));
|
|
6109
|
+
wss.clients.forEach((client) => {
|
|
6110
|
+
if (isSocketClientOpen(client)) {
|
|
6111
|
+
try {
|
|
6112
|
+
client.send(pendingPayload);
|
|
6113
|
+
}
|
|
6114
|
+
catch { }
|
|
6115
|
+
}
|
|
6116
|
+
});
|
|
6117
|
+
}
|
|
6118
|
+
catch { }
|
|
6119
|
+
const emitHmrUpdateSummary = () => {
|
|
6120
|
+
if (updateMetrics.emitted)
|
|
6121
|
+
return;
|
|
6122
|
+
updateMetrics.emitted = true;
|
|
6123
|
+
updateMetrics.tEnd = Date.now();
|
|
6124
|
+
try {
|
|
6125
|
+
const awaitMs = (updateMetrics.tAfterAwait || updateMetrics.t0) - updateMetrics.t0;
|
|
6126
|
+
const frameworkMs = (updateMetrics.tAfterFramework || updateMetrics.tAfterAwait || updateMetrics.t0) - (updateMetrics.tAfterAwait || updateMetrics.t0);
|
|
6127
|
+
const broadcastMs = updateMetrics.tEnd - (updateMetrics.tAfterFramework || updateMetrics.tAfterAwait || updateMetrics.t0);
|
|
6128
|
+
const totalMs = updateMetrics.tEnd - updateMetrics.t0;
|
|
6129
|
+
console.info(formatHmrUpdateSummary({
|
|
6130
|
+
file: updateMetrics.file,
|
|
6131
|
+
kind: updateMetrics.kind,
|
|
6132
|
+
awaitMs,
|
|
6133
|
+
frameworkMs,
|
|
6134
|
+
broadcastMs,
|
|
6135
|
+
totalMs,
|
|
6136
|
+
invalidated: updateMetrics.invalidated,
|
|
6137
|
+
recipients: updateMetrics.recipients,
|
|
6138
|
+
narrowed: updateMetrics.narrowed,
|
|
6139
|
+
}));
|
|
6140
|
+
}
|
|
6141
|
+
catch { }
|
|
6142
|
+
};
|
|
6143
|
+
// Track 1.3: the first /ns/m request kicks off populateInitialGraph
|
|
6144
|
+
// in the background. If an HMR update races in before that walk
|
|
6145
|
+
// completes, we'd lose transitive-importer data. Await completion
|
|
6146
|
+
// here so the delta computation below always sees a populated graph.
|
|
6147
|
+
if (graphInitialPopulationPromise) {
|
|
6148
|
+
try {
|
|
6149
|
+
await graphInitialPopulationPromise;
|
|
6150
|
+
}
|
|
6151
|
+
catch { }
|
|
6152
|
+
}
|
|
6153
|
+
updateMetrics.tAfterAwait = Date.now();
|
|
5871
6154
|
// Graph update for this file change (wrapped to avoid aborting rest of handler)
|
|
5872
6155
|
try {
|
|
5873
6156
|
const skipAngularHtmlGraphUpdate = ACTIVE_STRATEGY.flavor === 'angular' && /\.(html|htm)$/i.test(file);
|
|
@@ -5917,6 +6200,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
5917
6200
|
console.log(`[hmr-ws] Hot update for: ${file}`);
|
|
5918
6201
|
// Handle CSS updates
|
|
5919
6202
|
if (file.endsWith('.css')) {
|
|
6203
|
+
updateMetrics.tAfterFramework = Date.now();
|
|
5920
6204
|
try {
|
|
5921
6205
|
let rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
|
|
5922
6206
|
const origin = getServerOrigin(server);
|
|
@@ -5935,12 +6219,14 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
5935
6219
|
wss.clients.forEach((client) => {
|
|
5936
6220
|
if (isSocketClientOpen(client)) {
|
|
5937
6221
|
client.send(JSON.stringify(msg));
|
|
6222
|
+
updateMetrics.recipients += 1;
|
|
5938
6223
|
}
|
|
5939
6224
|
});
|
|
5940
6225
|
}
|
|
5941
6226
|
catch (error) {
|
|
5942
6227
|
console.warn('[hmr-ws] CSS update failed:', error);
|
|
5943
6228
|
}
|
|
6229
|
+
emitHmrUpdateSummary();
|
|
5944
6230
|
return;
|
|
5945
6231
|
}
|
|
5946
6232
|
// Framework-specific hot update handling
|
|
@@ -5956,6 +6242,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
5956
6242
|
});
|
|
5957
6243
|
if (!(isHtml || isTs))
|
|
5958
6244
|
return;
|
|
6245
|
+
updateMetrics.invalidated += angularHotUpdateRoots.length;
|
|
5959
6246
|
if (angularHotUpdateRoots.length) {
|
|
5960
6247
|
for (const mod of angularHotUpdateRoots) {
|
|
5961
6248
|
try {
|
|
@@ -5972,13 +6259,80 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
5972
6259
|
}
|
|
5973
6260
|
}
|
|
5974
6261
|
const angularTransitiveInvalidationRoots = (angularHotUpdateRoots.length ? angularHotUpdateRoots : ctx.modules);
|
|
5975
|
-
|
|
6262
|
+
// Round-six narrowing: read the source for `.ts/.tsx/.js/.jsx`
|
|
6263
|
+
// edits so `shouldInvalidateAngularTransitiveImporters` can
|
|
6264
|
+
// distinguish leaf modules (constants/utils) from real
|
|
6265
|
+
// Angular files. If `ctx.read()` throws (file deleted, race
|
|
6266
|
+
// against the watcher), `angularChangedSource` stays
|
|
6267
|
+
// undefined and we fall back to the conservative
|
|
6268
|
+
// pre-round-six "always invalidate transitively" behavior.
|
|
6269
|
+
let angularChangedSource;
|
|
6270
|
+
if (isTs) {
|
|
5976
6271
|
try {
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
6272
|
+
angularChangedSource = await ctx.read();
|
|
6273
|
+
}
|
|
6274
|
+
catch {
|
|
6275
|
+
angularChangedSource = undefined;
|
|
6276
|
+
}
|
|
6277
|
+
}
|
|
6278
|
+
const angularNeedsTransitive = shouldInvalidateAngularTransitiveImporters({
|
|
6279
|
+
flavor: ACTIVE_STRATEGY.flavor,
|
|
6280
|
+
file,
|
|
6281
|
+
source: angularChangedSource,
|
|
6282
|
+
});
|
|
6283
|
+
// Round-eight diagnostic. We surface the narrowing decision on
|
|
6284
|
+
// every `.ts` Angular hot update (HTML routes always invalidate
|
|
6285
|
+
// transitively today and aren't subject to Round-Seven, so we
|
|
6286
|
+
// leave them as `undefined` — the field is omitted from the
|
|
6287
|
+
// summary line). The boolean is the inverse of
|
|
6288
|
+
// `angularNeedsTransitive` because "needs transitive" is the
|
|
6289
|
+
// pre-Round-Seven (broad) behavior.
|
|
6290
|
+
if (isTs) {
|
|
6291
|
+
updateMetrics.narrowed = !angularNeedsTransitive;
|
|
6292
|
+
}
|
|
6293
|
+
// alpha.59 — Stable URL + Explicit Invalidation:
|
|
6294
|
+
//
|
|
6295
|
+
// Compute the transitive importer closure ONCE here and reuse it
|
|
6296
|
+
// for (a) `server.moduleGraph.invalidateModule` (so Vite's
|
|
6297
|
+
// transform pipeline re-runs on next request), (b) the shared
|
|
6298
|
+
// transform-request cache, and (c) the runtime eviction set we
|
|
6299
|
+
// broadcast in `ns:angular-update`. Pre-alpha.59 code computed
|
|
6300
|
+
// this list twice in adjacent try blocks; consolidating it
|
|
6301
|
+
// removes a redundant graph walk and guarantees the three
|
|
6302
|
+
// consumers see the exact same set of importers (otherwise a
|
|
6303
|
+
// late module-graph mutation between calls could leave an
|
|
6304
|
+
// asymmetric narrowed/broad mix).
|
|
6305
|
+
//
|
|
6306
|
+
// alpha.59.1 — separate Vite-transform narrowing from runtime
|
|
6307
|
+
// eviction. `angularNeedsTransitive` answers the question "does
|
|
6308
|
+
// the changed file's symbol shape change such that importers
|
|
6309
|
+
// must be re-transformed by Vite?". The runtime, however, has
|
|
6310
|
+
// a stricter requirement: ESM live bindings only refresh if
|
|
6311
|
+
// the importing module re-evaluates inside V8. A constants
|
|
6312
|
+
// file with no Angular decorator does NOT need a Vite
|
|
6313
|
+
// re-transform of its importers (their compiled JS is
|
|
6314
|
+
// identical), but its importers still hold stale bindings to
|
|
6315
|
+
// the OLD constants Module record. After eviction + re-import
|
|
6316
|
+
// of `main.ts`, V8 sees the cached importers, returns them
|
|
6317
|
+
// unchanged, and they continue to read the OLD values. The
|
|
6318
|
+
// user-visible symptom: HMR completes successfully, logs are
|
|
6319
|
+
// clean, but the simulator does not reflect the change.
|
|
6320
|
+
//
|
|
6321
|
+
// The fix: ALWAYS compute the transitive importer closure for
|
|
6322
|
+
// runtime eviction. Only skip Vite's `moduleGraph.invalidate`
|
|
6323
|
+
// + transform-cache purge when `angularNeedsTransitive` is
|
|
6324
|
+
// false — those are the genuine narrowing wins (saves
|
|
6325
|
+
// re-transform work on the server). The eviction set always
|
|
6326
|
+
// includes importers so V8 re-fetches and re-binds them.
|
|
6327
|
+
let transitiveImporters = [];
|
|
6328
|
+
try {
|
|
6329
|
+
transitiveImporters = collectAngularTransitiveImportersForInvalidation({
|
|
6330
|
+
modules: angularTransitiveInvalidationRoots,
|
|
6331
|
+
isExcluded: (id) => id.includes('/node_modules/'),
|
|
6332
|
+
maxDepth: 16,
|
|
6333
|
+
});
|
|
6334
|
+
if (angularNeedsTransitive) {
|
|
6335
|
+
updateMetrics.invalidated += transitiveImporters.length;
|
|
5982
6336
|
for (const mod of transitiveImporters) {
|
|
5983
6337
|
try {
|
|
5984
6338
|
server.moduleGraph.invalidateModule(mod);
|
|
@@ -5993,24 +6347,40 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
5993
6347
|
console.log('[hmr-ws][angular] invalidated transitive importers:', transitiveImporters.length);
|
|
5994
6348
|
}
|
|
5995
6349
|
}
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
6350
|
+
else if (isTs && typeof angularChangedSource === 'string') {
|
|
6351
|
+
// Round-eight: was previously gated on `verbose`. Surfacing this
|
|
6352
|
+
// log unconditionally lets the user immediately confirm whether
|
|
6353
|
+
// Round-Seven's narrowing fired for a given `.ts` edit (the
|
|
6354
|
+
// summary line below still emits `narrowed=yes`/`no`, but
|
|
6355
|
+
// having both makes the decision easier to spot in noisy logs
|
|
6356
|
+
// and lets the user diff scenarios without flipping
|
|
6357
|
+
// `NS_HMR_VERBOSE=true`).
|
|
6358
|
+
//
|
|
6359
|
+
// alpha.59.1 — narrowing now means "skip Vite re-transform"
|
|
6360
|
+
// (the importers still get evicted from the V8 module
|
|
6361
|
+
// registry so live bindings refresh). The log call-out is
|
|
6362
|
+
// preserved but extended with the importer count to make the
|
|
6363
|
+
// distinction visible.
|
|
6364
|
+
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)`);
|
|
5999
6365
|
}
|
|
6000
6366
|
}
|
|
6367
|
+
catch (error) {
|
|
6368
|
+
if (verbose)
|
|
6369
|
+
console.warn('[hmr-ws][angular] transitive importer collection failed', error);
|
|
6370
|
+
}
|
|
6001
6371
|
try {
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6372
|
+
// alpha.59.1 — purge shared transform cache for the changed
|
|
6373
|
+
// file + hot-update roots unconditionally (their transform
|
|
6374
|
+
// output IS different now). Transitive importers are only
|
|
6375
|
+
// purged when narrowing decides their output may have
|
|
6376
|
+
// changed; otherwise their cached transforms are still
|
|
6377
|
+
// valid (compiled JS is identical even though the runtime
|
|
6378
|
+
// must re-evaluate them to refresh ESM bindings).
|
|
6009
6379
|
const transformCacheInvalidationUrls = new Set(collectAngularTransformCacheInvalidationUrls({
|
|
6010
6380
|
file,
|
|
6011
6381
|
isTs,
|
|
6012
6382
|
hotUpdateRoots: angularHotUpdateRoots,
|
|
6013
|
-
transitiveImporters,
|
|
6383
|
+
transitiveImporters: angularNeedsTransitive ? transitiveImporters : [],
|
|
6014
6384
|
projectRoot: server.config.root || process.cwd(),
|
|
6015
6385
|
}));
|
|
6016
6386
|
if (transformCacheInvalidationUrls.size) {
|
|
@@ -6024,17 +6394,74 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
6024
6394
|
if (verbose)
|
|
6025
6395
|
console.warn('[hmr-ws][angular] shared transform cache purge failed', error);
|
|
6026
6396
|
}
|
|
6397
|
+
updateMetrics.tAfterFramework = Date.now();
|
|
6027
6398
|
try {
|
|
6028
6399
|
const root = server.config.root || process.cwd();
|
|
6029
6400
|
const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
|
|
6030
6401
|
rememberAngularReloadSuppression(root, file);
|
|
6031
6402
|
const origin = getServerOrigin(server);
|
|
6403
|
+
const bootstrapEntryRel = getBootstrapEntryRelPath();
|
|
6404
|
+
// alpha.59 — Stable URL + Explicit Invalidation:
|
|
6405
|
+
//
|
|
6406
|
+
// `evictPaths` is the canonical list of `/ns/m/<rel>` URLs
|
|
6407
|
+
// the runtime must drop from `g_moduleRegistry` before
|
|
6408
|
+
// re-importing `importerEntry`. Pre-alpha.59 the server
|
|
6409
|
+
// signaled invalidation by bumping a global `graphVersion`
|
|
6410
|
+
// counter and embedding it in every URL — but V8 keys the
|
|
6411
|
+
// module registry by full URL, so a v1 → v2 bump
|
|
6412
|
+
// effectively flushed the entire dependency graph from
|
|
6413
|
+
// the cache and forced the runtime to re-fetch + re-eval
|
|
6414
|
+
// every transitively-imported module on each save (~3s
|
|
6415
|
+
// HMR cycles, dominated by Vite's single-threaded transform
|
|
6416
|
+
// pipeline). The new model:
|
|
6417
|
+
//
|
|
6418
|
+
// 1. URLs are stable: `/ns/m/<rel>` everywhere, no `vN`.
|
|
6419
|
+
// 2. The server walks the inverse-dependency closure and
|
|
6420
|
+
// sends only the modules that actually need to be
|
|
6421
|
+
// re-evaluated (typically O(1) for component edits,
|
|
6422
|
+
// or the changed file + entry for narrowed edits).
|
|
6423
|
+
// 3. The client calls `__nsInvalidateModules(evictPaths)`
|
|
6424
|
+
// and re-imports `importerEntry`, which causes V8 to
|
|
6425
|
+
// refetch ONLY those modules. Everything else stays
|
|
6426
|
+
// hot in the registry.
|
|
6427
|
+
//
|
|
6428
|
+
// Invariants enforced by `collectAngularEvictionUrls`:
|
|
6429
|
+
// - Always includes the changed file (so the new source
|
|
6430
|
+
// is fetched).
|
|
6431
|
+
// - Always includes `importerEntry` (so re-import
|
|
6432
|
+
// re-evaluates).
|
|
6433
|
+
// - Excludes node_modules (vendor packages are stable).
|
|
6434
|
+
// - Excludes virtual / runtime-graph-excluded ids.
|
|
6435
|
+
// - Origin-prefixed: `http://host:port/ns/m/<rel>`.
|
|
6436
|
+
let evictPaths = [];
|
|
6437
|
+
try {
|
|
6438
|
+
evictPaths = collectAngularEvictionUrls({
|
|
6439
|
+
file,
|
|
6440
|
+
hotUpdateRoots: angularHotUpdateRoots,
|
|
6441
|
+
transitiveImporters,
|
|
6442
|
+
projectRoot: root,
|
|
6443
|
+
origin,
|
|
6444
|
+
bootstrapEntry: bootstrapEntryRel,
|
|
6445
|
+
});
|
|
6446
|
+
}
|
|
6447
|
+
catch (error) {
|
|
6448
|
+
if (verbose)
|
|
6449
|
+
console.warn('[hmr-ws][angular] eviction set computation failed', error);
|
|
6450
|
+
}
|
|
6451
|
+
if (verbose) {
|
|
6452
|
+
console.log('[hmr-ws][angular] eviction set', {
|
|
6453
|
+
count: evictPaths.length,
|
|
6454
|
+
importerEntry: bootstrapEntryRel,
|
|
6455
|
+
});
|
|
6456
|
+
}
|
|
6032
6457
|
const msg = {
|
|
6033
6458
|
type: 'ns:angular-update',
|
|
6034
6459
|
origin,
|
|
6035
6460
|
path: rel,
|
|
6036
6461
|
version: graphVersion,
|
|
6037
6462
|
timestamp: Date.now(),
|
|
6463
|
+
evictPaths,
|
|
6464
|
+
importerEntry: bootstrapEntryRel,
|
|
6038
6465
|
};
|
|
6039
6466
|
if (verbose) {
|
|
6040
6467
|
console.log('[hmr-ws][angular] broadcasting update', Array.from(wss.clients || []).map((client) => ({
|
|
@@ -6046,12 +6473,14 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
6046
6473
|
wss.clients.forEach((client) => {
|
|
6047
6474
|
if (isSocketClientOpen(client)) {
|
|
6048
6475
|
client.send(JSON.stringify(msg));
|
|
6476
|
+
updateMetrics.recipients += 1;
|
|
6049
6477
|
}
|
|
6050
6478
|
});
|
|
6051
6479
|
}
|
|
6052
6480
|
catch (error) {
|
|
6053
6481
|
console.warn('[hmr-ws][angular] update failed:', error);
|
|
6054
6482
|
}
|
|
6483
|
+
emitHmrUpdateSummary();
|
|
6055
6484
|
if (shouldSuppressDefaultViteHotUpdate({ flavor: ACTIVE_STRATEGY.flavor, file })) {
|
|
6056
6485
|
return [];
|
|
6057
6486
|
}
|
|
@@ -6059,6 +6488,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
6059
6488
|
}
|
|
6060
6489
|
// TypeScript flavor: emit generic graph delta for app XML/TS/style changes
|
|
6061
6490
|
if (ACTIVE_STRATEGY.flavor === 'typescript') {
|
|
6491
|
+
updateMetrics.tAfterFramework = Date.now();
|
|
6062
6492
|
try {
|
|
6063
6493
|
const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
|
|
6064
6494
|
if (verbose)
|
|
@@ -6072,6 +6502,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
6072
6502
|
if (verbose)
|
|
6073
6503
|
console.warn('[hmr-ws][ts] failed to emit delta for', file, e);
|
|
6074
6504
|
}
|
|
6505
|
+
emitHmrUpdateSummary();
|
|
6075
6506
|
return;
|
|
6076
6507
|
}
|
|
6077
6508
|
// Solid flavor: emit graph delta for app TSX/TS/JSX file changes.
|
|
@@ -6085,6 +6516,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
6085
6516
|
const isSolidFile = /\.(tsx?|jsx?)$/i.test(file);
|
|
6086
6517
|
if (!isSolidFile)
|
|
6087
6518
|
return;
|
|
6519
|
+
updateMetrics.tAfterFramework = Date.now();
|
|
6088
6520
|
try {
|
|
6089
6521
|
const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
|
|
6090
6522
|
if (verbose)
|
|
@@ -6109,6 +6541,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
6109
6541
|
if (verbose)
|
|
6110
6542
|
console.warn('[hmr-ws][solid] failed to handle hot update for', file, e);
|
|
6111
6543
|
}
|
|
6544
|
+
emitHmrUpdateSummary();
|
|
6112
6545
|
return;
|
|
6113
6546
|
}
|
|
6114
6547
|
// Handle .vue file updates
|
|
@@ -6394,6 +6827,10 @@ if (typeof __VUE_HMR_RUNTIME__ === 'undefined') {
|
|
|
6394
6827
|
console.warn('[hmr-ws] HMR update failed:', error);
|
|
6395
6828
|
console.error(error);
|
|
6396
6829
|
}
|
|
6830
|
+
// Vue path emits update summary at the end of the function so
|
|
6831
|
+
// every framework branch gets exactly one log line. Idempotent
|
|
6832
|
+
// — if any branch already emitted, this is a no-op.
|
|
6833
|
+
emitHmrUpdateSummary();
|
|
6397
6834
|
// CRITICAL: Return empty array to prevent Vite's default HMR
|
|
6398
6835
|
return [];
|
|
6399
6836
|
},
|