@nativescript/vite 8.0.0-alpha.5 → 8.0.0-alpha.7
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/base.js +8 -8
- package/configuration/base.js.map +1 -1
- package/helpers/global-defines.d.ts +49 -0
- package/helpers/global-defines.js +75 -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/helpers/main-entry.js +5 -6
- package/helpers/main-entry.js.map +1 -1
- package/helpers/ns-core-url.d.ts +5 -6
- package/helpers/ns-core-url.js +5 -6
- package/helpers/ns-core-url.js.map +1 -1
- package/hmr/client/css-handler.js +2 -1
- package/hmr/client/css-handler.js.map +1 -1
- package/hmr/client/hmr-pending-overlay.d.ts +27 -0
- package/hmr/client/hmr-pending-overlay.js +50 -0
- package/hmr/client/hmr-pending-overlay.js.map +1 -0
- package/hmr/client/index.js +73 -1
- package/hmr/client/index.js.map +1 -1
- package/hmr/client/utils.d.ts +5 -0
- package/hmr/client/utils.js +155 -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 +416 -11
- package/hmr/frameworks/angular/client/index.js.map +1 -1
- package/hmr/server/core-sanitize.js +8 -8
- package/hmr/server/core-sanitize.js.map +1 -1
- package/hmr/server/import-map.js +3 -4
- package/hmr/server/import-map.js.map +1 -1
- package/hmr/server/ns-core-cjs-shape.d.ts +2 -4
- package/hmr/server/ns-core-cjs-shape.js +3 -5
- package/hmr/server/ns-core-cjs-shape.js.map +1 -1
- package/hmr/server/perf-instrumentation.d.ts +114 -0
- package/hmr/server/perf-instrumentation.js +195 -0
- package/hmr/server/perf-instrumentation.js.map +1 -0
- package/hmr/server/shared-transform-request.js +12 -5
- package/hmr/server/shared-transform-request.js.map +1 -1
- package/hmr/server/vite-plugin.js +3 -1
- package/hmr/server/vite-plugin.js.map +1 -1
- package/hmr/server/websocket-angular-hot-update.d.ts +16 -0
- package/hmr/server/websocket-angular-hot-update.js +161 -1
- package/hmr/server/websocket-angular-hot-update.js.map +1 -1
- package/hmr/server/websocket-core-bridge.js +11 -16
- package/hmr/server/websocket-core-bridge.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 +58 -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 +3 -2
- package/hmr/server/websocket-runtime-compat.js.map +1 -1
- package/hmr/server/websocket-served-module-helpers.js +43 -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 +670 -214
- 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 +51 -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 +585 -12
- package/hmr/shared/runtime/dev-overlay.js.map +1 -1
- package/hmr/shared/runtime/session-bootstrap.js +52 -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,33 @@ 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
|
+
// Memoized resolver for the project bootstrap entry as a posix
|
|
84
|
+
// project-relative path (e.g. `/src/main.ts`). This mirrors the
|
|
85
|
+
// resolution the cold-boot wrapper performs (`getPackageJson().main` →
|
|
86
|
+
// project-relative under `/<APP_ROOT_DIR>/`) so the eviction set for
|
|
87
|
+
// HMR always lines up with the URL the runtime actually re-imports.
|
|
88
|
+
// Resolved at first call and cached: `package.json` is read at startup
|
|
89
|
+
// and never changes during a dev session, so it's safe to memoize.
|
|
90
|
+
let __ns_bootstrap_entry_rel_cached = null;
|
|
91
|
+
function getBootstrapEntryRelPath() {
|
|
92
|
+
if (__ns_bootstrap_entry_rel_cached)
|
|
93
|
+
return __ns_bootstrap_entry_rel_cached;
|
|
94
|
+
let entry = DEFAULT_MAIN_ENTRY_VIRTUAL;
|
|
95
|
+
try {
|
|
96
|
+
const pkg = getPackageJson();
|
|
97
|
+
const main = (pkg && pkg.main) || DEFAULT_MAIN_ENTRY;
|
|
98
|
+
const abs = getProjectFilePath(main).replace(/\\/g, '/');
|
|
99
|
+
const marker = `/${APP_ROOT_DIR}/`;
|
|
100
|
+
const idx = abs.indexOf(marker);
|
|
101
|
+
entry = idx >= 0 ? abs.substring(idx) : DEFAULT_MAIN_ENTRY_VIRTUAL;
|
|
102
|
+
}
|
|
103
|
+
catch { }
|
|
104
|
+
if (!entry.startsWith('/')) {
|
|
105
|
+
entry = '/' + entry;
|
|
106
|
+
}
|
|
107
|
+
__ns_bootstrap_entry_rel_cached = entry;
|
|
108
|
+
return entry;
|
|
109
|
+
}
|
|
75
110
|
const STRATEGY_REGISTRY = new Map([
|
|
76
111
|
['vue', vueServerStrategy],
|
|
77
112
|
['angular', angularServerStrategy],
|
|
@@ -318,44 +353,10 @@ function ensureGuardPlainDynamicImports(code, origin) {
|
|
|
318
353
|
return code;
|
|
319
354
|
}
|
|
320
355
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (code.includes('const __nsDynamicHmrImport ='))
|
|
326
|
-
return code;
|
|
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) {
|
|
356
|
+
// `ensureDynamicHmrImportHelper` lives in
|
|
357
|
+
// `./websocket-served-module-helpers.js`. See that file for the
|
|
358
|
+
// architectural rationale and the current helper implementation.
|
|
359
|
+
async function expandStarExports(code, server, projectRoot, verbose, sharedTransformer) {
|
|
359
360
|
const STAR_RE = /^[ \t]*(export\s+\*\s+from\s+["'])([^"']+)(["'];?)[ \t]*$/gm;
|
|
360
361
|
let match;
|
|
361
362
|
const replacements = [];
|
|
@@ -367,25 +368,41 @@ async function expandStarExports(code, server, projectRoot, verbose) {
|
|
|
367
368
|
}
|
|
368
369
|
if (!replacements.length)
|
|
369
370
|
return code;
|
|
370
|
-
|
|
371
|
+
// Pull target URLs through the shared runner when it's available so each
|
|
372
|
+
// node_modules path shares the 60s TTL cache with the main /ns/m pipeline
|
|
373
|
+
// and respects the global concurrency gate. Fan them out in parallel —
|
|
374
|
+
// this block used to be a serial `for await` loop, which dominated cold
|
|
375
|
+
// boot on apps with dozens of star-re-exports.
|
|
376
|
+
const transformer = sharedTransformer ?? ((url) => server.transformRequest(url));
|
|
377
|
+
const resolved = await Promise.all(replacements.map(async (rep) => {
|
|
371
378
|
try {
|
|
372
379
|
let vitePath = rep.url.replace(/^https?:\/\/[^/]+/, '');
|
|
373
380
|
vitePath = vitePath.replace(/^\/ns\/m\//, '/');
|
|
374
381
|
vitePath = vitePath.replace(/^\/__ns_boot__\/[^/]+/, '');
|
|
375
382
|
vitePath = vitePath.replace(/\/__ns_hmr__\/[^/]+/, '');
|
|
376
|
-
const result = await
|
|
383
|
+
const result = await transformer(vitePath);
|
|
377
384
|
if (!result?.code)
|
|
378
|
-
|
|
385
|
+
return null;
|
|
379
386
|
const names = extractExportedNames(result.code);
|
|
380
387
|
if (!names.length)
|
|
381
|
-
|
|
382
|
-
const explicit = `export { ${names.join(', ')} } from ${JSON.stringify(rep.url)};`;
|
|
383
|
-
code = code.replace(rep.full, explicit);
|
|
388
|
+
return null;
|
|
384
389
|
if (verbose) {
|
|
385
|
-
|
|
390
|
+
try {
|
|
391
|
+
console.log(`[ns/m] expanded export* -> ${names.length} names from ${vitePath}`);
|
|
392
|
+
}
|
|
393
|
+
catch { }
|
|
386
394
|
}
|
|
395
|
+
return { rep, names };
|
|
387
396
|
}
|
|
388
|
-
catch {
|
|
397
|
+
catch {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
}));
|
|
401
|
+
for (const entry of resolved) {
|
|
402
|
+
if (!entry)
|
|
403
|
+
continue;
|
|
404
|
+
const explicit = `export { ${entry.names.join(', ')} } from ${JSON.stringify(entry.rep.url)};`;
|
|
405
|
+
code = code.replace(entry.rep.full, explicit);
|
|
389
406
|
}
|
|
390
407
|
return code;
|
|
391
408
|
}
|
|
@@ -894,52 +911,13 @@ function toNodeModulesHttpModuleId(importPath) {
|
|
|
894
911
|
}
|
|
895
912
|
return `/ns/m/node_modules/${nodeModulesSpecifier}`;
|
|
896
913
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
return raw;
|
|
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
|
-
}
|
|
914
|
+
// `rewriteNsMImportPathForHmr` and `getNumericServeVersionTag` live in
|
|
915
|
+
// `./websocket-ns-m-paths.js`. The path rewriter is part of the
|
|
916
|
+
// "Stable URL + Explicit Invalidation" architecture and must be a
|
|
917
|
+
// single source of truth so the canonicalization rules can't drift
|
|
918
|
+
// between modules. They are imported above and re-exported below for
|
|
919
|
+
// tests / external callers that historically reached them through this
|
|
920
|
+
// module.
|
|
943
921
|
function normalizeAbsoluteFilesystemImport(spec, importerPath, projectRoot) {
|
|
944
922
|
if (!spec || typeof spec !== 'string') {
|
|
945
923
|
return null;
|
|
@@ -2331,10 +2309,21 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2331
2309
|
let registrySent = false;
|
|
2332
2310
|
let vendorBootstrapDone = false;
|
|
2333
2311
|
let pluginRoot;
|
|
2334
|
-
|
|
2312
|
+
// graphVersion starts at 1 so the very first /ns/m response uses a stable
|
|
2313
|
+
// `v1` URL tag (see dynamic-import helper at lines 398-432). Keeping it
|
|
2314
|
+
// stable during cold boot prevents double-loads when the graph fills up
|
|
2315
|
+
// lazily as modules are served.
|
|
2316
|
+
let graphVersion = 1;
|
|
2335
2317
|
// Transactional HMR batches: map graphVersion -> ordered list of changed ids for that version
|
|
2336
2318
|
const txnBatches = new Map();
|
|
2337
2319
|
const graph = new Map();
|
|
2320
|
+
// Tracks the background initial-graph population so handleHotUpdate can
|
|
2321
|
+
// await completion before computing delta roots for the first HMR event.
|
|
2322
|
+
let graphInitialPopulationPromise = null;
|
|
2323
|
+
// Cold-boot /ns/m request counter — populated the first time a /ns/m
|
|
2324
|
+
// request arrives, finalized when the request window goes idle.
|
|
2325
|
+
// See Shared across requests so a single counter spans the whole cold boot.
|
|
2326
|
+
let coldBootCounter = null;
|
|
2338
2327
|
function rememberAngularReloadSuppression(root, file, ttlMs = 3000) {
|
|
2339
2328
|
const absPath = normalizeHotReloadMatchPath(file);
|
|
2340
2329
|
const relPath = normalizeHotReloadMatchPath(file, root);
|
|
@@ -2506,12 +2495,17 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2506
2495
|
const classification = classifyGraphUpsert(existing, hash, normDeps);
|
|
2507
2496
|
if (classification === 'unchanged')
|
|
2508
2497
|
return existing;
|
|
2509
|
-
|
|
2498
|
+
// Version bumps are only meaningful for live edits — serve-time graph
|
|
2499
|
+
// warm-ups and the initial bulk walk should leave graphVersion stable.
|
|
2500
|
+
const bumpVersion = shouldBumpGraphVersion(classification, options?.bumpVersion !== false);
|
|
2501
|
+
if (bumpVersion) {
|
|
2502
|
+
graphVersion++;
|
|
2503
|
+
}
|
|
2510
2504
|
const gm = { id, deps: normDeps, hash };
|
|
2511
2505
|
graph.set(id, gm);
|
|
2512
2506
|
if (verbose) {
|
|
2513
2507
|
try {
|
|
2514
|
-
console.log('[hmr-ws][graph] upsert', { id, deps: normDeps, hash, graphVersion, classification });
|
|
2508
|
+
console.log('[hmr-ws][graph] upsert', { id, deps: normDeps, hash, graphVersion, classification, bumpVersion });
|
|
2515
2509
|
console.log('[hmr-ws][graph] size', graph.size);
|
|
2516
2510
|
}
|
|
2517
2511
|
catch { }
|
|
@@ -2539,6 +2533,8 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2539
2533
|
async function populateInitialGraph(server) {
|
|
2540
2534
|
if (graph.size)
|
|
2541
2535
|
return; // already populated
|
|
2536
|
+
const tStart = Date.now();
|
|
2537
|
+
const versionAtStart = graphVersion;
|
|
2542
2538
|
const root = server.config.root || process.cwd();
|
|
2543
2539
|
// Avoid direct require in ESM build: lazily obtain fs & path via createRequire or dynamic import
|
|
2544
2540
|
let fs;
|
|
@@ -2554,6 +2550,18 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2554
2550
|
fs = await import('fs');
|
|
2555
2551
|
pathMod = await import('path');
|
|
2556
2552
|
}
|
|
2553
|
+
// Route every bulk transform through `sharedTransformRequest` when it's
|
|
2554
|
+
// already been wired up — this way the background walk shares the 60s
|
|
2555
|
+
// TTL cache with live /ns/m requests, so the device sees cached results
|
|
2556
|
+
// for any file the walker already visited. The fallback keeps the
|
|
2557
|
+
// walker working during server tests where the shared runner isn't
|
|
2558
|
+
// constructed yet.
|
|
2559
|
+
const bulkTransform = (rel) => {
|
|
2560
|
+
if (sharedTransformRequest) {
|
|
2561
|
+
return sharedTransformRequest(rel);
|
|
2562
|
+
}
|
|
2563
|
+
return server.transformRequest(rel);
|
|
2564
|
+
};
|
|
2557
2565
|
async function walk(dir) {
|
|
2558
2566
|
for (const name of fs.readdirSync(dir)) {
|
|
2559
2567
|
if (name === 'node_modules' || name.startsWith('.') || shouldSkipRuntimeGraphDirectoryName(name))
|
|
@@ -2568,7 +2576,7 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2568
2576
|
const rel = '/' + pathMod.relative(root, full).split(pathMod.sep).join('/');
|
|
2569
2577
|
// Transform via Vite to gather deps (ignore failures)
|
|
2570
2578
|
try {
|
|
2571
|
-
const transformed = await
|
|
2579
|
+
const transformed = await bulkTransform(rel);
|
|
2572
2580
|
const code = transformed?.code || '';
|
|
2573
2581
|
const deps = [];
|
|
2574
2582
|
// fallback to import relationships via moduleGraph
|
|
@@ -2579,7 +2587,10 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2579
2587
|
deps.push(m.id.split('?')[0]);
|
|
2580
2588
|
}
|
|
2581
2589
|
}
|
|
2582
|
-
|
|
2590
|
+
// bumpVersion: false — the initial walk is a bulk load, not a live
|
|
2591
|
+
// edit. Keeping graphVersion stable during cold boot avoids double
|
|
2592
|
+
// cache-key drift.
|
|
2593
|
+
upsertGraphModule(rel, code, deps, { bumpVersion: false });
|
|
2583
2594
|
}
|
|
2584
2595
|
catch { }
|
|
2585
2596
|
}
|
|
@@ -2592,6 +2603,40 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2592
2603
|
await walk(pathMod.join(root, 'src'));
|
|
2593
2604
|
}
|
|
2594
2605
|
catch { }
|
|
2606
|
+
// Diagnostic summary. Gated behind the verbose flag so the
|
|
2607
|
+
// dev console stays quiet on a normal save. Flip
|
|
2608
|
+
// NS_VITE_VERBOSE=1 to surface slow cold-boot walks; a
|
|
2609
|
+
// `bumpedVersion=no` result is the happy path, `yes`
|
|
2610
|
+
// indicates a regression.
|
|
2611
|
+
if (verbose) {
|
|
2612
|
+
try {
|
|
2613
|
+
console.info(formatPopulateInitialGraphSummary({
|
|
2614
|
+
moduleCount: graph.size,
|
|
2615
|
+
durationMs: Date.now() - tStart,
|
|
2616
|
+
graphVersion,
|
|
2617
|
+
bumpedVersion: graphVersion !== versionAtStart,
|
|
2618
|
+
}));
|
|
2619
|
+
}
|
|
2620
|
+
catch { }
|
|
2621
|
+
}
|
|
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. We cap at 8 by default to match typical dev machines and
|
|
2673
|
+
// respect Vite's internal worker pool limits. Override via the
|
|
2674
|
+
// `NS_VITE_HMR_TRANSFORM_CONCURRENCY` 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
|
|
2678
|
+
// that unchanged neighbours of an edited file don't re-run
|
|
2679
|
+
// through the Angular/TypeScript/Vite transform pipeline. The
|
|
2680
|
+
// HMR flow explicitly invalidates affected URLs, so a longer TTL
|
|
2681
|
+
// is safe. Override with `NS_VITE_HMR_TRANSFORM_CACHE_MS`.
|
|
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,124 @@ 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
|
|
2695
|
+
// so anyone investigating perf can immediately see which build
|
|
2696
|
+
// is live and what knobs are active.
|
|
2697
|
+
try {
|
|
2698
|
+
let pkgVersion = 'unknown';
|
|
2699
|
+
try {
|
|
2700
|
+
const req = createRequire(import.meta.url);
|
|
2701
|
+
const pkg = req('@nativescript/vite/package.json');
|
|
2702
|
+
if (pkg && typeof pkg.version === 'string')
|
|
2703
|
+
pkgVersion = pkg.version;
|
|
2704
|
+
}
|
|
2705
|
+
catch {
|
|
2706
|
+
// `@nativescript/vite/package.json` is not always exported; fall
|
|
2707
|
+
// back to reading the file from disk next to this module.
|
|
2708
|
+
try {
|
|
2709
|
+
const here = new URL(import.meta.url).pathname;
|
|
2710
|
+
const pkgPath = path.resolve(path.dirname(here), '..', '..', 'package.json');
|
|
2711
|
+
if (existsSync(pkgPath)) {
|
|
2712
|
+
const parsed = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
2713
|
+
if (parsed && typeof parsed.version === 'string')
|
|
2714
|
+
pkgVersion = parsed.version;
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
catch { }
|
|
2718
|
+
}
|
|
2719
|
+
if (verbose) {
|
|
2720
|
+
console.info(formatServerStartupBanner({
|
|
2721
|
+
version: pkgVersion,
|
|
2722
|
+
transformConcurrency,
|
|
2723
|
+
transformCacheMs,
|
|
2724
|
+
lazyInitialGraph: true,
|
|
2725
|
+
graphVersion,
|
|
2726
|
+
}));
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
catch { }
|
|
2730
|
+
// Always-on cold-boot request trace. Runs in front of every
|
|
2731
|
+
// other middleware so it catches all NS dev routes (/ns/m/*,
|
|
2732
|
+
// /ns/rt/*, /ns/core/*, /__ns_boot__/*, etc.) with a single
|
|
2733
|
+
// hook. Closes itself after an idle window so HMR edits don't
|
|
2734
|
+
// get rolled into the cold-boot numbers. The idle window is
|
|
2735
|
+
// generous by default (5s) because V8's HTTP ESM resolver
|
|
2736
|
+
// pauses between dep levels while parsing — a too-tight window
|
|
2737
|
+
// was closing after the first wave and under-reporting boot by
|
|
2738
|
+
// 100x. Override via `NS_VITE_HMR_BOOT_TRACE_IDLE_MS` when
|
|
2739
|
+
// profiling something tricky.
|
|
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
|
+
// Gated on the verbose flag so cold-boot progress and
|
|
2750
|
+
// the final window-closed summary stay quiet by
|
|
2751
|
+
// default. Flip NS_VITE_VERBOSE=1 to surface them.
|
|
2752
|
+
log: (line) => {
|
|
2753
|
+
if (!verbose)
|
|
2754
|
+
return;
|
|
2755
|
+
try {
|
|
2756
|
+
console.info(line);
|
|
2757
|
+
}
|
|
2758
|
+
catch { }
|
|
2759
|
+
},
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
catch { }
|
|
2764
|
+
server.middlewares.use((req, res, next) => {
|
|
2765
|
+
try {
|
|
2766
|
+
const urlObj = new URL(req.url || '', 'http://localhost');
|
|
2767
|
+
const route = classifyBootRoute(urlObj.pathname);
|
|
2768
|
+
if (route === 'other')
|
|
2769
|
+
return next();
|
|
2770
|
+
if (!coldBootCounter)
|
|
2771
|
+
return next();
|
|
2772
|
+
const handle = coldBootCounter.record(urlObj.pathname);
|
|
2773
|
+
const finishOnce = () => {
|
|
2774
|
+
try {
|
|
2775
|
+
handle.finish();
|
|
2776
|
+
}
|
|
2777
|
+
catch { }
|
|
2778
|
+
};
|
|
2779
|
+
try {
|
|
2780
|
+
res.once('finish', finishOnce);
|
|
2781
|
+
res.once('close', finishOnce);
|
|
2782
|
+
}
|
|
2783
|
+
catch { }
|
|
2784
|
+
}
|
|
2785
|
+
catch { }
|
|
2786
|
+
next();
|
|
2787
|
+
});
|
|
2788
|
+
// Give `populateInitialGraph` a head start. Previously this only
|
|
2789
|
+
// kicked off on the first /ns/m hit, which meant populate was
|
|
2790
|
+
// competing with the device for the same 8 transform slots
|
|
2791
|
+
// throughout the first 4-5 seconds of cold boot. Starting at
|
|
2792
|
+
// `configureServer` time gives populate the full app
|
|
2793
|
+
// build/launch window (typically 2-3s on simulator) as a head
|
|
2794
|
+
// start, so more of its work lands before the device even
|
|
2795
|
+
// connects. Disable via `NS_VITE_HMR_DISABLE_POPULATE=1` when
|
|
2796
|
+
// profiling whether populate is helping or hurting a specific
|
|
2797
|
+
// app.
|
|
2798
|
+
try {
|
|
2799
|
+
const disablePopulate = process.env.NS_VITE_HMR_DISABLE_POPULATE === '1' || process.env.NS_VITE_HMR_DISABLE_POPULATE === 'true';
|
|
2800
|
+
if (disablePopulate) {
|
|
2801
|
+
if (verbose)
|
|
2802
|
+
console.info('[hmr-ws][populate] disabled via NS_VITE_HMR_DISABLE_POPULATE');
|
|
2803
|
+
// Short-circuit: mark as resolved so /ns/m never schedules it and
|
|
2804
|
+
// HMR still works (handleHotUpdate just has no pre-warmed graph).
|
|
2805
|
+
graphInitialPopulationPromise = Promise.resolve();
|
|
2806
|
+
}
|
|
2807
|
+
else {
|
|
2808
|
+
ensureInitialGraphPopulationStarted(server);
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
catch { }
|
|
2639
2812
|
// Attempt early vendor manifest bootstrap once per server.
|
|
2640
2813
|
if (!vendorBootstrapDone) {
|
|
2641
2814
|
vendorBootstrapDone = true;
|
|
@@ -2910,25 +3083,23 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
2910
3083
|
const urlObj = new URL(req.url || '', 'http://localhost');
|
|
2911
3084
|
if (!urlObj.pathname.startsWith('/ns/m'))
|
|
2912
3085
|
return next();
|
|
2913
|
-
//
|
|
2914
|
-
// non-zero
|
|
2915
|
-
//
|
|
2916
|
-
//
|
|
2917
|
-
//
|
|
2918
|
-
// and
|
|
2919
|
-
//
|
|
2920
|
-
//
|
|
2921
|
-
//
|
|
2922
|
-
//
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
}
|
|
2931
|
-
}
|
|
3086
|
+
// Previously we awaited `populateInitialGraph(server)` here so
|
|
3087
|
+
// graphVersion would be non-zero for the first /ns/m request.
|
|
3088
|
+
// That gave deterministic URL tags but blocked the cold boot on a
|
|
3089
|
+
// full src/ tree walk (hundreds of transformRequest calls, 3-6s).
|
|
3090
|
+
//
|
|
3091
|
+
// graphVersion now starts at 1 and stays stable during cold boot
|
|
3092
|
+
// (see `upsertGraphModule`'s bumpVersion option and the inline
|
|
3093
|
+
// comment at the graphVersion declaration). We kick off the
|
|
3094
|
+
// initial population in the background so it doesn't block the
|
|
3095
|
+
// first response. `handleHotUpdate` awaits the same promise so
|
|
3096
|
+
// the first HMR event still sees a fully populated graph.
|
|
3097
|
+
ensureInitialGraphPopulationStarted(server);
|
|
3098
|
+
// Cold-boot counter is now hooked via the leading boot-trace
|
|
3099
|
+
// middleware (see `configureServer` — it records the request
|
|
3100
|
+
// and tracks finish() via res.on('close'/'finish')). This
|
|
3101
|
+
// handler used to record here but that missed the
|
|
3102
|
+
// round-trip timing and didn't track per-route breakdowns.
|
|
2932
3103
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
2933
3104
|
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
2934
3105
|
// Disable caching for dev ESM endpoints to avoid device-side stale module reuse
|
|
@@ -3140,7 +3311,9 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
3140
3311
|
if (transformed?.code && ACTIVE_STRATEGY?.flavor === 'solid' && (resolvedCandidate || spec || '').includes('@solid-refresh')) {
|
|
3141
3312
|
const PATCH_SENTINEL = '/* __ns_solid_refresh_patched__ */';
|
|
3142
3313
|
const alreadyPatched = transformed.code.includes(PATCH_SENTINEL);
|
|
3143
|
-
|
|
3314
|
+
if (verbose) {
|
|
3315
|
+
console.log('[hmr-ws][solid] @solid-refresh patch check:', { spec: resolvedCandidate || spec, alreadyPatched, codeLen: transformed.code.length });
|
|
3316
|
+
}
|
|
3144
3317
|
if (!alreadyPatched) {
|
|
3145
3318
|
let patchedCode = transformed.code;
|
|
3146
3319
|
// Patch 1: Bypass shouldWarnAndDecline() — the vendor-bundled solid-js
|
|
@@ -3149,7 +3322,9 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
3149
3322
|
const declineCheck = 'function shouldWarnAndDecline() {';
|
|
3150
3323
|
if (patchedCode.includes(declineCheck)) {
|
|
3151
3324
|
patchedCode = patchedCode.replace(declineCheck, `${PATCH_SENTINEL}\nfunction shouldWarnAndDecline() { return false; /* NS HMR: always allow refresh */ }\nfunction __original_shouldWarnAndDecline() {`);
|
|
3152
|
-
|
|
3325
|
+
if (verbose) {
|
|
3326
|
+
console.log('[hmr-ws][solid] bypassed shouldWarnAndDecline() for NativeScript HMR');
|
|
3327
|
+
}
|
|
3153
3328
|
}
|
|
3154
3329
|
// Patch 2: Force createMemo path in createProxy.
|
|
3155
3330
|
// Without the 'development' condition, $DEVCOMP is not set on components,
|
|
@@ -3160,10 +3335,17 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
3160
3335
|
const proxyCondition = 'if (!s || $DEVCOMP in s) {';
|
|
3161
3336
|
if (patchedCode.includes(proxyCondition)) {
|
|
3162
3337
|
patchedCode = patchedCode.replace(proxyCondition, 'if (true) { /* NS HMR: always use createMemo for reactive HMR updates */');
|
|
3163
|
-
|
|
3338
|
+
if (verbose) {
|
|
3339
|
+
console.log('[hmr-ws][solid] forced createMemo path in createProxy for NativeScript HMR');
|
|
3340
|
+
}
|
|
3164
3341
|
}
|
|
3165
3342
|
// Patch 3: Inline patchRegistry call so updates apply immediately
|
|
3166
3343
|
// on module re-evaluation (accept callbacks are not invoked by the HMR client).
|
|
3344
|
+
// The injected `console.log` helpers run inside the user's runtime
|
|
3345
|
+
// when @solid-refresh re-evaluates a module, so they are a runtime
|
|
3346
|
+
// concern (stripped if the user disables the patch). Keeping them
|
|
3347
|
+
// behind the patch sentinel rather than the dev-server `verbose`
|
|
3348
|
+
// flag is intentional — the patch only runs when Solid HMR fires.
|
|
3167
3349
|
const marker = 'hot.data[SOLID_REFRESH] = hot.data[SOLID_REFRESH] || registry;';
|
|
3168
3350
|
if (patchedCode.includes(marker)) {
|
|
3169
3351
|
const patchCode = [
|
|
@@ -3177,7 +3359,9 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
3177
3359
|
`}`,
|
|
3178
3360
|
].join('\n ');
|
|
3179
3361
|
patchedCode = patchedCode.replace(marker, `${patchCode}\n ${marker}`);
|
|
3180
|
-
|
|
3362
|
+
if (verbose) {
|
|
3363
|
+
console.log('[hmr-ws][solid] added inline patchRegistry for NativeScript HMR');
|
|
3364
|
+
}
|
|
3181
3365
|
}
|
|
3182
3366
|
// Work on a copy to avoid mutating Vite's cached TransformResult
|
|
3183
3367
|
transformed = { ...transformed, code: patchedCode };
|
|
@@ -3207,7 +3391,9 @@ function createHmrWebSocketPlugin(opts) {
|
|
|
3207
3391
|
}
|
|
3208
3392
|
catch { }
|
|
3209
3393
|
}
|
|
3210
|
-
|
|
3394
|
+
// Serve-time warm-up: no live edit happened, so don't bump
|
|
3395
|
+
// graphVersion.
|
|
3396
|
+
upsertGraphModule(id, code, deps, { bumpVersion: false });
|
|
3211
3397
|
}
|
|
3212
3398
|
}
|
|
3213
3399
|
}
|
|
@@ -3357,7 +3543,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3357
3543
|
// misses re-exported names). By expanding to `export { a, b } from "url"`,
|
|
3358
3544
|
// the engine sees explicit named exports and resolves them correctly.
|
|
3359
3545
|
try {
|
|
3360
|
-
code = await expandStarExports(code, server, server.config?.root || process.cwd(), verbose);
|
|
3546
|
+
code = await expandStarExports(code, server, server.config?.root || process.cwd(), verbose, sharedTransformRequest);
|
|
3361
3547
|
}
|
|
3362
3548
|
catch (e) {
|
|
3363
3549
|
if (verbose)
|
|
@@ -3430,45 +3616,63 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3430
3616
|
}
|
|
3431
3617
|
}
|
|
3432
3618
|
catch { }
|
|
3619
|
+
// `/ns/rt` and `/ns/core` URL versioning.
|
|
3620
|
+
//
|
|
3621
|
+
// Older versions of the server emitted `/ns/rt/<ver>` and
|
|
3622
|
+
// `/ns/core/<ver>` so V8's HTTP module cache would see a
|
|
3623
|
+
// fresh URL on every save. The runtime canonicalizer
|
|
3624
|
+
// (`CanonicalizeHttpUrlKey` in HMRSupport.mm) collapses
|
|
3625
|
+
// these version segments to the bare `/ns/rt` and
|
|
3626
|
+
// `/ns/core` keys before lookup, so V8 actually saw a
|
|
3627
|
+
// single cache entry — but the server was doing extra
|
|
3628
|
+
// work to inject a version segment that the runtime then
|
|
3629
|
+
// immediately stripped. Now that the runtime supports
|
|
3630
|
+
// explicit eviction (and these bridge endpoints don't
|
|
3631
|
+
// change at HMR time anyway), the version segment is
|
|
3632
|
+
// purely vestigial.
|
|
3633
|
+
//
|
|
3634
|
+
// Rather than rip the helpers out (which would touch
|
|
3635
|
+
// every ensureVersionedImports caller and risk bumping
|
|
3636
|
+
// older runtimes), we keep them but pass `verNum=0`. The
|
|
3637
|
+
// helpers still normalize URL shape (strip the absolute
|
|
3638
|
+
// origin prefix when present) but emit a stable
|
|
3639
|
+
// `/ns/rt/0` / `/ns/core/0` URL — which collapses to
|
|
3640
|
+
// `/ns/rt` / `/ns/core` in the runtime.
|
|
3433
3641
|
try {
|
|
3434
|
-
const verNum =
|
|
3642
|
+
const verNum = 0;
|
|
3435
3643
|
code = ensureVersionedRtImports(code, getServerOrigin(server), verNum);
|
|
3436
3644
|
code = ACTIVE_STRATEGY.ensureVersionedImports(code, getServerOrigin(server), verNum);
|
|
3437
3645
|
code = ensureVersionedCoreImports(code, getServerOrigin(server), verNum);
|
|
3438
3646
|
}
|
|
3439
3647
|
catch { }
|
|
3440
|
-
//
|
|
3441
|
-
//
|
|
3442
|
-
//
|
|
3648
|
+
// `/ns/m` URL finalize step.
|
|
3649
|
+
//
|
|
3650
|
+
// `rewriteNsMImportPathForHmr` is a canonicalizer: it
|
|
3651
|
+
// strips legacy `__ns_hmr__/<tag>/` segments and adds
|
|
3652
|
+
// `__ns_boot__/b1/` only for boot-tagged requests. The
|
|
3653
|
+
// `ver` parameter is preserved on the signature for API
|
|
3654
|
+
// compatibility but is ignored for app modules (cache
|
|
3655
|
+
// busting is driven by `__nsInvalidateModules`, not URL
|
|
3656
|
+
// versioning). We pass `'v0'` as a stable placeholder —
|
|
3657
|
+
// the canonicalizer emits the same URL regardless of
|
|
3658
|
+
// this value, but a constant placeholder makes the
|
|
3659
|
+
// contract explicit.
|
|
3660
|
+
//
|
|
3661
|
+
// SFC URLs (line below, `/ns/sfc/${verTag}/...`) still
|
|
3662
|
+
// embed a version because the Vue SFC pathway does not
|
|
3663
|
+
// yet have an eviction protocol. The runtime
|
|
3664
|
+
// canonicalizer does NOT strip `/ns/sfc/<ver>/`, so Vue
|
|
3665
|
+
// users still see per-save SFC re-fetches — that's a
|
|
3666
|
+
// known follow-up.
|
|
3443
3667
|
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)}`;
|
|
3668
|
+
const verTag = (() => {
|
|
3669
|
+
const numeric = getNumericServeVersionTag(forcedVer, Number(graphVersion || 0));
|
|
3670
|
+
return numeric > 0 ? `v${numeric}` : 'v0';
|
|
3469
3671
|
})();
|
|
3470
3672
|
const origin = getServerOrigin(server);
|
|
3471
|
-
const rewritePath = (p) => rewriteNsMImportPathForHmr(p,
|
|
3673
|
+
const rewritePath = (p) => rewriteNsMImportPathForHmr(p, 'v0', bootTaggedRequest);
|
|
3674
|
+
// /ns/m URL forms — all collapse to canonical stable
|
|
3675
|
+
// URLs via the Phase 3a rewriter.
|
|
3472
3676
|
// 1) Static imports: import ... from "/ns/m/..."
|
|
3473
3677
|
code = code.replace(/(from\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
|
|
3474
3678
|
// 2) Side-effect imports: import "/ns/m/..."
|
|
@@ -3479,14 +3683,14 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3479
3683
|
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
3684
|
// 5) __ns_import(new URL('/ns/m/...', import.meta.url).href)
|
|
3481
3685
|
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 →
|
|
3686
|
+
// 6) Force absolute HTTP for new URL('/ns/m/...', import.meta.url).href → canonical stable URL.
|
|
3483
3687
|
try {
|
|
3484
3688
|
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
3689
|
}
|
|
3486
3690
|
catch { }
|
|
3487
|
-
// 7)
|
|
3691
|
+
// 7) SFC URLs (Vue) — still versioned. See header comment.
|
|
3488
3692
|
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/${
|
|
3693
|
+
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
3694
|
}
|
|
3491
3695
|
catch { }
|
|
3492
3696
|
}
|
|
@@ -3603,10 +3807,12 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3603
3807
|
// by the serving pipeline. Only warn, don't fatally block the importer.
|
|
3604
3808
|
const hasCjsPattern = /\bmodule\s*\.\s*exports\b/.test(targetCode) || /\bexports\s*\.\s*\w/.test(targetCode);
|
|
3605
3809
|
if (hasCjsPattern) {
|
|
3606
|
-
|
|
3607
|
-
|
|
3810
|
+
if (verbose) {
|
|
3811
|
+
try {
|
|
3812
|
+
console.warn(`[ns:m][link-check] CJS module without export default: ${u.pathname} (will be CJS-wrapped at serve time)`);
|
|
3813
|
+
}
|
|
3814
|
+
catch { }
|
|
3608
3815
|
}
|
|
3609
|
-
catch { }
|
|
3610
3816
|
continue;
|
|
3611
3817
|
}
|
|
3612
3818
|
const msg = `[link-check] Missing default export in ${u.pathname}${u.search} (imported by ${resolvedCandidate || spec})`;
|
|
@@ -3620,10 +3826,12 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3620
3826
|
}
|
|
3621
3827
|
}
|
|
3622
3828
|
catch (eLC) {
|
|
3623
|
-
|
|
3624
|
-
|
|
3829
|
+
if (verbose) {
|
|
3830
|
+
try {
|
|
3831
|
+
console.warn('[ns:m][link-check] failed', eLC?.message || eLC);
|
|
3832
|
+
}
|
|
3833
|
+
catch { }
|
|
3625
3834
|
}
|
|
3626
|
-
catch { }
|
|
3627
3835
|
}
|
|
3628
3836
|
res.statusCode = 200;
|
|
3629
3837
|
res.end(code);
|
|
@@ -3665,8 +3873,10 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3665
3873
|
`let __cached_rt = null;\n` +
|
|
3666
3874
|
`let __cached_vm = null;\n` +
|
|
3667
3875
|
`const __RT_REALM_TAG = (globalThis.__NS_RT_REALM__ ||= Math.random().toString(36).slice(2));\n` +
|
|
3668
|
-
//
|
|
3669
|
-
|
|
3876
|
+
// One-shot evaluation marker to confirm the bridge is executed on
|
|
3877
|
+
// device. Gated on __NS_ENV_VERBOSE__ so it stays silent unless
|
|
3878
|
+
// the developer opts in via NS_VITE_VERBOSE / VITE_DEBUG_LOGS.
|
|
3879
|
+
`try { if (!(globalThis.__NS_RT_ONCE__ && globalThis.__NS_RT_ONCE__.eval)) { (globalThis.__NS_RT_ONCE__ ||= {}).eval = true; if (globalThis.__NS_ENV_VERBOSE__) console.log('[ns-rt] evaluated', { rtRealm: __RT_REALM_TAG }); } } catch {}\n` +
|
|
3670
3880
|
`function __ensure(){\n` +
|
|
3671
3881
|
` if (__cached_rt) return __cached_rt;\n` +
|
|
3672
3882
|
` let vm = null;\n` +
|
|
@@ -3846,15 +4056,15 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3846
4056
|
});
|
|
3847
4057
|
// 2.6) ESM bridge for @nativescript/core: GET /ns/core[/<ver>][?p=sub/path]
|
|
3848
4058
|
//
|
|
3849
|
-
// Since bundle.mjs no longer bundles @nativescript/core (
|
|
3850
|
-
//
|
|
3851
|
-
//
|
|
3852
|
-
//
|
|
3853
|
-
//
|
|
3854
|
-
//
|
|
3855
|
-
//
|
|
3856
|
-
//
|
|
3857
|
-
//
|
|
4059
|
+
// Since bundle.mjs no longer bundles @nativescript/core (it is
|
|
4060
|
+
// declared external in the rolldown config under HMR), this
|
|
4061
|
+
// endpoint is the ONE place core is evaluated. Every consumer —
|
|
4062
|
+
// bundle.mjs's own `@nativescript/core*` imports (resolved to
|
|
4063
|
+
// full HTTP URLs in the entry virtual module), externalized
|
|
4064
|
+
// vendor packages, HTTP-served app modules — all end up here.
|
|
4065
|
+
// No more proxy bridge, no enumeration, no namespace detection,
|
|
4066
|
+
// no prototype-polluted maps. We just serve Vite's authoritative
|
|
4067
|
+
// transformed module content.
|
|
3858
4068
|
//
|
|
3859
4069
|
// iOS caches by URL path, so each unique URL is evaluated exactly
|
|
3860
4070
|
// once per app lifetime. Every class identity is shared, every
|
|
@@ -3961,16 +4171,15 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3961
4171
|
`const __TEST__ = false;`,
|
|
3962
4172
|
].join('\n');
|
|
3963
4173
|
// Boot-time instrumentation + module self-registration.
|
|
3964
|
-
//
|
|
3965
|
-
// -
|
|
3966
|
-
//
|
|
3967
|
-
//
|
|
3968
|
-
//
|
|
3969
|
-
//
|
|
3970
|
-
//
|
|
3971
|
-
// on mismatch so drift in any emitter surfaces
|
|
4174
|
+
//
|
|
4175
|
+
// - URL canonicalization: the same logical module must
|
|
4176
|
+
// always resolve to byte-identical URLs across every
|
|
4177
|
+
// emitter. The /ns/core handler records the first URL
|
|
4178
|
+
// seen for each canonical sub (or '' for main) in
|
|
4179
|
+
// `globalThis.__NS_CORE_FIRST_URL__` and fails hard on
|
|
4180
|
+
// mismatch so drift in any emitter surfaces
|
|
3972
4181
|
// immediately, before the realm splits.
|
|
3973
|
-
// -
|
|
4182
|
+
// - CJS/ESM boot order: CommonJS
|
|
3974
4183
|
// `require('@nativescript/core/...')` calls from
|
|
3975
4184
|
// vendor install() hooks must resolve to the SAME
|
|
3976
4185
|
// ESM namespace that ran this side-effect preamble.
|
|
@@ -3999,13 +4208,13 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
3999
4208
|
` const __nsUrl = ${JSON.stringify(canonicalUrl)};`,
|
|
4000
4209
|
` __nsSeen.push(__nsUrl);`,
|
|
4001
4210
|
` if (typeof __nsFirst[__nsKey] === 'string' && __nsFirst[__nsKey] !== __nsUrl) {`,
|
|
4002
|
-
` throw new Error('[ns-core] URL drift for sub=' + __nsKey + ': first=' + __nsFirst[__nsKey] + ' now=' + __nsUrl
|
|
4211
|
+
` throw new Error('[ns-core] URL drift for sub=' + __nsKey + ': first=' + __nsFirst[__nsKey] + ' now=' + __nsUrl);`,
|
|
4003
4212
|
` }`,
|
|
4004
4213
|
` if (!__nsFirst[__nsKey]) __nsFirst[__nsKey] = __nsUrl;`,
|
|
4005
4214
|
` globalThis.__NS_CORE_EVAL_COUNT__ = (globalThis.__NS_CORE_EVAL_COUNT__ || 0) + 1;`,
|
|
4006
4215
|
`} } catch (e) { try { console.warn('[ns-core] instrumentation failed:', (e && e.message) || e); } catch {} }`,
|
|
4007
4216
|
].join('\n');
|
|
4008
|
-
//
|
|
4217
|
+
// CJS/ESM interop shape — REGISTRATION side.
|
|
4009
4218
|
//
|
|
4010
4219
|
// The actual shape installer runs earlier in the module
|
|
4011
4220
|
// body (between preamble and selfImport; see
|
|
@@ -4021,9 +4230,6 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
4021
4230
|
// registration — the shape function is identity-preserving
|
|
4022
4231
|
// via WeakMap — gives a stable, shared, CJS-compatible
|
|
4023
4232
|
// view without copying on every require.
|
|
4024
|
-
//
|
|
4025
|
-
// See HMR_CORE_REALM_DETERMINISTIC_PLAN.md § "Invariant D"
|
|
4026
|
-
// for the full rationale.
|
|
4027
4233
|
const registrationFooter = [
|
|
4028
4234
|
`try { if (typeof globalThis !== 'undefined') {`,
|
|
4029
4235
|
` const __nsReg = globalThis.__NS_CORE_MODULES__ || (globalThis.__NS_CORE_MODULES__ = Object.create(null));`,
|
|
@@ -4135,7 +4341,8 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
4135
4341
|
content = 'export default async function start(){ console.error("[/ns/entry-rt] not found"); }\n';
|
|
4136
4342
|
}
|
|
4137
4343
|
}
|
|
4138
|
-
|
|
4344
|
+
if (verbose)
|
|
4345
|
+
console.log('[hmr-http] /ns/entry-rt serving', content.length, 'bytes');
|
|
4139
4346
|
res.statusCode = 200;
|
|
4140
4347
|
res.end(content);
|
|
4141
4348
|
}
|
|
@@ -5868,6 +6075,101 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
5868
6075
|
if (isRuntimeGraphExcludedPath(file)) {
|
|
5869
6076
|
return;
|
|
5870
6077
|
}
|
|
6078
|
+
// Always-on update timing. Captures the four phases (await,
|
|
6079
|
+
// framework, broadcast, total) plus invalidated module count
|
|
6080
|
+
// and recipient count. Emitted at the end of this function via
|
|
6081
|
+
// `emitHmrUpdateSummary()`. Single line, always-on so a
|
|
6082
|
+
// 6-second `.ts` save is immediately visible without flipping
|
|
6083
|
+
// verbose.
|
|
6084
|
+
const updateRoot = server.config.root || process.cwd();
|
|
6085
|
+
const updateRel = (() => {
|
|
6086
|
+
try {
|
|
6087
|
+
return '/' + path.posix.normalize(path.relative(updateRoot, file)).split(path.sep).join('/');
|
|
6088
|
+
}
|
|
6089
|
+
catch {
|
|
6090
|
+
return file;
|
|
6091
|
+
}
|
|
6092
|
+
})();
|
|
6093
|
+
const updateMetrics = {
|
|
6094
|
+
file: updateRel,
|
|
6095
|
+
kind: classifyHmrUpdateKind(file),
|
|
6096
|
+
t0: Date.now(),
|
|
6097
|
+
tAfterAwait: 0,
|
|
6098
|
+
tAfterFramework: 0,
|
|
6099
|
+
tEnd: 0,
|
|
6100
|
+
invalidated: 0,
|
|
6101
|
+
recipients: 0,
|
|
6102
|
+
// Narrowing diagnostic — populated by the angular branch when
|
|
6103
|
+
// the changed file is `.ts`, otherwise remains undefined and is
|
|
6104
|
+
// omitted from the summary line entirely.
|
|
6105
|
+
narrowed: undefined,
|
|
6106
|
+
emitted: false,
|
|
6107
|
+
};
|
|
6108
|
+
// Broadcast a "pending" notification at the very start of
|
|
6109
|
+
// handleHotUpdate so the client can show the HMR-applying
|
|
6110
|
+
// overlay BEFORE we spend time on graph updates / transforms /
|
|
6111
|
+
// dependency analysis (typically 7–200ms on a warm cache).
|
|
6112
|
+
// Without this, the overlay only appears at `ns:angular-update`
|
|
6113
|
+
// broadcast time and the user perceives a "delayed" reaction
|
|
6114
|
+
// to their save.
|
|
6115
|
+
//
|
|
6116
|
+
// Fire-and-forget: a failed pending broadcast must never
|
|
6117
|
+
// hold up the actual update. The client treats receipt of
|
|
6118
|
+
// `ns:angular-update` (or `ns:css-updates`) as authoritative;
|
|
6119
|
+
// the pending message is purely a UX hint.
|
|
6120
|
+
try {
|
|
6121
|
+
const pendingPayload = JSON.stringify(createHmrPendingMessage({
|
|
6122
|
+
origin: getServerOrigin(server),
|
|
6123
|
+
path: updateMetrics.file,
|
|
6124
|
+
kind: updateMetrics.kind,
|
|
6125
|
+
timestamp: updateMetrics.t0,
|
|
6126
|
+
}));
|
|
6127
|
+
wss.clients.forEach((client) => {
|
|
6128
|
+
if (isSocketClientOpen(client)) {
|
|
6129
|
+
try {
|
|
6130
|
+
client.send(pendingPayload);
|
|
6131
|
+
}
|
|
6132
|
+
catch { }
|
|
6133
|
+
}
|
|
6134
|
+
});
|
|
6135
|
+
}
|
|
6136
|
+
catch { }
|
|
6137
|
+
const emitHmrUpdateSummary = () => {
|
|
6138
|
+
if (updateMetrics.emitted)
|
|
6139
|
+
return;
|
|
6140
|
+
updateMetrics.emitted = true;
|
|
6141
|
+
updateMetrics.tEnd = Date.now();
|
|
6142
|
+
try {
|
|
6143
|
+
const awaitMs = (updateMetrics.tAfterAwait || updateMetrics.t0) - updateMetrics.t0;
|
|
6144
|
+
const frameworkMs = (updateMetrics.tAfterFramework || updateMetrics.tAfterAwait || updateMetrics.t0) - (updateMetrics.tAfterAwait || updateMetrics.t0);
|
|
6145
|
+
const broadcastMs = updateMetrics.tEnd - (updateMetrics.tAfterFramework || updateMetrics.tAfterAwait || updateMetrics.t0);
|
|
6146
|
+
const totalMs = updateMetrics.tEnd - updateMetrics.t0;
|
|
6147
|
+
console.info(formatHmrUpdateSummary({
|
|
6148
|
+
file: updateMetrics.file,
|
|
6149
|
+
kind: updateMetrics.kind,
|
|
6150
|
+
awaitMs,
|
|
6151
|
+
frameworkMs,
|
|
6152
|
+
broadcastMs,
|
|
6153
|
+
totalMs,
|
|
6154
|
+
invalidated: updateMetrics.invalidated,
|
|
6155
|
+
recipients: updateMetrics.recipients,
|
|
6156
|
+
narrowed: updateMetrics.narrowed,
|
|
6157
|
+
}));
|
|
6158
|
+
}
|
|
6159
|
+
catch { }
|
|
6160
|
+
};
|
|
6161
|
+
// The first /ns/m request kicks off populateInitialGraph in the
|
|
6162
|
+
// background. If an HMR update races in before that walk
|
|
6163
|
+
// completes, we'd lose transitive-importer data. Await
|
|
6164
|
+
// completion here so the delta computation below always sees a
|
|
6165
|
+
// populated graph.
|
|
6166
|
+
if (graphInitialPopulationPromise) {
|
|
6167
|
+
try {
|
|
6168
|
+
await graphInitialPopulationPromise;
|
|
6169
|
+
}
|
|
6170
|
+
catch { }
|
|
6171
|
+
}
|
|
6172
|
+
updateMetrics.tAfterAwait = Date.now();
|
|
5871
6173
|
// Graph update for this file change (wrapped to avoid aborting rest of handler)
|
|
5872
6174
|
try {
|
|
5873
6175
|
const skipAngularHtmlGraphUpdate = ACTIVE_STRATEGY.flavor === 'angular' && /\.(html|htm)$/i.test(file);
|
|
@@ -5917,6 +6219,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
5917
6219
|
console.log(`[hmr-ws] Hot update for: ${file}`);
|
|
5918
6220
|
// Handle CSS updates
|
|
5919
6221
|
if (file.endsWith('.css')) {
|
|
6222
|
+
updateMetrics.tAfterFramework = Date.now();
|
|
5920
6223
|
try {
|
|
5921
6224
|
let rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
|
|
5922
6225
|
const origin = getServerOrigin(server);
|
|
@@ -5935,12 +6238,14 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
5935
6238
|
wss.clients.forEach((client) => {
|
|
5936
6239
|
if (isSocketClientOpen(client)) {
|
|
5937
6240
|
client.send(JSON.stringify(msg));
|
|
6241
|
+
updateMetrics.recipients += 1;
|
|
5938
6242
|
}
|
|
5939
6243
|
});
|
|
5940
6244
|
}
|
|
5941
6245
|
catch (error) {
|
|
5942
6246
|
console.warn('[hmr-ws] CSS update failed:', error);
|
|
5943
6247
|
}
|
|
6248
|
+
emitHmrUpdateSummary();
|
|
5944
6249
|
return;
|
|
5945
6250
|
}
|
|
5946
6251
|
// Framework-specific hot update handling
|
|
@@ -5956,6 +6261,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
5956
6261
|
});
|
|
5957
6262
|
if (!(isHtml || isTs))
|
|
5958
6263
|
return;
|
|
6264
|
+
updateMetrics.invalidated += angularHotUpdateRoots.length;
|
|
5959
6265
|
if (angularHotUpdateRoots.length) {
|
|
5960
6266
|
for (const mod of angularHotUpdateRoots) {
|
|
5961
6267
|
try {
|
|
@@ -5972,13 +6278,80 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
5972
6278
|
}
|
|
5973
6279
|
}
|
|
5974
6280
|
const angularTransitiveInvalidationRoots = (angularHotUpdateRoots.length ? angularHotUpdateRoots : ctx.modules);
|
|
5975
|
-
|
|
6281
|
+
// Read the source for `.ts/.tsx/.js/.jsx` edits so
|
|
6282
|
+
// `shouldInvalidateAngularTransitiveImporters` can
|
|
6283
|
+
// distinguish leaf modules (constants/utils) from real
|
|
6284
|
+
// Angular files. If `ctx.read()` throws (file deleted, race
|
|
6285
|
+
// against the watcher), `angularChangedSource` stays
|
|
6286
|
+
// undefined and we fall back to the conservative "always
|
|
6287
|
+
// invalidate transitively" behavior.
|
|
6288
|
+
let angularChangedSource;
|
|
6289
|
+
if (isTs) {
|
|
5976
6290
|
try {
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
6291
|
+
angularChangedSource = await ctx.read();
|
|
6292
|
+
}
|
|
6293
|
+
catch {
|
|
6294
|
+
angularChangedSource = undefined;
|
|
6295
|
+
}
|
|
6296
|
+
}
|
|
6297
|
+
const angularNeedsTransitive = shouldInvalidateAngularTransitiveImporters({
|
|
6298
|
+
flavor: ACTIVE_STRATEGY.flavor,
|
|
6299
|
+
file,
|
|
6300
|
+
source: angularChangedSource,
|
|
6301
|
+
});
|
|
6302
|
+
// Surface the narrowing decision on every `.ts` Angular hot
|
|
6303
|
+
// update (HTML routes always invalidate transitively and
|
|
6304
|
+
// aren't subject to narrowing, so we leave them as
|
|
6305
|
+
// `undefined` — the field is omitted from the summary line).
|
|
6306
|
+
// The boolean is the inverse of `angularNeedsTransitive`
|
|
6307
|
+
// because "needs transitive" is the broad (un-narrowed)
|
|
6308
|
+
// behavior.
|
|
6309
|
+
if (isTs) {
|
|
6310
|
+
updateMetrics.narrowed = !angularNeedsTransitive;
|
|
6311
|
+
}
|
|
6312
|
+
// Stable URL + Explicit Invalidation:
|
|
6313
|
+
//
|
|
6314
|
+
// Compute the transitive importer closure ONCE here and reuse
|
|
6315
|
+
// it for (a) `server.moduleGraph.invalidateModule` (so Vite's
|
|
6316
|
+
// transform pipeline re-runs on next request), (b) the shared
|
|
6317
|
+
// transform-request cache, and (c) the runtime eviction set
|
|
6318
|
+
// we broadcast in `ns:angular-update`. Consolidating this
|
|
6319
|
+
// removes a redundant graph walk and guarantees the three
|
|
6320
|
+
// consumers see the exact same set of importers (otherwise a
|
|
6321
|
+
// late module-graph mutation between calls could leave an
|
|
6322
|
+
// asymmetric narrowed/broad mix).
|
|
6323
|
+
//
|
|
6324
|
+
// We separate Vite-transform narrowing from runtime eviction:
|
|
6325
|
+
// `angularNeedsTransitive` answers the question "does the
|
|
6326
|
+
// changed file's symbol shape change such that importers
|
|
6327
|
+
// must be re-transformed by Vite?". The runtime, however,
|
|
6328
|
+
// has a stricter requirement: ESM live bindings only refresh
|
|
6329
|
+
// if the importing module re-evaluates inside V8. A
|
|
6330
|
+
// constants file with no Angular decorator does NOT need a
|
|
6331
|
+
// Vite re-transform of its importers (their compiled JS is
|
|
6332
|
+
// identical), but its importers still hold stale bindings to
|
|
6333
|
+
// the OLD constants Module record. After eviction + re-import
|
|
6334
|
+
// of `main.ts`, V8 sees the cached importers, returns them
|
|
6335
|
+
// unchanged, and they continue to read the OLD values. The
|
|
6336
|
+
// user-visible symptom: HMR completes successfully, logs are
|
|
6337
|
+
// clean, but the simulator does not reflect the change.
|
|
6338
|
+
//
|
|
6339
|
+
// The fix: ALWAYS compute the transitive importer closure
|
|
6340
|
+
// for runtime eviction. Only skip Vite's
|
|
6341
|
+
// `moduleGraph.invalidate` + transform-cache purge when
|
|
6342
|
+
// `angularNeedsTransitive` is false — those are the genuine
|
|
6343
|
+
// narrowing wins (saves re-transform work on the server).
|
|
6344
|
+
// The eviction set always includes importers so V8 re-fetches
|
|
6345
|
+
// and re-binds them.
|
|
6346
|
+
let transitiveImporters = [];
|
|
6347
|
+
try {
|
|
6348
|
+
transitiveImporters = collectAngularTransitiveImportersForInvalidation({
|
|
6349
|
+
modules: angularTransitiveInvalidationRoots,
|
|
6350
|
+
isExcluded: (id) => id.includes('/node_modules/'),
|
|
6351
|
+
maxDepth: 16,
|
|
6352
|
+
});
|
|
6353
|
+
if (angularNeedsTransitive) {
|
|
6354
|
+
updateMetrics.invalidated += transitiveImporters.length;
|
|
5982
6355
|
for (const mod of transitiveImporters) {
|
|
5983
6356
|
try {
|
|
5984
6357
|
server.moduleGraph.invalidateModule(mod);
|
|
@@ -5993,24 +6366,39 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
5993
6366
|
console.log('[hmr-ws][angular] invalidated transitive importers:', transitiveImporters.length);
|
|
5994
6367
|
}
|
|
5995
6368
|
}
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
6369
|
+
else if (isTs && typeof angularChangedSource === 'string') {
|
|
6370
|
+
// Surfacing this log unconditionally lets the user
|
|
6371
|
+
// immediately confirm whether narrowing fired for a
|
|
6372
|
+
// given `.ts` edit (the summary line below still
|
|
6373
|
+
// emits `narrowed=yes`/`no`, but having both makes
|
|
6374
|
+
// the decision easier to spot in noisy logs and lets
|
|
6375
|
+
// the user diff scenarios without flipping
|
|
6376
|
+
// `NS_HMR_VERBOSE=true`).
|
|
6377
|
+
//
|
|
6378
|
+
// Narrowing means "skip Vite re-transform" (the
|
|
6379
|
+
// importers still get evicted from the V8 module
|
|
6380
|
+
// registry so live bindings refresh). The importer
|
|
6381
|
+
// count is appended so the distinction is visible.
|
|
6382
|
+
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
6383
|
}
|
|
6000
6384
|
}
|
|
6385
|
+
catch (error) {
|
|
6386
|
+
if (verbose)
|
|
6387
|
+
console.warn('[hmr-ws][angular] transitive importer collection failed', error);
|
|
6388
|
+
}
|
|
6001
6389
|
try {
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6390
|
+
// Purge shared transform cache for the changed file +
|
|
6391
|
+
// hot-update roots unconditionally (their transform
|
|
6392
|
+
// output IS different now). Transitive importers are
|
|
6393
|
+
// only purged when narrowing decides their output may
|
|
6394
|
+
// have changed; otherwise their cached transforms are
|
|
6395
|
+
// still valid (compiled JS is identical even though the
|
|
6396
|
+
// runtime must re-evaluate them to refresh ESM bindings).
|
|
6009
6397
|
const transformCacheInvalidationUrls = new Set(collectAngularTransformCacheInvalidationUrls({
|
|
6010
6398
|
file,
|
|
6011
6399
|
isTs,
|
|
6012
6400
|
hotUpdateRoots: angularHotUpdateRoots,
|
|
6013
|
-
transitiveImporters,
|
|
6401
|
+
transitiveImporters: angularNeedsTransitive ? transitiveImporters : [],
|
|
6014
6402
|
projectRoot: server.config.root || process.cwd(),
|
|
6015
6403
|
}));
|
|
6016
6404
|
if (transformCacheInvalidationUrls.size) {
|
|
@@ -6024,17 +6412,74 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
6024
6412
|
if (verbose)
|
|
6025
6413
|
console.warn('[hmr-ws][angular] shared transform cache purge failed', error);
|
|
6026
6414
|
}
|
|
6415
|
+
updateMetrics.tAfterFramework = Date.now();
|
|
6027
6416
|
try {
|
|
6028
6417
|
const root = server.config.root || process.cwd();
|
|
6029
6418
|
const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
|
|
6030
6419
|
rememberAngularReloadSuppression(root, file);
|
|
6031
6420
|
const origin = getServerOrigin(server);
|
|
6421
|
+
const bootstrapEntryRel = getBootstrapEntryRelPath();
|
|
6422
|
+
// Stable URL + Explicit Invalidation:
|
|
6423
|
+
//
|
|
6424
|
+
// `evictPaths` is the canonical list of `/ns/m/<rel>` URLs
|
|
6425
|
+
// the runtime must drop from `g_moduleRegistry` before
|
|
6426
|
+
// re-importing `importerEntry`. Older versions of the
|
|
6427
|
+
// server signaled invalidation by bumping a global
|
|
6428
|
+
// `graphVersion` counter and embedding it in every URL —
|
|
6429
|
+
// but V8 keys the module registry by full URL, so a v1 →
|
|
6430
|
+
// v2 bump effectively flushed the entire dependency
|
|
6431
|
+
// graph from the cache and forced the runtime to
|
|
6432
|
+
// re-fetch + re-eval every transitively-imported module
|
|
6433
|
+
// on each save (~3s HMR cycles, dominated by Vite's
|
|
6434
|
+
// single-threaded transform pipeline). The new model:
|
|
6435
|
+
//
|
|
6436
|
+
// 1. URLs are stable: `/ns/m/<rel>` everywhere, no `vN`.
|
|
6437
|
+
// 2. The server walks the inverse-dependency closure and
|
|
6438
|
+
// sends only the modules that actually need to be
|
|
6439
|
+
// re-evaluated (typically O(1) for component edits,
|
|
6440
|
+
// or the changed file + entry for narrowed edits).
|
|
6441
|
+
// 3. The client calls `__nsInvalidateModules(evictPaths)`
|
|
6442
|
+
// and re-imports `importerEntry`, which causes V8 to
|
|
6443
|
+
// refetch ONLY those modules. Everything else stays
|
|
6444
|
+
// hot in the registry.
|
|
6445
|
+
//
|
|
6446
|
+
// Invariants enforced by `collectAngularEvictionUrls`:
|
|
6447
|
+
// - Always includes the changed file (so the new source
|
|
6448
|
+
// is fetched).
|
|
6449
|
+
// - Always includes `importerEntry` (so re-import
|
|
6450
|
+
// re-evaluates).
|
|
6451
|
+
// - Excludes node_modules (vendor packages are stable).
|
|
6452
|
+
// - Excludes virtual / runtime-graph-excluded ids.
|
|
6453
|
+
// - Origin-prefixed: `http://host:port/ns/m/<rel>`.
|
|
6454
|
+
let evictPaths = [];
|
|
6455
|
+
try {
|
|
6456
|
+
evictPaths = collectAngularEvictionUrls({
|
|
6457
|
+
file,
|
|
6458
|
+
hotUpdateRoots: angularHotUpdateRoots,
|
|
6459
|
+
transitiveImporters,
|
|
6460
|
+
projectRoot: root,
|
|
6461
|
+
origin,
|
|
6462
|
+
bootstrapEntry: bootstrapEntryRel,
|
|
6463
|
+
});
|
|
6464
|
+
}
|
|
6465
|
+
catch (error) {
|
|
6466
|
+
if (verbose)
|
|
6467
|
+
console.warn('[hmr-ws][angular] eviction set computation failed', error);
|
|
6468
|
+
}
|
|
6469
|
+
if (verbose) {
|
|
6470
|
+
console.log('[hmr-ws][angular] eviction set', {
|
|
6471
|
+
count: evictPaths.length,
|
|
6472
|
+
importerEntry: bootstrapEntryRel,
|
|
6473
|
+
});
|
|
6474
|
+
}
|
|
6032
6475
|
const msg = {
|
|
6033
6476
|
type: 'ns:angular-update',
|
|
6034
6477
|
origin,
|
|
6035
6478
|
path: rel,
|
|
6036
6479
|
version: graphVersion,
|
|
6037
6480
|
timestamp: Date.now(),
|
|
6481
|
+
evictPaths,
|
|
6482
|
+
importerEntry: bootstrapEntryRel,
|
|
6038
6483
|
};
|
|
6039
6484
|
if (verbose) {
|
|
6040
6485
|
console.log('[hmr-ws][angular] broadcasting update', Array.from(wss.clients || []).map((client) => ({
|
|
@@ -6046,12 +6491,14 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
6046
6491
|
wss.clients.forEach((client) => {
|
|
6047
6492
|
if (isSocketClientOpen(client)) {
|
|
6048
6493
|
client.send(JSON.stringify(msg));
|
|
6494
|
+
updateMetrics.recipients += 1;
|
|
6049
6495
|
}
|
|
6050
6496
|
});
|
|
6051
6497
|
}
|
|
6052
6498
|
catch (error) {
|
|
6053
6499
|
console.warn('[hmr-ws][angular] update failed:', error);
|
|
6054
6500
|
}
|
|
6501
|
+
emitHmrUpdateSummary();
|
|
6055
6502
|
if (shouldSuppressDefaultViteHotUpdate({ flavor: ACTIVE_STRATEGY.flavor, file })) {
|
|
6056
6503
|
return [];
|
|
6057
6504
|
}
|
|
@@ -6059,6 +6506,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
6059
6506
|
}
|
|
6060
6507
|
// TypeScript flavor: emit generic graph delta for app XML/TS/style changes
|
|
6061
6508
|
if (ACTIVE_STRATEGY.flavor === 'typescript') {
|
|
6509
|
+
updateMetrics.tAfterFramework = Date.now();
|
|
6062
6510
|
try {
|
|
6063
6511
|
const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
|
|
6064
6512
|
if (verbose)
|
|
@@ -6072,6 +6520,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
6072
6520
|
if (verbose)
|
|
6073
6521
|
console.warn('[hmr-ws][ts] failed to emit delta for', file, e);
|
|
6074
6522
|
}
|
|
6523
|
+
emitHmrUpdateSummary();
|
|
6075
6524
|
return;
|
|
6076
6525
|
}
|
|
6077
6526
|
// Solid flavor: emit graph delta for app TSX/TS/JSX file changes.
|
|
@@ -6085,6 +6534,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
6085
6534
|
const isSolidFile = /\.(tsx?|jsx?)$/i.test(file);
|
|
6086
6535
|
if (!isSolidFile)
|
|
6087
6536
|
return;
|
|
6537
|
+
updateMetrics.tAfterFramework = Date.now();
|
|
6088
6538
|
try {
|
|
6089
6539
|
const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
|
|
6090
6540
|
if (verbose)
|
|
@@ -6109,6 +6559,7 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
6109
6559
|
if (verbose)
|
|
6110
6560
|
console.warn('[hmr-ws][solid] failed to handle hot update for', file, e);
|
|
6111
6561
|
}
|
|
6562
|
+
emitHmrUpdateSummary();
|
|
6112
6563
|
return;
|
|
6113
6564
|
}
|
|
6114
6565
|
// Handle .vue file updates
|
|
@@ -6117,7 +6568,8 @@ export const piniaSymbol = p.piniaSymbol;
|
|
|
6117
6568
|
console.log('[hmr-ws] Not a .vue file, skipping');
|
|
6118
6569
|
return;
|
|
6119
6570
|
}
|
|
6120
|
-
|
|
6571
|
+
if (verbose)
|
|
6572
|
+
console.log('[hmr-ws] Processing .vue file update...');
|
|
6121
6573
|
try {
|
|
6122
6574
|
const root = server.config.root || process.cwd();
|
|
6123
6575
|
let rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
|
|
@@ -6394,6 +6846,10 @@ if (typeof __VUE_HMR_RUNTIME__ === 'undefined') {
|
|
|
6394
6846
|
console.warn('[hmr-ws] HMR update failed:', error);
|
|
6395
6847
|
console.error(error);
|
|
6396
6848
|
}
|
|
6849
|
+
// Vue path emits update summary at the end of the function so
|
|
6850
|
+
// every framework branch gets exactly one log line. Idempotent
|
|
6851
|
+
// — if any branch already emitted, this is a no-op.
|
|
6852
|
+
emitHmrUpdateSummary();
|
|
6397
6853
|
// CRITICAL: Return empty array to prevent Vite's default HMR
|
|
6398
6854
|
return [];
|
|
6399
6855
|
},
|