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

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 +27 -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 +375 -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 +767 -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 +283 -12
  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 +2492 -798
  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
193
  }
81
- function isNativeScriptCoreModule(spec) {
82
- return /^(?:@nativescript[\/_-]core|@nativescript\/core)(?:\b|\/)/i.test(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+/, '')}`;
83
217
  }
84
- function isNativeScriptPluginModule(spec) {
85
- return /^@nativescript\//i.test(spec || '') && !isNativeScriptCoreModule(spec || '');
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');
86
226
  }
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;
114
- }
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) {
@@ -1825,16 +2002,50 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1825
2002
  // This can surface from upstream alias mishaps; mapping it here avoids device-side
1826
2003
  // "instantiate failed @" errors.
1827
2004
  if (spec === '@') {
1828
- const stub = `/ns/m/__invalid_at__.mjs`;
1829
- if (verbose) {
1830
- try {
1831
- console.warn(`[rewrite] mapped bare '@' spec to stub: ${stub}`);
1832
- }
1833
- catch { }
2005
+ const stub = `/ns/m/__invalid_at__.mjs`;
2006
+ if (verbose) {
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);
2243
2566
  }
2244
- emitDelta([gm], []);
2567
+ if (shouldBroadcastGraphUpsertDelta(classification, options?.emitDeltaOnInsert === true, options?.broadcastDelta !== false)) {
2568
+ emitDelta([gm], []);
2569
+ }
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,139 @@ 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 2b (NS HMR escape hatch): expose `source` on the proxy
3393
+ // itself via a non-enumerable, well-known symbol-ish key. This lets
3394
+ // our HMR remount path bypass solid-refresh's proxy chain and call
3395
+ // the freshly-patched underlying Home function directly. solid-refresh's
3396
+ // proxy wraps everything in nested createMemo's, which under
3397
+ // universal-renderer + nested-context (TanStack RouterContextProvider)
3398
+ // causes accumulating zombie memos that all subscribe to M_initial.source
3399
+ // — the visible symptom is "every other save applies".
3400
+ //
3401
+ // With this hatch we can do `proxy.__$$ns_resolve()` to obtain the
3402
+ // current underlying component (e.g. M_initial.proxy after first save,
3403
+ // or the actual Home function on the deepest hop) and then call
3404
+ // untrack(() => extract until we reach the actual function), then mount
3405
+ // against THAT function — no HMRComp memo proxy chain at the page level.
3406
+ // NS HMR escape hatch: stash `source` on the HMRComp function
3407
+ // itself AND short-circuit the Proxy's `get` handler so that
3408
+ // `proxy.__$ns_resolveSource` returns our exposed function.
3409
+ // Without the get-handler short-circuit, accessing the property
3410
+ // on the Proxy goes through `return source()[property]` — which
3411
+ // asks the *current source value* (which may itself be another
3412
+ // solid-refresh proxy or the underlying user function) for the
3413
+ // property. The user function doesn't have `__$ns_resolveSource`,
3414
+ // and a chained proxy would re-enter its own get handler. Either
3415
+ // way we end up with `undefined` at the page-level remount and
3416
+ // can't unwrap.
3417
+ //
3418
+ // NOTE: `$$` in String.prototype.replace replacement is treated
3419
+ // as a literal `$`. We use a single `$` to avoid that footgun.
3420
+ // Match only the unique opening fragment to avoid getting tripped up
3421
+ // by whitespace differences after the AST normalizer ran.
3422
+ const newProxyMarker = `if (property === 'location' || property === 'name') {`;
3423
+ if (patchedCode.includes(newProxyMarker)) {
3424
+ // 1. Inject `__$ns_resolveSource` as a property on the HMRComp
3425
+ // function itself (so its closure captures `source`).
3426
+ // CRITICAL: assign at module-eval time (right after HMRComp
3427
+ // is defined / before `return new Proxy(...)`), NOT inside
3428
+ // the HMRComp body — the body only runs when the proxy is
3429
+ // called, so before first call the property is undefined
3430
+ // and the page-level remount unwrap finds nothing.
3431
+ const setupMarker = `setComponentProperty(HMRComp, 'name', refreshName);`;
3432
+ patchedCode = patchedCode.replace(setupMarker, `HMRComp.__$ns_resolveSource = function() { return source(); }; ${setupMarker}`);
3433
+ // 2. Make the Proxy `get` handler short-circuit our property
3434
+ // so callers can do `proxy.__$ns_resolveSource()` without
3435
+ // going through `source()[property]` (which would unwrap one
3436
+ // hop early or reach the user function which doesn't have it).
3437
+ patchedCode = patchedCode.replace(newProxyMarker, `if (property === '__$ns_resolveSource') { return HMRComp.__$ns_resolveSource; } ${newProxyMarker}`);
3438
+ if (verbose) {
3439
+ console.log('[hmr-ws][solid] exposed __$ns_resolveSource on createProxy for NS HMR escape hatch');
3440
+ }
3441
+ }
3442
+ // Patch 3: Inline patchRegistry call so updates apply immediately
3443
+ // on module re-evaluation (accept callbacks are not invoked by the HMR client).
3444
+ //
3445
+ // The injected diagnostic logs are gated on
3446
+ // `globalThis.__NS_ENV_VERBOSE__` so they're silent in
3447
+ // normal use but resurface immediately when the user
3448
+ // re-runs with verbose logging enabled. The flag is
3449
+ // seeded by `mainEntryPlugin` from the same `verbose`
3450
+ // option that drives this server-side log gating.
3451
+ // Without these the visible HMR signal is just "did it
3452
+ // apply" — with them, devs can answer "did `hot.data`
3453
+ // persist", "did `patchRegistry` actually swap the
3454
+ // proxy's signal source", and "did the registry
3455
+ // component count change" without reaching for an
3456
+ // inspector.
3457
+ const marker = 'hot.data[SOLID_REFRESH] = hot.data[SOLID_REFRESH] || registry;';
3458
+ if (patchedCode.includes(marker)) {
3459
+ const patchCode = [
3460
+ `var __nsRefreshVerbose = (typeof globalThis !== 'undefined') && !!globalThis.__NS_ENV_VERBOSE__;`,
3461
+ `if (__nsRefreshVerbose) console.log('[ns-hmr][solid-refresh][$$refreshESM] hot.data has SOLID_REFRESH=', !!(hot.data && hot.data[SOLID_REFRESH]), 'registry components=', registry.components ? registry.components.size : 0);`,
3462
+ `if (hot.data[SOLID_REFRESH]) {`,
3463
+ ` var __nsOldComponents = hot.data[SOLID_REFRESH].components ? hot.data[SOLID_REFRESH].components.size : 0;`,
3464
+ ` var __nsShouldInvalidate = patchRegistry(hot.data[SOLID_REFRESH], registry);`,
3465
+ ` if (__nsRefreshVerbose) console.log('[ns-hmr][solid-refresh][$$refreshESM] patched: oldComponents=', __nsOldComponents, 'newComponents=', registry.components ? registry.components.size : 0, 'shouldInvalidate=', __nsShouldInvalidate);`,
3466
+ `} else {`,
3467
+ ` if (__nsRefreshVerbose) console.log('[ns-hmr][solid-refresh][$$refreshESM] first load — no prior registry to patch');`,
3468
+ `}`,
3469
+ ].join('\n ');
3470
+ patchedCode = patchedCode.replace(marker, `${patchCode}\n ${marker}`);
3471
+ if (verbose) {
3472
+ console.log('[hmr-ws][solid] added inline patchRegistry (with diagnostics) for NativeScript HMR');
3473
+ }
2705
3474
  }
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;
3475
+ // Work on a copy to avoid mutating Vite's cached TransformResult
3476
+ transformed = { ...transformed, code: patchedCode };
3477
+ }
3478
+ }
3479
+ }
3480
+ catch { }
3481
+ // NOTE: Path-based cache busting for /ns/m/* imports is applied in the
3482
+ // finalize step below (after rewriteImports adds the /ns/m/ prefix).
3483
+ // The block here only handles TypeScript-specific graph population.
3484
+ try {
3485
+ if (transformed?.code) {
3486
+ const code = transformed.code;
2718
3487
  // TypeScript-specific graph population: when TS flavor is active
2719
3488
  // and this is an application module under the virtual app root,
2720
3489
  // upsert it into the HMR graph so ns:hmr-full-graph is non-empty.
@@ -2723,15 +3492,14 @@ function createHmrWebSocketPlugin(opts) {
2723
3492
  const id = (resolvedCandidate || spec).replace(/[?#].*$/, '');
2724
3493
  // Only track app modules (under APP_VIRTUAL_WITH_SLASH) and ts/js/tsx/jsx/mjs.
2725
3494
  const isApp = id.startsWith(APP_VIRTUAL_WITH_SLASH) || id.startsWith('/app/');
2726
- if (isApp && /\.(ts|tsx|js|jsx|mjs|mts|cts)$/i.test(id)) {
3495
+ if (isApp && /\.(ts|tsx|js|jsx|mjs|mts|cts)$/i.test(id) && !isRuntimeGraphExcludedPath(id)) {
2727
3496
  const deps = Array.from(collectImportDependencies(code, id));
2728
3497
  if (verbose) {
2729
- try {
2730
- console.log('[hmr-ws][ts-graph] candidate', { id, depsCount: deps.length });
2731
- }
2732
- catch { }
3498
+ console.log('[hmr-ws][ts-graph] candidate', { id, depsCount: deps.length });
2733
3499
  }
2734
- upsertGraphModule(id, code, deps);
3500
+ // Serve-time warm-up: no live edit happened, so don't bump
3501
+ // graphVersion.
3502
+ upsertGraphModule(id, code, deps, { bumpVersion: false });
2735
3503
  }
2736
3504
  }
2737
3505
  }
@@ -2843,7 +3611,7 @@ export const piniaSymbol = p.piniaSymbol;
2843
3611
  if (!transformed?.code) {
2844
3612
  // Emit a module that throws with context for easier on-device debugging
2845
3613
  try {
2846
- const tried = Array.from(new Set(candidates)).slice(0, 12);
3614
+ const tried = Array.from(new Set(transformCandidates.length > 0 ? transformCandidates : candidates)).slice(0, 12);
2847
3615
  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
3616
  res.statusCode = 404;
2849
3617
  res.end(out);
@@ -2860,8 +3628,33 @@ export const piniaSymbol = p.piniaSymbol;
2860
3628
  // Prepend guard to capture any URL-based require attempts
2861
3629
  code = REQUIRE_GUARD_SNIPPET + code;
2862
3630
  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));
3631
+ const isNodeMod = /(?:^|\/)node_modules\//.test(resolvedCandidate || spec || '');
3632
+ code = processCodeForDevice(code, false, true, isNodeMod, resolvedCandidate || spec);
3633
+ // Solid HMR: The NativeScript iOS/Android runtime provides import.meta.hot
3634
+ // natively (via InitializeImportMetaHot in HMRSupport.mm) with C++-backed
3635
+ // persistent hot.data that survives across module re-evaluations.
3636
+ // cleanCode() strips Vite's __vite__createHotContext assignment, which is
3637
+ // correct — the runtime's native hot context is better.
3638
+ const projectRoot = server.config?.root || process.cwd();
3639
+ const serverOrigin = getServerOrigin(server);
3640
+ if (ACTIVE_STRATEGY?.flavor === 'angular') {
3641
+ code = prepareAngularEntryForDevice(code, resolvedCandidate || spec, sfcFileMap, depFileMap, projectRoot, !!verbose, undefined, serverOrigin, true);
3642
+ }
3643
+ else {
3644
+ code = rewriteImports(code, resolvedCandidate || spec, sfcFileMap, depFileMap, projectRoot, !!verbose, undefined, serverOrigin, true);
3645
+ }
3646
+ // Expand `export * from "url"` into explicit named re-exports.
3647
+ // NativeScript's HTTP ESM loader may not propagate star-re-exports across
3648
+ // HTTP module boundaries (the namespace object gets direct exports but
3649
+ // misses re-exported names). By expanding to `export { a, b } from "url"`,
3650
+ // the engine sees explicit named exports and resolves them correctly.
3651
+ try {
3652
+ code = await expandStarExports(code, server, server.config?.root || process.cwd(), verbose, sharedTransformRequest);
3653
+ }
3654
+ catch (e) {
3655
+ if (verbose)
3656
+ console.warn('[ns/m] export* expansion failed:', e?.message);
3657
+ }
2865
3658
  // Dedupe any /ns/rt named imports that duplicate destructured bindings off default /ns/rt
2866
3659
  try {
2867
3660
  code = dedupeRtNamedImportsAgainstDestructures(code);
@@ -2888,6 +3681,28 @@ export const piniaSymbol = p.piniaSymbol;
2888
3681
  }
2889
3682
  }
2890
3683
  catch { }
3684
+ // Final pass: deduplicate/resolve any bare-specifier imports that slipped
3685
+ // through the pipeline (e.g., extracted from JSDoc comments by import-splitting
3686
+ // regexes, or injected by the Angular linker on already-resolved code).
3687
+ try {
3688
+ code = deduplicateLinkerImports(code);
3689
+ }
3690
+ catch { }
3691
+ // CJS/UMD wrapping: if a module uses module.exports but has no ESM export default,
3692
+ // wrap it with CJS shims so the device HTTP ESM loader can consume it.
3693
+ // This handles npm packages that use CommonJS but aren't pre-bundled by Vite.
3694
+ //
3695
+ // Key constraints this must handle:
3696
+ // - CJS modules often declare local vars with the same names as their exports
3697
+ // (e.g. `function createLTTB() {...}; exports.createLTTB = createLTTB;`)
3698
+ // so `export var { createLTTB }` would cause a duplicate declaration.
3699
+ // - UMD modules reference `this` at top level (undefined in ESM) but
3700
+ // typically fall back to `self` or `globalThis`.
3701
+ // - `module`, `exports` must be shims since they don't exist in ESM.
3702
+ try {
3703
+ code = wrapCommonJsModuleForDevice(code, resolvedCandidate || null);
3704
+ }
3705
+ catch { }
2891
3706
  try {
2892
3707
  assertNoOptimizedArtifacts(code, `NS M ${resolvedCandidate || spec}`);
2893
3708
  }
@@ -2907,35 +3722,81 @@ export const piniaSymbol = p.piniaSymbol;
2907
3722
  }
2908
3723
  }
2909
3724
  catch { }
3725
+ // `/ns/rt` and `/ns/core` URL versioning.
3726
+ //
3727
+ // Older versions of the server emitted `/ns/rt/<ver>` and
3728
+ // `/ns/core/<ver>` so V8's HTTP module cache would see a
3729
+ // fresh URL on every save. The runtime canonicalizer
3730
+ // (`CanonicalizeHttpUrlKey` in HMRSupport.mm) collapses
3731
+ // these version segments to the bare `/ns/rt` and
3732
+ // `/ns/core` keys before lookup, so V8 actually saw a
3733
+ // single cache entry — but the server was doing extra
3734
+ // work to inject a version segment that the runtime then
3735
+ // immediately stripped. Now that the runtime supports
3736
+ // explicit eviction (and these bridge endpoints don't
3737
+ // change at HMR time anyway), the version segment is
3738
+ // purely vestigial.
3739
+ //
3740
+ // Rather than rip the helpers out (which would touch
3741
+ // every ensureVersionedImports caller and risk bumping
3742
+ // older runtimes), we keep them but pass `verNum=0`. The
3743
+ // helpers still normalize URL shape (strip the absolute
3744
+ // origin prefix when present) but emit a stable
3745
+ // `/ns/rt/0` / `/ns/core/0` URL — which collapses to
3746
+ // `/ns/rt` / `/ns/core` in the runtime.
2910
3747
  try {
2911
- const verNum = Number(forcedVer || graphVersion || 0);
3748
+ const verNum = 0;
2912
3749
  code = ensureVersionedRtImports(code, getServerOrigin(server), verNum);
2913
3750
  code = ACTIVE_STRATEGY.ensureVersionedImports(code, getServerOrigin(server), verNum);
2914
3751
  code = ensureVersionedCoreImports(code, getServerOrigin(server), verNum);
2915
3752
  }
2916
3753
  catch { }
2917
- // Finalize: also stamp all internal /ns/m imports with ?v=<ver> after all rewrites
3754
+ // `/ns/m` URL finalize step.
3755
+ //
3756
+ // `rewriteNsMImportPathForHmr` is a canonicalizer: it
3757
+ // strips legacy `__ns_hmr__/<tag>/` segments and adds
3758
+ // `__ns_boot__/b1/` only for boot-tagged requests. The
3759
+ // `ver` parameter is preserved on the signature for API
3760
+ // compatibility but is ignored for app modules (cache
3761
+ // busting is driven by `__nsInvalidateModules`, not URL
3762
+ // versioning). We pass `'v0'` as a stable placeholder —
3763
+ // the canonicalizer emits the same URL regardless of
3764
+ // this value, but a constant placeholder makes the
3765
+ // contract explicit.
3766
+ //
3767
+ // SFC URLs (line below, `/ns/sfc/${verTag}/...`) still
3768
+ // embed a version because the Vue SFC pathway does not
3769
+ // yet have an eviction protocol. The runtime
3770
+ // canonicalizer does NOT strip `/ns/sfc/<ver>/`, so Vue
3771
+ // users still see per-save SFC re-fetches — that's a
3772
+ // known follow-up.
2918
3773
  try {
2919
- const ver = String(forcedVer || graphVersion || 0);
3774
+ const verTag = (() => {
3775
+ const numeric = getNumericServeVersionTag(forcedVer, Number(graphVersion || 0));
3776
+ return numeric > 0 ? `v${numeric}` : 'v0';
3777
+ })();
2920
3778
  const origin = getServerOrigin(server);
3779
+ const rewritePath = (p) => rewriteNsMImportPathForHmr(p, 'v0', bootTaggedRequest);
3780
+ // /ns/m URL forms — all collapse to canonical stable
3781
+ // URLs via the Phase 3a rewriter.
2921
3782
  // 1) Static imports: import ... from "/ns/m/..."
2922
- code = code.replace(/(from\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, `$1$2?v=${ver}$3`);
3783
+ code = code.replace(/(from\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
2923
3784
  // 2) Side-effect imports: import "/ns/m/..."
2924
- code = code.replace(/(import\s*(?!\()\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, `$1$2?v=${ver}$3`);
3785
+ code = code.replace(/(import\s*(?!\()\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
2925
3786
  // 3) Dynamic imports: import("/ns/m/...")
2926
- code = code.replace(/(import\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*\))/g, `$1$2?v=${ver}$3`);
3787
+ code = code.replace(/(import\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*\))/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
2927
3788
  // 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`);
3789
+ 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
3790
  // 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/..."
3791
+ 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}`);
3792
+ // 6) Force absolute HTTP for new URL('/ns/m/...', import.meta.url).href → canonical stable URL.
2932
3793
  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}`)}`);
3794
+ 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
3795
  }
2935
3796
  catch { }
2936
- // 7) Also fix SFC new URL('/ns/sfc/...', import.meta.url).href "${origin}/ns/sfc/<ver>/..."
3797
+ // 7) SFC URLs (Vue) — still versioned. See header comment.
2937
3798
  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}`)}`);
3799
+ 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
3800
  }
2940
3801
  catch { }
2941
3802
  }
@@ -2946,13 +3807,25 @@ export const piniaSymbol = p.piniaSymbol;
2946
3807
  code = ensureDestructureCoreImports(code);
2947
3808
  }
2948
3809
  catch { }
3810
+ // Boot-time module graph progress: while the app is still replacing the
3811
+ // placeholder, emit lightweight progress updates as /ns/m modules begin
3812
+ // evaluating. This keeps the overlay moving during large initial graphs.
3813
+ try {
3814
+ if (bootTaggedRequest) {
3815
+ const bootModuleLabel = String(spec || '').replace(/\\/g, '/');
3816
+ const bootProgressSnippet = buildBootProgressSnippet(bootModuleLabel);
3817
+ code = bootProgressSnippet + code;
3818
+ code = hoistTopLevelStaticImports(code);
3819
+ }
3820
+ }
3821
+ catch { }
2949
3822
  // Dev-only: link-check static imports to surface missing bindings early
2950
3823
  try {
2951
3824
  const devCheck = process.env.NODE_ENV !== 'production';
2952
3825
  if (devCheck) {
2953
3826
  const ast = babelParse(code, {
2954
3827
  sourceType: 'module',
2955
- plugins: ['typescript', 'importMeta'],
3828
+ plugins: MODULE_IMPORT_ANALYSIS_PLUGINS,
2956
3829
  });
2957
3830
  const imports = [];
2958
3831
  babelTraverse(ast, {
@@ -3036,6 +3909,15 @@ export const piniaSymbol = p.piniaSymbol;
3036
3909
  continue;
3037
3910
  const hasDefault = /\bexport\s+default\b/.test(targetCode) || /export\s*\{\s*default\s*(?:as\s*default)?\s*\}/.test(targetCode);
3038
3911
  if (!hasDefault) {
3912
+ // CJS/UMD modules won't have `export default` — they get CJS-wrapped
3913
+ // by the serving pipeline. Only warn, don't fatally block the importer.
3914
+ const hasCjsPattern = /\bmodule\s*\.\s*exports\b/.test(targetCode) || /\bexports\s*\.\s*\w/.test(targetCode);
3915
+ if (hasCjsPattern) {
3916
+ if (verbose) {
3917
+ console.warn(`[ns:m][link-check] CJS module without export default: ${u.pathname} (will be CJS-wrapped at serve time)`);
3918
+ }
3919
+ continue;
3920
+ }
3039
3921
  const msg = `[link-check] Missing default export in ${u.pathname}${u.search} (imported by ${resolvedCandidate || spec})`;
3040
3922
  // Emit a module that throws to surface the exact offender
3041
3923
  res.statusCode = 200;
@@ -3047,19 +3929,15 @@ export const piniaSymbol = p.piniaSymbol;
3047
3929
  }
3048
3930
  }
3049
3931
  catch (eLC) {
3050
- try {
3932
+ if (verbose) {
3051
3933
  console.warn('[ns:m][link-check] failed', eLC?.message || eLC);
3052
3934
  }
3053
- catch { }
3054
3935
  }
3055
3936
  res.statusCode = 200;
3056
3937
  res.end(code);
3057
3938
  }
3058
3939
  catch (e) {
3059
- try {
3060
- console.warn('[sfc-asm] error serving', req.url, e && e.message ? e.message : e);
3061
- }
3062
- catch { }
3940
+ console.warn('[sfc-asm] error serving', req.url, e && e.message ? e.message : e);
3063
3941
  res.statusCode = 500;
3064
3942
  res.end('export {}\n');
3065
3943
  }
@@ -3092,8 +3970,10 @@ export const piniaSymbol = p.piniaSymbol;
3092
3970
  `let __cached_rt = null;\n` +
3093
3971
  `let __cached_vm = null;\n` +
3094
3972
  `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` +
3973
+ // One-shot evaluation marker to confirm the bridge is executed on
3974
+ // device. Gated on __NS_ENV_VERBOSE__ so it stays silent unless
3975
+ // the developer opts in via NS_VITE_VERBOSE / VITE_DEBUG_LOGS.
3976
+ `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
3977
  `function __ensure(){\n` +
3098
3978
  ` if (__cached_rt) return __cached_rt;\n` +
3099
3979
  ` let vm = null;\n` +
@@ -3185,7 +4065,7 @@ export const piniaSymbol = p.piniaSymbol;
3185
4065
  `export const vShow = (__ensure().vShow);\n` +
3186
4066
  `export const createApp = (...a) => (__ensure().createApp)(...a);\n` +
3187
4067
  `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` +
4068
+ `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
4069
  `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
4070
  `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
4071
  `export default {\n` +
@@ -3247,35 +4127,262 @@ export const piniaSymbol = p.piniaSymbol;
3247
4127
  return next();
3248
4128
  }
3249
4129
  });
4130
+ // 2.5.1) Catch-all redirect for stray /node_modules/@nativescript/core/*
4131
+ // requests — route them to the /ns/core bridge so they get the same
4132
+ // __DEV__/__IOS__ preamble and specifier rewriting. Without this,
4133
+ // Vite's default /node_modules/ handler serves the raw file, which
4134
+ // references bare __DEV__ and crashes at module eval.
4135
+ server.middlewares.use((req, _res, next) => {
4136
+ try {
4137
+ const urlObj = new URL(req.url || '', 'http://localhost');
4138
+ const coreNmPrefix = '/node_modules/@nativescript/core';
4139
+ if (!urlObj.pathname.startsWith(coreNmPrefix))
4140
+ return next();
4141
+ const sub = urlObj.pathname.slice(coreNmPrefix.length).replace(/^\/+/, '');
4142
+ if (sub === '' || sub === 'index.js' || sub === 'index') {
4143
+ req.url = `/ns/core`;
4144
+ }
4145
+ else {
4146
+ req.url = `/ns/core/${sub}`;
4147
+ }
4148
+ return next();
4149
+ }
4150
+ catch {
4151
+ return next();
4152
+ }
4153
+ });
3250
4154
  // 2.6) ESM bridge for @nativescript/core: GET /ns/core[/<ver>][?p=sub/path]
4155
+ //
4156
+ // Since bundle.mjs no longer bundles @nativescript/core (it is
4157
+ // declared external in the rolldown config under HMR), this
4158
+ // endpoint is the ONE place core is evaluated. Every consumer —
4159
+ // bundle.mjs's own `@nativescript/core*` imports (resolved to
4160
+ // full HTTP URLs in the entry virtual module), externalized
4161
+ // vendor packages, HTTP-served app modules — all end up here.
4162
+ // No more proxy bridge, no enumeration, no namespace detection,
4163
+ // no prototype-polluted maps. We just serve Vite's authoritative
4164
+ // transformed module content.
4165
+ //
4166
+ // iOS caches by URL path, so each unique URL is evaluated exactly
4167
+ // once per app lifetime. Every class identity is shared, every
4168
+ // `register()` side effect runs once, every `Application` reference
4169
+ // is the same iosApp singleton. The entire class of "does not
4170
+ // provide an export named X" and "Cannot redefine property" errors
4171
+ // is eliminated by construction.
3251
4172
  server.middlewares.use(async (req, res, next) => {
3252
4173
  try {
3253
4174
  const urlObj = new URL(req.url || '', 'http://localhost');
3254
- if (!(urlObj.pathname === '/ns/core' || /^\/ns\/core\/[\d]+$/.test(urlObj.pathname)))
4175
+ const coreRequest = parseCoreBridgeRequest(urlObj.pathname, urlObj.searchParams, Number(graphVersion || 0));
4176
+ if (!coreRequest)
3255
4177
  return next();
3256
4178
  res.setHeader('Access-Control-Allow-Origin', '*');
3257
4179
  res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
3258
4180
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
3259
4181
  res.setHeader('Pragma', 'no-cache');
3260
4182
  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`;
4183
+ const { normalizedSub, sub, ver } = coreRequest;
4184
+ const resolveModuleId = async (moduleId) => {
4185
+ const resolved = await server.pluginContainer?.resolveId?.(moduleId, undefined);
4186
+ return typeof resolved === 'string' ? resolved : resolved?.id || null;
4187
+ };
4188
+ let modulePath = null;
4189
+ if (sub) {
4190
+ const resolvedSubpath = normalizedSub || sub;
4191
+ modulePath = await resolveRuntimeCoreModulePath(resolvedSubpath, resolveModuleId);
4192
+ if (!modulePath) {
4193
+ modulePath = `/node_modules/@nativescript/core/${resolvedSubpath}`;
4194
+ }
4195
+ }
4196
+ else {
4197
+ modulePath = (await resolveModuleId('@nativescript/core')) || '/node_modules/@nativescript/core/index.js';
4198
+ }
4199
+ const transformed = await sharedTransformRequest(modulePath);
4200
+ if (!transformed?.code) {
4201
+ res.statusCode = 500;
4202
+ res.setHeader('Content-Type', 'application/json');
4203
+ res.end(JSON.stringify({ error: 'core-transform-failed', modulePath, sub: sub || null }));
4204
+ return;
4205
+ }
4206
+ // Vite's transform output references module IDs with /@fs,
4207
+ // relative specifiers, or absolute project paths. Rewrite
4208
+ // those to URLs iOS can fetch over HTTP.
4209
+ let rewritten = rewriteSpecifiersForDevice(transformed.code, getServerOrigin(server), Number(ver));
4210
+ // Invariant D (CJS/ESM interop shape) — EXPORT-SIDE fix.
4211
+ //
4212
+ // `@nativescript/core/index.js` declares namespace
4213
+ // re-exports like:
4214
+ // export * as Utils from './utils';
4215
+ // The ES spec says these produce Module Namespace Objects
4216
+ // with [[Prototype]] = null. Consumers that reach them
4217
+ // via direct ESM import — `import { Utils } from
4218
+ // '@nativescript/core'` — get the raw null-proto value,
4219
+ // bypassing any CJS `require` shim we install. Most
4220
+ // consumers tolerate this, but CJS-style interop (most
4221
+ // notably zone.js's `patchMethod`) calls
4222
+ // `hasOwnProperty` on the target and crashes on
4223
+ // null-proto.
4224
+ //
4225
+ // We rewrite the re-export to a shape-wrapped const:
4226
+ // import * as __ns_re_Utils__ from './utils';
4227
+ // export const Utils = __NS_CJS_SHAPE__(__ns_re_Utils__);
4228
+ // so the EXPORT itself is a plain object — visible to
4229
+ // both ESM and CJS consumers consistently.
4230
+ //
4231
+ // We only pay the rewrite cost when the module actually
4232
+ // contains namespace re-exports (i.e., the main
4233
+ // `index.js`). Subpaths (`/utils`, `/http`, …) don't
4234
+ // re-export via `export * as`; they expose named
4235
+ // exports directly, so the rewrite is a no-op on them.
4236
+ if (hasNamespaceReExport(rewritten)) {
4237
+ rewritten = rewriteNamespaceReExportsForShape(rewritten);
4238
+ }
4239
+ // Prepend the build-time defines (__DEV__, __IOS__, __ANDROID__,
4240
+ // __APPLE__, …) that @nativescript/core source references directly.
4241
+ // Vite's `define` config substitutes these in user-code transforms but
4242
+ // skips node_modules by default; since core is now external and served
4243
+ // over HTTP from this endpoint, the served transformed code still has
4244
+ // bare identifiers like `if (__DEV__) …`. Without these consts, V8
4245
+ // hits `ReferenceError: __DEV__ is not defined` at module eval because
4246
+ // globalThis.__DEV__ is set by bundle.mjs's body AFTER all static
4247
+ // imports (including these core modules) have resolved.
4248
+ //
4249
+ // We inject LITERAL boolean values based on CLI flags + dev-server
4250
+ // mode rather than reading from globalThis, so the defines are
4251
+ // resolved even before bundle.mjs's body runs.
4252
+ const __cliFlags = getCliFlags() || {};
4253
+ const __platformIsAndroid = !!__cliFlags.android;
4254
+ const __platformIsVisionOS = !!__cliFlags.visionos;
4255
+ const __platformIsIOS = !__platformIsAndroid && !__platformIsVisionOS;
4256
+ const preamble = [
4257
+ `const __ANDROID__ = ${__platformIsAndroid ? 'true' : 'false'};`,
4258
+ `const __IOS__ = ${__platformIsIOS ? 'true' : 'false'};`,
4259
+ `const __VISIONOS__ = ${__platformIsVisionOS ? 'true' : 'false'};`,
4260
+ `const __APPLE__ = __IOS__ || __VISIONOS__;`,
4261
+ `const __DEV__ = ${server.config?.mode === 'development' ? 'true' : 'false'};`,
4262
+ `const __COMMONJS__ = false;`,
4263
+ `const __NS_WEBPACK__ = false;`,
4264
+ `const __NS_ENV_VERBOSE__ = globalThis.__NS_ENV_VERBOSE__ !== undefined ? !!globalThis.__NS_ENV_VERBOSE__ : false;`,
4265
+ `const __CSS_PARSER__ = 'css-tree';`,
4266
+ `const __UI_USE_XML_PARSER__ = true;`,
4267
+ `const __UI_USE_EXTERNAL_RENDERER__ = false;`,
4268
+ `const __TEST__ = false;`,
4269
+ ].join('\n');
4270
+ // Boot-time instrumentation + module self-registration.
4271
+ //
4272
+ // - URL canonicalization: the same logical module must
4273
+ // always resolve to byte-identical URLs across every
4274
+ // emitter. The /ns/core handler records the first URL
4275
+ // seen for each canonical sub (or '' for main) in
4276
+ // `globalThis.__NS_CORE_FIRST_URL__` and fails hard on
4277
+ // mismatch so drift in any emitter surfaces
4278
+ // immediately, before the realm splits.
4279
+ // - CJS/ESM boot order: CommonJS
4280
+ // `require('@nativescript/core/...')` calls from
4281
+ // vendor install() hooks must resolve to the SAME
4282
+ // ESM namespace that ran this side-effect preamble.
4283
+ // The registration below keys the namespace object
4284
+ // under BOTH the bare specifier and the canonical
4285
+ // subpath (and raw subpath for back-compat) so the
4286
+ // vendor shim's `createRequire` and the main-entry
4287
+ // `_nsReq` hit on any lookup form.
4288
+ const rawSub = normalizedSub || sub || '';
4289
+ const canonicalSub = normalizeCoreSubCanonical(rawSub);
4290
+ const registrationKeySet = new Set();
4291
+ registrationKeySet.add(canonicalSub ? `@nativescript/core/${canonicalSub}` : '@nativescript/core');
4292
+ registrationKeySet.add(canonicalSub);
4293
+ if (rawSub && rawSub !== canonicalSub) {
4294
+ registrationKeySet.add(`@nativescript/core/${rawSub}`);
4295
+ registrationKeySet.add(rawSub);
4296
+ }
4297
+ const registrationKeys = Array.from(registrationKeySet).map((k) => JSON.stringify(k));
4298
+ const canonicalUrl = `${getServerOrigin(server)}` + (canonicalSub ? `/ns/core/${canonicalSub}` : '/ns/core');
4299
+ const instrumentationHeader = [
4300
+ `/* @nativescript/core bridge — canonical URL: ${canonicalUrl} */`,
4301
+ `try { if (typeof globalThis !== 'undefined') {`,
4302
+ ` const __nsFirst = globalThis.__NS_CORE_FIRST_URL__ || (globalThis.__NS_CORE_FIRST_URL__ = Object.create(null));`,
4303
+ ` const __nsSeen = globalThis.__NS_CORE_FETCHED_URLS__ || (globalThis.__NS_CORE_FETCHED_URLS__ = []);`,
4304
+ ` const __nsKey = ${JSON.stringify(canonicalSub)};`,
4305
+ ` const __nsUrl = ${JSON.stringify(canonicalUrl)};`,
4306
+ ` __nsSeen.push(__nsUrl);`,
4307
+ ` if (typeof __nsFirst[__nsKey] === 'string' && __nsFirst[__nsKey] !== __nsUrl) {`,
4308
+ ` throw new Error('[ns-core] URL drift for sub=' + __nsKey + ': first=' + __nsFirst[__nsKey] + ' now=' + __nsUrl);`,
4309
+ ` }`,
4310
+ ` if (!__nsFirst[__nsKey]) __nsFirst[__nsKey] = __nsUrl;`,
4311
+ ` globalThis.__NS_CORE_EVAL_COUNT__ = (globalThis.__NS_CORE_EVAL_COUNT__ || 0) + 1;`,
4312
+ `} } catch (e) { console.warn('[ns-core] instrumentation failed:', (e && e.message) || e); }`,
4313
+ ].join('\n');
4314
+ // CJS/ESM interop shape — REGISTRATION side.
4315
+ //
4316
+ // The actual shape installer runs earlier in the module
4317
+ // body (between preamble and selfImport; see
4318
+ // buildShapeInstallHeader). At this point we just read
4319
+ // globalThis.__NS_CJS_SHAPE__ and apply it to the self
4320
+ // namespace before registering under the CJS key space.
4321
+ //
4322
+ // Why shape self at registration: consumers that reach
4323
+ // `@nativescript/core` via `require()` (legacy vendors,
4324
+ // `globalThis.require` shim) look up the registry. They
4325
+ // expect a plain object (Object.prototype in chain) so
4326
+ // `.hasOwnProperty` / `.toString` work. Shaping once on
4327
+ // registration — the shape function is identity-preserving
4328
+ // via WeakMap — gives a stable, shared, CJS-compatible
4329
+ // view without copying on every require.
4330
+ const registrationFooter = [
4331
+ `try { if (typeof globalThis !== 'undefined') {`,
4332
+ ` const __nsReg = globalThis.__NS_CORE_MODULES__ || (globalThis.__NS_CORE_MODULES__ = Object.create(null));`,
4333
+ ` const __nsShapeFn = typeof globalThis.__NS_CJS_SHAPE__ === 'function' ? globalThis.__NS_CJS_SHAPE__ : function (x) { return x; };`,
4334
+ ` const __nsSelfRaw = (typeof __ns_core_self_ns__ !== 'undefined') ? __ns_core_self_ns__ : { default: undefined };`,
4335
+ ` const __nsSelf = __nsShapeFn(__nsSelfRaw);`,
4336
+ ...registrationKeys.map((k) => ` __nsReg[${k}] = __nsSelf;`),
4337
+ `} } catch (e) { console.warn('[ns-core] self-register failed:', (e && e.message) || e); }`,
4338
+ ].join('\n');
4339
+ // Bind `import * as __ns_core_self_ns__` to the module's
4340
+ // own export namespace so the footer can stash it into
4341
+ // the registry. Self-import is a no-op at eval time —
4342
+ // V8 resolves it to the module record we're already
4343
+ // evaluating and the final namespace is the same object
4344
+ // the registry receives. We use the CANONICAL URL here
4345
+ // so the self-import participates in Invariant A along
4346
+ // with every other @nativescript/core URL.
4347
+ const canonicalUrlForSelf = canonicalSub ? `/ns/core/${canonicalSub}` : '/ns/core';
4348
+ const selfImport = `import * as __ns_core_self_ns__ from ${JSON.stringify(canonicalUrlForSelf)};`;
4349
+ // Invariant D — SHAPE INSTALLER.
4350
+ //
4351
+ // Emits idempotent body-code that installs
4352
+ // globalThis.__NS_CJS_SHAPE__ BEFORE `rewritten`'s body
4353
+ // runs. This matters because the rewrite step above may
4354
+ // have produced statements like
4355
+ // `export const Utils = (typeof globalThis.__NS_CJS_SHAPE__ ...)(__ns_re_Utils__);`
4356
+ // that execute during module evaluation. Without the
4357
+ // installer running first, the ternary falls back to
4358
+ // identity — still safe, but the null-proto namespace
4359
+ // leaks through and consumers that expect a plain
4360
+ // object would still crash.
4361
+ //
4362
+ // Placement is important: BEFORE selfImport in the
4363
+ // concatenation. ESM imports are hoisted regardless of
4364
+ // textual position, but body code executes in source
4365
+ // order. Placing the installer first guarantees it
4366
+ // runs before any body statement in `rewritten`.
4367
+ //
4368
+ // Install is idempotent: `|| (globalThis.X = ...)` so
4369
+ // whichever /ns/core module evaluates first wins and
4370
+ // every subsequent module becomes a no-op.
4371
+ const shapeInstallHeader = buildShapeInstallHeader();
4372
+ // Invariant D — DEFAULT EXPORT BRIDGE.
4373
+ //
4374
+ // See `buildDefaultExportFooter` in ns-core-cjs-shape.ts
4375
+ // for the full rationale (consumer matrix, skip conditions,
4376
+ // why the default isn't shaped). The short version:
4377
+ // upstream rewrites turn `import { X } from '@nativescript/core'`
4378
+ // into a DEFAULT import, and the bridge has to provide one.
4379
+ const defaultExportFooter = buildDefaultExportFooter(rewritten);
4380
+ const moduleCode = [instrumentationHeader, preamble, shapeInstallHeader, selfImport, rewritten, defaultExportFooter, registrationFooter].join('\n');
3275
4381
  res.statusCode = 200;
3276
- res.end(code);
4382
+ res.end(moduleCode);
3277
4383
  }
3278
4384
  catch (e) {
4385
+ console.warn('[ns-core-bridge] serve failed:', e?.message);
3279
4386
  next();
3280
4387
  }
3281
4388
  });
@@ -3285,14 +4392,11 @@ export const piniaSymbol = p.piniaSymbol;
3285
4392
  const urlObj = new URL(req.url || '', 'http://localhost');
3286
4393
  if (!(urlObj.pathname === '/ns/entry-rt'))
3287
4394
  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
- }
4395
+ if (verbose) {
4396
+ const ra = req.socket?.remoteAddress;
4397
+ const rp = req.socket?.remotePort;
4398
+ console.log('[hmr-http] GET /ns/entry-rt from', ra + (rp ? ':' + rp : ''));
3294
4399
  }
3295
- catch { }
3296
4400
  res.setHeader('Access-Control-Allow-Origin', '*');
3297
4401
  res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
3298
4402
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
@@ -3300,18 +4404,41 @@ export const piniaSymbol = p.piniaSymbol;
3300
4404
  res.setHeader('Expires', '0');
3301
4405
  let content = '';
3302
4406
  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');
4407
+ const _req = createRequire(import.meta.url);
4408
+ const entryRtPath = _req.resolve('@nativescript/vite/hmr/entry-runtime.js');
4409
+ content = readFileSync(entryRtPath, 'utf-8');
3307
4410
  }
3308
4411
  catch (e) {
3309
- content = 'export default async function start(){ console.error("[/ns/entry-rt] not found"); }\n';
4412
+ // .js not found (source tree without build) — transform .ts on the fly
4413
+ try {
4414
+ const tsPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', 'entry-runtime.ts');
4415
+ if (existsSync(tsPath)) {
4416
+ const tsSource = readFileSync(tsPath, 'utf-8');
4417
+ const result = babelCore.transformSync(tsSource, {
4418
+ filename: tsPath,
4419
+ plugins: [[pluginTransformTypescript, { isTSX: false, allowDeclareFields: true }]],
4420
+ sourceType: 'module',
4421
+ });
4422
+ if (result?.code) {
4423
+ content = result.code;
4424
+ }
4425
+ }
4426
+ }
4427
+ catch (e2) {
4428
+ if (verbose)
4429
+ console.warn('[hmr-http] entry-runtime.ts transform failed', e2);
4430
+ }
4431
+ if (!content) {
4432
+ content = 'export default async function start(){ console.error("[/ns/entry-rt] not found"); }\n';
4433
+ }
3310
4434
  }
4435
+ if (verbose)
4436
+ console.log('[hmr-http] /ns/entry-rt serving', content.length, 'bytes');
3311
4437
  res.statusCode = 200;
3312
4438
  res.end(content);
3313
4439
  }
3314
4440
  catch (e) {
4441
+ console.warn('[hmr-http] /ns/entry-rt error', e);
3315
4442
  next();
3316
4443
  }
3317
4444
  });
@@ -3514,10 +4641,7 @@ export const piniaSymbol = p.piniaSymbol;
3514
4641
  }
3515
4642
  if (!transformed?.code) {
3516
4643
  if (verbose) {
3517
- try {
3518
- console.warn(`[sfc][serve] transform miss for`, fullSpec);
3519
- }
3520
- catch { }
4644
+ console.warn(`[sfc][serve] transform miss for`, fullSpec);
3521
4645
  }
3522
4646
  // Emit an erroring module to surface the failure at import site with helpful hints
3523
4647
  try {
@@ -3633,10 +4757,7 @@ export const piniaSymbol = p.piniaSymbol;
3633
4757
  }
3634
4758
  catch (eTplSelf) {
3635
4759
  if (verbose) {
3636
- try {
3637
- console.warn('[sfc][template][self-compile][fail]', fullSpec, eTplSelf?.message);
3638
- }
3639
- catch { }
4760
+ console.warn('[sfc][template][self-compile][fail]', fullSpec, eTplSelf?.message);
3640
4761
  }
3641
4762
  code = transformed.code || 'export {}\n';
3642
4763
  code = processTemplateVariantMinimal(code);
@@ -3741,7 +4862,7 @@ export const piniaSymbol = p.piniaSymbol;
3741
4862
  code = outCode;
3742
4863
  }
3743
4864
  catch { }
3744
- code = processCodeForDevice(code, false);
4865
+ code = processCodeForDevice(code, false, true, /(?:^|\/)node_modules\//.test(fullSpec), fullSpec);
3745
4866
  // Transform static .vue imports into static imports from the assembler (no TLA) via AST
3746
4867
  try {
3747
4868
  const importerPath = fullSpec.replace(/[?#].*$/, '');
@@ -3803,10 +4924,7 @@ export const piniaSymbol = p.piniaSymbol;
3803
4924
  }
3804
4925
  catch (eTsVar) {
3805
4926
  if (verbose) {
3806
- try {
3807
- console.warn('[sfc][variant:script][babel-ts][fail]', fullSpec, eTsVar?.message);
3808
- }
3809
- catch { }
4927
+ console.warn('[sfc][variant:script][babel-ts][fail]', fullSpec, eTsVar?.message);
3810
4928
  }
3811
4929
  }
3812
4930
  }
@@ -3870,10 +4988,7 @@ export const piniaSymbol = p.piniaSymbol;
3870
4988
  const kind = isVariant ? `variant:${variantType || 'unknown'}` : 'full';
3871
4989
  const sig = `// [sfc] kind=${kind} path=${importerPath} len=${code.length} default=${hasDefault} wrapped=${false}\n`;
3872
4990
  if (verbose) {
3873
- try {
3874
- console.log(`[sfc][serve] ${fullSpec} kind=${kind} default=${hasDefault} bytes=${code.length}`);
3875
- }
3876
- catch { }
4991
+ console.log(`[sfc][serve] ${fullSpec} kind=${kind} default=${hasDefault} bytes=${code.length}`);
3877
4992
  }
3878
4993
  // Ensure script variants always provide a default export if they declare a component
3879
4994
  if (!hasDefault) {
@@ -3942,16 +5057,10 @@ export const piniaSymbol = p.piniaSymbol;
3942
5057
  // 6) Object property forms (rare in template output) render: (...) => {}
3943
5058
  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
5059
  if (hasRender && verbose) {
3945
- try {
3946
- console.log('[sfc-meta] detected render for', base);
3947
- }
3948
- catch { }
5060
+ console.log('[sfc-meta] detected render for', base);
3949
5061
  }
3950
5062
  else if (!hasRender && verbose) {
3951
- try {
3952
- console.warn('[sfc-meta] render NOT detected for', base);
3953
- }
3954
- catch { }
5063
+ console.warn('[sfc-meta] render NOT detected for', base);
3955
5064
  }
3956
5065
  const hash = createHash('md5').update(base).digest('hex').slice(0, 8);
3957
5066
  const payload = {
@@ -4085,10 +5194,7 @@ export const piniaSymbol = p.piniaSymbol;
4085
5194
  }
4086
5195
  catch (eScript) {
4087
5196
  if (verbose) {
4088
- try {
4089
- console.warn('[sfc-asm][compileScript] failed', base, eScript?.message);
4090
- }
4091
- catch { }
5197
+ console.warn('[sfc-asm][compileScript] failed', base, eScript?.message);
4092
5198
  }
4093
5199
  // Retry without inlineTemplate
4094
5200
  try {
@@ -4104,10 +5210,7 @@ export const piniaSymbol = p.piniaSymbol;
4104
5210
  }
4105
5211
  catch (eNoInline) {
4106
5212
  if (verbose) {
4107
- try {
4108
- console.warn('[sfc-asm][compileScript][no-inline-fallback] failed', base, eNoInline?.message);
4109
- }
4110
- catch { }
5213
+ console.warn('[sfc-asm][compileScript][no-inline-fallback] failed', base, eNoInline?.message);
4111
5214
  }
4112
5215
  }
4113
5216
  }
@@ -4138,10 +5241,7 @@ export const piniaSymbol = p.piniaSymbol;
4138
5241
  }
4139
5242
  catch (eNoInline) {
4140
5243
  if (verbose) {
4141
- try {
4142
- console.warn('[sfc-asm][compileScript][no-inline-fallback] failed', base, eNoInline?.message);
4143
- }
4144
- catch { }
5244
+ console.warn('[sfc-asm][compileScript][no-inline-fallback] failed', base, eNoInline?.message);
4145
5245
  }
4146
5246
  }
4147
5247
  }
@@ -4164,20 +5264,14 @@ export const piniaSymbol = p.piniaSymbol;
4164
5264
  });
4165
5265
  compiledTplCode = (ct && (ct.code || '')) || '';
4166
5266
  if (ct?.errors?.length && verbose) {
4167
- try {
4168
- console.warn('[sfc-asm][compileTemplate][errors]', base, ct.errors);
4169
- }
4170
- catch { }
5267
+ console.warn('[sfc-asm][compileTemplate][errors]', base, ct.errors);
4171
5268
  }
4172
5269
  }
4173
5270
  }
4174
5271
  catch (eTpl) {
4175
5272
  templateErr = eTpl;
4176
5273
  if (verbose) {
4177
- try {
4178
- console.warn('[sfc-asm][compileTemplate] failed', base, eTpl?.message);
4179
- }
4180
- catch { }
5274
+ console.warn('[sfc-asm][compileTemplate] failed', base, eTpl?.message);
4181
5275
  }
4182
5276
  // Fallback: use the variant-transformed template code if available
4183
5277
  try {
@@ -4240,10 +5334,7 @@ export const piniaSymbol = p.piniaSymbol;
4240
5334
  }
4241
5335
  catch (eExtract) {
4242
5336
  if (verbose) {
4243
- try {
4244
- console.warn('[sfc-asm][extractTemplateRender] failed', base, eExtract?.message);
4245
- }
4246
- catch { }
5337
+ console.warn('[sfc-asm][extractTemplateRender] failed', base, eExtract?.message);
4247
5338
  }
4248
5339
  }
4249
5340
  }
@@ -4293,11 +5384,10 @@ export const piniaSymbol = p.piniaSymbol;
4293
5384
  parts.push(scriptTransformed);
4294
5385
  parts.push(renderDecl);
4295
5386
  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
5387
  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
5388
  parts.push(`export default __ns_sfc__`);
4299
5389
  let inlineCode = parts.filter(Boolean).join('\n');
4300
- inlineCode = processCodeForDevice(inlineCode, false);
5390
+ inlineCode = processCodeForDevice(inlineCode, false, true);
4301
5391
  try {
4302
5392
  inlineCode = ensureVersionedCoreImports(inlineCode, getServerOrigin(server), Number(ver));
4303
5393
  }
@@ -4322,10 +5412,7 @@ export const piniaSymbol = p.piniaSymbol;
4322
5412
  }
4323
5413
  catch (eTs) {
4324
5414
  if (verbose) {
4325
- try {
4326
- console.warn('[sfc-asm][babel-ts][fail]', base, eTs?.message);
4327
- }
4328
- catch { }
5415
+ console.warn('[sfc-asm][babel-ts][fail]', base, eTs?.message);
4329
5416
  }
4330
5417
  }
4331
5418
  // Hoist imports + strip residual TS via AST
@@ -4335,18 +5422,12 @@ export const piniaSymbol = p.piniaSymbol;
4335
5422
  importLines = astRes.imports;
4336
5423
  scriptTransformed = astRes.body;
4337
5424
  if (astRes.diagnostics.length && verbose) {
4338
- try {
4339
- console.warn('[sfc-asm][ast]', base, astRes.diagnostics.join('; '));
4340
- }
4341
- catch { }
5425
+ console.warn('[sfc-asm][ast]', base, astRes.diagnostics.join('; '));
4342
5426
  }
4343
5427
  }
4344
5428
  catch (eAst) {
4345
5429
  if (verbose) {
4346
- try {
4347
- console.warn('[sfc-asm][ast][fail]', base, eAst?.message);
4348
- }
4349
- catch { }
5430
+ console.warn('[sfc-asm][ast][fail]', base, eAst?.message);
4350
5431
  }
4351
5432
  }
4352
5433
  // Ensure renderDecl ends with closing brace ONLY for function declaration forms
@@ -4372,12 +5453,11 @@ export const piniaSymbol = p.piniaSymbol;
4372
5453
  outParts.push(renderDecl);
4373
5454
  }
4374
5455
  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
5456
  // Export named render as a function that resolves lazily
4377
5457
  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
5458
  outParts.push('export default __ns_sfc__');
4379
5459
  let inlineCode2 = outParts.filter(Boolean).join('\n');
4380
- inlineCode2 = processCodeForDevice(inlineCode2, false);
5460
+ inlineCode2 = processCodeForDevice(inlineCode2, false, true);
4381
5461
  try {
4382
5462
  inlineCode2 = ensureVersionedCoreImports(inlineCode2, getServerOrigin(server), Number(ver));
4383
5463
  }
@@ -4673,12 +5753,11 @@ export const piniaSymbol = p.piniaSymbol;
4673
5753
  }
4674
5754
  let asm;
4675
5755
  if (inlineOk) {
4676
- const diagLine = `// diagnostic:inlineOk ver=${ver} inlineBlock=${!!(inlineBlock && inlineBlock.trim())} helperBindingsLen=${helperBindings.length} renderDeclLen=${renderDecl.length}`;
4677
5756
  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');
5757
+ 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
5758
  }
4680
5759
  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');
5760
+ 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
5761
  }
4683
5762
  }
4684
5763
  else {
@@ -4689,7 +5768,7 @@ export const piniaSymbol = p.piniaSymbol;
4689
5768
  }
4690
5769
  // Run full device processing so helper aliasing and globals are consistent in this path too
4691
5770
  let code = REQUIRE_GUARD_SNIPPET + asm;
4692
- code = processCodeForDevice(code, false);
5771
+ code = processCodeForDevice(code, false, true, /(?:^|\/)node_modules\//.test(base), base);
4693
5772
  try {
4694
5773
  code = ensureVersionedCoreImports(code, getServerOrigin(server), Number(ver));
4695
5774
  }
@@ -4862,7 +5941,7 @@ export const piniaSymbol = p.piniaSymbol;
4862
5941
  let code = transformed.code;
4863
5942
  // Reuse existing sanitation chain (lightweight)
4864
5943
  code = cleanCode(code);
4865
- code = processCodeForDevice(code, false);
5944
+ code = processCodeForDevice(code, false, true, /(?:^|\/)node_modules\//.test(resolvedCandidate || spec), resolvedCandidate || spec);
4866
5945
  try {
4867
5946
  code = ensureVersionedCoreImports(code, getServerOrigin(server), graphVersion);
4868
5947
  }
@@ -4917,7 +5996,7 @@ export const piniaSymbol = p.piniaSymbol;
4917
5996
  if (depTrans?.code && depResolved) {
4918
5997
  let depCode = depTrans.code;
4919
5998
  depCode = cleanCode(depCode);
4920
- depCode = processCodeForDevice(depCode, false);
5999
+ depCode = processCodeForDevice(depCode, false, true, /(?:^|\/)node_modules\//.test(depResolved), depResolved);
4921
6000
  try {
4922
6001
  depCode = ensureVersionedCoreImports(depCode, getServerOrigin(server), graphVersion);
4923
6002
  }
@@ -4950,8 +6029,8 @@ export const piniaSymbol = p.piniaSymbol;
4950
6029
  ts: Date.now(),
4951
6030
  delta: true,
4952
6031
  };
4953
- wss.clients.forEach((c) => {
4954
- if (c.readyState === c.OPEN) {
6032
+ wss?.clients.forEach((c) => {
6033
+ if (isSocketClientOpen(c)) {
4955
6034
  try {
4956
6035
  c.send(JSON.stringify(single));
4957
6036
  }
@@ -4990,32 +6069,33 @@ export const piniaSymbol = p.piniaSymbol;
4990
6069
  if (verbose)
4991
6070
  console.warn('[hmr-ws][graph] initial population failed', e);
4992
6071
  }
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
- }
6072
+ // Send SFC registry on every connection (not just the first).
6073
+ // When the NativeScript app restarts (e.g. CLI auto-reload), the new
6074
+ // JS context has an empty sfcArtifactMap. Without the registry the
6075
+ // rescue-mount cannot find the root .vue component.
6076
+ try {
6077
+ await ACTIVE_STRATEGY.buildRegistry({
6078
+ server,
6079
+ sfcFileMap,
6080
+ depFileMap,
6081
+ wss: wss,
6082
+ verbose,
6083
+ helpers: {
6084
+ cleanCode,
6085
+ collectImportDependencies,
6086
+ isCoreGlobalsReference,
6087
+ isNativeScriptCoreModule,
6088
+ isNativeScriptPluginModule,
6089
+ resolveVendorFromCandidate,
6090
+ createHash: (value) => createHash('md5').update(value).digest('hex'),
6091
+ rewriteImports,
6092
+ processSfcCode,
6093
+ },
6094
+ });
6095
+ registrySent = true;
6096
+ }
6097
+ catch (error) {
6098
+ console.warn('[hmr-ws] Failed to send registry:', error);
5019
6099
  }
5020
6100
  emitFullGraph(ws);
5021
6101
  // After sending registry & graph also send current module manifest if any
@@ -5038,16 +6118,148 @@ export const piniaSymbol = p.piniaSymbol;
5038
6118
  if (!wss) {
5039
6119
  return;
5040
6120
  }
6121
+ if (isRuntimeGraphExcludedPath(file)) {
6122
+ return;
6123
+ }
6124
+ // Always-on update timing. Captures the four phases (await,
6125
+ // framework, broadcast, total) plus invalidated module count
6126
+ // and recipient count. Emitted at the end of this function via
6127
+ // `emitHmrUpdateSummary()`. Single line, always-on so a
6128
+ // 6-second `.ts` save is immediately visible without flipping
6129
+ // verbose.
6130
+ const updateRoot = server.config.root || process.cwd();
6131
+ const updateRel = (() => {
6132
+ try {
6133
+ return '/' + path.posix.normalize(path.relative(updateRoot, file)).split(path.sep).join('/');
6134
+ }
6135
+ catch {
6136
+ return file;
6137
+ }
6138
+ })();
6139
+ const updateMetrics = {
6140
+ file: updateRel,
6141
+ kind: classifyHmrUpdateKind(file),
6142
+ t0: Date.now(),
6143
+ tAfterAwait: 0,
6144
+ tAfterFramework: 0,
6145
+ tEnd: 0,
6146
+ invalidated: 0,
6147
+ recipients: 0,
6148
+ // Narrowing diagnostic — populated by the angular branch when
6149
+ // the changed file is `.ts`, otherwise remains undefined and is
6150
+ // omitted from the summary line entirely.
6151
+ narrowed: undefined,
6152
+ emitted: false,
6153
+ };
6154
+ // Broadcast a "pending" notification at the very start of
6155
+ // handleHotUpdate so the client can show the HMR-applying
6156
+ // overlay BEFORE we spend time on graph updates / transforms /
6157
+ // dependency analysis (typically 7–200ms on a warm cache).
6158
+ // Without this, the overlay only appears at `ns:angular-update`
6159
+ // broadcast time and the user perceives a "delayed" reaction
6160
+ // to their save.
6161
+ //
6162
+ // Fire-and-forget: a failed pending broadcast must never
6163
+ // hold up the actual update. The client treats receipt of
6164
+ // `ns:angular-update` (or `ns:css-updates`) as authoritative;
6165
+ // the pending message is purely a UX hint.
6166
+ try {
6167
+ const pendingPayload = JSON.stringify(createHmrPendingMessage({
6168
+ origin: getServerOrigin(server),
6169
+ path: updateMetrics.file,
6170
+ kind: updateMetrics.kind,
6171
+ timestamp: updateMetrics.t0,
6172
+ }));
6173
+ wss.clients.forEach((client) => {
6174
+ if (isSocketClientOpen(client)) {
6175
+ try {
6176
+ client.send(pendingPayload);
6177
+ }
6178
+ catch { }
6179
+ }
6180
+ });
6181
+ }
6182
+ catch { }
6183
+ const emitHmrUpdateSummary = () => {
6184
+ if (updateMetrics.emitted)
6185
+ return;
6186
+ updateMetrics.emitted = true;
6187
+ updateMetrics.tEnd = Date.now();
6188
+ try {
6189
+ const awaitMs = (updateMetrics.tAfterAwait || updateMetrics.t0) - updateMetrics.t0;
6190
+ const frameworkMs = (updateMetrics.tAfterFramework || updateMetrics.tAfterAwait || updateMetrics.t0) - (updateMetrics.tAfterAwait || updateMetrics.t0);
6191
+ const broadcastMs = updateMetrics.tEnd - (updateMetrics.tAfterFramework || updateMetrics.tAfterAwait || updateMetrics.t0);
6192
+ const totalMs = updateMetrics.tEnd - updateMetrics.t0;
6193
+ console.info(formatHmrUpdateSummary({
6194
+ file: updateMetrics.file,
6195
+ kind: updateMetrics.kind,
6196
+ awaitMs,
6197
+ frameworkMs,
6198
+ broadcastMs,
6199
+ totalMs,
6200
+ invalidated: updateMetrics.invalidated,
6201
+ recipients: updateMetrics.recipients,
6202
+ narrowed: updateMetrics.narrowed,
6203
+ }));
6204
+ }
6205
+ catch { }
6206
+ };
6207
+ // The first /ns/m request kicks off populateInitialGraph in the
6208
+ // background. If an HMR update races in before that walk
6209
+ // completes, we'd lose transitive-importer data. Await
6210
+ // completion here so the delta computation below always sees a
6211
+ // populated graph.
6212
+ if (graphInitialPopulationPromise) {
6213
+ try {
6214
+ await graphInitialPopulationPromise;
6215
+ }
6216
+ catch { }
6217
+ }
6218
+ updateMetrics.tAfterAwait = Date.now();
5041
6219
  // Graph update for this file change (wrapped to avoid aborting rest of handler)
5042
6220
  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);
6221
+ const skipAngularHtmlGraphUpdate = ACTIVE_STRATEGY.flavor === 'angular' && /\.(html|htm)$/i.test(file);
6222
+ if (!skipAngularHtmlGraphUpdate) {
6223
+ const graphTargets = collectGraphUpdateModulesForHotUpdate({
6224
+ file,
6225
+ flavor: ACTIVE_STRATEGY.flavor,
6226
+ modules: ctx.modules,
6227
+ getModuleById: (id) => server.moduleGraph.getModuleById(id),
6228
+ verbose,
6229
+ });
6230
+ for (const mod of graphTargets) {
6231
+ if (!mod?.id)
6232
+ continue;
6233
+ try {
6234
+ const deps = Array.from(mod.importedModules || [])
6235
+ .map((m) => (m.id || '').replace(/\?.*$/, ''))
6236
+ .filter(Boolean);
6237
+ const transformed = await server.transformRequest(mod.id);
6238
+ const code = transformed?.code || '';
6239
+ upsertGraphModule((mod.id || '').replace(/\?.*$/, ''), code, deps, {
6240
+ emitDeltaOnInsert: true,
6241
+ // Defer the delta broadcast until AFTER the framework
6242
+ // hot-update handler has had a chance to invalidate the
6243
+ // shared transform-request cache + Vite's moduleGraph
6244
+ // for the changed file and its transitive importers.
6245
+ // Otherwise the client races: it receives the delta
6246
+ // (eviction + re-import via tagged URL) before the
6247
+ // server has purged its caches, and the re-import is
6248
+ // served from cache → V8 evaluates the previous save's
6249
+ // transformed code → patchRegistry runs against an
6250
+ // unchanged source → the visible page is "one save
6251
+ // behind". Angular has always taken this path; Solid
6252
+ // needs the same contract because Solid HMR depends
6253
+ // on the client re-fetching the just-changed module
6254
+ // to drive `solid-refresh.patchRegistry`.
6255
+ broadcastDelta: ACTIVE_STRATEGY.flavor !== 'angular' && ACTIVE_STRATEGY.flavor !== 'solid',
6256
+ });
6257
+ }
6258
+ catch (error) {
6259
+ if (verbose)
6260
+ console.warn('[hmr-ws][v2] failed graph update target', mod.id, error);
6261
+ }
6262
+ }
5051
6263
  }
5052
6264
  }
5053
6265
  catch (e) {
@@ -5068,6 +6280,7 @@ export const piniaSymbol = p.piniaSymbol;
5068
6280
  console.log(`[hmr-ws] Hot update for: ${file}`);
5069
6281
  // Handle CSS updates
5070
6282
  if (file.endsWith('.css')) {
6283
+ updateMetrics.tAfterFramework = Date.now();
5071
6284
  try {
5072
6285
  let rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
5073
6286
  const origin = getServerOrigin(server);
@@ -5084,14 +6297,16 @@ export const piniaSymbol = p.piniaSymbol;
5084
6297
  ],
5085
6298
  };
5086
6299
  wss.clients.forEach((client) => {
5087
- if (client.readyState === client.OPEN) {
6300
+ if (isSocketClientOpen(client)) {
5088
6301
  client.send(JSON.stringify(msg));
6302
+ updateMetrics.recipients += 1;
5089
6303
  }
5090
6304
  });
5091
6305
  }
5092
6306
  catch (error) {
5093
6307
  console.warn('[hmr-ws] CSS update failed:', error);
5094
6308
  }
6309
+ emitHmrUpdateSummary();
5095
6310
  return;
5096
6311
  }
5097
6312
  // Framework-specific hot update handling
@@ -5099,31 +6314,281 @@ export const piniaSymbol = p.piniaSymbol;
5099
6314
  // For Angular, react to component TS or external template HTML changes under /src
5100
6315
  const isHtml = file.endsWith('.html');
5101
6316
  const isTs = file.endsWith('.ts');
6317
+ const angularHotUpdateRoots = collectAngularHotUpdateRoots({
6318
+ file,
6319
+ modules: ctx.modules,
6320
+ getModuleById: (id) => server.moduleGraph.getModuleById(id),
6321
+ getModulesByFile: (targetFile) => server.moduleGraph.getModulesByFile?.(targetFile),
6322
+ });
6323
+ if (verbose) {
6324
+ console.info(`[ns-hmr-diag][server] hot-update file=${file} isHtml=${isHtml} isTs=${isTs} ctxModules=${Array.from(ctx.modules || []).length} hotUpdateRoots=${angularHotUpdateRoots.length} (${angularHotUpdateRoots
6325
+ .map((m) => m?.id ?? '(none)')
6326
+ .slice(0, 8)
6327
+ .join(', ')}${angularHotUpdateRoots.length > 8 ? ', …' : ''})`);
6328
+ }
5102
6329
  if (!(isHtml || isTs))
5103
6330
  return;
6331
+ updateMetrics.invalidated += angularHotUpdateRoots.length;
6332
+ if (angularHotUpdateRoots.length) {
6333
+ for (const mod of angularHotUpdateRoots) {
6334
+ try {
6335
+ server.moduleGraph.invalidateModule(mod);
6336
+ }
6337
+ catch (invalidationError) {
6338
+ if (verbose) {
6339
+ console.warn('[hmr-ws][angular] hot-update root invalidation failed', mod?.id, invalidationError);
6340
+ }
6341
+ }
6342
+ }
6343
+ if (verbose) {
6344
+ console.log('[hmr-ws][angular] invalidated hot-update root modules:', angularHotUpdateRoots.length);
6345
+ }
6346
+ }
6347
+ const angularTransitiveInvalidationRoots = (angularHotUpdateRoots.length ? angularHotUpdateRoots : ctx.modules);
6348
+ // Read the source for `.ts/.tsx/.js/.jsx` edits so
6349
+ // `shouldInvalidateAngularTransitiveImporters` can
6350
+ // distinguish leaf modules (constants/utils) from real
6351
+ // Angular files. If `ctx.read()` throws (file deleted, race
6352
+ // against the watcher), `angularChangedSource` stays
6353
+ // undefined and we fall back to the conservative "always
6354
+ // invalidate transitively" behavior.
6355
+ let angularChangedSource;
6356
+ if (isTs) {
6357
+ try {
6358
+ angularChangedSource = await ctx.read();
6359
+ }
6360
+ catch {
6361
+ angularChangedSource = undefined;
6362
+ }
6363
+ }
6364
+ const angularNeedsTransitive = shouldInvalidateAngularTransitiveImporters({
6365
+ flavor: ACTIVE_STRATEGY.flavor,
6366
+ file,
6367
+ source: angularChangedSource,
6368
+ });
6369
+ // Surface the narrowing decision on every `.ts` Angular hot
6370
+ // update (HTML routes always invalidate transitively and
6371
+ // aren't subject to narrowing, so we leave them as
6372
+ // `undefined` — the field is omitted from the summary line).
6373
+ // The boolean is the inverse of `angularNeedsTransitive`
6374
+ // because "needs transitive" is the broad (un-narrowed)
6375
+ // behavior.
6376
+ if (isTs) {
6377
+ updateMetrics.narrowed = !angularNeedsTransitive;
6378
+ }
6379
+ // Stable URL + Explicit Invalidation:
6380
+ //
6381
+ // Compute the transitive importer closure ONCE here and reuse
6382
+ // it for (a) `server.moduleGraph.invalidateModule` (so Vite's
6383
+ // transform pipeline re-runs on next request), (b) the shared
6384
+ // transform-request cache, and (c) the runtime eviction set
6385
+ // we broadcast in `ns:angular-update`. Consolidating this
6386
+ // removes a redundant graph walk and guarantees the three
6387
+ // consumers see the exact same set of importers (otherwise a
6388
+ // late module-graph mutation between calls could leave an
6389
+ // asymmetric narrowed/broad mix).
6390
+ //
6391
+ // We separate Vite-transform narrowing from runtime eviction:
6392
+ // `angularNeedsTransitive` answers the question "does the
6393
+ // changed file's symbol shape change such that importers
6394
+ // must be re-transformed by Vite?". The runtime, however,
6395
+ // has a stricter requirement: ESM live bindings only refresh
6396
+ // if the importing module re-evaluates inside V8. A
6397
+ // constants file with no Angular decorator does NOT need a
6398
+ // Vite re-transform of its importers (their compiled JS is
6399
+ // identical), but its importers still hold stale bindings to
6400
+ // the OLD constants Module record. After eviction + re-import
6401
+ // of `main.ts`, V8 sees the cached importers, returns them
6402
+ // unchanged, and they continue to read the OLD values. The
6403
+ // user-visible symptom: HMR completes successfully, logs are
6404
+ // clean, but the simulator does not reflect the change.
6405
+ //
6406
+ // The fix: ALWAYS compute the transitive importer closure
6407
+ // for runtime eviction. Only skip Vite's
6408
+ // `moduleGraph.invalidate` + transform-cache purge when
6409
+ // `angularNeedsTransitive` is false — those are the genuine
6410
+ // narrowing wins (saves re-transform work on the server).
6411
+ // The eviction set always includes importers so V8 re-fetches
6412
+ // and re-binds them.
6413
+ if (verbose) {
6414
+ console.info(`[ns-hmr-diag][server] angularNeedsTransitive=${angularNeedsTransitive} (file=${path.basename(file)})`);
6415
+ }
6416
+ let transitiveImporters = [];
6417
+ try {
6418
+ transitiveImporters = collectAngularTransitiveImportersForInvalidation({
6419
+ modules: angularTransitiveInvalidationRoots,
6420
+ isExcluded: (id) => id.includes('/node_modules/'),
6421
+ maxDepth: 16,
6422
+ });
6423
+ if (verbose) {
6424
+ console.info(`[ns-hmr-diag][server] transitiveImporters count=${transitiveImporters.length} firstN=`, transitiveImporters.slice(0, 16).map((m) => m?.id ?? '(none)'));
6425
+ }
6426
+ if (angularNeedsTransitive) {
6427
+ updateMetrics.invalidated += transitiveImporters.length;
6428
+ for (const mod of transitiveImporters) {
6429
+ try {
6430
+ server.moduleGraph.invalidateModule(mod);
6431
+ }
6432
+ catch (invalidationError) {
6433
+ if (verbose) {
6434
+ console.warn('[hmr-ws][angular] transitive importer invalidation failed', mod?.id, invalidationError);
6435
+ }
6436
+ }
6437
+ }
6438
+ if (verbose && transitiveImporters.length) {
6439
+ console.log('[hmr-ws][angular] invalidated transitive importers:', transitiveImporters.length);
6440
+ }
6441
+ }
6442
+ else if (isTs && typeof angularChangedSource === 'string') {
6443
+ // Surfacing this log unconditionally lets the user
6444
+ // immediately confirm whether narrowing fired for a
6445
+ // given `.ts` edit (the summary line below still
6446
+ // emits `narrowed=yes`/`no`, but having both makes
6447
+ // the decision easier to spot in noisy logs and lets
6448
+ // the user diff scenarios without flipping
6449
+ // `NS_HMR_VERBOSE=true`).
6450
+ //
6451
+ // Narrowing means "skip Vite re-transform" (the
6452
+ // importers still get evicted from the V8 module
6453
+ // registry so live bindings refresh). The importer
6454
+ // count is appended so the distinction is visible.
6455
+ 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)`);
6456
+ }
6457
+ }
6458
+ catch (error) {
6459
+ if (verbose)
6460
+ console.warn('[hmr-ws][angular] transitive importer collection failed', error);
6461
+ }
6462
+ try {
6463
+ // Purge shared transform cache for the changed file +
6464
+ // hot-update roots unconditionally (their transform
6465
+ // output IS different now). Transitive importers are
6466
+ // only purged when narrowing decides their output may
6467
+ // have changed; otherwise their cached transforms are
6468
+ // still valid (compiled JS is identical even though the
6469
+ // runtime must re-evaluate them to refresh ESM bindings).
6470
+ const transformCacheInvalidationUrls = new Set(collectAngularTransformCacheInvalidationUrls({
6471
+ file,
6472
+ isTs,
6473
+ hotUpdateRoots: angularHotUpdateRoots,
6474
+ transitiveImporters: angularNeedsTransitive ? transitiveImporters : [],
6475
+ projectRoot: server.config.root || process.cwd(),
6476
+ }));
6477
+ if (transformCacheInvalidationUrls.size) {
6478
+ sharedTransformRequest.invalidateMany(transformCacheInvalidationUrls);
6479
+ if (verbose) {
6480
+ console.log('[hmr-ws][angular] purged shared transform cache entries:', transformCacheInvalidationUrls.size);
6481
+ }
6482
+ }
6483
+ }
6484
+ catch (error) {
6485
+ if (verbose)
6486
+ console.warn('[hmr-ws][angular] shared transform cache purge failed', error);
6487
+ }
6488
+ updateMetrics.tAfterFramework = Date.now();
5104
6489
  try {
5105
6490
  const root = server.config.root || process.cwd();
5106
6491
  const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
6492
+ rememberAngularReloadSuppression(root, file);
5107
6493
  const origin = getServerOrigin(server);
6494
+ const bootstrapEntryRel = getBootstrapEntryRelPath();
6495
+ // Stable URL + Explicit Invalidation:
6496
+ //
6497
+ // `evictPaths` is the canonical list of `/ns/m/<rel>` URLs
6498
+ // the runtime must drop from `g_moduleRegistry` before
6499
+ // re-importing `importerEntry`. Older versions of the
6500
+ // server signaled invalidation by bumping a global
6501
+ // `graphVersion` counter and embedding it in every URL —
6502
+ // but V8 keys the module registry by full URL, so a v1 →
6503
+ // v2 bump effectively flushed the entire dependency
6504
+ // graph from the cache and forced the runtime to
6505
+ // re-fetch + re-eval every transitively-imported module
6506
+ // on each save (~3s HMR cycles, dominated by Vite's
6507
+ // single-threaded transform pipeline). The new model:
6508
+ //
6509
+ // 1. URLs are stable: `/ns/m/<rel>` everywhere, no `vN`.
6510
+ // 2. The server walks the inverse-dependency closure and
6511
+ // sends only the modules that actually need to be
6512
+ // re-evaluated (typically O(1) for component edits,
6513
+ // or the changed file + entry for narrowed edits).
6514
+ // 3. The client calls `__nsInvalidateModules(evictPaths)`
6515
+ // and re-imports `importerEntry`, which causes V8 to
6516
+ // refetch ONLY those modules. Everything else stays
6517
+ // hot in the registry.
6518
+ //
6519
+ // Invariants enforced by `collectAngularEvictionUrls`:
6520
+ // - Always includes the changed file (so the new source
6521
+ // is fetched).
6522
+ // - Always includes `importerEntry` (so re-import
6523
+ // re-evaluates).
6524
+ // - Excludes node_modules (vendor packages are stable).
6525
+ // - Excludes virtual / runtime-graph-excluded ids.
6526
+ // - Origin-prefixed: `http://host:port/ns/m/<rel>`.
6527
+ let evictPaths = [];
6528
+ try {
6529
+ evictPaths = collectAngularEvictionUrls({
6530
+ file,
6531
+ hotUpdateRoots: angularHotUpdateRoots,
6532
+ transitiveImporters,
6533
+ projectRoot: root,
6534
+ origin,
6535
+ bootstrapEntry: bootstrapEntryRel,
6536
+ });
6537
+ }
6538
+ catch (error) {
6539
+ if (verbose) {
6540
+ console.warn('[ns-hmr-diag][server] eviction set computation failed', error);
6541
+ }
6542
+ }
6543
+ if (verbose) {
6544
+ try {
6545
+ const tsRel = rel.replace(/\.(html|htm)$/i, '.ts');
6546
+ const jsRel = rel.replace(/\.(html|htm)$/i, '.js');
6547
+ const containsRelatedTs = evictPaths.some((u) => u.endsWith(tsRel));
6548
+ const containsRelatedJs = evictPaths.some((u) => u.endsWith(jsRel));
6549
+ const sample = evictPaths.slice(0, 32);
6550
+ console.info(`[ns-hmr-diag][server] evict-set count=${evictPaths.length} importerEntry=${bootstrapEntryRel ?? '(none)'} containsRelatedTs=${containsRelatedTs} containsRelatedJs=${containsRelatedJs} firstN=`, sample);
6551
+ if (evictPaths.length > sample.length) {
6552
+ console.info(`[ns-hmr-diag][server] evict-set hidden=${evictPaths.length - sample.length} (showed first ${sample.length})`);
6553
+ }
6554
+ }
6555
+ catch { }
6556
+ }
5108
6557
  const msg = {
5109
6558
  type: 'ns:angular-update',
5110
6559
  origin,
5111
6560
  path: rel,
6561
+ version: graphVersion,
5112
6562
  timestamp: Date.now(),
6563
+ evictPaths,
6564
+ importerEntry: bootstrapEntryRel,
5113
6565
  };
6566
+ if (verbose) {
6567
+ console.log('[hmr-ws][angular] broadcasting update', Array.from(wss.clients || []).map((client) => ({
6568
+ role: getHmrSocketRole(client),
6569
+ readyState: client.readyState,
6570
+ openState: client.OPEN,
6571
+ })));
6572
+ }
5114
6573
  wss.clients.forEach((client) => {
5115
- if (client.readyState === client.OPEN) {
6574
+ if (isSocketClientOpen(client)) {
5116
6575
  client.send(JSON.stringify(msg));
6576
+ updateMetrics.recipients += 1;
5117
6577
  }
5118
6578
  });
5119
6579
  }
5120
6580
  catch (error) {
5121
6581
  console.warn('[hmr-ws][angular] update failed:', error);
5122
6582
  }
6583
+ emitHmrUpdateSummary();
6584
+ if (shouldSuppressDefaultViteHotUpdate({ flavor: ACTIVE_STRATEGY.flavor, file })) {
6585
+ return [];
6586
+ }
5123
6587
  return;
5124
6588
  }
5125
6589
  // TypeScript flavor: emit generic graph delta for app XML/TS/style changes
5126
6590
  if (ACTIVE_STRATEGY.flavor === 'typescript') {
6591
+ updateMetrics.tAfterFramework = Date.now();
5127
6592
  try {
5128
6593
  const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
5129
6594
  if (verbose)
@@ -5131,15 +6596,236 @@ export const piniaSymbol = p.piniaSymbol;
5131
6596
  // Treat the changed file itself as a graph module with no deps. We only
5132
6597
  // care that its hash/identity changes so the client sees a delta and can
5133
6598
  // 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], []);
6599
+ upsertGraphModule(rel, '', [], { emitDeltaOnInsert: true });
5138
6600
  }
5139
6601
  catch (e) {
5140
6602
  if (verbose)
5141
6603
  console.warn('[hmr-ws][ts] failed to emit delta for', file, e);
5142
6604
  }
6605
+ emitHmrUpdateSummary();
6606
+ return;
6607
+ }
6608
+ // Solid flavor: emit graph delta for app TSX/TS/JSX file changes.
6609
+ // The common graph-update block above (moduleGraph lookup) may have
6610
+ // already emitted a delta if the file was in Vite's module graph.
6611
+ // This handler ensures a delta is emitted even if the module wasn't
6612
+ // found (e.g. new file, or moduleGraph mismatch), and provides
6613
+ // Solid-specific logging. The client-side processQueue handles
6614
+ // propagation from non-component .ts files to .tsx component boundaries.
6615
+ if (ACTIVE_STRATEGY.flavor === 'solid') {
6616
+ const isSolidFile = /\.(tsx?|jsx?)$/i.test(file);
6617
+ if (!isSolidFile)
6618
+ return;
6619
+ updateMetrics.tAfterFramework = Date.now();
6620
+ try {
6621
+ const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
6622
+ if (verbose)
6623
+ console.log('[hmr-ws][solid] app file hot update', { file, rel });
6624
+ // If the common block already upserted (hash changed), this will
6625
+ // detect unchanged hash and no-op. If the common block missed it
6626
+ // (module not in Vite's graph), this forces the delta emission.
6627
+ const normalizedId = normalizeGraphId(rel);
6628
+ const existing = graph.get(normalizedId);
6629
+ if (!existing) {
6630
+ // Module not in graph yet — force upsert with timestamp-based
6631
+ // hash so the client sees a change.
6632
+ upsertGraphModule(rel, `/* solid-hmr ${Date.now()} */`, [], { emitDeltaOnInsert: true });
6633
+ }
6634
+ // Log what we're sending so devs can trace the flow on the server side.
6635
+ if (verbose) {
6636
+ const gm = graph.get(normalizedId);
6637
+ console.log('[hmr-ws][solid] delta module', { id: gm?.id, hash: gm?.hash });
6638
+ }
6639
+ // Purge the shared transform-request cache AND Vite's own
6640
+ // moduleGraph transformResult cache for the changed file
6641
+ // AND every transitive importer.
6642
+ //
6643
+ // Why this matters for Solid HMR specifically:
6644
+ // - The HMR client evicts V8's module cache for the
6645
+ // canonical /ns/m/<path> URL and re-imports the module.
6646
+ // - The dev server resolves /ns/m/* by calling
6647
+ // `sharedTransformRequest(...)`, which has a 60s TTL on
6648
+ // transform results to amortize cost across HMR
6649
+ // cycles. The shared cache wraps `server.transformRequest`,
6650
+ // which itself caches the compiled output on each
6651
+ // `ModuleNode.transformResult`. Both layers must be
6652
+ // invalidated, or the re-import resolves to whatever
6653
+ // the previous save populated.
6654
+ // - Without invalidation at *both* layers, the second
6655
+ // save of a file within the cache window returns the
6656
+ // FIRST save's transform — V8 evaluates stale code,
6657
+ // `solid-refresh.patchRegistry` runs against an
6658
+ // unchanged source body, and the visible page picks
6659
+ // up the previous save's edit instead of the current
6660
+ // one (the "one-save-behind" symptom users reported).
6661
+ //
6662
+ // Critically, transitive importers must also be invalidated
6663
+ // because TanStack file-based routing (and similar frameworks)
6664
+ // use route files that statically import their components.
6665
+ // When `home.tsx` changes, `routes/index.tsx`'s transform
6666
+ // output references the imported home module identity. Even
6667
+ // though the route file's source bytes did not change, its
6668
+ // *resolved* import target has — and its cached transform
6669
+ // might still encode the previous resolution. Forcing a
6670
+ // fresh transform of the importer guarantees the route
6671
+ // file's `import Home from ...` re-resolves against the
6672
+ // freshly evaluated home module on V8 side.
6673
+ //
6674
+ // The Angular path performs the equivalent purge via
6675
+ // `collectAngularTransformCacheInvalidationUrls` /
6676
+ // `sharedTransformRequest.invalidateMany`. We replicate
6677
+ // that contract for Solid here. The transitive walk is
6678
+ // bounded the same way (max depth 16, node_modules /
6679
+ // virtual ids excluded) so vendor packages stay hot.
6680
+ try {
6681
+ const projectRoot = server.config.root || process.cwd();
6682
+ const cacheInvalidationUrls = new Set();
6683
+ const addCacheKey = (rawId) => {
6684
+ const id = String(rawId || '');
6685
+ if (!id)
6686
+ return;
6687
+ const cacheKey = canonicalizeTransformRequestCacheKey(id, projectRoot);
6688
+ cacheInvalidationUrls.add(cacheKey);
6689
+ const noQuery = cacheKey.replace(/\?.*$/, '');
6690
+ const stripped = noQuery.replace(/\.(?:[mc]?[jt]sx?)$/i, '');
6691
+ if (stripped !== noQuery) {
6692
+ cacheInvalidationUrls.add(stripped);
6693
+ }
6694
+ };
6695
+ addCacheKey(file);
6696
+ const rootModules = server.moduleGraph.getModulesByFile?.(file);
6697
+ const transitiveImporters = collectAngularTransitiveImportersForInvalidation({
6698
+ modules: rootModules ? Array.from(rootModules) : [],
6699
+ isExcluded: (id) => id.includes('/node_modules/') || isRuntimeGraphExcludedPath(id),
6700
+ maxDepth: 16,
6701
+ });
6702
+ // Invalidate Vite's moduleGraph for the changed file +
6703
+ // every transitive importer so `server.transformRequest`
6704
+ // re-runs the transform pipeline instead of returning
6705
+ // the cached `ModuleNode.transformResult`. We call
6706
+ // `onFileChange` (Vite's authoritative file-changed
6707
+ // signal — walks all module variants including `?v=`,
6708
+ // `?import`, `?t=`) AND per-module `invalidateModule`
6709
+ // for transitive importers (which onFileChange
6710
+ // doesn't reach).
6711
+ try {
6712
+ server.moduleGraph.onFileChange(file);
6713
+ }
6714
+ catch { }
6715
+ if (rootModules) {
6716
+ for (const mod of rootModules) {
6717
+ try {
6718
+ server.moduleGraph.invalidateModule(mod);
6719
+ }
6720
+ catch { }
6721
+ }
6722
+ }
6723
+ for (const mod of transitiveImporters) {
6724
+ addCacheKey(mod?.id);
6725
+ try {
6726
+ server.moduleGraph.invalidateModule(mod);
6727
+ }
6728
+ catch { }
6729
+ }
6730
+ if (cacheInvalidationUrls.size && sharedTransformRequest) {
6731
+ sharedTransformRequest.invalidateMany(cacheInvalidationUrls);
6732
+ if (verbose) {
6733
+ console.log('[hmr-ws][solid] purged shared transform cache entries:', cacheInvalidationUrls.size, 'transitiveImporters=', transitiveImporters.length);
6734
+ }
6735
+ }
6736
+ // Sledgehammer: nuke EVERY entry in sharedTransformRequest's
6737
+ // result cache. The targeted `invalidateMany` above only
6738
+ // clears keys we know about. The `/ns/m/` handler iterates
6739
+ // a long list of candidate extensions (`.ts`, `.js`, `.tsx`,
6740
+ // `.jsx`, `.mjs`, `.mts`, `.cts`, `.vue`, `index.*`) and
6741
+ // EACH candidate is a separate cache key. If a previous
6742
+ // serve populated cache for `/src/components/home.js` (via
6743
+ // extension fallback that resolves to `home.tsx`), our
6744
+ // targeted invalidate misses it and iOS HITs the stale
6745
+ // entry — serving the previous save's transformed code.
6746
+ try {
6747
+ sharedTransformRequest.clear();
6748
+ }
6749
+ catch { }
6750
+ }
6751
+ catch (e) {
6752
+ if (verbose)
6753
+ console.warn('[hmr-ws][solid] transform cache invalidation failed', e);
6754
+ }
6755
+ // Re-run the transform AFTER all caches are invalidated, then
6756
+ // re-upsert the graph so the broadcast hash matches the freshly-
6757
+ // transformed content. The common upsert block above ran
6758
+ // `server.transformRequest` BEFORE invalidation — at that
6759
+ // moment Vite's auto-invalidate hadn't fired yet (it runs after
6760
+ // `plugin.handleHotUpdate`), so the result it cached was the
6761
+ // previous save's. Without this re-transform, the broadcast
6762
+ // carries a stale hash and iOS evaluates the previous save's
6763
+ // bytes ("one save behind").
6764
+ //
6765
+ // We pre-populate the cache for every extension variant Vite's
6766
+ // /ns/m/ handler might try, so the first request from iOS hits
6767
+ // fresh data regardless of which candidate it resolves first.
6768
+ try {
6769
+ const ext = file.match(/\.(?:[mc]?[jt]sx?)$/i)?.[0] || '';
6770
+ const baseSpec = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
6771
+ const baseNoExt = ext ? baseSpec.replace(/\.(?:[mc]?[jt]sx?)$/i, '') : baseSpec;
6772
+ const candidates = Array.from(new Set([
6773
+ baseSpec,
6774
+ baseNoExt,
6775
+ baseNoExt + '.ts',
6776
+ baseNoExt + '.tsx',
6777
+ baseNoExt + '.js',
6778
+ baseNoExt + '.jsx',
6779
+ baseNoExt + '.mjs',
6780
+ baseNoExt + '.mts',
6781
+ baseNoExt + '.cts',
6782
+ file,
6783
+ ]));
6784
+ let freshCode = '';
6785
+ for (const cand of candidates) {
6786
+ try {
6787
+ const fresh = await sharedTransformRequest(cand, 30000);
6788
+ if (fresh?.code && !freshCode)
6789
+ freshCode = fresh.code;
6790
+ }
6791
+ catch { }
6792
+ }
6793
+ if (freshCode) {
6794
+ const existingGm = graph.get(normalizedId);
6795
+ const existingDeps = existingGm?.deps || [];
6796
+ upsertGraphModule(normalizedId, freshCode, existingDeps, {
6797
+ broadcastDelta: false,
6798
+ });
6799
+ }
6800
+ }
6801
+ catch (e) {
6802
+ if (verbose)
6803
+ console.warn('[hmr-ws][solid] post-invalidation re-transform failed', e);
6804
+ }
6805
+ // Broadcast the (now-fresh) delta. Suppressing this in the
6806
+ // common upsert block (`broadcastDelta: ACTIVE_STRATEGY.flavor
6807
+ // !== 'solid'`) and emitting it here ensures the client's
6808
+ // eviction + re-import doesn't race the server's cache
6809
+ // invalidation.
6810
+ try {
6811
+ const gm = graph.get(normalizedId);
6812
+ if (gm) {
6813
+ emitDelta([gm], []);
6814
+ if (verbose) {
6815
+ console.log('[hmr-ws][solid] broadcast delta after cache invalidation', { id: gm.id, hash: gm.hash });
6816
+ }
6817
+ }
6818
+ }
6819
+ catch (e) {
6820
+ if (verbose)
6821
+ console.warn('[hmr-ws][solid] post-invalidation broadcast failed', e);
6822
+ }
6823
+ }
6824
+ catch (e) {
6825
+ if (verbose)
6826
+ console.warn('[hmr-ws][solid] failed to handle hot update for', file, e);
6827
+ }
6828
+ emitHmrUpdateSummary();
5143
6829
  return;
5144
6830
  }
5145
6831
  // Handle .vue file updates
@@ -5148,7 +6834,8 @@ export const piniaSymbol = p.piniaSymbol;
5148
6834
  console.log('[hmr-ws] Not a .vue file, skipping');
5149
6835
  return;
5150
6836
  }
5151
- console.log('[hmr-ws] Processing .vue file update...');
6837
+ if (verbose)
6838
+ console.log('[hmr-ws] Processing .vue file update...');
5152
6839
  try {
5153
6840
  const root = server.config.root || process.cwd();
5154
6841
  let rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
@@ -5249,6 +6936,7 @@ export const piniaSymbol = p.piniaSymbol;
5249
6936
  // Rewrite ONLY .vue imports (everything else is now inlined)
5250
6937
  const projectRoot = server.config.root || process.cwd();
5251
6938
  code = rewriteImports(code, rel, sfcFileMap, depFileMap, projectRoot, opts.verbose, undefined);
6939
+ upsertGraphModule(rel, code, [...deps, ...vueDeps]);
5252
6940
  // Add HMR runtime prelude (CRITICAL for runtime)
5253
6941
  const hmrPrelude = `
5254
6942
  // Embedded HMR Runtime for NativeScript runtime
@@ -5291,7 +6979,7 @@ if (typeof __VUE_HMR_RUNTIME__ === 'undefined') {
5291
6979
  if (typeof spec === 'string' && /^(?:https?:)\/\//.test(spec)) {
5292
6980
  const err = new Error('[ns-hmr][require-guard] require of URL: ' + spec + ' via ' + label);
5293
6981
  const stack = err.stack || '';
5294
- try { console.error(err.message + '\n' + stack); } catch {}
6982
+ console.error(err.message + '\n' + stack);
5295
6983
  try { g.__NS_REQUIRE_GUARD_LAST__ = { spec, stack, label, ts: Date.now() }; } catch {}
5296
6984
  }
5297
6985
  } catch {}
@@ -5324,7 +7012,7 @@ if (typeof __VUE_HMR_RUNTIME__ === 'undefined') {
5324
7012
  version: graphVersion,
5325
7013
  };
5326
7014
  wss.clients.forEach((client) => {
5327
- if (client.readyState === client.OPEN) {
7015
+ if (isSocketClientOpen(client)) {
5328
7016
  client.send(JSON.stringify(registryUpdateMsg));
5329
7017
  }
5330
7018
  });
@@ -5424,6 +7112,10 @@ if (typeof __VUE_HMR_RUNTIME__ === 'undefined') {
5424
7112
  console.warn('[hmr-ws] HMR update failed:', error);
5425
7113
  console.error(error);
5426
7114
  }
7115
+ // Vue path emits update summary at the end of the function so
7116
+ // every framework branch gets exactly one log line. Idempotent
7117
+ // — if any branch already emitted, this is a no-op.
7118
+ emitHmrUpdateSummary();
5427
7119
  // CRITICAL: Return empty array to prevent Vite's default HMR
5428
7120
  return [];
5429
7121
  },
@@ -5495,4 +7187,6 @@ function getServerOrigin(server) {
5495
7187
  // Test-only export: allow unit tests to run the sanitizer on snippets without booting a server
5496
7188
  // Safe in production builds; this is a named export that tests can import explicitly.
5497
7189
  export const __test_processCodeForDevice = processCodeForDevice;
7190
+ export const __test_resolveVendorRouting = resolveVendorRouting;
7191
+ export const __test_getBlockedDeviceNodeModulesReason = getBlockedDeviceNodeModulesReason;
5498
7192
  //# sourceMappingURL=websocket.js.map