@nativescript/vite 8.0.0-alpha.1 → 8.0.0-alpha.10

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 (220) hide show
  1. package/configuration/angular.d.ts +1 -1
  2. package/configuration/angular.js +486 -140
  3. package/configuration/angular.js.map +1 -1
  4. package/configuration/base.js +159 -29
  5. package/configuration/base.js.map +1 -1
  6. package/configuration/javascript.js +3 -3
  7. package/configuration/javascript.js.map +1 -1
  8. package/configuration/solid.js +7 -0
  9. package/configuration/solid.js.map +1 -1
  10. package/configuration/typescript.js +4 -4
  11. package/configuration/typescript.js.map +1 -1
  12. package/helpers/angular/angular-linker.js +38 -42
  13. package/helpers/angular/angular-linker.js.map +1 -1
  14. package/helpers/angular/inject-component-hmr-registration.d.ts +112 -0
  15. package/helpers/angular/inject-component-hmr-registration.js +359 -0
  16. package/helpers/angular/inject-component-hmr-registration.js.map +1 -0
  17. package/helpers/angular/inline-decorator-component-templates.d.ts +3 -0
  18. package/helpers/angular/inline-decorator-component-templates.js +400 -0
  19. package/helpers/angular/inline-decorator-component-templates.js.map +1 -0
  20. package/helpers/angular/shared-linker.d.ts +7 -0
  21. package/helpers/angular/shared-linker.js +37 -1
  22. package/helpers/angular/shared-linker.js.map +1 -1
  23. package/helpers/angular/synthesize-decorator-ctor-parameters.d.ts +1 -0
  24. package/helpers/angular/synthesize-decorator-ctor-parameters.js +256 -0
  25. package/helpers/angular/synthesize-decorator-ctor-parameters.js.map +1 -0
  26. package/helpers/angular/synthesize-injectable-factories.d.ts +3 -0
  27. package/helpers/angular/synthesize-injectable-factories.js +414 -0
  28. package/helpers/angular/synthesize-injectable-factories.js.map +1 -0
  29. package/helpers/angular/util.d.ts +1 -0
  30. package/helpers/angular/util.js +88 -0
  31. package/helpers/angular/util.js.map +1 -1
  32. package/helpers/commonjs-plugins.d.ts +5 -2
  33. package/helpers/commonjs-plugins.js +126 -0
  34. package/helpers/commonjs-plugins.js.map +1 -1
  35. package/helpers/config-as-json.js +10 -0
  36. package/helpers/config-as-json.js.map +1 -1
  37. package/helpers/esbuild-platform-resolver.js +5 -5
  38. package/helpers/esbuild-platform-resolver.js.map +1 -1
  39. package/helpers/external-configs.d.ts +9 -1
  40. package/helpers/external-configs.js +31 -6
  41. package/helpers/external-configs.js.map +1 -1
  42. package/helpers/global-defines.d.ts +51 -0
  43. package/helpers/global-defines.js +77 -0
  44. package/helpers/global-defines.js.map +1 -1
  45. package/helpers/import-meta-path.d.ts +4 -0
  46. package/helpers/import-meta-path.js +5 -0
  47. package/helpers/import-meta-path.js.map +1 -0
  48. package/helpers/import-specifier.d.ts +1 -0
  49. package/helpers/import-specifier.js +18 -0
  50. package/helpers/import-specifier.js.map +1 -0
  51. package/helpers/logging.d.ts +1 -0
  52. package/helpers/logging.js +63 -3
  53. package/helpers/logging.js.map +1 -1
  54. package/helpers/main-entry.d.ts +5 -2
  55. package/helpers/main-entry.js +365 -116
  56. package/helpers/main-entry.js.map +1 -1
  57. package/helpers/nativeclass-transform.js +8 -127
  58. package/helpers/nativeclass-transform.js.map +1 -1
  59. package/helpers/nativeclass-transformer-plugin.d.ts +19 -1
  60. package/helpers/nativeclass-transformer-plugin.js +318 -36
  61. package/helpers/nativeclass-transformer-plugin.js.map +1 -1
  62. package/helpers/ns-core-url.d.ts +83 -0
  63. package/helpers/ns-core-url.js +167 -0
  64. package/helpers/ns-core-url.js.map +1 -0
  65. package/helpers/prelink-angular.js +1 -4
  66. package/helpers/prelink-angular.js.map +1 -1
  67. package/helpers/project.d.ts +35 -0
  68. package/helpers/project.js +120 -2
  69. package/helpers/project.js.map +1 -1
  70. package/helpers/ts-config-paths.js +50 -2
  71. package/helpers/ts-config-paths.js.map +1 -1
  72. package/helpers/workers.d.ts +20 -19
  73. package/helpers/workers.js +620 -3
  74. package/helpers/workers.js.map +1 -1
  75. package/hmr/client/css-handler.js +60 -19
  76. package/hmr/client/css-handler.js.map +1 -1
  77. package/hmr/client/hmr-pending-overlay.d.ts +27 -0
  78. package/hmr/client/hmr-pending-overlay.js +50 -0
  79. package/hmr/client/hmr-pending-overlay.js.map +1 -0
  80. package/hmr/client/index.js +597 -24
  81. package/hmr/client/index.js.map +1 -1
  82. package/hmr/client/utils.d.ts +5 -0
  83. package/hmr/client/utils.js +212 -21
  84. package/hmr/client/utils.js.map +1 -1
  85. package/hmr/entry-runtime.d.ts +10 -0
  86. package/hmr/entry-runtime.js +330 -42
  87. package/hmr/entry-runtime.js.map +1 -1
  88. package/hmr/frameworks/angular/client/index.d.ts +3 -1
  89. package/hmr/frameworks/angular/client/index.js +821 -25
  90. package/hmr/frameworks/angular/client/index.js.map +1 -1
  91. package/hmr/frameworks/angular/server/linker.js +37 -6
  92. package/hmr/frameworks/angular/server/linker.js.map +1 -1
  93. package/hmr/frameworks/angular/server/strategy.js +30 -6
  94. package/hmr/frameworks/angular/server/strategy.js.map +1 -1
  95. package/hmr/frameworks/typescript/server/strategy.js +8 -2
  96. package/hmr/frameworks/typescript/server/strategy.js.map +1 -1
  97. package/hmr/frameworks/vue/client/index.js +18 -42
  98. package/hmr/frameworks/vue/client/index.js.map +1 -1
  99. package/hmr/helpers/ast-normalizer.js +22 -10
  100. package/hmr/helpers/ast-normalizer.js.map +1 -1
  101. package/hmr/helpers/cjs-named-exports.d.ts +23 -0
  102. package/hmr/helpers/cjs-named-exports.js +152 -0
  103. package/hmr/helpers/cjs-named-exports.js.map +1 -0
  104. package/hmr/server/constants.d.ts +1 -0
  105. package/hmr/server/constants.js +14 -3
  106. package/hmr/server/constants.js.map +1 -1
  107. package/hmr/server/core-sanitize.d.ts +49 -2
  108. package/hmr/server/core-sanitize.js +267 -24
  109. package/hmr/server/core-sanitize.js.map +1 -1
  110. package/hmr/server/import-map.d.ts +65 -0
  111. package/hmr/server/import-map.js +222 -0
  112. package/hmr/server/import-map.js.map +1 -0
  113. package/hmr/server/index.d.ts +2 -1
  114. package/hmr/server/index.js.map +1 -1
  115. package/hmr/server/ns-core-cjs-shape.d.ts +204 -0
  116. package/hmr/server/ns-core-cjs-shape.js +271 -0
  117. package/hmr/server/ns-core-cjs-shape.js.map +1 -0
  118. package/hmr/server/perf-instrumentation.d.ts +114 -0
  119. package/hmr/server/perf-instrumentation.js +195 -0
  120. package/hmr/server/perf-instrumentation.js.map +1 -0
  121. package/hmr/server/runtime-graph-filter.d.ts +5 -0
  122. package/hmr/server/runtime-graph-filter.js +21 -0
  123. package/hmr/server/runtime-graph-filter.js.map +1 -0
  124. package/hmr/server/shared-transform-request.d.ts +12 -0
  125. package/hmr/server/shared-transform-request.js +144 -0
  126. package/hmr/server/shared-transform-request.js.map +1 -0
  127. package/hmr/server/vite-plugin.d.ts +21 -1
  128. package/hmr/server/vite-plugin.js +461 -22
  129. package/hmr/server/vite-plugin.js.map +1 -1
  130. package/hmr/server/websocket-angular-entry.d.ts +2 -0
  131. package/hmr/server/websocket-angular-entry.js +68 -0
  132. package/hmr/server/websocket-angular-entry.js.map +1 -0
  133. package/hmr/server/websocket-angular-hot-update.d.ts +78 -0
  134. package/hmr/server/websocket-angular-hot-update.js +413 -0
  135. package/hmr/server/websocket-angular-hot-update.js.map +1 -0
  136. package/hmr/server/websocket-core-bridge.d.ts +21 -0
  137. package/hmr/server/websocket-core-bridge.js +357 -0
  138. package/hmr/server/websocket-core-bridge.js.map +1 -0
  139. package/hmr/server/websocket-graph-upsert.d.ts +21 -0
  140. package/hmr/server/websocket-graph-upsert.js +33 -0
  141. package/hmr/server/websocket-graph-upsert.js.map +1 -0
  142. package/hmr/server/websocket-hmr-pending.d.ts +43 -0
  143. package/hmr/server/websocket-hmr-pending.js +55 -0
  144. package/hmr/server/websocket-hmr-pending.js.map +1 -0
  145. package/hmr/server/websocket-module-bindings.d.ts +6 -0
  146. package/hmr/server/websocket-module-bindings.js +471 -0
  147. package/hmr/server/websocket-module-bindings.js.map +1 -0
  148. package/hmr/server/websocket-module-specifiers.d.ts +101 -0
  149. package/hmr/server/websocket-module-specifiers.js +820 -0
  150. package/hmr/server/websocket-module-specifiers.js.map +1 -0
  151. package/hmr/server/websocket-ns-m-finalize.d.ts +22 -0
  152. package/hmr/server/websocket-ns-m-finalize.js +88 -0
  153. package/hmr/server/websocket-ns-m-finalize.js.map +1 -0
  154. package/hmr/server/websocket-ns-m-paths.d.ts +3 -0
  155. package/hmr/server/websocket-ns-m-paths.js +92 -0
  156. package/hmr/server/websocket-ns-m-paths.js.map +1 -0
  157. package/hmr/server/websocket-ns-m-request.d.ts +45 -0
  158. package/hmr/server/websocket-ns-m-request.js +196 -0
  159. package/hmr/server/websocket-ns-m-request.js.map +1 -0
  160. package/hmr/server/websocket-runtime-compat.d.ts +19 -0
  161. package/hmr/server/websocket-runtime-compat.js +287 -0
  162. package/hmr/server/websocket-runtime-compat.js.map +1 -0
  163. package/hmr/server/websocket-served-module-helpers.d.ts +36 -0
  164. package/hmr/server/websocket-served-module-helpers.js +631 -0
  165. package/hmr/server/websocket-served-module-helpers.js.map +1 -0
  166. package/hmr/server/websocket-txn.d.ts +6 -0
  167. package/hmr/server/websocket-txn.js +45 -0
  168. package/hmr/server/websocket-txn.js.map +1 -0
  169. package/hmr/server/websocket-vendor-unifier.d.ts +10 -0
  170. package/hmr/server/websocket-vendor-unifier.js +51 -0
  171. package/hmr/server/websocket-vendor-unifier.js.map +1 -0
  172. package/hmr/server/websocket-vue-sfc.d.ts +27 -0
  173. package/hmr/server/websocket-vue-sfc.js +1069 -0
  174. package/hmr/server/websocket-vue-sfc.js.map +1 -0
  175. package/hmr/server/websocket.d.ts +26 -3
  176. package/hmr/server/websocket.js +2233 -796
  177. package/hmr/server/websocket.js.map +1 -1
  178. package/hmr/shared/package-classifier.d.ts +9 -0
  179. package/hmr/shared/package-classifier.js +58 -0
  180. package/hmr/shared/package-classifier.js.map +1 -0
  181. package/hmr/shared/runtime/boot-timeline.d.ts +17 -0
  182. package/hmr/shared/runtime/boot-timeline.js +51 -0
  183. package/hmr/shared/runtime/boot-timeline.js.map +1 -0
  184. package/hmr/shared/runtime/browser-runtime-contract.d.ts +64 -0
  185. package/hmr/shared/runtime/browser-runtime-contract.js +54 -0
  186. package/hmr/shared/runtime/browser-runtime-contract.js.map +1 -0
  187. package/hmr/shared/runtime/dev-overlay.d.ts +85 -0
  188. package/hmr/shared/runtime/dev-overlay.js +1236 -0
  189. package/hmr/shared/runtime/dev-overlay.js.map +1 -0
  190. package/hmr/shared/runtime/http-only-boot.d.ts +1 -0
  191. package/hmr/shared/runtime/http-only-boot.js +53 -6
  192. package/hmr/shared/runtime/http-only-boot.js.map +1 -1
  193. package/hmr/shared/runtime/module-provenance.d.ts +1 -0
  194. package/hmr/shared/runtime/module-provenance.js +63 -0
  195. package/hmr/shared/runtime/module-provenance.js.map +1 -0
  196. package/hmr/shared/runtime/platform-polyfills.d.ts +26 -0
  197. package/hmr/shared/runtime/platform-polyfills.js +122 -0
  198. package/hmr/shared/runtime/platform-polyfills.js.map +1 -0
  199. package/hmr/shared/runtime/root-placeholder.d.ts +1 -0
  200. package/hmr/shared/runtime/root-placeholder.js +552 -82
  201. package/hmr/shared/runtime/root-placeholder.js.map +1 -1
  202. package/hmr/shared/runtime/session-bootstrap.d.ts +1 -0
  203. package/hmr/shared/runtime/session-bootstrap.js +195 -0
  204. package/hmr/shared/runtime/session-bootstrap.js.map +1 -0
  205. package/hmr/shared/runtime/vendor-bootstrap.js +52 -15
  206. package/hmr/shared/runtime/vendor-bootstrap.js.map +1 -1
  207. package/hmr/shared/vendor/manifest.d.ts +37 -0
  208. package/hmr/shared/vendor/manifest.js +677 -57
  209. package/hmr/shared/vendor/manifest.js.map +1 -1
  210. package/hmr/shared/vendor/registry.js +104 -7
  211. package/hmr/shared/vendor/registry.js.map +1 -1
  212. package/index.d.ts +1 -0
  213. package/index.js +5 -0
  214. package/index.js.map +1 -1
  215. package/package.json +14 -2
  216. package/runtime/core-aliases-early.js +94 -67
  217. package/runtime/core-aliases-early.js.map +1 -1
  218. package/shims/solid-jsx-runtime.d.ts +7 -0
  219. package/shims/solid-jsx-runtime.js +17 -0
  220. package/shims/solid-jsx-runtime.js.map +1 -0
@@ -1,23 +1,24 @@
1
1
  import { createRequire } from 'node:module';
2
- import { normalizeStrayCoreStringLiterals, fixDanglingCoreFrom, normalizeAnyCoreSpecToBridge } from './core-sanitize.js';
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';
6
7
  import babelCore from '@babel/core';
7
- import pluginTransformTypescript from '@babel/plugin-transform-typescript';
8
8
  import traverse from '@babel/traverse';
9
9
  // Ensure traverse callable across CJS/ESM builds
10
10
  const babelTraverse = traverse?.default || traverse;
11
11
  import * as t from '@babel/types';
12
- import { existsSync, readFileSync } from 'fs';
12
+ import { existsSync, readFileSync, statSync } from 'fs';
13
13
  import { astNormalizeModuleImportsAndHelpers, astVerifyAndAnnotateDuplicates } from '../helpers/ast-normalizer.js';
14
+ import { getCjsNamedExports } from '../helpers/cjs-named-exports.js';
14
15
  import { stripRtCoreSentinel, stripDanglingViteCjsImports } from '../helpers/sanitize.js';
15
16
  import { WebSocketServer } from 'ws';
16
17
  import * as path from 'path';
17
18
  import { createHash } from 'crypto';
18
19
  import * as PAT from './constants.js';
19
20
  import { getVendorManifest, resolveVendorSpecifier } from '../shared/vendor/registry.js';
20
- import { getPackageJson, getProjectFilePath } from '../../helpers/project.js';
21
+ import { getMonorepoWorkspaceRoot, getPackageJson, getProjectFilePath, getProjectRootPath } from '../../helpers/project.js';
21
22
  import { loadPrebuiltVendorManifest } from '../shared/vendor/manifest-loader.js';
22
23
  import '../vendor-bootstrap.js';
23
24
  import { NS_NATIVE_TAGS } from './compiler.js';
@@ -30,12 +31,83 @@ import { typescriptServerStrategy } from '../frameworks/typescript/server/strate
30
31
  import { buildInlineTemplateBlock, createProcessSfcCode, extractTemplateRender, processTemplateVariantMinimal } from '../frameworks/vue/server/sfc-transforms.js';
31
32
  import { astExtractImportsAndStripTypes } from '../helpers/ast-extract.js';
32
33
  import { getProjectAppPath, getProjectAppRelativePath, getProjectAppVirtualPath } from '../../helpers/utils.js';
34
+ import { buildRuntimeConfig, generateImportMap } from './import-map.js';
35
+ import { getCliFlags } from '../../helpers/cli-flags.js';
36
+ import { normalizeCoreSub as normalizeCoreSubCanonical } from '../../helpers/ns-core-url.js';
37
+ import { isRuntimeGraphExcludedPath, matchesRuntimeGraphModuleId, normalizeRuntimeGraphPath, shouldIncludeRuntimeGraphFile, shouldSkipRuntimeGraphDirectoryName } from './runtime-graph-filter.js';
38
+ import { resolveAngularCoreHmrImportSource, rewriteAngularEntryRegisterOnly } from './websocket-angular-entry.js';
39
+ import { angularSourceHasSemanticDecorator, canonicalizeTransformRequestCacheKey, collectAngularEvictionUrls, collectAngularHotUpdateRoots, collectAngularTransformCacheInvalidationUrls, collectAngularTransitiveImportersForInvalidation, collectGraphUpdateModulesForHotUpdate, normalizeHotReloadMatchPath, shouldInvalidateAngularTransitiveImporters, shouldSuppressDefaultViteHotUpdate, shouldSuppressViteFullReloadPayload } from './websocket-angular-hot-update.js';
40
+ import { classifyGraphUpsert, shouldBroadcastGraphUpsertDelta, shouldBumpGraphVersion } from './websocket-graph-upsert.js';
41
+ import { classifyBootRoute, classifyHmrUpdateKind, createColdBootRequestCounter, formatHmrUpdateSummary, formatPopulateInitialGraphSummary, formatServerStartupBanner } from './perf-instrumentation.js';
42
+ import { createHmrPendingMessage } from './websocket-hmr-pending.js';
43
+ import { extractVitePrebundleId, filterExistingNodeModulesTransformCandidates, getBlockedDeviceNodeModulesReason, getFlattenedManifestMap, isCoreGlobalsReference, isEsmFrameworkPackageSpecifier, isLikelyNativeScriptPluginSpecifier, isLikelyNativeScriptRuntimePluginSpecifier, isNativeScriptCoreModule, isNativeScriptPluginModule, normalizeNativeScriptCoreSpecifier, normalizeNodeModulesSpecifier, resolveCandidateFilePath, resolveInternalRuntimePluginBareSpecifier, resolveNodeModulesPackageBoundary, resolveVendorFromCandidate, resolveVendorRouting, rewriteFsAbsoluteToNsM, shouldPreserveBareRuntimePluginSubpathImport, stripDecoratedServePrefixes, tryReadRawExplicitJavaScriptModule, viteDepsPathToBareSpecifier, } from './websocket-module-specifiers.js';
44
+ import { ensureNativeScriptModuleBindings, getProcessCodeResolvedSpecifierOverrides } from './websocket-module-bindings.js';
45
+ import { collectStaticExportNamesFromFile, collectStaticExportOriginsFromFile, ensureVersionedCoreImports, extractDirectExportedNames, normalizeCoreExportOriginsForRuntime, parseCoreBridgeRequest, resolveRuntimeCoreModulePath } from './websocket-core-bridge.js';
46
+ import { createSharedTransformRequestRunner } from './shared-transform-request.js';
47
+ import { formatNsMHmrServeTag, getNumericServeVersionTag, rewriteNsMImportPathForHmr } from './websocket-ns-m-paths.js';
48
+ import { ensureDynamicHmrImportHelper } from './websocket-served-module-helpers.js';
49
+ export { ensureNativeScriptModuleBindings, getProcessCodeResolvedSpecifierOverrides } from './websocket-module-bindings.js';
50
+ export { stripDecoratedServePrefixes, tryReadRawExplicitJavaScriptModule } from './websocket-module-specifiers.js';
51
+ export { collectStaticExportNamesFromFile, collectStaticExportOriginsFromFile, ensureVersionedCoreImports, normalizeCoreExportOriginsForRuntime, parseCoreBridgeRequest } from './websocket-core-bridge.js';
52
+ export { rewriteAngularEntryRegisterOnly } from './websocket-angular-entry.js';
53
+ // Re-export the canonical URL rewriter from `websocket-ns-m-paths.js` so the
54
+ // existing test suites (which import from `./websocket.js`) keep working
55
+ // without churn while the implementation lives in a focused module.
56
+ export { formatNsMHmrServeTag, rewriteNsMImportPathForHmr } from './websocket-ns-m-paths.js';
57
+ export { angularSourceHasSemanticDecorator, canonicalizeTransformRequestCacheKey, collectAngularEvictionUrls, collectAngularHotUpdateRoots, collectAngularTransformCacheInvalidationUrls, collectAngularTransitiveImportersForInvalidation, collectGraphUpdateModulesForHotUpdate, createSharedTransformRequestRunner, normalizeHotReloadMatchPath, shouldInvalidateAngularTransitiveImporters, shouldSuppressDefaultViteHotUpdate, shouldSuppressViteFullReloadPayload, classifyGraphUpsert, shouldBroadcastGraphUpsertDelta, shouldBumpGraphVersion };
58
+ const pluginTransformTypescript = (() => {
59
+ const requireFromHere = createRequire(import.meta.url);
60
+ const loaded = requireFromHere('@babel/plugin-transform-typescript');
61
+ return loaded?.default || loaded;
62
+ })();
63
+ // Build a serialized process.env object from CLI --env.* flags.
64
+ // This is injected into every HTTP-served module so app code referencing
65
+ // process.env.TEST_ENV (etc.) works on device in HMR dev mode.
66
+ const __processEnvEntries = { NODE_ENV: 'development' };
67
+ try {
68
+ const flags = getCliFlags();
69
+ for (const [k, v] of Object.entries(flags || {})) {
70
+ // Skip internal NativeScript build flags
71
+ if (['ios', 'android', 'visionos', 'platform', 'hmr', 'verbose'].includes(k))
72
+ continue;
73
+ __processEnvEntries[k] = String(v);
74
+ }
75
+ }
76
+ catch { }
77
+ const __processEnvJson = JSON.stringify(__processEnvEntries);
33
78
  const { parse, compileTemplate, compileScript } = vueSfcCompiler;
34
79
  const APP_ROOT_DIR = getProjectAppPath();
35
80
  const APP_VIRTUAL_PREFIX = getProjectAppVirtualPath();
36
81
  const APP_VIRTUAL_WITH_SLASH = `${APP_VIRTUAL_PREFIX}/`;
37
82
  const DEFAULT_MAIN_ENTRY = getProjectAppRelativePath('app.ts');
38
83
  const DEFAULT_MAIN_ENTRY_VIRTUAL = getProjectAppVirtualPath('app.ts');
84
+ // Memoized resolver for the project bootstrap entry as a posix
85
+ // project-relative path (e.g. `/src/main.ts`). This mirrors the
86
+ // resolution the cold-boot wrapper performs (`getPackageJson().main` →
87
+ // project-relative under `/<APP_ROOT_DIR>/`) so the eviction set for
88
+ // HMR always lines up with the URL the runtime actually re-imports.
89
+ // Resolved at first call and cached: `package.json` is read at startup
90
+ // and never 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
+ }
39
111
  const STRATEGY_REGISTRY = new Map([
40
112
  ['vue', vueServerStrategy],
41
113
  ['angular', angularServerStrategy],
@@ -43,255 +115,177 @@ const STRATEGY_REGISTRY = new Map([
43
115
  ['typescript', typescriptServerStrategy],
44
116
  ]);
45
117
  function resolveFrameworkStrategy(flavor) {
46
- return STRATEGY_REGISTRY.get(flavor);
118
+ const strategy = STRATEGY_REGISTRY.get(flavor);
119
+ if (!strategy) {
120
+ throw new Error(`[ns-hmr] Unsupported framework strategy: ${flavor}`);
121
+ }
122
+ return strategy;
47
123
  }
48
124
  let ACTIVE_STRATEGY;
125
+ function isSocketClientOpen(client) {
126
+ if (!client) {
127
+ return false;
128
+ }
129
+ const openState = typeof client.OPEN === 'number' ? client.OPEN : 1;
130
+ return client.readyState === openState;
131
+ }
132
+ function getHmrSocketRoleFromRequestUrl(requestUrl) {
133
+ try {
134
+ const url = new URL(requestUrl || '/ns-hmr', 'http://localhost');
135
+ return url.searchParams.get('ns_hmr_role') || 'unknown';
136
+ }
137
+ catch {
138
+ return 'unknown';
139
+ }
140
+ }
141
+ function getHmrSocketRole(client) {
142
+ if (!client) {
143
+ return 'unknown';
144
+ }
145
+ return typeof client.__nsHmrClientRole === 'string' && client.__nsHmrClientRole ? client.__nsHmrClientRole : 'unknown';
146
+ }
147
+ function shouldAllowLocalCoreSanitizerPaths(contextLabel) {
148
+ return /\bnode_modules\/@nativescript\/vite\/hmr\/(?:client|frameworks)\//.test(contextLabel);
149
+ }
150
+ export function prepareAngularEntryForDevice(code, importerPath, sfcFileMap, depFileMap, projectRoot, verbose = false, outputDirOverrideRel, httpOrigin, resolveVendorAsHttp = false) {
151
+ const rewrittenCode = rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot, verbose, outputDirOverrideRel, httpOrigin, resolveVendorAsHttp);
152
+ return rewriteAngularEntryRegisterOnly(rewrittenCode, resolveAngularCoreHmrImportSource(rewrittenCode, httpOrigin));
153
+ }
49
154
  const processSfcCode = createProcessSfcCode(processCodeForDevice);
50
155
  // Bare specifiers and special skip patterns (virtual, data:, etc.)
51
156
  const VENDOR_PACKAGES = /^[A-Za-z@][^:\/\s]*$/;
52
157
  const SKIP_PATTERNS = /^(?:data:|blob:|node:|virtual:|vite:|\0|\/@@?id|\/__vite|__vite|__x00__)/;
53
- // Minimal helpers to support vendor pre-bundle detection
54
- function extractVitePrebundleId(spec) {
55
- const m = spec.match(/\.vite\/deps\/([^?]+?)\.[mc]?js/);
56
- if (m)
57
- return m[1];
58
- const m2 = spec.match(/__x00__([^?]+?)\.[mc]?js/);
59
- if (m2)
60
- return m2[1];
61
- return null;
62
- }
63
- function getFlattenedManifestMap(manifest) {
64
- const map = new Map();
65
- const mods = Object.keys(manifest.modules || {});
66
- for (const canonical of mods) {
67
- const flat = canonical.replace(/\./g, '__').replace(/\//g, '_');
68
- map.set(flat, canonical);
69
- const alias = manifest.aliases?.[canonical];
70
- if (alias) {
71
- const aliasFlat = String(alias).replace(/\./g, '__').replace(/\//g, '_');
72
- map.set(aliasFlat, canonical);
158
+ const MODULE_IMPORT_ANALYSIS_PLUGINS = ['typescript', 'jsx', 'importMeta', 'topLevelAwait', 'classProperties', 'classPrivateProperties', 'classPrivateMethods', 'decorators-legacy'];
159
+ function collectTopLevelImportRecords(code) {
160
+ if (!code || typeof code !== 'string' || !/\bimport\b/.test(code)) {
161
+ return [];
162
+ }
163
+ try {
164
+ const ast = babelParse(code, {
165
+ sourceType: 'module',
166
+ plugins: MODULE_IMPORT_ANALYSIS_PLUGINS,
167
+ });
168
+ const body = ast?.program?.body;
169
+ if (!Array.isArray(body)) {
170
+ return [];
73
171
  }
172
+ return body
173
+ .filter((node) => t.isImportDeclaration(node) && typeof node.start === 'number' && typeof node.end === 'number' && typeof node.source?.value === 'string')
174
+ .map((node) => ({
175
+ start: node.start,
176
+ end: node.end,
177
+ text: code.slice(node.start, node.end),
178
+ source: node.source.value,
179
+ hasOnlyNamedSpecifiers: Array.isArray(node.specifiers) && node.specifiers.length > 0 && node.specifiers.every((spec) => t.isImportSpecifier(spec)),
180
+ namedBindings: Array.isArray(node.specifiers)
181
+ ? node.specifiers
182
+ .filter((spec) => t.isImportSpecifier(spec) && typeof spec.start === 'number' && typeof spec.end === 'number')
183
+ .map((spec) => ({
184
+ importedName: t.isIdentifier(spec.imported) ? spec.imported.name : String(spec.imported?.value || ''),
185
+ text: code.slice(spec.start, spec.end),
186
+ }))
187
+ : [],
188
+ }));
189
+ }
190
+ catch {
191
+ return [];
74
192
  }
75
- return map;
76
- }
77
- // NativeScript module detectors
78
- function isCoreGlobalsReference(spec) {
79
- return /@nativescript(?:[\/_-])core(?:[\/_-])globals/.test(spec || '');
80
- }
81
- function isNativeScriptCoreModule(spec) {
82
- return /^(?:@nativescript[\/_-]core|@nativescript\/core)(?:\b|\/)/i.test(spec || '');
83
193
  }
84
- function isNativeScriptPluginModule(spec) {
85
- return /^@nativescript\//i.test(spec || '') && !isNativeScriptCoreModule(spec || '');
194
+ function hoistTopLevelStaticImports(code) {
195
+ const imports = collectTopLevelImportRecords(code);
196
+ if (!imports.length) {
197
+ return code;
198
+ }
199
+ let stripped = code;
200
+ for (const imp of [...imports].sort((left, right) => right.start - left.start)) {
201
+ stripped = stripped.slice(0, imp.start) + stripped.slice(imp.end);
202
+ }
203
+ const hoisted = [];
204
+ const seen = new Set();
205
+ for (const imp of imports) {
206
+ const text = imp.text.trim();
207
+ if (!text || seen.has(text)) {
208
+ continue;
209
+ }
210
+ seen.add(text);
211
+ hoisted.push(text);
212
+ }
213
+ if (!hoisted.length) {
214
+ return stripped;
215
+ }
216
+ return `${hoisted.join('\n')}\n${stripped.replace(/^\s*\n+/, '')}`;
86
217
  }
87
- // Looser detector for NativeScript plugin-style specifiers that should be resolved
88
- // via device require() rather than HTTP during HMR. This includes popular community
89
- // scopes in addition to @nativescript/* (excluding core).
90
- function isLikelyNativeScriptPluginSpecifier(spec) {
91
- if (!spec)
92
- return false;
93
- const s = spec.replace(PAT.QUERY_PATTERN, '');
94
- // Absolute or relative paths are not bare packages
95
- if (/^(?:\.|\/|https?:\/\/)/i.test(s))
96
- return false;
97
- // App alias paths like '@/...' are not vendor packages
98
- if (s.startsWith('@@/'))
99
- return false; // extremely rare double '@' alias
100
- if (s.startsWith('~/'))
101
- return false; // NativeScript tilde alias (app root)
102
- if (s.startsWith('@/'))
103
- return false; // Common Vite alias for src
104
- // .vue SFCs are not vendor packages
105
- if (/\.vue(?:\?|$)/i.test(s))
106
- return false;
107
- // Exclude core and vue runtime which are handled by dedicated bridges
108
- if (/^@nativescript\/core(\b|\/)/i.test(s))
109
- return false;
110
- if (/^(?:vue|nativescript-vue)(?:\b|\/)/i.test(s))
111
- return false;
112
- // Treat any other bare package id as device-resolved (require) during HMR
113
- return true;
218
+ export function buildBootProgressSnippet(bootModuleLabel) {
219
+ const normalizedLabel = JSON.stringify(String(bootModuleLabel || '').replace(/\\/g, '/'));
220
+ return [
221
+ `const __nsBootGlobal=globalThis;`,
222
+ `try{if(!__nsBootGlobal.__NS_HMR_BOOT_COMPLETE__){const __nsBootApi=__nsBootGlobal.__NS_HMR_DEV_OVERLAY__;if(__nsBootApi&&typeof __nsBootApi.setBootStage==='function'){const __nsBootCount=(__nsBootGlobal.__NS_HMR_BOOT_MODULE_COUNT__=Number(__nsBootGlobal.__NS_HMR_BOOT_MODULE_COUNT__||0)+1);__nsBootGlobal.__NS_HMR_BOOT_LAST_MODULE__=${normalizedLabel};const __nsBootNow=Date.now();const __nsBootLast=Number(__nsBootGlobal.__NS_HMR_BOOT_LAST_PROGRESS_AT__||0);if(__nsBootCount<=8||__nsBootCount%6===0||__nsBootNow-__nsBootLast>90){__nsBootGlobal.__NS_HMR_BOOT_LAST_PROGRESS_AT__=__nsBootNow;const __nsBootProgress=Math.min(94,82+Math.min(10,Math.round((Math.log(__nsBootCount+1)/Math.LN2)*2)));__nsBootApi.setBootStage('importing-main',{detail:'Evaluated '+__nsBootCount+' modules\\n'+__nsBootGlobal.__NS_HMR_BOOT_LAST_MODULE__,attempt:Number(__nsBootGlobal.__NS_HMR_BOOT_MAIN_ATTEMPT__||1),attempts:Number(__nsBootGlobal.__NS_HMR_BOOT_MAIN_ATTEMPTS__||6),progress:__nsBootProgress});}}}}catch(__nsBootErr){}`,
223
+ `if(!__nsBootGlobal.__NS_HMR_BOOT_COMPLETE__){const __nsBootCount=Number(__nsBootGlobal.__NS_HMR_BOOT_MODULE_COUNT__||0);if(__nsBootCount<=24||__nsBootCount%8===0){await new Promise((resolve)=>setTimeout(resolve,0));}}`,
224
+ '',
225
+ ].join('\n');
114
226
  }
115
- export function ensureNativeScriptModuleBindings(code) {
116
- // Proceed even if a vendor manifest isn't available; we'll still vendor-bind
117
- // likely NativeScript plugin-style specifiers (e.g., 'pinia', '@scope/pkg')
118
- // via require() so device can resolve them from the app bundle.
119
- const importRegex = /(^|\n)\s*import\s+([\s\S]*?)\s+from\s+["']([^"']+)["'];?/gm;
120
- const sideEffectRegex = /(^|\n)\s*import\s+["']([^"']+)["'];?/gm;
121
- // Collect non-vendor imports so we can hoist them above the injected vendor prelude.
122
- // This ensures any residual ESM imports (like SFCs) remain at the true top-level for parsers
123
- // that require imports to precede other statements.
124
- const preservedImports = [];
125
- const modules = new Map();
126
- const getModuleBinding = (canonical) => {
127
- let entry = modules.get(canonical);
128
- if (!entry) {
129
- entry = {
130
- default: new Set(),
131
- namespace: new Set(),
132
- named: [],
133
- sideEffectOnly: false,
134
- };
135
- modules.set(canonical, entry);
227
+ function rewriteVitePrebundleImportsForDevice(code, preserveVendorImports) {
228
+ const imports = collectTopLevelImportRecords(code);
229
+ if (!imports.length) {
230
+ return code;
231
+ }
232
+ const edits = [];
233
+ for (const imp of imports) {
234
+ const source = imp.source;
235
+ const depMatch = source.match(/(?:^|\/)node_modules\/\.vite\/deps\/(.+)$/);
236
+ const depPath = depMatch?.[1] || (source.startsWith('.vite/deps/') ? source.slice('.vite/deps/'.length) : null);
237
+ if (!depPath) {
238
+ continue;
136
239
  }
137
- return entry;
138
- };
139
- const parseNamedImports = (clause, binding) => {
140
- const inner = clause.replace(/^\{/, '').replace(/\}$/, '');
141
- inner
142
- .split(',')
143
- .map((segment) => segment.trim())
144
- .filter(Boolean)
145
- .forEach((segment) => {
146
- const [imported, local] = segment.split(/\s+as\s+/i).map((s) => s.trim());
147
- const resolvedImported = imported;
148
- const resolvedLocal = local || imported;
149
- if (resolvedImported) {
150
- binding.named.push({
151
- imported: resolvedImported,
152
- local: resolvedLocal,
153
- });
240
+ let replacement = '';
241
+ if (preserveVendorImports) {
242
+ const canonical = resolveVendorFromCandidate(`.vite/deps/${depPath}`);
243
+ const bareSpecifier = canonical || viteDepsPathToBareSpecifier(depPath);
244
+ if (bareSpecifier) {
245
+ replacement = imp.text.replace(source, bareSpecifier);
154
246
  }
155
- });
156
- };
157
- // Handle "import ... from 'x'" forms
158
- code = code.replace(importRegex, (full, pfx, clause, rawSpec) => {
159
- // Capture original for potential preservation (strip the leading newline to avoid double spacing when hoisted)
160
- const original = full.replace(/^\n/, '');
161
- // Do not touch type-only imports other than hoisting
162
- if (full.trimStart().startsWith('import type')) {
163
- preservedImports.push(original);
164
- return pfx || '';
165
- }
166
- const specifier = rawSpec.replace(PAT.QUERY_PATTERN, '');
167
- let canonical = resolveVendorFromCandidate(specifier);
168
- // If not found in vendor manifest, treat well-known NativeScript plugin-style packages
169
- // as require() based modules so the device can resolve them from the app bundle or vendor.
170
- if (!canonical && isLikelyNativeScriptPluginSpecifier(specifier)) {
171
- canonical = specifier;
172
- }
173
- // CRITICAL: never vendor-inject @nativescript/core here — preserve for the later core-bridge pass.
174
- if (canonical && /^@nativescript\/core(\b|\/)/i.test(canonical)) {
175
- preservedImports.push(original);
176
- return pfx || '';
177
- }
178
- if (!canonical) {
179
- preservedImports.push(original);
180
- return pfx || '';
181
- }
182
- const binding = getModuleBinding(canonical);
183
- const trimmed = String(clause).trim();
184
- if (!trimmed) {
185
- binding.sideEffectOnly = true;
186
- return pfx || ''; // erase the import line
187
- }
188
- // namespace: import * as ns from 'x'
189
- if (trimmed.startsWith('*')) {
190
- const m = trimmed.match(/\*\s+as\s+(\w+)/i);
191
- if (m?.[1])
192
- binding.namespace.add(m[1]);
193
- return pfx || '';
194
- }
195
- // named: import { a, b as c } from 'x'
196
- if (trimmed.startsWith('{')) {
197
- parseNamedImports(trimmed, binding);
198
- return pfx || '';
199
- }
200
- // default + named: import Default, { a as A } from 'x'
201
- if (trimmed.includes(',') && trimmed.includes('{')) {
202
- const [defaultPart, namedPart] = trimmed.split(/,(.+)/, 2);
203
- const def = defaultPart.trim();
204
- if (def)
205
- binding.default.add(def);
206
- if (namedPart)
207
- parseNamedImports(namedPart.trim(), binding);
208
- return pfx || '';
209
- }
210
- // default only
211
- binding.default.add(trimmed);
212
- return pfx || '';
213
- });
214
- // Handle side-effect only imports: import 'x'
215
- code = code.replace(sideEffectRegex, (full, _pfx, rawSpec) => {
216
- const original = full.replace(/^\n/, '');
217
- const specifier = rawSpec.replace(PAT.QUERY_PATTERN, '');
218
- let canonical = resolveVendorFromCandidate(specifier);
219
- if (!canonical && isLikelyNativeScriptPluginSpecifier(specifier)) {
220
- canonical = specifier;
221
- }
222
- if (canonical && /^@nativescript\/core(\b|\/)/i.test(canonical)) {
223
- preservedImports.push(original);
224
- return _pfx || '';
225
- }
226
- if (!canonical) {
227
- preservedImports.push(original);
228
- return _pfx || '';
229
- }
230
- const binding = getModuleBinding(canonical);
231
- binding.sideEffectOnly = true;
232
- return _pfx || '';
233
- });
234
- // If there are no vendor modules to bind, still hoist preserved imports if any were collected.
235
- if (!modules.size) {
236
- if (preservedImports.length) {
237
- const preserved = preservedImports.join('') + '\n';
238
- return preserved + code;
239
247
  }
248
+ edits.push({
249
+ start: imp.start,
250
+ end: imp.end,
251
+ text: replacement,
252
+ });
253
+ }
254
+ if (!edits.length) {
240
255
  return code;
241
256
  }
242
- let injection = 'const __nsVendorRegistry = (globalThis.__nsVendorRegistry ||= new Map());\n';
243
- // Soft vendor fallback mode: when a plugin module is not available during HMR, provide a stub so the module can instantiate.
244
- // Toggle with globalThis.__NS_VENDOR_SOFT__ (default true)
245
- // Use JS-safe global access (no TS casts) to avoid syntax errors on device
246
- injection += "const __NS_VENDOR_SOFT__ = (typeof globalThis.__NS_VENDOR_SOFT__ !== 'undefined' ? !!globalThis.__NS_VENDOR_SOFT__ : true);\n";
247
- // Provide a require fallback that throws lazily so callers can soft-stub in the catch block.
248
- injection += "const __nsVendorRequire = (typeof globalThis.__nsRequire === 'function' ? globalThis.__nsRequire : (typeof globalThis.require === 'function' ? globalThis.require : (spec => { throw new Error('__nsVendorRequire unavailable'); })));\n";
249
- // One-time diagnostic if require is missing; avoid spewing on every module
250
- injection += "try { (globalThis.__NS_VENDOR_ONCE__ ||= { loggedRequireMissing: false }); if (!globalThis.__NS_VENDOR_ONCE__.loggedRequireMissing && typeof __nsVendorRequire !== 'function') { console.warn('[ns-hmr][vendor][require-missing] using soft stubs=', __NS_VENDOR_SOFT__); globalThis.__NS_VENDOR_ONCE__.loggedRequireMissing = true; } } catch {}\n";
251
- injection += "function __nsMissing(name){ try { const fn = function(){ try { console.warn('[ns-hmr][vendor][stub]', name); } catch {} }; return new Proxy(fn, { get: (_t, p) => __nsMissing(name + '.' + String(p)) }); } catch { return {}; } }\n";
252
- // Helper utils to simplify robust property/default selection without using optional chaining/nullish
253
- injection += "function __nsHasInstall(x){ try { return (typeof x === 'function') || (typeof x === 'object' && x && typeof x.install === 'function'); } catch { return false; } }\n";
254
- injection += "function __nsDefault(mod){ try { return (mod && mod['default'] !== undefined) ? mod['default'] : mod; } catch { return mod; } }\n";
255
- injection += "function __nsNestedDefault(mod){ try { return (mod && mod.default && (typeof mod.default.default === 'function' || (typeof mod.default.default === 'object' && mod.default.default && typeof mod.default.default.install === 'function'))) ? mod.default.default : undefined; } catch { return undefined; } }\n";
256
- injection += "function __nsPick(mod, name){ try { if (mod && mod['default'] && mod['default'][name] !== undefined) return mod['default'][name]; } catch {} try { if (mod && mod[name] !== undefined) return mod[name]; } catch {} try { if (mod && typeof mod['default'] === 'function' && mod['default'].name === name) return mod['default']; } catch {} return undefined; }\n";
257
- let index = 0;
258
- for (const [canonical, binding] of modules) {
259
- const cacheKey = JSON.stringify(canonical);
260
- const moduleVar = `__nsVendorModule_${index++}`;
261
- injection += `const ${moduleVar} = __nsVendorRegistry.has(${cacheKey}) ? __nsVendorRegistry.get(${cacheKey}) : (() => { try { const mod = __nsVendorRequire(${cacheKey}); __nsVendorRegistry.set(${cacheKey}, mod); return mod; } catch (e) { try { console.error('[ns-hmr][vendor][require-failed]', ${cacheKey}, (e && (e.message||e)) ); } catch {} try { if (__NS_VENDOR_SOFT__) { const stub = __nsMissing(${cacheKey}); __nsVendorRegistry.set(${cacheKey}, stub); return stub; } } catch {} throw e; } })();\n`;
262
- binding.namespace.forEach((alias) => {
263
- // For namespace imports, expose both the raw module and a default fallback for interop consumers
264
- injection += `const ${alias} = ${moduleVar};\n`;
265
- injection += `(${alias} && typeof ${alias} === 'object' && !('default' in ${alias})) && (${alias}.default = ${alias});\n`;
266
- });
267
- if (binding.named.length) {
268
- // Bind each named import robustly from either default or namespace using helper.
269
- for (const { imported, local } of binding.named) {
270
- const localName = local;
271
- const importedName = imported;
272
- injection += `const ${localName} = __nsPick(${moduleVar}, ${JSON.stringify(importedName)});\n`;
273
- }
274
- }
275
- if (binding.default.size) {
276
- // Create one stable default candidate per module and reuse for all default locals
277
- const defVar = `${moduleVar}__def`;
278
- injection += `const ${defVar} = __nsDefault(${moduleVar});\n`;
279
- binding.default.forEach((localName) => {
280
- injection += `const ${localName} = (__nsHasInstall(${defVar})
281
- ? ${defVar}
282
- : (__nsHasInstall(${moduleVar})
283
- ? ${moduleVar}
284
- : (function(){ const _n = __nsNestedDefault(${moduleVar}); return _n !== undefined ? _n : ${defVar}; })()));\n`;
285
- });
286
- }
287
- if (binding.sideEffectOnly && !binding.namespace.size && !binding.named.length && !binding.default.size) {
288
- injection += `void ${moduleVar};\n`;
257
+ let next = code;
258
+ for (const edit of edits.sort((left, right) => right.start - left.start)) {
259
+ next = next.slice(0, edit.start) + edit.text + next.slice(edit.end);
260
+ }
261
+ return next;
262
+ }
263
+ function buildNodeModuleProvenancePrelude(sourceId) {
264
+ if (!sourceId) {
265
+ return '';
266
+ }
267
+ const cleaned = sourceId.replace(PAT.QUERY_PATTERN, '');
268
+ let normalized = normalizeNodeModulesSpecifier(cleaned);
269
+ if (!normalized) {
270
+ const viteDepsMatch = cleaned.match(/(?:^|\/)node_modules\/\.vite\/deps\/([^?#]+)/);
271
+ if (viteDepsMatch?.[1]) {
272
+ normalized = `.vite/deps/${viteDepsMatch[1]}`;
289
273
  }
290
274
  }
291
- injection += '\n';
292
- // Hoist preserved non-vendor imports to the very top for maximum ESM compatibility
293
- const preserved = preservedImports.length ? preservedImports.join('') + '\n' : '';
294
- return preserved + injection + code;
275
+ if (!normalized) {
276
+ return '';
277
+ }
278
+ let packageSpecifier = normalized;
279
+ let via = 'node_modules';
280
+ if (normalized.startsWith('.vite/deps/')) {
281
+ via = 'vite-deps';
282
+ packageSpecifier = viteDepsPathToBareSpecifier(normalized.slice('.vite/deps/'.length)) || normalized;
283
+ }
284
+ const rootPackage = resolveNodeModulesPackageBoundary(packageSpecifier, getProjectRootPath()).packageName;
285
+ if (!rootPackage) {
286
+ return '';
287
+ }
288
+ return `try { const __nsRecord = globalThis.__NS_RECORD_MODULE_PROVENANCE__; if (typeof __nsRecord === 'function') { __nsRecord(${JSON.stringify(rootPackage)}, ${JSON.stringify({ kind: 'http-esm', specifier: packageSpecifier, url: sourceId, via })}); } } catch {}\n`;
295
289
  }
296
290
  // Guard any bare dynamic import(spec) occurring in assembled module code.
297
291
  // We cannot override native dynamic import globally; for SFC assembler outputs we inline
@@ -318,113 +312,6 @@ function guardBareDynamicImports(code) {
318
312
  return code;
319
313
  }
320
314
  }
321
- function normalizeNativeScriptCoreSpecifier(spec) {
322
- let normalized = spec.replace(/@nativescript[_-]core/gi, '@nativescript/core').replace(/@nativescript\/core\/index\.js$/i, '@nativescript/core/index.js');
323
- if (normalized.startsWith('/node_modules/')) {
324
- const idx = normalized.toLowerCase().indexOf('@nativescript/core');
325
- if (idx !== -1) {
326
- normalized = normalized.slice(idx);
327
- }
328
- }
329
- if (normalized.toLowerCase().startsWith('@nativescript/core')) {
330
- normalized = normalized.replace(/\?[^"'`]*$/, '');
331
- }
332
- return normalized;
333
- }
334
- function normalizeNodeModulesSpecifier(spec) {
335
- if (!spec) {
336
- return null;
337
- }
338
- let normalized = spec.replace(/\\/g, '/');
339
- const idx = normalized.lastIndexOf('/node_modules/');
340
- if (idx === -1) {
341
- return null;
342
- }
343
- let subPath = normalized.slice(idx + '/node_modules/'.length);
344
- if (!subPath) {
345
- return null;
346
- }
347
- subPath = subPath.replace(PAT.QUERY_PATTERN, '');
348
- if (!subPath) {
349
- return null;
350
- }
351
- // Skip Vite pre-bundled deps that we already map to vendor
352
- if (subPath.startsWith('.vite/')) {
353
- return null;
354
- }
355
- return subPath.startsWith('/') ? subPath.slice(1) : subPath;
356
- }
357
- function resolveVendorFromCandidate(specifier) {
358
- if (!specifier) {
359
- return null;
360
- }
361
- const manifest = getVendorManifest();
362
- if (!manifest) {
363
- return null;
364
- }
365
- const cleaned = specifier.replace(PAT.QUERY_PATTERN, '');
366
- const direct = resolveVendorSpecifier(cleaned);
367
- if (direct) {
368
- return direct;
369
- }
370
- const flattenedId = extractVitePrebundleId(cleaned);
371
- if (flattenedId) {
372
- const flattenedMap = getFlattenedManifestMap(manifest);
373
- const flatMatch = flattenedMap.get(flattenedId);
374
- if (flatMatch) {
375
- return flatMatch;
376
- }
377
- for (const [flatKey, canonical] of flattenedMap.entries()) {
378
- if (flattenedId === flatKey || flattenedId.startsWith(`${flatKey}_`)) {
379
- return canonical;
380
- }
381
- }
382
- const guessedId = flattenedId.replace(/__/g, '.').replace(/_/g, '/');
383
- if (guessedId && guessedId !== flattenedId) {
384
- const guessedCanonical = resolveVendorSpecifier(guessedId);
385
- if (guessedCanonical) {
386
- return guessedCanonical;
387
- }
388
- const prefix = findVendorPrefix(guessedId, manifest);
389
- if (prefix) {
390
- return prefix;
391
- }
392
- }
393
- }
394
- const normalizedCore = normalizeNativeScriptCoreSpecifier(cleaned);
395
- if (normalizedCore !== cleaned) {
396
- const nsCanonical = resolveVendorSpecifier(normalizedCore);
397
- if (nsCanonical) {
398
- return nsCanonical;
399
- }
400
- }
401
- const nodeModulesSpecifier = normalizeNodeModulesSpecifier(cleaned);
402
- if (nodeModulesSpecifier) {
403
- const canonical = resolveVendorSpecifier(nodeModulesSpecifier);
404
- if (canonical) {
405
- return canonical;
406
- }
407
- const prefix = findVendorPrefix(nodeModulesSpecifier, manifest);
408
- if (prefix) {
409
- return prefix;
410
- }
411
- }
412
- const prefix = findVendorPrefix(cleaned, manifest);
413
- if (prefix) {
414
- return prefix;
415
- }
416
- return null;
417
- }
418
- function findVendorPrefix(specifier, manifest) {
419
- const { modules } = manifest;
420
- const keys = Object.keys(modules || {});
421
- for (const key of keys) {
422
- if (specifier === key || specifier.startsWith(`${key}/`)) {
423
- return key;
424
- }
425
- }
426
- return null;
427
- }
428
315
  function stripCoreGlobalsImports(code) {
429
316
  const pattern = /^\s*(?:import\s+(?:[^'"\n]*from\s+)?|export\s+\*\s+from\s+)["'][^"']*(?:@nativescript(?:[/_-])core(?:[\/_-])globals|@nativescript_core_globals)[^"']*["'];?\s*$/gm;
430
317
  return code.replace(pattern, '');
@@ -452,18 +339,14 @@ function ensureVariableDynamicImportHelper(code) {
452
339
  `};\n`;
453
340
  return `${helper}${code}`;
454
341
  }
455
- // Final safety net for plain dynamic import(expressions) that might slip through
456
- // Vite's helper transformation. We rewrite occurrences of `import(` to `__ns_import(`
457
- // and inject a small wrapper that maps the anomalous request '@' to a harmless stub.
458
342
  function ensureGuardPlainDynamicImports(code, origin) {
459
343
  try {
460
344
  if (!code || !/\bimport\s*\(/.test(code))
461
345
  return code;
462
- const w = `const __ns_import = (s) => { try { if (s === '@') { return import(new URL('/ns/m/__invalid_at__.mjs', import.meta.url).href); } } catch {} return import(s); }\n`;
463
- // Replace only when `import(` is not part of an identifier or property (no preceding "." or word char)
346
+ const wrapper = `const __ns_import = (s) => { try { if (s === '@') { return import(new URL('/ns/m/__invalid_at__.mjs', import.meta.url).href); } } catch {} return import(s); }\n`;
464
347
  const replaced = code.replace(/(^|[^\.\w$])import\s*\(/g, (_m, p1) => `${p1}__ns_import(`);
465
348
  if (replaced !== code) {
466
- return w + replaced;
349
+ return wrapper + replaced;
467
350
  }
468
351
  return code;
469
352
  }
@@ -471,13 +354,63 @@ function ensureGuardPlainDynamicImports(code, origin) {
471
354
  return code;
472
355
  }
473
356
  }
474
- // Heal accidental "import ... = expr" assignments produced by upstream transforms.
475
- // These are invalid JS; convert to equivalent const assignments.
357
+ // `ensureDynamicHmrImportHelper` lives in
358
+ // `./websocket-served-module-helpers.js`. See that file for the
359
+ // architectural rationale and the current helper implementation.
360
+ async function expandStarExports(code, server, projectRoot, verbose, sharedTransformer) {
361
+ const STAR_RE = /^[ \t]*(export\s+\*\s+from\s+["'])([^"']+)(["'];?)[ \t]*$/gm;
362
+ let match;
363
+ const replacements = [];
364
+ while ((match = STAR_RE.exec(code)) !== null) {
365
+ const url = match[2];
366
+ if (!url.includes('/node_modules/'))
367
+ continue;
368
+ replacements.push({ full: match[0], url, prefix: match[1], suffix: match[3] });
369
+ }
370
+ if (!replacements.length)
371
+ return code;
372
+ // Pull target URLs through the shared runner when it's available so each
373
+ // node_modules path shares the 60s TTL cache with the main /ns/m pipeline
374
+ // and respects the global concurrency gate. Fan them out in parallel —
375
+ // this block used to be a serial `for await` loop, which dominated cold
376
+ // boot on apps with dozens of star-re-exports.
377
+ const transformer = sharedTransformer ?? ((url) => server.transformRequest(url));
378
+ const resolved = await Promise.all(replacements.map(async (rep) => {
379
+ try {
380
+ let vitePath = rep.url.replace(/^https?:\/\/[^/]+/, '');
381
+ vitePath = vitePath.replace(/^\/ns\/m\//, '/');
382
+ vitePath = vitePath.replace(/^\/__ns_boot__\/[^/]+/, '');
383
+ vitePath = vitePath.replace(/\/__ns_hmr__\/[^/]+/, '');
384
+ const result = await transformer(vitePath);
385
+ if (!result?.code)
386
+ return null;
387
+ const names = extractExportedNames(result.code);
388
+ if (!names.length)
389
+ return null;
390
+ if (verbose) {
391
+ console.log(`[ns/m] expanded export* -> ${names.length} names from ${vitePath}`);
392
+ }
393
+ return { rep, names };
394
+ }
395
+ catch {
396
+ return null;
397
+ }
398
+ }));
399
+ for (const entry of resolved) {
400
+ if (!entry)
401
+ continue;
402
+ const explicit = `export { ${entry.names.join(', ')} } from ${JSON.stringify(entry.rep.url)};`;
403
+ code = code.replace(entry.rep.full, explicit);
404
+ }
405
+ return code;
406
+ }
407
+ function extractExportedNames(code) {
408
+ return extractDirectExportedNames(code);
409
+ }
476
410
  function repairImportEqualsAssignments(code) {
477
411
  try {
478
412
  if (!code || typeof code !== 'string')
479
413
  return code;
480
- // import { a, b as c } = expr; -> const { a, b: c } = expr;
481
414
  code = code.replace(/(^|\n)\s*import\s*\{([^}]+)\}\s*=\s*([^;]+);?/g, (_m, p1, specList, rhs) => {
482
415
  const cleaned = String(specList)
483
416
  .split(',')
@@ -487,56 +420,28 @@ function repairImportEqualsAssignments(code) {
487
420
  .join(', ');
488
421
  return `${p1}const { ${cleaned} } = ${rhs};`;
489
422
  });
490
- // import * as ns = expr; -> const ns = (expr);
491
- code = code.replace(/(^|\n)\s*import\s*\*\s*as\s*([A-Za-z_$][\w$]*)\s*=\s*([^;]+);?/g, (_m, p1, ns, rhs) => {
492
- return `${p1}const ${ns} = (${rhs});`;
493
- });
494
- // import name = expr; -> const name = expr;
495
- code = code.replace(/(^|\n)\s*import\s+([A-Za-z_$][\w$]*)\s*=\s*([^;]+);?/g, (_m, p1, id, rhs) => {
496
- return `${p1}const ${id} = ${rhs};`;
497
- });
423
+ code = code.replace(/(^|\n)\s*import\s*\*\s*as\s*([A-Za-z_$][\w$]*)\s*=\s*([^;]+);?/g, (_m, p1, ns, rhs) => `${p1}const ${ns} = (${rhs});`);
424
+ code = code.replace(/(^|\n)\s*import\s+([A-Za-z_$][\w$]*)\s*=\s*([^;]+);?/g, (_m, p1, id, rhs) => `${p1}const ${id} = ${rhs};`);
498
425
  }
499
426
  catch { }
500
427
  return code;
501
428
  }
502
- // Ensure imports of the NativeScript-Vue runtime bridge '/ns/rt' are versioned to
503
- // bust the device HTTP loader cache whenever the HMR graph version increments.
504
429
  function ensureVersionedRtImports(code, origin, ver) {
505
430
  if (!code || !origin || !Number.isFinite(ver))
506
431
  return code;
507
- // Static imports: import { ... } from ".../ns/rt" (plus optional version)
508
432
  code = code.replace(/(from\s+["'])(?:https?:\/\/[^"']+)?\/(?:\ns|ns)\/rt(?:\/[\d]+)?(["'])/g, (_m, p1, p3) => `${p1}/ns/rt/${ver}${p3}`);
509
- // Dynamic imports: import(".../ns/rt") (plus optional version)
510
433
  code = code.replace(/(import\(\s*["'])(?:https?:\/\/[^"']+)?\/(?:\@ns|ns)\/rt(?:\/[\d]+)?(["']\s*\))/g, (_m, p1, p3) => `${p1}/ns/rt/${ver}${p3}`);
511
434
  return code;
512
435
  }
513
- // Ensure imports of @nativescript/core resolve via the unified /ns/core bridge to keep a single realm
514
- function ensureVersionedCoreImports(code, origin, ver) {
515
- try {
516
- // Static imports already handled in rewriteImports; just ensure absolute origin prefix and version
517
- code = code.replace(/(["'])\/ns\/core(\?p=[^"']+)?\1/g, (_m, q, qp) => `${q}/ns/core/${ver}${qp || ''}${q}`);
518
- // Dynamic imports already handled in rewriteImports; ensure origin and version
519
- code = code.replace(/import\(\s*(["'])\/ns\/core(\?p=[^"']+)?\1\s*\)/g, (_m, q, qp) => `import(${q}/ns/core/${ver}${qp || ''}${q})`);
520
- }
521
- catch { }
522
- return code;
523
- }
524
- // Hardened removal of Vite's virtual dynamic-import-helper. Some variants (side-effect only
525
- // or minified forms) slipped past earlier regexes causing runtime attempts to resolve
526
- // /@id/__x00__vite/dynamic-import-helper.js which does not exist in the device mirror.
527
- // We aggressively strip any reference and inline a helper if necessary.
528
436
  function stripViteDynamicImportVirtual(code) {
529
437
  if (!/\/@id\/__x00__vite\/dynamic-import-helper/.test(code)) {
530
438
  return code;
531
439
  }
532
440
  const original = code;
533
- // Remove any import lines referencing the virtual helper (with or without bindings)
534
441
  code = code.replace(/^[\t ]*import[^\n]*\/@id\/__x00__vite\/dynamic-import-helper[^\n]*$/gm, '');
535
- // If any raw spec strings remain (e.g. concatenated), neutralize them
536
442
  if (/\/@id\/__x00__vite\/dynamic-import-helper/.test(code)) {
537
443
  code = code.replace(/\/@id\/__x00__vite\/dynamic-import-helper[^"'`)]*/g, '/__NS_UNUSED_DYNAMIC_IMPORT_HELPER__');
538
444
  }
539
- // Ensure helper present
540
445
  if (!/__variableDynamicImportRuntimeHelper/.test(code)) {
541
446
  const inline = `const __variableDynamicImportRuntimeHelper = (map, request, importMode) => {\n try { if (request === '@') { return import('/ns/m/__invalid_at__.mjs'); } } catch {}\n const loader = map && (map[request] || map[request?.replace(/\\\\/g, '/')]);\n if (!loader) { const e = new Error('Cannot dynamically import: ' + request); /*@ts-ignore*/ e.code = 'ERR_MODULE_NOT_FOUND'; return Promise.reject(e); }\n try { return loader(importMode); } catch (e) { return Promise.reject(e); }\n};\n`;
542
447
  code = inline + code;
@@ -546,17 +451,7 @@ function stripViteDynamicImportVirtual(code) {
546
451
  }
547
452
  return code;
548
453
  }
549
- // Small snippet injected into device-delivered modules to capture any require('http(s)://') calls
550
- const REQUIRE_GUARD_SNIPPET = `// [guard] install require('http(s)://') detector\n(()=>{try{var g=globalThis;if(g.__NS_REQUIRE_GUARD_INSTALLED__){}else{var mk=function(o,l){return function(){try{var s=arguments[0];if(typeof s==='string'&&/^(?:https?:)\\/\\//.test(s)){var e=new Error('[ns-hmr][require-guard] require of URL: '+s+' via '+l);try{console.error(e.message+'\\n'+(e.stack||''));}catch(e2){}try{g.__NS_REQUIRE_GUARD_LAST__={spec:s,stack:e.stack,label:l,ts:Date.now()};}catch(e3){}}}catch(e1){}return o.apply(this, arguments);};};if(typeof g.require==='function'&&!g.require.__NS_REQ_GUARDED__){var o1=g.require;g.require=mk(o1,'require');g.require.__NS_REQ_GUARDED__=true;}if(typeof g.__nsRequire==='function'&&!g.__nsRequire.__NS_REQ_GUARDED__){var o2=g.__nsRequire;g.__nsRequire=mk(o2,'__nsRequire');g.__nsRequire.__NS_REQ_GUARDED__=true;}g.__NS_REQUIRE_GUARD_INSTALLED__=true;}}catch(e){}})();\n`;
551
- // ============================================================================
552
- // HELPER FUNCTIONS
553
- // ============================================================================
554
- // Origin invariant: we own both client and server. All URLs must use an explicit
555
- // http(s)://host:port origin with no trailing slash. Build it deterministically
556
- // where needed; do not post-sanitize.
557
- /**
558
- * Check if an import spec should be remapped to dep-*.mjs
559
- */
454
+ const REQUIRE_GUARD_SNIPPET = `// [guard] install require('http(s)://') detector\n(()=>{try{var g=globalThis;if(g.__NS_REQUIRE_GUARD_INSTALLED__){}else{var mk=function(o,l){return function(){try{var s=arguments[0];if(typeof s==='string'&&/^(?:https?:)\\/\\//.test(s)){var e=new Error('[ns-hmr][require-guard] require of URL: '+s+' via '+l);console.error(e.message+'\\n'+(e.stack||''));try{g.__NS_REQUIRE_GUARD_LAST__={spec:s,stack:e.stack,label:l,ts:Date.now()};}catch(e3){}}}catch(e1){}return o.apply(this, arguments);};};if(typeof g.require==='function'&&!g.require.__NS_REQ_GUARDED__){var o1=g.require;g.require=mk(o1,'require');g.require.__NS_REQ_GUARDED__=true;}if(typeof g.__nsRequire==='function'&&!g.__nsRequire.__NS_REQ_GUARDED__){var o2=g.__nsRequire;g.__nsRequire=mk(o2,'__nsRequire');g.__nsRequire.__NS_REQ_GUARDED__=true;}g.__NS_REQUIRE_GUARD_INSTALLED__=true;}}catch(e){}})();\n`;
560
455
  function shouldRemapImport(spec) {
561
456
  if (!spec || typeof spec !== 'string')
562
457
  return false;
@@ -577,12 +472,9 @@ function shouldRemapImport(spec) {
577
472
  }
578
473
  return true;
579
474
  }
580
- // (legacy wrapSfcWithStableDefault removed; full SFCs now delegate to /ns/asm)
581
475
  function removeNamedImports(code, names) {
582
476
  const regex = /^(\s*import\s*\{)([^}]*)(\}\s*from\s*['"][^'"]+['"];?)/gm;
583
477
  return code.replace(regex, (_m, p1, specList, p3) => {
584
- // Only strip for known globalized framework sources (Vue/Nativescript-Vue).
585
- // Keep imports from all other packages (Pinia, third-party libs, app modules) intact.
586
478
  const srcMatch = /from\s*['"]\s*([^'"\s]+)\s*['"]/i.exec(_m);
587
479
  const src = (srcMatch?.[1] || '').toLowerCase();
588
480
  const isVueSource = /^(?:vue|nativescript-vue)(?:\b|\/)/i.test(src);
@@ -681,7 +573,7 @@ function normalizeImportPath(spec, importerDir) {
681
573
  else if (spec.startsWith('./') || spec.startsWith('../')) {
682
574
  key = path.posix.normalize(path.posix.join(importerDir, spec));
683
575
  if (!key.startsWith('/')) {
684
- key = '/' + key;
576
+ key = `/${key}`;
685
577
  }
686
578
  }
687
579
  else {
@@ -748,6 +640,70 @@ function findDependencyFileName(depFileMap, key) {
748
640
  }
749
641
  return undefined;
750
642
  }
643
+ function isRuntimePluginRootEntrySpecifier(specifier, projectRoot) {
644
+ if (!specifier) {
645
+ return false;
646
+ }
647
+ const cleaned = specifier.replace(PAT.QUERY_PATTERN, '');
648
+ const normalized = normalizeNodeModulesSpecifier(cleaned) || cleaned.replace(/^\/+/, '');
649
+ if (!normalized) {
650
+ return false;
651
+ }
652
+ const { packageName, subpath } = resolveNodeModulesPackageBoundary(normalized, projectRoot);
653
+ if (!packageName || !isLikelyNativeScriptRuntimePluginSpecifier(packageName, projectRoot)) {
654
+ return false;
655
+ }
656
+ if (!subpath) {
657
+ return true;
658
+ }
659
+ if (subpath.includes('/')) {
660
+ return false;
661
+ }
662
+ const pkgBaseName = packageName.split('/').pop() || '';
663
+ const withoutExt = /(?:\.(?:ios|android|visionos))?\.(?:ts|tsx|js|jsx|mjs|mts|cts)$/i.test(subpath) ? subpath.replace(/\.[^.]+$/, '') : subpath;
664
+ const withoutPlatform = withoutExt.replace(/\.(ios|android|visionos)$/i, '');
665
+ return withoutPlatform === 'index' || withoutPlatform === pkgBaseName;
666
+ }
667
+ function collectMixedRuntimePluginHttpRootPackages(code, projectRoot) {
668
+ const nonRootSubpathPackages = new Set();
669
+ const rootEntryPackages = new Set();
670
+ const visitSpecifier = (rawSpecifier) => {
671
+ if (!rawSpecifier) {
672
+ return;
673
+ }
674
+ const specifier = normalizeNativeScriptCoreSpecifier(rawSpecifier).replace(PAT.QUERY_PATTERN, '');
675
+ if (!specifier) {
676
+ return;
677
+ }
678
+ if (/^https?:\/\//.test(specifier) || specifier.startsWith('/ns/')) {
679
+ return;
680
+ }
681
+ if (/^(?:\.|\/)/.test(specifier) && !specifier.includes('/node_modules/')) {
682
+ return;
683
+ }
684
+ const normalized = normalizeNodeModulesSpecifier(specifier) || specifier.replace(/^\/+/, '');
685
+ if (!normalized) {
686
+ return;
687
+ }
688
+ const { packageName } = resolveNodeModulesPackageBoundary(normalized, projectRoot);
689
+ if (!packageName || !isLikelyNativeScriptRuntimePluginSpecifier(packageName, projectRoot)) {
690
+ return;
691
+ }
692
+ if (isRuntimePluginRootEntrySpecifier(normalized, projectRoot)) {
693
+ rootEntryPackages.add(packageName);
694
+ return;
695
+ }
696
+ nonRootSubpathPackages.add(packageName);
697
+ };
698
+ for (const pattern of [PAT.IMPORT_PATTERN_1, PAT.IMPORT_PATTERN_2, PAT.IMPORT_PATTERN_3, PAT.IMPORT_PATTERN_SIDE_EFFECT]) {
699
+ pattern.lastIndex = 0;
700
+ let match;
701
+ while ((match = pattern.exec(code)) !== null) {
702
+ visitSpecifier(match[2]);
703
+ }
704
+ }
705
+ return new Set(Array.from(nonRootSubpathPackages).filter((packageName) => rootEntryPackages.has(packageName)));
706
+ }
751
707
  function collectImportDependencies(code, importerPath) {
752
708
  const importerDir = path.posix.dirname(importerPath);
753
709
  const deps = new Set();
@@ -809,68 +765,15 @@ function cleanCode(code) {
809
765
  result = ACTIVE_STRATEGY.preClean(result);
810
766
  result = ACTIVE_STRATEGY.rewriteFrameworkImports(result);
811
767
  // Vendor manifest-driven import rewrites
768
+ // NOTE: Static and side-effect vendor imports are intentionally NOT rewritten here.
769
+ // They are left as import statements so that ensureNativeScriptModuleBindings()
770
+ // (called later in processCodeForDevice) can transform them using the robust
771
+ // __nsVendorRequire + __nsPick pattern that works on device.
772
+ // Only dynamic imports are handled here since ensureNativeScriptModuleBindings
773
+ // does not process dynamic import() calls.
812
774
  try {
813
775
  const manifest = getVendorManifest();
814
776
  if (manifest) {
815
- // Pattern: capture full import statement (static) with optional bindings
816
- // import X from 'pkg'; | import {a,b as c} from "pkg"; | import * as ns from 'pkg';
817
- const staticImportRE = /(import\s+([^;]*?)\s+from\s*["'])([^"']+)(["'];?)/g;
818
- result = result.replace(staticImportRE, (full, pre, bindings, spec, post) => {
819
- // Do not vendor-rewrite @nativescript/core — handled by the unified HTTP bridge later
820
- if (isNativeScriptCoreModule(spec))
821
- return full;
822
- const resolved = resolveVendorSpecifier(spec);
823
- if (!resolved || /^@nativescript\/core(\b|\/)/i.test(resolved))
824
- return full; // not vendor or is core
825
- // Determine binding style
826
- const trimmed = (bindings || '').trim();
827
- let injected = '';
828
- if (!trimmed || trimmed === '') {
829
- // Side-effect import: import 'pkg'; -> we drop it (vendor already evaluated)
830
- return `/* vendor side-effect dropped: ${spec} */`;
831
- }
832
- // Default + named or default only
833
- // Examples of trimmed:
834
- // defaultExport
835
- // { a, b as c }
836
- // * as ns
837
- // defaultExport, { a, b }
838
- const globalAccessor = `globalThis.__nsVendor && globalThis.__nsVendor(${JSON.stringify(resolved)})`;
839
- const ensureHelper = `globalThis.__nsVendor=require? (globalThis.__nsVendor|| (globalThis.__nsVendor=(id)=>{const m=(globalThis.__NS_VENDOR_MANIFEST__?globalThis.__NS_VENDOR_MANIFEST__.modules[id]:null);return (globalThis.__nsModules && globalThis.__nsModules.get? (globalThis.__nsModules.get(id)||globalThis.__nsModules.get(m?.id||id)):undefined);})):globalThis.__nsVendor`;
840
- if (trimmed.startsWith('{')) {
841
- // Named only
842
- injected = `${ensureHelper}; const ${trimmed} = ${globalAccessor} || {};`;
843
- }
844
- else if (trimmed.startsWith('*')) {
845
- // Namespace import: * as ns
846
- const m = /\*\s+as\s+(\w+)/.exec(trimmed);
847
- if (m) {
848
- injected = `${ensureHelper}; const ${m[1]} = ${globalAccessor} || {};`;
849
- }
850
- }
851
- else if (trimmed.includes(',')) {
852
- // default plus named
853
- const parts = trimmed.split(',');
854
- const def = parts[0].trim();
855
- const named = parts.slice(1).join(',').trim();
856
- injected = `${ensureHelper}; const __vmod = ${globalAccessor} || {}; const ${def} = __vmod.default || __vmod; const ${named} = __vmod;`;
857
- }
858
- else {
859
- // default only
860
- injected = `${ensureHelper}; const ${trimmed} = (${globalAccessor}||{}).default || ${globalAccessor};`;
861
- }
862
- return injected;
863
- });
864
- // Bare side-effect imports: import 'pkg';
865
- const sideEffectRE = /(import\s*["'])([^"']+)(["'];?)/g;
866
- result = result.replace(sideEffectRE, (full, pre, spec, post) => {
867
- if (isNativeScriptCoreModule(spec))
868
- return full;
869
- const resolved = resolveVendorSpecifier(spec);
870
- if (!resolved || /^@nativescript\/core(\b|\/)/i.test(resolved))
871
- return full;
872
- return `/* vendor side-effect skipped: ${spec} */`;
873
- });
874
777
  // Dynamic import rewrites: import('pkg') -> Promise.resolve(__nsVendor('id'))
875
778
  const dynImportRE = /(import\(\s*["'])([^"']+)(["']\s*\))/g;
876
779
  result = result.replace(dynImportRE, (full, pre, spec, post) => {
@@ -999,6 +902,20 @@ function toAppModuleBaseId(importPath, projectRoot) {
999
902
  const base = projectRelative.replace(/\.mjs$/i, '');
1000
903
  return `/${base}`;
1001
904
  }
905
+ function toNodeModulesHttpModuleId(importPath) {
906
+ const nodeModulesSpecifier = normalizeNodeModulesSpecifier(importPath);
907
+ if (!nodeModulesSpecifier) {
908
+ return null;
909
+ }
910
+ return `/ns/m/node_modules/${nodeModulesSpecifier}`;
911
+ }
912
+ // `rewriteNsMImportPathForHmr` and `getNumericServeVersionTag` live in
913
+ // `./websocket-ns-m-paths.js`. The path rewriter is part of the
914
+ // "Stable URL + Explicit Invalidation" architecture and must be a
915
+ // single source of truth so the canonicalization rules can't drift
916
+ // between modules. They are imported above and re-exported below for
917
+ // tests / external callers that historically reached them through this
918
+ // module.
1002
919
  function normalizeAbsoluteFilesystemImport(spec, importerPath, projectRoot) {
1003
920
  if (!spec || typeof spec !== 'string') {
1004
921
  return null;
@@ -1025,13 +942,211 @@ function normalizeAbsoluteFilesystemImport(spec, importerPath, projectRoot) {
1025
942
  }
1026
943
  return absolute;
1027
944
  }
945
+ /**
946
+ * After the Angular linker runs on code that Vite has already resolved (bare
947
+ * specifiers → full URLs), the linker injects NEW import statements with bare
948
+ * specifiers (e.g. `import {Component} from '@angular/core'`). These cause:
949
+ * 1. Duplicate-identifier SyntaxErrors (the name was already imported via URL)
950
+ * 2. Unresolvable bare specifiers at runtime on device
951
+ *
952
+ * This function:
953
+ * • builds a map packageName → resolvedURL from existing resolved imports
954
+ * • collects all binding names already imported per package
955
+ * • for each bare-specifier import, removes duplicate bindings
956
+ * • rewrites any genuinely-new bindings to use the resolved URL
957
+ */
958
+ function deduplicateLinkerImports(code) {
959
+ if (!code)
960
+ return code;
961
+ try {
962
+ const imports = collectTopLevelImportRecords(code);
963
+ if (!imports.length) {
964
+ return code;
965
+ }
966
+ // ── Step 1: collect resolved imports already in the file ──────────
967
+ const pkgUrlMap = new Map();
968
+ const pkgBindings = new Map();
969
+ for (const imp of imports) {
970
+ const url = imp.source;
971
+ if (!/^https?:\/\//.test(url) && !url.startsWith('/')) {
972
+ continue;
973
+ }
974
+ const nmIdx = url.lastIndexOf('/node_modules/');
975
+ if (nmIdx === -1)
976
+ continue;
977
+ const afterNm = url.substring(nmIdx + '/node_modules/'.length);
978
+ const parts = afterNm.split('/');
979
+ const pkg = parts[0].startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
980
+ if (!pkgUrlMap.has(pkg))
981
+ pkgUrlMap.set(pkg, url);
982
+ if (imp.namedBindings.length) {
983
+ if (!pkgBindings.has(pkg))
984
+ pkgBindings.set(pkg, new Set());
985
+ for (const binding of imp.namedBindings) {
986
+ if (binding.importedName)
987
+ pkgBindings.get(pkg).add(binding.importedName);
988
+ }
989
+ }
990
+ }
991
+ if (pkgUrlMap.size === 0)
992
+ return code;
993
+ // ── Step 2: rewrite bare-specifier imports ───────────────────────
994
+ const edits = [];
995
+ for (const imp of imports) {
996
+ if (!imp.hasOnlyNamedSpecifiers) {
997
+ continue;
998
+ }
999
+ const specifier = imp.source;
1000
+ if (specifier.startsWith('/') || specifier.startsWith('.') || specifier.startsWith('http')) {
1001
+ continue;
1002
+ }
1003
+ const parts = specifier.split('/');
1004
+ const pkg = specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
1005
+ const url = pkgUrlMap.get(pkg);
1006
+ if (!url) {
1007
+ continue;
1008
+ }
1009
+ const existing = pkgBindings.get(pkg) || new Set();
1010
+ const newBindings = imp.namedBindings.filter((binding) => !existing.has(binding.importedName));
1011
+ if (newBindings.length === 0) {
1012
+ edits.push({ start: imp.start, end: imp.end, text: '' });
1013
+ continue;
1014
+ }
1015
+ if (newBindings.length === imp.namedBindings.length) {
1016
+ continue;
1017
+ }
1018
+ for (const binding of newBindings) {
1019
+ existing.add(binding.importedName);
1020
+ }
1021
+ edits.push({
1022
+ start: imp.start,
1023
+ end: imp.end,
1024
+ text: `import { ${newBindings.map((binding) => binding.text).join(', ')} } from ${JSON.stringify(url)};`,
1025
+ });
1026
+ }
1027
+ if (!edits.length) {
1028
+ return code;
1029
+ }
1030
+ let next = code;
1031
+ for (const edit of edits.sort((left, right) => right.start - left.start)) {
1032
+ next = next.slice(0, edit.start) + edit.text + next.slice(edit.end);
1033
+ }
1034
+ return next;
1035
+ }
1036
+ catch {
1037
+ return code;
1038
+ }
1039
+ }
1040
+ export function wrapCommonJsModuleForDevice(code, absolutePath) {
1041
+ if (!code)
1042
+ return code;
1043
+ try {
1044
+ const hasExportDefault = /\bexport\s+default\b/.test(code) || /export\s*\{\s*default\s*(?:as\s*default)?\s*\}/.test(code);
1045
+ const hasNamedExports = /\bexport\s+(?:const|let|var|function|class|async)\b/.test(code) || /\bexport\s*\{/.test(code);
1046
+ const hasCjsExports = /\bmodule\s*\.\s*exports\b/.test(code) || /\bexports\s*\.\s*\w/.test(code);
1047
+ if (hasExportDefault || hasNamedExports || !hasCjsExports) {
1048
+ return code;
1049
+ }
1050
+ const namedExports = new Set();
1051
+ const exportsRe = /\bexports\s*\.\s*([A-Za-z_$][\w$]*)\s*=/g;
1052
+ let em;
1053
+ while ((em = exportsRe.exec(code)) !== null) {
1054
+ const name = em[1];
1055
+ if (name !== '__esModule' && name !== 'default') {
1056
+ namedExports.add(name);
1057
+ }
1058
+ }
1059
+ const defPropRe = /Object\s*\.\s*defineProperty\s*\(\s*exports\s*,\s*['"]([^'"]+)['"]/g;
1060
+ while ((em = defPropRe.exec(code)) !== null) {
1061
+ const name = em[1];
1062
+ if (name !== '__esModule' && name !== 'default') {
1063
+ namedExports.add(name);
1064
+ }
1065
+ }
1066
+ // Static enumeration only sees `exports.foo = ...` and `Object.defineProperty(exports, 'foo', ...)`.
1067
+ // Real-world packages like lodash attach their entire surface to a function inside an IIFE and
1068
+ // then `module.exports = thatFunction`. Static analysis returns zero in that case. To handle
1069
+ // these modules we ALSO load the package in the dev-server's Node context (only when we have a
1070
+ // node_modules path) and merge the runtime keys. See `helpers/cjs-named-exports.ts` for the
1071
+ // reasoning and safety boundaries.
1072
+ if (absolutePath) {
1073
+ try {
1074
+ for (const n of getCjsNamedExports(absolutePath)) {
1075
+ namedExports.add(n);
1076
+ }
1077
+ }
1078
+ catch {
1079
+ /* fall through to whatever we caught statically */
1080
+ }
1081
+ }
1082
+ let suffix = `\nvar __cjs_mod = module.exports;\nexport default __cjs_mod;\n`;
1083
+ if (namedExports.size) {
1084
+ const entries = Array.from(namedExports);
1085
+ const temps = entries.map((name, i) => `var __cjs_e${i} = __cjs_mod[${JSON.stringify(name)}];`);
1086
+ const reExports = entries.map((name, i) => `__cjs_e${i} as ${name}`);
1087
+ suffix += `${temps.join(' ')}\nexport { ${reExports.join(', ')} };\n`;
1088
+ }
1089
+ const prelude = `var module = { exports: {} }; var exports = module.exports;\n` +
1090
+ `var __ns_cjs_require_base = (typeof globalThis.__nsBaseRequire === 'function' ? globalThis.__nsBaseRequire : (typeof globalThis.__nsRequire === 'function' ? globalThis.__nsRequire : (typeof globalThis.require === 'function' ? globalThis.require : undefined)));\n` +
1091
+ `var __ns_cjs_require_kind = (typeof globalThis.__nsBaseRequire === 'function' ? 'base-require' : (typeof globalThis.__nsRequire === 'function' ? 'vendor-require' : 'global-require'));\n` +
1092
+ `var require = function(spec) {\n` +
1093
+ ` if (!__ns_cjs_require_base) { throw new Error('require is not defined'); }\n` +
1094
+ // Resolve relative specifiers against the HTTP-served module's URL
1095
+ // before delegating to NS's runtime require. Without this step,
1096
+ // \`require('./base64-vlq')\` inside a CJS module served from
1097
+ // \`http://.../ns/m/node_modules/source-map-js/lib/source-map-generator.js\`
1098
+ // would pass a literal '"./base64-vlq"' to the native require, which
1099
+ // has no notion of the current HTTP-module's location and either
1100
+ // throws "Module not found" or fetches an arbitrary filesystem path
1101
+ // that happens to parse as code (producing misleading syntax errors
1102
+ // like "missing ) after argument list" from unrelated modules).
1103
+ ` var __nsResolvedSpec = spec;\n` +
1104
+ ` try {\n` +
1105
+ ` if (typeof spec === 'string' && (spec.indexOf('./') === 0 || spec.indexOf('../') === 0)) {\n` +
1106
+ ` var __nsParentUrl = (typeof import.meta !== 'undefined' && import.meta && typeof import.meta.url === 'string') ? import.meta.url : null;\n` +
1107
+ ` if (__nsParentUrl) {\n` +
1108
+ ` var __nsResolvedUrl = new URL(spec, __nsParentUrl);\n` +
1109
+ ` // Common Node-style bare extensions: prefer .js if the resolved URL lacks an extension in its last path segment.\n` +
1110
+ ` if (!/\\.[A-Za-z0-9]+$/.test(__nsResolvedUrl.pathname.split('/').pop() || '')) {\n` +
1111
+ ` __nsResolvedUrl.pathname = __nsResolvedUrl.pathname.replace(/\\/+$/, '') + '.js';\n` +
1112
+ ` }\n` +
1113
+ ` __nsResolvedSpec = __nsResolvedUrl.href;\n` +
1114
+ ` }\n` +
1115
+ ` }\n` +
1116
+ ` } catch (e) {}\n` +
1117
+ ` 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` +
1118
+ ` var mod = __ns_cjs_require_base(__nsResolvedSpec);\n` +
1119
+ ` try {\n` +
1120
+ ` if (mod && (typeof mod === 'object' || typeof mod === 'function') && mod.default !== undefined) {\n` +
1121
+ ` var keys = [];\n` +
1122
+ ` try { keys = Object.keys(mod); } catch (e) {}\n` +
1123
+ ` var defaultOnly = keys.length === 1 && keys[0] === 'default';\n` +
1124
+ ` var esModuleOnly = keys.length === 2 && keys.indexOf('default') !== -1 && keys.indexOf('__esModule') !== -1;\n` +
1125
+ ` if (mod.__esModule || defaultOnly || esModuleOnly) { return mod.default; }\n` +
1126
+ ` }\n` +
1127
+ ` } catch (e) {}\n` +
1128
+ ` return mod;\n` +
1129
+ `};\n`;
1130
+ return `${prelude}${code}${suffix}`;
1131
+ }
1132
+ catch {
1133
+ return code;
1134
+ }
1135
+ }
1028
1136
  /**
1029
1137
  * Process code for device: inject globals, remove framework imports
1030
1138
  */
1031
- function processCodeForDevice(code, isVitePreBundled) {
1139
+ function processCodeForDevice(code, isVitePreBundled, preserveVendorImports = false, isNodeModule = false, sourceId, options) {
1032
1140
  let result = code;
1141
+ const resolvedSpecifierOverrides = options?.resolvedSpecifierOverrides || getProcessCodeResolvedSpecifierOverrides(sourceId, getProjectRootPath());
1142
+ const bindingOptions = {
1143
+ preserveNonPluginVendorImports: preserveVendorImports,
1144
+ resolvedSpecifierOverrides,
1145
+ };
1033
1146
  // Ensure Angular partial declarations are linked before any sanitizers run so runtime never hits the JIT path.
1034
1147
  result = linkAngularPartialsIfNeeded(result);
1148
+ // Post-linker: deduplicate/resolve imports the Angular linker injected with bare specifiers
1149
+ result = deduplicateLinkerImports(result);
1035
1150
  // First: aggressively strip any lingering virtual dynamic-import-helper before anything else.
1036
1151
  // Doing this up-front prevents downstream dependency collection from seeing the virtual id.
1037
1152
  result = stripViteDynamicImportVirtual(result);
@@ -1042,13 +1157,17 @@ function processCodeForDevice(code, isVitePreBundled) {
1042
1157
  // Inject ALL NativeScript/build globals at the top (matching global-defines.ts)
1043
1158
  // This ensures any code using __DEV__, __ANDROID__, __IOS__, etc. works correctly
1044
1159
  const allGlobals = [
1160
+ // Minimal process shim — populated with CLI --env.* flags at module load time.
1161
+ // In production builds, Vite/Rollup replaces process.env.* statically.
1162
+ // In HMR dev mode the code runs as-is on device, so we need the shim.
1163
+ `if (typeof process === "undefined") { globalThis.process = { env: ${__processEnvJson} }; } else if (!process.env) { process.env = ${__processEnvJson}; }`,
1045
1164
  'const __ANDROID__ = globalThis.__ANDROID__ !== undefined ? globalThis.__ANDROID__ : false;',
1046
1165
  'const __IOS__ = globalThis.__IOS__ !== undefined ? globalThis.__IOS__ : false;',
1047
1166
  'const __VISIONOS__ = globalThis.__VISIONOS__ !== undefined ? globalThis.__VISIONOS__ : false;',
1048
1167
  'const __APPLE__ = globalThis.__APPLE__ !== undefined ? globalThis.__APPLE__ : (__IOS__ || __VISIONOS__);',
1049
1168
  'const __DEV__ = globalThis.__DEV__ !== undefined ? globalThis.__DEV__ : false;',
1050
1169
  'const __COMMONJS__ = globalThis.__COMMONJS__ !== undefined ? globalThis.__COMMONJS__ : false;',
1051
- 'const __NS_WEBPACK__ = globalThis.__NS_WEBPACK__ !== undefined ? globalThis.__NS_WEBPACK__ : true;',
1170
+ 'const __NS_WEBPACK__ = globalThis.__NS_WEBPACK__ !== undefined ? globalThis.__NS_WEBPACK__ : false;',
1052
1171
  'const __NS_ENV_VERBOSE__ = globalThis.__NS_ENV_VERBOSE__ !== undefined ? !!globalThis.__NS_ENV_VERBOSE__ : false;',
1053
1172
  "const __CSS_PARSER__ = globalThis.__CSS_PARSER__ !== undefined ? globalThis.__CSS_PARSER__ : 'css-tree';",
1054
1173
  'const __UI_USE_XML_PARSER__ = globalThis.__UI_USE_XML_PARSER__ !== undefined ? globalThis.__UI_USE_XML_PARSER__ : true;',
@@ -1056,19 +1175,29 @@ function processCodeForDevice(code, isVitePreBundled) {
1056
1175
  'const __TEST__ = globalThis.__TEST__ !== undefined ? globalThis.__TEST__ : false;',
1057
1176
  ];
1058
1177
  result = allGlobals.join('\n') + '\n' + result;
1059
- // Prefer AST-based normalization for imports and helper aliases; fallback regex if parsing fails
1060
- try {
1061
- result = astNormalizeModuleImportsAndHelpers(result);
1178
+ const nodeModuleProvenancePrelude = buildNodeModuleProvenancePrelude(sourceId);
1179
+ if (nodeModuleProvenancePrelude) {
1180
+ result = nodeModuleProvenancePrelude + result;
1062
1181
  }
1063
- catch { }
1064
- // Verify there are no duplicate top-level const/let bindings after AST normalization
1065
- try {
1066
- result = astVerifyAndAnnotateDuplicates(result);
1182
+ // AST normalization: inject /ns/rt helper aliases for underscore-prefixed identifiers.
1183
+ // ONLY for app source files library code in node_modules should be served as-is.
1184
+ // Running the normalizer on libraries like tslib injects harmful destructures
1185
+ // (e.g., `const { SuppressedError } = __ns_rt_ns_1`) that shadow globals.
1186
+ if (!isNodeModule) {
1187
+ try {
1188
+ result = astNormalizeModuleImportsAndHelpers(result);
1189
+ }
1190
+ catch { }
1191
+ // Verify there are no duplicate top-level const/let bindings after AST normalization
1192
+ try {
1193
+ result = astVerifyAndAnnotateDuplicates(result);
1194
+ }
1195
+ catch { }
1067
1196
  }
1068
- catch { }
1069
- // If AST marker present, skip regex-based helper alias injection to avoid duplicates
1070
- // Accept both line and block comment markers emitted by the normalizer
1071
- if (!/^\s*(?:\/\/|\/\*) \[ast-normalized\]/m.test(result)) {
1197
+ // If AST marker present OR this is a node_modules file, skip regex-based helper
1198
+ // alias injection. Library code should NOT get /ns/rt destructures injected
1199
+ // underscore-prefixed identifiers in libraries are internal variables, not NS helpers.
1200
+ if (!isNodeModule && !/^\s*(?:\/\/|\/\*) \[ast-normalized\]/m.test(result)) {
1072
1201
  try {
1073
1202
  const underscored = new Set();
1074
1203
  const re = /(^|[^.\w$])_([A-Za-z]\w*)\b/g;
@@ -1149,7 +1278,11 @@ function processCodeForDevice(code, isVitePreBundled) {
1149
1278
  result = result.replace(/(^|\n)([\t ]*import\s+[^;]*?\s+from)\s*\n\s*("\/?node_modules\/\.vite\/deps\/[^"\n]+"\s*;?\s*)/gm, (_m, p1, p2, p3) => `${p1}${p2} ${p3}`);
1150
1279
  }
1151
1280
  catch { }
1152
- result = ensureNativeScriptModuleBindings(result);
1281
+ // When preserveVendorImports is true (HMR /ns/m/ endpoint), skip the
1282
+ // __nsVendorRequire + __nsPick rewrite. Vendor imports stay as bare
1283
+ // specifiers so the device-side import map resolves them via V8's native
1284
+ // module system, which correctly handles export * re-exports.
1285
+ result = ensureNativeScriptModuleBindings(result, bindingOptions);
1153
1286
  // Repair any accidental "import ... = expr" assignments that may have slipped in.
1154
1287
  try {
1155
1288
  result = repairImportEqualsAssignments(result);
@@ -1158,10 +1291,7 @@ function processCodeForDevice(code, isVitePreBundled) {
1158
1291
  // Strip Vite prebundle deps imports (both named and side-effect) and any malformed const string artifacts
1159
1292
  // Example problematic line observed: const "/node_modules/.vite/deps/@nativescript_firebase-messaging.js?v=...";
1160
1293
  if (/node_modules\/\.vite\/deps\//.test(result)) {
1161
- // Named imports from prebundle deps
1162
- result = result.replace(/^[\t ]*import\s+[^;]*from\s+["']\/?node_modules\/\.vite\/deps\/[^"']+["'];?\s*$/gm, '');
1163
- // Side-effect only imports from prebundle deps
1164
- result = result.replace(/^[\t ]*import\s+["']\/?node_modules\/\.vite\/deps\/[^"']+["'];?\s*$/gm, '');
1294
+ result = rewriteVitePrebundleImportsForDevice(result, preserveVendorImports);
1165
1295
  // Malformed const string lines accidentally produced by upstream transforms
1166
1296
  result = result.replace(/^[\t ]*const\s+["']\/?node_modules\/\.vite\/deps\/[^"']+["'];?\s*$/gm, '// [hmr-sanitize] stripped malformed const prebundle ref\n');
1167
1297
  // Naked string-only lines pointing at prebundle deps
@@ -1248,7 +1378,7 @@ function processCodeForDevice(code, isVitePreBundled) {
1248
1378
  }
1249
1379
  // Ensure vendor bindings also apply after potential wrapper injections above
1250
1380
  // (idempotent: second pass will be a no-op if imports already consumed).
1251
- result = ensureNativeScriptModuleBindings(result);
1381
+ result = ensureNativeScriptModuleBindings(result, bindingOptions);
1252
1382
  try {
1253
1383
  result = repairImportEqualsAssignments(result);
1254
1384
  }
@@ -1289,17 +1419,17 @@ function processCodeForDevice(code, isVitePreBundled) {
1289
1419
  result = normalizeStrayCoreStringLiterals(result);
1290
1420
  }
1291
1421
  catch { }
1422
+ try {
1423
+ result = fixDanglingCoreFrom(result);
1424
+ }
1425
+ catch { }
1426
+ try {
1427
+ result = normalizeAnyCoreSpecToBridge(result);
1428
+ }
1429
+ catch { }
1292
1430
  result = ensureVariableDynamicImportHelper(result);
1293
1431
  // Normalize any lingering @nativescript/core imports to the /ns/core bridge (non-destructive best-effort)
1294
1432
  try {
1295
- result = result.replace(/from\s+["']@nativescript\/core([^"'\n]*)["']/g, (_m, sub) => {
1296
- const qp = (sub || '').trim().replace(/^\//, '');
1297
- return `from "/ns/core${qp ? `?p=${qp}` : ''}"`;
1298
- });
1299
- result = result.replace(/import\(\s*["']@nativescript\/core([^"'\n]*)["']\s*\)/g, (_m, sub) => {
1300
- const qp = (sub || '').trim().replace(/^\//, '');
1301
- return `import("/ns/core${qp ? `?p=${qp}` : ''}")`;
1302
- });
1303
1433
  // Rewrite named imports from the /ns/core bridge into default import + destructuring.
1304
1434
  // This makes `import { Frame } from '@nativescript/core'` work even if the bridge provides only a default export.
1305
1435
  {
@@ -1315,6 +1445,9 @@ function processCodeForDevice(code, isVitePreBundled) {
1315
1445
  .join(', ');
1316
1446
  const reNamed = /(^|\n)\s*import\s*\{([^}]+)\}\s*from\s*["']((?:https?:\/\/[^"']+)?\/ns\/core(?:\/[\d]+)?(?:\?p=[^"']+)?)['"];?\s*/gm;
1317
1447
  result = result.replace(reNamed, (_full, pfx, specList, src) => {
1448
+ // Deep subpath URLs serve actual ESM with real named exports — skip.
1449
+ if (isDeepCoreSubpath(src))
1450
+ return _full;
1318
1451
  __core_ns_seq++;
1319
1452
  const tmp = `__ns_core_ns${__core_ns_seq}`;
1320
1453
  const decl = `const { ${toDestructureCore(specList)} } = ${tmp};`;
@@ -1322,6 +1455,8 @@ function processCodeForDevice(code, isVitePreBundled) {
1322
1455
  });
1323
1456
  const reMixed = /(^|\n)\s*import\s+([A-Za-z_$][\w$]*)\s*,\s*\{([^}]+)\}\s*from\s*["']((?:https?:\/\/[^"']+)?\/ns\/core(?:\/[\d]+)?(?:\?p=[^"']+)?)['"];?\s*/gm;
1324
1457
  result = result.replace(reMixed, (_full, pfx, defName, specList, src) => {
1458
+ if (isDeepCoreSubpath(src))
1459
+ return _full;
1325
1460
  const decl = `const { ${toDestructureCore(specList)} } = ${defName};`;
1326
1461
  return `${pfx}import ${defName} from ${JSON.stringify(src)};\n${decl}\n`;
1327
1462
  });
@@ -1335,8 +1470,19 @@ function processCodeForDevice(code, isVitePreBundled) {
1335
1470
  // Keep a single semicolon before the import to avoid generating ';;'
1336
1471
  result = result.replace(/;\s*import\s+/g, ';\nimport ');
1337
1472
  result = result.replace(/}\s*import\s+/g, '}\nimport ');
1338
- // Fallback: ensure any static import that isn't at start of line gets a newline before it
1339
- result = result.replace(/([^\n])\s*(import\s+[^;\n]*\s+from\s*["'][^"']+["'])/g, '$1\n$2');
1473
+ // Fallback: ensure any static import that isn't at start of line gets a newline before it.
1474
+ //
1475
+ // Only match after **structural** statement-ending characters: `;`, `}`, `)`, `]`. We
1476
+ // deliberately do NOT include `'`, `"`, or `` ` `` here — those are string-literal
1477
+ // terminators (and openers!), and including them caused the regex to fire inside
1478
+ // example code embedded in error strings. Concrete failure observed:
1479
+ // `@supabase/realtime-js` throws an Error whose message contains the literal
1480
+ // `' import ws from "ws"\n' +`. With `'` in the delimiter class, the engine matched
1481
+ // the opening `'` of that string literal as a "statement terminator" and rewrote the
1482
+ // example to `'\nimport ws from "..."` — splitting the string across two lines and
1483
+ // producing a SyntaxError on device. The structural delimiters below do not appear
1484
+ // inside string-literal openers, so the rewrite is safe.
1485
+ result = result.replace(/([;}\)\]])\s*(import\s+[^;\n]*\s+from\s*["'][^"']+["'])/g, '$1\n$2');
1340
1486
  }
1341
1487
  catch { }
1342
1488
  // Collapse duplicate destructuring from the same temp namespace var (e.g., multiple const { x } = __ns_rt_ns1)
@@ -1370,22 +1516,13 @@ function processCodeForDevice(code, isVitePreBundled) {
1370
1516
  // always come before any statements that might reference their bindings. This ordering avoids
1371
1517
  // device runtimes that are stricter about imports-first semantics during module instantiation.
1372
1518
  try {
1373
- const importLineRe = /^\s*import\s+[^;]+;?\s*$/gm;
1374
- const lines = [];
1375
- result = result.replace(importLineRe, (imp) => {
1376
- lines.push(imp.trim());
1377
- return '';
1378
- });
1379
- if (lines.length) {
1380
- const hoisted = Array.from(new Set(lines)).join('\n') + '\n';
1381
- result = hoisted + result;
1382
- }
1519
+ result = hoistTopLevelStaticImports(result);
1383
1520
  }
1384
1521
  catch { }
1385
1522
  // Final safety: normalize any lingering named imports from /ns/rt into default+destructure
1386
- // Skip when AST normalization marker present to avoid introducing duplicate temp imports
1523
+ // Skip for node_modules (no /ns/rt helpers needed) and when AST marker present
1387
1524
  try {
1388
- if (!/^\s*\/\* \[ast-normalized\] \*\//m.test(result)) {
1525
+ if (!isNodeModule && !/^\s*\/\* \[ast-normalized\] \*\//m.test(result)) {
1389
1526
  result = ensureDestructureRtImports(result);
1390
1527
  }
1391
1528
  }
@@ -1531,6 +1668,16 @@ function assertNoOptimizedArtifacts(code, contextLabel) {
1531
1668
  }
1532
1669
  }
1533
1670
  if (localCore.test(ln)) {
1671
+ // Comments can never cause split-realm risk at runtime — skip them.
1672
+ // Library authors commonly reference @nativescript/core in comments
1673
+ // (e.g. TSDoc /// <reference> directives, module resolution notes).
1674
+ const trimmed = ln.trimStart();
1675
+ if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) {
1676
+ continue;
1677
+ }
1678
+ if (shouldAllowLocalCoreSanitizerPaths(contextLabel)) {
1679
+ continue;
1680
+ }
1534
1681
  offenders.push(`${i + 1}: ${ln.substring(0, 200)} [local-core-path]`);
1535
1682
  }
1536
1683
  if (offenders.length >= 10)
@@ -1555,6 +1702,7 @@ function assertNoOptimizedArtifacts(code, contextLabel) {
1555
1702
  function ensureDestructureCoreImports(code) {
1556
1703
  try {
1557
1704
  let result = code;
1705
+ let coreImportCounter = 0;
1558
1706
  const toDestructure = (specList) => specList
1559
1707
  .split(',')
1560
1708
  .map((s) => s.trim())
@@ -1567,13 +1715,19 @@ function ensureDestructureCoreImports(code) {
1567
1715
  // import { A, B } from '/ns/core[/ver][?p=...]'
1568
1716
  const reNamed = /(^|\n)\s*import\s*\{([^}]+)\}\s*from\s*["']((?:https?:\/\/[^"']+)?\/ns\/core(?:\/[\d]+)?(?:\?p=[^"']+)?)['"];?\s*/gm;
1569
1717
  result = result.replace(reNamed, (_full, pfx, specList, src) => {
1570
- const tmp = `__ns_core_ns_re`; // temp binding name is not reused elsewhere after hoist
1718
+ // Deep subpath URLs serve actual ESM with real named exports skip.
1719
+ if (isDeepCoreSubpath(src))
1720
+ return _full;
1721
+ const tmp = `__ns_core_ns_re${coreImportCounter > 0 ? `_${coreImportCounter}` : ''}`;
1722
+ coreImportCounter++;
1571
1723
  const decl = `const { ${toDestructure(specList)} } = ${tmp};`;
1572
1724
  return `${pfx}import ${tmp} from ${JSON.stringify(src)};\n${decl}\n`;
1573
1725
  });
1574
1726
  // import Default, { A, B } from '/ns/core[...]'
1575
1727
  const reMixed = /(^|\n)\s*import\s+([A-Za-z_$][\w$]*)\s*,\s*\{([^}]+)\}\s*from\s*["']((?:https?:\/\/[^"']+)?\/ns\/core(?:\/[\d]+)?(?:\?p=[^"']+)?)['"];?\s*/gm;
1576
1728
  result = result.replace(reMixed, (_full, pfx, defName, specList, src) => {
1729
+ if (isDeepCoreSubpath(src))
1730
+ return _full;
1577
1731
  const decl = `const { ${toDestructure(specList)} } = ${defName};`;
1578
1732
  return `${pfx}import ${defName} from ${JSON.stringify(src)};\n${decl}\n`;
1579
1733
  });
@@ -1685,14 +1839,21 @@ function dedupeRtNamedImportsAgainstDestructures(code) {
1685
1839
  /**
1686
1840
  * THE SINGLE REWRITE FUNCTION - used everywhere for consistency
1687
1841
  */
1688
- function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot, verbose = false, outputDirOverrideRel, httpOrigin) {
1842
+ export function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot, verbose = false, outputDirOverrideRel, httpOrigin, resolveVendorAsHttp = false) {
1689
1843
  let result = code;
1690
1844
  const httpOriginSafe = httpOrigin;
1845
+ const mixedRuntimePluginHttpRootPackages = collectMixedRuntimePluginHttpRootPackages(result, projectRoot);
1846
+ const isDynamicImportPrefix = (prefix) => /import\(\s*["']?$/.test(prefix.trimStart());
1691
1847
  const importerDir = path.posix.dirname(importerPath);
1848
+ // Resolved once per `rewriteImports` call so the per-import `/@fs/` rewriter
1849
+ // can convert workspace-lib paths back into our `/ns/m/` pipeline. Memoized
1850
+ // upstream — calling here is cheap and we reuse the value below.
1851
+ const monorepoWorkspaceRootForRewrite = getMonorepoWorkspaceRoot(projectRoot);
1692
1852
  // Determine importer output relative path (project-relative .mjs) to compute relative imports consistently
1693
1853
  const importerOutRel = outputDirOverrideRel || getProjectRelativeImportPath(importerPath, projectRoot) || stripToProjectRelative(importerPath, projectRoot).replace(/\.(ts|js|tsx|jsx|mjs|mts|cts)$/i, '.mjs');
1694
1854
  const importerOutDir = importerOutRel ? path.posix.dirname(importerOutRel) : '';
1695
1855
  const ensureRel = (p) => (p.startsWith('.') ? p : `./${p}`);
1856
+ const isNsSfcSpecifier = (spec) => /^(?:https?:\/\/[^/]+)?\/ns\/sfc(?:\/\d+)?(?:\/|$)/.test(spec.replace(PAT.QUERY_PATTERN, ''));
1696
1857
  // Normalize all @nativescript/core imports to the unified HTTP ESM core bridge to guarantee a single realm on device
1697
1858
  try {
1698
1859
  let coreAliasIdx = 0;
@@ -1754,7 +1915,23 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1754
1915
  const cleanPath = jsonPath.split('?')[0];
1755
1916
  // Resolve the JSON file path relative to the importer
1756
1917
  let fullPath;
1757
- if (cleanPath.startsWith('/')) {
1918
+ if (cleanPath.startsWith('/@fs/')) {
1919
+ // Vite filesystem URL: `/@fs/<abs-path>`. Strip the `/@fs` prefix
1920
+ // (4 chars, leaving the leading `/`) to recover the absolute
1921
+ // path. This matches `rewriteFsAbsoluteToNsM`'s convention and
1922
+ // covers both bare specifiers Vite pre-resolved out of the
1923
+ // project root (e.g. `emojibase-data/en/compact.json` →
1924
+ // `/@fs/.../node_modules/.../compact.json`) and tsconfig
1925
+ // path-alias targets that resolve outside the project root
1926
+ // (e.g. `~shared/...metadata.json` → `/@fs/.../tools/...json`).
1927
+ // Without this branch the next `else if` would `path.join` the
1928
+ // `/@fs/...` URL onto `projectRoot`, collapsing the leading `/`
1929
+ // and producing a malformed nested path that always misses on
1930
+ // `existsSync` and triggers a `ReferenceError` at runtime when
1931
+ // the JSON-import-failed comment leaves the binding undefined.
1932
+ fullPath = cleanPath.slice('/@fs'.length);
1933
+ }
1934
+ else if (cleanPath.startsWith('/')) {
1758
1935
  // Absolute from project root
1759
1936
  fullPath = path.join(projectRoot, cleanPath);
1760
1937
  }
@@ -1776,7 +1953,7 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1776
1953
  return `const ${varName} = ${jsonContent};`;
1777
1954
  }
1778
1955
  else {
1779
- console.warn(`[rewrite] JSON file not found: ${fullPath}`);
1956
+ console.warn(`[rewrite] JSON file not found: ${fullPath} (specifier=${jsonPath})`);
1780
1957
  }
1781
1958
  }
1782
1959
  catch (error) {
@@ -1827,14 +2004,48 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1827
2004
  if (spec === '@') {
1828
2005
  const stub = `/ns/m/__invalid_at__.mjs`;
1829
2006
  if (verbose) {
1830
- try {
1831
- console.warn(`[rewrite] mapped bare '@' spec to stub: ${stub}`);
1832
- }
1833
- catch { }
2007
+ console.warn(`[rewrite] mapped bare '@' spec to stub: ${stub}`);
1834
2008
  }
1835
2009
  return `${prefix}${stub}${suffix}`;
1836
2010
  }
1837
2011
  spec = normalizeNativeScriptCoreSpecifier(spec);
2012
+ // Pull `/@fs/<abs-path>` URLs back into the `/ns/m/` pipeline so they
2013
+ // hit our CJS/UMD-wrapping handler. Vite emits `/@fs/...` for any
2014
+ // resolved id outside the configured `root` — including hoisted
2015
+ // `node_modules/<pkg>` entries and workspace libs in monorepos. Left
2016
+ // untouched, the device fetches them through Vite's standard
2017
+ // middleware which never invokes `wrapCommonJsModuleForDevice`, so a
2018
+ // UMD module like papaparse crashes on `(this).Papa = factory()`
2019
+ // because top-level `this` is `undefined` in ESM context.
2020
+ if (spec.startsWith('/@fs/')) {
2021
+ const rewritten = rewriteFsAbsoluteToNsM(spec, projectRoot, monorepoWorkspaceRootForRewrite);
2022
+ if (rewritten) {
2023
+ if (httpOriginSafe) {
2024
+ return `${prefix}${httpOriginSafe}${rewritten}${suffix}`;
2025
+ }
2026
+ return `${prefix}${rewritten}${suffix}`;
2027
+ }
2028
+ // Path resolves outside both roots — leave Vite's URL alone as a
2029
+ // last resort. The original behaviour was to fall through here
2030
+ // and let downstream branches (e.g. `normalizeNodeModulesSpecifier`)
2031
+ // handle paths whose abs form happens to contain `/node_modules/`,
2032
+ // so preserve that for the unrewritable case below.
2033
+ }
2034
+ // Route Vite virtual modules (/@solid-refresh, etc.) through /ns/m/ so their
2035
+ // internal imports (e.g. solid-js) get vendor-rewritten by our pipeline.
2036
+ // Skip known Vite internals (/@vite/, /@id/) which are handled elsewhere.
2037
+ // `/@fs/` is intentionally excluded above; if we ever reach here with a
2038
+ // `/@fs/` spec it means the rewrite-to-`/ns/m/` pass couldn't anchor it
2039
+ // under projectRoot or workspaceRoot, so we fall through and rely on the
2040
+ // `normalizeNodeModulesSpecifier` branch below for paths that still
2041
+ // contain a `/node_modules/<pkg>/` segment.
2042
+ if (spec.startsWith('/@') && !/^\/@(?:vite|id|fs)\//.test(spec)) {
2043
+ const out = `/ns/m${spec}`;
2044
+ if (httpOriginSafe) {
2045
+ return `${prefix}${httpOriginSafe}${out}${suffix}`;
2046
+ }
2047
+ return `${prefix}${out}${suffix}`;
2048
+ }
1838
2049
  // Route internal NS endpoints to absolute HTTP origin for device
1839
2050
  if (spec.startsWith('/ns/')) {
1840
2051
  if (httpOriginSafe) {
@@ -1846,20 +2057,50 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1846
2057
  return `${prefix}${spec}${suffix}`;
1847
2058
  }
1848
2059
  const nodeModulesSpecifier = normalizeNodeModulesSpecifier(spec);
1849
- const candidateNativeScriptSpec = nodeModulesSpecifier ?? spec;
1850
- const vendorCanonical = resolveVendorFromCandidate(nodeModulesSpecifier ?? spec);
1851
- if (vendorCanonical) {
1852
- if (nodeModulesSpecifier) {
1853
- return `${prefix}${nodeModulesSpecifier.replace(PAT.QUERY_PATTERN, '')}${suffix}`;
2060
+ const normalizedRuntimePluginSpec = nodeModulesSpecifier || spec.replace(PAT.QUERY_PATTERN, '').replace(/^\/+/, '');
2061
+ if (normalizedRuntimePluginSpec && mixedRuntimePluginHttpRootPackages.size > 0) {
2062
+ const { packageName } = resolveNodeModulesPackageBoundary(normalizedRuntimePluginSpec, projectRoot);
2063
+ if (packageName && mixedRuntimePluginHttpRootPackages.has(packageName)) {
2064
+ const httpNodeModulesSpecifier = nodeModulesSpecifier || normalizedRuntimePluginSpec;
2065
+ const httpSpec = `/ns/m/node_modules/${httpNodeModulesSpecifier}`;
2066
+ if (httpOriginSafe) {
2067
+ return `${prefix}${httpOriginSafe}${httpSpec}${suffix}`;
2068
+ }
2069
+ return `${prefix}${httpSpec}${suffix}`;
1854
2070
  }
1855
- return `${prefix}${spec.replace(PAT.QUERY_PATTERN, '')}${suffix}`;
1856
2071
  }
1857
- if (isNativeScriptPluginModule(candidateNativeScriptSpec)) {
1858
- const bareSpecifier = candidateNativeScriptSpec.replace(PAT.QUERY_PATTERN, '');
1859
- return `${prefix}${bareSpecifier}${suffix}`;
2072
+ if (shouldPreserveBareRuntimePluginSubpathImport(spec, projectRoot)) {
2073
+ const httpSpec = `/ns/m/node_modules/${spec.replace(PAT.QUERY_PATTERN, '').replace(/^\/+/, '')}`;
2074
+ if (httpOriginSafe) {
2075
+ return `${prefix}${httpOriginSafe}${httpSpec}${suffix}`;
2076
+ }
2077
+ return `${prefix}${httpSpec}${suffix}`;
1860
2078
  }
2079
+ const candidateNativeScriptSpec = nodeModulesSpecifier ?? spec;
2080
+ // ── Node modules routing ──────────────────────────────────────
2081
+ // Uses the package's own package.json exports field to determine
2082
+ // whether an import is the main entry (→ vendor bridge) or a
2083
+ // subpath entry (→ HTTP). This replaces the old heuristic-based
2084
+ // approach that tried to guess from file paths.
1861
2085
  if (nodeModulesSpecifier) {
1862
- return `${prefix}${nodeModulesSpecifier}${suffix}`;
2086
+ const vendorRouting = resolveVendorRouting(nodeModulesSpecifier, projectRoot);
2087
+ if (vendorRouting) {
2088
+ if (vendorRouting.route === 'vendor') {
2089
+ return `${prefix}${vendorRouting.bareSpec}${suffix}`;
2090
+ }
2091
+ // Vendor package but subpath/platform-specific → HTTP
2092
+ const httpSpec = `/ns/m/node_modules/${nodeModulesSpecifier}`;
2093
+ if (httpOriginSafe) {
2094
+ return `${prefix}${httpOriginSafe}${httpSpec}${suffix}`;
2095
+ }
2096
+ return `${prefix}${httpSpec}${suffix}`;
2097
+ }
2098
+ // Not a vendor package → serve via HTTP from Vite dev server
2099
+ const httpSpec = `/ns/m/node_modules/${nodeModulesSpecifier}`;
2100
+ if (httpOriginSafe) {
2101
+ return `${prefix}${httpOriginSafe}${httpSpec}${suffix}`;
2102
+ }
2103
+ return `${prefix}${httpSpec}${suffix}`;
1863
2104
  }
1864
2105
  // Handle .vue imports
1865
2106
  if (PAT.VUE_FILE_PATTERN.test(spec)) {
@@ -1883,7 +2124,7 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1883
2124
  return `${prefix}${out}${suffix}`;
1884
2125
  }
1885
2126
  // Case B: plain .vue module → rewrite to SFC endpoint or local artifact
1886
- const vueKey = resolveVueKey(spec.replace(PAT.QUERY_PATTERN, ''));
2127
+ const vueKey = resolveVueKey(spec.replace(PAT.QUERY_PATTERN, '')) || '';
1887
2128
  if (vueKey) {
1888
2129
  if (true) {
1889
2130
  const absVue = vueKey.startsWith('/') ? vueKey : '/' + vueKey;
@@ -1913,9 +2154,25 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1913
2154
  // Rewrite relative application imports to HTTP for served modules
1914
2155
  if (spec.startsWith('./') || spec.startsWith('../')) {
1915
2156
  const absMaybe = normalizeImportPath(spec, importerDir);
2157
+ const nodeModulesHttpSpec = absMaybe ? toNodeModulesHttpModuleId(absMaybe) : null;
2158
+ if (nodeModulesHttpSpec) {
2159
+ if (isDynamicImportPrefix(prefix)) {
2160
+ if (verbose)
2161
+ console.log(`[rewrite][http] dynamic relative node_modules import → ${nodeModulesHttpSpec} (from ${spec})`);
2162
+ return `__nsDynamicHmrImport(${JSON.stringify(nodeModulesHttpSpec)})`;
2163
+ }
2164
+ if (verbose)
2165
+ console.log(`[rewrite][http] relative node_modules import → ${nodeModulesHttpSpec} (from ${spec})`);
2166
+ return `${prefix}${nodeModulesHttpSpec}${suffix}`;
2167
+ }
1916
2168
  const baseId = absMaybe ? toAppModuleBaseId(absMaybe, projectRoot) : null; // e.g. /src/foo.mjs
1917
2169
  if (baseId) {
1918
2170
  const httpSpec = `/ns/m${baseId}`;
2171
+ if (isDynamicImportPrefix(prefix)) {
2172
+ if (verbose)
2173
+ console.log(`[rewrite][http] dynamic relative app import → ${httpSpec} (from ${spec})`);
2174
+ return `__nsDynamicHmrImport(${JSON.stringify(httpSpec)})`;
2175
+ }
1919
2176
  if (verbose)
1920
2177
  console.log(`[rewrite][http] relative app import → ${httpSpec} (from ${spec})`);
1921
2178
  return `${prefix}${httpSpec}${suffix}`;
@@ -1928,6 +2185,11 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1928
2185
  const baseId = toAppModuleBaseId(spec, projectRoot);
1929
2186
  if (baseId) {
1930
2187
  const httpSpec = `/ns/m${baseId}`;
2188
+ if (isDynamicImportPrefix(prefix)) {
2189
+ if (verbose)
2190
+ console.log(`[rewrite][http] dynamic app import → ${httpSpec} (from ${spec})`);
2191
+ return `__nsDynamicHmrImport(${JSON.stringify(httpSpec)})`;
2192
+ }
1931
2193
  if (verbose)
1932
2194
  console.log(`[rewrite][http] absolute app import → ${httpSpec} (from ${spec})`);
1933
2195
  return `${prefix}${httpSpec}${suffix}`;
@@ -1951,6 +2213,27 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1951
2213
  return `${prefix}./${depFile}${suffix}`;
1952
2214
  }
1953
2215
  }
2216
+ // Bare npm package specifier fallback — route to /ns/m/node_modules/.
2217
+ // This catches specifiers like `source-map-js/lib/source-map-generator.js`
2218
+ // emitted by helpers such as the CommonJS compat transform, which Vite
2219
+ // would normally resolve to an absolute path but which pass through the
2220
+ // rewriter as bare strings here. Under HMR (core external) bundle.mjs
2221
+ // depends on these resolving over HTTP rather than via a filesystem
2222
+ // bare-specifier lookup, which iOS can't satisfy and which crashes with
2223
+ // "Module not found".
2224
+ if (spec && !spec.startsWith('/') && !spec.startsWith('./') && !spec.startsWith('../') && !/^https?:\/\//i.test(spec) && !spec.startsWith('ns-vendor:') && !spec.startsWith('@nativescript/core')) {
2225
+ // Only treat as a package spec if it looks like one — disallow
2226
+ // plain identifiers like `moment` unresolved (those are left alone
2227
+ // for existing vendor-routing paths to handle).
2228
+ const bareNpmRe = /^(?:@[A-Za-z0-9][\w.-]*\/)?[A-Za-z0-9][\w.-]*(?:\/[\w.\-/]+)?$/;
2229
+ if (bareNpmRe.test(spec)) {
2230
+ const httpSpec = `/ns/m/node_modules/${spec}`;
2231
+ if (httpOriginSafe) {
2232
+ return `${prefix}${httpOriginSafe}${httpSpec}${suffix}`;
2233
+ }
2234
+ return `${prefix}${httpSpec}${suffix}`;
2235
+ }
2236
+ }
1954
2237
  // Leave everything else unchanged (vendor imports, etc.)
1955
2238
  return `${prefix}${spec}${suffix}`;
1956
2239
  };
@@ -1959,16 +2242,17 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1959
2242
  result = result.replace(PAT.IMPORT_PATTERN_2, replaceVueImport);
1960
2243
  result = result.replace(PAT.EXPORT_PATTERN, replaceVueImport);
1961
2244
  result = result.replace(PAT.IMPORT_PATTERN_3, replaceVueImport);
2245
+ // Side-effect imports (import "spec") — must run AFTER named-import patterns
2246
+ // since IMPORT_PATTERN_1 already handles `import ... from "spec"`.
2247
+ result = result.replace(PAT.IMPORT_PATTERN_SIDE_EFFECT, replaceVueImport);
2248
+ result = ensureDynamicHmrImportHelper(result);
1962
2249
  // Extra guard: map any lingering dynamic import('@') to a safe stub module path
1963
2250
  // to prevent device runtime normalization errors.
1964
2251
  // Example matched: import('@') or import("@") with optional whitespace before closing paren
1965
2252
  result = result.replace(/(import\(\s*['"])@(['"]\s*\))/g, (_m) => {
1966
2253
  const stubExpr = `import(new URL('/ns/m/__invalid_at__.mjs', import.meta.url).href)`;
1967
2254
  if (verbose) {
1968
- try {
1969
- console.warn(`[rewrite] mapped dynamic import('@') to /ns/m/__invalid_at__.mjs via import.meta.url`);
1970
- }
1971
- catch { }
2255
+ console.warn(`[rewrite] mapped dynamic import('@') to /ns/m/__invalid_at__.mjs via import.meta.url`);
1972
2256
  }
1973
2257
  return stubExpr;
1974
2258
  });
@@ -1977,10 +2261,7 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1977
2261
  result = result.replace(/(from\s*['"])@(['"])/g, (_m, p1, p2) => {
1978
2262
  const stub = `/ns/m/__invalid_at__.mjs`;
1979
2263
  if (verbose) {
1980
- try {
1981
- console.warn(`[rewrite] mapped static from '@' to ${stub}`);
1982
- }
1983
- catch { }
2264
+ console.warn(`[rewrite] mapped static from '@' to ${stub}`);
1984
2265
  }
1985
2266
  return `${p1}${stub}${p2}`;
1986
2267
  });
@@ -1988,10 +2269,7 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1988
2269
  result = result.replace(/(import\s*(?!\()\s*['"])@(['"])/g, (_m, p1, p2) => {
1989
2270
  const stub = `/ns/m/__invalid_at__.mjs`;
1990
2271
  if (verbose) {
1991
- try {
1992
- console.warn(`[rewrite] mapped side-effect import '@' to ${stub}`);
1993
- }
1994
- catch { }
2272
+ console.warn(`[rewrite] mapped side-effect import '@' to ${stub}`);
1995
2273
  }
1996
2274
  return `${p1}${stub}${p2}`;
1997
2275
  });
@@ -1999,6 +2277,11 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1999
2277
  // In HTTP mode, skip legacy local-path rewrite to avoid mixing module origins
2000
2278
  result = result.replace(PAT.VUE_FILE_IMPORT, (_m, p1, spec, p3) => {
2001
2279
  if (httpOrigin) {
2280
+ if (isNsSfcSpecifier(spec)) {
2281
+ if (verbose)
2282
+ console.log(`[rewrite] .vue already routed (VUE_FILE_IMPORT http): ${spec}`);
2283
+ return `${p1}${spec}${p3}`;
2284
+ }
2002
2285
  // Route via /ns/sfc with full query preserved
2003
2286
  try {
2004
2287
  let base = spec;
@@ -2048,6 +2331,13 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
2048
2331
  console.log(`[rewrite][http] internal ns import (dynamic) → ${spec} via import.meta.url`);
2049
2332
  return expr;
2050
2333
  }
2334
+ const nodeModulesHttpSpec = toNodeModulesHttpModuleId(spec);
2335
+ if (nodeModulesHttpSpec) {
2336
+ const expr = `import(new URL('${nodeModulesHttpSpec}', import.meta.url).href)`;
2337
+ if (verbose)
2338
+ console.log(`[rewrite][http] absolute dynamic node_modules import → ${nodeModulesHttpSpec} via import.meta.url (from ${spec})`);
2339
+ return expr;
2340
+ }
2051
2341
  const baseId = toAppModuleBaseId(spec, projectRoot);
2052
2342
  if (!baseId)
2053
2343
  return match;
@@ -2064,6 +2354,8 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
2064
2354
  function createHmrWebSocketPlugin(opts) {
2065
2355
  const verbose = !!opts.verbose;
2066
2356
  let wss = null;
2357
+ let sharedTransformRequest;
2358
+ const pendingAngularReloadSuppressions = new Map();
2067
2359
  const sfcFileMap = new Map();
2068
2360
  const depFileMap = new Map();
2069
2361
  // Generic module manifest (spec -> emitted relative .mjs path)
@@ -2074,14 +2366,42 @@ function createHmrWebSocketPlugin(opts) {
2074
2366
  let registrySent = false;
2075
2367
  let vendorBootstrapDone = false;
2076
2368
  let pluginRoot;
2077
- let graphVersion = 0;
2369
+ // graphVersion starts at 1 so the very first /ns/m response uses a stable
2370
+ // `v1` URL tag (see dynamic-import helper at lines 398-432). Keeping it
2371
+ // stable during cold boot prevents double-loads when the graph fills up
2372
+ // lazily as modules are served.
2373
+ let graphVersion = 1;
2078
2374
  // Transactional HMR batches: map graphVersion -> ordered list of changed ids for that version
2079
2375
  const txnBatches = new Map();
2080
2376
  const graph = new Map();
2377
+ // Tracks the background initial-graph population so handleHotUpdate can
2378
+ // await completion before computing delta roots for the first HMR event.
2379
+ let graphInitialPopulationPromise = null;
2380
+ // Cold-boot /ns/m request counter — populated the first time a /ns/m
2381
+ // request arrives, finalized when the request window goes idle.
2382
+ // See Shared across requests so a single counter spans the whole cold boot.
2383
+ let coldBootCounter = null;
2384
+ function rememberAngularReloadSuppression(root, file, ttlMs = 3000) {
2385
+ const absPath = normalizeHotReloadMatchPath(file);
2386
+ const relPath = normalizeHotReloadMatchPath(file, root);
2387
+ pendingAngularReloadSuppressions.set(absPath, {
2388
+ absPath,
2389
+ relPath,
2390
+ expiresAt: Date.now() + ttlMs,
2391
+ });
2392
+ }
2393
+ function pruneAngularReloadSuppressions(now = Date.now()) {
2394
+ for (const [key, entry] of pendingAngularReloadSuppressions) {
2395
+ if (!entry || entry.expiresAt <= now) {
2396
+ pendingAngularReloadSuppressions.delete(key);
2397
+ }
2398
+ }
2399
+ }
2081
2400
  // Compute a dependency-closed, topologically sorted list of modules for a given set of changed ids.
2082
2401
  // Only include application modules we can serve (e.g., under /src and known .vue/.ts/.js entries in the graph).
2083
2402
  function computeTxnOrderForChanged(changedIds) {
2084
- const includeExt = (id) => ACTIVE_STRATEGY.matchesFile(id) || /\.(ts|js|mjs|tsx|jsx)$/i.test(id);
2403
+ const includeAppModule = (id) => matchesRuntimeGraphModuleId(id, APP_VIRTUAL_WITH_SLASH, /\.(ts|js|mjs|tsx|jsx)$/i);
2404
+ const includeExt = (id) => ACTIVE_STRATEGY.matchesFile(id) || includeAppModule(id);
2085
2405
  const isApp = (id) => id.startsWith(APP_VIRTUAL_WITH_SLASH);
2086
2406
  const roots = changedIds.map(normalizeGraphId).filter((id) => graph.has(id) && (isApp(id) || ACTIVE_STRATEGY.matchesFile(id)) && includeExt(id));
2087
2407
  const toVisit = new Set();
@@ -2220,7 +2540,7 @@ function createHmrWebSocketPlugin(opts) {
2220
2540
  catch { }
2221
2541
  });
2222
2542
  }
2223
- function upsertGraphModule(rawId, code, deps) {
2543
+ function upsertGraphModule(rawId, code, deps, options) {
2224
2544
  const id = normalizeGraphId(rawId);
2225
2545
  const normDeps = deps
2226
2546
  .map((d) => normalizeGraphId(d))
@@ -2229,19 +2549,25 @@ function createHmrWebSocketPlugin(opts) {
2229
2549
  .sort();
2230
2550
  const hash = computeHash(code);
2231
2551
  const existing = graph.get(id);
2232
- if (existing && existing.hash === hash && existing.deps.length === normDeps.length && existing.deps.every((d, i) => d === normDeps[i]))
2233
- return; // unchanged
2234
- graphVersion++;
2552
+ const classification = classifyGraphUpsert(existing, hash, normDeps);
2553
+ if (classification === 'unchanged')
2554
+ return existing;
2555
+ // Version bumps are only meaningful for live edits — serve-time graph
2556
+ // warm-ups and the initial bulk walk should leave graphVersion stable.
2557
+ const bumpVersion = shouldBumpGraphVersion(classification, options?.bumpVersion !== false);
2558
+ if (bumpVersion) {
2559
+ graphVersion++;
2560
+ }
2235
2561
  const gm = { id, deps: normDeps, hash };
2236
2562
  graph.set(id, gm);
2237
2563
  if (verbose) {
2238
- try {
2239
- console.log('[hmr-ws][graph] upsert', { id, deps: normDeps, hash, graphVersion });
2240
- console.log('[hmr-ws][graph] size', graph.size);
2241
- }
2242
- catch { }
2564
+ console.log('[hmr-ws][graph] upsert', { id, deps: normDeps, hash, graphVersion, classification, bumpVersion });
2565
+ console.log('[hmr-ws][graph] size', graph.size);
2566
+ }
2567
+ if (shouldBroadcastGraphUpsertDelta(classification, options?.emitDeltaOnInsert === true, options?.broadcastDelta !== false)) {
2568
+ emitDelta([gm], []);
2243
2569
  }
2244
- emitDelta([gm], []);
2570
+ return gm;
2245
2571
  }
2246
2572
  function isTypescriptFlavor() {
2247
2573
  try {
@@ -2261,6 +2587,8 @@ function createHmrWebSocketPlugin(opts) {
2261
2587
  async function populateInitialGraph(server) {
2262
2588
  if (graph.size)
2263
2589
  return; // already populated
2590
+ const tStart = Date.now();
2591
+ const versionAtStart = graphVersion;
2264
2592
  const root = server.config.root || process.cwd();
2265
2593
  // Avoid direct require in ESM build: lazily obtain fs & path via createRequire or dynamic import
2266
2594
  let fs;
@@ -2276,21 +2604,33 @@ function createHmrWebSocketPlugin(opts) {
2276
2604
  fs = await import('fs');
2277
2605
  pathMod = await import('path');
2278
2606
  }
2607
+ // Route every bulk transform through `sharedTransformRequest` when it's
2608
+ // already been wired up — this way the background walk shares the 60s
2609
+ // TTL cache with live /ns/m requests, so the device sees cached results
2610
+ // for any file the walker already visited. The fallback keeps the
2611
+ // walker working during server tests where the shared runner isn't
2612
+ // constructed yet.
2613
+ const bulkTransform = (rel) => {
2614
+ if (sharedTransformRequest) {
2615
+ return sharedTransformRequest(rel);
2616
+ }
2617
+ return server.transformRequest(rel);
2618
+ };
2279
2619
  async function walk(dir) {
2280
2620
  for (const name of fs.readdirSync(dir)) {
2281
- const full = pathMod.join(dir, name);
2282
- if (name === 'node_modules' || name.startsWith('.'))
2621
+ if (name === 'node_modules' || name.startsWith('.') || shouldSkipRuntimeGraphDirectoryName(name))
2283
2622
  continue;
2623
+ const full = pathMod.join(dir, name);
2284
2624
  try {
2285
2625
  const stat = fs.statSync(full);
2286
2626
  if (stat.isDirectory())
2287
2627
  await walk(full);
2288
2628
  else if (stat.isFile()) {
2289
- if (/\.(vue|ts|js|mjs|tsx|jsx)$/.test(name)) {
2629
+ if (shouldIncludeRuntimeGraphFile(full, /\.(vue|ts|js|mjs|tsx|jsx)$/i)) {
2290
2630
  const rel = '/' + pathMod.relative(root, full).split(pathMod.sep).join('/');
2291
2631
  // Transform via Vite to gather deps (ignore failures)
2292
2632
  try {
2293
- const transformed = await server.transformRequest(rel);
2633
+ const transformed = await bulkTransform(rel);
2294
2634
  const code = transformed?.code || '';
2295
2635
  const deps = [];
2296
2636
  // fallback to import relationships via moduleGraph
@@ -2301,7 +2641,10 @@ function createHmrWebSocketPlugin(opts) {
2301
2641
  deps.push(m.id.split('?')[0]);
2302
2642
  }
2303
2643
  }
2304
- upsertGraphModule(rel, code, deps);
2644
+ // bumpVersion: false — the initial walk is a bulk load, not a live
2645
+ // edit. Keeping graphVersion stable during cold boot avoids double
2646
+ // cache-key drift.
2647
+ upsertGraphModule(rel, code, deps, { bumpVersion: false });
2305
2648
  }
2306
2649
  catch { }
2307
2650
  }
@@ -2314,6 +2657,37 @@ function createHmrWebSocketPlugin(opts) {
2314
2657
  await walk(pathMod.join(root, 'src'));
2315
2658
  }
2316
2659
  catch { }
2660
+ // Diagnostic summary. Gated behind the verbose flag so the
2661
+ // dev console stays quiet on a normal save. Flip
2662
+ // NS_VITE_VERBOSE=1 to surface slow cold-boot walks; a
2663
+ // `bumpedVersion=no` result is the happy path, `yes`
2664
+ // indicates a regression.
2665
+ if (verbose) {
2666
+ console.info(formatPopulateInitialGraphSummary({
2667
+ moduleCount: graph.size,
2668
+ durationMs: Date.now() - tStart,
2669
+ graphVersion,
2670
+ bumpedVersion: graphVersion !== versionAtStart,
2671
+ }));
2672
+ }
2673
+ }
2674
+ // Kick off `populateInitialGraph` in the background (non-awaited) so /ns/m
2675
+ // responses are never blocked on a full tree walk. Returns the shared
2676
+ // promise so hot-update code paths can await completion before computing
2677
+ // delta roots for the first HMR event.
2678
+ function ensureInitialGraphPopulationStarted(server) {
2679
+ if (graphInitialPopulationPromise) {
2680
+ return graphInitialPopulationPromise;
2681
+ }
2682
+ if (graph.size) {
2683
+ graphInitialPopulationPromise = Promise.resolve();
2684
+ return graphInitialPopulationPromise;
2685
+ }
2686
+ graphInitialPopulationPromise = populateInitialGraph(server).catch((error) => {
2687
+ if (verbose)
2688
+ console.warn('[hmr-ws][graph] background initial population failed', error);
2689
+ });
2690
+ return graphInitialPopulationPromise;
2317
2691
  }
2318
2692
  return {
2319
2693
  name: 'nativescript-hmr-websocket',
@@ -2323,6 +2697,163 @@ function createHmrWebSocketPlugin(opts) {
2323
2697
  const httpServer = server.httpServer;
2324
2698
  if (!httpServer)
2325
2699
  return;
2700
+ const wsAny = server.ws;
2701
+ if (!wsAny.__NS_ANGULAR_FULL_RELOAD_FILTER_INSTALLED__) {
2702
+ const originalSend = server.ws.send.bind(server.ws);
2703
+ wsAny.__NS_ANGULAR_FULL_RELOAD_FILTER_INSTALLED__ = true;
2704
+ server.ws.send = ((payload, ...rest) => {
2705
+ pruneAngularReloadSuppressions();
2706
+ if (shouldSuppressViteFullReloadPayload({
2707
+ payload,
2708
+ pendingEntries: pendingAngularReloadSuppressions.values(),
2709
+ root: pluginRoot,
2710
+ })) {
2711
+ if (verbose) {
2712
+ console.log('[hmr-ws][angular] suppressed vite full-reload payload', payload);
2713
+ }
2714
+ return;
2715
+ }
2716
+ return originalSend(payload, ...rest);
2717
+ });
2718
+ }
2719
+ // Transform concurrency. Historically we defaulted to 1 to avoid
2720
+ // race conditions during HTTP HMR startup, but the shared runner
2721
+ // already has per-URL coalescing and an async-cached result map,
2722
+ // so higher fan-out is safe and dramatically reduces cold-boot
2723
+ // time. We cap at 8 by default to match typical dev machines and
2724
+ // respect Vite's internal worker pool limits. Override via the
2725
+ // `NS_VITE_HMR_TRANSFORM_CONCURRENCY` env var when needed.
2726
+ const configuredTransformConcurrency = Number.parseInt(process.env.NS_VITE_HMR_TRANSFORM_CONCURRENCY || '', 10);
2727
+ const transformConcurrency = Number.isFinite(configuredTransformConcurrency) && configuredTransformConcurrency > 0 ? configuredTransformConcurrency : 8;
2728
+ // Keep transformed code cached for longer across HMR updates so
2729
+ // that unchanged neighbours of an edited file don't re-run
2730
+ // through the Angular/TypeScript/Vite transform pipeline. The
2731
+ // HMR flow explicitly invalidates affected URLs, so a longer TTL
2732
+ // is safe. Override with `NS_VITE_HMR_TRANSFORM_CACHE_MS`.
2733
+ const configuredTransformCacheMs = Number.parseInt(process.env.NS_VITE_HMR_TRANSFORM_CACHE_MS || '', 10);
2734
+ const transformCacheMs = Number.isFinite(configuredTransformCacheMs) && configuredTransformCacheMs >= 0 ? configuredTransformCacheMs : 60000;
2735
+ sharedTransformRequest = createSharedTransformRequestRunner((url) => server.transformRequest(url), (url, timeoutMs) => {
2736
+ console.warn('[ns:m] slow transformRequest for', url, '(>' + timeoutMs + 'ms)');
2737
+ }, {
2738
+ maxConcurrent: transformConcurrency,
2739
+ resultCacheTtlMs: transformCacheMs,
2740
+ getResultCacheKey: (url) => canonicalizeTransformRequestCacheKey(url, pluginRoot || process.cwd()),
2741
+ });
2742
+ // Always-on startup banner — prints once per dev server process
2743
+ // so anyone investigating perf can immediately see which build
2744
+ // is live and what knobs are active.
2745
+ try {
2746
+ let pkgVersion = 'unknown';
2747
+ try {
2748
+ const req = createRequire(import.meta.url);
2749
+ const pkg = req('@nativescript/vite/package.json');
2750
+ if (pkg && typeof pkg.version === 'string')
2751
+ pkgVersion = pkg.version;
2752
+ }
2753
+ catch {
2754
+ // `@nativescript/vite/package.json` is not always exported; fall
2755
+ // back to reading the file from disk next to this module.
2756
+ try {
2757
+ const here = new URL(import.meta.url).pathname;
2758
+ const pkgPath = path.resolve(path.dirname(here), '..', '..', 'package.json');
2759
+ if (existsSync(pkgPath)) {
2760
+ const parsed = JSON.parse(readFileSync(pkgPath, 'utf-8'));
2761
+ if (parsed && typeof parsed.version === 'string')
2762
+ pkgVersion = parsed.version;
2763
+ }
2764
+ }
2765
+ catch { }
2766
+ }
2767
+ if (verbose) {
2768
+ console.info(formatServerStartupBanner({
2769
+ version: pkgVersion,
2770
+ transformConcurrency,
2771
+ transformCacheMs,
2772
+ lazyInitialGraph: true,
2773
+ graphVersion,
2774
+ }));
2775
+ }
2776
+ }
2777
+ catch { }
2778
+ // Always-on cold-boot request trace. Runs in front of every
2779
+ // other middleware so it catches all NS dev routes (/ns/m/*,
2780
+ // /ns/rt/*, /ns/core/*, /__ns_boot__/*, etc.) with a single
2781
+ // hook. Closes itself after an idle window so HMR edits don't
2782
+ // get rolled into the cold-boot numbers. The idle window is
2783
+ // generous by default (5s) because V8's HTTP ESM resolver
2784
+ // pauses between dep levels while parsing — a too-tight window
2785
+ // was closing after the first wave and under-reporting boot by
2786
+ // 100x. Override via `NS_VITE_HMR_BOOT_TRACE_IDLE_MS` when
2787
+ // profiling something tricky.
2788
+ try {
2789
+ const configuredIdleMs = Number.parseInt(process.env.NS_VITE_HMR_BOOT_TRACE_IDLE_MS || '', 10);
2790
+ const idleWindowMs = Number.isFinite(configuredIdleMs) && configuredIdleMs > 0 ? configuredIdleMs : 5000;
2791
+ const configuredSummaryEvery = Number.parseInt(process.env.NS_VITE_HMR_BOOT_TRACE_PROGRESS_EVERY || '', 10);
2792
+ const summaryEvery = Number.isFinite(configuredSummaryEvery) && configuredSummaryEvery >= 0 ? configuredSummaryEvery : 25;
2793
+ if (!coldBootCounter) {
2794
+ coldBootCounter = createColdBootRequestCounter({
2795
+ summaryEvery,
2796
+ idleWindowMs,
2797
+ // Gated on the verbose flag so cold-boot progress and
2798
+ // the final window-closed summary stay quiet by
2799
+ // default. Flip NS_VITE_VERBOSE=1 to surface them.
2800
+ log: (line) => {
2801
+ if (!verbose)
2802
+ return;
2803
+ console.info(line);
2804
+ },
2805
+ });
2806
+ }
2807
+ }
2808
+ catch { }
2809
+ server.middlewares.use((req, res, next) => {
2810
+ try {
2811
+ const urlObj = new URL(req.url || '', 'http://localhost');
2812
+ const route = classifyBootRoute(urlObj.pathname);
2813
+ if (route === 'other')
2814
+ return next();
2815
+ if (!coldBootCounter)
2816
+ return next();
2817
+ const handle = coldBootCounter.record(urlObj.pathname);
2818
+ const finishOnce = () => {
2819
+ try {
2820
+ handle.finish();
2821
+ }
2822
+ catch { }
2823
+ };
2824
+ try {
2825
+ res.once('finish', finishOnce);
2826
+ res.once('close', finishOnce);
2827
+ }
2828
+ catch { }
2829
+ }
2830
+ catch { }
2831
+ next();
2832
+ });
2833
+ // Give `populateInitialGraph` a head start. Previously this only
2834
+ // kicked off on the first /ns/m hit, which meant populate was
2835
+ // competing with the device for the same 8 transform slots
2836
+ // throughout the first 4-5 seconds of cold boot. Starting at
2837
+ // `configureServer` time gives populate the full app
2838
+ // build/launch window (typically 2-3s on simulator) as a head
2839
+ // start, so more of its work lands before the device even
2840
+ // connects. Disable via `NS_VITE_HMR_DISABLE_POPULATE=1` when
2841
+ // profiling whether populate is helping or hurting a specific
2842
+ // app.
2843
+ try {
2844
+ const disablePopulate = process.env.NS_VITE_HMR_DISABLE_POPULATE === '1' || process.env.NS_VITE_HMR_DISABLE_POPULATE === 'true';
2845
+ if (disablePopulate) {
2846
+ if (verbose)
2847
+ console.info('[hmr-ws][populate] disabled via NS_VITE_HMR_DISABLE_POPULATE');
2848
+ // Short-circuit: mark as resolved so /ns/m never schedules it and
2849
+ // HMR still works (handleHotUpdate just has no pre-warmed graph).
2850
+ graphInitialPopulationPromise = Promise.resolve();
2851
+ }
2852
+ else {
2853
+ ensureInitialGraphPopulationStarted(server);
2854
+ }
2855
+ }
2856
+ catch { }
2326
2857
  // Attempt early vendor manifest bootstrap once per server.
2327
2858
  if (!vendorBootstrapDone) {
2328
2859
  vendorBootstrapDone = true;
@@ -2366,20 +2897,63 @@ function createHmrWebSocketPlugin(opts) {
2366
2897
  });
2367
2898
  // Additional connection diagnostics
2368
2899
  wss.on('connection', (ws, req) => {
2900
+ const role = getHmrSocketRoleFromRequestUrl(req.url);
2901
+ ws.__nsHmrClientRole = role;
2369
2902
  try {
2370
2903
  if (verbose) {
2371
2904
  const ra = req.socket?.remoteAddress;
2372
2905
  const rp = req.socket?.remotePort;
2373
- console.log('[hmr-ws] Client connected', ra + (rp ? ':' + rp : ''));
2906
+ console.log('[hmr-ws] Client connected', { role, remote: ra + (rp ? ':' + rp : '') });
2374
2907
  }
2375
2908
  }
2376
2909
  catch { }
2910
+ ws.on('close', () => {
2911
+ try {
2912
+ if (verbose) {
2913
+ const ra = req.socket?.remoteAddress;
2914
+ const rp = req.socket?.remotePort;
2915
+ console.log('[hmr-ws] Client disconnected', { role, remote: ra + (rp ? ':' + rp : '') });
2916
+ }
2917
+ }
2918
+ catch { }
2919
+ });
2377
2920
  });
2378
2921
  wss.on('error', (err) => {
2922
+ console.warn('[hmr-ws] server error:', err?.message || String(err));
2923
+ });
2924
+ // Import map endpoint: GET /ns/import-map.json
2925
+ // Returns the import map + runtime config for __nsConfigureRuntime()
2926
+ server.middlewares.use(async (req, res, next) => {
2379
2927
  try {
2380
- console.warn('[hmr-ws] server error:', err?.message || String(err));
2928
+ const urlObj = new URL(req.url || '', 'http://localhost');
2929
+ if (urlObj.pathname !== '/ns/import-map.json')
2930
+ return next();
2931
+ res.setHeader('Access-Control-Allow-Origin', '*');
2932
+ res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
2933
+ if (req.method === 'OPTIONS') {
2934
+ res.statusCode = 204;
2935
+ res.end();
2936
+ return;
2937
+ }
2938
+ // Determine origin from request headers or server config
2939
+ const host = req.headers.host || 'localhost:5173';
2940
+ const protocol = 'http';
2941
+ const origin = `${protocol}://${host}`;
2942
+ const runtimeConfig = buildRuntimeConfig({
2943
+ origin,
2944
+ flavor: ACTIVE_STRATEGY?.flavor || 'typescript',
2945
+ });
2946
+ res.setHeader('Content-Type', 'application/json');
2947
+ res.end(JSON.stringify({
2948
+ importMap: JSON.parse(runtimeConfig.importMap),
2949
+ volatilePatterns: runtimeConfig.volatilePatterns,
2950
+ }, null, 2));
2951
+ }
2952
+ catch (err) {
2953
+ console.error('[import-map] error generating import map:', err?.message || err);
2954
+ res.statusCode = 500;
2955
+ res.end(JSON.stringify({ error: 'Failed to generate import map' }));
2381
2956
  }
2382
- catch { }
2383
2957
  });
2384
2958
  // Dev-only HTTP ESM loader endpoint for device clients
2385
2959
  // 1) Legacy JSON module endpoint (kept temporarily): GET /ns-module?path=/abs -> { path, code, additionalFiles }
@@ -2415,13 +2989,16 @@ function createHmrWebSocketPlugin(opts) {
2415
2989
  // Transform via Vite with variant resolution (same as ws ns:fetch-module)
2416
2990
  const hasExt = /\.(ts|tsx|js|jsx|mjs|mts|cts|vue)$/i.test(spec);
2417
2991
  const baseNoExt = hasExt ? spec.replace(/\.(ts|tsx|js|jsx|mjs|mts|cts)$/i, '') : spec;
2992
+ const transformRoot = server.config?.root || process.cwd();
2993
+ const transformWorkspaceRoot = getMonorepoWorkspaceRoot(transformRoot);
2418
2994
  const candidates = [];
2419
2995
  if (hasExt)
2420
2996
  candidates.push(spec);
2421
2997
  candidates.push(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');
2998
+ const transformCandidates = filterExistingNodeModulesTransformCandidates(spec, candidates, transformRoot, transformWorkspaceRoot);
2422
2999
  let transformed = null;
2423
3000
  let resolvedCandidate = null;
2424
- for (const cand of candidates) {
3001
+ for (const cand of transformCandidates) {
2425
3002
  try {
2426
3003
  const r = await server.transformRequest(cand);
2427
3004
  if (r?.code) {
@@ -2443,7 +3020,10 @@ function createHmrWebSocketPlugin(opts) {
2443
3020
  code = REQUIRE_GUARD_SNIPPET + code;
2444
3021
  // Apply same sanitation/rewrite pipeline used for WS path
2445
3022
  code = cleanCode(code);
2446
- code = processCodeForDevice(code, false);
3023
+ // preserveVendorImports=true: vendor imports stay as bare specifiers
3024
+ // for the device-side import map (ns-vendor://) instead of being
3025
+ // transformed to __nsVendorRequire calls with fragile __nsPick lookups.
3026
+ code = processCodeForDevice(code, false, true, /(?:^|\/)node_modules\//.test(resolvedCandidate || spec), resolvedCandidate || spec);
2447
3027
  code = rewriteImports(code, spec, sfcFileMap, depFileMap, server.config?.root || process.cwd(), !!verbose, undefined, getServerOrigin(server));
2448
3028
  code = ensureVariableDynamicImportHelper(code);
2449
3029
  // Enforce upstream guarantee: no optimized deps or virtual ids remain
@@ -2455,18 +3035,6 @@ function createHmrWebSocketPlugin(opts) {
2455
3035
  res.setHeader('Content-Type', 'application/json');
2456
3036
  return void res.end(JSON.stringify({ error: e?.message || String(e) }));
2457
3037
  }
2458
- // Optional diagnostics: when ?diag=1, inject simple entry/exit logs to help isolate
2459
- // execution-time failures on device without changing semantics.
2460
- try {
2461
- const wantDiag = urlObj.searchParams.get('diag') === '1';
2462
- if (wantDiag) {
2463
- const importerPath = spec.replace(/[?#].*$/, '');
2464
- const enter = `try { console.log('[sfc][enter]', ${JSON.stringify(importerPath)}, 'hasReq=', (typeof globalThis.__nsRequire==='function'||typeof globalThis.require==='function')); } catch {}`;
2465
- const exit = `\n;try { console.log('[sfc][loaded]', ${JSON.stringify(importerPath)}); } catch {}`;
2466
- code = `${enter}\n${code}${exit}`;
2467
- }
2468
- }
2469
- catch { }
2470
3038
  try {
2471
3039
  const origin = getServerOrigin(server);
2472
3040
  code = ensureVersionedRtImports(code, origin, graphVersion);
@@ -2499,7 +3067,7 @@ function createHmrWebSocketPlugin(opts) {
2499
3067
  if (seen.has(depBase))
2500
3068
  continue;
2501
3069
  seen.add(depBase);
2502
- const depCandidates = [depBase + '.ts', depBase + '.js', depBase + '.tsx', depBase + '.jsx', depBase + '.mjs', depBase + '.mts', depBase + '.cts', depBase + '.vue', depBase + '/index.ts', depBase + '/index.js', depBase + '/index.tsx', depBase + '/index.jsx', depBase + '/index.mjs'];
3070
+ const depCandidates = filterExistingNodeModulesTransformCandidates(depBase, [depBase + '.ts', depBase + '.js', depBase + '.tsx', depBase + '.jsx', depBase + '.mjs', depBase + '.mts', depBase + '.cts', depBase + '.vue', depBase + '/index.ts', depBase + '/index.js', depBase + '/index.tsx', depBase + '/index.jsx', depBase + '/index.mjs'], transformRoot, transformWorkspaceRoot);
2503
3071
  let depTrans = null;
2504
3072
  let depResolved = null;
2505
3073
  for (const c of depCandidates) {
@@ -2516,7 +3084,7 @@ function createHmrWebSocketPlugin(opts) {
2516
3084
  if (depTrans?.code && depResolved) {
2517
3085
  let depCode = depTrans.code;
2518
3086
  depCode = cleanCode(depCode);
2519
- depCode = processCodeForDevice(depCode, false);
3087
+ depCode = processCodeForDevice(depCode, false, true, /(?:^|\/)node_modules\//.test(depResolved), depResolved);
2520
3088
  depCode = rewriteImports(depCode, depResolved, sfcFileMap, depFileMap, server.config?.root || process.cwd(), !!verbose, undefined, getServerOrigin(server));
2521
3089
  depCode = ensureVariableDynamicImportHelper(depCode);
2522
3090
  try {
@@ -2558,6 +3126,23 @@ function createHmrWebSocketPlugin(opts) {
2558
3126
  const urlObj = new URL(req.url || '', 'http://localhost');
2559
3127
  if (!urlObj.pathname.startsWith('/ns/m'))
2560
3128
  return next();
3129
+ // Previously we awaited `populateInitialGraph(server)` here so
3130
+ // graphVersion would be non-zero for the first /ns/m request.
3131
+ // That gave deterministic URL tags but blocked the cold boot on a
3132
+ // full src/ tree walk (hundreds of transformRequest calls, 3-6s).
3133
+ //
3134
+ // graphVersion now starts at 1 and stays stable during cold boot
3135
+ // (see `upsertGraphModule`'s bumpVersion option and the inline
3136
+ // comment at the graphVersion declaration). We kick off the
3137
+ // initial population in the background so it doesn't block the
3138
+ // first response. `handleHotUpdate` awaits the same promise so
3139
+ // the first HMR event still sees a fully populated graph.
3140
+ ensureInitialGraphPopulationStarted(server);
3141
+ // Cold-boot counter is now hooked via the leading boot-trace
3142
+ // middleware (see `configureServer` — it records the request
3143
+ // and tracks finish() via res.on('close'/'finish')). This
3144
+ // handler used to record here but that missed the
3145
+ // round-trip timing and didn't track per-route breakdowns.
2561
3146
  res.setHeader('Access-Control-Allow-Origin', '*');
2562
3147
  res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
2563
3148
  // Disable caching for dev ESM endpoints to avoid device-side stale module reuse
@@ -2567,7 +3152,8 @@ function createHmrWebSocketPlugin(opts) {
2567
3152
  // Support both query (?path=/abs) and path-style (/ns/m/abs)
2568
3153
  let spec = urlObj.searchParams.get('path') || '';
2569
3154
  // Optional graph version pin for deterministic boot
2570
- const forcedVer = urlObj.searchParams.get('v');
3155
+ let forcedVer = urlObj.searchParams.get('v');
3156
+ let bootTaggedRequest = false;
2571
3157
  if (!spec) {
2572
3158
  const base = '/ns/m';
2573
3159
  let rest = urlObj.pathname.slice(base.length);
@@ -2585,22 +3171,26 @@ function createHmrWebSocketPlugin(opts) {
2585
3171
  res.end('export {}\n');
2586
3172
  return;
2587
3173
  }
3174
+ const serverRoot = (server.config?.root || process.cwd());
3175
+ const monorepoWorkspaceRoot = getMonorepoWorkspaceRoot(serverRoot);
2588
3176
  spec = spec.replace(/[?#].*$/, '');
2589
- // Accept path-based HMR cache-busting: /ns/m/__ns_hmr__/<tag>/<real-spec>
3177
+ // Accept path-based boot/HMR prefixes:
3178
+ // /ns/m/__ns_boot__/b1/<real-spec>
3179
+ // /ns/m/__ns_hmr__/<tag>/<real-spec>
3180
+ // /ns/m/__ns_boot__/b1/__ns_hmr__/<tag>/<real-spec>
2590
3181
  // The iOS HTTP ESM loader canonicalizes cache keys by stripping query params,
2591
3182
  // so we must carry the cache-buster in the path.
2592
3183
  try {
2593
- const m = spec.match(/^\/?__ns_hmr__\/[^\/]+(\/.*)?$/);
2594
- if (m) {
2595
- spec = m[1] || '/';
2596
- }
3184
+ const decorated = stripDecoratedServePrefixes(spec);
3185
+ spec = decorated.cleanedSpec;
3186
+ bootTaggedRequest = decorated.bootTaggedRequest;
3187
+ forcedVer || (forcedVer = decorated.forcedVer);
2597
3188
  }
2598
3189
  catch { }
2599
3190
  // Normalize absolute filesystem paths back to project-relative ids (e.g. /src/app.ts)
2600
3191
  try {
2601
- const projectRoot = (server.config?.root || process.cwd());
2602
3192
  const toPosix = (p) => p.replace(/\\/g, '/');
2603
- const rootPosix = toPosix(projectRoot);
3193
+ const rootPosix = toPosix(serverRoot);
2604
3194
  const specPosix = toPosix(spec);
2605
3195
  // If spec is an absolute path under the project root, convert to '/'+relative
2606
3196
  const isAbsFs = /^\//.test(specPosix) || /^[A-Za-z]:\//.test(spec); // posix or win drive
@@ -2615,27 +3205,78 @@ function createHmrWebSocketPlugin(opts) {
2615
3205
  }
2616
3206
  }
2617
3207
  catch { }
3208
+ // Serve Vite virtual modules (/@id/ prefix). These are internal
3209
+ // virtual modules (e.g., \0nsvite:nsconfig-json for ~/package.json)
3210
+ // that don't exist on disk. Decode the ID and load via plugin container.
3211
+ if (spec.startsWith('/@id/')) {
3212
+ try {
3213
+ // First try Vite's transform pipeline directly
3214
+ const vr = await sharedTransformRequest(spec);
3215
+ if (vr?.code) {
3216
+ res.statusCode = 200;
3217
+ res.end(vr.code);
3218
+ return;
3219
+ }
3220
+ }
3221
+ catch { }
3222
+ try {
3223
+ // Fallback: decode the virtual module ID (__x00__ → \0) and
3224
+ // load through the plugin container directly
3225
+ const rawId = spec.slice('/@id/'.length).replace(/__x00__/g, '\0');
3226
+ const loadResult = await server.pluginContainer.load(rawId);
3227
+ if (loadResult) {
3228
+ const code = typeof loadResult === 'string' ? loadResult : loadResult.code;
3229
+ if (code) {
3230
+ res.statusCode = 200;
3231
+ res.end(code);
3232
+ return;
3233
+ }
3234
+ }
3235
+ }
3236
+ catch { }
3237
+ }
2618
3238
  if (spec.startsWith('@/'))
2619
3239
  spec = APP_VIRTUAL_WITH_SLASH + spec.slice(2);
2620
3240
  if (spec.startsWith('./'))
2621
3241
  spec = spec.slice(1);
3242
+ const blockedNodeModulesReason = getBlockedDeviceNodeModulesReason(spec);
3243
+ if (blockedNodeModulesReason) {
3244
+ res.statusCode = 404;
3245
+ res.end(`// [ns:m] blocked device import\nthrow new Error(${JSON.stringify(`[ns/m] ${blockedNodeModulesReason}`)});\nexport {};\n`);
3246
+ return;
3247
+ }
2622
3248
  if (!spec.startsWith('/'))
2623
3249
  spec = '/' + spec;
2624
3250
  const hasExt = /\.(ts|tsx|js|jsx|mjs|mts|cts|vue)$/i.test(spec);
2625
3251
  const baseNoExt = hasExt ? spec.replace(/\.(ts|tsx|js|jsx|mjs|mts|cts)$/i, '') : spec;
2626
3252
  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'];
3253
+ const transformCandidates = filterExistingNodeModulesTransformCandidates(spec, candidates, serverRoot, monorepoWorkspaceRoot);
2627
3254
  let transformed = null;
2628
3255
  let resolvedCandidate = null;
2629
- for (const cand of candidates) {
2630
- try {
2631
- const r = await server.transformRequest(cand);
2632
- if (r?.code) {
2633
- transformed = r;
2634
- resolvedCandidate = cand;
2635
- break;
3256
+ const rawExplicitModule = tryReadRawExplicitJavaScriptModule(spec, serverRoot);
3257
+ if (rawExplicitModule) {
3258
+ transformed = { code: rawExplicitModule.code };
3259
+ resolvedCandidate = rawExplicitModule.resolvedId;
3260
+ }
3261
+ // Queue and dedupe transformRequest calls so heavy app graphs do not
3262
+ // overwhelm Vite with concurrent work. Slow-transform warnings start only
3263
+ // when the transform actually begins executing, and requests stay pending
3264
+ // until Vite returns a real result.
3265
+ const transformWithTimeout = (url, timeoutMs = 120000) => {
3266
+ return sharedTransformRequest(url, timeoutMs);
3267
+ };
3268
+ if (!transformed?.code) {
3269
+ for (const cand of transformCandidates) {
3270
+ try {
3271
+ const r = await transformWithTimeout(cand);
3272
+ if (r?.code) {
3273
+ transformed = r;
3274
+ resolvedCandidate = cand;
3275
+ break;
3276
+ }
2636
3277
  }
3278
+ catch { }
2637
3279
  }
2638
- catch { }
2639
3280
  }
2640
3281
  // Fallback 1: ask Vite to resolve the id, then transform the resolved id (handles aliases and virtual ids)
2641
3282
  if (!transformed?.code) {
@@ -2643,7 +3284,7 @@ function createHmrWebSocketPlugin(opts) {
2643
3284
  const rid = await server.pluginContainer?.resolveId?.(spec, undefined);
2644
3285
  const ridStr = typeof rid === 'string' ? rid : rid?.id || null;
2645
3286
  if (ridStr) {
2646
- const r = await server.transformRequest(ridStr);
3287
+ const r = await transformWithTimeout(ridStr);
2647
3288
  if (r?.code) {
2648
3289
  transformed = r;
2649
3290
  resolvedCandidate = ridStr;
@@ -2652,27 +3293,55 @@ function createHmrWebSocketPlugin(opts) {
2652
3293
  }
2653
3294
  catch { }
2654
3295
  }
2655
- // Fallback 2: try /@fs absolute path under project root (Vite file system alias)
3296
+ // Fallback 1b: if spec is a /node_modules/ path, extract bare specifier
3297
+ // and try resolveId with that. This handles package.json "exports" field
3298
+ // resolution (e.g., solid-js/jsx-runtime → solid-js/dist/solid.js).
3299
+ if (!transformed?.code && spec.includes('/node_modules/')) {
3300
+ try {
3301
+ const nmIdx = spec.lastIndexOf('/node_modules/');
3302
+ const bare = spec.slice(nmIdx + '/node_modules/'.length);
3303
+ if (bare && !bare.startsWith('.')) {
3304
+ const rid = await server.pluginContainer?.resolveId?.(bare, undefined);
3305
+ const ridStr = typeof rid === 'string' ? rid : rid?.id || null;
3306
+ if (ridStr) {
3307
+ const r = await sharedTransformRequest(ridStr);
3308
+ if (r?.code) {
3309
+ transformed = r;
3310
+ resolvedCandidate = ridStr;
3311
+ }
3312
+ }
3313
+ }
3314
+ }
3315
+ catch { }
3316
+ }
3317
+ // Fallback 2: try /@fs absolute path under project root (Vite file system alias).
3318
+ // In a monorepo with hoisted node_modules the file may live above
3319
+ // `serverRoot`, so try the workspace root next.
2656
3320
  if (!transformed?.code) {
2657
3321
  try {
2658
- const projectRoot = (server.config?.root || process.cwd());
2659
3322
  const toPosix = (p) => p.replace(/\\/g, '/');
2660
- const rootPosix = toPosix(projectRoot).replace(/\/$/, '');
2661
- const absPosix = `${rootPosix}${spec.startsWith('/') ? '' : '/'}${spec}`;
2662
- const fsId = `/@fs${absPosix}`;
2663
- const r = await server.transformRequest(fsId);
2664
- if (r?.code) {
2665
- transformed = r;
2666
- resolvedCandidate = fsId;
3323
+ const rootsToTry = [serverRoot, ...(monorepoWorkspaceRoot && path.resolve(monorepoWorkspaceRoot) !== path.resolve(serverRoot) ? [monorepoWorkspaceRoot] : [])];
3324
+ for (const root of rootsToTry) {
3325
+ const rootPosix = toPosix(root).replace(/\/$/, '');
3326
+ const absPosix = `${rootPosix}${spec.startsWith('/') ? '' : '/'}${spec}`;
3327
+ const fsId = `/@fs${absPosix}`;
3328
+ if (resolveCandidateFilePath(fsId, serverRoot, monorepoWorkspaceRoot)) {
3329
+ const r = await transformWithTimeout(fsId);
3330
+ if (r?.code) {
3331
+ transformed = r;
3332
+ resolvedCandidate = fsId;
3333
+ break;
3334
+ }
3335
+ }
2667
3336
  }
2668
3337
  }
2669
3338
  catch { }
2670
3339
  }
2671
3340
  // Fallback 3: try adding ?import to hint Vite's transform pipeline
2672
3341
  if (!transformed?.code) {
2673
- for (const cand of candidates) {
3342
+ for (const cand of transformCandidates) {
2674
3343
  try {
2675
- const r = await server.transformRequest(`${cand}${cand.includes('?') ? '&' : '?'}import`);
3344
+ const r = await transformWithTimeout(`${cand}${cand.includes('?') ? '&' : '?'}import`);
2676
3345
  if (r?.code) {
2677
3346
  transformed = r;
2678
3347
  resolvedCandidate = `${cand}?import`;
@@ -2682,39 +3351,80 @@ function createHmrWebSocketPlugin(opts) {
2682
3351
  catch { }
2683
3352
  }
2684
3353
  }
2685
- // Post-transform: inject cache-busting version for all internal /ns/m/* imports to avoid stale module reuse on device.
2686
- // IMPORTANT: use PATH-based busting (not query) because the iOS HTTP ESM loader strips query params
2687
- // when computing module cache keys.
3354
+ // Solid HMR: patch @@solid-refresh's $$refreshESM to do inline patching
3355
+ // during module re-evaluation instead of deferring to hot.accept() callback.
3356
+ // In NativeScript's HTTP ESM environment, accept callbacks are registered
3357
+ // but not invoked by the HMR client. By adding a direct patchRegistry()
3358
+ // call when hot.data already has a stored registry, component updates
3359
+ // apply immediately when the module re-evaluates.
2688
3360
  try {
2689
- if (transformed?.code) {
2690
- const ver = Number(global.graphVersion || graphVersion || 0);
2691
- let code = transformed.code;
2692
- const prefix = `/ns/m/__ns_hmr__/v${ver}`;
2693
- const rewrite = (p) => {
2694
- try {
2695
- if (!p || typeof p !== 'string')
2696
- return p;
2697
- if (!p.startsWith('/ns/m/'))
2698
- return p;
2699
- if (p.startsWith('/ns/m/__ns_hmr__/'))
2700
- return p;
2701
- return prefix + p.slice('/ns/m'.length);
3361
+ if (transformed?.code && ACTIVE_STRATEGY?.flavor === 'solid' && (resolvedCandidate || spec || '').includes('@solid-refresh')) {
3362
+ const PATCH_SENTINEL = '/* __ns_solid_refresh_patched__ */';
3363
+ const alreadyPatched = transformed.code.includes(PATCH_SENTINEL);
3364
+ if (verbose) {
3365
+ console.log('[hmr-ws][solid] @solid-refresh patch check:', { spec: resolvedCandidate || spec, alreadyPatched, codeLen: transformed.code.length });
3366
+ }
3367
+ if (!alreadyPatched) {
3368
+ let patchedCode = transformed.code;
3369
+ // Patch 1: Bypass shouldWarnAndDecline() — the vendor-bundled solid-js
3370
+ // may not have the 'development' condition active, making DEV empty/undefined.
3371
+ // In NativeScript HMR mode we are always in dev, so force it to return false.
3372
+ const declineCheck = 'function shouldWarnAndDecline() {';
3373
+ if (patchedCode.includes(declineCheck)) {
3374
+ patchedCode = patchedCode.replace(declineCheck, `${PATCH_SENTINEL}\nfunction shouldWarnAndDecline() { return false; /* NS HMR: always allow refresh */ }\nfunction __original_shouldWarnAndDecline() {`);
3375
+ if (verbose) {
3376
+ console.log('[hmr-ws][solid] bypassed shouldWarnAndDecline() for NativeScript HMR');
3377
+ }
2702
3378
  }
2703
- catch {
2704
- return p;
3379
+ // Patch 2: Force createMemo path in createProxy.
3380
+ // Without the 'development' condition, $DEVCOMP is not set on components,
3381
+ // so createProxy falls through to `return s(props)` — a direct call with
3382
+ // no reactive subscription. When patchComponent fires update() (the signal
3383
+ // setter), nobody is listening. By forcing the createMemo path, HMRComp
3384
+ // subscribes to the signal and re-renders when the component changes.
3385
+ const proxyCondition = 'if (!s || $DEVCOMP in s) {';
3386
+ if (patchedCode.includes(proxyCondition)) {
3387
+ patchedCode = patchedCode.replace(proxyCondition, 'if (true) { /* NS HMR: always use createMemo for reactive HMR updates */');
3388
+ if (verbose) {
3389
+ console.log('[hmr-ws][solid] forced createMemo path in createProxy for NativeScript HMR');
3390
+ }
3391
+ }
3392
+ // Patch 3: Inline patchRegistry call so updates apply immediately
3393
+ // on module re-evaluation (accept callbacks are not invoked by the HMR client).
3394
+ // The injected `console.log` helpers run inside the user's runtime
3395
+ // when @solid-refresh re-evaluates a module, so they are a runtime
3396
+ // concern (stripped if the user disables the patch). Keeping them
3397
+ // behind the patch sentinel rather than the dev-server `verbose`
3398
+ // flag is intentional — the patch only runs when Solid HMR fires.
3399
+ const marker = 'hot.data[SOLID_REFRESH] = hot.data[SOLID_REFRESH] || registry;';
3400
+ if (patchedCode.includes(marker)) {
3401
+ const patchCode = [
3402
+ `console.log('[solid-refresh][$$refreshESM] hot.data keys=', hot.data ? Object.keys(hot.data) : 'no-data', 'has=', !!(hot.data && hot.data[SOLID_REFRESH]));`,
3403
+ `if (hot.data[SOLID_REFRESH]) {`,
3404
+ ` console.log('[solid-refresh][$$refreshESM] patching: oldComponents=', hot.data[SOLID_REFRESH].components ? hot.data[SOLID_REFRESH].components.size : 0, 'newComponents=', registry.components ? registry.components.size : 0);`,
3405
+ ` var _shouldInvalidate = patchRegistry(hot.data[SOLID_REFRESH], registry);`,
3406
+ ` console.log('[solid-refresh][$$refreshESM] patchRegistry result: shouldInvalidate=', _shouldInvalidate);`,
3407
+ `} else {`,
3408
+ ` console.log('[solid-refresh][$$refreshESM] first load — creating registry, components=', registry.components ? registry.components.size : 0);`,
3409
+ `}`,
3410
+ ].join('\n ');
3411
+ patchedCode = patchedCode.replace(marker, `${patchCode}\n ${marker}`);
3412
+ if (verbose) {
3413
+ console.log('[hmr-ws][solid] added inline patchRegistry for NativeScript HMR');
3414
+ }
2705
3415
  }
2706
- };
2707
- // 1) Static imports: import ... from "/ns/m/..."
2708
- code = code.replace(/(from\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, (_m, a, p, b) => `${a}${rewrite(p)}${b}`);
2709
- // 2) Side-effect imports: import "/ns/m/..."
2710
- code = code.replace(/(import\s*(?!\()\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, (_m, a, p, b) => `${a}${rewrite(p)}${b}`);
2711
- // 3) Dynamic imports: import("/ns/m/...")
2712
- code = code.replace(/(import\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*\))/g, (_m, a, p, b) => `${a}${rewrite(p)}${b}`);
2713
- // 4) new URL("/ns/m/...", import.meta.url)
2714
- code = code.replace(/(new\s+URL\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*,\s*import\.meta\.url\s*\))/g, (_m, a, p, b) => `${a}${rewrite(p)}${b}`);
2715
- // 5) __ns_import(new URL('/ns/m/...', import.meta.url).href)
2716
- code = code.replace(/(new\s+URL\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*,\s*import\.meta\.url\s*\)\.href)/g, (_m, a, p, b) => `${a}${rewrite(p)}${b}`);
2717
- transformed.code = code;
3416
+ // Work on a copy to avoid mutating Vite's cached TransformResult
3417
+ transformed = { ...transformed, code: patchedCode };
3418
+ }
3419
+ }
3420
+ }
3421
+ catch { }
3422
+ // NOTE: Path-based cache busting for /ns/m/* imports is applied in the
3423
+ // finalize step below (after rewriteImports adds the /ns/m/ prefix).
3424
+ // The block here only handles TypeScript-specific graph population.
3425
+ try {
3426
+ if (transformed?.code) {
3427
+ const code = transformed.code;
2718
3428
  // TypeScript-specific graph population: when TS flavor is active
2719
3429
  // and this is an application module under the virtual app root,
2720
3430
  // upsert it into the HMR graph so ns:hmr-full-graph is non-empty.
@@ -2723,15 +3433,14 @@ function createHmrWebSocketPlugin(opts) {
2723
3433
  const id = (resolvedCandidate || spec).replace(/[?#].*$/, '');
2724
3434
  // Only track app modules (under APP_VIRTUAL_WITH_SLASH) and ts/js/tsx/jsx/mjs.
2725
3435
  const isApp = id.startsWith(APP_VIRTUAL_WITH_SLASH) || id.startsWith('/app/');
2726
- if (isApp && /\.(ts|tsx|js|jsx|mjs|mts|cts)$/i.test(id)) {
3436
+ if (isApp && /\.(ts|tsx|js|jsx|mjs|mts|cts)$/i.test(id) && !isRuntimeGraphExcludedPath(id)) {
2727
3437
  const deps = Array.from(collectImportDependencies(code, id));
2728
3438
  if (verbose) {
2729
- try {
2730
- console.log('[hmr-ws][ts-graph] candidate', { id, depsCount: deps.length });
2731
- }
2732
- catch { }
3439
+ console.log('[hmr-ws][ts-graph] candidate', { id, depsCount: deps.length });
2733
3440
  }
2734
- upsertGraphModule(id, code, deps);
3441
+ // Serve-time warm-up: no live edit happened, so don't bump
3442
+ // graphVersion.
3443
+ upsertGraphModule(id, code, deps, { bumpVersion: false });
2735
3444
  }
2736
3445
  }
2737
3446
  }
@@ -2843,7 +3552,7 @@ export const piniaSymbol = p.piniaSymbol;
2843
3552
  if (!transformed?.code) {
2844
3553
  // Emit a module that throws with context for easier on-device debugging
2845
3554
  try {
2846
- const tried = Array.from(new Set(candidates)).slice(0, 12);
3555
+ const tried = Array.from(new Set(transformCandidates.length > 0 ? transformCandidates : candidates)).slice(0, 12);
2847
3556
  const out = `// [ns:m] transform miss path=${spec} tried=${tried.length}\n` + `throw new Error(${JSON.stringify(`[ns/m] transform failed for ${spec} (tried ${tried.length} candidates).`)});\nexport {};\n`;
2848
3557
  res.statusCode = 404;
2849
3558
  res.end(out);
@@ -2860,8 +3569,33 @@ export const piniaSymbol = p.piniaSymbol;
2860
3569
  // Prepend guard to capture any URL-based require attempts
2861
3570
  code = REQUIRE_GUARD_SNIPPET + code;
2862
3571
  code = cleanCode(code);
2863
- code = processCodeForDevice(code, false);
2864
- code = rewriteImports(code, resolvedCandidate || spec, sfcFileMap, depFileMap, server.config?.root || process.cwd(), !!verbose, undefined, getServerOrigin(server));
3572
+ const isNodeMod = /(?:^|\/)node_modules\//.test(resolvedCandidate || spec || '');
3573
+ code = processCodeForDevice(code, false, true, isNodeMod, resolvedCandidate || spec);
3574
+ // Solid HMR: The NativeScript iOS/Android runtime provides import.meta.hot
3575
+ // natively (via InitializeImportMetaHot in HMRSupport.mm) with C++-backed
3576
+ // persistent hot.data that survives across module re-evaluations.
3577
+ // cleanCode() strips Vite's __vite__createHotContext assignment, which is
3578
+ // correct — the runtime's native hot context is better.
3579
+ const projectRoot = server.config?.root || process.cwd();
3580
+ const serverOrigin = getServerOrigin(server);
3581
+ if (ACTIVE_STRATEGY?.flavor === 'angular') {
3582
+ code = prepareAngularEntryForDevice(code, resolvedCandidate || spec, sfcFileMap, depFileMap, projectRoot, !!verbose, undefined, serverOrigin, true);
3583
+ }
3584
+ else {
3585
+ code = rewriteImports(code, resolvedCandidate || spec, sfcFileMap, depFileMap, projectRoot, !!verbose, undefined, serverOrigin, true);
3586
+ }
3587
+ // Expand `export * from "url"` into explicit named re-exports.
3588
+ // NativeScript's HTTP ESM loader may not propagate star-re-exports across
3589
+ // HTTP module boundaries (the namespace object gets direct exports but
3590
+ // misses re-exported names). By expanding to `export { a, b } from "url"`,
3591
+ // the engine sees explicit named exports and resolves them correctly.
3592
+ try {
3593
+ code = await expandStarExports(code, server, server.config?.root || process.cwd(), verbose, sharedTransformRequest);
3594
+ }
3595
+ catch (e) {
3596
+ if (verbose)
3597
+ console.warn('[ns/m] export* expansion failed:', e?.message);
3598
+ }
2865
3599
  // Dedupe any /ns/rt named imports that duplicate destructured bindings off default /ns/rt
2866
3600
  try {
2867
3601
  code = dedupeRtNamedImportsAgainstDestructures(code);
@@ -2888,6 +3622,28 @@ export const piniaSymbol = p.piniaSymbol;
2888
3622
  }
2889
3623
  }
2890
3624
  catch { }
3625
+ // Final pass: deduplicate/resolve any bare-specifier imports that slipped
3626
+ // through the pipeline (e.g., extracted from JSDoc comments by import-splitting
3627
+ // regexes, or injected by the Angular linker on already-resolved code).
3628
+ try {
3629
+ code = deduplicateLinkerImports(code);
3630
+ }
3631
+ catch { }
3632
+ // CJS/UMD wrapping: if a module uses module.exports but has no ESM export default,
3633
+ // wrap it with CJS shims so the device HTTP ESM loader can consume it.
3634
+ // This handles npm packages that use CommonJS but aren't pre-bundled by Vite.
3635
+ //
3636
+ // Key constraints this must handle:
3637
+ // - CJS modules often declare local vars with the same names as their exports
3638
+ // (e.g. `function createLTTB() {...}; exports.createLTTB = createLTTB;`)
3639
+ // so `export var { createLTTB }` would cause a duplicate declaration.
3640
+ // - UMD modules reference `this` at top level (undefined in ESM) but
3641
+ // typically fall back to `self` or `globalThis`.
3642
+ // - `module`, `exports` must be shims since they don't exist in ESM.
3643
+ try {
3644
+ code = wrapCommonJsModuleForDevice(code, resolvedCandidate || null);
3645
+ }
3646
+ catch { }
2891
3647
  try {
2892
3648
  assertNoOptimizedArtifacts(code, `NS M ${resolvedCandidate || spec}`);
2893
3649
  }
@@ -2907,35 +3663,81 @@ export const piniaSymbol = p.piniaSymbol;
2907
3663
  }
2908
3664
  }
2909
3665
  catch { }
3666
+ // `/ns/rt` and `/ns/core` URL versioning.
3667
+ //
3668
+ // Older versions of the server emitted `/ns/rt/<ver>` and
3669
+ // `/ns/core/<ver>` so V8's HTTP module cache would see a
3670
+ // fresh URL on every save. The runtime canonicalizer
3671
+ // (`CanonicalizeHttpUrlKey` in HMRSupport.mm) collapses
3672
+ // these version segments to the bare `/ns/rt` and
3673
+ // `/ns/core` keys before lookup, so V8 actually saw a
3674
+ // single cache entry — but the server was doing extra
3675
+ // work to inject a version segment that the runtime then
3676
+ // immediately stripped. Now that the runtime supports
3677
+ // explicit eviction (and these bridge endpoints don't
3678
+ // change at HMR time anyway), the version segment is
3679
+ // purely vestigial.
3680
+ //
3681
+ // Rather than rip the helpers out (which would touch
3682
+ // every ensureVersionedImports caller and risk bumping
3683
+ // older runtimes), we keep them but pass `verNum=0`. The
3684
+ // helpers still normalize URL shape (strip the absolute
3685
+ // origin prefix when present) but emit a stable
3686
+ // `/ns/rt/0` / `/ns/core/0` URL — which collapses to
3687
+ // `/ns/rt` / `/ns/core` in the runtime.
2910
3688
  try {
2911
- const verNum = Number(forcedVer || graphVersion || 0);
3689
+ const verNum = 0;
2912
3690
  code = ensureVersionedRtImports(code, getServerOrigin(server), verNum);
2913
3691
  code = ACTIVE_STRATEGY.ensureVersionedImports(code, getServerOrigin(server), verNum);
2914
3692
  code = ensureVersionedCoreImports(code, getServerOrigin(server), verNum);
2915
3693
  }
2916
3694
  catch { }
2917
- // Finalize: also stamp all internal /ns/m imports with ?v=<ver> after all rewrites
3695
+ // `/ns/m` URL finalize step.
3696
+ //
3697
+ // `rewriteNsMImportPathForHmr` is a canonicalizer: it
3698
+ // strips legacy `__ns_hmr__/<tag>/` segments and adds
3699
+ // `__ns_boot__/b1/` only for boot-tagged requests. The
3700
+ // `ver` parameter is preserved on the signature for API
3701
+ // compatibility but is ignored for app modules (cache
3702
+ // busting is driven by `__nsInvalidateModules`, not URL
3703
+ // versioning). We pass `'v0'` as a stable placeholder —
3704
+ // the canonicalizer emits the same URL regardless of
3705
+ // this value, but a constant placeholder makes the
3706
+ // contract explicit.
3707
+ //
3708
+ // SFC URLs (line below, `/ns/sfc/${verTag}/...`) still
3709
+ // embed a version because the Vue SFC pathway does not
3710
+ // yet have an eviction protocol. The runtime
3711
+ // canonicalizer does NOT strip `/ns/sfc/<ver>/`, so Vue
3712
+ // users still see per-save SFC re-fetches — that's a
3713
+ // known follow-up.
2918
3714
  try {
2919
- const ver = String(forcedVer || graphVersion || 0);
3715
+ const verTag = (() => {
3716
+ const numeric = getNumericServeVersionTag(forcedVer, Number(graphVersion || 0));
3717
+ return numeric > 0 ? `v${numeric}` : 'v0';
3718
+ })();
2920
3719
  const origin = getServerOrigin(server);
3720
+ const rewritePath = (p) => rewriteNsMImportPathForHmr(p, 'v0', bootTaggedRequest);
3721
+ // /ns/m URL forms — all collapse to canonical stable
3722
+ // URLs via the Phase 3a rewriter.
2921
3723
  // 1) Static imports: import ... from "/ns/m/..."
2922
- code = code.replace(/(from\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, `$1$2?v=${ver}$3`);
3724
+ code = code.replace(/(from\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
2923
3725
  // 2) Side-effect imports: import "/ns/m/..."
2924
- code = code.replace(/(import\s*(?!\()\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, `$1$2?v=${ver}$3`);
3726
+ code = code.replace(/(import\s*(?!\()\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
2925
3727
  // 3) Dynamic imports: import("/ns/m/...")
2926
- code = code.replace(/(import\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*\))/g, `$1$2?v=${ver}$3`);
3728
+ code = code.replace(/(import\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*\))/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
2927
3729
  // 4) new URL("/ns/m/...", import.meta.url)
2928
- code = code.replace(/(new\s+URL\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*,\s*import\.meta\.url\s*\))/g, `$1$2?v=${ver}$3`);
3730
+ code = code.replace(/(new\s+URL\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*,\s*import\.meta\.url\s*\))/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
2929
3731
  // 5) __ns_import(new URL('/ns/m/...', import.meta.url).href)
2930
- code = code.replace(/(new\s+URL\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*,\s*import\.meta\.url\s*\)\.href)/g, `$1$2?v=${ver}$3`);
2931
- // 6) Force absolute HTTP for new URL('/ns/m/...', import.meta.url).href → "${origin}/ns/m/..."
3732
+ 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}`);
3733
+ // 6) Force absolute HTTP for new URL('/ns/m/...', import.meta.url).href → canonical stable URL.
2932
3734
  try {
2933
- code = code.replace(/new\s+URL\(\s*["'](\/ns\/m\/[^"'?]+)(?:\?[^"']*)?["']\s*,\s*import\.meta\.url\s*\)\.href/g, (_m, p1) => `${JSON.stringify(`${origin}${p1}?v=${ver}`)}`);
3735
+ code = code.replace(/new\s+URL\(\s*["'](\/ns\/m\/[^"'?]+)(?:\?[^"']*)?["']\s*,\s*import\.meta\.url\s*\)\.href/g, (_m, p1) => `${JSON.stringify(`${origin}${rewritePath(p1)}`)}`);
2934
3736
  }
2935
3737
  catch { }
2936
- // 7) Also fix SFC new URL('/ns/sfc/...', import.meta.url).href "${origin}/ns/sfc/<ver>/..."
3738
+ // 7) SFC URLs (Vue) — still versioned. See header comment.
2937
3739
  try {
2938
- code = code.replace(/new\s+URL\(\s*["']\/ns\/sfc(\/[^"'?]+)(?:\?[^"']*)?["']\s*,\s*import\.meta\.url\s*\)\.href/g, (_m, p1) => `${JSON.stringify(`${origin}/ns/sfc/${ver}${p1}`)}`);
3740
+ 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}`)}`);
2939
3741
  }
2940
3742
  catch { }
2941
3743
  }
@@ -2946,13 +3748,25 @@ export const piniaSymbol = p.piniaSymbol;
2946
3748
  code = ensureDestructureCoreImports(code);
2947
3749
  }
2948
3750
  catch { }
3751
+ // Boot-time module graph progress: while the app is still replacing the
3752
+ // placeholder, emit lightweight progress updates as /ns/m modules begin
3753
+ // evaluating. This keeps the overlay moving during large initial graphs.
3754
+ try {
3755
+ if (bootTaggedRequest) {
3756
+ const bootModuleLabel = String(spec || '').replace(/\\/g, '/');
3757
+ const bootProgressSnippet = buildBootProgressSnippet(bootModuleLabel);
3758
+ code = bootProgressSnippet + code;
3759
+ code = hoistTopLevelStaticImports(code);
3760
+ }
3761
+ }
3762
+ catch { }
2949
3763
  // Dev-only: link-check static imports to surface missing bindings early
2950
3764
  try {
2951
3765
  const devCheck = process.env.NODE_ENV !== 'production';
2952
3766
  if (devCheck) {
2953
3767
  const ast = babelParse(code, {
2954
3768
  sourceType: 'module',
2955
- plugins: ['typescript', 'importMeta'],
3769
+ plugins: MODULE_IMPORT_ANALYSIS_PLUGINS,
2956
3770
  });
2957
3771
  const imports = [];
2958
3772
  babelTraverse(ast, {
@@ -3036,6 +3850,15 @@ export const piniaSymbol = p.piniaSymbol;
3036
3850
  continue;
3037
3851
  const hasDefault = /\bexport\s+default\b/.test(targetCode) || /export\s*\{\s*default\s*(?:as\s*default)?\s*\}/.test(targetCode);
3038
3852
  if (!hasDefault) {
3853
+ // CJS/UMD modules won't have `export default` — they get CJS-wrapped
3854
+ // by the serving pipeline. Only warn, don't fatally block the importer.
3855
+ const hasCjsPattern = /\bmodule\s*\.\s*exports\b/.test(targetCode) || /\bexports\s*\.\s*\w/.test(targetCode);
3856
+ if (hasCjsPattern) {
3857
+ if (verbose) {
3858
+ console.warn(`[ns:m][link-check] CJS module without export default: ${u.pathname} (will be CJS-wrapped at serve time)`);
3859
+ }
3860
+ continue;
3861
+ }
3039
3862
  const msg = `[link-check] Missing default export in ${u.pathname}${u.search} (imported by ${resolvedCandidate || spec})`;
3040
3863
  // Emit a module that throws to surface the exact offender
3041
3864
  res.statusCode = 200;
@@ -3047,19 +3870,15 @@ export const piniaSymbol = p.piniaSymbol;
3047
3870
  }
3048
3871
  }
3049
3872
  catch (eLC) {
3050
- try {
3873
+ if (verbose) {
3051
3874
  console.warn('[ns:m][link-check] failed', eLC?.message || eLC);
3052
3875
  }
3053
- catch { }
3054
3876
  }
3055
3877
  res.statusCode = 200;
3056
3878
  res.end(code);
3057
3879
  }
3058
3880
  catch (e) {
3059
- try {
3060
- console.warn('[sfc-asm] error serving', req.url, e && e.message ? e.message : e);
3061
- }
3062
- catch { }
3881
+ console.warn('[sfc-asm] error serving', req.url, e && e.message ? e.message : e);
3063
3882
  res.statusCode = 500;
3064
3883
  res.end('export {}\n');
3065
3884
  }
@@ -3092,8 +3911,10 @@ export const piniaSymbol = p.piniaSymbol;
3092
3911
  `let __cached_rt = null;\n` +
3093
3912
  `let __cached_vm = null;\n` +
3094
3913
  `const __RT_REALM_TAG = (globalThis.__NS_RT_REALM__ ||= Math.random().toString(36).slice(2));\n` +
3095
- // Unconditional one-shot evaluation marker to confirm bridge is executed on device
3096
- `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` +
3914
+ // One-shot evaluation marker to confirm the bridge is executed on
3915
+ // device. Gated on __NS_ENV_VERBOSE__ so it stays silent unless
3916
+ // the developer opts in via NS_VITE_VERBOSE / VITE_DEBUG_LOGS.
3917
+ `try { if (!(globalThis.__NS_RT_ONCE__ && globalThis.__NS_RT_ONCE__.eval)) { (globalThis.__NS_RT_ONCE__ ||= {}).eval = true; if (globalThis.__NS_ENV_VERBOSE__) console.log('[ns-rt] evaluated', { rtRealm: __RT_REALM_TAG }); } } catch {}\n` +
3097
3918
  `function __ensure(){\n` +
3098
3919
  ` if (__cached_rt) return __cached_rt;\n` +
3099
3920
  ` let vm = null;\n` +
@@ -3185,7 +4006,7 @@ export const piniaSymbol = p.piniaSymbol;
3185
4006
  `export const vShow = (__ensure().vShow);\n` +
3186
4007
  `export const createApp = (...a) => (__ensure().createApp)(...a);\n` +
3187
4008
  `export const registerElement = (...a) => (__ensure().registerElement)(...a);\n` +
3188
- `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` +
4009
+ `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) { console.error('[ns-rt] $navigateTo app navigator error', e); throw e; } } console.error('[ns-rt] $navigateTo unavailable: app navigator missing'); throw new Error('$navigateTo unavailable: app navigator missing'); } ;\n` +
3189
4010
  `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` +
3190
4011
  `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` +
3191
4012
  `export default {\n` +
@@ -3247,35 +4068,262 @@ export const piniaSymbol = p.piniaSymbol;
3247
4068
  return next();
3248
4069
  }
3249
4070
  });
4071
+ // 2.5.1) Catch-all redirect for stray /node_modules/@nativescript/core/*
4072
+ // requests — route them to the /ns/core bridge so they get the same
4073
+ // __DEV__/__IOS__ preamble and specifier rewriting. Without this,
4074
+ // Vite's default /node_modules/ handler serves the raw file, which
4075
+ // references bare __DEV__ and crashes at module eval.
4076
+ server.middlewares.use((req, _res, next) => {
4077
+ try {
4078
+ const urlObj = new URL(req.url || '', 'http://localhost');
4079
+ const coreNmPrefix = '/node_modules/@nativescript/core';
4080
+ if (!urlObj.pathname.startsWith(coreNmPrefix))
4081
+ return next();
4082
+ const sub = urlObj.pathname.slice(coreNmPrefix.length).replace(/^\/+/, '');
4083
+ if (sub === '' || sub === 'index.js' || sub === 'index') {
4084
+ req.url = `/ns/core`;
4085
+ }
4086
+ else {
4087
+ req.url = `/ns/core/${sub}`;
4088
+ }
4089
+ return next();
4090
+ }
4091
+ catch {
4092
+ return next();
4093
+ }
4094
+ });
3250
4095
  // 2.6) ESM bridge for @nativescript/core: GET /ns/core[/<ver>][?p=sub/path]
4096
+ //
4097
+ // Since bundle.mjs no longer bundles @nativescript/core (it is
4098
+ // declared external in the rolldown config under HMR), this
4099
+ // endpoint is the ONE place core is evaluated. Every consumer —
4100
+ // bundle.mjs's own `@nativescript/core*` imports (resolved to
4101
+ // full HTTP URLs in the entry virtual module), externalized
4102
+ // vendor packages, HTTP-served app modules — all end up here.
4103
+ // No more proxy bridge, no enumeration, no namespace detection,
4104
+ // no prototype-polluted maps. We just serve Vite's authoritative
4105
+ // transformed module content.
4106
+ //
4107
+ // iOS caches by URL path, so each unique URL is evaluated exactly
4108
+ // once per app lifetime. Every class identity is shared, every
4109
+ // `register()` side effect runs once, every `Application` reference
4110
+ // is the same iosApp singleton. The entire class of "does not
4111
+ // provide an export named X" and "Cannot redefine property" errors
4112
+ // is eliminated by construction.
3251
4113
  server.middlewares.use(async (req, res, next) => {
3252
4114
  try {
3253
4115
  const urlObj = new URL(req.url || '', 'http://localhost');
3254
- if (!(urlObj.pathname === '/ns/core' || /^\/ns\/core\/[\d]+$/.test(urlObj.pathname)))
4116
+ const coreRequest = parseCoreBridgeRequest(urlObj.pathname, urlObj.searchParams, Number(graphVersion || 0));
4117
+ if (!coreRequest)
3255
4118
  return next();
3256
4119
  res.setHeader('Access-Control-Allow-Origin', '*');
3257
4120
  res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
3258
4121
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
3259
4122
  res.setHeader('Pragma', 'no-cache');
3260
4123
  res.setHeader('Expires', '0');
3261
- const verSeg = urlObj.pathname.replace(/^\/ns\/core\/?/, '');
3262
- const ver = /^[0-9]+$/.test(verSeg) ? verSeg : String(graphVersion || 0);
3263
- const sub = urlObj.searchParams.get('p') || '';
3264
- const key = sub ? `@nativescript/core/${sub}` : `@nativescript/core`;
3265
- // HTTP-only core bridge: do NOT use require/createRequire. Export a proxy that maps
3266
- // property access to globalThis first, then to any available vendor registry module.
3267
- let code = REQUIRE_GUARD_SNIPPET +
3268
- `// [ns-core-bridge][v${ver}] HTTP-only ESM bridge (default proxy only)\n` +
3269
- `const g = globalThis;\n` +
3270
- `const reg = (g.__nsVendorRegistry ||= new Map());\n` +
3271
- `const __getVendorCore = () => { try { const m = reg && reg.get ? (reg.get(${JSON.stringify(key)}) || reg.get('@nativescript/core')) : null; return (m && (m.__esModule && m.default ? m.default : (m.default || m))) || m || null; } catch { return null; } };\n` +
3272
- `const __core = new Proxy({}, { get(_t, p){ if (p === 'default') return __core; if (p === Symbol.toStringTag) return 'Module'; try { const v = g[p]; if (v !== undefined) return v; } catch {} try { const vc = __getVendorCore(); return vc ? vc[p] : undefined; } catch {} return undefined; } });\n` +
3273
- `// Default export: namespace-like proxy\n` +
3274
- `export default __core;\n`;
4124
+ const { normalizedSub, sub, ver } = coreRequest;
4125
+ const resolveModuleId = async (moduleId) => {
4126
+ const resolved = await server.pluginContainer?.resolveId?.(moduleId, undefined);
4127
+ return typeof resolved === 'string' ? resolved : resolved?.id || null;
4128
+ };
4129
+ let modulePath = null;
4130
+ if (sub) {
4131
+ const resolvedSubpath = normalizedSub || sub;
4132
+ modulePath = await resolveRuntimeCoreModulePath(resolvedSubpath, resolveModuleId);
4133
+ if (!modulePath) {
4134
+ modulePath = `/node_modules/@nativescript/core/${resolvedSubpath}`;
4135
+ }
4136
+ }
4137
+ else {
4138
+ modulePath = (await resolveModuleId('@nativescript/core')) || '/node_modules/@nativescript/core/index.js';
4139
+ }
4140
+ const transformed = await sharedTransformRequest(modulePath);
4141
+ if (!transformed?.code) {
4142
+ res.statusCode = 500;
4143
+ res.setHeader('Content-Type', 'application/json');
4144
+ res.end(JSON.stringify({ error: 'core-transform-failed', modulePath, sub: sub || null }));
4145
+ return;
4146
+ }
4147
+ // Vite's transform output references module IDs with /@fs,
4148
+ // relative specifiers, or absolute project paths. Rewrite
4149
+ // those to URLs iOS can fetch over HTTP.
4150
+ let rewritten = rewriteSpecifiersForDevice(transformed.code, getServerOrigin(server), Number(ver));
4151
+ // Invariant D (CJS/ESM interop shape) — EXPORT-SIDE fix.
4152
+ //
4153
+ // `@nativescript/core/index.js` declares namespace
4154
+ // re-exports like:
4155
+ // export * as Utils from './utils';
4156
+ // The ES spec says these produce Module Namespace Objects
4157
+ // with [[Prototype]] = null. Consumers that reach them
4158
+ // via direct ESM import — `import { Utils } from
4159
+ // '@nativescript/core'` — get the raw null-proto value,
4160
+ // bypassing any CJS `require` shim we install. Most
4161
+ // consumers tolerate this, but CJS-style interop (most
4162
+ // notably zone.js's `patchMethod`) calls
4163
+ // `hasOwnProperty` on the target and crashes on
4164
+ // null-proto.
4165
+ //
4166
+ // We rewrite the re-export to a shape-wrapped const:
4167
+ // import * as __ns_re_Utils__ from './utils';
4168
+ // export const Utils = __NS_CJS_SHAPE__(__ns_re_Utils__);
4169
+ // so the EXPORT itself is a plain object — visible to
4170
+ // both ESM and CJS consumers consistently.
4171
+ //
4172
+ // We only pay the rewrite cost when the module actually
4173
+ // contains namespace re-exports (i.e., the main
4174
+ // `index.js`). Subpaths (`/utils`, `/http`, …) don't
4175
+ // re-export via `export * as`; they expose named
4176
+ // exports directly, so the rewrite is a no-op on them.
4177
+ if (hasNamespaceReExport(rewritten)) {
4178
+ rewritten = rewriteNamespaceReExportsForShape(rewritten);
4179
+ }
4180
+ // Prepend the build-time defines (__DEV__, __IOS__, __ANDROID__,
4181
+ // __APPLE__, …) that @nativescript/core source references directly.
4182
+ // Vite's `define` config substitutes these in user-code transforms but
4183
+ // skips node_modules by default; since core is now external and served
4184
+ // over HTTP from this endpoint, the served transformed code still has
4185
+ // bare identifiers like `if (__DEV__) …`. Without these consts, V8
4186
+ // hits `ReferenceError: __DEV__ is not defined` at module eval because
4187
+ // globalThis.__DEV__ is set by bundle.mjs's body AFTER all static
4188
+ // imports (including these core modules) have resolved.
4189
+ //
4190
+ // We inject LITERAL boolean values based on CLI flags + dev-server
4191
+ // mode rather than reading from globalThis, so the defines are
4192
+ // resolved even before bundle.mjs's body runs.
4193
+ const __cliFlags = getCliFlags() || {};
4194
+ const __platformIsAndroid = !!__cliFlags.android;
4195
+ const __platformIsVisionOS = !!__cliFlags.visionos;
4196
+ const __platformIsIOS = !__platformIsAndroid && !__platformIsVisionOS;
4197
+ const preamble = [
4198
+ `const __ANDROID__ = ${__platformIsAndroid ? 'true' : 'false'};`,
4199
+ `const __IOS__ = ${__platformIsIOS ? 'true' : 'false'};`,
4200
+ `const __VISIONOS__ = ${__platformIsVisionOS ? 'true' : 'false'};`,
4201
+ `const __APPLE__ = __IOS__ || __VISIONOS__;`,
4202
+ `const __DEV__ = ${server.config?.mode === 'development' ? 'true' : 'false'};`,
4203
+ `const __COMMONJS__ = false;`,
4204
+ `const __NS_WEBPACK__ = false;`,
4205
+ `const __NS_ENV_VERBOSE__ = globalThis.__NS_ENV_VERBOSE__ !== undefined ? !!globalThis.__NS_ENV_VERBOSE__ : false;`,
4206
+ `const __CSS_PARSER__ = 'css-tree';`,
4207
+ `const __UI_USE_XML_PARSER__ = true;`,
4208
+ `const __UI_USE_EXTERNAL_RENDERER__ = false;`,
4209
+ `const __TEST__ = false;`,
4210
+ ].join('\n');
4211
+ // Boot-time instrumentation + module self-registration.
4212
+ //
4213
+ // - URL canonicalization: the same logical module must
4214
+ // always resolve to byte-identical URLs across every
4215
+ // emitter. The /ns/core handler records the first URL
4216
+ // seen for each canonical sub (or '' for main) in
4217
+ // `globalThis.__NS_CORE_FIRST_URL__` and fails hard on
4218
+ // mismatch so drift in any emitter surfaces
4219
+ // immediately, before the realm splits.
4220
+ // - CJS/ESM boot order: CommonJS
4221
+ // `require('@nativescript/core/...')` calls from
4222
+ // vendor install() hooks must resolve to the SAME
4223
+ // ESM namespace that ran this side-effect preamble.
4224
+ // The registration below keys the namespace object
4225
+ // under BOTH the bare specifier and the canonical
4226
+ // subpath (and raw subpath for back-compat) so the
4227
+ // vendor shim's `createRequire` and the main-entry
4228
+ // `_nsReq` hit on any lookup form.
4229
+ const rawSub = normalizedSub || sub || '';
4230
+ const canonicalSub = normalizeCoreSubCanonical(rawSub);
4231
+ const registrationKeySet = new Set();
4232
+ registrationKeySet.add(canonicalSub ? `@nativescript/core/${canonicalSub}` : '@nativescript/core');
4233
+ registrationKeySet.add(canonicalSub);
4234
+ if (rawSub && rawSub !== canonicalSub) {
4235
+ registrationKeySet.add(`@nativescript/core/${rawSub}`);
4236
+ registrationKeySet.add(rawSub);
4237
+ }
4238
+ const registrationKeys = Array.from(registrationKeySet).map((k) => JSON.stringify(k));
4239
+ const canonicalUrl = `${getServerOrigin(server)}` + (canonicalSub ? `/ns/core/${canonicalSub}` : '/ns/core');
4240
+ const instrumentationHeader = [
4241
+ `/* @nativescript/core bridge — canonical URL: ${canonicalUrl} */`,
4242
+ `try { if (typeof globalThis !== 'undefined') {`,
4243
+ ` const __nsFirst = globalThis.__NS_CORE_FIRST_URL__ || (globalThis.__NS_CORE_FIRST_URL__ = Object.create(null));`,
4244
+ ` const __nsSeen = globalThis.__NS_CORE_FETCHED_URLS__ || (globalThis.__NS_CORE_FETCHED_URLS__ = []);`,
4245
+ ` const __nsKey = ${JSON.stringify(canonicalSub)};`,
4246
+ ` const __nsUrl = ${JSON.stringify(canonicalUrl)};`,
4247
+ ` __nsSeen.push(__nsUrl);`,
4248
+ ` if (typeof __nsFirst[__nsKey] === 'string' && __nsFirst[__nsKey] !== __nsUrl) {`,
4249
+ ` throw new Error('[ns-core] URL drift for sub=' + __nsKey + ': first=' + __nsFirst[__nsKey] + ' now=' + __nsUrl);`,
4250
+ ` }`,
4251
+ ` if (!__nsFirst[__nsKey]) __nsFirst[__nsKey] = __nsUrl;`,
4252
+ ` globalThis.__NS_CORE_EVAL_COUNT__ = (globalThis.__NS_CORE_EVAL_COUNT__ || 0) + 1;`,
4253
+ `} } catch (e) { console.warn('[ns-core] instrumentation failed:', (e && e.message) || e); }`,
4254
+ ].join('\n');
4255
+ // CJS/ESM interop shape — REGISTRATION side.
4256
+ //
4257
+ // The actual shape installer runs earlier in the module
4258
+ // body (between preamble and selfImport; see
4259
+ // buildShapeInstallHeader). At this point we just read
4260
+ // globalThis.__NS_CJS_SHAPE__ and apply it to the self
4261
+ // namespace before registering under the CJS key space.
4262
+ //
4263
+ // Why shape self at registration: consumers that reach
4264
+ // `@nativescript/core` via `require()` (legacy vendors,
4265
+ // `globalThis.require` shim) look up the registry. They
4266
+ // expect a plain object (Object.prototype in chain) so
4267
+ // `.hasOwnProperty` / `.toString` work. Shaping once on
4268
+ // registration — the shape function is identity-preserving
4269
+ // via WeakMap — gives a stable, shared, CJS-compatible
4270
+ // view without copying on every require.
4271
+ const registrationFooter = [
4272
+ `try { if (typeof globalThis !== 'undefined') {`,
4273
+ ` const __nsReg = globalThis.__NS_CORE_MODULES__ || (globalThis.__NS_CORE_MODULES__ = Object.create(null));`,
4274
+ ` const __nsShapeFn = typeof globalThis.__NS_CJS_SHAPE__ === 'function' ? globalThis.__NS_CJS_SHAPE__ : function (x) { return x; };`,
4275
+ ` const __nsSelfRaw = (typeof __ns_core_self_ns__ !== 'undefined') ? __ns_core_self_ns__ : { default: undefined };`,
4276
+ ` const __nsSelf = __nsShapeFn(__nsSelfRaw);`,
4277
+ ...registrationKeys.map((k) => ` __nsReg[${k}] = __nsSelf;`),
4278
+ `} } catch (e) { console.warn('[ns-core] self-register failed:', (e && e.message) || e); }`,
4279
+ ].join('\n');
4280
+ // Bind `import * as __ns_core_self_ns__` to the module's
4281
+ // own export namespace so the footer can stash it into
4282
+ // the registry. Self-import is a no-op at eval time —
4283
+ // V8 resolves it to the module record we're already
4284
+ // evaluating and the final namespace is the same object
4285
+ // the registry receives. We use the CANONICAL URL here
4286
+ // so the self-import participates in Invariant A along
4287
+ // with every other @nativescript/core URL.
4288
+ const canonicalUrlForSelf = canonicalSub ? `/ns/core/${canonicalSub}` : '/ns/core';
4289
+ const selfImport = `import * as __ns_core_self_ns__ from ${JSON.stringify(canonicalUrlForSelf)};`;
4290
+ // Invariant D — SHAPE INSTALLER.
4291
+ //
4292
+ // Emits idempotent body-code that installs
4293
+ // globalThis.__NS_CJS_SHAPE__ BEFORE `rewritten`'s body
4294
+ // runs. This matters because the rewrite step above may
4295
+ // have produced statements like
4296
+ // `export const Utils = (typeof globalThis.__NS_CJS_SHAPE__ ...)(__ns_re_Utils__);`
4297
+ // that execute during module evaluation. Without the
4298
+ // installer running first, the ternary falls back to
4299
+ // identity — still safe, but the null-proto namespace
4300
+ // leaks through and consumers that expect a plain
4301
+ // object would still crash.
4302
+ //
4303
+ // Placement is important: BEFORE selfImport in the
4304
+ // concatenation. ESM imports are hoisted regardless of
4305
+ // textual position, but body code executes in source
4306
+ // order. Placing the installer first guarantees it
4307
+ // runs before any body statement in `rewritten`.
4308
+ //
4309
+ // Install is idempotent: `|| (globalThis.X = ...)` so
4310
+ // whichever /ns/core module evaluates first wins and
4311
+ // every subsequent module becomes a no-op.
4312
+ const shapeInstallHeader = buildShapeInstallHeader();
4313
+ // Invariant D — DEFAULT EXPORT BRIDGE.
4314
+ //
4315
+ // See `buildDefaultExportFooter` in ns-core-cjs-shape.ts
4316
+ // for the full rationale (consumer matrix, skip conditions,
4317
+ // why the default isn't shaped). The short version:
4318
+ // upstream rewrites turn `import { X } from '@nativescript/core'`
4319
+ // into a DEFAULT import, and the bridge has to provide one.
4320
+ const defaultExportFooter = buildDefaultExportFooter(rewritten);
4321
+ const moduleCode = [instrumentationHeader, preamble, shapeInstallHeader, selfImport, rewritten, defaultExportFooter, registrationFooter].join('\n');
3275
4322
  res.statusCode = 200;
3276
- res.end(code);
4323
+ res.end(moduleCode);
3277
4324
  }
3278
4325
  catch (e) {
4326
+ console.warn('[ns-core-bridge] serve failed:', e?.message);
3279
4327
  next();
3280
4328
  }
3281
4329
  });
@@ -3285,14 +4333,11 @@ export const piniaSymbol = p.piniaSymbol;
3285
4333
  const urlObj = new URL(req.url || '', 'http://localhost');
3286
4334
  if (!(urlObj.pathname === '/ns/entry-rt'))
3287
4335
  return next();
3288
- try {
3289
- if (verbose) {
3290
- const ra = req.socket?.remoteAddress;
3291
- const rp = req.socket?.remotePort;
3292
- console.log('[hmr-http] GET /ns/entry-rt from', ra + (rp ? ':' + rp : ''));
3293
- }
4336
+ if (verbose) {
4337
+ const ra = req.socket?.remoteAddress;
4338
+ const rp = req.socket?.remotePort;
4339
+ console.log('[hmr-http] GET /ns/entry-rt from', ra + (rp ? ':' + rp : ''));
3294
4340
  }
3295
- catch { }
3296
4341
  res.setHeader('Access-Control-Allow-Origin', '*');
3297
4342
  res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
3298
4343
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
@@ -3300,18 +4345,41 @@ export const piniaSymbol = p.piniaSymbol;
3300
4345
  res.setHeader('Expires', '0');
3301
4346
  let content = '';
3302
4347
  try {
3303
- const req = createRequire(import.meta.url);
3304
- const entryRtPath = req.resolve('@nativescript/vite/hmr/entry-runtime.js');
3305
- const fs = req('fs');
3306
- content = fs.readFileSync(entryRtPath, 'utf-8');
4348
+ const _req = createRequire(import.meta.url);
4349
+ const entryRtPath = _req.resolve('@nativescript/vite/hmr/entry-runtime.js');
4350
+ content = readFileSync(entryRtPath, 'utf-8');
3307
4351
  }
3308
4352
  catch (e) {
3309
- content = 'export default async function start(){ console.error("[/ns/entry-rt] not found"); }\n';
4353
+ // .js not found (source tree without build) — transform .ts on the fly
4354
+ try {
4355
+ const tsPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', 'entry-runtime.ts');
4356
+ if (existsSync(tsPath)) {
4357
+ const tsSource = readFileSync(tsPath, 'utf-8');
4358
+ const result = babelCore.transformSync(tsSource, {
4359
+ filename: tsPath,
4360
+ plugins: [[pluginTransformTypescript, { isTSX: false, allowDeclareFields: true }]],
4361
+ sourceType: 'module',
4362
+ });
4363
+ if (result?.code) {
4364
+ content = result.code;
4365
+ }
4366
+ }
4367
+ }
4368
+ catch (e2) {
4369
+ if (verbose)
4370
+ console.warn('[hmr-http] entry-runtime.ts transform failed', e2);
4371
+ }
4372
+ if (!content) {
4373
+ content = 'export default async function start(){ console.error("[/ns/entry-rt] not found"); }\n';
4374
+ }
3310
4375
  }
4376
+ if (verbose)
4377
+ console.log('[hmr-http] /ns/entry-rt serving', content.length, 'bytes');
3311
4378
  res.statusCode = 200;
3312
4379
  res.end(content);
3313
4380
  }
3314
4381
  catch (e) {
4382
+ console.warn('[hmr-http] /ns/entry-rt error', e);
3315
4383
  next();
3316
4384
  }
3317
4385
  });
@@ -3514,10 +4582,7 @@ export const piniaSymbol = p.piniaSymbol;
3514
4582
  }
3515
4583
  if (!transformed?.code) {
3516
4584
  if (verbose) {
3517
- try {
3518
- console.warn(`[sfc][serve] transform miss for`, fullSpec);
3519
- }
3520
- catch { }
4585
+ console.warn(`[sfc][serve] transform miss for`, fullSpec);
3521
4586
  }
3522
4587
  // Emit an erroring module to surface the failure at import site with helpful hints
3523
4588
  try {
@@ -3633,10 +4698,7 @@ export const piniaSymbol = p.piniaSymbol;
3633
4698
  }
3634
4699
  catch (eTplSelf) {
3635
4700
  if (verbose) {
3636
- try {
3637
- console.warn('[sfc][template][self-compile][fail]', fullSpec, eTplSelf?.message);
3638
- }
3639
- catch { }
4701
+ console.warn('[sfc][template][self-compile][fail]', fullSpec, eTplSelf?.message);
3640
4702
  }
3641
4703
  code = transformed.code || 'export {}\n';
3642
4704
  code = processTemplateVariantMinimal(code);
@@ -3741,7 +4803,7 @@ export const piniaSymbol = p.piniaSymbol;
3741
4803
  code = outCode;
3742
4804
  }
3743
4805
  catch { }
3744
- code = processCodeForDevice(code, false);
4806
+ code = processCodeForDevice(code, false, true, /(?:^|\/)node_modules\//.test(fullSpec), fullSpec);
3745
4807
  // Transform static .vue imports into static imports from the assembler (no TLA) via AST
3746
4808
  try {
3747
4809
  const importerPath = fullSpec.replace(/[?#].*$/, '');
@@ -3803,10 +4865,7 @@ export const piniaSymbol = p.piniaSymbol;
3803
4865
  }
3804
4866
  catch (eTsVar) {
3805
4867
  if (verbose) {
3806
- try {
3807
- console.warn('[sfc][variant:script][babel-ts][fail]', fullSpec, eTsVar?.message);
3808
- }
3809
- catch { }
4868
+ console.warn('[sfc][variant:script][babel-ts][fail]', fullSpec, eTsVar?.message);
3810
4869
  }
3811
4870
  }
3812
4871
  }
@@ -3870,10 +4929,7 @@ export const piniaSymbol = p.piniaSymbol;
3870
4929
  const kind = isVariant ? `variant:${variantType || 'unknown'}` : 'full';
3871
4930
  const sig = `// [sfc] kind=${kind} path=${importerPath} len=${code.length} default=${hasDefault} wrapped=${false}\n`;
3872
4931
  if (verbose) {
3873
- try {
3874
- console.log(`[sfc][serve] ${fullSpec} kind=${kind} default=${hasDefault} bytes=${code.length}`);
3875
- }
3876
- catch { }
4932
+ console.log(`[sfc][serve] ${fullSpec} kind=${kind} default=${hasDefault} bytes=${code.length}`);
3877
4933
  }
3878
4934
  // Ensure script variants always provide a default export if they declare a component
3879
4935
  if (!hasDefault) {
@@ -3942,16 +4998,10 @@ export const piniaSymbol = p.piniaSymbol;
3942
4998
  // 6) Object property forms (rare in template output) render: (...) => {}
3943
4999
  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);
3944
5000
  if (hasRender && verbose) {
3945
- try {
3946
- console.log('[sfc-meta] detected render for', base);
3947
- }
3948
- catch { }
5001
+ console.log('[sfc-meta] detected render for', base);
3949
5002
  }
3950
5003
  else if (!hasRender && verbose) {
3951
- try {
3952
- console.warn('[sfc-meta] render NOT detected for', base);
3953
- }
3954
- catch { }
5004
+ console.warn('[sfc-meta] render NOT detected for', base);
3955
5005
  }
3956
5006
  const hash = createHash('md5').update(base).digest('hex').slice(0, 8);
3957
5007
  const payload = {
@@ -4085,10 +5135,7 @@ export const piniaSymbol = p.piniaSymbol;
4085
5135
  }
4086
5136
  catch (eScript) {
4087
5137
  if (verbose) {
4088
- try {
4089
- console.warn('[sfc-asm][compileScript] failed', base, eScript?.message);
4090
- }
4091
- catch { }
5138
+ console.warn('[sfc-asm][compileScript] failed', base, eScript?.message);
4092
5139
  }
4093
5140
  // Retry without inlineTemplate
4094
5141
  try {
@@ -4104,10 +5151,7 @@ export const piniaSymbol = p.piniaSymbol;
4104
5151
  }
4105
5152
  catch (eNoInline) {
4106
5153
  if (verbose) {
4107
- try {
4108
- console.warn('[sfc-asm][compileScript][no-inline-fallback] failed', base, eNoInline?.message);
4109
- }
4110
- catch { }
5154
+ console.warn('[sfc-asm][compileScript][no-inline-fallback] failed', base, eNoInline?.message);
4111
5155
  }
4112
5156
  }
4113
5157
  }
@@ -4138,10 +5182,7 @@ export const piniaSymbol = p.piniaSymbol;
4138
5182
  }
4139
5183
  catch (eNoInline) {
4140
5184
  if (verbose) {
4141
- try {
4142
- console.warn('[sfc-asm][compileScript][no-inline-fallback] failed', base, eNoInline?.message);
4143
- }
4144
- catch { }
5185
+ console.warn('[sfc-asm][compileScript][no-inline-fallback] failed', base, eNoInline?.message);
4145
5186
  }
4146
5187
  }
4147
5188
  }
@@ -4164,20 +5205,14 @@ export const piniaSymbol = p.piniaSymbol;
4164
5205
  });
4165
5206
  compiledTplCode = (ct && (ct.code || '')) || '';
4166
5207
  if (ct?.errors?.length && verbose) {
4167
- try {
4168
- console.warn('[sfc-asm][compileTemplate][errors]', base, ct.errors);
4169
- }
4170
- catch { }
5208
+ console.warn('[sfc-asm][compileTemplate][errors]', base, ct.errors);
4171
5209
  }
4172
5210
  }
4173
5211
  }
4174
5212
  catch (eTpl) {
4175
5213
  templateErr = eTpl;
4176
5214
  if (verbose) {
4177
- try {
4178
- console.warn('[sfc-asm][compileTemplate] failed', base, eTpl?.message);
4179
- }
4180
- catch { }
5215
+ console.warn('[sfc-asm][compileTemplate] failed', base, eTpl?.message);
4181
5216
  }
4182
5217
  // Fallback: use the variant-transformed template code if available
4183
5218
  try {
@@ -4240,10 +5275,7 @@ export const piniaSymbol = p.piniaSymbol;
4240
5275
  }
4241
5276
  catch (eExtract) {
4242
5277
  if (verbose) {
4243
- try {
4244
- console.warn('[sfc-asm][extractTemplateRender] failed', base, eExtract?.message);
4245
- }
4246
- catch { }
5278
+ console.warn('[sfc-asm][extractTemplateRender] failed', base, eExtract?.message);
4247
5279
  }
4248
5280
  }
4249
5281
  }
@@ -4293,11 +5325,10 @@ export const piniaSymbol = p.piniaSymbol;
4293
5325
  parts.push(scriptTransformed);
4294
5326
  parts.push(renderDecl);
4295
5327
  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){}`);
4296
- parts.push(`// diagnostic: hadScriptDefaultPre=${hadScriptDefaultPre} triedInlineTemplate=${triedInlineTemplate} renderOk=${renderOk} tplBytes=${compiledTplCode.length} scriptBytes=${(compiledScript || '').length} templateErr=${templateErr ? templateErr?.message : ''}`);
4297
5328
  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; }`);
4298
5329
  parts.push(`export default __ns_sfc__`);
4299
5330
  let inlineCode = parts.filter(Boolean).join('\n');
4300
- inlineCode = processCodeForDevice(inlineCode, false);
5331
+ inlineCode = processCodeForDevice(inlineCode, false, true);
4301
5332
  try {
4302
5333
  inlineCode = ensureVersionedCoreImports(inlineCode, getServerOrigin(server), Number(ver));
4303
5334
  }
@@ -4322,10 +5353,7 @@ export const piniaSymbol = p.piniaSymbol;
4322
5353
  }
4323
5354
  catch (eTs) {
4324
5355
  if (verbose) {
4325
- try {
4326
- console.warn('[sfc-asm][babel-ts][fail]', base, eTs?.message);
4327
- }
4328
- catch { }
5356
+ console.warn('[sfc-asm][babel-ts][fail]', base, eTs?.message);
4329
5357
  }
4330
5358
  }
4331
5359
  // Hoist imports + strip residual TS via AST
@@ -4335,18 +5363,12 @@ export const piniaSymbol = p.piniaSymbol;
4335
5363
  importLines = astRes.imports;
4336
5364
  scriptTransformed = astRes.body;
4337
5365
  if (astRes.diagnostics.length && verbose) {
4338
- try {
4339
- console.warn('[sfc-asm][ast]', base, astRes.diagnostics.join('; '));
4340
- }
4341
- catch { }
5366
+ console.warn('[sfc-asm][ast]', base, astRes.diagnostics.join('; '));
4342
5367
  }
4343
5368
  }
4344
5369
  catch (eAst) {
4345
5370
  if (verbose) {
4346
- try {
4347
- console.warn('[sfc-asm][ast][fail]', base, eAst?.message);
4348
- }
4349
- catch { }
5371
+ console.warn('[sfc-asm][ast][fail]', base, eAst?.message);
4350
5372
  }
4351
5373
  }
4352
5374
  // Ensure renderDecl ends with closing brace ONLY for function declaration forms
@@ -4372,12 +5394,11 @@ export const piniaSymbol = p.piniaSymbol;
4372
5394
  outParts.push(renderDecl);
4373
5395
  }
4374
5396
  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){}`);
4375
- outParts.push(`// diagnostic: hadScriptDefaultPre=${hadScriptDefaultPre} triedInlineTemplate=${triedInlineTemplate} renderOk=${renderOk} tplBytes=${compiledTplCode.length} scriptBytes=${(compiledScript || '').length} templateErr=${templateErr ? templateErr?.message : ''}`);
4376
5397
  // Export named render as a function that resolves lazily
4377
5398
  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; }');
4378
5399
  outParts.push('export default __ns_sfc__');
4379
5400
  let inlineCode2 = outParts.filter(Boolean).join('\n');
4380
- inlineCode2 = processCodeForDevice(inlineCode2, false);
5401
+ inlineCode2 = processCodeForDevice(inlineCode2, false, true);
4381
5402
  try {
4382
5403
  inlineCode2 = ensureVersionedCoreImports(inlineCode2, getServerOrigin(server), Number(ver));
4383
5404
  }
@@ -4673,12 +5694,11 @@ export const piniaSymbol = p.piniaSymbol;
4673
5694
  }
4674
5695
  let asm;
4675
5696
  if (inlineOk) {
4676
- const diagLine = `// diagnostic:inlineOk ver=${ver} inlineBlock=${!!(inlineBlock && inlineBlock.trim())} helperBindingsLen=${helperBindings.length} renderDeclLen=${renderDecl.length}`;
4677
5697
  if (inlineBlock && inlineBlock.trim()) {
4678
- 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__;`, diagLine].join('\n');
5698
+ 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');
4679
5699
  }
4680
5700
  else {
4681
- 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__;`, diagLine].filter(Boolean).join('\n');
5701
+ 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');
4682
5702
  }
4683
5703
  }
4684
5704
  else {
@@ -4689,7 +5709,7 @@ export const piniaSymbol = p.piniaSymbol;
4689
5709
  }
4690
5710
  // Run full device processing so helper aliasing and globals are consistent in this path too
4691
5711
  let code = REQUIRE_GUARD_SNIPPET + asm;
4692
- code = processCodeForDevice(code, false);
5712
+ code = processCodeForDevice(code, false, true, /(?:^|\/)node_modules\//.test(base), base);
4693
5713
  try {
4694
5714
  code = ensureVersionedCoreImports(code, getServerOrigin(server), Number(ver));
4695
5715
  }
@@ -4862,7 +5882,7 @@ export const piniaSymbol = p.piniaSymbol;
4862
5882
  let code = transformed.code;
4863
5883
  // Reuse existing sanitation chain (lightweight)
4864
5884
  code = cleanCode(code);
4865
- code = processCodeForDevice(code, false);
5885
+ code = processCodeForDevice(code, false, true, /(?:^|\/)node_modules\//.test(resolvedCandidate || spec), resolvedCandidate || spec);
4866
5886
  try {
4867
5887
  code = ensureVersionedCoreImports(code, getServerOrigin(server), graphVersion);
4868
5888
  }
@@ -4917,7 +5937,7 @@ export const piniaSymbol = p.piniaSymbol;
4917
5937
  if (depTrans?.code && depResolved) {
4918
5938
  let depCode = depTrans.code;
4919
5939
  depCode = cleanCode(depCode);
4920
- depCode = processCodeForDevice(depCode, false);
5940
+ depCode = processCodeForDevice(depCode, false, true, /(?:^|\/)node_modules\//.test(depResolved), depResolved);
4921
5941
  try {
4922
5942
  depCode = ensureVersionedCoreImports(depCode, getServerOrigin(server), graphVersion);
4923
5943
  }
@@ -4950,8 +5970,8 @@ export const piniaSymbol = p.piniaSymbol;
4950
5970
  ts: Date.now(),
4951
5971
  delta: true,
4952
5972
  };
4953
- wss.clients.forEach((c) => {
4954
- if (c.readyState === c.OPEN) {
5973
+ wss?.clients.forEach((c) => {
5974
+ if (isSocketClientOpen(c)) {
4955
5975
  try {
4956
5976
  c.send(JSON.stringify(single));
4957
5977
  }
@@ -4990,32 +6010,33 @@ export const piniaSymbol = p.piniaSymbol;
4990
6010
  if (verbose)
4991
6011
  console.warn('[hmr-ws][graph] initial population failed', e);
4992
6012
  }
4993
- // Send SFC registry on first connection
4994
- if (!registrySent) {
4995
- try {
4996
- await ACTIVE_STRATEGY.buildRegistry({
4997
- server,
4998
- sfcFileMap,
4999
- depFileMap,
5000
- wss: wss,
5001
- verbose,
5002
- helpers: {
5003
- cleanCode,
5004
- collectImportDependencies,
5005
- isCoreGlobalsReference,
5006
- isNativeScriptCoreModule,
5007
- isNativeScriptPluginModule,
5008
- resolveVendorFromCandidate,
5009
- createHash: (value) => createHash('md5').update(value).digest('hex'),
5010
- rewriteImports,
5011
- processSfcCode,
5012
- },
5013
- });
5014
- registrySent = true;
5015
- }
5016
- catch (error) {
5017
- console.warn('[hmr-ws] Failed to send registry:', error);
5018
- }
6013
+ // Send SFC registry on every connection (not just the first).
6014
+ // When the NativeScript app restarts (e.g. CLI auto-reload), the new
6015
+ // JS context has an empty sfcArtifactMap. Without the registry the
6016
+ // rescue-mount cannot find the root .vue component.
6017
+ try {
6018
+ await ACTIVE_STRATEGY.buildRegistry({
6019
+ server,
6020
+ sfcFileMap,
6021
+ depFileMap,
6022
+ wss: wss,
6023
+ verbose,
6024
+ helpers: {
6025
+ cleanCode,
6026
+ collectImportDependencies,
6027
+ isCoreGlobalsReference,
6028
+ isNativeScriptCoreModule,
6029
+ isNativeScriptPluginModule,
6030
+ resolveVendorFromCandidate,
6031
+ createHash: (value) => createHash('md5').update(value).digest('hex'),
6032
+ rewriteImports,
6033
+ processSfcCode,
6034
+ },
6035
+ });
6036
+ registrySent = true;
6037
+ }
6038
+ catch (error) {
6039
+ console.warn('[hmr-ws] Failed to send registry:', error);
5019
6040
  }
5020
6041
  emitFullGraph(ws);
5021
6042
  // After sending registry & graph also send current module manifest if any
@@ -5038,16 +6059,134 @@ export const piniaSymbol = p.piniaSymbol;
5038
6059
  if (!wss) {
5039
6060
  return;
5040
6061
  }
6062
+ if (isRuntimeGraphExcludedPath(file)) {
6063
+ return;
6064
+ }
6065
+ // Always-on update timing. Captures the four phases (await,
6066
+ // framework, broadcast, total) plus invalidated module count
6067
+ // and recipient count. Emitted at the end of this function via
6068
+ // `emitHmrUpdateSummary()`. Single line, always-on so a
6069
+ // 6-second `.ts` save is immediately visible without flipping
6070
+ // verbose.
6071
+ const updateRoot = server.config.root || process.cwd();
6072
+ const updateRel = (() => {
6073
+ try {
6074
+ return '/' + path.posix.normalize(path.relative(updateRoot, file)).split(path.sep).join('/');
6075
+ }
6076
+ catch {
6077
+ return file;
6078
+ }
6079
+ })();
6080
+ const updateMetrics = {
6081
+ file: updateRel,
6082
+ kind: classifyHmrUpdateKind(file),
6083
+ t0: Date.now(),
6084
+ tAfterAwait: 0,
6085
+ tAfterFramework: 0,
6086
+ tEnd: 0,
6087
+ invalidated: 0,
6088
+ recipients: 0,
6089
+ // Narrowing diagnostic — populated by the angular branch when
6090
+ // the changed file is `.ts`, otherwise remains undefined and is
6091
+ // omitted from the summary line entirely.
6092
+ narrowed: undefined,
6093
+ emitted: false,
6094
+ };
6095
+ // Broadcast a "pending" notification at the very start of
6096
+ // handleHotUpdate so the client can show the HMR-applying
6097
+ // overlay BEFORE we spend time on graph updates / transforms /
6098
+ // dependency analysis (typically 7–200ms on a warm cache).
6099
+ // Without this, the overlay only appears at `ns:angular-update`
6100
+ // broadcast time and the user perceives a "delayed" reaction
6101
+ // to their save.
6102
+ //
6103
+ // Fire-and-forget: a failed pending broadcast must never
6104
+ // hold up the actual update. The client treats receipt of
6105
+ // `ns:angular-update` (or `ns:css-updates`) as authoritative;
6106
+ // the pending message is purely a UX hint.
6107
+ try {
6108
+ const pendingPayload = JSON.stringify(createHmrPendingMessage({
6109
+ origin: getServerOrigin(server),
6110
+ path: updateMetrics.file,
6111
+ kind: updateMetrics.kind,
6112
+ timestamp: updateMetrics.t0,
6113
+ }));
6114
+ wss.clients.forEach((client) => {
6115
+ if (isSocketClientOpen(client)) {
6116
+ try {
6117
+ client.send(pendingPayload);
6118
+ }
6119
+ catch { }
6120
+ }
6121
+ });
6122
+ }
6123
+ catch { }
6124
+ const emitHmrUpdateSummary = () => {
6125
+ if (updateMetrics.emitted)
6126
+ return;
6127
+ updateMetrics.emitted = true;
6128
+ updateMetrics.tEnd = Date.now();
6129
+ try {
6130
+ const awaitMs = (updateMetrics.tAfterAwait || updateMetrics.t0) - updateMetrics.t0;
6131
+ const frameworkMs = (updateMetrics.tAfterFramework || updateMetrics.tAfterAwait || updateMetrics.t0) - (updateMetrics.tAfterAwait || updateMetrics.t0);
6132
+ const broadcastMs = updateMetrics.tEnd - (updateMetrics.tAfterFramework || updateMetrics.tAfterAwait || updateMetrics.t0);
6133
+ const totalMs = updateMetrics.tEnd - updateMetrics.t0;
6134
+ console.info(formatHmrUpdateSummary({
6135
+ file: updateMetrics.file,
6136
+ kind: updateMetrics.kind,
6137
+ awaitMs,
6138
+ frameworkMs,
6139
+ broadcastMs,
6140
+ totalMs,
6141
+ invalidated: updateMetrics.invalidated,
6142
+ recipients: updateMetrics.recipients,
6143
+ narrowed: updateMetrics.narrowed,
6144
+ }));
6145
+ }
6146
+ catch { }
6147
+ };
6148
+ // The first /ns/m request kicks off populateInitialGraph in the
6149
+ // background. If an HMR update races in before that walk
6150
+ // completes, we'd lose transitive-importer data. Await
6151
+ // completion here so the delta computation below always sees a
6152
+ // populated graph.
6153
+ if (graphInitialPopulationPromise) {
6154
+ try {
6155
+ await graphInitialPopulationPromise;
6156
+ }
6157
+ catch { }
6158
+ }
6159
+ updateMetrics.tAfterAwait = Date.now();
5041
6160
  // Graph update for this file change (wrapped to avoid aborting rest of handler)
5042
6161
  try {
5043
- const mod = server.moduleGraph.getModuleById(file) || server.moduleGraph.getModuleById(file + '?vue');
5044
- if (mod) {
5045
- const deps = Array.from(mod.importedModules)
5046
- .map((m) => (m.id || '').replace(/\?.*$/, ''))
5047
- .filter(Boolean);
5048
- const transformed = await server.transformRequest(mod.id);
5049
- const code = transformed?.code || '';
5050
- upsertGraphModule((mod.id || '').replace(/\?.*$/, ''), code, deps);
6162
+ const skipAngularHtmlGraphUpdate = ACTIVE_STRATEGY.flavor === 'angular' && /\.(html|htm)$/i.test(file);
6163
+ if (!skipAngularHtmlGraphUpdate) {
6164
+ const graphTargets = collectGraphUpdateModulesForHotUpdate({
6165
+ file,
6166
+ flavor: ACTIVE_STRATEGY.flavor,
6167
+ modules: ctx.modules,
6168
+ getModuleById: (id) => server.moduleGraph.getModuleById(id),
6169
+ verbose,
6170
+ });
6171
+ for (const mod of graphTargets) {
6172
+ if (!mod?.id)
6173
+ continue;
6174
+ try {
6175
+ const deps = Array.from(mod.importedModules || [])
6176
+ .map((m) => (m.id || '').replace(/\?.*$/, ''))
6177
+ .filter(Boolean);
6178
+ const transformed = await server.transformRequest(mod.id);
6179
+ const code = transformed?.code || '';
6180
+ upsertGraphModule((mod.id || '').replace(/\?.*$/, ''), code, deps, {
6181
+ emitDeltaOnInsert: true,
6182
+ broadcastDelta: ACTIVE_STRATEGY.flavor !== 'angular',
6183
+ });
6184
+ }
6185
+ catch (error) {
6186
+ if (verbose)
6187
+ console.warn('[hmr-ws][v2] failed graph update target', mod.id, error);
6188
+ }
6189
+ }
5051
6190
  }
5052
6191
  }
5053
6192
  catch (e) {
@@ -5068,6 +6207,7 @@ export const piniaSymbol = p.piniaSymbol;
5068
6207
  console.log(`[hmr-ws] Hot update for: ${file}`);
5069
6208
  // Handle CSS updates
5070
6209
  if (file.endsWith('.css')) {
6210
+ updateMetrics.tAfterFramework = Date.now();
5071
6211
  try {
5072
6212
  let rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
5073
6213
  const origin = getServerOrigin(server);
@@ -5084,14 +6224,16 @@ export const piniaSymbol = p.piniaSymbol;
5084
6224
  ],
5085
6225
  };
5086
6226
  wss.clients.forEach((client) => {
5087
- if (client.readyState === client.OPEN) {
6227
+ if (isSocketClientOpen(client)) {
5088
6228
  client.send(JSON.stringify(msg));
6229
+ updateMetrics.recipients += 1;
5089
6230
  }
5090
6231
  });
5091
6232
  }
5092
6233
  catch (error) {
5093
6234
  console.warn('[hmr-ws] CSS update failed:', error);
5094
6235
  }
6236
+ emitHmrUpdateSummary();
5095
6237
  return;
5096
6238
  }
5097
6239
  // Framework-specific hot update handling
@@ -5099,31 +6241,281 @@ export const piniaSymbol = p.piniaSymbol;
5099
6241
  // For Angular, react to component TS or external template HTML changes under /src
5100
6242
  const isHtml = file.endsWith('.html');
5101
6243
  const isTs = file.endsWith('.ts');
6244
+ const angularHotUpdateRoots = collectAngularHotUpdateRoots({
6245
+ file,
6246
+ modules: ctx.modules,
6247
+ getModuleById: (id) => server.moduleGraph.getModuleById(id),
6248
+ getModulesByFile: (targetFile) => server.moduleGraph.getModulesByFile?.(targetFile),
6249
+ });
6250
+ if (verbose) {
6251
+ console.info(`[ns-hmr-diag][server] hot-update file=${file} isHtml=${isHtml} isTs=${isTs} ctxModules=${Array.from(ctx.modules || []).length} hotUpdateRoots=${angularHotUpdateRoots.length} (${angularHotUpdateRoots
6252
+ .map((m) => m?.id ?? '(none)')
6253
+ .slice(0, 8)
6254
+ .join(', ')}${angularHotUpdateRoots.length > 8 ? ', …' : ''})`);
6255
+ }
5102
6256
  if (!(isHtml || isTs))
5103
6257
  return;
6258
+ updateMetrics.invalidated += angularHotUpdateRoots.length;
6259
+ if (angularHotUpdateRoots.length) {
6260
+ for (const mod of angularHotUpdateRoots) {
6261
+ try {
6262
+ server.moduleGraph.invalidateModule(mod);
6263
+ }
6264
+ catch (invalidationError) {
6265
+ if (verbose) {
6266
+ console.warn('[hmr-ws][angular] hot-update root invalidation failed', mod?.id, invalidationError);
6267
+ }
6268
+ }
6269
+ }
6270
+ if (verbose) {
6271
+ console.log('[hmr-ws][angular] invalidated hot-update root modules:', angularHotUpdateRoots.length);
6272
+ }
6273
+ }
6274
+ const angularTransitiveInvalidationRoots = (angularHotUpdateRoots.length ? angularHotUpdateRoots : ctx.modules);
6275
+ // Read the source for `.ts/.tsx/.js/.jsx` edits so
6276
+ // `shouldInvalidateAngularTransitiveImporters` can
6277
+ // distinguish leaf modules (constants/utils) from real
6278
+ // Angular files. If `ctx.read()` throws (file deleted, race
6279
+ // against the watcher), `angularChangedSource` stays
6280
+ // undefined and we fall back to the conservative "always
6281
+ // invalidate transitively" behavior.
6282
+ let angularChangedSource;
6283
+ if (isTs) {
6284
+ try {
6285
+ angularChangedSource = await ctx.read();
6286
+ }
6287
+ catch {
6288
+ angularChangedSource = undefined;
6289
+ }
6290
+ }
6291
+ const angularNeedsTransitive = shouldInvalidateAngularTransitiveImporters({
6292
+ flavor: ACTIVE_STRATEGY.flavor,
6293
+ file,
6294
+ source: angularChangedSource,
6295
+ });
6296
+ // Surface the narrowing decision on every `.ts` Angular hot
6297
+ // update (HTML routes always invalidate transitively and
6298
+ // aren't subject to narrowing, so we leave them as
6299
+ // `undefined` — the field is omitted from the summary line).
6300
+ // The boolean is the inverse of `angularNeedsTransitive`
6301
+ // because "needs transitive" is the broad (un-narrowed)
6302
+ // behavior.
6303
+ if (isTs) {
6304
+ updateMetrics.narrowed = !angularNeedsTransitive;
6305
+ }
6306
+ // Stable URL + Explicit Invalidation:
6307
+ //
6308
+ // Compute the transitive importer closure ONCE here and reuse
6309
+ // it for (a) `server.moduleGraph.invalidateModule` (so Vite's
6310
+ // transform pipeline re-runs on next request), (b) the shared
6311
+ // transform-request cache, and (c) the runtime eviction set
6312
+ // we broadcast in `ns:angular-update`. Consolidating this
6313
+ // removes a redundant graph walk and guarantees the three
6314
+ // consumers see the exact same set of importers (otherwise a
6315
+ // late module-graph mutation between calls could leave an
6316
+ // asymmetric narrowed/broad mix).
6317
+ //
6318
+ // We separate Vite-transform narrowing from runtime eviction:
6319
+ // `angularNeedsTransitive` answers the question "does the
6320
+ // changed file's symbol shape change such that importers
6321
+ // must be re-transformed by Vite?". The runtime, however,
6322
+ // has a stricter requirement: ESM live bindings only refresh
6323
+ // if the importing module re-evaluates inside V8. A
6324
+ // constants file with no Angular decorator does NOT need a
6325
+ // Vite re-transform of its importers (their compiled JS is
6326
+ // identical), but its importers still hold stale bindings to
6327
+ // the OLD constants Module record. After eviction + re-import
6328
+ // of `main.ts`, V8 sees the cached importers, returns them
6329
+ // unchanged, and they continue to read the OLD values. The
6330
+ // user-visible symptom: HMR completes successfully, logs are
6331
+ // clean, but the simulator does not reflect the change.
6332
+ //
6333
+ // The fix: ALWAYS compute the transitive importer closure
6334
+ // for runtime eviction. Only skip Vite's
6335
+ // `moduleGraph.invalidate` + transform-cache purge when
6336
+ // `angularNeedsTransitive` is false — those are the genuine
6337
+ // narrowing wins (saves re-transform work on the server).
6338
+ // The eviction set always includes importers so V8 re-fetches
6339
+ // and re-binds them.
6340
+ if (verbose) {
6341
+ console.info(`[ns-hmr-diag][server] angularNeedsTransitive=${angularNeedsTransitive} (file=${path.basename(file)})`);
6342
+ }
6343
+ let transitiveImporters = [];
6344
+ try {
6345
+ transitiveImporters = collectAngularTransitiveImportersForInvalidation({
6346
+ modules: angularTransitiveInvalidationRoots,
6347
+ isExcluded: (id) => id.includes('/node_modules/'),
6348
+ maxDepth: 16,
6349
+ });
6350
+ if (verbose) {
6351
+ console.info(`[ns-hmr-diag][server] transitiveImporters count=${transitiveImporters.length} firstN=`, transitiveImporters.slice(0, 16).map((m) => m?.id ?? '(none)'));
6352
+ }
6353
+ if (angularNeedsTransitive) {
6354
+ updateMetrics.invalidated += transitiveImporters.length;
6355
+ for (const mod of transitiveImporters) {
6356
+ try {
6357
+ server.moduleGraph.invalidateModule(mod);
6358
+ }
6359
+ catch (invalidationError) {
6360
+ if (verbose) {
6361
+ console.warn('[hmr-ws][angular] transitive importer invalidation failed', mod?.id, invalidationError);
6362
+ }
6363
+ }
6364
+ }
6365
+ if (verbose && transitiveImporters.length) {
6366
+ console.log('[hmr-ws][angular] invalidated transitive importers:', transitiveImporters.length);
6367
+ }
6368
+ }
6369
+ else if (isTs && typeof angularChangedSource === 'string') {
6370
+ // Surfacing this log unconditionally lets the user
6371
+ // immediately confirm whether narrowing fired for a
6372
+ // given `.ts` edit (the summary line below still
6373
+ // emits `narrowed=yes`/`no`, but having both makes
6374
+ // the decision easier to spot in noisy logs and lets
6375
+ // the user diff scenarios without flipping
6376
+ // `NS_HMR_VERBOSE=true`).
6377
+ //
6378
+ // Narrowing means "skip Vite re-transform" (the
6379
+ // importers still get evicted from the V8 module
6380
+ // registry so live bindings refresh). The importer
6381
+ // count is appended so the distinction is visible.
6382
+ console.log(`[hmr-ws][angular] narrowed transitive invalidation (no @Component/@Directive/@Pipe/@Injectable/@NgModule): ${updateRel} — Vite transform skipped, runtime eviction includes ${transitiveImporters.length} importer(s)`);
6383
+ }
6384
+ }
6385
+ catch (error) {
6386
+ if (verbose)
6387
+ console.warn('[hmr-ws][angular] transitive importer collection failed', error);
6388
+ }
6389
+ try {
6390
+ // Purge shared transform cache for the changed file +
6391
+ // hot-update roots unconditionally (their transform
6392
+ // output IS different now). Transitive importers are
6393
+ // only purged when narrowing decides their output may
6394
+ // have changed; otherwise their cached transforms are
6395
+ // still valid (compiled JS is identical even though the
6396
+ // runtime must re-evaluate them to refresh ESM bindings).
6397
+ const transformCacheInvalidationUrls = new Set(collectAngularTransformCacheInvalidationUrls({
6398
+ file,
6399
+ isTs,
6400
+ hotUpdateRoots: angularHotUpdateRoots,
6401
+ transitiveImporters: angularNeedsTransitive ? transitiveImporters : [],
6402
+ projectRoot: server.config.root || process.cwd(),
6403
+ }));
6404
+ if (transformCacheInvalidationUrls.size) {
6405
+ sharedTransformRequest.invalidateMany(transformCacheInvalidationUrls);
6406
+ if (verbose) {
6407
+ console.log('[hmr-ws][angular] purged shared transform cache entries:', transformCacheInvalidationUrls.size);
6408
+ }
6409
+ }
6410
+ }
6411
+ catch (error) {
6412
+ if (verbose)
6413
+ console.warn('[hmr-ws][angular] shared transform cache purge failed', error);
6414
+ }
6415
+ updateMetrics.tAfterFramework = Date.now();
5104
6416
  try {
5105
6417
  const root = server.config.root || process.cwd();
5106
6418
  const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
6419
+ rememberAngularReloadSuppression(root, file);
5107
6420
  const origin = getServerOrigin(server);
6421
+ const bootstrapEntryRel = getBootstrapEntryRelPath();
6422
+ // Stable URL + Explicit Invalidation:
6423
+ //
6424
+ // `evictPaths` is the canonical list of `/ns/m/<rel>` URLs
6425
+ // the runtime must drop from `g_moduleRegistry` before
6426
+ // re-importing `importerEntry`. Older versions of the
6427
+ // server signaled invalidation by bumping a global
6428
+ // `graphVersion` counter and embedding it in every URL —
6429
+ // but V8 keys the module registry by full URL, so a v1 →
6430
+ // v2 bump effectively flushed the entire dependency
6431
+ // graph from the cache and forced the runtime to
6432
+ // re-fetch + re-eval every transitively-imported module
6433
+ // on each save (~3s HMR cycles, dominated by Vite's
6434
+ // single-threaded transform pipeline). The new model:
6435
+ //
6436
+ // 1. URLs are stable: `/ns/m/<rel>` everywhere, no `vN`.
6437
+ // 2. The server walks the inverse-dependency closure and
6438
+ // sends only the modules that actually need to be
6439
+ // re-evaluated (typically O(1) for component edits,
6440
+ // or the changed file + entry for narrowed edits).
6441
+ // 3. The client calls `__nsInvalidateModules(evictPaths)`
6442
+ // and re-imports `importerEntry`, which causes V8 to
6443
+ // refetch ONLY those modules. Everything else stays
6444
+ // hot in the registry.
6445
+ //
6446
+ // Invariants enforced by `collectAngularEvictionUrls`:
6447
+ // - Always includes the changed file (so the new source
6448
+ // is fetched).
6449
+ // - Always includes `importerEntry` (so re-import
6450
+ // re-evaluates).
6451
+ // - Excludes node_modules (vendor packages are stable).
6452
+ // - Excludes virtual / runtime-graph-excluded ids.
6453
+ // - Origin-prefixed: `http://host:port/ns/m/<rel>`.
6454
+ let evictPaths = [];
6455
+ try {
6456
+ evictPaths = collectAngularEvictionUrls({
6457
+ file,
6458
+ hotUpdateRoots: angularHotUpdateRoots,
6459
+ transitiveImporters,
6460
+ projectRoot: root,
6461
+ origin,
6462
+ bootstrapEntry: bootstrapEntryRel,
6463
+ });
6464
+ }
6465
+ catch (error) {
6466
+ if (verbose) {
6467
+ console.warn('[ns-hmr-diag][server] eviction set computation failed', error);
6468
+ }
6469
+ }
6470
+ if (verbose) {
6471
+ try {
6472
+ const tsRel = rel.replace(/\.(html|htm)$/i, '.ts');
6473
+ const jsRel = rel.replace(/\.(html|htm)$/i, '.js');
6474
+ const containsRelatedTs = evictPaths.some((u) => u.endsWith(tsRel));
6475
+ const containsRelatedJs = evictPaths.some((u) => u.endsWith(jsRel));
6476
+ const sample = evictPaths.slice(0, 32);
6477
+ console.info(`[ns-hmr-diag][server] evict-set count=${evictPaths.length} importerEntry=${bootstrapEntryRel ?? '(none)'} containsRelatedTs=${containsRelatedTs} containsRelatedJs=${containsRelatedJs} firstN=`, sample);
6478
+ if (evictPaths.length > sample.length) {
6479
+ console.info(`[ns-hmr-diag][server] evict-set hidden=${evictPaths.length - sample.length} (showed first ${sample.length})`);
6480
+ }
6481
+ }
6482
+ catch { }
6483
+ }
5108
6484
  const msg = {
5109
6485
  type: 'ns:angular-update',
5110
6486
  origin,
5111
6487
  path: rel,
6488
+ version: graphVersion,
5112
6489
  timestamp: Date.now(),
6490
+ evictPaths,
6491
+ importerEntry: bootstrapEntryRel,
5113
6492
  };
6493
+ if (verbose) {
6494
+ console.log('[hmr-ws][angular] broadcasting update', Array.from(wss.clients || []).map((client) => ({
6495
+ role: getHmrSocketRole(client),
6496
+ readyState: client.readyState,
6497
+ openState: client.OPEN,
6498
+ })));
6499
+ }
5114
6500
  wss.clients.forEach((client) => {
5115
- if (client.readyState === client.OPEN) {
6501
+ if (isSocketClientOpen(client)) {
5116
6502
  client.send(JSON.stringify(msg));
6503
+ updateMetrics.recipients += 1;
5117
6504
  }
5118
6505
  });
5119
6506
  }
5120
6507
  catch (error) {
5121
6508
  console.warn('[hmr-ws][angular] update failed:', error);
5122
6509
  }
6510
+ emitHmrUpdateSummary();
6511
+ if (shouldSuppressDefaultViteHotUpdate({ flavor: ACTIVE_STRATEGY.flavor, file })) {
6512
+ return [];
6513
+ }
5123
6514
  return;
5124
6515
  }
5125
6516
  // TypeScript flavor: emit generic graph delta for app XML/TS/style changes
5126
6517
  if (ACTIVE_STRATEGY.flavor === 'typescript') {
6518
+ updateMetrics.tAfterFramework = Date.now();
5127
6519
  try {
5128
6520
  const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
5129
6521
  if (verbose)
@@ -5131,15 +6523,52 @@ export const piniaSymbol = p.piniaSymbol;
5131
6523
  // Treat the changed file itself as a graph module with no deps. We only
5132
6524
  // care that its hash/identity changes so the client sees a delta and can
5133
6525
  // perform a TS root reset. Code is not used for execution here.
5134
- upsertGraphModule(rel, '', []);
5135
- const gm = graph.get(normalizeGraphId(rel));
5136
- if (gm)
5137
- emitDelta([gm], []);
6526
+ upsertGraphModule(rel, '', [], { emitDeltaOnInsert: true });
5138
6527
  }
5139
6528
  catch (e) {
5140
6529
  if (verbose)
5141
6530
  console.warn('[hmr-ws][ts] failed to emit delta for', file, e);
5142
6531
  }
6532
+ emitHmrUpdateSummary();
6533
+ return;
6534
+ }
6535
+ // Solid flavor: emit graph delta for app TSX/TS/JSX file changes.
6536
+ // The common graph-update block above (moduleGraph lookup) may have
6537
+ // already emitted a delta if the file was in Vite's module graph.
6538
+ // This handler ensures a delta is emitted even if the module wasn't
6539
+ // found (e.g. new file, or moduleGraph mismatch), and provides
6540
+ // Solid-specific logging. The client-side processQueue handles
6541
+ // propagation from non-component .ts files to .tsx component boundaries.
6542
+ if (ACTIVE_STRATEGY.flavor === 'solid') {
6543
+ const isSolidFile = /\.(tsx?|jsx?)$/i.test(file);
6544
+ if (!isSolidFile)
6545
+ return;
6546
+ updateMetrics.tAfterFramework = Date.now();
6547
+ try {
6548
+ const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
6549
+ if (verbose)
6550
+ console.log('[hmr-ws][solid] app file hot update', { file, rel });
6551
+ // If the common block already upserted (hash changed), this will
6552
+ // detect unchanged hash and no-op. If the common block missed it
6553
+ // (module not in Vite's graph), this forces the delta emission.
6554
+ const normalizedId = normalizeGraphId(rel);
6555
+ const existing = graph.get(normalizedId);
6556
+ if (!existing) {
6557
+ // Module not in graph yet — force upsert with timestamp-based
6558
+ // hash so the client sees a change.
6559
+ upsertGraphModule(rel, `/* solid-hmr ${Date.now()} */`, [], { emitDeltaOnInsert: true });
6560
+ }
6561
+ // Log what we're sending so devs can trace the flow on the server side.
6562
+ if (verbose) {
6563
+ const gm = graph.get(normalizedId);
6564
+ console.log('[hmr-ws][solid] delta module', { id: gm?.id, hash: gm?.hash });
6565
+ }
6566
+ }
6567
+ catch (e) {
6568
+ if (verbose)
6569
+ console.warn('[hmr-ws][solid] failed to handle hot update for', file, e);
6570
+ }
6571
+ emitHmrUpdateSummary();
5143
6572
  return;
5144
6573
  }
5145
6574
  // Handle .vue file updates
@@ -5148,7 +6577,8 @@ export const piniaSymbol = p.piniaSymbol;
5148
6577
  console.log('[hmr-ws] Not a .vue file, skipping');
5149
6578
  return;
5150
6579
  }
5151
- console.log('[hmr-ws] Processing .vue file update...');
6580
+ if (verbose)
6581
+ console.log('[hmr-ws] Processing .vue file update...');
5152
6582
  try {
5153
6583
  const root = server.config.root || process.cwd();
5154
6584
  let rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
@@ -5249,6 +6679,7 @@ export const piniaSymbol = p.piniaSymbol;
5249
6679
  // Rewrite ONLY .vue imports (everything else is now inlined)
5250
6680
  const projectRoot = server.config.root || process.cwd();
5251
6681
  code = rewriteImports(code, rel, sfcFileMap, depFileMap, projectRoot, opts.verbose, undefined);
6682
+ upsertGraphModule(rel, code, [...deps, ...vueDeps]);
5252
6683
  // Add HMR runtime prelude (CRITICAL for runtime)
5253
6684
  const hmrPrelude = `
5254
6685
  // Embedded HMR Runtime for NativeScript runtime
@@ -5291,7 +6722,7 @@ if (typeof __VUE_HMR_RUNTIME__ === 'undefined') {
5291
6722
  if (typeof spec === 'string' && /^(?:https?:)\/\//.test(spec)) {
5292
6723
  const err = new Error('[ns-hmr][require-guard] require of URL: ' + spec + ' via ' + label);
5293
6724
  const stack = err.stack || '';
5294
- try { console.error(err.message + '\n' + stack); } catch {}
6725
+ console.error(err.message + '\n' + stack);
5295
6726
  try { g.__NS_REQUIRE_GUARD_LAST__ = { spec, stack, label, ts: Date.now() }; } catch {}
5296
6727
  }
5297
6728
  } catch {}
@@ -5324,7 +6755,7 @@ if (typeof __VUE_HMR_RUNTIME__ === 'undefined') {
5324
6755
  version: graphVersion,
5325
6756
  };
5326
6757
  wss.clients.forEach((client) => {
5327
- if (client.readyState === client.OPEN) {
6758
+ if (isSocketClientOpen(client)) {
5328
6759
  client.send(JSON.stringify(registryUpdateMsg));
5329
6760
  }
5330
6761
  });
@@ -5424,6 +6855,10 @@ if (typeof __VUE_HMR_RUNTIME__ === 'undefined') {
5424
6855
  console.warn('[hmr-ws] HMR update failed:', error);
5425
6856
  console.error(error);
5426
6857
  }
6858
+ // Vue path emits update summary at the end of the function so
6859
+ // every framework branch gets exactly one log line. Idempotent
6860
+ // — if any branch already emitted, this is a no-op.
6861
+ emitHmrUpdateSummary();
5427
6862
  // CRITICAL: Return empty array to prevent Vite's default HMR
5428
6863
  return [];
5429
6864
  },
@@ -5495,4 +6930,6 @@ function getServerOrigin(server) {
5495
6930
  // Test-only export: allow unit tests to run the sanitizer on snippets without booting a server
5496
6931
  // Safe in production builds; this is a named export that tests can import explicitly.
5497
6932
  export const __test_processCodeForDevice = processCodeForDevice;
6933
+ export const __test_resolveVendorRouting = resolveVendorRouting;
6934
+ export const __test_getBlockedDeviceNodeModulesReason = getBlockedDeviceNodeModulesReason;
5498
6935
  //# sourceMappingURL=websocket.js.map