@nativescript/vite 8.0.0-alpha.4 → 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.
Files changed (84) hide show
  1. package/configuration/base.js +57 -0
  2. package/configuration/base.js.map +1 -1
  3. package/helpers/config-as-json.js +10 -0
  4. package/helpers/config-as-json.js.map +1 -1
  5. package/helpers/global-defines.d.ts +55 -0
  6. package/helpers/global-defines.js +81 -0
  7. package/helpers/global-defines.js.map +1 -1
  8. package/helpers/logging.d.ts +1 -0
  9. package/helpers/logging.js +36 -3
  10. package/helpers/logging.js.map +1 -1
  11. package/helpers/main-entry.js +167 -13
  12. package/helpers/main-entry.js.map +1 -1
  13. package/helpers/ns-core-url.d.ts +84 -0
  14. package/helpers/ns-core-url.js +168 -0
  15. package/helpers/ns-core-url.js.map +1 -0
  16. package/hmr/client/hmr-pending-overlay.d.ts +27 -0
  17. package/hmr/client/hmr-pending-overlay.js +50 -0
  18. package/hmr/client/hmr-pending-overlay.js.map +1 -0
  19. package/hmr/client/index.js +72 -1
  20. package/hmr/client/index.js.map +1 -1
  21. package/hmr/client/utils.d.ts +5 -0
  22. package/hmr/client/utils.js +153 -15
  23. package/hmr/client/utils.js.map +1 -1
  24. package/hmr/entry-runtime.js +95 -31
  25. package/hmr/entry-runtime.js.map +1 -1
  26. package/hmr/frameworks/angular/client/index.d.ts +1 -0
  27. package/hmr/frameworks/angular/client/index.js +424 -11
  28. package/hmr/frameworks/angular/client/index.js.map +1 -1
  29. package/hmr/server/core-sanitize.d.ts +8 -4
  30. package/hmr/server/core-sanitize.js +71 -41
  31. package/hmr/server/core-sanitize.js.map +1 -1
  32. package/hmr/server/import-map.js +7 -3
  33. package/hmr/server/import-map.js.map +1 -1
  34. package/hmr/server/ns-core-cjs-shape.d.ts +206 -0
  35. package/hmr/server/ns-core-cjs-shape.js +273 -0
  36. package/hmr/server/ns-core-cjs-shape.js.map +1 -0
  37. package/hmr/server/perf-instrumentation.d.ts +118 -0
  38. package/hmr/server/perf-instrumentation.js +198 -0
  39. package/hmr/server/perf-instrumentation.js.map +1 -0
  40. package/hmr/server/shared-transform-request.js +12 -5
  41. package/hmr/server/shared-transform-request.js.map +1 -1
  42. package/hmr/server/websocket-angular-hot-update.d.ts +16 -0
  43. package/hmr/server/websocket-angular-hot-update.js +163 -1
  44. package/hmr/server/websocket-angular-hot-update.js.map +1 -1
  45. package/hmr/server/websocket-core-bridge.d.ts +0 -2
  46. package/hmr/server/websocket-core-bridge.js +60 -58
  47. package/hmr/server/websocket-core-bridge.js.map +1 -1
  48. package/hmr/server/websocket-graph-upsert.d.ts +15 -0
  49. package/hmr/server/websocket-graph-upsert.js +20 -0
  50. package/hmr/server/websocket-graph-upsert.js.map +1 -1
  51. package/hmr/server/websocket-hmr-pending.d.ts +43 -0
  52. package/hmr/server/websocket-hmr-pending.js +55 -0
  53. package/hmr/server/websocket-hmr-pending.js.map +1 -0
  54. package/hmr/server/websocket-module-specifiers.js +12 -0
  55. package/hmr/server/websocket-module-specifiers.js.map +1 -1
  56. package/hmr/server/websocket-ns-m-finalize.d.ts +0 -10
  57. package/hmr/server/websocket-ns-m-finalize.js +26 -11
  58. package/hmr/server/websocket-ns-m-finalize.js.map +1 -1
  59. package/hmr/server/websocket-ns-m-paths.d.ts +1 -1
  60. package/hmr/server/websocket-ns-m-paths.js +59 -13
  61. package/hmr/server/websocket-ns-m-paths.js.map +1 -1
  62. package/hmr/server/websocket-ns-m-request.js +1 -16
  63. package/hmr/server/websocket-ns-m-request.js.map +1 -1
  64. package/hmr/server/websocket-runtime-compat.js.map +1 -1
  65. package/hmr/server/websocket-served-module-helpers.d.ts +36 -0
  66. package/hmr/server/websocket-served-module-helpers.js +613 -0
  67. package/hmr/server/websocket-served-module-helpers.js.map +1 -0
  68. package/hmr/server/websocket-vue-sfc.d.ts +0 -8
  69. package/hmr/server/websocket-vue-sfc.js +17 -19
  70. package/hmr/server/websocket-vue-sfc.js.map +1 -1
  71. package/hmr/server/websocket.d.ts +5 -5
  72. package/hmr/server/websocket.js +2867 -252
  73. package/hmr/server/websocket.js.map +1 -1
  74. package/hmr/shared/runtime/boot-timeline.d.ts +17 -0
  75. package/hmr/shared/runtime/boot-timeline.js +54 -0
  76. package/hmr/shared/runtime/boot-timeline.js.map +1 -0
  77. package/hmr/shared/runtime/dev-overlay.d.ts +49 -2
  78. package/hmr/shared/runtime/dev-overlay.js +587 -12
  79. package/hmr/shared/runtime/dev-overlay.js.map +1 -1
  80. package/hmr/shared/runtime/session-bootstrap.js +49 -0
  81. package/hmr/shared/runtime/session-bootstrap.js.map +1 -1
  82. package/hmr/shared/vendor/manifest.js +114 -12
  83. package/hmr/shared/vendor/manifest.js.map +1 -1
  84. package/package.json +1 -1
@@ -1,53 +1,64 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { normalizeStrayCoreStringLiterals, fixDanglingCoreFrom, normalizeAnyCoreSpecToBridge, isDeepCoreSubpath, rewriteSpecifiersForDevice } from './core-sanitize.js';
3
+ import { buildDefaultExportFooter, buildShapeInstallHeader, hasNamespaceReExport, rewriteNamespaceReExportsForShape } from './ns-core-cjs-shape.js';
3
4
  // AST tooling for robust transformations
4
5
  import { parse as babelParse } from '@babel/parser';
5
6
  import { genCode } from '../helpers/babel.js';
7
+ import babelCore from '@babel/core';
6
8
  import traverse from '@babel/traverse';
7
9
  // Ensure traverse callable across CJS/ESM builds
8
10
  const babelTraverse = traverse?.default || traverse;
9
11
  import * as t from '@babel/types';
10
12
  import { existsSync, readFileSync, statSync } from 'fs';
11
13
  import { astNormalizeModuleImportsAndHelpers, astVerifyAndAnnotateDuplicates } from '../helpers/ast-normalizer.js';
12
- import { stripDanglingViteCjsImports } from '../helpers/sanitize.js';
14
+ import { stripRtCoreSentinel, stripDanglingViteCjsImports } from '../helpers/sanitize.js';
13
15
  import { WebSocketServer } from 'ws';
14
16
  import * as path from 'path';
15
17
  import { createHash } from 'crypto';
16
18
  import * as PAT from './constants.js';
17
19
  import { getVendorManifest, resolveVendorSpecifier } from '../shared/vendor/registry.js';
18
- import { getProjectRootPath } from '../../helpers/project.js';
20
+ import { getPackageJson, getProjectFilePath, getProjectRootPath } from '../../helpers/project.js';
19
21
  import { loadPrebuiltVendorManifest } from '../shared/vendor/manifest-loader.js';
20
22
  import '../vendor-bootstrap.js';
23
+ import { NS_NATIVE_TAGS } from './compiler.js';
24
+ import { vueSfcCompiler } from '../frameworks/vue/server/compiler.js';
21
25
  import { linkAngularPartialsIfNeeded } from '../frameworks/angular/server/linker.js';
22
26
  import { vueServerStrategy } from '../frameworks/vue/server/strategy.js';
23
27
  import { angularServerStrategy } from '../frameworks/angular/server/strategy.js';
24
28
  import { solidServerStrategy } from '../frameworks/solid/server/strategy.js';
25
29
  import { typescriptServerStrategy } from '../frameworks/typescript/server/strategy.js';
26
- import { createProcessSfcCode } from '../frameworks/vue/server/sfc-transforms.js';
30
+ import { buildInlineTemplateBlock, createProcessSfcCode, extractTemplateRender, processTemplateVariantMinimal } from '../frameworks/vue/server/sfc-transforms.js';
31
+ import { astExtractImportsAndStripTypes } from '../helpers/ast-extract.js';
27
32
  import { getProjectAppPath, getProjectAppRelativePath, getProjectAppVirtualPath } from '../../helpers/utils.js';
28
33
  import { buildRuntimeConfig, generateImportMap } from './import-map.js';
29
34
  import { getCliFlags } from '../../helpers/cli-flags.js';
35
+ import { normalizeCoreSub as normalizeCoreSubCanonical } from '../../helpers/ns-core-url.js';
30
36
  import { isRuntimeGraphExcludedPath, matchesRuntimeGraphModuleId, normalizeRuntimeGraphPath, shouldIncludeRuntimeGraphFile, shouldSkipRuntimeGraphDirectoryName } from './runtime-graph-filter.js';
31
37
  import { resolveAngularCoreHmrImportSource, rewriteAngularEntryRegisterOnly } from './websocket-angular-entry.js';
32
- import { canonicalizeTransformRequestCacheKey, collectAngularHotUpdateRoots, collectAngularTransformCacheInvalidationUrls, collectAngularTransitiveImportersForInvalidation, collectGraphUpdateModulesForHotUpdate, normalizeHotReloadMatchPath, shouldInvalidateAngularTransitiveImporters, shouldSuppressDefaultViteHotUpdate, shouldSuppressViteFullReloadPayload } from './websocket-angular-hot-update.js';
33
- 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';
34
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';
35
43
  import { ensureNativeScriptModuleBindings, getProcessCodeResolvedSpecifierOverrides } from './websocket-module-bindings.js';
36
- import { buildVersionedCoreMainBridgeModule, buildVersionedCoreSubpathAliasModule, collectStaticExportNamesFromFile, collectStaticExportOriginsFromFile, ensureVersionedCoreImports, extractDirectExportedNames, hasModuleDefaultExport, normalizeCoreExportOriginsForRuntime, parseCoreBridgeRequest, resolveRuntimeCoreModulePath } from './websocket-core-bridge.js';
37
- import { finalizeNsMServedModule } from './websocket-ns-m-finalize.js';
38
- import { createNsMRequestContext, resolveNsMTransformedModule } from './websocket-ns-m-request.js';
39
- import { getNumericServeVersionTag, rewriteNsMImportPathForHmr } from './websocket-ns-m-paths.js';
40
- import { registerRuntimeCompatHandlers } from './websocket-runtime-compat.js';
41
- import { registerTxnHandler } from './websocket-txn.js';
42
- import { registerVendorUnifierHandler } from './websocket-vendor-unifier.js';
43
- import { registerVueSfcHandlers } from './websocket-vue-sfc.js';
44
+ import { collectStaticExportNamesFromFile, collectStaticExportOriginsFromFile, ensureVersionedCoreImports, extractDirectExportedNames, normalizeCoreExportOriginsForRuntime, parseCoreBridgeRequest, resolveRuntimeCoreModulePath } from './websocket-core-bridge.js';
44
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';
45
48
  export { ensureNativeScriptModuleBindings, getProcessCodeResolvedSpecifierOverrides } from './websocket-module-bindings.js';
46
49
  export { stripDecoratedServePrefixes, tryReadRawExplicitJavaScriptModule } from './websocket-module-specifiers.js';
47
- export { buildVersionedCoreMainBridgeModule, buildVersionedCoreSubpathAliasModule, collectStaticExportNamesFromFile, collectStaticExportOriginsFromFile, ensureVersionedCoreImports, normalizeCoreExportOriginsForRuntime, parseCoreBridgeRequest } from './websocket-core-bridge.js';
48
- export { rewriteNsMImportPathForHmr } from './websocket-ns-m-paths.js';
50
+ export { collectStaticExportNamesFromFile, collectStaticExportOriginsFromFile, ensureVersionedCoreImports, normalizeCoreExportOriginsForRuntime, parseCoreBridgeRequest } from './websocket-core-bridge.js';
49
51
  export { rewriteAngularEntryRegisterOnly } from './websocket-angular-entry.js';
50
- export { canonicalizeTransformRequestCacheKey, collectAngularHotUpdateRoots, collectAngularTransformCacheInvalidationUrls, collectAngularTransitiveImportersForInvalidation, collectGraphUpdateModulesForHotUpdate, createSharedTransformRequestRunner, normalizeHotReloadMatchPath, shouldInvalidateAngularTransitiveImporters, shouldSuppressDefaultViteHotUpdate, shouldSuppressViteFullReloadPayload, classifyGraphUpsert, shouldBroadcastGraphUpsertDelta };
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 };
57
+ const pluginTransformTypescript = (() => {
58
+ const requireFromHere = createRequire(import.meta.url);
59
+ const loaded = requireFromHere('@babel/plugin-transform-typescript');
60
+ return loaded?.default || loaded;
61
+ })();
51
62
  // Build a serialized process.env object from CLI --env.* flags.
52
63
  // This is injected into every HTTP-served module so app code referencing
53
64
  // process.env.TEST_ENV (etc.) works on device in HMR dev mode.
@@ -63,11 +74,40 @@ try {
63
74
  }
64
75
  catch { }
65
76
  const __processEnvJson = JSON.stringify(__processEnvEntries);
77
+ const { parse, compileTemplate, compileScript } = vueSfcCompiler;
66
78
  const APP_ROOT_DIR = getProjectAppPath();
67
79
  const APP_VIRTUAL_PREFIX = getProjectAppVirtualPath();
68
80
  const APP_VIRTUAL_WITH_SLASH = `${APP_VIRTUAL_PREFIX}/`;
69
81
  const DEFAULT_MAIN_ENTRY = getProjectAppRelativePath('app.ts');
70
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
+ }
71
111
  const STRATEGY_REGISTRY = new Map([
72
112
  ['vue', vueServerStrategy],
73
113
  ['angular', angularServerStrategy],
@@ -314,44 +354,12 @@ function ensureGuardPlainDynamicImports(code, origin) {
314
354
  return code;
315
355
  }
316
356
  }
317
- function ensureDynamicHmrImportHelper(code) {
318
- try {
319
- if (!code.includes('__nsDynamicHmrImport('))
320
- return code;
321
- if (code.includes('const __nsDynamicHmrImport ='))
322
- return code;
323
- const helper = 'const __nsDynamicHmrImport = (spec) => {\n' +
324
- " const __nsm = '/ns' + '/m';\n" +
325
- " const __nsBootPrefix = typeof import.meta !== 'undefined' && import.meta && typeof import.meta.url === 'string' && import.meta.url.includes('/__ns_boot__/b1/') ? '/__ns_boot__/b1' : '';\n" +
326
- " const __nsImporterTagMatch = typeof import.meta !== 'undefined' && import.meta && typeof import.meta.url === 'string' ? import.meta.url.match(/\\/__ns_hmr__\\/([^/]+)\\//) : null;\n" +
327
- " const __nsImporterTag = __nsImporterTagMatch && __nsImporterTagMatch[1] ? decodeURIComponent(__nsImporterTagMatch[1]) : '';\n" +
328
- " try { if (!spec || spec === '@') { return import(new URL(__nsm + '/__invalid_at__.mjs', import.meta.url).href); } } catch {}\n" +
329
- ' try {\n' +
330
- " if (typeof spec === 'string' && spec.startsWith(__nsm + '/')) {\n" +
331
- ' const g = globalThis;\n' +
332
- " const graphVersion = typeof g.__NS_HMR_GRAPH_VERSION__ === 'number' ? g.__NS_HMR_GRAPH_VERSION__ : 0;\n" +
333
- " const nonce = typeof g.__NS_HMR_IMPORT_NONCE__ === 'number' ? g.__NS_HMR_IMPORT_NONCE__ : 0;\n" +
334
- " const __nsActiveBootPrefix = graphVersion || nonce ? '' : __nsBootPrefix;\n" +
335
- " if (spec.includes('/__ns_hmr__/')) {\n" +
336
- " const __preservedSpec = !nonce && __nsBootPrefix && spec.startsWith(__nsm + '/__ns_hmr__/') && !spec.includes('/node_modules/') ? __nsm + __nsBootPrefix + spec.slice(__nsm.length) : spec;\n" +
337
- ' return import(new URL(__preservedSpec, import.meta.url).href);\n' +
338
- ' }\n' +
339
- " if (spec.startsWith(__nsm + '/node_modules/')) { return import(new URL(spec, import.meta.url).href); }\n" +
340
- " const tag = nonce ? `n${nonce}` : (graphVersion ? `v${graphVersion}` : (__nsImporterTag || 'live'));\n" +
341
- " const nextPath = __nsm + __nsActiveBootPrefix + '/__ns_hmr__/' + encodeURIComponent(tag) + spec.slice(__nsm.length);\n" +
342
- " const origin = typeof g.__NS_HTTP_ORIGIN__ === 'string' && /^https?:\\/\\//.test(g.__NS_HTTP_ORIGIN__) ? g.__NS_HTTP_ORIGIN__ : '';\n" +
343
- ' return import(origin ? origin + nextPath : new URL(nextPath, import.meta.url).href);\n' +
344
- ' }\n' +
345
- ' } catch {}\n' +
346
- ' return import(spec);\n' +
347
- '};\n';
348
- return helper + code;
349
- }
350
- catch {
351
- return code;
352
- }
353
- }
354
- 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) {
355
363
  const STAR_RE = /^[ \t]*(export\s+\*\s+from\s+["'])([^"']+)(["'];?)[ \t]*$/gm;
356
364
  let match;
357
365
  const replacements = [];
@@ -363,25 +371,41 @@ async function expandStarExports(code, server, projectRoot, verbose) {
363
371
  }
364
372
  if (!replacements.length)
365
373
  return code;
366
- for (const rep of replacements) {
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) => {
367
381
  try {
368
382
  let vitePath = rep.url.replace(/^https?:\/\/[^/]+/, '');
369
383
  vitePath = vitePath.replace(/^\/ns\/m\//, '/');
370
384
  vitePath = vitePath.replace(/^\/__ns_boot__\/[^/]+/, '');
371
385
  vitePath = vitePath.replace(/\/__ns_hmr__\/[^/]+/, '');
372
- const result = await server.transformRequest(vitePath);
386
+ const result = await transformer(vitePath);
373
387
  if (!result?.code)
374
- continue;
388
+ return null;
375
389
  const names = extractExportedNames(result.code);
376
390
  if (!names.length)
377
- continue;
378
- const explicit = `export { ${names.join(', ')} } from ${JSON.stringify(rep.url)};`;
379
- code = code.replace(rep.full, explicit);
391
+ return null;
380
392
  if (verbose) {
381
- console.log(`[ns/m] expanded export* -> ${names.length} names from ${vitePath}`);
393
+ try {
394
+ console.log(`[ns/m] expanded export* -> ${names.length} names from ${vitePath}`);
395
+ }
396
+ catch { }
382
397
  }
398
+ return { rep, names };
383
399
  }
384
- 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);
385
409
  }
386
410
  return code;
387
411
  }
@@ -890,6 +914,14 @@ function toNodeModulesHttpModuleId(importPath) {
890
914
  }
891
915
  return `/ns/m/node_modules/${nodeModulesSpecifier}`;
892
916
  }
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.
893
925
  function normalizeAbsoluteFilesystemImport(spec, importerPath, projectRoot) {
894
926
  if (!spec || typeof spec !== 'string') {
895
927
  return null;
@@ -1049,8 +1081,31 @@ export function wrapCommonJsModuleForDevice(code) {
1049
1081
  `var __ns_cjs_require_kind = (typeof globalThis.__nsBaseRequire === 'function' ? 'base-require' : (typeof globalThis.__nsRequire === 'function' ? 'vendor-require' : 'global-require'));\n` +
1050
1082
  `var require = function(spec) {\n` +
1051
1083
  ` if (!__ns_cjs_require_base) { throw new Error('require is not defined'); }\n` +
1052
- ` try { var __nsRecord = globalThis.__NS_RECORD_MODULE_PROVENANCE__; if (typeof __nsRecord === 'function') { __nsRecord(String(spec), { kind: __ns_cjs_require_kind, specifier: String(spec), via: 'cjs-wrapper', parent: (typeof import.meta !== 'undefined' && import.meta && import.meta.url) ? import.meta.url : undefined }); } } catch (e) {}\n` +
1053
- ` var mod = __ns_cjs_require_base(spec);\n` +
1084
+ // Resolve relative specifiers against the HTTP-served module's URL
1085
+ // before delegating to NS's runtime require. Without this step,
1086
+ // \`require('./base64-vlq')\` inside a CJS module served from
1087
+ // \`http://.../ns/m/node_modules/source-map-js/lib/source-map-generator.js\`
1088
+ // would pass a literal '"./base64-vlq"' to the native require, which
1089
+ // has no notion of the current HTTP-module's location and either
1090
+ // throws "Module not found" or fetches an arbitrary filesystem path
1091
+ // that happens to parse as code (producing misleading syntax errors
1092
+ // like "missing ) after argument list" from unrelated modules).
1093
+ ` var __nsResolvedSpec = spec;\n` +
1094
+ ` try {\n` +
1095
+ ` if (typeof spec === 'string' && (spec.indexOf('./') === 0 || spec.indexOf('../') === 0)) {\n` +
1096
+ ` var __nsParentUrl = (typeof import.meta !== 'undefined' && import.meta && typeof import.meta.url === 'string') ? import.meta.url : null;\n` +
1097
+ ` if (__nsParentUrl) {\n` +
1098
+ ` var __nsResolvedUrl = new URL(spec, __nsParentUrl);\n` +
1099
+ ` // Common Node-style bare extensions: prefer .js if the resolved URL lacks an extension in its last path segment.\n` +
1100
+ ` if (!/\\.[A-Za-z0-9]+$/.test(__nsResolvedUrl.pathname.split('/').pop() || '')) {\n` +
1101
+ ` __nsResolvedUrl.pathname = __nsResolvedUrl.pathname.replace(/\\/+$/, '') + '.js';\n` +
1102
+ ` }\n` +
1103
+ ` __nsResolvedSpec = __nsResolvedUrl.href;\n` +
1104
+ ` }\n` +
1105
+ ` }\n` +
1106
+ ` } catch (e) {}\n` +
1107
+ ` try { var __nsRecord = globalThis.__NS_RECORD_MODULE_PROVENANCE__; if (typeof __nsRecord === 'function') { __nsRecord(String(__nsResolvedSpec), { kind: __ns_cjs_require_kind, specifier: String(spec), url: __nsResolvedSpec !== spec ? __nsResolvedSpec : undefined, via: 'cjs-wrapper', parent: (typeof import.meta !== 'undefined' && import.meta && import.meta.url) ? import.meta.url : undefined }); } } catch (e) {}\n` +
1108
+ ` var mod = __ns_cjs_require_base(__nsResolvedSpec);\n` +
1054
1109
  ` try {\n` +
1055
1110
  ` if (mod && (typeof mod === 'object' || typeof mod === 'function') && mod.default !== undefined) {\n` +
1056
1111
  ` var keys = [];\n` +
@@ -2096,6 +2151,27 @@ export function rewriteImports(code, importerPath, sfcFileMap, depFileMap, proje
2096
2151
  return `${prefix}./${depFile}${suffix}`;
2097
2152
  }
2098
2153
  }
2154
+ // Bare npm package specifier fallback — route to /ns/m/node_modules/.
2155
+ // This catches specifiers like `source-map-js/lib/source-map-generator.js`
2156
+ // emitted by helpers such as the CommonJS compat transform, which Vite
2157
+ // would normally resolve to an absolute path but which pass through the
2158
+ // rewriter as bare strings here. Under HMR (core external) bundle.mjs
2159
+ // depends on these resolving over HTTP rather than via a filesystem
2160
+ // bare-specifier lookup, which iOS can't satisfy and which crashes with
2161
+ // "Module not found".
2162
+ if (spec && !spec.startsWith('/') && !spec.startsWith('./') && !spec.startsWith('../') && !/^https?:\/\//i.test(spec) && !spec.startsWith('ns-vendor:') && !spec.startsWith('@nativescript/core')) {
2163
+ // Only treat as a package spec if it looks like one — disallow
2164
+ // plain identifiers like `moment` unresolved (those are left alone
2165
+ // for existing vendor-routing paths to handle).
2166
+ const bareNpmRe = /^(?:@[A-Za-z0-9][\w.-]*\/)?[A-Za-z0-9][\w.-]*(?:\/[\w.\-/]+)?$/;
2167
+ if (bareNpmRe.test(spec)) {
2168
+ const httpSpec = `/ns/m/node_modules/${spec}`;
2169
+ if (httpOriginSafe) {
2170
+ return `${prefix}${httpOriginSafe}${httpSpec}${suffix}`;
2171
+ }
2172
+ return `${prefix}${httpSpec}${suffix}`;
2173
+ }
2174
+ }
2099
2175
  // Leave everything else unchanged (vendor imports, etc.)
2100
2176
  return `${prefix}${spec}${suffix}`;
2101
2177
  };
@@ -2237,10 +2313,21 @@ function createHmrWebSocketPlugin(opts) {
2237
2313
  let registrySent = false;
2238
2314
  let vendorBootstrapDone = false;
2239
2315
  let pluginRoot;
2240
- let graphVersion = 0;
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;
2241
2321
  // Transactional HMR batches: map graphVersion -> ordered list of changed ids for that version
2242
2322
  const txnBatches = new Map();
2243
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;
2244
2331
  function rememberAngularReloadSuppression(root, file, ttlMs = 3000) {
2245
2332
  const absPath = normalizeHotReloadMatchPath(file);
2246
2333
  const relPath = normalizeHotReloadMatchPath(file, root);
@@ -2412,12 +2499,17 @@ function createHmrWebSocketPlugin(opts) {
2412
2499
  const classification = classifyGraphUpsert(existing, hash, normDeps);
2413
2500
  if (classification === 'unchanged')
2414
2501
  return existing;
2415
- graphVersion++;
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
+ }
2416
2508
  const gm = { id, deps: normDeps, hash };
2417
2509
  graph.set(id, gm);
2418
2510
  if (verbose) {
2419
2511
  try {
2420
- 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 });
2421
2513
  console.log('[hmr-ws][graph] size', graph.size);
2422
2514
  }
2423
2515
  catch { }
@@ -2445,6 +2537,8 @@ function createHmrWebSocketPlugin(opts) {
2445
2537
  async function populateInitialGraph(server) {
2446
2538
  if (graph.size)
2447
2539
  return; // already populated
2540
+ const tStart = Date.now();
2541
+ const versionAtStart = graphVersion;
2448
2542
  const root = server.config.root || process.cwd();
2449
2543
  // Avoid direct require in ESM build: lazily obtain fs & path via createRequire or dynamic import
2450
2544
  let fs;
@@ -2460,6 +2554,18 @@ function createHmrWebSocketPlugin(opts) {
2460
2554
  fs = await import('fs');
2461
2555
  pathMod = await import('path');
2462
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
+ };
2463
2569
  async function walk(dir) {
2464
2570
  for (const name of fs.readdirSync(dir)) {
2465
2571
  if (name === 'node_modules' || name.startsWith('.') || shouldSkipRuntimeGraphDirectoryName(name))
@@ -2474,7 +2580,7 @@ function createHmrWebSocketPlugin(opts) {
2474
2580
  const rel = '/' + pathMod.relative(root, full).split(pathMod.sep).join('/');
2475
2581
  // Transform via Vite to gather deps (ignore failures)
2476
2582
  try {
2477
- const transformed = await server.transformRequest(rel);
2583
+ const transformed = await bulkTransform(rel);
2478
2584
  const code = transformed?.code || '';
2479
2585
  const deps = [];
2480
2586
  // fallback to import relationships via moduleGraph
@@ -2485,7 +2591,10 @@ function createHmrWebSocketPlugin(opts) {
2485
2591
  deps.push(m.id.split('?')[0]);
2486
2592
  }
2487
2593
  }
2488
- upsertGraphModule(rel, code, deps);
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 });
2489
2598
  }
2490
2599
  catch { }
2491
2600
  }
@@ -2498,6 +2607,36 @@ function createHmrWebSocketPlugin(opts) {
2498
2607
  await walk(pathMod.join(root, 'src'));
2499
2608
  }
2500
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;
2501
2640
  }
2502
2641
  return {
2503
2642
  name: 'nativescript-hmr-websocket',
@@ -2526,12 +2665,22 @@ function createHmrWebSocketPlugin(opts) {
2526
2665
  return originalSend(payload, ...rest);
2527
2666
  });
2528
2667
  }
2529
- // Default to serialized transform execution for deterministic HTTP HMR startup.
2530
- // Higher fan-out can be re-enabled explicitly via NS_VITE_HMR_TRANSFORM_CONCURRENCY.
2531
- const configuredTransformConcurrency = Number.parseInt(process.env.NS_VITE_HMR_TRANSFORM_CONCURRENCY || '1', 10);
2532
- const transformConcurrency = Number.isFinite(configuredTransformConcurrency) && configuredTransformConcurrency > 0 ? configuredTransformConcurrency : 1;
2533
- const configuredTransformCacheMs = Number.parseInt(process.env.NS_VITE_HMR_TRANSFORM_CACHE_MS || '15000', 10);
2534
- const transformCacheMs = Number.isFinite(configuredTransformCacheMs) && configuredTransformCacheMs >= 0 ? configuredTransformCacheMs : 15000;
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;
2535
2684
  sharedTransformRequest = createSharedTransformRequestRunner((url) => server.transformRequest(url), (url, timeoutMs) => {
2536
2685
  try {
2537
2686
  console.warn('[ns:m] slow transformRequest for', url, '(>' + timeoutMs + 'ms)');
@@ -2542,6 +2691,120 @@ function createHmrWebSocketPlugin(opts) {
2542
2691
  resultCacheTtlMs: transformCacheMs,
2543
2692
  getResultCacheKey: (url) => canonicalizeTransformRequestCacheKey(url, pluginRoot || process.cwd()),
2544
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 { }
2545
2808
  // Attempt early vendor manifest bootstrap once per server.
2546
2809
  if (!vendorBootstrapDone) {
2547
2810
  vendorBootstrapDone = true;
@@ -2816,27 +3079,137 @@ function createHmrWebSocketPlugin(opts) {
2816
3079
  const urlObj = new URL(req.url || '', 'http://localhost');
2817
3080
  if (!urlObj.pathname.startsWith('/ns/m'))
2818
3081
  return next();
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.
2819
3099
  res.setHeader('Access-Control-Allow-Origin', '*');
2820
3100
  res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
2821
3101
  // Disable caching for dev ESM endpoints to avoid device-side stale module reuse
2822
3102
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
2823
3103
  res.setHeader('Pragma', 'no-cache');
2824
3104
  res.setHeader('Expires', '0');
2825
- const serverRoot = (server.config?.root || process.cwd());
2826
- const requestContextResult = createNsMRequestContext(req.url || '', serverRoot, APP_VIRTUAL_WITH_SLASH);
2827
- if (requestContextResult.kind === 'next')
2828
- return next();
2829
- if (requestContextResult.kind === 'response') {
2830
- res.statusCode = requestContextResult.statusCode;
2831
- res.end(requestContextResult.code);
3105
+ // Support both query (?path=/abs) and path-style (/ns/m/abs)
3106
+ let spec = urlObj.searchParams.get('path') || '';
3107
+ // Optional graph version pin for deterministic boot
3108
+ let forcedVer = urlObj.searchParams.get('v');
3109
+ let bootTaggedRequest = false;
3110
+ if (!spec) {
3111
+ const base = '/ns/m';
3112
+ let rest = urlObj.pathname.slice(base.length);
3113
+ if (rest && rest !== '/')
3114
+ spec = rest;
3115
+ }
3116
+ // Special-case stub for anomalous '@' imports emitted as '/__invalid_at__.mjs'
3117
+ if (spec === '/__invalid_at__.mjs' || spec === '__invalid_at__.mjs') {
3118
+ res.statusCode = 200;
3119
+ res.end("// invalid '@' import stub\nexport {}\n");
3120
+ return;
3121
+ }
3122
+ if (!spec) {
3123
+ res.statusCode = 200;
3124
+ res.end('export {}\n');
2832
3125
  return;
2833
3126
  }
2834
- let { spec, forcedVer, bootTaggedRequest, transformCandidates, candidates } = requestContextResult.value;
3127
+ const serverRoot = (server.config?.root || process.cwd());
3128
+ spec = spec.replace(/[?#].*$/, '');
3129
+ // Accept path-based boot/HMR prefixes:
3130
+ // /ns/m/__ns_boot__/b1/<real-spec>
3131
+ // /ns/m/__ns_hmr__/<tag>/<real-spec>
3132
+ // /ns/m/__ns_boot__/b1/__ns_hmr__/<tag>/<real-spec>
3133
+ // The iOS HTTP ESM loader canonicalizes cache keys by stripping query params,
3134
+ // so we must carry the cache-buster in the path.
3135
+ try {
3136
+ const decorated = stripDecoratedServePrefixes(spec);
3137
+ spec = decorated.cleanedSpec;
3138
+ bootTaggedRequest = decorated.bootTaggedRequest;
3139
+ forcedVer || (forcedVer = decorated.forcedVer);
3140
+ }
3141
+ catch { }
3142
+ // Normalize absolute filesystem paths back to project-relative ids (e.g. /src/app.ts)
3143
+ try {
3144
+ const toPosix = (p) => p.replace(/\\/g, '/');
3145
+ const rootPosix = toPosix(serverRoot);
3146
+ const specPosix = toPosix(spec);
3147
+ // If spec is an absolute path under the project root, convert to '/'+relative
3148
+ const isAbsFs = /^\//.test(specPosix) || /^[A-Za-z]:\//.test(spec); // posix or win drive
3149
+ if (isAbsFs) {
3150
+ let rel = specPosix.startsWith(rootPosix) ? specPosix.slice(rootPosix.length) : require('path').posix.relative(rootPosix, specPosix);
3151
+ if (!rel.startsWith('..')) {
3152
+ if (!rel.startsWith('/'))
3153
+ rel = '/' + rel;
3154
+ // Ensure leading '/src' style when path maps into src
3155
+ spec = rel;
3156
+ }
3157
+ }
3158
+ }
3159
+ catch { }
2835
3160
  // Serve Vite virtual modules (/@id/ prefix). These are internal
2836
3161
  // virtual modules (e.g., \0nsvite:nsconfig-json for ~/package.json)
2837
3162
  // that don't exist on disk. Decode the ID and load via plugin container.
3163
+ if (spec.startsWith('/@id/')) {
3164
+ try {
3165
+ // First try Vite's transform pipeline directly
3166
+ const vr = await sharedTransformRequest(spec);
3167
+ if (vr?.code) {
3168
+ res.statusCode = 200;
3169
+ res.end(vr.code);
3170
+ return;
3171
+ }
3172
+ }
3173
+ catch { }
3174
+ try {
3175
+ // Fallback: decode the virtual module ID (__x00__ → \0) and
3176
+ // load through the plugin container directly
3177
+ const rawId = spec.slice('/@id/'.length).replace(/__x00__/g, '\0');
3178
+ const loadResult = await server.pluginContainer.load(rawId);
3179
+ if (loadResult) {
3180
+ const code = typeof loadResult === 'string' ? loadResult : loadResult.code;
3181
+ if (code) {
3182
+ res.statusCode = 200;
3183
+ res.end(code);
3184
+ return;
3185
+ }
3186
+ }
3187
+ }
3188
+ catch { }
3189
+ }
3190
+ if (spec.startsWith('@/'))
3191
+ spec = APP_VIRTUAL_WITH_SLASH + spec.slice(2);
3192
+ if (spec.startsWith('./'))
3193
+ spec = spec.slice(1);
3194
+ const blockedNodeModulesReason = getBlockedDeviceNodeModulesReason(spec);
3195
+ if (blockedNodeModulesReason) {
3196
+ res.statusCode = 404;
3197
+ res.end(`// [ns:m] blocked device import\nthrow new Error(${JSON.stringify(`[ns/m] ${blockedNodeModulesReason}`)});\nexport {};\n`);
3198
+ return;
3199
+ }
3200
+ if (!spec.startsWith('/'))
3201
+ spec = '/' + spec;
3202
+ const hasExt = /\.(ts|tsx|js|jsx|mjs|mts|cts|vue)$/i.test(spec);
3203
+ const baseNoExt = hasExt ? spec.replace(/\.(ts|tsx|js|jsx|mjs|mts|cts)$/i, '') : spec;
3204
+ const candidates = [...(hasExt ? [spec] : []), baseNoExt + '.ts', baseNoExt + '.js', baseNoExt + '.tsx', baseNoExt + '.jsx', baseNoExt + '.mjs', baseNoExt + '.mts', baseNoExt + '.cts', baseNoExt + '.vue', baseNoExt + '/index.ts', baseNoExt + '/index.js', baseNoExt + '/index.tsx', baseNoExt + '/index.jsx', baseNoExt + '/index.mjs'];
3205
+ const transformCandidates = filterExistingNodeModulesTransformCandidates(spec, candidates, serverRoot);
2838
3206
  let transformed = null;
2839
3207
  let resolvedCandidate = null;
3208
+ const rawExplicitModule = tryReadRawExplicitJavaScriptModule(spec, serverRoot);
3209
+ if (rawExplicitModule) {
3210
+ transformed = { code: rawExplicitModule.code };
3211
+ resolvedCandidate = rawExplicitModule.resolvedId;
3212
+ }
2840
3213
  // Queue and dedupe transformRequest calls so heavy app graphs do not
2841
3214
  // overwhelm Vite with concurrent work. Slow-transform warnings start only
2842
3215
  // when the transform actually begins executing, and requests stay pending
@@ -2844,15 +3217,86 @@ function createHmrWebSocketPlugin(opts) {
2844
3217
  const transformWithTimeout = (url, timeoutMs = 120000) => {
2845
3218
  return sharedTransformRequest(url, timeoutMs);
2846
3219
  };
2847
- ({ transformed, resolvedCandidate } = await resolveNsMTransformedModule({
2848
- context: requestContextResult.value,
2849
- transformRequest: transformWithTimeout,
2850
- resolveId: async (id) => {
2851
- const resolved = await server.pluginContainer?.resolveId?.(id, undefined);
2852
- return typeof resolved === 'string' ? resolved : resolved?.id || null;
2853
- },
2854
- loadVirtualId: async (id) => await server.pluginContainer.load(id),
2855
- }));
3220
+ if (!transformed?.code) {
3221
+ for (const cand of transformCandidates) {
3222
+ try {
3223
+ const r = await transformWithTimeout(cand);
3224
+ if (r?.code) {
3225
+ transformed = r;
3226
+ resolvedCandidate = cand;
3227
+ break;
3228
+ }
3229
+ }
3230
+ catch { }
3231
+ }
3232
+ }
3233
+ // Fallback 1: ask Vite to resolve the id, then transform the resolved id (handles aliases and virtual ids)
3234
+ if (!transformed?.code) {
3235
+ try {
3236
+ const rid = await server.pluginContainer?.resolveId?.(spec, undefined);
3237
+ const ridStr = typeof rid === 'string' ? rid : rid?.id || null;
3238
+ if (ridStr) {
3239
+ const r = await transformWithTimeout(ridStr);
3240
+ if (r?.code) {
3241
+ transformed = r;
3242
+ resolvedCandidate = ridStr;
3243
+ }
3244
+ }
3245
+ }
3246
+ catch { }
3247
+ }
3248
+ // Fallback 1b: if spec is a /node_modules/ path, extract bare specifier
3249
+ // and try resolveId with that. This handles package.json "exports" field
3250
+ // resolution (e.g., solid-js/jsx-runtime → solid-js/dist/solid.js).
3251
+ if (!transformed?.code && spec.includes('/node_modules/')) {
3252
+ try {
3253
+ const nmIdx = spec.lastIndexOf('/node_modules/');
3254
+ const bare = spec.slice(nmIdx + '/node_modules/'.length);
3255
+ if (bare && !bare.startsWith('.')) {
3256
+ const rid = await server.pluginContainer?.resolveId?.(bare, undefined);
3257
+ const ridStr = typeof rid === 'string' ? rid : rid?.id || null;
3258
+ if (ridStr) {
3259
+ const r = await sharedTransformRequest(ridStr);
3260
+ if (r?.code) {
3261
+ transformed = r;
3262
+ resolvedCandidate = ridStr;
3263
+ }
3264
+ }
3265
+ }
3266
+ }
3267
+ catch { }
3268
+ }
3269
+ // Fallback 2: try /@fs absolute path under project root (Vite file system alias)
3270
+ if (!transformed?.code) {
3271
+ try {
3272
+ const toPosix = (p) => p.replace(/\\/g, '/');
3273
+ const rootPosix = toPosix(serverRoot).replace(/\/$/, '');
3274
+ const absPosix = `${rootPosix}${spec.startsWith('/') ? '' : '/'}${spec}`;
3275
+ const fsId = `/@fs${absPosix}`;
3276
+ if (resolveCandidateFilePath(fsId, serverRoot)) {
3277
+ const r = await transformWithTimeout(fsId);
3278
+ if (r?.code) {
3279
+ transformed = r;
3280
+ resolvedCandidate = fsId;
3281
+ }
3282
+ }
3283
+ }
3284
+ catch { }
3285
+ }
3286
+ // Fallback 3: try adding ?import to hint Vite's transform pipeline
3287
+ if (!transformed?.code) {
3288
+ for (const cand of transformCandidates) {
3289
+ try {
3290
+ const r = await transformWithTimeout(`${cand}${cand.includes('?') ? '&' : '?'}import`);
3291
+ if (r?.code) {
3292
+ transformed = r;
3293
+ resolvedCandidate = `${cand}?import`;
3294
+ break;
3295
+ }
3296
+ }
3297
+ catch { }
3298
+ }
3299
+ }
2856
3300
  // Solid HMR: patch @@solid-refresh's $$refreshESM to do inline patching
2857
3301
  // during module re-evaluation instead of deferring to hot.accept() callback.
2858
3302
  // In NativeScript's HTTP ESM environment, accept callbacks are registered
@@ -2930,7 +3374,9 @@ function createHmrWebSocketPlugin(opts) {
2930
3374
  }
2931
3375
  catch { }
2932
3376
  }
2933
- upsertGraphModule(id, code, deps);
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 });
2934
3380
  }
2935
3381
  }
2936
3382
  }
@@ -3055,47 +3501,200 @@ export const piniaSymbol = p.piniaSymbol;
3055
3501
  }
3056
3502
  }
3057
3503
  }
3504
+ let code = transformed.code;
3505
+ // Prepend guard to capture any URL-based require attempts
3506
+ code = REQUIRE_GUARD_SNIPPET + code;
3507
+ code = cleanCode(code);
3508
+ const isNodeMod = /(?:^|\/)node_modules\//.test(resolvedCandidate || spec || '');
3509
+ code = processCodeForDevice(code, false, true, isNodeMod, resolvedCandidate || spec);
3510
+ // Solid HMR: The NativeScript iOS/Android runtime provides import.meta.hot
3511
+ // natively (via InitializeImportMetaHot in HMRSupport.mm) with C++-backed
3512
+ // persistent hot.data that survives across module re-evaluations.
3513
+ // cleanCode() strips Vite's __vite__createHotContext assignment, which is
3514
+ // correct — the runtime's native hot context is better.
3058
3515
  const projectRoot = server.config?.root || process.cwd();
3059
3516
  const serverOrigin = getServerOrigin(server);
3060
- let code;
3517
+ if (ACTIVE_STRATEGY?.flavor === 'angular') {
3518
+ code = prepareAngularEntryForDevice(code, resolvedCandidate || spec, sfcFileMap, depFileMap, projectRoot, !!verbose, undefined, serverOrigin, true);
3519
+ }
3520
+ else {
3521
+ code = rewriteImports(code, resolvedCandidate || spec, sfcFileMap, depFileMap, projectRoot, !!verbose, undefined, serverOrigin, true);
3522
+ }
3523
+ // Expand `export * from "url"` into explicit named re-exports.
3524
+ // NativeScript's HTTP ESM loader may not propagate star-re-exports across
3525
+ // HTTP module boundaries (the namespace object gets direct exports but
3526
+ // misses re-exported names). By expanding to `export { a, b } from "url"`,
3527
+ // the engine sees explicit named exports and resolves them correctly.
3061
3528
  try {
3062
- code = await finalizeNsMServedModule({
3063
- code: transformed.code,
3064
- spec,
3065
- resolvedCandidate,
3066
- forcedVer,
3067
- bootTaggedRequest,
3068
- graphVersion: Number(graphVersion || 0),
3069
- serverOrigin,
3070
- strategy: ACTIVE_STRATEGY,
3071
- helpers: {
3072
- requireGuardSnippet: REQUIRE_GUARD_SNIPPET,
3073
- cleanCode,
3074
- processCodeForDevice: (value, sourceId) => processCodeForDevice(value, false, true, /(?:^|\/)node_modules\//.test(sourceId || spec || ''), sourceId || spec),
3075
- rewriteImports: (value, importerPath) => rewriteImports(value, importerPath, sfcFileMap, depFileMap, projectRoot, !!verbose, undefined, serverOrigin, true),
3076
- rewriteAngularEntry: (value, importerPath) => prepareAngularEntryForDevice(value, importerPath, sfcFileMap, depFileMap, projectRoot, !!verbose, undefined, serverOrigin, true),
3077
- expandStarExports: async (value) => expandStarExports(value, server, projectRoot, verbose),
3078
- dedupeRtNamedImportsAgainstDestructures,
3079
- ensureVariableDynamicImportHelper,
3080
- ensureGuardPlainDynamicImports,
3081
- deduplicateLinkerImports,
3082
- wrapCommonJsModuleForDevice,
3083
- assertNoOptimizedArtifacts,
3084
- ensureVersionedRtImports,
3085
- ensureDestructureCoreImports,
3086
- buildBootProgressSnippet,
3087
- hoistTopLevelStaticImports,
3088
- warn: (message, error) => {
3089
- if (verbose)
3090
- console.warn(`${message}:`, error?.message || error);
3091
- },
3092
- },
3093
- });
3529
+ code = await expandStarExports(code, server, server.config?.root || process.cwd(), verbose, sharedTransformRequest);
3530
+ }
3531
+ catch (e) {
3532
+ if (verbose)
3533
+ console.warn('[ns/m] export* expansion failed:', e?.message);
3534
+ }
3535
+ // Dedupe any /ns/rt named imports that duplicate destructured bindings off default /ns/rt
3536
+ try {
3537
+ code = dedupeRtNamedImportsAgainstDestructures(code);
3538
+ }
3539
+ catch { }
3540
+ code = ensureVariableDynamicImportHelper(code);
3541
+ // Final safety: guard any plain dynamic import(...) occurrences to reroute anomalous '@' specs
3542
+ try {
3543
+ code = ensureGuardPlainDynamicImports(code, getServerOrigin(server));
3544
+ }
3545
+ catch { }
3546
+ // Extra hardening: normalize any remaining core references to the unified bridge
3547
+ // - Stray string-literals
3548
+ // - Dangling `from` merges
3549
+ // - Any spec (including /node_modules resolves) that still references '@nativescript/core'
3550
+ // Do this right before the final fast-fail assertion. If a rewrite occurred, add a small marker for diagnostics.
3551
+ try {
3552
+ const __before = code;
3553
+ code = normalizeStrayCoreStringLiterals(code);
3554
+ code = fixDanglingCoreFrom(code);
3555
+ code = normalizeAnyCoreSpecToBridge(code);
3556
+ if (code !== __before) {
3557
+ code = `// [hmr-sanitize] core-literal->bridge\n` + code;
3558
+ }
3559
+ }
3560
+ catch { }
3561
+ // Final pass: deduplicate/resolve any bare-specifier imports that slipped
3562
+ // through the pipeline (e.g., extracted from JSDoc comments by import-splitting
3563
+ // regexes, or injected by the Angular linker on already-resolved code).
3564
+ try {
3565
+ code = deduplicateLinkerImports(code);
3566
+ }
3567
+ catch { }
3568
+ // CJS/UMD wrapping: if a module uses module.exports but has no ESM export default,
3569
+ // wrap it with CJS shims so the device HTTP ESM loader can consume it.
3570
+ // This handles npm packages that use CommonJS but aren't pre-bundled by Vite.
3571
+ //
3572
+ // Key constraints this must handle:
3573
+ // - CJS modules often declare local vars with the same names as their exports
3574
+ // (e.g. `function createLTTB() {...}; exports.createLTTB = createLTTB;`)
3575
+ // so `export var { createLTTB }` would cause a duplicate declaration.
3576
+ // - UMD modules reference `this` at top level (undefined in ESM) but
3577
+ // typically fall back to `self` or `globalThis`.
3578
+ // - `module`, `exports` must be shims since they don't exist in ESM.
3579
+ try {
3580
+ code = wrapCommonJsModuleForDevice(code);
3581
+ }
3582
+ catch { }
3583
+ try {
3584
+ assertNoOptimizedArtifacts(code, `NS M ${resolvedCandidate || spec}`);
3094
3585
  }
3095
3586
  catch (e) {
3096
3587
  res.statusCode = 500;
3097
3588
  return void res.end(`throw new Error(${JSON.stringify(e?.message || String(e))});\nexport {};`);
3098
3589
  }
3590
+ // Defensive export normalization: if a module defines `routes` and only exports it named,
3591
+ // add a default export alias so both `import { routes }` and `import routes` work.
3592
+ try {
3593
+ if (!/\bexport\s+default\b/.test(code)) {
3594
+ const hasNamedRoutes = /\bexport\s*\{\s*routes\s*\}/.test(code);
3595
+ const hasConstRoutes = /\bconst\s+routes\s*=/.test(code) || /\bvar\s+routes\s*=/.test(code) || /\blet\s+routes\s*=/.test(code);
3596
+ if (hasNamedRoutes && hasConstRoutes) {
3597
+ code += `\nexport default routes;\n`;
3598
+ }
3599
+ }
3600
+ }
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.
3623
+ try {
3624
+ const verNum = 0;
3625
+ code = ensureVersionedRtImports(code, getServerOrigin(server), verNum);
3626
+ code = ACTIVE_STRATEGY.ensureVersionedImports(code, getServerOrigin(server), verNum);
3627
+ code = ensureVersionedCoreImports(code, getServerOrigin(server), verNum);
3628
+ }
3629
+ catch { }
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").
3649
+ try {
3650
+ const verTag = (() => {
3651
+ const numeric = getNumericServeVersionTag(forcedVer, Number(graphVersion || 0));
3652
+ return numeric > 0 ? `v${numeric}` : 'v0';
3653
+ })();
3654
+ const origin = getServerOrigin(server);
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.
3658
+ // 1) Static imports: import ... from "/ns/m/..."
3659
+ code = code.replace(/(from\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
3660
+ // 2) Side-effect imports: import "/ns/m/..."
3661
+ code = code.replace(/(import\s*(?!\()\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
3662
+ // 3) Dynamic imports: import("/ns/m/...")
3663
+ code = code.replace(/(import\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*\))/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
3664
+ // 4) new URL("/ns/m/...", import.meta.url)
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}`);
3666
+ // 5) __ns_import(new URL('/ns/m/...', import.meta.url).href)
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}`);
3668
+ // 6) Force absolute HTTP for new URL('/ns/m/...', import.meta.url).href → canonical stable URL.
3669
+ try {
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)}`)}`);
3671
+ }
3672
+ catch { }
3673
+ // 7) SFC URLs (Vue) — still versioned. See header comment.
3674
+ try {
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}`)}`);
3676
+ }
3677
+ catch { }
3678
+ }
3679
+ catch { }
3680
+ // Final guard: eliminate any lingering named imports from /ns/core to avoid
3681
+ // evaluation-time "does not provide an export named ..." in the device runtime.
3682
+ try {
3683
+ code = ensureDestructureCoreImports(code);
3684
+ }
3685
+ catch { }
3686
+ // Boot-time module graph progress: while the app is still replacing the
3687
+ // placeholder, emit lightweight progress updates as /ns/m modules begin
3688
+ // evaluating. This keeps the overlay moving during large initial graphs.
3689
+ try {
3690
+ if (bootTaggedRequest) {
3691
+ const bootModuleLabel = String(spec || '').replace(/\\/g, '/');
3692
+ const bootProgressSnippet = buildBootProgressSnippet(bootModuleLabel);
3693
+ code = bootProgressSnippet + code;
3694
+ code = hoistTopLevelStaticImports(code);
3695
+ }
3696
+ }
3697
+ catch { }
3099
3698
  // Dev-only: link-check static imports to surface missing bindings early
3100
3699
  try {
3101
3700
  const devCheck = process.env.NODE_ENV !== 'production';
@@ -3224,147 +3823,1912 @@ export const piniaSymbol = p.piniaSymbol;
3224
3823
  res.end('export {}\n');
3225
3824
  }
3226
3825
  });
3227
- registerRuntimeCompatHandlers(server, {
3228
- verbose,
3229
- requireGuardSnippet: REQUIRE_GUARD_SNIPPET,
3230
- appRootDir: APP_ROOT_DIR,
3231
- defaultMainEntry: DEFAULT_MAIN_ENTRY,
3232
- defaultMainEntryVirtual: DEFAULT_MAIN_ENTRY_VIRTUAL,
3233
- getGraphVersion: () => Number(graphVersion || 0),
3234
- getServerOrigin,
3826
+ // 2.5) ESM runtime bridge for NativeScript-Vue: GET /ns/rt
3827
+ // Provides a single authoritative source of Vue helpers bound to the NativeScript renderer.
3828
+ // V2.1: Lazy ensure bridge — does not statically import vue. It lazily resolves helpers from
3829
+ // globalThis or vendor registry/require on first evaluation, then exports references so SFCs
3830
+ // can immediately call them during module evaluation.
3831
+ server.middlewares.use(async (req, res, next) => {
3832
+ try {
3833
+ const urlObj = new URL(req.url || '', 'http://localhost');
3834
+ // Accept only /ns/rt and /ns/rt/<ver> for cache-busting semantics
3835
+ if (!(urlObj.pathname === '/ns/rt' || /^\/ns\/rt\/[\d]+$/.test(urlObj.pathname)))
3836
+ return next();
3837
+ res.setHeader('Access-Control-Allow-Origin', '*');
3838
+ res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
3839
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
3840
+ res.setHeader('Pragma', 'no-cache');
3841
+ res.setHeader('Expires', '0');
3842
+ const rtVerSeg = urlObj.pathname.replace(/^\/ns\/rt\/?/, '');
3843
+ const rtVer = /^[0-9]+$/.test(rtVerSeg) ? rtVerSeg : String(graphVersion || 0);
3844
+ const origin = getServerOrigin(server);
3845
+ let code = `// [ns-rt][v2.3] NativeScript-Vue runtime bridge (module-scoped cache, no globals)\n` +
3846
+ `const __origin = ((typeof globalThis !== 'undefined' && globalThis && globalThis.__NS_HTTP_ORIGIN__) || (new URL(import.meta.url)).origin);\n` +
3847
+ `let __ns_core_bridge = null; try { import(__origin + "/ns/core/${rtVer}").then(m => { __ns_core_bridge = m; }).catch(() => {}); } catch {}\n` +
3848
+ `const g = globalThis;\n` +
3849
+ `const reg = (g.__nsVendorRegistry ||= new Map());\n` +
3850
+ `const req = reg && reg.get ? (g.__nsVendorRequire || g.__nsRequire || g.require) : (g.__nsRequire || g.require);\n` +
3851
+ `let __cached_rt = null;\n` +
3852
+ `let __cached_vm = null;\n` +
3853
+ `const __RT_REALM_TAG = (globalThis.__NS_RT_REALM__ ||= Math.random().toString(36).slice(2));\n` +
3854
+ // Unconditional one-shot evaluation marker to confirm bridge is executed on device
3855
+ `try { if (!(globalThis.__NS_RT_ONCE__ && globalThis.__NS_RT_ONCE__.eval)) { (globalThis.__NS_RT_ONCE__ ||= {}).eval = true; console.log('[ns-rt] evaluated', { rtRealm: __RT_REALM_TAG }); } } catch {}\n` +
3856
+ `function __ensure(){\n` +
3857
+ ` if (__cached_rt) return __cached_rt;\n` +
3858
+ ` let vm = null;\n` +
3859
+ ` try { vm = reg && reg.has && reg.has('nativescript-vue') ? reg.get('nativescript-vue') : (typeof req==='function' ? req('nativescript-vue') : null); } catch {}\n` +
3860
+ ` if (!vm) { try { vm = reg && reg.has && reg.has('vue') ? reg.get('vue') : (typeof req==='function' ? req('vue') : null); } catch {} }\n` +
3861
+ ` const rt = (vm && (vm.default ?? vm)) || {};\n` +
3862
+ ` __cached_vm = vm;\n` +
3863
+ ` __cached_rt = rt;\n` +
3864
+ ` return rt;\n` +
3865
+ `}\n` +
3866
+ `// Soft-globals for @nativescript/core when missing (dev-only safety)\n` +
3867
+ `try {\n` +
3868
+ ` const dev = typeof __DEV__ !== 'undefined' ? __DEV__ : true;\n` +
3869
+ ` if (dev) {\n` +
3870
+ ` const ns = (__ns_core_bridge && (__ns_core_bridge.__esModule && __ns_core_bridge.default ? __ns_core_bridge.default : (__ns_core_bridge.default || __ns_core_bridge))) || __ns_core_bridge || {};\n` +
3871
+ ` if (ns) {\n` +
3872
+ ` if (!g.Frame && ns.Frame) g.Frame = ns.Frame;\n` +
3873
+ ` if (!g.Page && ns.Page) g.Page = ns.Page;\n` +
3874
+ ` if (!g.Application && (ns.Application||ns.app||ns.application)) g.Application = (ns.Application||ns.app||ns.application);\n` +
3875
+ ` }\n` +
3876
+ ` }\n` +
3877
+ `} catch {}\n` +
3878
+ `const __get = (k) => { const rt = __ensure(); const v = rt && rt[k]; if (typeof v !== 'function' && v === undefined) { throw new Error('[ns-rt] missing export '+k); } return v; };\n` +
3879
+ `export const __realm = __RT_REALM_TAG;\n` +
3880
+ `export const defineComponent = (...a) => (__get('defineComponent'))(...a);\n` +
3881
+ `export const resolveComponent = (...a) => (__ensure().resolveComponent)(...a);\n` +
3882
+ `export const createVNode = (...a) => (__ensure().createVNode)(...a);\n` +
3883
+ `export const createTextVNode = (...a) => (__ensure().createTextVNode)(...a);\n` +
3884
+ `export const createCommentVNode = (...a) => (__ensure().createCommentVNode)(...a);\n` +
3885
+ `export const Fragment = (__ensure().Fragment);\n` +
3886
+ `export const Teleport = (__ensure().Teleport);\n` +
3887
+ `export const Transition = (__ensure().Transition);\n` +
3888
+ `export const TransitionGroup = (__ensure().TransitionGroup);\n` +
3889
+ `export const KeepAlive = (__ensure().KeepAlive);\n` +
3890
+ `export const Suspense = (__ensure().Suspense);\n` +
3891
+ `export const withCtx = (...a) => (__ensure().withCtx)(...a);\n` +
3892
+ `export const openBlock = (...a) => (__ensure().openBlock)(...a);\n` +
3893
+ `export const createBlock = (...a) => (__ensure().createBlock)(...a);\n` +
3894
+ `export const createElementVNode = (...a) => (__ensure().createElementVNode)(...a);\n` +
3895
+ `export const createElementBlock = (...a) => (__ensure().createElementBlock)(...a);\n` +
3896
+ `export const renderSlot = (...a) => (__ensure().renderSlot)(...a);\n` +
3897
+ `export const mergeProps = (...a) => (__ensure().mergeProps)(...a);\n` +
3898
+ `export const toHandlers = (...a) => (__ensure().toHandlers)(...a);\n` +
3899
+ `export const renderList = (...a) => (__ensure().renderList)(...a);\n` +
3900
+ `export const normalizeProps = (...a) => (__ensure().normalizeProps)(...a);\n` +
3901
+ `export const guardReactiveProps = (...a) => (__ensure().guardReactiveProps)(...a);\n` +
3902
+ `export const normalizeClass = (...a) => (__ensure().normalizeClass)(...a);\n` +
3903
+ `export const normalizeStyle = (...a) => (__ensure().normalizeStyle)(...a);\n` +
3904
+ `export const toDisplayString = (...a) => (__ensure().toDisplayString)(...a);\n` +
3905
+ `export const withDirectives = (...a) => (__ensure().withDirectives)(...a);\n` +
3906
+ `export const resolveDirective = (...a) => (__ensure().resolveDirective)(...a);\n` +
3907
+ `export const withModifiers = (...a) => (__ensure().withModifiers)(...a);\n` +
3908
+ `export const withKeys = (...a) => (__ensure().withKeys)(...a);\n` +
3909
+ `export const resolveDynamicComponent = (...a) => (__ensure().resolveDynamicComponent)(...a);\n` +
3910
+ `export const isVNode = (...a) => (__ensure().isVNode)(...a);\n` +
3911
+ `export const cloneVNode = (...a) => (__ensure().cloneVNode)(...a);\n` +
3912
+ `export const isRef = (...a) => (__ensure().isRef)(...a);\n` +
3913
+ `export const ref = (...a) => (__ensure().ref)(...a);\n` +
3914
+ `export const shallowRef = (...a) => (__ensure().shallowRef)(...a);\n` +
3915
+ `export const unref = (...a) => (__ensure().unref)(...a);\n` +
3916
+ `export const computed = (...a) => (__ensure().computed)(...a);\n` +
3917
+ `export const reactive = (...a) => (__ensure().reactive)(...a);\n` +
3918
+ `export const readonly = (...a) => (__ensure().readonly)(...a);\n` +
3919
+ `export const isReactive = (...a) => (__ensure().isReactive)(...a);\n` +
3920
+ `export const isReadonly = (...a) => (__ensure().isReadonly)(...a);\n` +
3921
+ `export const toRaw = (...a) => (__ensure().toRaw)(...a);\n` +
3922
+ `export const markRaw = (...a) => (__ensure().markRaw)(...a);\n` +
3923
+ `export const shallowReactive = (...a) => (__ensure().shallowReactive)(...a);\n` +
3924
+ `export const shallowReadonly = (...a) => (__ensure().shallowReadonly)(...a);\n` +
3925
+ `export const watch = (...a) => (__ensure().watch)(...a);\n` +
3926
+ `export const watchEffect = (...a) => (__ensure().watchEffect)(...a);\n` +
3927
+ `export const watchPostEffect = (...a) => (__ensure().watchPostEffect)(...a);\n` +
3928
+ `export const watchSyncEffect = (...a) => (__ensure().watchSyncEffect)(...a);\n` +
3929
+ `export const onBeforeMount = (...a) => (__ensure().onBeforeMount)(...a);\n` +
3930
+ `export const onMounted = (...a) => (__ensure().onMounted)(...a);\n` +
3931
+ `export const onBeforeUpdate = (...a) => (__ensure().onBeforeUpdate)(...a);\n` +
3932
+ `export const onUpdated = (...a) => (__ensure().onUpdated)(...a);\n` +
3933
+ `export const onBeforeUnmount = (...a) => (__ensure().onBeforeUnmount)(...a);\n` +
3934
+ `export const onUnmounted = (...a) => (__ensure().onUnmounted)(...a);\n` +
3935
+ `export const onActivated = (...a) => (__ensure().onActivated)(...a);\n` +
3936
+ `export const onDeactivated = (...a) => (__ensure().onDeactivated)(...a);\n` +
3937
+ `export const onErrorCaptured = (...a) => (__ensure().onErrorCaptured)(...a);\n` +
3938
+ `export const onRenderTracked = (...a) => (__ensure().onRenderTracked)(...a);\n` +
3939
+ `export const onRenderTriggered = (...a) => (__ensure().onRenderTriggered)(...a);\n` +
3940
+ `export const nextTick = (...a) => (__ensure().nextTick)(...a);\n` +
3941
+ `export const h = (...a) => (__ensure().h)(...a);\n` +
3942
+ `export const provide = (...a) => (__ensure().provide)(...a);\n` +
3943
+ `export const inject = (...a) => (__ensure().inject)(...a);\n` +
3944
+ `export const vShow = (__ensure().vShow);\n` +
3945
+ `export const createApp = (...a) => (__ensure().createApp)(...a);\n` +
3946
+ `export const registerElement = (...a) => (__ensure().registerElement)(...a);\n` +
3947
+ `export const $navigateTo = (...a) => { const vm = (__cached_vm || (void __ensure(), __cached_vm)); const rt = __ensure(); try { if (!(g && g.Frame)) { const ns = (__ns_core_bridge && (__ns_core_bridge.__esModule && __ns_core_bridge.default ? __ns_core_bridge.default : (__ns_core_bridge.default || __ns_core_bridge))) || __ns_core_bridge || {}; if (ns) { if (!g.Frame && ns.Frame) g.Frame = ns.Frame; if (!g.Page && ns.Page) g.Page = ns.Page; if (!g.Application && (ns.Application||ns.app||ns.application)) g.Application = (ns.Application||ns.app||ns.application); } } } catch {} try { const hmrRealm = (g && g.__NS_HMR_REALM__) || 'unknown'; const hasTop = !!(g && g.Frame && g.Frame.topmost && g.Frame.topmost()); const top = hasTop ? g.Frame.topmost() : null; const ctor = top && top.constructor && top.constructor.name; } catch {} if (g && typeof g.__nsNavigateUsingApp === 'function') { try { return g.__nsNavigateUsingApp(...a); } catch (e) { try { console.error('[ns-rt] $navigateTo app navigator error', e); } catch {} throw e; } } try { console.error('[ns-rt] $navigateTo unavailable: app navigator missing'); } catch {} throw new Error('$navigateTo unavailable: app navigator missing'); } ;\n` +
3948
+ `export const $navigateBack = (...a) => { const vm = (__cached_vm || (void __ensure(), __cached_vm)); const rt = __ensure(); const impl = (vm && (vm.$navigateBack || (vm.default && vm.default.$navigateBack))) || (rt && (rt.$navigateBack || (rt.runtimeHelpers && rt.runtimeHelpers.navigateBack))); let res; try { const via = (impl && (impl === (vm && vm.$navigateBack) || impl === (vm && vm.default && vm.default.$navigateBack))) ? 'vm' : (impl ? 'rt' : 'none'); } catch {} try { if (typeof impl === 'function') res = impl(...a); } catch {} try { const top = (g && g.Frame && g.Frame.topmost && g.Frame.topmost()); if (!res && top && top.canGoBack && top.canGoBack()) { res = top.goBack(); } } catch {} try { const hook = g && (g.__NS_HMR_ON_NAVIGATE_BACK || g.__NS_HMR_ON_BACK || g.__nsAttemptBackRemount); if (typeof hook === 'function') hook(); } catch {} return res; }\n` +
3949
+ `export const $showModal = (...a) => { const vm = (__cached_vm || (void __ensure(), __cached_vm)); const rt = __ensure(); const impl = (vm && (vm.$showModal || (vm.default && vm.default.$showModal))) || (rt && (rt.$showModal || (rt.runtimeHelpers && rt.runtimeHelpers.showModal))); try { if (typeof impl === 'function') return impl(...a); } catch (e) { } return undefined; }\n` +
3950
+ `export default {\n` +
3951
+ ` defineComponent, resolveComponent, createVNode, createTextVNode, createCommentVNode,\n` +
3952
+ ` Fragment, Teleport, Transition, TransitionGroup, KeepAlive, Suspense, withCtx, openBlock,\n` +
3953
+ ` createBlock, createElementVNode, createElementBlock, renderSlot, mergeProps, toHandlers,\n` +
3954
+ ` renderList, normalizeProps, guardReactiveProps, normalizeClass, normalizeStyle, toDisplayString,\n` +
3955
+ ` withDirectives, resolveDirective, withModifiers, withKeys, resolveDynamicComponent,\n` +
3956
+ ` isVNode, cloneVNode, isRef, ref, shallowRef, unref, computed, reactive, readonly, isReactive, isReadonly, toRaw, markRaw, shallowReactive, shallowReadonly,\n` +
3957
+ ` watch, watchEffect, watchPostEffect, watchSyncEffect, onBeforeMount, onMounted, onBeforeUpdate, onUpdated,\n` +
3958
+ ` onBeforeUnmount, onUnmounted, onActivated, onDeactivated, onErrorCaptured, onRenderTracked, onRenderTriggered, nextTick, h, provide, inject, vShow, createApp, registerElement,\n` +
3959
+ ` $navigateTo, $navigateBack, $showModal\n` +
3960
+ `};\n`;
3961
+ // Prepend guard and ship (harmless, keeps diagnostics consistent)
3962
+ code = REQUIRE_GUARD_SNIPPET + code;
3963
+ res.statusCode = 200;
3964
+ res.end(code);
3965
+ }
3966
+ catch (e) {
3967
+ res.statusCode = 500;
3968
+ res.end('export {}\n');
3969
+ }
3235
3970
  });
3236
3971
  // 2.55) Dev-only vendor import unifier: rewrite 'vue'/'nativescript-vue' to /ns/rt/<ver>
3237
3972
  // This ensures plugins and app share a single Vue/NativeScript-Vue instance/realm.
3238
- registerVendorUnifierHandler(server, {
3239
- getGraphVersion: () => Number(graphVersion || 0),
3240
- getServerOrigin,
3241
- getStrategy: () => ACTIVE_STRATEGY,
3242
- });
3243
- // 2.6) ESM bridge for @nativescript/core: GET /ns/core[/<ver>][?p=sub/path]
3244
3973
  server.middlewares.use(async (req, res, next) => {
3245
3974
  try {
3246
3975
  const urlObj = new URL(req.url || '', 'http://localhost');
3247
- const coreRequest = parseCoreBridgeRequest(urlObj.pathname, urlObj.searchParams, Number(graphVersion || 0));
3248
- if (!coreRequest)
3976
+ const p = urlObj.pathname || '';
3977
+ // Ignore our own core/rt bridge endpoints and non-JS assets, but DO allow /ns/m/* through
3978
+ if (/^\/ns\/(?:rt|core)(?:\/|$)/.test(p))
3979
+ return next();
3980
+ if (!/(\.m?js$|\.ts$|\/node_modules\/|\/\.vite\/deps\/|^\/@id\/|^\/@fs\/)/.test(p))
3981
+ return next();
3982
+ if (/\.css($|\?)/.test(p))
3983
+ return next();
3984
+ const reqUrl = req.url || '';
3985
+ const transformed = await server.transformRequest(reqUrl);
3986
+ if (!transformed?.code)
3987
+ return next();
3988
+ const origin = getServerOrigin(server);
3989
+ const ver = Number(graphVersion || 0);
3990
+ const rewrite = ACTIVE_STRATEGY.rewriteVendorSpec;
3991
+ if (!rewrite)
3992
+ return next();
3993
+ const before = transformed.code;
3994
+ const code = rewrite(before, origin, ver);
3995
+ if (code === before)
3249
3996
  return next();
3250
3997
  res.setHeader('Access-Control-Allow-Origin', '*');
3251
3998
  res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
3252
3999
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
3253
4000
  res.setHeader('Pragma', 'no-cache');
3254
4001
  res.setHeader('Expires', '0');
3255
- const { hasExplicitVersion, key, normalizedSub, sub, ver } = coreRequest;
3256
- // Any @nativescript/core subpath import (including shallow ones like
3257
- // `utils`) may expose exports that are not available from the root
3258
- // vendor bundle namespace. Serve the actual transformed module content
3259
- // instead of the lightweight proxy bridge.
3260
- if (sub) {
3261
- try {
3262
- const resolvedSubpath = normalizedSub || sub;
3263
- const projectRoot = server.config?.root || process.cwd();
3264
- const resolveModuleId = async (moduleId) => {
3265
- const resolved = await server.pluginContainer?.resolveId?.(moduleId, undefined);
3266
- return typeof resolved === 'string' ? resolved : resolved?.id || null;
3267
- };
3268
- const resolvedId = await resolveRuntimeCoreModulePath(resolvedSubpath, resolveModuleId);
3269
- const modulePath = resolvedId || `/node_modules/@nativescript/core/${resolvedSubpath}`;
3270
- const transformed = await sharedTransformRequest(modulePath);
3271
- if (!hasExplicitVersion) {
3272
- if (transformed?.code) {
3273
- const expandedModuleCode = await expandStarExports(transformed.code, server, projectRoot, verbose);
3274
- res.statusCode = 200;
3275
- res.end(buildVersionedCoreSubpathAliasModule(resolvedSubpath, ver, extractExportedNames(expandedModuleCode), hasModuleDefaultExport(expandedModuleCode)));
3276
- return;
4002
+ res.statusCode = 200;
4003
+ res.end(code);
4004
+ }
4005
+ catch {
4006
+ return next();
4007
+ }
4008
+ });
4009
+ // 2.5.1) Catch-all redirect for stray /node_modules/@nativescript/core/*
4010
+ // requests route them to the /ns/core bridge so they get the same
4011
+ // __DEV__/__IOS__ preamble and specifier rewriting. Without this,
4012
+ // Vite's default /node_modules/ handler serves the raw file, which
4013
+ // references bare __DEV__ and crashes at module eval.
4014
+ server.middlewares.use((req, _res, next) => {
4015
+ try {
4016
+ const urlObj = new URL(req.url || '', 'http://localhost');
4017
+ const coreNmPrefix = '/node_modules/@nativescript/core';
4018
+ if (!urlObj.pathname.startsWith(coreNmPrefix))
4019
+ return next();
4020
+ const sub = urlObj.pathname.slice(coreNmPrefix.length).replace(/^\/+/, '');
4021
+ if (sub === '' || sub === 'index.js' || sub === 'index') {
4022
+ req.url = `/ns/core`;
4023
+ }
4024
+ else {
4025
+ req.url = `/ns/core/${sub}`;
4026
+ }
4027
+ return next();
4028
+ }
4029
+ catch {
4030
+ return next();
4031
+ }
4032
+ });
4033
+ // 2.6) ESM bridge for @nativescript/core: GET /ns/core[/<ver>][?p=sub/path]
4034
+ //
4035
+ // Since bundle.mjs no longer bundles @nativescript/core (see
4036
+ // HMR_CORE_REALM_DETERMINISTIC_PLAN.md — external in the rolldown
4037
+ // config under HMR), this endpoint is the ONE place core is
4038
+ // evaluated. Every consumer — bundle.mjs's own `@nativescript/core*`
4039
+ // imports (resolved to full HTTP URLs in the entry virtual module),
4040
+ // externalized vendor packages, HTTP-served app modules — all end
4041
+ // up here. No more proxy bridge, no enumeration, no namespace
4042
+ // detection, no prototype-polluted maps. We just serve Vite's
4043
+ // authoritative transformed module content.
4044
+ //
4045
+ // iOS caches by URL path, so each unique URL is evaluated exactly
4046
+ // once per app lifetime. Every class identity is shared, every
4047
+ // `register()` side effect runs once, every `Application` reference
4048
+ // is the same iosApp singleton. The entire class of "does not
4049
+ // provide an export named X" and "Cannot redefine property" errors
4050
+ // is eliminated by construction.
4051
+ server.middlewares.use(async (req, res, next) => {
4052
+ try {
4053
+ const urlObj = new URL(req.url || '', 'http://localhost');
4054
+ const coreRequest = parseCoreBridgeRequest(urlObj.pathname, urlObj.searchParams, Number(graphVersion || 0));
4055
+ if (!coreRequest)
4056
+ return next();
4057
+ res.setHeader('Access-Control-Allow-Origin', '*');
4058
+ res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
4059
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
4060
+ res.setHeader('Pragma', 'no-cache');
4061
+ res.setHeader('Expires', '0');
4062
+ const { normalizedSub, sub, ver } = coreRequest;
4063
+ const resolveModuleId = async (moduleId) => {
4064
+ const resolved = await server.pluginContainer?.resolveId?.(moduleId, undefined);
4065
+ return typeof resolved === 'string' ? resolved : resolved?.id || null;
4066
+ };
4067
+ let modulePath = null;
4068
+ if (sub) {
4069
+ const resolvedSubpath = normalizedSub || sub;
4070
+ modulePath = await resolveRuntimeCoreModulePath(resolvedSubpath, resolveModuleId);
4071
+ if (!modulePath) {
4072
+ modulePath = `/node_modules/@nativescript/core/${resolvedSubpath}`;
4073
+ }
4074
+ }
4075
+ else {
4076
+ modulePath = (await resolveModuleId('@nativescript/core')) || '/node_modules/@nativescript/core/index.js';
4077
+ }
4078
+ const transformed = await sharedTransformRequest(modulePath);
4079
+ if (!transformed?.code) {
4080
+ res.statusCode = 500;
4081
+ res.setHeader('Content-Type', 'application/json');
4082
+ res.end(JSON.stringify({ error: 'core-transform-failed', modulePath, sub: sub || null }));
4083
+ return;
4084
+ }
4085
+ // Vite's transform output references module IDs with /@fs,
4086
+ // relative specifiers, or absolute project paths. Rewrite
4087
+ // those to URLs iOS can fetch over HTTP.
4088
+ let rewritten = rewriteSpecifiersForDevice(transformed.code, getServerOrigin(server), Number(ver));
4089
+ // Invariant D (CJS/ESM interop shape) — EXPORT-SIDE fix.
4090
+ //
4091
+ // `@nativescript/core/index.js` declares namespace
4092
+ // re-exports like:
4093
+ // export * as Utils from './utils';
4094
+ // The ES spec says these produce Module Namespace Objects
4095
+ // with [[Prototype]] = null. Consumers that reach them
4096
+ // via direct ESM import — `import { Utils } from
4097
+ // '@nativescript/core'` — get the raw null-proto value,
4098
+ // bypassing any CJS `require` shim we install. Most
4099
+ // consumers tolerate this, but CJS-style interop (most
4100
+ // notably zone.js's `patchMethod`) calls
4101
+ // `hasOwnProperty` on the target and crashes on
4102
+ // null-proto.
4103
+ //
4104
+ // We rewrite the re-export to a shape-wrapped const:
4105
+ // import * as __ns_re_Utils__ from './utils';
4106
+ // export const Utils = __NS_CJS_SHAPE__(__ns_re_Utils__);
4107
+ // so the EXPORT itself is a plain object — visible to
4108
+ // both ESM and CJS consumers consistently.
4109
+ //
4110
+ // We only pay the rewrite cost when the module actually
4111
+ // contains namespace re-exports (i.e., the main
4112
+ // `index.js`). Subpaths (`/utils`, `/http`, …) don't
4113
+ // re-export via `export * as`; they expose named
4114
+ // exports directly, so the rewrite is a no-op on them.
4115
+ if (hasNamespaceReExport(rewritten)) {
4116
+ rewritten = rewriteNamespaceReExportsForShape(rewritten);
4117
+ }
4118
+ // Prepend the build-time defines (__DEV__, __IOS__, __ANDROID__,
4119
+ // __APPLE__, …) that @nativescript/core source references directly.
4120
+ // Vite's `define` config substitutes these in user-code transforms but
4121
+ // skips node_modules by default; since core is now external and served
4122
+ // over HTTP from this endpoint, the served transformed code still has
4123
+ // bare identifiers like `if (__DEV__) …`. Without these consts, V8
4124
+ // hits `ReferenceError: __DEV__ is not defined` at module eval because
4125
+ // globalThis.__DEV__ is set by bundle.mjs's body AFTER all static
4126
+ // imports (including these core modules) have resolved.
4127
+ //
4128
+ // We inject LITERAL boolean values based on CLI flags + dev-server
4129
+ // mode rather than reading from globalThis, so the defines are
4130
+ // resolved even before bundle.mjs's body runs.
4131
+ const __cliFlags = getCliFlags() || {};
4132
+ const __platformIsAndroid = !!__cliFlags.android;
4133
+ const __platformIsVisionOS = !!__cliFlags.visionos;
4134
+ const __platformIsIOS = !__platformIsAndroid && !__platformIsVisionOS;
4135
+ const preamble = [
4136
+ `const __ANDROID__ = ${__platformIsAndroid ? 'true' : 'false'};`,
4137
+ `const __IOS__ = ${__platformIsIOS ? 'true' : 'false'};`,
4138
+ `const __VISIONOS__ = ${__platformIsVisionOS ? 'true' : 'false'};`,
4139
+ `const __APPLE__ = __IOS__ || __VISIONOS__;`,
4140
+ `const __DEV__ = ${server.config?.mode === 'development' ? 'true' : 'false'};`,
4141
+ `const __COMMONJS__ = false;`,
4142
+ `const __NS_WEBPACK__ = false;`,
4143
+ `const __NS_ENV_VERBOSE__ = globalThis.__NS_ENV_VERBOSE__ !== undefined ? !!globalThis.__NS_ENV_VERBOSE__ : false;`,
4144
+ `const __CSS_PARSER__ = 'css-tree';`,
4145
+ `const __UI_USE_XML_PARSER__ = true;`,
4146
+ `const __UI_USE_EXTERNAL_RENDERER__ = false;`,
4147
+ `const __TEST__ = false;`,
4148
+ ].join('\n');
4149
+ // Boot-time instrumentation + module self-registration.
4150
+ // See HMR_CORE_REALM_DETERMINISTIC_PLAN.md:
4151
+ // - Invariant A (URL canonicalization): the same
4152
+ // logical module must always resolve to byte-
4153
+ // identical URLs across every emitter. The /ns/core
4154
+ // handler records the first URL seen for each
4155
+ // canonical sub (or '' for main) in
4156
+ // `globalThis.__NS_CORE_FIRST_URL__` and fails hard
4157
+ // on mismatch so drift in any emitter surfaces
4158
+ // immediately, before the realm splits.
4159
+ // - Invariant C (boot-order): CommonJS
4160
+ // `require('@nativescript/core/...')` calls from
4161
+ // vendor install() hooks must resolve to the SAME
4162
+ // ESM namespace that ran this side-effect preamble.
4163
+ // The registration below keys the namespace object
4164
+ // under BOTH the bare specifier and the canonical
4165
+ // subpath (and raw subpath for back-compat) so the
4166
+ // vendor shim's `createRequire` and the main-entry
4167
+ // `_nsReq` hit on any lookup form.
4168
+ const rawSub = normalizedSub || sub || '';
4169
+ const canonicalSub = normalizeCoreSubCanonical(rawSub);
4170
+ const registrationKeySet = new Set();
4171
+ registrationKeySet.add(canonicalSub ? `@nativescript/core/${canonicalSub}` : '@nativescript/core');
4172
+ registrationKeySet.add(canonicalSub);
4173
+ if (rawSub && rawSub !== canonicalSub) {
4174
+ registrationKeySet.add(`@nativescript/core/${rawSub}`);
4175
+ registrationKeySet.add(rawSub);
4176
+ }
4177
+ const registrationKeys = Array.from(registrationKeySet).map((k) => JSON.stringify(k));
4178
+ const canonicalUrl = `${getServerOrigin(server)}` + (canonicalSub ? `/ns/core/${canonicalSub}` : '/ns/core');
4179
+ const instrumentationHeader = [
4180
+ `/* @nativescript/core bridge — canonical URL: ${canonicalUrl} */`,
4181
+ `try { if (typeof globalThis !== 'undefined') {`,
4182
+ ` const __nsFirst = globalThis.__NS_CORE_FIRST_URL__ || (globalThis.__NS_CORE_FIRST_URL__ = Object.create(null));`,
4183
+ ` const __nsSeen = globalThis.__NS_CORE_FETCHED_URLS__ || (globalThis.__NS_CORE_FETCHED_URLS__ = []);`,
4184
+ ` const __nsKey = ${JSON.stringify(canonicalSub)};`,
4185
+ ` const __nsUrl = ${JSON.stringify(canonicalUrl)};`,
4186
+ ` __nsSeen.push(__nsUrl);`,
4187
+ ` if (typeof __nsFirst[__nsKey] === 'string' && __nsFirst[__nsKey] !== __nsUrl) {`,
4188
+ ` throw new Error('[ns-core] URL drift for sub=' + __nsKey + ': first=' + __nsFirst[__nsKey] + ' now=' + __nsUrl + ' (see HMR_CORE_REALM_DETERMINISTIC_PLAN.md Invariant A)');`,
4189
+ ` }`,
4190
+ ` if (!__nsFirst[__nsKey]) __nsFirst[__nsKey] = __nsUrl;`,
4191
+ ` globalThis.__NS_CORE_EVAL_COUNT__ = (globalThis.__NS_CORE_EVAL_COUNT__ || 0) + 1;`,
4192
+ `} } catch (e) { try { console.warn('[ns-core] instrumentation failed:', (e && e.message) || e); } catch {} }`,
4193
+ ].join('\n');
4194
+ // Invariant D (CJS/ESM interop shape) — REGISTRATION side.
4195
+ //
4196
+ // The actual shape installer runs earlier in the module
4197
+ // body (between preamble and selfImport; see
4198
+ // buildShapeInstallHeader). At this point we just read
4199
+ // globalThis.__NS_CJS_SHAPE__ and apply it to the self
4200
+ // namespace before registering under the CJS key space.
4201
+ //
4202
+ // Why shape self at registration: consumers that reach
4203
+ // `@nativescript/core` via `require()` (legacy vendors,
4204
+ // `globalThis.require` shim) look up the registry. They
4205
+ // expect a plain object (Object.prototype in chain) so
4206
+ // `.hasOwnProperty` / `.toString` work. Shaping once on
4207
+ // registration — the shape function is identity-preserving
4208
+ // via WeakMap — gives a stable, shared, CJS-compatible
4209
+ // view without copying on every require.
4210
+ //
4211
+ // See HMR_CORE_REALM_DETERMINISTIC_PLAN.md § "Invariant D"
4212
+ // for the full rationale.
4213
+ const registrationFooter = [
4214
+ `try { if (typeof globalThis !== 'undefined') {`,
4215
+ ` const __nsReg = globalThis.__NS_CORE_MODULES__ || (globalThis.__NS_CORE_MODULES__ = Object.create(null));`,
4216
+ ` const __nsShapeFn = typeof globalThis.__NS_CJS_SHAPE__ === 'function' ? globalThis.__NS_CJS_SHAPE__ : function (x) { return x; };`,
4217
+ ` const __nsSelfRaw = (typeof __ns_core_self_ns__ !== 'undefined') ? __ns_core_self_ns__ : { default: undefined };`,
4218
+ ` const __nsSelf = __nsShapeFn(__nsSelfRaw);`,
4219
+ ...registrationKeys.map((k) => ` __nsReg[${k}] = __nsSelf;`),
4220
+ `} } catch (e) { try { console.warn('[ns-core] self-register failed:', (e && e.message) || e); } catch {} }`,
4221
+ ].join('\n');
4222
+ // Bind `import * as __ns_core_self_ns__` to the module's
4223
+ // own export namespace so the footer can stash it into
4224
+ // the registry. Self-import is a no-op at eval time —
4225
+ // V8 resolves it to the module record we're already
4226
+ // evaluating and the final namespace is the same object
4227
+ // the registry receives. We use the CANONICAL URL here
4228
+ // so the self-import participates in Invariant A along
4229
+ // with every other @nativescript/core URL.
4230
+ const canonicalUrlForSelf = canonicalSub ? `/ns/core/${canonicalSub}` : '/ns/core';
4231
+ const selfImport = `import * as __ns_core_self_ns__ from ${JSON.stringify(canonicalUrlForSelf)};`;
4232
+ // Invariant D — SHAPE INSTALLER.
4233
+ //
4234
+ // Emits idempotent body-code that installs
4235
+ // globalThis.__NS_CJS_SHAPE__ BEFORE `rewritten`'s body
4236
+ // runs. This matters because the rewrite step above may
4237
+ // have produced statements like
4238
+ // `export const Utils = (typeof globalThis.__NS_CJS_SHAPE__ ...)(__ns_re_Utils__);`
4239
+ // that execute during module evaluation. Without the
4240
+ // installer running first, the ternary falls back to
4241
+ // identity — still safe, but the null-proto namespace
4242
+ // leaks through and consumers that expect a plain
4243
+ // object would still crash.
4244
+ //
4245
+ // Placement is important: BEFORE selfImport in the
4246
+ // concatenation. ESM imports are hoisted regardless of
4247
+ // textual position, but body code executes in source
4248
+ // order. Placing the installer first guarantees it
4249
+ // runs before any body statement in `rewritten`.
4250
+ //
4251
+ // Install is idempotent: `|| (globalThis.X = ...)` so
4252
+ // whichever /ns/core module evaluates first wins and
4253
+ // every subsequent module becomes a no-op.
4254
+ const shapeInstallHeader = buildShapeInstallHeader();
4255
+ // Invariant D — DEFAULT EXPORT BRIDGE.
4256
+ //
4257
+ // See `buildDefaultExportFooter` in ns-core-cjs-shape.ts
4258
+ // for the full rationale (consumer matrix, skip conditions,
4259
+ // why the default isn't shaped). The short version:
4260
+ // upstream rewrites turn `import { X } from '@nativescript/core'`
4261
+ // into a DEFAULT import, and the bridge has to provide one.
4262
+ const defaultExportFooter = buildDefaultExportFooter(rewritten);
4263
+ const moduleCode = [instrumentationHeader, preamble, shapeInstallHeader, selfImport, rewritten, defaultExportFooter, registrationFooter].join('\n');
4264
+ res.statusCode = 200;
4265
+ res.end(moduleCode);
4266
+ }
4267
+ catch (e) {
4268
+ try {
4269
+ console.warn('[ns-core-bridge] serve failed:', e?.message);
4270
+ }
4271
+ catch { }
4272
+ next();
4273
+ }
4274
+ });
4275
+ // 2.6a) Serve compiled entry runtime module: GET /ns/entry-rt[?v=<ver>]
4276
+ server.middlewares.use(async (req, res, next) => {
4277
+ try {
4278
+ const urlObj = new URL(req.url || '', 'http://localhost');
4279
+ if (!(urlObj.pathname === '/ns/entry-rt'))
4280
+ return next();
4281
+ try {
4282
+ if (verbose) {
4283
+ const ra = req.socket?.remoteAddress;
4284
+ const rp = req.socket?.remotePort;
4285
+ console.log('[hmr-http] GET /ns/entry-rt from', ra + (rp ? ':' + rp : ''));
4286
+ }
4287
+ }
4288
+ catch { }
4289
+ res.setHeader('Access-Control-Allow-Origin', '*');
4290
+ res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
4291
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
4292
+ res.setHeader('Pragma', 'no-cache');
4293
+ res.setHeader('Expires', '0');
4294
+ let content = '';
4295
+ try {
4296
+ const _req = createRequire(import.meta.url);
4297
+ const entryRtPath = _req.resolve('@nativescript/vite/hmr/entry-runtime.js');
4298
+ content = readFileSync(entryRtPath, 'utf-8');
4299
+ }
4300
+ catch (e) {
4301
+ // .js not found (source tree without build) — transform .ts on the fly
4302
+ try {
4303
+ const tsPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', 'entry-runtime.ts');
4304
+ if (existsSync(tsPath)) {
4305
+ const tsSource = readFileSync(tsPath, 'utf-8');
4306
+ const result = babelCore.transformSync(tsSource, {
4307
+ filename: tsPath,
4308
+ plugins: [[pluginTransformTypescript, { isTSX: false, allowDeclareFields: true }]],
4309
+ sourceType: 'module',
4310
+ });
4311
+ if (result?.code) {
4312
+ content = result.code;
4313
+ }
4314
+ }
4315
+ }
4316
+ catch (e2) {
4317
+ if (verbose)
4318
+ console.warn('[hmr-http] entry-runtime.ts transform failed', e2);
4319
+ }
4320
+ if (!content) {
4321
+ content = 'export default async function start(){ console.error("[/ns/entry-rt] not found"); }\n';
4322
+ }
4323
+ }
4324
+ console.log('[hmr-http] /ns/entry-rt serving', content.length, 'bytes');
4325
+ res.statusCode = 200;
4326
+ res.end(content);
4327
+ }
4328
+ catch (e) {
4329
+ console.warn('[hmr-http] /ns/entry-rt error', e);
4330
+ next();
4331
+ }
4332
+ });
4333
+ // 2.6b) HTTP-only app entry endpoint: GET /ns/entry[/<ver>]
4334
+ // Thin wrapper that imports the compiled entry runtime and starts it with parameters.
4335
+ server.middlewares.use(async (req, res, next) => {
4336
+ try {
4337
+ const urlObj = new URL(req.url || '', 'http://localhost');
4338
+ if (!(urlObj.pathname === '/ns/entry' || /^\/ns\/entry\/[\d]+$/.test(urlObj.pathname)))
4339
+ return next();
4340
+ try {
4341
+ if (verbose) {
4342
+ const ra = req.socket?.remoteAddress;
4343
+ const rp = req.socket?.remotePort;
4344
+ console.log('[hmr-http] GET /ns/entry from', ra + (rp ? ':' + rp : ''));
4345
+ }
4346
+ }
4347
+ catch { }
4348
+ const verSeg = urlObj.pathname.replace(/^\/ns\/entry\/?/, '');
4349
+ // Resolve app main entry to an absolute path-like key used by /ns/m
4350
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
4351
+ res.setHeader('Pragma', 'no-cache');
4352
+ res.setHeader('Expires', '0');
4353
+ const ver = /^[0-9]+$/.test(verSeg) ? verSeg : String(graphVersion || 0);
4354
+ const origin = getServerOrigin(server) || `${urlObj.protocol}//${urlObj.host}`;
4355
+ // Resolve app main entry to an absolute path-like key used by /ns/m
4356
+ let mainEntry = '/';
4357
+ try {
4358
+ const pkg = getPackageJson();
4359
+ const main = pkg?.main || DEFAULT_MAIN_ENTRY;
4360
+ const abs = getProjectFilePath(main).replace(/\\/g, '/');
4361
+ // Normalize to '/app/...'
4362
+ const marker = `/${APP_ROOT_DIR}/`;
4363
+ const idx = abs.indexOf(marker);
4364
+ mainEntry = idx >= 0 ? abs.substring(idx) : DEFAULT_MAIN_ENTRY_VIRTUAL;
4365
+ }
4366
+ catch { }
4367
+ // Build a tiny wrapper that imports the compiled entry runtime from the dev server
4368
+ let code = REQUIRE_GUARD_SNIPPET +
4369
+ `// [ns-entry][v${ver}] wrapper (script-safe) bytes will follow\n` +
4370
+ `(async function(){\n` +
4371
+ ` let origin = ${JSON.stringify(origin)}; const main = ${JSON.stringify(mainEntry)}; const __ns_graph_ver = ${JSON.stringify(ver)};\n` +
4372
+ ` try { const __b = (globalThis && globalThis.__NS_ENTRY_BASE__) ? String(globalThis.__NS_ENTRY_BASE__) : ''; if (__b) { try { const __o = new URL(__b).origin; if (__o) origin = __o; } catch {} } } catch {}\n` +
4373
+ ` const __VERBOSE__ = (typeof __NS_ENV_VERBOSE__ !== 'undefined' && __NS_ENV_VERBOSE__) || (globalThis && globalThis.process && globalThis.process.env && globalThis.process.env.verbose) || (globalThis && globalThis.__NS_ENV_VERBOSE__) || ${JSON.stringify(!!verbose)};\n` +
4374
+ ` if (__VERBOSE__) console.info('[ns-entry][wrapper] start', { origin, main, ver: __ns_graph_ver });\n` +
4375
+ ` async function __ns_import_entry_rt(u){\n` +
4376
+ ` // Prefer fetch+eval script transformation to avoid module import limitations on device\n` +
4377
+ ` try { const r = await fetch(u); const t = await r.text(); if (__VERBOSE__) console.info('[ns-entry][wrapper] entry-rt fetched bytes', (t&&t.length)||0);\n` +
4378
+ ` // Transform 'export default function' or 'export default async function' into global assignment\n` +
4379
+ ` let s = t.replace(/export\\s+default\\s+async\\s+function\\s+([A-Za-z0-9_$]+)?/,'globalThis.__NS_START_ENTRY__=async function $1')\n` +
4380
+ ` .replace(/export\\s+default\\s+function\\s+([A-Za-z0-9_$]+)?/,'globalThis.__NS_START_ENTRY__=function $1');\n` +
4381
+ ` // Fallback: if function-form replacements didn't run, handle expression default export too\n` +
4382
+ ` if (String(s).indexOf('__NS_START_ENTRY__') === -1) { s = 'globalThis.__NS_START_ENTRY__=' + s.replace(/export\\s+default\\s*/,''); }\n` +
4383
+ ` try { (0,eval)(s); } catch (ee) { console.error('[ns-entry][wrapper] eval entry-rt failed', ee && (ee.message||ee)); throw ee; }\n` +
4384
+ ` const fn = globalThis.__NS_START_ENTRY__; if (!fn) { throw new Error('entry-rt missing __NS_START_ENTRY__'); }\n` +
4385
+ ` return { default: fn };\n` +
4386
+ ` } catch(e) { console.error('[ns-entry][wrapper] entry-rt fetch/eval failed', e && (e.message||e)); throw e; }\n` +
4387
+ ` }\n` +
4388
+ ` const __entryRtUrl = '/ns/entry-rt?v=' + String(__ns_graph_ver);\n` +
4389
+ ` let __mod; try { __mod = await __ns_import_entry_rt(__entryRtUrl); if (__VERBOSE__) console.info('[ns-entry][wrapper] entry-rt ready'); } catch (e) { console.error('[ns-entry][wrapper] failed to prepare entry-rt', e && (e.message||e)); throw e; }\n` +
4390
+ ` const startEntry = (__mod && (__mod.default || __mod));\n` +
4391
+ ` try { await startEntry({ origin, main, ver: __ns_graph_ver, verbose: !!__VERBOSE__ }); if (__VERBOSE__) console.info('[ns-entry][wrapper] startEntry() resolved'); } catch (e) { console.error('[ns-entry][wrapper] startEntry() failed', e && (e.message||e)); throw e; }\n` +
4392
+ `})();\n`;
4393
+ code = code + `\n//# sourceURL=${origin}/ns/entry`;
4394
+ res.statusCode = 200;
4395
+ res.end(code);
4396
+ }
4397
+ catch (e) {
4398
+ next();
4399
+ }
4400
+ });
4401
+ // 2.6) Transactional HMR endpoint: GET /ns/txn/<ver>
4402
+ // Returns a single ESM that sequentially imports all changed modules for the given graphVersion.
4403
+ server.middlewares.use(async (req, res, next) => {
4404
+ try {
4405
+ const urlObj = new URL(req.url || '', 'http://localhost');
4406
+ const p = urlObj.pathname || '';
4407
+ if (!p.startsWith('/ns/txn'))
4408
+ return next();
4409
+ let verStr = p.replace('/ns/txn', '').replace(/^\//, '');
4410
+ const ver = Number(verStr || urlObj.searchParams.get('v') || 0);
4411
+ let ids = txnBatches.get(ver) || [];
4412
+ if (!ids.length) {
4413
+ // Attempt to rebuild from any changed modules at this version if present in graph history is unavailable.
4414
+ // Fallback heuristic: use all modules with latest hash change equal to this version (we don't store per-module version, so use any changedIds from query 'ids' if provided)
4415
+ try {
4416
+ const q = (urlObj.searchParams.get('ids') || '')
4417
+ .split(',')
4418
+ .map((s) => s.trim())
4419
+ .filter(Boolean);
4420
+ if (q.length)
4421
+ ids = computeTxnOrderForChanged(q);
4422
+ }
4423
+ catch { }
4424
+ }
4425
+ const origin = getServerOrigin(server) || `${urlObj.protocol}//${urlObj.host}`;
4426
+ const lines = [];
4427
+ lines.push(`// [txn] version=${ver} count=${ids.length}`);
4428
+ if (!ids.length) {
4429
+ lines.push(`export default true;`);
4430
+ res.setHeader('Access-Control-Allow-Origin', '*');
4431
+ res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
4432
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
4433
+ res.setHeader('Pragma', 'no-cache');
4434
+ res.setHeader('Expires', '0');
4435
+ res.statusCode = 200;
4436
+ res.end(lines.join('\n'));
4437
+ return;
4438
+ }
4439
+ for (const id of ids) {
4440
+ const isVue = /\.vue$/i.test(id);
4441
+ const safe = id.startsWith('/') ? id : '/' + id;
4442
+ const abs = isVue ? `/ns/asm/${ver}?path=${encodeURIComponent(safe)}` : `/ns/m${safe}`;
4443
+ lines.push(`await import(${JSON.stringify(abs)});`);
4444
+ }
4445
+ lines.push(`export default true;`);
4446
+ const code = lines.join('\n');
4447
+ res.setHeader('Access-Control-Allow-Origin', '*');
4448
+ res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
4449
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
4450
+ res.setHeader('Pragma', 'no-cache');
4451
+ res.setHeader('Expires', '0');
4452
+ res.statusCode = 200;
4453
+ res.end(code);
4454
+ return;
4455
+ }
4456
+ catch (e) {
4457
+ /* fallthrough */
4458
+ }
4459
+ return next();
4460
+ });
4461
+ // 3) ESM endpoint for SFC modules: GET /ns/sfc?path=/src/Comp.vue[?vue&type=*] OR /ns/sfc/src/Comp.vue[?vue&type=*]
4462
+ // Also accept alias /ns/sfc
4463
+ // Preserves variant queries (?vue&type=script|template|style) and adds a diagnostic signature comment.
4464
+ server.middlewares.use(async (req, res, next) => {
4465
+ try {
4466
+ const urlObj = new URL(req.url || '', 'http://localhost');
4467
+ const p = urlObj.pathname;
4468
+ // Only match exactly "/ns/sfc" or paths under it.
4469
+ const isNs = p === '/ns/sfc' || p.startsWith('/ns/sfc/');
4470
+ if (!isNs)
4471
+ return next();
4472
+ if (p.startsWith('/ns/asm') || p.startsWith('/ns/sfc-meta'))
4473
+ return next();
4474
+ res.setHeader('Access-Control-Allow-Origin', '*');
4475
+ res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
4476
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
4477
+ res.setHeader('Pragma', 'no-cache');
4478
+ res.setHeader('Expires', '0');
4479
+ const base = '/ns/sfc';
4480
+ // Determine request spec, preserving variant query when present and handling optional version in path
4481
+ let pathParam = urlObj.searchParams.get('path') || ''; // may include its own query
4482
+ const rawRemainder = urlObj.pathname.slice(base.length) || '';
4483
+ let verFromPath = null;
4484
+ let pathStyle = rawRemainder;
4485
+ if (rawRemainder && rawRemainder.startsWith('/')) {
4486
+ const parts = rawRemainder.split('/'); // ["", maybe "<ver>", ...]
4487
+ if (parts.length > 2 && /^[0-9]+$/.test(parts[1] || '')) {
4488
+ verFromPath = parts[1];
4489
+ pathStyle = '/' + parts.slice(2).join('/');
4490
+ }
4491
+ }
4492
+ if (pathStyle && pathStyle !== '/' && !pathParam) {
4493
+ if (!pathStyle.startsWith('/'))
4494
+ pathStyle = '/' + pathStyle;
4495
+ // Include endpoint query for variant-style requests (e.g. /ns/sfc/Comp.vue?vue&type=template)
4496
+ pathParam = pathStyle + (urlObj.search || '');
4497
+ }
4498
+ let fullSpec = pathParam || '';
4499
+ if (!fullSpec) {
4500
+ res.statusCode = 200;
4501
+ res.end('export {}\n');
4502
+ return;
4503
+ }
4504
+ if (fullSpec.startsWith('@/'))
4505
+ fullSpec = APP_VIRTUAL_WITH_SLASH + fullSpec.slice(2);
4506
+ if (!fullSpec.startsWith('/'))
4507
+ fullSpec = '/' + fullSpec;
4508
+ const isVariant = /[?&]vue&type=/.test(fullSpec);
4509
+ const variantTypeMatch = /[?&]type=([^&]+)/.exec(fullSpec);
4510
+ const variantType = variantTypeMatch?.[1] || null;
4511
+ const isStyleVariant = /[?&]type=style\b/.test(fullSpec);
4512
+ // Determine candidate for transformRequest
4513
+ // For full SFCs we prefer a clean base path + '?vue'; if that fails, try base without query as fallback.
4514
+ let candidate = fullSpec;
4515
+ let transformed = null;
4516
+ if (!isVariant) {
4517
+ const basePath = fullSpec.replace(/[?#].*$/, '');
4518
+ const candidates = [basePath + (basePath.includes('?') ? '&' : '?') + 'vue', basePath];
4519
+ for (const c of candidates) {
4520
+ try {
4521
+ const r = await server.transformRequest(c);
4522
+ if (r?.code) {
4523
+ transformed = r;
4524
+ candidate = c;
4525
+ break;
4526
+ }
4527
+ }
4528
+ catch { }
4529
+ }
4530
+ if (!transformed?.code) {
4531
+ if (verbose) {
4532
+ try {
4533
+ console.warn(`[sfc][serve] transform miss for`, fullSpec);
4534
+ }
4535
+ catch { }
4536
+ }
4537
+ // Emit an erroring module to surface the failure at import site with helpful hints
4538
+ try {
4539
+ const tried = candidates.slice(0, 8);
4540
+ const out = `// [sfc] transform miss kind=full path=${fullSpec.replace(/\n/g, '')} tried=${tried.length}\n` + `throw new Error(${JSON.stringify('[ns/sfc] transform failed for full SFC: ' + fullSpec + ' (tried ' + tried.length + ')')});\nexport {}\n`;
4541
+ res.statusCode = 404;
4542
+ res.end(out);
4543
+ return;
4544
+ }
4545
+ catch {
4546
+ res.statusCode = 404;
4547
+ res.end('export {}\n');
4548
+ return;
4549
+ }
4550
+ }
4551
+ }
4552
+ else {
4553
+ try {
4554
+ transformed = await server.transformRequest(candidate);
4555
+ }
4556
+ catch { }
4557
+ if (!transformed?.code) {
4558
+ try {
4559
+ const out = `// [sfc] transform miss kind=variant path=${fullSpec.replace(/\n/g, '')}\n` + `throw new Error(${JSON.stringify('[ns/sfc] transform failed for variant: ' + fullSpec)});\nexport {}\n`;
4560
+ res.statusCode = 404;
4561
+ res.end(out);
4562
+ return;
4563
+ }
4564
+ catch {
4565
+ res.statusCode = 404;
4566
+ res.end('export {}\n');
4567
+ return;
4568
+ }
4569
+ }
4570
+ }
4571
+ // For style variants, return an empty module immediately
4572
+ if (isStyleVariant) {
4573
+ const sig = `// [sfc] kind=variant:style path=${fullSpec.replace(/\n/g, '')} len=0 default=false\n`;
4574
+ res.statusCode = 200;
4575
+ res.end(`${sig}export {}\n`);
4576
+ return;
4577
+ }
4578
+ let code = transformed.code;
4579
+ // Prepend guard to capture any URL-based require attempts
4580
+ code = REQUIRE_GUARD_SNIPPET + code;
4581
+ const projectRoot = server.config?.root || process.cwd();
4582
+ // IMPORTANT: Do not run cleanCode() on template variant; it can strip required pieces.
4583
+ // We'll handle script/full SFC below, and treat template minimally right away.
4584
+ // Full SFCs delegate to deterministic assembler module; variants (script/template) still go through processing
4585
+ if (!isVariant) {
4586
+ const importerPath = fullSpec.replace(/[?#].*$/, '');
4587
+ const origin = getServerOrigin(server);
4588
+ const ver = verFromPath || '0';
4589
+ const asmPath = `/ns/asm/${ver}?path=${encodeURIComponent(importerPath)}`;
4590
+ const delegated = `// [sfc] kind=full (delegated to assembler) path=${importerPath}\nexport * from ${JSON.stringify(asmPath)};\nexport { default } from ${JSON.stringify(asmPath)};\n`;
4591
+ res.statusCode = 200;
4592
+ res.end(delegated);
4593
+ return;
4594
+ }
4595
+ else {
4596
+ // Variants
4597
+ if (variantType === 'template') {
4598
+ const preferSelfCompile = !!process.env.NS_HMR_SELF_COMPILE_TEMPLATE;
4599
+ // Compile the template ourselves to guarantee no Vite HMR code and stable output
4600
+ if (preferSelfCompile)
4601
+ try {
4602
+ const projectRootT = server.config?.root || process.cwd();
4603
+ const basePath = fullSpec.replace(/[?#].*$/, '');
4604
+ const abs = path.join(projectRootT, basePath.replace(/^\//, ''));
4605
+ let sfcSrc = '';
4606
+ try {
4607
+ sfcSrc = readFileSync(abs, 'utf-8');
4608
+ }
4609
+ catch { }
4610
+ if (sfcSrc) {
4611
+ const { descriptor } = parse(sfcSrc, { filename: abs });
4612
+ const id = createHash('md5').update(abs).digest('hex').slice(0, 8);
4613
+ let bindingMetadata = undefined;
4614
+ try {
4615
+ const s = compileScript(descriptor, {
4616
+ id,
4617
+ inlineTemplate: false,
4618
+ reactivityTransform: false,
4619
+ });
4620
+ bindingMetadata = s?.bindings;
4621
+ }
4622
+ catch { }
4623
+ const tpl = descriptor.template?.content || '';
4624
+ const ct = compileTemplate({
4625
+ source: tpl,
4626
+ id,
4627
+ filename: abs,
4628
+ isProd: false,
4629
+ ssr: false,
4630
+ compilerOptions: {
4631
+ bindingMetadata,
4632
+ isCustomElement: (tag) => NS_NATIVE_TAGS.has(tag),
4633
+ },
4634
+ });
4635
+ let out = (ct && (ct.code || '')) || '';
4636
+ // Map Vue helper imports to runtime bridge
4637
+ try {
4638
+ out = out.replace(/from\s+["'](?:nativescript-vue|vue)[^"']*["']/g, 'from "/ns/rt"');
4639
+ }
4640
+ catch { }
4641
+ // No import.meta.hot present when compiling ourselves, but keep minimal sanitizer just in case
4642
+ out = processTemplateVariantMinimal(out);
4643
+ code = out;
4644
+ }
4645
+ else {
4646
+ code = 'export {}\n';
4647
+ }
4648
+ }
4649
+ catch (eTplSelf) {
4650
+ if (verbose) {
4651
+ try {
4652
+ console.warn('[sfc][template][self-compile][fail]', fullSpec, eTplSelf?.message);
4653
+ }
4654
+ catch { }
4655
+ }
4656
+ code = transformed.code || 'export {}\n';
4657
+ code = processTemplateVariantMinimal(code);
4658
+ }
4659
+ else {
4660
+ // Prefer using Vite's template transform and apply minimal sanitization; avoids compiler mismatches and warnings
4661
+ code = transformed.code || 'export {}\n';
4662
+ code = processTemplateVariantMinimal(code);
4663
+ }
4664
+ // fall through to shared post-processing (versioning, signature, etc.)
4665
+ }
4666
+ // Script variants still need vendor mappings and general device processing (no SFC assembly)
4667
+ // IMPORTANT: Use a Babel AST transform to remove imports of the template variant and
4668
+ // neutralize their usage without brittle regex.
4669
+ try {
4670
+ const ast = babelParse(code, {
4671
+ sourceType: 'module',
4672
+ plugins: ['typescript'],
4673
+ });
4674
+ const templateBindings = new Set();
4675
+ const navToLocals = [];
4676
+ const navBackLocals = [];
4677
+ babelTraverse(ast, {
4678
+ ImportDeclaration(path) {
4679
+ const spec = path.node.source.value || '';
4680
+ // Remove template variant imports and collect their local identifiers for neutralization
4681
+ if (typeof spec === 'string' && /\.vue\?[^\n]*type=template/.test(spec)) {
4682
+ const ids = [];
4683
+ for (const s of path.node.specifiers) {
4684
+ if (t.isImportSpecifier(s)) {
4685
+ const imported = t.isIdentifier(s.imported) ? s.imported.name : undefined;
4686
+ const local = t.isIdentifier(s.local) ? s.local.name : undefined;
4687
+ if ((imported === 'render' || imported === undefined) && local)
4688
+ ids.push(local);
4689
+ }
4690
+ else if (t.isImportDefaultSpecifier(s) || t.isImportNamespaceSpecifier(s)) {
4691
+ if (t.isIdentifier(s.local))
4692
+ ids.push(s.local.name);
4693
+ }
4694
+ }
4695
+ ids.forEach((n) => templateBindings.add(n));
4696
+ path.remove();
4697
+ return;
4698
+ }
4699
+ // Rewrite $navigateTo/$navigateBack imports from nativescript-vue (or prebundle) to use globals
4700
+ const isNsVue = typeof spec === 'string' && (/nativescript-vue/.test(spec) || /vendor\.mjs$/.test(spec) || /\/node_modules\/\.vite\/deps\/nativescript-vue\.js/.test(spec));
4701
+ if (isNsVue) {
4702
+ const remain = [];
4703
+ for (const s of path.node.specifiers) {
4704
+ if (t.isImportSpecifier(s)) {
4705
+ const imported = t.isIdentifier(s.imported) ? s.imported.name : undefined;
4706
+ const local = t.isIdentifier(s.local) ? s.local.name : undefined;
4707
+ if (local && (imported === '$navigateTo' || imported === 'navigateTo')) {
4708
+ navToLocals.push(local);
4709
+ continue;
4710
+ }
4711
+ if (local && (imported === '$navigateBack' || imported === 'navigateBack')) {
4712
+ navBackLocals.push(local);
4713
+ continue;
4714
+ }
4715
+ }
4716
+ remain.push(s);
4717
+ }
4718
+ if (remain.length) {
4719
+ path.node.specifiers = remain;
4720
+ }
4721
+ else {
4722
+ path.remove();
4723
+ }
4724
+ }
4725
+ },
4726
+ });
4727
+ if (templateBindings.size) {
4728
+ babelTraverse(ast, {
4729
+ Identifier(path) {
4730
+ if (templateBindings.has(path.node.name)) {
4731
+ path.replaceWith(t.identifier('undefined'));
4732
+ }
4733
+ },
4734
+ AssignmentExpression(path) {
4735
+ // Guard component.render = <alias> to avoid TDZ when alias is undefined
4736
+ if (t.isMemberExpression(path.node.left) &&
4737
+ t.isIdentifier(path.node.left.property, {
4738
+ name: 'render',
4739
+ })) {
4740
+ const e = t.identifier('__e');
4741
+ const guarded = t.tryStatement(t.blockStatement([t.variableDeclaration('const', [t.variableDeclarator(e, path.node.right)]), t.ifStatement(t.logicalExpression('&&', t.binaryExpression('!==', t.unaryExpression('typeof', path.node.left.object, true), t.stringLiteral('undefined')), t.binaryExpression('!==', t.unaryExpression('typeof', e, true), t.stringLiteral('undefined'))), t.blockStatement([t.expressionStatement(t.assignmentExpression('=', path.node.left, e))]))]), t.catchClause(t.identifier('_e'), t.blockStatement([])));
4742
+ path.replaceWithMultiple([guarded]);
4743
+ }
4744
+ },
4745
+ });
4746
+ }
4747
+ let outCode = genCode(ast).code;
4748
+ if (navToLocals.length || navBackLocals.length) {
4749
+ const shimLines = [];
4750
+ for (const n of navToLocals)
4751
+ shimLines.push(`import __ns_rt_nav_to_mod from "/ns/rt";\nconst ${n} = (...args) => __ns_rt_nav_to_mod.$navigateTo(...args);`);
4752
+ for (const n of navBackLocals)
4753
+ shimLines.push(`import __ns_rt_nav_back_mod from "/ns/rt";\nconst ${n} = (...args) => __ns_rt_nav_back_mod.$navigateBack(...args);`);
4754
+ outCode = shimLines.join('\n') + '\n' + outCode;
4755
+ }
4756
+ code = outCode;
4757
+ }
4758
+ catch { }
4759
+ code = processCodeForDevice(code, false, true, /(?:^|\/)node_modules\//.test(fullSpec), fullSpec);
4760
+ // Transform static .vue imports into static imports from the assembler (no TLA) via AST
4761
+ try {
4762
+ const importerPath = fullSpec.replace(/[?#].*$/, '');
4763
+ const origin = getServerOrigin(server);
4764
+ const ver = verFromPath || '0';
4765
+ const ast2 = babelParse(code, {
4766
+ sourceType: 'module',
4767
+ plugins: ['typescript'],
4768
+ });
4769
+ babelTraverse(ast2, {
4770
+ ImportDeclaration(p) {
4771
+ const src = p.node.source.value || '';
4772
+ if (typeof src !== 'string')
4773
+ return;
4774
+ if (/^https?:\/\//.test(src))
4775
+ return; // leave absolute URLs
4776
+ if (/\.vue(?:$|\?)/.test(src)) {
4777
+ let spec = src;
4778
+ // Resolve to absolute project path
4779
+ if (spec.startsWith('./') || spec.startsWith('../')) {
4780
+ spec = path.posix.normalize(path.posix.join(path.posix.dirname(importerPath), spec));
4781
+ if (!spec.startsWith('/'))
4782
+ spec = '/' + spec;
4783
+ }
4784
+ else if (!spec.startsWith('/')) {
4785
+ // Handle '@/'
4786
+ if (spec.startsWith('@@/'))
4787
+ spec = '/' + spec.slice(2);
4788
+ if (spec.startsWith('@/'))
4789
+ spec = APP_VIRTUAL_WITH_SLASH + spec.slice(2);
4790
+ }
4791
+ // Strip query for plain .vue (keep variant imports intact)
4792
+ if (!/\bvue&type=/.test(src)) {
4793
+ spec = spec.replace(/[?#].*$/, '');
4794
+ const asmUrl = `/ns/asm/${ver}?path=${encodeURIComponent(spec)}&mode=inline`;
4795
+ p.node.source = t.stringLiteral(asmUrl);
4796
+ }
4797
+ }
4798
+ },
4799
+ });
4800
+ code = genCode(ast2).code;
4801
+ }
4802
+ catch { }
4803
+ // After rewrites, strip any TypeScript syntax from the script variant to avoid device-side parse errors
4804
+ try {
4805
+ const importerPath = fullSpec.replace(/[?#].*$/, '');
4806
+ const tsRes = await babelCore.transformAsync(code, {
4807
+ plugins: [[pluginTransformTypescript, { allowDeclareFields: true }]],
4808
+ sourceType: 'module',
4809
+ // Help Babel infer TS parsing even if the virtual filename isn't .ts
4810
+ filename: importerPath.endsWith('.vue') ? importerPath.replace(/\.vue$/, '.ts') : importerPath + '.ts',
4811
+ comments: true,
4812
+ configFile: false,
4813
+ babelrc: false,
4814
+ });
4815
+ if (tsRes?.code) {
4816
+ code = tsRes.code;
4817
+ }
4818
+ }
4819
+ catch (eTsVar) {
4820
+ if (verbose) {
4821
+ try {
4822
+ console.warn('[sfc][variant:script][babel-ts][fail]', fullSpec, eTsVar?.message);
4823
+ }
4824
+ catch { }
4825
+ }
4826
+ }
4827
+ }
4828
+ const importerPath = fullSpec.replace(/[?#].*$/, '');
4829
+ // Only run cleanCode for non-template cases (script/full). Template code must remain intact.
4830
+ if (!isVariant || variantType !== 'template') {
4831
+ code = cleanCode(code);
4832
+ }
4833
+ code = rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot, !!verbose, undefined, getServerOrigin(server));
4834
+ code = ensureVariableDynamicImportHelper(code);
4835
+ try {
4836
+ // For variant requests under /ns/sfc, prefer the version from the path segment when present
4837
+ // so that any internal '/ns/rt', '/ns/core', or '/ns/sfc' imports are aligned with the same version.
4838
+ const verNum = Number(verFromPath || '0');
4839
+ if (Number.isFinite(verNum) && verNum > 0) {
4840
+ code = ensureVersionedRtImports(code, getServerOrigin(server), verNum);
4841
+ code = ACTIVE_STRATEGY.ensureVersionedImports(code, getServerOrigin(server), verNum);
4842
+ code = ensureVersionedCoreImports(code, getServerOrigin(server), verNum);
4843
+ }
4844
+ else {
4845
+ code = ensureVersionedRtImports(code, getServerOrigin(server), graphVersion);
4846
+ code = ACTIVE_STRATEGY.ensureVersionedImports(code, getServerOrigin(server), graphVersion);
4847
+ code = ensureVersionedCoreImports(code, getServerOrigin(server), graphVersion);
4848
+ }
4849
+ }
4850
+ catch { }
4851
+ // Final guard for SFC variant output as well
4852
+ try {
4853
+ code = ensureDestructureCoreImports(code);
4854
+ }
4855
+ catch { }
4856
+ // CRITICAL: As a last step for script/template variants, re-run AST normalization and strip
4857
+ // any sentinel destructures that could cause duplicate locals, then re-apply core versioning.
4858
+ try {
4859
+ code = astNormalizeModuleImportsAndHelpers(code);
4860
+ }
4861
+ catch { }
4862
+ try {
4863
+ // Remove any rt->core sentinel destructures that slipped in late
4864
+ code = stripRtCoreSentinel(code);
4865
+ }
4866
+ catch { }
4867
+ try {
4868
+ const verNum = Number(verFromPath || '0');
4869
+ if (Number.isFinite(verNum) && verNum > 0) {
4870
+ code = ensureVersionedRtImports(code, getServerOrigin(server), verNum);
4871
+ code = ensureVersionedCoreImports(code, getServerOrigin(server), verNum);
4872
+ }
4873
+ else {
4874
+ code = ensureVersionedRtImports(code, getServerOrigin(server), graphVersion);
4875
+ code = ensureVersionedCoreImports(code, getServerOrigin(server), graphVersion);
4876
+ }
4877
+ }
4878
+ catch { }
4879
+ // Last-chance sanitizer for dangling Vite CJS import helper usages that may surface after late transforms
4880
+ try {
4881
+ code = stripDanglingViteCjsImports(code);
4882
+ }
4883
+ catch { }
4884
+ const hasDefault = /\bexport\s+default\b/.test(code);
4885
+ const kind = isVariant ? `variant:${variantType || 'unknown'}` : 'full';
4886
+ const sig = `// [sfc] kind=${kind} path=${importerPath} len=${code.length} default=${hasDefault} wrapped=${false}\n`;
4887
+ if (verbose) {
4888
+ try {
4889
+ console.log(`[sfc][serve] ${fullSpec} kind=${kind} default=${hasDefault} bytes=${code.length}`);
4890
+ }
4891
+ catch { }
4892
+ }
4893
+ // Ensure script variants always provide a default export if they declare a component
4894
+ if (!hasDefault) {
4895
+ // Prefer an explicit identifier if present
4896
+ const m = code.match(/\b(?:const|let|var)\s+(__ns_sfc__|_sfc_main)\b/);
4897
+ if (m && m[1]) {
4898
+ code += `\nexport default ${m[1]};`;
4899
+ }
4900
+ else if (/\b_defineComponent\s*\(|\bdefineComponent\s*\(/.test(code)) {
4901
+ // Fallback: export whichever is defined at runtime without throwing on missing identifiers
4902
+ code += `\nexport default (typeof __ns_sfc__ !== "undefined" ? __ns_sfc__ : (typeof _sfc_main !== "undefined" ? _sfc_main : undefined));`;
4903
+ }
4904
+ }
4905
+ res.statusCode = 200;
4906
+ res.end(sig + code);
4907
+ }
4908
+ catch (e) {
4909
+ res.statusCode = 500;
4910
+ res.end('export {}\n');
4911
+ }
4912
+ });
4913
+ // 4) JSON metadata endpoint for SFCs: GET /ns/sfc-meta?path=/src/Comp.vue OR /ns/sfc-meta/<ver>?path=/src/Comp.vue
4914
+ server.middlewares.use(async (req, res, next) => {
4915
+ try {
4916
+ const urlObj = new URL(req.url || '', 'http://localhost');
4917
+ if (!urlObj.pathname.startsWith('/ns/sfc-meta'))
4918
+ return next();
4919
+ res.setHeader('Access-Control-Allow-Origin', '*');
4920
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
4921
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
4922
+ res.setHeader('Pragma', 'no-cache');
4923
+ res.setHeader('Expires', '0');
4924
+ // Accept optional version segment similar to /ns/sfc
4925
+ {
4926
+ const metaBase = '/ns/sfc-meta';
4927
+ if (urlObj.pathname.startsWith(metaBase + '/')) {
4928
+ const rawRemainder = urlObj.pathname.slice(metaBase.length);
4929
+ const parts = rawRemainder.split('/');
4930
+ if (parts.length > 2 && /^[0-9]+$/.test(parts[1] || '')) {
4931
+ // consume version but we don't need it server-side
4932
+ }
4933
+ }
4934
+ }
4935
+ let spec = urlObj.searchParams.get('path') || '';
4936
+ if (!spec) {
4937
+ res.statusCode = 400;
4938
+ res.end(JSON.stringify({ error: 'missing path' }));
4939
+ return;
4940
+ }
4941
+ if (spec.startsWith('@/'))
4942
+ spec = APP_VIRTUAL_WITH_SLASH + spec.slice(2);
4943
+ if (!spec.startsWith('/'))
4944
+ spec = '/' + spec;
4945
+ const base = spec.replace(/[?#].*$/, '');
4946
+ // Transform variants to inspect exports
4947
+ const [scriptR, templateR] = await Promise.all([server.transformRequest(base + '?vue&type=script'), server.transformRequest(base + '?vue&type=template')]);
4948
+ const scriptCode = scriptR?.code || '';
4949
+ const templateCode = templateR?.code || '';
4950
+ const scriptMeta = extractExportMetadata(scriptCode);
4951
+ // Robust render detection: Vue compiler may emit several shapes:
4952
+ // 1) export function render(_ctx, _cache) { ... }
4953
+ // 2) function render(_ctx,_cache) { ... } (later exported)
4954
+ // 3) export const render = (_ctx,_cache) => { ... }
4955
+ // 4) const render = (...) => { ... } (later exported)
4956
+ // 5) export { render } or export { render as render }
4957
+ // 6) Object property forms (rare in template output) render: (...) => {}
4958
+ const hasRender = /export\s+function\s+render\s*\(/.test(templateCode) || /(?:^|\n)\s*function\s+render\s*\(/.test(templateCode) || /export\s+(?:const|let|var)\s+render\s*=/.test(templateCode) || /(?:^|\n)\s*(?:const|let|var)\s+render\s*=/.test(templateCode) || /\brender\s*[:=]\s*/.test(templateCode) || /export\s*\{\s*render\s*(?:as\s*render)?\s*\}/.test(templateCode);
4959
+ if (hasRender && verbose) {
4960
+ try {
4961
+ console.log('[sfc-meta] detected render for', base);
4962
+ }
4963
+ catch { }
4964
+ }
4965
+ else if (!hasRender && verbose) {
4966
+ try {
4967
+ console.warn('[sfc-meta] render NOT detected for', base);
4968
+ }
4969
+ catch { }
4970
+ }
4971
+ const hash = createHash('md5').update(base).digest('hex').slice(0, 8);
4972
+ const payload = {
4973
+ path: base,
4974
+ hasScript: !!scriptCode,
4975
+ hasTemplate: !!templateCode,
4976
+ hasStyle: false,
4977
+ scriptExports: scriptMeta.named,
4978
+ scriptHasDefault: scriptMeta.hasDefault,
4979
+ templateHasRender: hasRender,
4980
+ hmrId: hash,
4981
+ };
4982
+ res.statusCode = 200;
4983
+ res.end(JSON.stringify(payload));
4984
+ }
4985
+ catch (e) {
4986
+ res.statusCode = 500;
4987
+ res.end(JSON.stringify({ error: e?.message || String(e) }));
4988
+ }
4989
+ });
4990
+ // 5) Deterministic SFC assembler: GET /ns/asm?path=/src/Comp.vue
4991
+ // Place BEFORE any broader /ns/sfc* handlers that might accidentally match and delegate.
4992
+ server.middlewares.use(async (req, res, next) => {
4993
+ try {
4994
+ const urlObj = new URL(req.url || '', 'http://localhost');
4995
+ if (!urlObj.pathname.startsWith('/ns/asm'))
4996
+ return next();
4997
+ res.setHeader('Access-Control-Allow-Origin', '*');
4998
+ res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
4999
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
5000
+ res.setHeader('Pragma', 'no-cache');
5001
+ res.setHeader('Expires', '0');
5002
+ // Optional version segment as first path component after /ns/asm
5003
+ const asmBase = '/ns/asm';
5004
+ const asmRemainder = urlObj.pathname.slice(asmBase.length) || '';
5005
+ let verFromPath = null;
5006
+ if (asmRemainder && asmRemainder.startsWith('/')) {
5007
+ const p = asmRemainder.split('/');
5008
+ if (p.length > 1 && /^[0-9]+$/.test(p[1] || '')) {
5009
+ verFromPath = p[1];
5010
+ }
5011
+ }
5012
+ let spec = urlObj.searchParams.get('path') || '';
5013
+ const diag = urlObj.searchParams.get('diag') === '1';
5014
+ if (!spec) {
5015
+ res.statusCode = 400;
5016
+ res.end('export {}\n');
5017
+ return;
5018
+ }
5019
+ if (spec.startsWith('@/'))
5020
+ spec = APP_VIRTUAL_WITH_SLASH + spec.slice(2);
5021
+ if (!spec.startsWith('/'))
5022
+ spec = '/' + spec;
5023
+ const base = spec.replace(/[?#].*$/, '');
5024
+ if (diag) {
5025
+ const code = `// [sfc-asm] ${base} (diag)\n` + `// vue shim for diag-only instantiation\n` + `var _createElementVNode = globalThis.createElementVNode || globalThis._createElementVNode;\n` + `const __ns_sfc__ = { name: ${JSON.stringify(base.split('/').pop() || 'DiagComp')}, render(){ return _createElementVNode ? _createElementVNode('StackLayout') : (globalThis.createElementVNode ? globalThis.createElementVNode('StackLayout') : {}); } };\nexport default __ns_sfc__;\n`;
5026
+ res.statusCode = 200;
5027
+ res.end(code);
5028
+ return;
5029
+ }
5030
+ const projectRoot = server.config?.root || process.cwd();
5031
+ // Ensure variant transforms exist so imports resolve (avoid Promise.all short-circuit on single failure)
5032
+ const safeTransform = async (cand) => {
5033
+ try {
5034
+ return await server.transformRequest(cand);
5035
+ }
5036
+ catch {
5037
+ return null;
5038
+ }
5039
+ };
5040
+ const scriptR = await safeTransform(base + '?vue&type=script');
5041
+ const templateR = await safeTransform(base + '?vue&type=template');
5042
+ const fullR = await safeTransform(base + '?vue');
5043
+ const hasScript = !!scriptR?.code;
5044
+ const hasTemplate = !!templateR?.code;
5045
+ const origin = getServerOrigin(server);
5046
+ const ver = String(verFromPath || graphVersion || Date.now());
5047
+ const scriptUrl = `${origin}/ns/sfc/${ver}${base}?vue&type=script`;
5048
+ const templateCode = templateR?.code || '';
5049
+ // INLINE-FIRST assembler: compile SFC source into a self-contained ESM module (enhanced diagnostics)
5050
+ try {
5051
+ const root = server.config?.root || process.cwd();
5052
+ const abs = path.join(root, base.replace(/^\//, ''));
5053
+ let sfcSrc = '';
5054
+ try {
5055
+ sfcSrc = readFileSync(abs, 'utf-8');
5056
+ }
5057
+ catch { }
5058
+ if (sfcSrc) {
5059
+ const { descriptor } = parse(sfcSrc, { filename: abs });
5060
+ const id = createHash('md5').update(abs).digest('hex').slice(0, 8);
5061
+ // 1) Compile script (prefer inlineTemplate for a complete module)
5062
+ let compiledScript = '';
5063
+ let bindingMetadata = undefined;
5064
+ let triedInlineTemplate = false;
5065
+ let hadScriptDefaultPre = false;
5066
+ let usedInlineScript = false;
5067
+ try {
5068
+ // First try inlineTemplate for a holistic, self-contained module with render + hoists
5069
+ // Use a strict NativeScript native element detector for inlineTemplate that does NOT treat generic PascalCase as native.
5070
+ // This ensures imported components like PageWrapper remain true components and get referenced via bindings.
5071
+ const isNSNative = (tag) => NS_NATIVE_TAGS.has(tag);
5072
+ const sInline = compileScript(descriptor, {
5073
+ id,
5074
+ inlineTemplate: true,
5075
+ reactivityTransform: false,
5076
+ // Pass only strict NS native element predicate; avoid broad PascalCase heuristic here.
5077
+ templateOptions: {
5078
+ compilerOptions: { isCustomElement: isNSNative },
5079
+ },
5080
+ });
5081
+ triedInlineTemplate = true;
5082
+ if (/export\s+default/.test(sInline?.content || '')) {
5083
+ compiledScript = sInline.content;
5084
+ bindingMetadata = sInline?.bindings;
5085
+ hadScriptDefaultPre = true;
5086
+ usedInlineScript = true;
5087
+ }
5088
+ else {
5089
+ // Fallback to standard script (no inline) and attempt separate template compile
5090
+ const s = compileScript(descriptor, {
5091
+ id,
5092
+ inlineTemplate: false,
5093
+ reactivityTransform: false,
5094
+ });
5095
+ compiledScript = s?.content || '';
5096
+ bindingMetadata = s?.bindings;
5097
+ hadScriptDefaultPre = /export\s+default/.test(compiledScript);
5098
+ usedInlineScript = false;
5099
+ }
5100
+ }
5101
+ catch (eScript) {
5102
+ if (verbose) {
5103
+ try {
5104
+ console.warn('[sfc-asm][compileScript] failed', base, eScript?.message);
5105
+ }
5106
+ catch { }
5107
+ }
5108
+ // Retry without inlineTemplate
5109
+ try {
5110
+ const s = compileScript(descriptor, {
5111
+ id,
5112
+ inlineTemplate: false,
5113
+ reactivityTransform: false,
5114
+ });
5115
+ compiledScript = s?.content || '';
5116
+ bindingMetadata = s?.bindings;
5117
+ hadScriptDefaultPre = /export\s+default/.test(compiledScript);
5118
+ usedInlineScript = false;
5119
+ }
5120
+ catch (eNoInline) {
5121
+ if (verbose) {
5122
+ try {
5123
+ console.warn('[sfc-asm][compileScript][no-inline-fallback] failed', base, eNoInline?.message);
5124
+ }
5125
+ catch { }
5126
+ }
3277
5127
  }
3278
- res.statusCode = 200;
3279
- res.end(buildVersionedCoreSubpathAliasModule(resolvedSubpath, ver));
3280
- return;
3281
5128
  }
3282
- if (transformed?.code) {
3283
- // Minimal pipeline: Vite already produces correct ESM.
3284
- // ONLY rewrite specifier strings to device-fetchable URLs.
3285
- // Do NOT run processCodeForDevice, rewriteImports, or any
3286
- // other heavy transform — those mangle newlines, eat exports,
3287
- // and cause cascading "does not provide an export" failures.
3288
- const moduleCode = rewriteSpecifiersForDevice(transformed.code, getServerOrigin(server), Number(ver));
3289
- res.statusCode = 200;
3290
- res.end(moduleCode);
3291
- return;
5129
+ // Final fallback: if script compile yielded nothing, use the variant-transformed script
5130
+ if (!compiledScript && scriptR?.code) {
5131
+ try {
5132
+ compiledScript = scriptR.code;
5133
+ hadScriptDefaultPre = /export\s+default/.test(compiledScript);
5134
+ }
5135
+ catch { }
3292
5136
  }
3293
- }
3294
- catch (e) {
5137
+ // If inlineTemplate produced a default export AND visibly contains a render, allow early-return.
5138
+ // Visible render forms we accept:
5139
+ // - export function render(...) { ... }
5140
+ // - setup(...) { ... return (_ctx, _cache) => { ... } }
5141
+ const hasInlineRender = /(^|\n)\s*export\s+function\s+render\s*\(/.test(compiledScript || '') || /\breturn\s*\(\s*_ctx\s*,\s*_cache\s*\)\s*=>\s*\{/.test(compiledScript || '');
5142
+ // Always use canonical assembler path; avoid inlineTemplate early-return which can miss render attachment
5143
+ // If we reached here, we are going to assemble canonically. Ensure the script we use does NOT include inlineTemplate render.
5144
+ if (usedInlineScript) {
5145
+ try {
5146
+ const sNoInline = compileScript(descriptor, {
5147
+ id,
5148
+ inlineTemplate: false,
5149
+ reactivityTransform: false,
5150
+ });
5151
+ compiledScript = sNoInline?.content || compiledScript;
5152
+ bindingMetadata = sNoInline?.bindings || bindingMetadata;
5153
+ }
5154
+ catch (eNoInline) {
5155
+ if (verbose) {
5156
+ try {
5157
+ console.warn('[sfc-asm][compileScript][no-inline-fallback] failed', base, eNoInline?.message);
5158
+ }
5159
+ catch { }
5160
+ }
5161
+ }
5162
+ }
5163
+ // 2) Compile template
5164
+ let compiledTplCode = '';
5165
+ let templateErr = null;
3295
5166
  try {
3296
- console.warn('[ns-core-bridge] deep subpath serve failed:', sub, e?.message);
5167
+ const tplSrc = descriptor.template?.content || '';
5168
+ if (tplSrc) {
5169
+ const ct = compileTemplate({
5170
+ source: tplSrc,
5171
+ id,
5172
+ filename: abs,
5173
+ isProd: false,
5174
+ ssr: false,
5175
+ compilerOptions: {
5176
+ bindingMetadata,
5177
+ isCustomElement: (tag) => NS_NATIVE_TAGS.has(tag),
5178
+ },
5179
+ });
5180
+ compiledTplCode = (ct && (ct.code || '')) || '';
5181
+ if (ct?.errors?.length && verbose) {
5182
+ try {
5183
+ console.warn('[sfc-asm][compileTemplate][errors]', base, ct.errors);
5184
+ }
5185
+ catch { }
5186
+ }
5187
+ }
3297
5188
  }
3298
- catch { }
3299
- }
3300
- }
3301
- // Main entry or shallow subpath: use proxy bridge
3302
- let code = buildVersionedCoreMainBridgeModule(key, ver);
3303
- if (!sub) {
3304
- try {
3305
- const projectRoot = server.config?.root || process.cwd();
3306
- const coreSpecifier = '@nativescript/core';
3307
- const resolved = await server.pluginContainer?.resolveId?.(coreSpecifier, undefined);
3308
- const resolvedId = typeof resolved === 'string' ? resolved : resolved?.id || null;
3309
- const modulePath = resolvedId || '/node_modules/@nativescript/core/index.js';
3310
- const staticExportNames = collectStaticExportNamesFromFile(modulePath);
3311
- const staticExportOrigins = await normalizeCoreExportOriginsForRuntime(collectStaticExportOriginsFromFile(modulePath), async (moduleId) => {
3312
- const nextResolved = await server.pluginContainer?.resolveId?.(moduleId, undefined);
3313
- return typeof nextResolved === 'string' ? nextResolved : nextResolved?.id || null;
3314
- }, modulePath);
3315
- if (staticExportNames.length) {
3316
- code = buildVersionedCoreMainBridgeModule(key, ver, staticExportNames, staticExportOrigins);
5189
+ catch (eTpl) {
5190
+ templateErr = eTpl;
5191
+ if (verbose) {
5192
+ try {
5193
+ console.warn('[sfc-asm][compileTemplate] failed', base, eTpl?.message);
5194
+ }
5195
+ catch { }
5196
+ }
5197
+ // Fallback: use the variant-transformed template code if available
5198
+ try {
5199
+ if (templateR?.code)
5200
+ compiledTplCode = templateR.code;
5201
+ }
5202
+ catch { }
5203
+ }
5204
+ // If still no template code, synthesize a minimal render stub so the module is valid
5205
+ if (!compiledTplCode) {
5206
+ try {
5207
+ compiledTplCode = "export function render(){ const _ = (globalThis.createElementVNode||globalThis._createElementVNode); return _? _('StackLayout') : {}; }\n";
5208
+ }
5209
+ catch { }
5210
+ }
5211
+ // 3) Sanitize script and rewrite .vue imports to inline assembler
5212
+ let scriptBody = compiledScript || '';
5213
+ if (scriptBody) {
5214
+ // Do NOT strip Vue/nativescript-vue imports; retarget them to the runtime bridge so helpers (e.g., onMounted) are bound.
5215
+ // Preserve the import clause and only rewrite the source to '/ns/rt'.
5216
+ scriptBody = scriptBody.replace(/(^|\n)\s*import\s+([^;\n]+)\s+from\s+["'](?:vue|nativescript-vue|~\/vendor\.mjs)(?:\/[^"]*)?["'];?/g, (_m, pfx, clause) => `${pfx}import ${clause} from "/ns/rt";`);
5217
+ try {
5218
+ const importerDir = path.posix.dirname(base);
5219
+ scriptBody = scriptBody.replace(/(^|\n)\s*import\s+([^;\n]+)\s+from\s+["']([^"'\n]+\.vue)(?:\?[^"'\n]*)?["'];?/g, (_m, pfx, clause, spec) => {
5220
+ let absImp = spec;
5221
+ if (spec.startsWith('./') || spec.startsWith('../')) {
5222
+ absImp = path.posix.normalize(path.posix.join(importerDir, spec));
5223
+ if (!absImp.startsWith('/'))
5224
+ absImp = '/' + absImp;
5225
+ }
5226
+ else if (!spec.startsWith('/')) {
5227
+ if (absImp.startsWith('@/'))
5228
+ absImp = APP_VIRTUAL_WITH_SLASH + absImp.slice(2);
5229
+ }
5230
+ const asmUrl = `/ns/asm/${ver}?path=${encodeURIComponent(absImp)}&mode=inline`;
5231
+ return `${pfx}import ${clause} from ${JSON.stringify(asmUrl)};`;
5232
+ });
5233
+ }
5234
+ catch { }
5235
+ }
5236
+ // 4) Extract render from compiled template and prepare a full inline template block
5237
+ let helperBindings = '';
5238
+ let renderDecl = '';
5239
+ let inlineBlock = undefined;
5240
+ let renderOk = false;
5241
+ if (compiledTplCode) {
5242
+ try {
5243
+ // Build a full inline template block to preserve hoists where possible
5244
+ inlineBlock = buildInlineTemplateBlock(compiledTplCode) || undefined;
5245
+ if (!inlineBlock) {
5246
+ const extracted = extractTemplateRender(compiledTplCode);
5247
+ helperBindings = extracted.helperBindings;
5248
+ renderDecl = extracted.renderDecl;
5249
+ inlineBlock = extracted.inlineBlock;
5250
+ renderOk = extracted.ok;
5251
+ }
5252
+ else {
5253
+ renderOk = true;
5254
+ }
5255
+ }
5256
+ catch (eExtract) {
5257
+ if (verbose) {
5258
+ try {
5259
+ console.warn('[sfc-asm][extractTemplateRender] failed', base, eExtract?.message);
5260
+ }
5261
+ catch { }
5262
+ }
5263
+ }
5264
+ }
5265
+ // Final guard: if no inline render extracted, attempt to import template variant or synthesize a no-op render
5266
+ if (!renderOk && !inlineBlock) {
5267
+ try {
5268
+ const templateUrl = `${origin}/ns/sfc/${ver}${base}?vue&type=template`;
5269
+ const importLine = `import * as __template from ${JSON.stringify(templateUrl)};`;
5270
+ // Attach only if scriptTransformed produces __ns_sfc__ later
5271
+ helperBindings += `\n${importLine}`;
5272
+ renderDecl += `\nfunction __ns_getRender(){\n try {\n if (__template && __template.render) return __template.render;\n } catch (_e) {}\n try {\n const _ = globalThis.createElementVNode || globalThis._createElementVNode;\n return _ ? function(){ return _('StackLayout'); } : function(){ return {}; };\n } catch (_e) { return function(){ return {}; }; }\n}\n`;
5273
+ renderOk = true;
5274
+ }
5275
+ catch { }
5276
+ }
5277
+ // 5) Convert default export to const __ns_sfc__
5278
+ let scriptTransformed = scriptBody;
5279
+ if (scriptTransformed) {
5280
+ scriptTransformed = scriptTransformed.replace(/(^|\n)\s*export\s+default\s+/g, '$1const __ns_sfc__ = ').replace(/(^|\n)\s*export\s*\{[^}]*\}\s*;?\s*/g, '\n/* removed named exports for inline asm */\n');
5281
+ // Normalize any prior declaration of __ns_sfc__ to a plain assignment to avoid redeclare
5282
+ // Accept a semicolon before the declaration too
5283
+ scriptTransformed = scriptTransformed.replace(/(^|[\n;])\s*(?:const|let|var)\s+__ns_sfc__\s*=\s*/g, '$1__ns_sfc__ = ');
5284
+ // Ensure a single declaration appears once before first assignment
5285
+ if (!/(^|[\n;])\s*(?:const|let|var)\s+__ns_sfc__\b/.test(scriptTransformed)) {
5286
+ scriptTransformed = `let __ns_sfc__;\n` + scriptTransformed;
5287
+ }
5288
+ // Remove stray leading braces (artifact defense)
5289
+ scriptTransformed = scriptTransformed.replace(/^\s*\}+(?=\s*[^}])/, (m) => `/* [asm-fix] removed ${m.length} stray leading braces */\n`);
3317
5290
  }
3318
5291
  else {
3319
- const transformed = await sharedTransformRequest(modulePath);
3320
- if (transformed?.code) {
3321
- const expandedModuleCode = await expandStarExports(transformed.code, server, projectRoot, verbose);
3322
- code = buildVersionedCoreMainBridgeModule(key, ver, extractExportedNames(expandedModuleCode));
5292
+ try {
5293
+ const compName = (base.split('/').pop() || 'Component').replace(/\.vue$/i, '') || 'Component';
5294
+ scriptTransformed = `import { defineComponent as _defineComponent } from "/ns/rt";\nlet __ns_sfc__;\n__ns_sfc__ = /*@__PURE__*/_defineComponent({ __name: ${JSON.stringify(compName)} });`;
5295
+ }
5296
+ catch {
5297
+ scriptTransformed = `import { defineComponent as _defineComponent } from "/ns/rt";\nlet __ns_sfc__;\n__ns_sfc__ = /*@__PURE__*/_defineComponent({});`;
3323
5298
  }
3324
5299
  }
3325
- }
3326
- catch (e) {
5300
+ // 6) Emit final inline module with diagnostics comment
5301
+ const parts = [];
5302
+ parts.push(`// [sfc-asm] ${base} (inline-compiled)`);
5303
+ // Deterministic path: always use extracted helperBindings + renderDecl + scriptTransformed (ignore inlineBlock)
5304
+ // Emit hoisted template bindings first
5305
+ if (helperBindings)
5306
+ parts.push(helperBindings);
5307
+ // IMPORTANT: place script (with its imports) BEFORE renderDecl so imports never appear inside the render function.
5308
+ parts.push(scriptTransformed);
5309
+ parts.push(renderDecl);
5310
+ parts.push(`try { if (!__ns_sfc__.render) Object.defineProperty(__ns_sfc__, 'render', { configurable: true, enumerable: true, get(){ const r = (typeof __ns_getRender==='function' ? __ns_getRender() : undefined); Object.defineProperty(__ns_sfc__, 'render', { value: r, writable: true, configurable: true, enumerable: true }); return r; }, set(v){ Object.defineProperty(__ns_sfc__, 'render', { value: v, writable: true, configurable: true, enumerable: true }); } }); } catch(_e){}`);
5311
+ parts.push(`export function render(){ const f = (typeof __ns_getRender==='function' ? __ns_getRender() : (__ns_sfc__ && __ns_sfc__.render)); return typeof f==='function' ? f.apply(this, arguments) : undefined; }`);
5312
+ parts.push(`export default __ns_sfc__`);
5313
+ let inlineCode = parts.filter(Boolean).join('\n');
5314
+ inlineCode = processCodeForDevice(inlineCode, false, true);
3327
5315
  try {
3328
- console.warn('[ns-core-bridge] main bridge export discovery failed:', e?.message);
5316
+ inlineCode = ensureVersionedCoreImports(inlineCode, getServerOrigin(server), Number(ver));
3329
5317
  }
3330
5318
  catch { }
5319
+ try {
5320
+ inlineCode = ensureDestructureCoreImports(inlineCode);
5321
+ }
5322
+ catch { }
5323
+ // Replace legacy mutation pipeline with canonical assembler for reliability
5324
+ {
5325
+ // First: strip TypeScript robustly using Babel transform
5326
+ try {
5327
+ const tsRes = await babelCore.transformAsync(scriptTransformed, {
5328
+ plugins: [[pluginTransformTypescript, { allowDeclareFields: true }]],
5329
+ ast: false,
5330
+ sourceType: 'module',
5331
+ configFile: false,
5332
+ babelrc: false,
5333
+ });
5334
+ if (tsRes?.code)
5335
+ scriptTransformed = tsRes.code;
5336
+ }
5337
+ catch (eTs) {
5338
+ if (verbose) {
5339
+ try {
5340
+ console.warn('[sfc-asm][babel-ts][fail]', base, eTs?.message);
5341
+ }
5342
+ catch { }
5343
+ }
5344
+ }
5345
+ // Hoist imports + strip residual TS via AST
5346
+ let importLines = [];
5347
+ try {
5348
+ const astRes = astExtractImportsAndStripTypes(scriptTransformed);
5349
+ importLines = astRes.imports;
5350
+ scriptTransformed = astRes.body;
5351
+ if (astRes.diagnostics.length && verbose) {
5352
+ try {
5353
+ console.warn('[sfc-asm][ast]', base, astRes.diagnostics.join('; '));
5354
+ }
5355
+ catch { }
5356
+ }
5357
+ }
5358
+ catch (eAst) {
5359
+ if (verbose) {
5360
+ try {
5361
+ console.warn('[sfc-asm][ast][fail]', base, eAst?.message);
5362
+ }
5363
+ catch { }
5364
+ }
5365
+ }
5366
+ // Ensure renderDecl ends with closing brace ONLY for function declaration forms
5367
+ // Avoid appending to const-assignment forms like: const __ns_render = (function(){ ... })();
5368
+ if (renderDecl && /(^|\n)\s*(?:export\s+)?function\s+__ns_render\s*\(/.test(renderDecl) && !/\}\s*$/.test(renderDecl)) {
5369
+ renderDecl = renderDecl.trimEnd() + '\n}';
5370
+ }
5371
+ const outParts = [];
5372
+ outParts.push(`// [sfc-asm] ${base} (inline-compiled)`);
5373
+ outParts.push('// [sfc-asm][canonical]');
5374
+ if (importLines.length)
5375
+ outParts.push(Array.from(new Set(importLines)).join('\n'));
5376
+ // Place component script first so the component object exists before we attach render.
5377
+ outParts.push(scriptTransformed);
5378
+ // Prefer full template block to guarantee presence of all hoisted constants.
5379
+ if (inlineBlock) {
5380
+ outParts.push(inlineBlock);
5381
+ }
5382
+ else {
5383
+ if (helperBindings)
5384
+ outParts.push(helperBindings);
5385
+ if (renderDecl && renderDecl.trim())
5386
+ outParts.push(renderDecl);
5387
+ }
5388
+ outParts.push(`try { if (!__ns_sfc__.render) Object.defineProperty(__ns_sfc__, 'render', { configurable: true, enumerable: true, get(){ const r = (typeof __ns_getRender==='function' ? __ns_getRender() : (typeof __ns_render==='function' ? __ns_render : undefined)); Object.defineProperty(__ns_sfc__, 'render', { value: r, writable: true, configurable: true, enumerable: true }); return r; }, set(v){ Object.defineProperty(__ns_sfc__, 'render', { value: v, writable: true, configurable: true, enumerable: true }); } }); } catch(_e){}`);
5389
+ // Export named render as a function that resolves lazily
5390
+ outParts.push('export function render(){ const f = (typeof __ns_getRender==="function" ? __ns_getRender() : (typeof __ns_render==="function" ? __ns_render : (__ns_sfc__ && __ns_sfc__.render))); return typeof f === "function" ? f.apply(this, arguments) : undefined; }');
5391
+ outParts.push('export default __ns_sfc__');
5392
+ let inlineCode2 = outParts.filter(Boolean).join('\n');
5393
+ inlineCode2 = processCodeForDevice(inlineCode2, false, true);
5394
+ try {
5395
+ inlineCode2 = ensureVersionedCoreImports(inlineCode2, getServerOrigin(server), Number(ver));
5396
+ }
5397
+ catch { }
5398
+ try {
5399
+ inlineCode2 = ensureDestructureCoreImports(inlineCode2);
5400
+ }
5401
+ catch { }
5402
+ // Hoist any late imports that accidentally landed after render or script assembly
5403
+ try {
5404
+ const lateImportRe = /^(?!\/\/).*^\s*import\s+[^;]+;?$/gm;
5405
+ const allImports = [];
5406
+ inlineCode2 = inlineCode2.replace(lateImportRe, (imp) => {
5407
+ allImports.push(imp);
5408
+ return '';
5409
+ });
5410
+ if (allImports.length) {
5411
+ // Place after helperBindings sentinel
5412
+ inlineCode2 = inlineCode2.replace(/(\/\/ \[sfc-asm\]\[canonical\]\n)/, `$1${Array.from(new Set(allImports)).join('\n')}\n/* [asm-fix] re-hoisted ${allImports.length} imports */\n`);
5413
+ }
5414
+ }
5415
+ catch { }
5416
+ // After hoisting, re-run AST normalization and duplicate-binding verification.
5417
+ // This guards against freshly hoisted imports reintroducing identifiers that collide
5418
+ // with earlier destructures (e.g., __ns_core_ns_1), which would otherwise surface at device runtime.
5419
+ try {
5420
+ inlineCode2 = astNormalizeModuleImportsAndHelpers(inlineCode2);
5421
+ }
5422
+ catch { }
5423
+ try {
5424
+ inlineCode2 = astVerifyAndAnnotateDuplicates(inlineCode2);
5425
+ if (/^\s*\/\/ \[ast-verify\]\[duplicate-bindings\]/m.test(inlineCode2)) {
5426
+ const diagnosticLine = (inlineCode2.match(/^\s*\/\/ \[ast-verify\]\[duplicate-bindings\][^\n]*/m) || [])[0] || '// [ast-verify][duplicate-bindings]';
5427
+ const brief = diagnosticLine.replace(/^[^:]*:?\s?/, '');
5428
+ const escaped = brief.replace(/["\\]/g, '\\$&');
5429
+ const thrower = `throw new Error("[nsv-hmr] Duplicate top-level bindings detected post-hoist: ${escaped}");`;
5430
+ inlineCode2 = `${thrower}\n` + inlineCode2;
5431
+ }
5432
+ }
5433
+ catch { }
5434
+ // Minimal cleanup only (avoid destructive type stripping breaking object literal property defaults)
5435
+ try {
5436
+ // Heal cases where a TS type strip earlier removed initializer: plain 'default' inside props objects
5437
+ // becomes 'default: undefined'. We only match when followed by ',' or '}' or newline to avoid 'export default'.
5438
+ inlineCode2 = inlineCode2.replace(/\bdefault\b\s*(?=\}|,|\n)/g, 'default: undefined');
5439
+ // Remove obvious leftover angle generic markers
5440
+ inlineCode2 = inlineCode2.replace(/<unknown>/g, '');
5441
+ // Fix accidental '}=> {' sequences
5442
+ inlineCode2 = inlineCode2.replace(/}\s*=>\s*\{/g, '');
5443
+ // No-op: removed prior broken normalization. Handlers are fixed in the dedicated passes below.
5444
+ }
5445
+ catch { }
5446
+ // Removed redundant render closure heal that could inject an extra '}' before component script.
5447
+ // Rewrite any remaining imports (e.g., relative app paths) to HTTP ESM endpoints
5448
+ try {
5449
+ inlineCode2 = rewriteImports(inlineCode2, base, sfcFileMap, depFileMap, projectRoot, !!verbose, undefined, getServerOrigin(server));
5450
+ }
5451
+ catch { }
5452
+ // Final TS strip on the whole assembled module (safety net)
5453
+ try {
5454
+ const tsFinal = await babelCore.transformAsync(inlineCode2, {
5455
+ plugins: [[pluginTransformTypescript, { allowDeclareFields: true }]],
5456
+ ast: false,
5457
+ sourceType: 'module',
5458
+ configFile: false,
5459
+ babelrc: false,
5460
+ });
5461
+ if (tsFinal?.code)
5462
+ inlineCode2 = tsFinal.code;
5463
+ }
5464
+ catch { }
5465
+ // Heal Vue v-model update handlers that lost the ": else" branch during transforms:
5466
+ // "onUpdate:modelValue": _cache[N] || (_cache[N] = $event => _isRef(name) ? name.value = $event)
5467
+ // → add else branch to keep syntax valid: : (name = $event)
5468
+ try {
5469
+ // Fix missing else branch on v-model handlers: support dotted expressions (e.g., $setup.acceptTerms)
5470
+ const reMissingElse = /\"onUpdate:modelValue\"\s*:\s*_cache\[(\d+)\]\s*\|\|\s*\(_cache\[\1\]\s*=\s*\$event\s*=>\s*_isRef\(\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\)\s*\?\s*\2\.value\s*=\s*\$event\s*\)/g;
5471
+ inlineCode2 = inlineCode2.replace(reMissingElse, (_m, idx, expr) => {
5472
+ return `\"onUpdate:modelValue\": _cache[${idx}] || (_cache[${idx}] = $event => (_isRef(${expr}) ? (${expr}.value = $event) : (${expr} = $event)))`;
5473
+ });
5474
+ // Repair malformed handlers without an arrow (introduced by previous transforms):
5475
+ // Convert pattern assigning to $event without an arrow into a proper arrow using the same target expression.
5476
+ const reMalformed = /\"onUpdate:modelValue\"\s*:\s*_cache\[(\d+)\]\s*\|\|\s*\(_cache\[\1\]\s*=\s*[^=]*\(\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\)[^=]*=\s*\$event\s*\)\s*\)/g;
5477
+ inlineCode2 = inlineCode2.replace(reMalformed, (_m, idx, expr) => {
5478
+ return `\"onUpdate:modelValue\": _cache[${idx}] || (_cache[${idx}] = $event => (_isRef(${expr}) ? (${expr}.value = $event) : (${expr} = $event)))`;
5479
+ });
5480
+ }
5481
+ catch { }
5482
+ // Structural heal: ensure balanced braces before the first import statement
5483
+ try {
5484
+ const idx = inlineCode2.search(/^[\t ]*import\b/m);
5485
+ if (idx > 0) {
5486
+ const prefix = inlineCode2.slice(0, idx);
5487
+ let open = 0, close = 0;
5488
+ let inS = false, inD = false, inT = false, inLC = false, inBC = false;
5489
+ for (let i = 0; i < prefix.length; i++) {
5490
+ const ch = prefix[i], nx = prefix[i + 1];
5491
+ if (inLC) {
5492
+ if (ch === '\n')
5493
+ inLC = false;
5494
+ continue;
5495
+ }
5496
+ if (inBC) {
5497
+ if (ch === '*' && nx === '/') {
5498
+ inBC = false;
5499
+ i++;
5500
+ }
5501
+ continue;
5502
+ }
5503
+ if (inS) {
5504
+ if (ch === '\\') {
5505
+ i++;
5506
+ continue;
5507
+ }
5508
+ if (ch === "'")
5509
+ inS = false;
5510
+ continue;
5511
+ }
5512
+ if (inD) {
5513
+ if (ch === '\\') {
5514
+ i++;
5515
+ continue;
5516
+ }
5517
+ if (ch === '"')
5518
+ inD = false;
5519
+ continue;
5520
+ }
5521
+ if (inT) {
5522
+ if (ch === '\\') {
5523
+ i++;
5524
+ continue;
5525
+ }
5526
+ if (ch === '`')
5527
+ inT = false;
5528
+ continue;
5529
+ }
5530
+ if (ch === '/' && nx === '/') {
5531
+ inLC = true;
5532
+ i++;
5533
+ continue;
5534
+ }
5535
+ if (ch === '/' && nx === '*') {
5536
+ inBC = true;
5537
+ i++;
5538
+ continue;
5539
+ }
5540
+ if (ch === "'") {
5541
+ inS = true;
5542
+ continue;
5543
+ }
5544
+ if (ch === '"') {
5545
+ inD = true;
5546
+ continue;
5547
+ }
5548
+ if (ch === '`') {
5549
+ inT = true;
5550
+ continue;
5551
+ }
5552
+ if (ch === '{')
5553
+ open++;
5554
+ else if (ch === '}')
5555
+ close++;
5556
+ }
5557
+ const missing = open - close;
5558
+ if (missing > 0) {
5559
+ inlineCode2 = inlineCode2.slice(0, idx) + '}'.repeat(missing) + '\n' + inlineCode2.slice(idx);
5560
+ }
5561
+ }
5562
+ }
5563
+ catch { }
5564
+ // Final TS strip on the whole assembled module (safety net)
5565
+ try {
5566
+ const tsFinal = await babelCore.transformAsync(inlineCode2, {
5567
+ plugins: [[pluginTransformTypescript, { allowDeclareFields: true }]],
5568
+ ast: false,
5569
+ sourceType: 'module',
5570
+ configFile: false,
5571
+ babelrc: false,
5572
+ });
5573
+ if (tsFinal?.code)
5574
+ inlineCode2 = tsFinal.code;
5575
+ }
5576
+ catch { }
5577
+ inlineCode2 = ensureVariableDynamicImportHelper(inlineCode2);
5578
+ inlineCode2 = ensureGuardPlainDynamicImports(inlineCode2, origin);
5579
+ inlineCode2 = REQUIRE_GUARD_SNIPPET + inlineCode2;
5580
+ // If no render materialized, return a clear error module for deterministic failure
5581
+ try {
5582
+ const lacksRender = !/__ns_render\b/.test(inlineCode2) && !/__ns_sfc__\.render\s*=/.test(inlineCode2);
5583
+ if (lacksRender) {
5584
+ const err = `throw new Error(\"[sfc-asm] ${base}: no render generated by assembler\");\nexport default {};`;
5585
+ res.statusCode = 200;
5586
+ res.end(err);
5587
+ return;
5588
+ }
5589
+ }
5590
+ catch { }
5591
+ // Cosmetic and parser-friendly: ensure a newline after the canonical banner
5592
+ try {
5593
+ inlineCode2 = inlineCode2.replace(/(\/\/ \[sfc-asm\]\[canonical\])(?!\n)/, '$1\n');
5594
+ }
5595
+ catch { }
5596
+ // Bust device cache for runtime bridge so helpers are always current for this graph version
5597
+ try {
5598
+ const origin = getServerOrigin(server);
5599
+ inlineCode2 = ensureVersionedRtImports(inlineCode2, origin, Number(ver));
5600
+ inlineCode2 = ACTIVE_STRATEGY.ensureVersionedImports(inlineCode2, origin, Number(ver));
5601
+ inlineCode2 = ensureVersionedCoreImports(inlineCode2, origin, Number(ver));
5602
+ }
5603
+ catch { }
5604
+ // Normalize imports/helpers via AST to ensure _defineComponent and other helpers are bound once
5605
+ try {
5606
+ inlineCode2 = astNormalizeModuleImportsAndHelpers(inlineCode2);
5607
+ }
5608
+ catch { }
5609
+ // Guarantee a concrete component object exists before exporting default.
5610
+ try {
5611
+ // Detect an existing declaration of __ns_sfc__ even if it's appended after a semicolon on the same line
5612
+ // e.g., "import ...;let __ns_sfc__;" (no newline). Accept start-of-string, newline, or semicolon as anchors.
5613
+ const hasDecl = /(^|[\n;])\s*(?:const|let|var)\s+__ns_sfc__\b/.test(inlineCode2);
5614
+ if (!hasDecl) {
5615
+ inlineCode2 = inlineCode2.replace(/(\/\/ \[sfc-asm\]\[canonical\]\n)/, `$1let __ns_sfc__ = {};\n`);
5616
+ }
5617
+ // Heal empty declarations (e.g., "let __ns_sfc__;" → initialize to {}), also when preceded by a semicolon
5618
+ inlineCode2 = inlineCode2.replace(/(^|[\n;])\s*let\s+__ns_sfc__\s*;?/g, '$1let __ns_sfc__ = {};');
5619
+ inlineCode2 = inlineCode2.replace(/(^|[\n;])\s*var\s+__ns_sfc__\s*;?/g, '$1var __ns_sfc__ = {};');
5620
+ }
5621
+ catch { }
5622
+ if (!/export\s+default\s+__ns_sfc__/.test(inlineCode2) && /__ns_sfc__/.test(inlineCode2))
5623
+ inlineCode2 += '\nexport default __ns_sfc__';
5624
+ res.statusCode = 200;
5625
+ res.end(inlineCode2);
5626
+ return;
5627
+ }
5628
+ }
5629
+ }
5630
+ catch { }
5631
+ // Do not use compiled ?vue or variant fallbacks; assembler must succeed or emit an error
5632
+ // Prefer compiling template from source via compiler-sfc; fallback to variant extraction
5633
+ let inlineOk = false;
5634
+ let helperBindings = '';
5635
+ let renderDecl = '';
5636
+ let inlineBlock = undefined;
5637
+ try {
5638
+ const root = server.config?.root || process.cwd();
5639
+ const abs = path.join(root, base.replace(/^\//, ''));
5640
+ let sfcSrc = '';
5641
+ try {
5642
+ sfcSrc = readFileSync(abs, 'utf-8');
5643
+ }
5644
+ catch { }
5645
+ if (sfcSrc) {
5646
+ const { descriptor } = parse(sfcSrc, { filename: abs });
5647
+ const tpl = descriptor.template?.content || '';
5648
+ if (tpl) {
5649
+ const id = createHash('md5').update(abs).digest('hex').slice(0, 8);
5650
+ const ct = compileTemplate({
5651
+ source: tpl,
5652
+ id,
5653
+ filename: abs,
5654
+ isProd: false,
5655
+ ssr: false,
5656
+ compilerOptions: {
5657
+ isCustomElement: (tag) => NS_NATIVE_TAGS.has(tag),
5658
+ },
5659
+ });
5660
+ let compiled = (ct && (ct.code || '')) || '';
5661
+ if (compiled) {
5662
+ // Prefer a full inline template block preserving hoists
5663
+ inlineBlock = buildInlineTemplateBlock(compiled) || undefined;
5664
+ if (inlineBlock) {
5665
+ inlineOk = true;
5666
+ }
5667
+ else {
5668
+ const extracted = extractTemplateRender(compiled);
5669
+ inlineOk = extracted.ok;
5670
+ helperBindings = extracted.helperBindings;
5671
+ renderDecl = extracted.renderDecl;
5672
+ inlineBlock = extracted.inlineBlock;
5673
+ }
5674
+ }
5675
+ }
3331
5676
  }
3332
5677
  }
5678
+ catch { }
5679
+ // If compiler-sfc path didn't succeed, attempt variant extraction once
5680
+ if (!inlineOk) {
5681
+ const extracted = extractTemplateRender(templateCode);
5682
+ inlineOk = extracted.ok;
5683
+ helperBindings = extracted.helperBindings;
5684
+ renderDecl = extracted.renderDecl;
5685
+ inlineBlock = extracted.inlineBlock;
5686
+ }
5687
+ let asm;
5688
+ if (inlineOk) {
5689
+ if (inlineBlock && inlineBlock.trim()) {
5690
+ asm = [`// [sfc-asm] ${base} (inlined template body)`, `export * from ${JSON.stringify(scriptUrl)};`, `import * as __script from ${JSON.stringify(scriptUrl)};`, inlineBlock, `const __ns_sfc__ = (__script && __script.default) ? __script.default : {};`, `try { if (typeof __ns_render === 'function' && !__ns_sfc__.render) __ns_sfc__.render = __ns_render; } catch {}`, `export default __ns_sfc__;`].join('\n');
5691
+ }
5692
+ else {
5693
+ asm = [`// [sfc-asm] ${base} (inlined template)`, `export * from ${JSON.stringify(scriptUrl)};`, `import * as __script from ${JSON.stringify(scriptUrl)};`, helperBindings, renderDecl, `const __ns_sfc__ = (__script && __script.default) ? __script.default : {};`, `try { if (typeof __ns_render === 'function' && !__ns_sfc__.render) __ns_sfc__.render = __ns_render; } catch {}`, `export default __ns_sfc__;`].filter(Boolean).join('\n');
5694
+ }
5695
+ }
5696
+ else {
5697
+ // Deterministic error path when template extraction failed
5698
+ res.statusCode = 500;
5699
+ res.end(`throw new Error('[sfc-asm] ${base}: template extraction failed');\nexport default {};`);
5700
+ return;
5701
+ }
5702
+ // Run full device processing so helper aliasing and globals are consistent in this path too
5703
+ let code = REQUIRE_GUARD_SNIPPET + asm;
5704
+ code = processCodeForDevice(code, false, true, /(?:^|\/)node_modules\//.test(base), base);
5705
+ try {
5706
+ code = ensureVersionedCoreImports(code, getServerOrigin(server), Number(ver));
5707
+ }
5708
+ catch { }
5709
+ code = rewriteImports(code, base, sfcFileMap, depFileMap, projectRoot, !!verbose, undefined, getServerOrigin(server));
5710
+ try {
5711
+ code = ensureDestructureCoreImports(code);
5712
+ }
5713
+ catch { }
5714
+ code = ensureVariableDynamicImportHelper(code);
5715
+ code = ensureGuardPlainDynamicImports(code, origin);
5716
+ try {
5717
+ const origin = getServerOrigin(server);
5718
+ code = ensureVersionedRtImports(code, origin, Number(ver));
5719
+ code = ACTIVE_STRATEGY.ensureVersionedImports(code, origin, Number(ver));
5720
+ code = ensureVersionedCoreImports(code, origin, Number(ver));
5721
+ }
5722
+ catch { }
5723
+ // Inline-template body path already runs processCodeForDevice (AST + sanitizers); no additional _defineComponent fix needed
3333
5724
  res.statusCode = 200;
3334
5725
  res.end(code);
3335
5726
  }
3336
5727
  catch (e) {
3337
- next();
5728
+ res.statusCode = 500;
5729
+ res.end('export {}\n');
3338
5730
  }
3339
5731
  });
3340
- registerTxnHandler(server, {
3341
- resolveTxnIds: (version, fallbackChangedIds) => {
3342
- const ids = txnBatches.get(version) || [];
3343
- if (ids.length) {
3344
- return ids;
3345
- }
3346
- return fallbackChangedIds.length ? computeTxnOrderForChanged(fallbackChangedIds) : [];
3347
- },
3348
- });
3349
- registerVueSfcHandlers(server, {
3350
- verbose,
3351
- requireGuardSnippet: REQUIRE_GUARD_SNIPPET,
3352
- appVirtualWithSlash: APP_VIRTUAL_WITH_SLASH,
3353
- sfcFileMap,
3354
- depFileMap,
3355
- getGraphVersion: () => Number(graphVersion || 0),
3356
- getStrategy: () => ACTIVE_STRATEGY,
3357
- getServerOrigin,
3358
- cleanCode,
3359
- processCodeForDevice,
3360
- rewriteImports,
3361
- ensureVariableDynamicImportHelper,
3362
- ensureGuardPlainDynamicImports,
3363
- ensureVersionedRtImports,
3364
- ensureVersionedCoreImports,
3365
- ensureDestructureCoreImports,
3366
- extractExportMetadata,
3367
- });
3368
5732
  wss.on('connection', async (ws) => {
3369
5733
  if (verbose)
3370
5734
  console.log('[hmr-ws] Client connected (dynamic fetch mode)');
@@ -3690,6 +6054,103 @@ export const piniaSymbol = p.piniaSymbol;
3690
6054
  if (isRuntimeGraphExcludedPath(file)) {
3691
6055
  return;
3692
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();
3693
6154
  // Graph update for this file change (wrapped to avoid aborting rest of handler)
3694
6155
  try {
3695
6156
  const skipAngularHtmlGraphUpdate = ACTIVE_STRATEGY.flavor === 'angular' && /\.(html|htm)$/i.test(file);
@@ -3739,6 +6200,7 @@ export const piniaSymbol = p.piniaSymbol;
3739
6200
  console.log(`[hmr-ws] Hot update for: ${file}`);
3740
6201
  // Handle CSS updates
3741
6202
  if (file.endsWith('.css')) {
6203
+ updateMetrics.tAfterFramework = Date.now();
3742
6204
  try {
3743
6205
  let rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
3744
6206
  const origin = getServerOrigin(server);
@@ -3757,12 +6219,14 @@ export const piniaSymbol = p.piniaSymbol;
3757
6219
  wss.clients.forEach((client) => {
3758
6220
  if (isSocketClientOpen(client)) {
3759
6221
  client.send(JSON.stringify(msg));
6222
+ updateMetrics.recipients += 1;
3760
6223
  }
3761
6224
  });
3762
6225
  }
3763
6226
  catch (error) {
3764
6227
  console.warn('[hmr-ws] CSS update failed:', error);
3765
6228
  }
6229
+ emitHmrUpdateSummary();
3766
6230
  return;
3767
6231
  }
3768
6232
  // Framework-specific hot update handling
@@ -3778,6 +6242,7 @@ export const piniaSymbol = p.piniaSymbol;
3778
6242
  });
3779
6243
  if (!(isHtml || isTs))
3780
6244
  return;
6245
+ updateMetrics.invalidated += angularHotUpdateRoots.length;
3781
6246
  if (angularHotUpdateRoots.length) {
3782
6247
  for (const mod of angularHotUpdateRoots) {
3783
6248
  try {
@@ -3794,13 +6259,80 @@ export const piniaSymbol = p.piniaSymbol;
3794
6259
  }
3795
6260
  }
3796
6261
  const angularTransitiveInvalidationRoots = (angularHotUpdateRoots.length ? angularHotUpdateRoots : ctx.modules);
3797
- if (shouldInvalidateAngularTransitiveImporters({ flavor: ACTIVE_STRATEGY.flavor, file })) {
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) {
3798
6271
  try {
3799
- const transitiveImporters = collectAngularTransitiveImportersForInvalidation({
3800
- modules: angularTransitiveInvalidationRoots,
3801
- isExcluded: (id) => id.includes('/node_modules/'),
3802
- maxDepth: 16,
3803
- });
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;
3804
6336
  for (const mod of transitiveImporters) {
3805
6337
  try {
3806
6338
  server.moduleGraph.invalidateModule(mod);
@@ -3815,24 +6347,40 @@ export const piniaSymbol = p.piniaSymbol;
3815
6347
  console.log('[hmr-ws][angular] invalidated transitive importers:', transitiveImporters.length);
3816
6348
  }
3817
6349
  }
3818
- catch (error) {
3819
- if (verbose)
3820
- console.warn('[hmr-ws][angular] transitive importer collection failed', error);
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)`);
3821
6365
  }
3822
6366
  }
6367
+ catch (error) {
6368
+ if (verbose)
6369
+ console.warn('[hmr-ws][angular] transitive importer collection failed', error);
6370
+ }
3823
6371
  try {
3824
- const transitiveImporters = shouldInvalidateAngularTransitiveImporters({ flavor: ACTIVE_STRATEGY.flavor, file })
3825
- ? collectAngularTransitiveImportersForInvalidation({
3826
- modules: angularTransitiveInvalidationRoots,
3827
- isExcluded: (id) => id.includes('/node_modules/'),
3828
- maxDepth: 16,
3829
- })
3830
- : [];
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).
3831
6379
  const transformCacheInvalidationUrls = new Set(collectAngularTransformCacheInvalidationUrls({
3832
6380
  file,
3833
6381
  isTs,
3834
6382
  hotUpdateRoots: angularHotUpdateRoots,
3835
- transitiveImporters,
6383
+ transitiveImporters: angularNeedsTransitive ? transitiveImporters : [],
3836
6384
  projectRoot: server.config.root || process.cwd(),
3837
6385
  }));
3838
6386
  if (transformCacheInvalidationUrls.size) {
@@ -3846,17 +6394,74 @@ export const piniaSymbol = p.piniaSymbol;
3846
6394
  if (verbose)
3847
6395
  console.warn('[hmr-ws][angular] shared transform cache purge failed', error);
3848
6396
  }
6397
+ updateMetrics.tAfterFramework = Date.now();
3849
6398
  try {
3850
6399
  const root = server.config.root || process.cwd();
3851
6400
  const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
3852
6401
  rememberAngularReloadSuppression(root, file);
3853
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
+ }
3854
6457
  const msg = {
3855
6458
  type: 'ns:angular-update',
3856
6459
  origin,
3857
6460
  path: rel,
3858
6461
  version: graphVersion,
3859
6462
  timestamp: Date.now(),
6463
+ evictPaths,
6464
+ importerEntry: bootstrapEntryRel,
3860
6465
  };
3861
6466
  if (verbose) {
3862
6467
  console.log('[hmr-ws][angular] broadcasting update', Array.from(wss.clients || []).map((client) => ({
@@ -3868,12 +6473,14 @@ export const piniaSymbol = p.piniaSymbol;
3868
6473
  wss.clients.forEach((client) => {
3869
6474
  if (isSocketClientOpen(client)) {
3870
6475
  client.send(JSON.stringify(msg));
6476
+ updateMetrics.recipients += 1;
3871
6477
  }
3872
6478
  });
3873
6479
  }
3874
6480
  catch (error) {
3875
6481
  console.warn('[hmr-ws][angular] update failed:', error);
3876
6482
  }
6483
+ emitHmrUpdateSummary();
3877
6484
  if (shouldSuppressDefaultViteHotUpdate({ flavor: ACTIVE_STRATEGY.flavor, file })) {
3878
6485
  return [];
3879
6486
  }
@@ -3881,6 +6488,7 @@ export const piniaSymbol = p.piniaSymbol;
3881
6488
  }
3882
6489
  // TypeScript flavor: emit generic graph delta for app XML/TS/style changes
3883
6490
  if (ACTIVE_STRATEGY.flavor === 'typescript') {
6491
+ updateMetrics.tAfterFramework = Date.now();
3884
6492
  try {
3885
6493
  const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
3886
6494
  if (verbose)
@@ -3894,6 +6502,7 @@ export const piniaSymbol = p.piniaSymbol;
3894
6502
  if (verbose)
3895
6503
  console.warn('[hmr-ws][ts] failed to emit delta for', file, e);
3896
6504
  }
6505
+ emitHmrUpdateSummary();
3897
6506
  return;
3898
6507
  }
3899
6508
  // Solid flavor: emit graph delta for app TSX/TS/JSX file changes.
@@ -3907,6 +6516,7 @@ export const piniaSymbol = p.piniaSymbol;
3907
6516
  const isSolidFile = /\.(tsx?|jsx?)$/i.test(file);
3908
6517
  if (!isSolidFile)
3909
6518
  return;
6519
+ updateMetrics.tAfterFramework = Date.now();
3910
6520
  try {
3911
6521
  const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
3912
6522
  if (verbose)
@@ -3931,6 +6541,7 @@ export const piniaSymbol = p.piniaSymbol;
3931
6541
  if (verbose)
3932
6542
  console.warn('[hmr-ws][solid] failed to handle hot update for', file, e);
3933
6543
  }
6544
+ emitHmrUpdateSummary();
3934
6545
  return;
3935
6546
  }
3936
6547
  // Handle .vue file updates
@@ -4216,6 +6827,10 @@ if (typeof __VUE_HMR_RUNTIME__ === 'undefined') {
4216
6827
  console.warn('[hmr-ws] HMR update failed:', error);
4217
6828
  console.error(error);
4218
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();
4219
6834
  // CRITICAL: Return empty array to prevent Vite's default HMR
4220
6835
  return [];
4221
6836
  },