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

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 (145) hide show
  1. package/configuration/angular.d.ts +1 -1
  2. package/configuration/angular.js +323 -119
  3. package/configuration/angular.js.map +1 -1
  4. package/configuration/base.js +41 -24
  5. package/configuration/base.js.map +1 -1
  6. package/configuration/javascript.js +3 -3
  7. package/configuration/javascript.js.map +1 -1
  8. package/configuration/solid.js +7 -0
  9. package/configuration/solid.js.map +1 -1
  10. package/configuration/typescript.js +3 -3
  11. package/configuration/typescript.js.map +1 -1
  12. package/helpers/angular/angular-linker.js +39 -34
  13. package/helpers/angular/angular-linker.js.map +1 -1
  14. package/helpers/angular/inline-decorator-component-templates.d.ts +3 -0
  15. package/helpers/angular/inline-decorator-component-templates.js +400 -0
  16. package/helpers/angular/inline-decorator-component-templates.js.map +1 -0
  17. package/helpers/angular/shared-linker.d.ts +7 -0
  18. package/helpers/angular/shared-linker.js +37 -1
  19. package/helpers/angular/shared-linker.js.map +1 -1
  20. package/helpers/angular/synthesize-decorator-ctor-parameters.d.ts +1 -0
  21. package/helpers/angular/synthesize-decorator-ctor-parameters.js +256 -0
  22. package/helpers/angular/synthesize-decorator-ctor-parameters.js.map +1 -0
  23. package/helpers/angular/synthesize-injectable-factories.d.ts +3 -0
  24. package/helpers/angular/synthesize-injectable-factories.js +414 -0
  25. package/helpers/angular/synthesize-injectable-factories.js.map +1 -0
  26. package/helpers/commonjs-plugins.d.ts +5 -2
  27. package/helpers/commonjs-plugins.js +126 -0
  28. package/helpers/commonjs-plugins.js.map +1 -1
  29. package/helpers/esbuild-platform-resolver.js +5 -5
  30. package/helpers/esbuild-platform-resolver.js.map +1 -1
  31. package/helpers/external-configs.d.ts +9 -1
  32. package/helpers/external-configs.js +31 -6
  33. package/helpers/external-configs.js.map +1 -1
  34. package/helpers/import-meta-path.d.ts +4 -0
  35. package/helpers/import-meta-path.js +5 -0
  36. package/helpers/import-meta-path.js.map +1 -0
  37. package/helpers/import-specifier.d.ts +1 -0
  38. package/helpers/import-specifier.js +18 -0
  39. package/helpers/import-specifier.js.map +1 -0
  40. package/helpers/main-entry.d.ts +5 -2
  41. package/helpers/main-entry.js +112 -95
  42. package/helpers/main-entry.js.map +1 -1
  43. package/helpers/nativeclass-transform.js +8 -127
  44. package/helpers/nativeclass-transform.js.map +1 -1
  45. package/helpers/nativeclass-transformer-plugin.d.ts +12 -1
  46. package/helpers/nativeclass-transformer-plugin.js +175 -36
  47. package/helpers/nativeclass-transformer-plugin.js.map +1 -1
  48. package/hmr/client/css-handler.js +60 -20
  49. package/hmr/client/css-handler.js.map +1 -1
  50. package/hmr/client/index.js +524 -23
  51. package/hmr/client/index.js.map +1 -1
  52. package/hmr/client/utils.js +57 -6
  53. package/hmr/client/utils.js.map +1 -1
  54. package/hmr/entry-runtime.d.ts +10 -0
  55. package/hmr/entry-runtime.js +263 -21
  56. package/hmr/entry-runtime.js.map +1 -1
  57. package/hmr/frameworks/angular/client/index.d.ts +2 -1
  58. package/hmr/frameworks/angular/client/index.js +72 -19
  59. package/hmr/frameworks/angular/client/index.js.map +1 -1
  60. package/hmr/frameworks/angular/server/linker.js +36 -2
  61. package/hmr/frameworks/angular/server/linker.js.map +1 -1
  62. package/hmr/frameworks/angular/server/strategy.js +20 -5
  63. package/hmr/frameworks/angular/server/strategy.js.map +1 -1
  64. package/hmr/frameworks/typescript/server/strategy.js +8 -2
  65. package/hmr/frameworks/typescript/server/strategy.js.map +1 -1
  66. package/hmr/helpers/ast-normalizer.js +22 -10
  67. package/hmr/helpers/ast-normalizer.js.map +1 -1
  68. package/hmr/server/constants.d.ts +1 -0
  69. package/hmr/server/constants.js +2 -0
  70. package/hmr/server/constants.js.map +1 -1
  71. package/hmr/server/core-sanitize.d.ts +43 -0
  72. package/hmr/server/core-sanitize.js +219 -13
  73. package/hmr/server/core-sanitize.js.map +1 -1
  74. package/hmr/server/import-map.d.ts +65 -0
  75. package/hmr/server/import-map.js +219 -0
  76. package/hmr/server/import-map.js.map +1 -0
  77. package/hmr/server/index.d.ts +2 -1
  78. package/hmr/server/index.js.map +1 -1
  79. package/hmr/server/runtime-graph-filter.d.ts +5 -0
  80. package/hmr/server/runtime-graph-filter.js +21 -0
  81. package/hmr/server/runtime-graph-filter.js.map +1 -0
  82. package/hmr/server/shared-transform-request.d.ts +12 -0
  83. package/hmr/server/shared-transform-request.js +137 -0
  84. package/hmr/server/shared-transform-request.js.map +1 -0
  85. package/hmr/server/vite-plugin.d.ts +21 -1
  86. package/hmr/server/vite-plugin.js +443 -22
  87. package/hmr/server/vite-plugin.js.map +1 -1
  88. package/hmr/server/websocket-angular-entry.d.ts +2 -0
  89. package/hmr/server/websocket-angular-entry.js +68 -0
  90. package/hmr/server/websocket-angular-entry.js.map +1 -0
  91. package/hmr/server/websocket-angular-hot-update.d.ts +61 -0
  92. package/hmr/server/websocket-angular-hot-update.js +239 -0
  93. package/hmr/server/websocket-angular-hot-update.js.map +1 -0
  94. package/hmr/server/websocket-core-bridge.d.ts +23 -0
  95. package/hmr/server/websocket-core-bridge.js +360 -0
  96. package/hmr/server/websocket-core-bridge.js.map +1 -0
  97. package/hmr/server/websocket-graph-upsert.d.ts +6 -0
  98. package/hmr/server/websocket-graph-upsert.js +13 -0
  99. package/hmr/server/websocket-graph-upsert.js.map +1 -0
  100. package/hmr/server/websocket-module-bindings.d.ts +6 -0
  101. package/hmr/server/websocket-module-bindings.js +471 -0
  102. package/hmr/server/websocket-module-bindings.js.map +1 -0
  103. package/hmr/server/websocket-module-specifiers.d.ts +37 -0
  104. package/hmr/server/websocket-module-specifiers.js +637 -0
  105. package/hmr/server/websocket-module-specifiers.js.map +1 -0
  106. package/hmr/server/websocket.d.ts +26 -3
  107. package/hmr/server/websocket.js +1402 -678
  108. package/hmr/server/websocket.js.map +1 -1
  109. package/hmr/shared/package-classifier.d.ts +9 -0
  110. package/hmr/shared/package-classifier.js +58 -0
  111. package/hmr/shared/package-classifier.js.map +1 -0
  112. package/hmr/shared/runtime/browser-runtime-contract.d.ts +64 -0
  113. package/hmr/shared/runtime/browser-runtime-contract.js +54 -0
  114. package/hmr/shared/runtime/browser-runtime-contract.js.map +1 -0
  115. package/hmr/shared/runtime/dev-overlay.d.ts +38 -0
  116. package/hmr/shared/runtime/dev-overlay.js +675 -0
  117. package/hmr/shared/runtime/dev-overlay.js.map +1 -0
  118. package/hmr/shared/runtime/http-only-boot.d.ts +1 -0
  119. package/hmr/shared/runtime/http-only-boot.js +53 -6
  120. package/hmr/shared/runtime/http-only-boot.js.map +1 -1
  121. package/hmr/shared/runtime/module-provenance.d.ts +1 -0
  122. package/hmr/shared/runtime/module-provenance.js +66 -0
  123. package/hmr/shared/runtime/module-provenance.js.map +1 -0
  124. package/hmr/shared/runtime/platform-polyfills.d.ts +26 -0
  125. package/hmr/shared/runtime/platform-polyfills.js +122 -0
  126. package/hmr/shared/runtime/platform-polyfills.js.map +1 -0
  127. package/hmr/shared/runtime/root-placeholder.d.ts +1 -0
  128. package/hmr/shared/runtime/root-placeholder.js +576 -76
  129. package/hmr/shared/runtime/root-placeholder.js.map +1 -1
  130. package/hmr/shared/runtime/session-bootstrap.d.ts +1 -0
  131. package/hmr/shared/runtime/session-bootstrap.js +146 -0
  132. package/hmr/shared/runtime/session-bootstrap.js.map +1 -0
  133. package/hmr/shared/runtime/vendor-bootstrap.js +51 -6
  134. package/hmr/shared/runtime/vendor-bootstrap.js.map +1 -1
  135. package/hmr/shared/vendor/manifest.d.ts +7 -0
  136. package/hmr/shared/vendor/manifest.js +363 -23
  137. package/hmr/shared/vendor/manifest.js.map +1 -1
  138. package/hmr/shared/vendor/registry.js +104 -7
  139. package/hmr/shared/vendor/registry.js.map +1 -1
  140. package/package.json +12 -2
  141. package/runtime/core-aliases-early.js +83 -32
  142. package/runtime/core-aliases-early.js.map +1 -1
  143. package/shims/solid-jsx-runtime.d.ts +7 -0
  144. package/shims/solid-jsx-runtime.js +17 -0
  145. package/shims/solid-jsx-runtime.js.map +1 -0
@@ -1,15 +1,14 @@
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
3
  // AST tooling for robust transformations
4
4
  import { parse as babelParse } from '@babel/parser';
5
5
  import { genCode } from '../helpers/babel.js';
6
6
  import babelCore from '@babel/core';
7
- import pluginTransformTypescript from '@babel/plugin-transform-typescript';
8
7
  import traverse from '@babel/traverse';
9
8
  // Ensure traverse callable across CJS/ESM builds
10
9
  const babelTraverse = traverse?.default || traverse;
11
10
  import * as t from '@babel/types';
12
- import { existsSync, readFileSync } from 'fs';
11
+ import { existsSync, readFileSync, statSync } from 'fs';
13
12
  import { astNormalizeModuleImportsAndHelpers, astVerifyAndAnnotateDuplicates } from '../helpers/ast-normalizer.js';
14
13
  import { stripRtCoreSentinel, stripDanglingViteCjsImports } from '../helpers/sanitize.js';
15
14
  import { WebSocketServer } from 'ws';
@@ -17,7 +16,7 @@ import * as path from 'path';
17
16
  import { createHash } from 'crypto';
18
17
  import * as PAT from './constants.js';
19
18
  import { getVendorManifest, resolveVendorSpecifier } from '../shared/vendor/registry.js';
20
- import { getPackageJson, getProjectFilePath } from '../../helpers/project.js';
19
+ import { getPackageJson, getProjectFilePath, getProjectRootPath } from '../../helpers/project.js';
21
20
  import { loadPrebuiltVendorManifest } from '../shared/vendor/manifest-loader.js';
22
21
  import '../vendor-bootstrap.js';
23
22
  import { NS_NATIVE_TAGS } from './compiler.js';
@@ -30,6 +29,41 @@ import { typescriptServerStrategy } from '../frameworks/typescript/server/strate
30
29
  import { buildInlineTemplateBlock, createProcessSfcCode, extractTemplateRender, processTemplateVariantMinimal } from '../frameworks/vue/server/sfc-transforms.js';
31
30
  import { astExtractImportsAndStripTypes } from '../helpers/ast-extract.js';
32
31
  import { getProjectAppPath, getProjectAppRelativePath, getProjectAppVirtualPath } from '../../helpers/utils.js';
32
+ import { buildRuntimeConfig, generateImportMap } from './import-map.js';
33
+ import { getCliFlags } from '../../helpers/cli-flags.js';
34
+ import { isRuntimeGraphExcludedPath, matchesRuntimeGraphModuleId, normalizeRuntimeGraphPath, shouldIncludeRuntimeGraphFile, shouldSkipRuntimeGraphDirectoryName } from './runtime-graph-filter.js';
35
+ import { resolveAngularCoreHmrImportSource, rewriteAngularEntryRegisterOnly } from './websocket-angular-entry.js';
36
+ import { canonicalizeTransformRequestCacheKey, collectAngularHotUpdateRoots, collectAngularTransformCacheInvalidationUrls, collectAngularTransitiveImportersForInvalidation, collectGraphUpdateModulesForHotUpdate, normalizeHotReloadMatchPath, shouldInvalidateAngularTransitiveImporters, shouldSuppressDefaultViteHotUpdate, shouldSuppressViteFullReloadPayload } from './websocket-angular-hot-update.js';
37
+ import { classifyGraphUpsert, shouldBroadcastGraphUpsertDelta } from './websocket-graph-upsert.js';
38
+ import { extractVitePrebundleId, filterExistingNodeModulesTransformCandidates, getBlockedDeviceNodeModulesReason, getFlattenedManifestMap, isCoreGlobalsReference, isEsmFrameworkPackageSpecifier, isLikelyNativeScriptPluginSpecifier, isLikelyNativeScriptRuntimePluginSpecifier, isNativeScriptCoreModule, isNativeScriptPluginModule, normalizeNativeScriptCoreSpecifier, normalizeNodeModulesSpecifier, resolveCandidateFilePath, resolveInternalRuntimePluginBareSpecifier, resolveNodeModulesPackageBoundary, resolveVendorFromCandidate, resolveVendorRouting, shouldPreserveBareRuntimePluginSubpathImport, stripDecoratedServePrefixes, tryReadRawExplicitJavaScriptModule, viteDepsPathToBareSpecifier, } from './websocket-module-specifiers.js';
39
+ import { ensureNativeScriptModuleBindings, getProcessCodeResolvedSpecifierOverrides } from './websocket-module-bindings.js';
40
+ import { buildVersionedCoreMainBridgeModule, buildVersionedCoreSubpathAliasModule, collectStaticExportNamesFromFile, collectStaticExportOriginsFromFile, ensureVersionedCoreImports, extractDirectExportedNames, hasModuleDefaultExport, normalizeCoreExportOriginsForRuntime, parseCoreBridgeRequest, resolveRuntimeCoreModulePath } from './websocket-core-bridge.js';
41
+ import { createSharedTransformRequestRunner } from './shared-transform-request.js';
42
+ export { ensureNativeScriptModuleBindings, getProcessCodeResolvedSpecifierOverrides } from './websocket-module-bindings.js';
43
+ export { stripDecoratedServePrefixes, tryReadRawExplicitJavaScriptModule } from './websocket-module-specifiers.js';
44
+ export { buildVersionedCoreMainBridgeModule, buildVersionedCoreSubpathAliasModule, collectStaticExportNamesFromFile, collectStaticExportOriginsFromFile, ensureVersionedCoreImports, normalizeCoreExportOriginsForRuntime, parseCoreBridgeRequest } from './websocket-core-bridge.js';
45
+ export { rewriteAngularEntryRegisterOnly } from './websocket-angular-entry.js';
46
+ export { canonicalizeTransformRequestCacheKey, collectAngularHotUpdateRoots, collectAngularTransformCacheInvalidationUrls, collectAngularTransitiveImportersForInvalidation, collectGraphUpdateModulesForHotUpdate, createSharedTransformRequestRunner, normalizeHotReloadMatchPath, shouldInvalidateAngularTransitiveImporters, shouldSuppressDefaultViteHotUpdate, shouldSuppressViteFullReloadPayload, classifyGraphUpsert, shouldBroadcastGraphUpsertDelta };
47
+ const pluginTransformTypescript = (() => {
48
+ const requireFromHere = createRequire(import.meta.url);
49
+ const loaded = requireFromHere('@babel/plugin-transform-typescript');
50
+ return loaded?.default || loaded;
51
+ })();
52
+ // Build a serialized process.env object from CLI --env.* flags.
53
+ // This is injected into every HTTP-served module so app code referencing
54
+ // process.env.TEST_ENV (etc.) works on device in HMR dev mode.
55
+ const __processEnvEntries = { NODE_ENV: 'development' };
56
+ try {
57
+ const flags = getCliFlags();
58
+ for (const [k, v] of Object.entries(flags || {})) {
59
+ // Skip internal NativeScript build flags
60
+ if (['ios', 'android', 'visionos', 'platform', 'hmr', 'verbose'].includes(k))
61
+ continue;
62
+ __processEnvEntries[k] = String(v);
63
+ }
64
+ }
65
+ catch { }
66
+ const __processEnvJson = JSON.stringify(__processEnvEntries);
33
67
  const { parse, compileTemplate, compileScript } = vueSfcCompiler;
34
68
  const APP_ROOT_DIR = getProjectAppPath();
35
69
  const APP_VIRTUAL_PREFIX = getProjectAppVirtualPath();
@@ -43,255 +77,177 @@ const STRATEGY_REGISTRY = new Map([
43
77
  ['typescript', typescriptServerStrategy],
44
78
  ]);
45
79
  function resolveFrameworkStrategy(flavor) {
46
- return STRATEGY_REGISTRY.get(flavor);
80
+ const strategy = STRATEGY_REGISTRY.get(flavor);
81
+ if (!strategy) {
82
+ throw new Error(`[ns-hmr] Unsupported framework strategy: ${flavor}`);
83
+ }
84
+ return strategy;
47
85
  }
48
86
  let ACTIVE_STRATEGY;
87
+ function isSocketClientOpen(client) {
88
+ if (!client) {
89
+ return false;
90
+ }
91
+ const openState = typeof client.OPEN === 'number' ? client.OPEN : 1;
92
+ return client.readyState === openState;
93
+ }
94
+ function getHmrSocketRoleFromRequestUrl(requestUrl) {
95
+ try {
96
+ const url = new URL(requestUrl || '/ns-hmr', 'http://localhost');
97
+ return url.searchParams.get('ns_hmr_role') || 'unknown';
98
+ }
99
+ catch {
100
+ return 'unknown';
101
+ }
102
+ }
103
+ function getHmrSocketRole(client) {
104
+ if (!client) {
105
+ return 'unknown';
106
+ }
107
+ return typeof client.__nsHmrClientRole === 'string' && client.__nsHmrClientRole ? client.__nsHmrClientRole : 'unknown';
108
+ }
109
+ function shouldAllowLocalCoreSanitizerPaths(contextLabel) {
110
+ return /\bnode_modules\/@nativescript\/vite\/hmr\/(?:client|frameworks)\//.test(contextLabel);
111
+ }
112
+ export function prepareAngularEntryForDevice(code, importerPath, sfcFileMap, depFileMap, projectRoot, verbose = false, outputDirOverrideRel, httpOrigin, resolveVendorAsHttp = false) {
113
+ const rewrittenCode = rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot, verbose, outputDirOverrideRel, httpOrigin, resolveVendorAsHttp);
114
+ return rewriteAngularEntryRegisterOnly(rewrittenCode, resolveAngularCoreHmrImportSource(rewrittenCode, httpOrigin));
115
+ }
49
116
  const processSfcCode = createProcessSfcCode(processCodeForDevice);
50
117
  // Bare specifiers and special skip patterns (virtual, data:, etc.)
51
118
  const VENDOR_PACKAGES = /^[A-Za-z@][^:\/\s]*$/;
52
119
  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);
120
+ const MODULE_IMPORT_ANALYSIS_PLUGINS = ['typescript', 'jsx', 'importMeta', 'topLevelAwait', 'classProperties', 'classPrivateProperties', 'classPrivateMethods', 'decorators-legacy'];
121
+ function collectTopLevelImportRecords(code) {
122
+ if (!code || typeof code !== 'string' || !/\bimport\b/.test(code)) {
123
+ return [];
124
+ }
125
+ try {
126
+ const ast = babelParse(code, {
127
+ sourceType: 'module',
128
+ plugins: MODULE_IMPORT_ANALYSIS_PLUGINS,
129
+ });
130
+ const body = ast?.program?.body;
131
+ if (!Array.isArray(body)) {
132
+ return [];
73
133
  }
134
+ return body
135
+ .filter((node) => t.isImportDeclaration(node) && typeof node.start === 'number' && typeof node.end === 'number' && typeof node.source?.value === 'string')
136
+ .map((node) => ({
137
+ start: node.start,
138
+ end: node.end,
139
+ text: code.slice(node.start, node.end),
140
+ source: node.source.value,
141
+ hasOnlyNamedSpecifiers: Array.isArray(node.specifiers) && node.specifiers.length > 0 && node.specifiers.every((spec) => t.isImportSpecifier(spec)),
142
+ namedBindings: Array.isArray(node.specifiers)
143
+ ? node.specifiers
144
+ .filter((spec) => t.isImportSpecifier(spec) && typeof spec.start === 'number' && typeof spec.end === 'number')
145
+ .map((spec) => ({
146
+ importedName: t.isIdentifier(spec.imported) ? spec.imported.name : String(spec.imported?.value || ''),
147
+ text: code.slice(spec.start, spec.end),
148
+ }))
149
+ : [],
150
+ }));
151
+ }
152
+ catch {
153
+ return [];
74
154
  }
75
- return map;
76
- }
77
- // NativeScript module detectors
78
- function isCoreGlobalsReference(spec) {
79
- return /@nativescript(?:[\/_-])core(?:[\/_-])globals/.test(spec || '');
80
- }
81
- function isNativeScriptCoreModule(spec) {
82
- return /^(?:@nativescript[\/_-]core|@nativescript\/core)(?:\b|\/)/i.test(spec || '');
83
155
  }
84
- function isNativeScriptPluginModule(spec) {
85
- return /^@nativescript\//i.test(spec || '') && !isNativeScriptCoreModule(spec || '');
156
+ function hoistTopLevelStaticImports(code) {
157
+ const imports = collectTopLevelImportRecords(code);
158
+ if (!imports.length) {
159
+ return code;
160
+ }
161
+ let stripped = code;
162
+ for (const imp of [...imports].sort((left, right) => right.start - left.start)) {
163
+ stripped = stripped.slice(0, imp.start) + stripped.slice(imp.end);
164
+ }
165
+ const hoisted = [];
166
+ const seen = new Set();
167
+ for (const imp of imports) {
168
+ const text = imp.text.trim();
169
+ if (!text || seen.has(text)) {
170
+ continue;
171
+ }
172
+ seen.add(text);
173
+ hoisted.push(text);
174
+ }
175
+ if (!hoisted.length) {
176
+ return stripped;
177
+ }
178
+ return `${hoisted.join('\n')}\n${stripped.replace(/^\s*\n+/, '')}`;
86
179
  }
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;
180
+ export function buildBootProgressSnippet(bootModuleLabel) {
181
+ const normalizedLabel = JSON.stringify(String(bootModuleLabel || '').replace(/\\/g, '/'));
182
+ return [
183
+ `const __nsBootGlobal=globalThis;`,
184
+ `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){}`,
185
+ `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));}}`,
186
+ '',
187
+ ].join('\n');
114
188
  }
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);
189
+ function rewriteVitePrebundleImportsForDevice(code, preserveVendorImports) {
190
+ const imports = collectTopLevelImportRecords(code);
191
+ if (!imports.length) {
192
+ return code;
193
+ }
194
+ const edits = [];
195
+ for (const imp of imports) {
196
+ const source = imp.source;
197
+ const depMatch = source.match(/(?:^|\/)node_modules\/\.vite\/deps\/(.+)$/);
198
+ const depPath = depMatch?.[1] || (source.startsWith('.vite/deps/') ? source.slice('.vite/deps/'.length) : null);
199
+ if (!depPath) {
200
+ continue;
136
201
  }
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
- });
202
+ let replacement = '';
203
+ if (preserveVendorImports) {
204
+ const canonical = resolveVendorFromCandidate(`.vite/deps/${depPath}`);
205
+ const bareSpecifier = canonical || viteDepsPathToBareSpecifier(depPath);
206
+ if (bareSpecifier) {
207
+ replacement = imp.text.replace(source, bareSpecifier);
154
208
  }
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
209
  }
210
+ edits.push({
211
+ start: imp.start,
212
+ end: imp.end,
213
+ text: replacement,
214
+ });
215
+ }
216
+ if (!edits.length) {
240
217
  return code;
241
218
  }
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`;
219
+ let next = code;
220
+ for (const edit of edits.sort((left, right) => right.start - left.start)) {
221
+ next = next.slice(0, edit.start) + edit.text + next.slice(edit.end);
222
+ }
223
+ return next;
224
+ }
225
+ function buildNodeModuleProvenancePrelude(sourceId) {
226
+ if (!sourceId) {
227
+ return '';
228
+ }
229
+ const cleaned = sourceId.replace(PAT.QUERY_PATTERN, '');
230
+ let normalized = normalizeNodeModulesSpecifier(cleaned);
231
+ if (!normalized) {
232
+ const viteDepsMatch = cleaned.match(/(?:^|\/)node_modules\/\.vite\/deps\/([^?#]+)/);
233
+ if (viteDepsMatch?.[1]) {
234
+ normalized = `.vite/deps/${viteDepsMatch[1]}`;
289
235
  }
290
236
  }
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;
237
+ if (!normalized) {
238
+ return '';
239
+ }
240
+ let packageSpecifier = normalized;
241
+ let via = 'node_modules';
242
+ if (normalized.startsWith('.vite/deps/')) {
243
+ via = 'vite-deps';
244
+ packageSpecifier = viteDepsPathToBareSpecifier(normalized.slice('.vite/deps/'.length)) || normalized;
245
+ }
246
+ const rootPackage = resolveNodeModulesPackageBoundary(packageSpecifier, getProjectRootPath()).packageName;
247
+ if (!rootPackage) {
248
+ return '';
249
+ }
250
+ 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
251
  }
296
252
  // Guard any bare dynamic import(spec) occurring in assembled module code.
297
253
  // We cannot override native dynamic import globally; for SFC assembler outputs we inline
@@ -318,113 +274,6 @@ function guardBareDynamicImports(code) {
318
274
  return code;
319
275
  }
320
276
  }
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
277
  function stripCoreGlobalsImports(code) {
429
278
  const pattern = /^\s*(?:import\s+(?:[^'"\n]*from\s+)?|export\s+\*\s+from\s+)["'][^"']*(?:@nativescript(?:[/_-])core(?:[\/_-])globals|@nativescript_core_globals)[^"']*["'];?\s*$/gm;
430
279
  return code.replace(pattern, '');
@@ -452,18 +301,14 @@ function ensureVariableDynamicImportHelper(code) {
452
301
  `};\n`;
453
302
  return `${helper}${code}`;
454
303
  }
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
304
  function ensureGuardPlainDynamicImports(code, origin) {
459
305
  try {
460
306
  if (!code || !/\bimport\s*\(/.test(code))
461
307
  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)
308
+ 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
309
  const replaced = code.replace(/(^|[^\.\w$])import\s*\(/g, (_m, p1) => `${p1}__ns_import(`);
465
310
  if (replaced !== code) {
466
- return w + replaced;
311
+ return wrapper + replaced;
467
312
  }
468
313
  return code;
469
314
  }
@@ -471,13 +316,84 @@ function ensureGuardPlainDynamicImports(code, origin) {
471
316
  return code;
472
317
  }
473
318
  }
474
- // Heal accidental "import ... = expr" assignments produced by upstream transforms.
475
- // These are invalid JS; convert to equivalent const assignments.
319
+ function ensureDynamicHmrImportHelper(code) {
320
+ try {
321
+ if (!code.includes('__nsDynamicHmrImport('))
322
+ return code;
323
+ if (code.includes('const __nsDynamicHmrImport ='))
324
+ return code;
325
+ const helper = 'const __nsDynamicHmrImport = (spec) => {\n' +
326
+ " const __nsm = '/ns' + '/m';\n" +
327
+ " const __nsBootPrefix = typeof import.meta !== 'undefined' && import.meta && typeof import.meta.url === 'string' && import.meta.url.includes('/__ns_boot__/b1/') ? '/__ns_boot__/b1' : '';\n" +
328
+ " const __nsImporterTagMatch = typeof import.meta !== 'undefined' && import.meta && typeof import.meta.url === 'string' ? import.meta.url.match(/\\/__ns_hmr__\\/([^/]+)\\//) : null;\n" +
329
+ " const __nsImporterTag = __nsImporterTagMatch && __nsImporterTagMatch[1] ? decodeURIComponent(__nsImporterTagMatch[1]) : '';\n" +
330
+ " try { if (!spec || spec === '@') { return import(new URL(__nsm + '/__invalid_at__.mjs', import.meta.url).href); } } catch {}\n" +
331
+ ' try {\n' +
332
+ " if (typeof spec === 'string' && spec.startsWith(__nsm + '/')) {\n" +
333
+ ' const g = globalThis;\n' +
334
+ " const graphVersion = typeof g.__NS_HMR_GRAPH_VERSION__ === 'number' ? g.__NS_HMR_GRAPH_VERSION__ : 0;\n" +
335
+ " const nonce = typeof g.__NS_HMR_IMPORT_NONCE__ === 'number' ? g.__NS_HMR_IMPORT_NONCE__ : 0;\n" +
336
+ " const __nsActiveBootPrefix = graphVersion || nonce ? '' : __nsBootPrefix;\n" +
337
+ " if (spec.includes('/__ns_hmr__/')) {\n" +
338
+ " const __preservedSpec = !nonce && __nsBootPrefix && spec.startsWith(__nsm + '/__ns_hmr__/') && !spec.includes('/node_modules/') ? __nsm + __nsBootPrefix + spec.slice(__nsm.length) : spec;\n" +
339
+ ' return import(new URL(__preservedSpec, import.meta.url).href);\n' +
340
+ ' }\n' +
341
+ " if (spec.startsWith(__nsm + '/node_modules/')) { return import(new URL(spec, import.meta.url).href); }\n" +
342
+ " const tag = nonce ? `n${nonce}` : (graphVersion ? `v${graphVersion}` : (__nsImporterTag || 'live'));\n" +
343
+ " const nextPath = __nsm + __nsActiveBootPrefix + '/__ns_hmr__/' + encodeURIComponent(tag) + spec.slice(__nsm.length);\n" +
344
+ " const origin = typeof g.__NS_HTTP_ORIGIN__ === 'string' && /^https?:\\/\\//.test(g.__NS_HTTP_ORIGIN__) ? g.__NS_HTTP_ORIGIN__ : '';\n" +
345
+ ' return import(origin ? origin + nextPath : new URL(nextPath, import.meta.url).href);\n' +
346
+ ' }\n' +
347
+ ' } catch {}\n' +
348
+ ' return import(spec);\n' +
349
+ '};\n';
350
+ return helper + code;
351
+ }
352
+ catch {
353
+ return code;
354
+ }
355
+ }
356
+ async function expandStarExports(code, server, projectRoot, verbose) {
357
+ const STAR_RE = /^[ \t]*(export\s+\*\s+from\s+["'])([^"']+)(["'];?)[ \t]*$/gm;
358
+ let match;
359
+ const replacements = [];
360
+ while ((match = STAR_RE.exec(code)) !== null) {
361
+ const url = match[2];
362
+ if (!url.includes('/node_modules/'))
363
+ continue;
364
+ replacements.push({ full: match[0], url, prefix: match[1], suffix: match[3] });
365
+ }
366
+ if (!replacements.length)
367
+ return code;
368
+ for (const rep of replacements) {
369
+ try {
370
+ let vitePath = rep.url.replace(/^https?:\/\/[^/]+/, '');
371
+ vitePath = vitePath.replace(/^\/ns\/m\//, '/');
372
+ vitePath = vitePath.replace(/^\/__ns_boot__\/[^/]+/, '');
373
+ vitePath = vitePath.replace(/\/__ns_hmr__\/[^/]+/, '');
374
+ const result = await server.transformRequest(vitePath);
375
+ if (!result?.code)
376
+ continue;
377
+ const names = extractExportedNames(result.code);
378
+ if (!names.length)
379
+ continue;
380
+ const explicit = `export { ${names.join(', ')} } from ${JSON.stringify(rep.url)};`;
381
+ code = code.replace(rep.full, explicit);
382
+ if (verbose) {
383
+ console.log(`[ns/m] expanded export* -> ${names.length} names from ${vitePath}`);
384
+ }
385
+ }
386
+ catch { }
387
+ }
388
+ return code;
389
+ }
390
+ function extractExportedNames(code) {
391
+ return extractDirectExportedNames(code);
392
+ }
476
393
  function repairImportEqualsAssignments(code) {
477
394
  try {
478
395
  if (!code || typeof code !== 'string')
479
396
  return code;
480
- // import { a, b as c } = expr; -> const { a, b: c } = expr;
481
397
  code = code.replace(/(^|\n)\s*import\s*\{([^}]+)\}\s*=\s*([^;]+);?/g, (_m, p1, specList, rhs) => {
482
398
  const cleaned = String(specList)
483
399
  .split(',')
@@ -487,56 +403,28 @@ function repairImportEqualsAssignments(code) {
487
403
  .join(', ');
488
404
  return `${p1}const { ${cleaned} } = ${rhs};`;
489
405
  });
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
- });
406
+ 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});`);
407
+ code = code.replace(/(^|\n)\s*import\s+([A-Za-z_$][\w$]*)\s*=\s*([^;]+);?/g, (_m, p1, id, rhs) => `${p1}const ${id} = ${rhs};`);
498
408
  }
499
409
  catch { }
500
410
  return code;
501
411
  }
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
412
  function ensureVersionedRtImports(code, origin, ver) {
505
413
  if (!code || !origin || !Number.isFinite(ver))
506
414
  return code;
507
- // Static imports: import { ... } from ".../ns/rt" (plus optional version)
508
415
  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
416
  code = code.replace(/(import\(\s*["'])(?:https?:\/\/[^"']+)?\/(?:\@ns|ns)\/rt(?:\/[\d]+)?(["']\s*\))/g, (_m, p1, p3) => `${p1}/ns/rt/${ver}${p3}`);
511
417
  return code;
512
418
  }
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
419
  function stripViteDynamicImportVirtual(code) {
529
420
  if (!/\/@id\/__x00__vite\/dynamic-import-helper/.test(code)) {
530
421
  return code;
531
422
  }
532
423
  const original = code;
533
- // Remove any import lines referencing the virtual helper (with or without bindings)
534
424
  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
425
  if (/\/@id\/__x00__vite\/dynamic-import-helper/.test(code)) {
537
426
  code = code.replace(/\/@id\/__x00__vite\/dynamic-import-helper[^"'`)]*/g, '/__NS_UNUSED_DYNAMIC_IMPORT_HELPER__');
538
427
  }
539
- // Ensure helper present
540
428
  if (!/__variableDynamicImportRuntimeHelper/.test(code)) {
541
429
  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
430
  code = inline + code;
@@ -546,17 +434,7 @@ function stripViteDynamicImportVirtual(code) {
546
434
  }
547
435
  return code;
548
436
  }
549
- // Small snippet injected into device-delivered modules to capture any require('http(s)://') calls
550
437
  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
- */
560
438
  function shouldRemapImport(spec) {
561
439
  if (!spec || typeof spec !== 'string')
562
440
  return false;
@@ -577,12 +455,9 @@ function shouldRemapImport(spec) {
577
455
  }
578
456
  return true;
579
457
  }
580
- // (legacy wrapSfcWithStableDefault removed; full SFCs now delegate to /ns/asm)
581
458
  function removeNamedImports(code, names) {
582
459
  const regex = /^(\s*import\s*\{)([^}]*)(\}\s*from\s*['"][^'"]+['"];?)/gm;
583
460
  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
461
  const srcMatch = /from\s*['"]\s*([^'"\s]+)\s*['"]/i.exec(_m);
587
462
  const src = (srcMatch?.[1] || '').toLowerCase();
588
463
  const isVueSource = /^(?:vue|nativescript-vue)(?:\b|\/)/i.test(src);
@@ -681,7 +556,7 @@ function normalizeImportPath(spec, importerDir) {
681
556
  else if (spec.startsWith('./') || spec.startsWith('../')) {
682
557
  key = path.posix.normalize(path.posix.join(importerDir, spec));
683
558
  if (!key.startsWith('/')) {
684
- key = '/' + key;
559
+ key = `/${key}`;
685
560
  }
686
561
  }
687
562
  else {
@@ -748,6 +623,70 @@ function findDependencyFileName(depFileMap, key) {
748
623
  }
749
624
  return undefined;
750
625
  }
626
+ function isRuntimePluginRootEntrySpecifier(specifier, projectRoot) {
627
+ if (!specifier) {
628
+ return false;
629
+ }
630
+ const cleaned = specifier.replace(PAT.QUERY_PATTERN, '');
631
+ const normalized = normalizeNodeModulesSpecifier(cleaned) || cleaned.replace(/^\/+/, '');
632
+ if (!normalized) {
633
+ return false;
634
+ }
635
+ const { packageName, subpath } = resolveNodeModulesPackageBoundary(normalized, projectRoot);
636
+ if (!packageName || !isLikelyNativeScriptRuntimePluginSpecifier(packageName, projectRoot)) {
637
+ return false;
638
+ }
639
+ if (!subpath) {
640
+ return true;
641
+ }
642
+ if (subpath.includes('/')) {
643
+ return false;
644
+ }
645
+ const pkgBaseName = packageName.split('/').pop() || '';
646
+ const withoutExt = /(?:\.(?:ios|android|visionos))?\.(?:ts|tsx|js|jsx|mjs|mts|cts)$/i.test(subpath) ? subpath.replace(/\.[^.]+$/, '') : subpath;
647
+ const withoutPlatform = withoutExt.replace(/\.(ios|android|visionos)$/i, '');
648
+ return withoutPlatform === 'index' || withoutPlatform === pkgBaseName;
649
+ }
650
+ function collectMixedRuntimePluginHttpRootPackages(code, projectRoot) {
651
+ const nonRootSubpathPackages = new Set();
652
+ const rootEntryPackages = new Set();
653
+ const visitSpecifier = (rawSpecifier) => {
654
+ if (!rawSpecifier) {
655
+ return;
656
+ }
657
+ const specifier = normalizeNativeScriptCoreSpecifier(rawSpecifier).replace(PAT.QUERY_PATTERN, '');
658
+ if (!specifier) {
659
+ return;
660
+ }
661
+ if (/^https?:\/\//.test(specifier) || specifier.startsWith('/ns/')) {
662
+ return;
663
+ }
664
+ if (/^(?:\.|\/)/.test(specifier) && !specifier.includes('/node_modules/')) {
665
+ return;
666
+ }
667
+ const normalized = normalizeNodeModulesSpecifier(specifier) || specifier.replace(/^\/+/, '');
668
+ if (!normalized) {
669
+ return;
670
+ }
671
+ const { packageName } = resolveNodeModulesPackageBoundary(normalized, projectRoot);
672
+ if (!packageName || !isLikelyNativeScriptRuntimePluginSpecifier(packageName, projectRoot)) {
673
+ return;
674
+ }
675
+ if (isRuntimePluginRootEntrySpecifier(normalized, projectRoot)) {
676
+ rootEntryPackages.add(packageName);
677
+ return;
678
+ }
679
+ nonRootSubpathPackages.add(packageName);
680
+ };
681
+ for (const pattern of [PAT.IMPORT_PATTERN_1, PAT.IMPORT_PATTERN_2, PAT.IMPORT_PATTERN_3, PAT.IMPORT_PATTERN_SIDE_EFFECT]) {
682
+ pattern.lastIndex = 0;
683
+ let match;
684
+ while ((match = pattern.exec(code)) !== null) {
685
+ visitSpecifier(match[2]);
686
+ }
687
+ }
688
+ return new Set(Array.from(nonRootSubpathPackages).filter((packageName) => rootEntryPackages.has(packageName)));
689
+ }
751
690
  function collectImportDependencies(code, importerPath) {
752
691
  const importerDir = path.posix.dirname(importerPath);
753
692
  const deps = new Set();
@@ -809,68 +748,15 @@ function cleanCode(code) {
809
748
  result = ACTIVE_STRATEGY.preClean(result);
810
749
  result = ACTIVE_STRATEGY.rewriteFrameworkImports(result);
811
750
  // Vendor manifest-driven import rewrites
751
+ // NOTE: Static and side-effect vendor imports are intentionally NOT rewritten here.
752
+ // They are left as import statements so that ensureNativeScriptModuleBindings()
753
+ // (called later in processCodeForDevice) can transform them using the robust
754
+ // __nsVendorRequire + __nsPick pattern that works on device.
755
+ // Only dynamic imports are handled here since ensureNativeScriptModuleBindings
756
+ // does not process dynamic import() calls.
812
757
  try {
813
758
  const manifest = getVendorManifest();
814
759
  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
760
  // Dynamic import rewrites: import('pkg') -> Promise.resolve(__nsVendor('id'))
875
761
  const dynImportRE = /(import\(\s*["'])([^"']+)(["']\s*\))/g;
876
762
  result = result.replace(dynImportRE, (full, pre, spec, post) => {
@@ -999,6 +885,59 @@ function toAppModuleBaseId(importPath, projectRoot) {
999
885
  const base = projectRelative.replace(/\.mjs$/i, '');
1000
886
  return `/${base}`;
1001
887
  }
888
+ function toNodeModulesHttpModuleId(importPath) {
889
+ const nodeModulesSpecifier = normalizeNodeModulesSpecifier(importPath);
890
+ if (!nodeModulesSpecifier) {
891
+ return null;
892
+ }
893
+ return `/ns/m/node_modules/${nodeModulesSpecifier}`;
894
+ }
895
+ export function rewriteNsMImportPathForHmr(p, ver, bootTaggedRequest) {
896
+ const toHmrServeTag = (value) => {
897
+ const raw = String(value ?? '').trim();
898
+ if (!raw) {
899
+ return 'v0';
900
+ }
901
+ if (raw === 'live' || /^n\d+$/i.test(raw) || /^v[^/]+$/i.test(raw)) {
902
+ return raw;
903
+ }
904
+ if (/^\d+$/.test(raw)) {
905
+ return `v${raw}`;
906
+ }
907
+ return raw;
908
+ };
909
+ if (!p || !p.startsWith('/ns/m/')) {
910
+ return p;
911
+ }
912
+ const canonicalNodeModulesPath = p.replace(/^\/ns\/m\/__ns_boot__\/b1\/__ns_hmr__\/[^/]+\/node_modules\//, '/ns/m/node_modules/').replace(/^\/ns\/m\/__ns_hmr__\/[^/]+\/node_modules\//, '/ns/m/node_modules/');
913
+ if (canonicalNodeModulesPath.startsWith('/ns/m/node_modules/')) {
914
+ return canonicalNodeModulesPath;
915
+ }
916
+ if (canonicalNodeModulesPath.startsWith('/ns/m/__ns_boot__/')) {
917
+ return canonicalNodeModulesPath;
918
+ }
919
+ if (canonicalNodeModulesPath.startsWith('/ns/m/__ns_hmr__/')) {
920
+ return bootTaggedRequest ? `/ns/m/__ns_boot__/b1${canonicalNodeModulesPath.slice('/ns/m'.length)}` : canonicalNodeModulesPath;
921
+ }
922
+ const tag = toHmrServeTag(ver);
923
+ const hmrPrefix = `/ns/m/__ns_hmr__/${tag}`;
924
+ const bootHmrPrefix = `/ns/m/__ns_boot__/b1/__ns_hmr__/${tag}`;
925
+ return (bootTaggedRequest ? bootHmrPrefix : hmrPrefix) + canonicalNodeModulesPath.slice('/ns/m'.length);
926
+ }
927
+ function getNumericServeVersionTag(tag, fallback) {
928
+ const raw = String(tag || '').trim();
929
+ if (!raw) {
930
+ return fallback;
931
+ }
932
+ const versionMatch = raw.match(/^v(\d+)$/);
933
+ if (versionMatch?.[1]) {
934
+ return Number(versionMatch[1]);
935
+ }
936
+ if (/^\d+$/.test(raw)) {
937
+ return Number(raw);
938
+ }
939
+ return fallback;
940
+ }
1002
941
  function normalizeAbsoluteFilesystemImport(spec, importerPath, projectRoot) {
1003
942
  if (!spec || typeof spec !== 'string') {
1004
943
  return null;
@@ -1025,13 +964,172 @@ function normalizeAbsoluteFilesystemImport(spec, importerPath, projectRoot) {
1025
964
  }
1026
965
  return absolute;
1027
966
  }
967
+ /**
968
+ * After the Angular linker runs on code that Vite has already resolved (bare
969
+ * specifiers → full URLs), the linker injects NEW import statements with bare
970
+ * specifiers (e.g. `import {Component} from '@angular/core'`). These cause:
971
+ * 1. Duplicate-identifier SyntaxErrors (the name was already imported via URL)
972
+ * 2. Unresolvable bare specifiers at runtime on device
973
+ *
974
+ * This function:
975
+ * • builds a map packageName → resolvedURL from existing resolved imports
976
+ * • collects all binding names already imported per package
977
+ * • for each bare-specifier import, removes duplicate bindings
978
+ * • rewrites any genuinely-new bindings to use the resolved URL
979
+ */
980
+ function deduplicateLinkerImports(code) {
981
+ if (!code)
982
+ return code;
983
+ try {
984
+ const imports = collectTopLevelImportRecords(code);
985
+ if (!imports.length) {
986
+ return code;
987
+ }
988
+ // ── Step 1: collect resolved imports already in the file ──────────
989
+ const pkgUrlMap = new Map();
990
+ const pkgBindings = new Map();
991
+ for (const imp of imports) {
992
+ const url = imp.source;
993
+ if (!/^https?:\/\//.test(url) && !url.startsWith('/')) {
994
+ continue;
995
+ }
996
+ const nmIdx = url.lastIndexOf('/node_modules/');
997
+ if (nmIdx === -1)
998
+ continue;
999
+ const afterNm = url.substring(nmIdx + '/node_modules/'.length);
1000
+ const parts = afterNm.split('/');
1001
+ const pkg = parts[0].startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
1002
+ if (!pkgUrlMap.has(pkg))
1003
+ pkgUrlMap.set(pkg, url);
1004
+ if (imp.namedBindings.length) {
1005
+ if (!pkgBindings.has(pkg))
1006
+ pkgBindings.set(pkg, new Set());
1007
+ for (const binding of imp.namedBindings) {
1008
+ if (binding.importedName)
1009
+ pkgBindings.get(pkg).add(binding.importedName);
1010
+ }
1011
+ }
1012
+ }
1013
+ if (pkgUrlMap.size === 0)
1014
+ return code;
1015
+ // ── Step 2: rewrite bare-specifier imports ───────────────────────
1016
+ const edits = [];
1017
+ for (const imp of imports) {
1018
+ if (!imp.hasOnlyNamedSpecifiers) {
1019
+ continue;
1020
+ }
1021
+ const specifier = imp.source;
1022
+ if (specifier.startsWith('/') || specifier.startsWith('.') || specifier.startsWith('http')) {
1023
+ continue;
1024
+ }
1025
+ const parts = specifier.split('/');
1026
+ const pkg = specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
1027
+ const url = pkgUrlMap.get(pkg);
1028
+ if (!url) {
1029
+ continue;
1030
+ }
1031
+ const existing = pkgBindings.get(pkg) || new Set();
1032
+ const newBindings = imp.namedBindings.filter((binding) => !existing.has(binding.importedName));
1033
+ if (newBindings.length === 0) {
1034
+ edits.push({ start: imp.start, end: imp.end, text: '' });
1035
+ continue;
1036
+ }
1037
+ if (newBindings.length === imp.namedBindings.length) {
1038
+ continue;
1039
+ }
1040
+ for (const binding of newBindings) {
1041
+ existing.add(binding.importedName);
1042
+ }
1043
+ edits.push({
1044
+ start: imp.start,
1045
+ end: imp.end,
1046
+ text: `import { ${newBindings.map((binding) => binding.text).join(', ')} } from ${JSON.stringify(url)};`,
1047
+ });
1048
+ }
1049
+ if (!edits.length) {
1050
+ return code;
1051
+ }
1052
+ let next = code;
1053
+ for (const edit of edits.sort((left, right) => right.start - left.start)) {
1054
+ next = next.slice(0, edit.start) + edit.text + next.slice(edit.end);
1055
+ }
1056
+ return next;
1057
+ }
1058
+ catch {
1059
+ return code;
1060
+ }
1061
+ }
1062
+ export function wrapCommonJsModuleForDevice(code) {
1063
+ if (!code)
1064
+ return code;
1065
+ try {
1066
+ const hasExportDefault = /\bexport\s+default\b/.test(code) || /export\s*\{\s*default\s*(?:as\s*default)?\s*\}/.test(code);
1067
+ const hasNamedExports = /\bexport\s+(?:const|let|var|function|class|async)\b/.test(code) || /\bexport\s*\{/.test(code);
1068
+ const hasCjsExports = /\bmodule\s*\.\s*exports\b/.test(code) || /\bexports\s*\.\s*\w/.test(code);
1069
+ if (hasExportDefault || hasNamedExports || !hasCjsExports) {
1070
+ return code;
1071
+ }
1072
+ const namedExports = new Set();
1073
+ const exportsRe = /\bexports\s*\.\s*([A-Za-z_$][\w$]*)\s*=/g;
1074
+ let em;
1075
+ while ((em = exportsRe.exec(code)) !== null) {
1076
+ const name = em[1];
1077
+ if (name !== '__esModule' && name !== 'default') {
1078
+ namedExports.add(name);
1079
+ }
1080
+ }
1081
+ const defPropRe = /Object\s*\.\s*defineProperty\s*\(\s*exports\s*,\s*['"]([^'"]+)['"]/g;
1082
+ while ((em = defPropRe.exec(code)) !== null) {
1083
+ const name = em[1];
1084
+ if (name !== '__esModule' && name !== 'default') {
1085
+ namedExports.add(name);
1086
+ }
1087
+ }
1088
+ let suffix = `\nvar __cjs_mod = module.exports;\nexport default __cjs_mod;\n`;
1089
+ if (namedExports.size) {
1090
+ const entries = Array.from(namedExports);
1091
+ const temps = entries.map((name, i) => `var __cjs_e${i} = __cjs_mod[${JSON.stringify(name)}];`);
1092
+ const reExports = entries.map((name, i) => `__cjs_e${i} as ${name}`);
1093
+ suffix += `${temps.join(' ')}\nexport { ${reExports.join(', ')} };\n`;
1094
+ }
1095
+ const prelude = `var module = { exports: {} }; var exports = module.exports;\n` +
1096
+ `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` +
1097
+ `var __ns_cjs_require_kind = (typeof globalThis.__nsBaseRequire === 'function' ? 'base-require' : (typeof globalThis.__nsRequire === 'function' ? 'vendor-require' : 'global-require'));\n` +
1098
+ `var require = function(spec) {\n` +
1099
+ ` if (!__ns_cjs_require_base) { throw new Error('require is not defined'); }\n` +
1100
+ ` try { var __nsRecord = globalThis.__NS_RECORD_MODULE_PROVENANCE__; if (typeof __nsRecord === 'function') { __nsRecord(String(spec), { kind: __ns_cjs_require_kind, specifier: String(spec), via: 'cjs-wrapper', parent: (typeof import.meta !== 'undefined' && import.meta && import.meta.url) ? import.meta.url : undefined }); } } catch (e) {}\n` +
1101
+ ` var mod = __ns_cjs_require_base(spec);\n` +
1102
+ ` try {\n` +
1103
+ ` if (mod && (typeof mod === 'object' || typeof mod === 'function') && mod.default !== undefined) {\n` +
1104
+ ` var keys = [];\n` +
1105
+ ` try { keys = Object.keys(mod); } catch (e) {}\n` +
1106
+ ` var defaultOnly = keys.length === 1 && keys[0] === 'default';\n` +
1107
+ ` var esModuleOnly = keys.length === 2 && keys.indexOf('default') !== -1 && keys.indexOf('__esModule') !== -1;\n` +
1108
+ ` if (mod.__esModule || defaultOnly || esModuleOnly) { return mod.default; }\n` +
1109
+ ` }\n` +
1110
+ ` } catch (e) {}\n` +
1111
+ ` return mod;\n` +
1112
+ `};\n`;
1113
+ return `${prelude}${code}${suffix}`;
1114
+ }
1115
+ catch {
1116
+ return code;
1117
+ }
1118
+ }
1028
1119
  /**
1029
1120
  * Process code for device: inject globals, remove framework imports
1030
1121
  */
1031
- function processCodeForDevice(code, isVitePreBundled) {
1122
+ function processCodeForDevice(code, isVitePreBundled, preserveVendorImports = false, isNodeModule = false, sourceId, options) {
1032
1123
  let result = code;
1124
+ const resolvedSpecifierOverrides = options?.resolvedSpecifierOverrides || getProcessCodeResolvedSpecifierOverrides(sourceId, getProjectRootPath());
1125
+ const bindingOptions = {
1126
+ preserveNonPluginVendorImports: preserveVendorImports,
1127
+ resolvedSpecifierOverrides,
1128
+ };
1033
1129
  // Ensure Angular partial declarations are linked before any sanitizers run so runtime never hits the JIT path.
1034
1130
  result = linkAngularPartialsIfNeeded(result);
1131
+ // Post-linker: deduplicate/resolve imports the Angular linker injected with bare specifiers
1132
+ result = deduplicateLinkerImports(result);
1035
1133
  // First: aggressively strip any lingering virtual dynamic-import-helper before anything else.
1036
1134
  // Doing this up-front prevents downstream dependency collection from seeing the virtual id.
1037
1135
  result = stripViteDynamicImportVirtual(result);
@@ -1042,13 +1140,17 @@ function processCodeForDevice(code, isVitePreBundled) {
1042
1140
  // Inject ALL NativeScript/build globals at the top (matching global-defines.ts)
1043
1141
  // This ensures any code using __DEV__, __ANDROID__, __IOS__, etc. works correctly
1044
1142
  const allGlobals = [
1143
+ // Minimal process shim — populated with CLI --env.* flags at module load time.
1144
+ // In production builds, Vite/Rollup replaces process.env.* statically.
1145
+ // In HMR dev mode the code runs as-is on device, so we need the shim.
1146
+ `if (typeof process === "undefined") { globalThis.process = { env: ${__processEnvJson} }; } else if (!process.env) { process.env = ${__processEnvJson}; }`,
1045
1147
  'const __ANDROID__ = globalThis.__ANDROID__ !== undefined ? globalThis.__ANDROID__ : false;',
1046
1148
  'const __IOS__ = globalThis.__IOS__ !== undefined ? globalThis.__IOS__ : false;',
1047
1149
  'const __VISIONOS__ = globalThis.__VISIONOS__ !== undefined ? globalThis.__VISIONOS__ : false;',
1048
1150
  'const __APPLE__ = globalThis.__APPLE__ !== undefined ? globalThis.__APPLE__ : (__IOS__ || __VISIONOS__);',
1049
1151
  'const __DEV__ = globalThis.__DEV__ !== undefined ? globalThis.__DEV__ : false;',
1050
1152
  'const __COMMONJS__ = globalThis.__COMMONJS__ !== undefined ? globalThis.__COMMONJS__ : false;',
1051
- 'const __NS_WEBPACK__ = globalThis.__NS_WEBPACK__ !== undefined ? globalThis.__NS_WEBPACK__ : true;',
1153
+ 'const __NS_WEBPACK__ = globalThis.__NS_WEBPACK__ !== undefined ? globalThis.__NS_WEBPACK__ : false;',
1052
1154
  'const __NS_ENV_VERBOSE__ = globalThis.__NS_ENV_VERBOSE__ !== undefined ? !!globalThis.__NS_ENV_VERBOSE__ : false;',
1053
1155
  "const __CSS_PARSER__ = globalThis.__CSS_PARSER__ !== undefined ? globalThis.__CSS_PARSER__ : 'css-tree';",
1054
1156
  'const __UI_USE_XML_PARSER__ = globalThis.__UI_USE_XML_PARSER__ !== undefined ? globalThis.__UI_USE_XML_PARSER__ : true;',
@@ -1056,19 +1158,29 @@ function processCodeForDevice(code, isVitePreBundled) {
1056
1158
  'const __TEST__ = globalThis.__TEST__ !== undefined ? globalThis.__TEST__ : false;',
1057
1159
  ];
1058
1160
  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);
1161
+ const nodeModuleProvenancePrelude = buildNodeModuleProvenancePrelude(sourceId);
1162
+ if (nodeModuleProvenancePrelude) {
1163
+ result = nodeModuleProvenancePrelude + result;
1062
1164
  }
1063
- catch { }
1064
- // Verify there are no duplicate top-level const/let bindings after AST normalization
1065
- try {
1066
- result = astVerifyAndAnnotateDuplicates(result);
1165
+ // AST normalization: inject /ns/rt helper aliases for underscore-prefixed identifiers.
1166
+ // ONLY for app source files library code in node_modules should be served as-is.
1167
+ // Running the normalizer on libraries like tslib injects harmful destructures
1168
+ // (e.g., `const { SuppressedError } = __ns_rt_ns_1`) that shadow globals.
1169
+ if (!isNodeModule) {
1170
+ try {
1171
+ result = astNormalizeModuleImportsAndHelpers(result);
1172
+ }
1173
+ catch { }
1174
+ // Verify there are no duplicate top-level const/let bindings after AST normalization
1175
+ try {
1176
+ result = astVerifyAndAnnotateDuplicates(result);
1177
+ }
1178
+ catch { }
1067
1179
  }
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)) {
1180
+ // If AST marker present OR this is a node_modules file, skip regex-based helper
1181
+ // alias injection. Library code should NOT get /ns/rt destructures injected
1182
+ // underscore-prefixed identifiers in libraries are internal variables, not NS helpers.
1183
+ if (!isNodeModule && !/^\s*(?:\/\/|\/\*) \[ast-normalized\]/m.test(result)) {
1072
1184
  try {
1073
1185
  const underscored = new Set();
1074
1186
  const re = /(^|[^.\w$])_([A-Za-z]\w*)\b/g;
@@ -1149,7 +1261,11 @@ function processCodeForDevice(code, isVitePreBundled) {
1149
1261
  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
1262
  }
1151
1263
  catch { }
1152
- result = ensureNativeScriptModuleBindings(result);
1264
+ // When preserveVendorImports is true (HMR /ns/m/ endpoint), skip the
1265
+ // __nsVendorRequire + __nsPick rewrite. Vendor imports stay as bare
1266
+ // specifiers so the device-side import map resolves them via V8's native
1267
+ // module system, which correctly handles export * re-exports.
1268
+ result = ensureNativeScriptModuleBindings(result, bindingOptions);
1153
1269
  // Repair any accidental "import ... = expr" assignments that may have slipped in.
1154
1270
  try {
1155
1271
  result = repairImportEqualsAssignments(result);
@@ -1158,10 +1274,7 @@ function processCodeForDevice(code, isVitePreBundled) {
1158
1274
  // Strip Vite prebundle deps imports (both named and side-effect) and any malformed const string artifacts
1159
1275
  // Example problematic line observed: const "/node_modules/.vite/deps/@nativescript_firebase-messaging.js?v=...";
1160
1276
  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, '');
1277
+ result = rewriteVitePrebundleImportsForDevice(result, preserveVendorImports);
1165
1278
  // Malformed const string lines accidentally produced by upstream transforms
1166
1279
  result = result.replace(/^[\t ]*const\s+["']\/?node_modules\/\.vite\/deps\/[^"']+["'];?\s*$/gm, '// [hmr-sanitize] stripped malformed const prebundle ref\n');
1167
1280
  // Naked string-only lines pointing at prebundle deps
@@ -1248,7 +1361,7 @@ function processCodeForDevice(code, isVitePreBundled) {
1248
1361
  }
1249
1362
  // Ensure vendor bindings also apply after potential wrapper injections above
1250
1363
  // (idempotent: second pass will be a no-op if imports already consumed).
1251
- result = ensureNativeScriptModuleBindings(result);
1364
+ result = ensureNativeScriptModuleBindings(result, bindingOptions);
1252
1365
  try {
1253
1366
  result = repairImportEqualsAssignments(result);
1254
1367
  }
@@ -1289,17 +1402,17 @@ function processCodeForDevice(code, isVitePreBundled) {
1289
1402
  result = normalizeStrayCoreStringLiterals(result);
1290
1403
  }
1291
1404
  catch { }
1405
+ try {
1406
+ result = fixDanglingCoreFrom(result);
1407
+ }
1408
+ catch { }
1409
+ try {
1410
+ result = normalizeAnyCoreSpecToBridge(result);
1411
+ }
1412
+ catch { }
1292
1413
  result = ensureVariableDynamicImportHelper(result);
1293
1414
  // Normalize any lingering @nativescript/core imports to the /ns/core bridge (non-destructive best-effort)
1294
1415
  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
1416
  // Rewrite named imports from the /ns/core bridge into default import + destructuring.
1304
1417
  // This makes `import { Frame } from '@nativescript/core'` work even if the bridge provides only a default export.
1305
1418
  {
@@ -1315,6 +1428,9 @@ function processCodeForDevice(code, isVitePreBundled) {
1315
1428
  .join(', ');
1316
1429
  const reNamed = /(^|\n)\s*import\s*\{([^}]+)\}\s*from\s*["']((?:https?:\/\/[^"']+)?\/ns\/core(?:\/[\d]+)?(?:\?p=[^"']+)?)['"];?\s*/gm;
1317
1430
  result = result.replace(reNamed, (_full, pfx, specList, src) => {
1431
+ // Deep subpath URLs serve actual ESM with real named exports — skip.
1432
+ if (isDeepCoreSubpath(src))
1433
+ return _full;
1318
1434
  __core_ns_seq++;
1319
1435
  const tmp = `__ns_core_ns${__core_ns_seq}`;
1320
1436
  const decl = `const { ${toDestructureCore(specList)} } = ${tmp};`;
@@ -1322,6 +1438,8 @@ function processCodeForDevice(code, isVitePreBundled) {
1322
1438
  });
1323
1439
  const reMixed = /(^|\n)\s*import\s+([A-Za-z_$][\w$]*)\s*,\s*\{([^}]+)\}\s*from\s*["']((?:https?:\/\/[^"']+)?\/ns\/core(?:\/[\d]+)?(?:\?p=[^"']+)?)['"];?\s*/gm;
1324
1440
  result = result.replace(reMixed, (_full, pfx, defName, specList, src) => {
1441
+ if (isDeepCoreSubpath(src))
1442
+ return _full;
1325
1443
  const decl = `const { ${toDestructureCore(specList)} } = ${defName};`;
1326
1444
  return `${pfx}import ${defName} from ${JSON.stringify(src)};\n${decl}\n`;
1327
1445
  });
@@ -1335,8 +1453,11 @@ function processCodeForDevice(code, isVitePreBundled) {
1335
1453
  // Keep a single semicolon before the import to avoid generating ';;'
1336
1454
  result = result.replace(/;\s*import\s+/g, ';\nimport ');
1337
1455
  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');
1456
+ // Fallback: ensure any static import that isn't at start of line gets a newline before it.
1457
+ // Only match after statement-ending characters (;, }, ), ], quotes) — NOT after `*` or
1458
+ // spaces inside JSDoc comment blocks, which would accidentally extract example imports
1459
+ // from documentation comments and hoist them as real code.
1460
+ result = result.replace(/([;}\)\]'"`])\s*(import\s+[^;\n]*\s+from\s*["'][^"']+["'])/g, '$1\n$2');
1340
1461
  }
1341
1462
  catch { }
1342
1463
  // Collapse duplicate destructuring from the same temp namespace var (e.g., multiple const { x } = __ns_rt_ns1)
@@ -1370,22 +1491,13 @@ function processCodeForDevice(code, isVitePreBundled) {
1370
1491
  // always come before any statements that might reference their bindings. This ordering avoids
1371
1492
  // device runtimes that are stricter about imports-first semantics during module instantiation.
1372
1493
  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
- }
1494
+ result = hoistTopLevelStaticImports(result);
1383
1495
  }
1384
1496
  catch { }
1385
1497
  // 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
1498
+ // Skip for node_modules (no /ns/rt helpers needed) and when AST marker present
1387
1499
  try {
1388
- if (!/^\s*\/\* \[ast-normalized\] \*\//m.test(result)) {
1500
+ if (!isNodeModule && !/^\s*\/\* \[ast-normalized\] \*\//m.test(result)) {
1389
1501
  result = ensureDestructureRtImports(result);
1390
1502
  }
1391
1503
  }
@@ -1531,6 +1643,16 @@ function assertNoOptimizedArtifacts(code, contextLabel) {
1531
1643
  }
1532
1644
  }
1533
1645
  if (localCore.test(ln)) {
1646
+ // Comments can never cause split-realm risk at runtime — skip them.
1647
+ // Library authors commonly reference @nativescript/core in comments
1648
+ // (e.g. TSDoc /// <reference> directives, module resolution notes).
1649
+ const trimmed = ln.trimStart();
1650
+ if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) {
1651
+ continue;
1652
+ }
1653
+ if (shouldAllowLocalCoreSanitizerPaths(contextLabel)) {
1654
+ continue;
1655
+ }
1534
1656
  offenders.push(`${i + 1}: ${ln.substring(0, 200)} [local-core-path]`);
1535
1657
  }
1536
1658
  if (offenders.length >= 10)
@@ -1555,6 +1677,7 @@ function assertNoOptimizedArtifacts(code, contextLabel) {
1555
1677
  function ensureDestructureCoreImports(code) {
1556
1678
  try {
1557
1679
  let result = code;
1680
+ let coreImportCounter = 0;
1558
1681
  const toDestructure = (specList) => specList
1559
1682
  .split(',')
1560
1683
  .map((s) => s.trim())
@@ -1567,13 +1690,19 @@ function ensureDestructureCoreImports(code) {
1567
1690
  // import { A, B } from '/ns/core[/ver][?p=...]'
1568
1691
  const reNamed = /(^|\n)\s*import\s*\{([^}]+)\}\s*from\s*["']((?:https?:\/\/[^"']+)?\/ns\/core(?:\/[\d]+)?(?:\?p=[^"']+)?)['"];?\s*/gm;
1569
1692
  result = result.replace(reNamed, (_full, pfx, specList, src) => {
1570
- const tmp = `__ns_core_ns_re`; // temp binding name is not reused elsewhere after hoist
1693
+ // Deep subpath URLs serve actual ESM with real named exports skip.
1694
+ if (isDeepCoreSubpath(src))
1695
+ return _full;
1696
+ const tmp = `__ns_core_ns_re${coreImportCounter > 0 ? `_${coreImportCounter}` : ''}`;
1697
+ coreImportCounter++;
1571
1698
  const decl = `const { ${toDestructure(specList)} } = ${tmp};`;
1572
1699
  return `${pfx}import ${tmp} from ${JSON.stringify(src)};\n${decl}\n`;
1573
1700
  });
1574
1701
  // import Default, { A, B } from '/ns/core[...]'
1575
1702
  const reMixed = /(^|\n)\s*import\s+([A-Za-z_$][\w$]*)\s*,\s*\{([^}]+)\}\s*from\s*["']((?:https?:\/\/[^"']+)?\/ns\/core(?:\/[\d]+)?(?:\?p=[^"']+)?)['"];?\s*/gm;
1576
1703
  result = result.replace(reMixed, (_full, pfx, defName, specList, src) => {
1704
+ if (isDeepCoreSubpath(src))
1705
+ return _full;
1577
1706
  const decl = `const { ${toDestructure(specList)} } = ${defName};`;
1578
1707
  return `${pfx}import ${defName} from ${JSON.stringify(src)};\n${decl}\n`;
1579
1708
  });
@@ -1685,14 +1814,17 @@ function dedupeRtNamedImportsAgainstDestructures(code) {
1685
1814
  /**
1686
1815
  * THE SINGLE REWRITE FUNCTION - used everywhere for consistency
1687
1816
  */
1688
- function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot, verbose = false, outputDirOverrideRel, httpOrigin) {
1817
+ export function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot, verbose = false, outputDirOverrideRel, httpOrigin, resolveVendorAsHttp = false) {
1689
1818
  let result = code;
1690
1819
  const httpOriginSafe = httpOrigin;
1820
+ const mixedRuntimePluginHttpRootPackages = collectMixedRuntimePluginHttpRootPackages(result, projectRoot);
1821
+ const isDynamicImportPrefix = (prefix) => /import\(\s*["']?$/.test(prefix.trimStart());
1691
1822
  const importerDir = path.posix.dirname(importerPath);
1692
1823
  // Determine importer output relative path (project-relative .mjs) to compute relative imports consistently
1693
1824
  const importerOutRel = outputDirOverrideRel || getProjectRelativeImportPath(importerPath, projectRoot) || stripToProjectRelative(importerPath, projectRoot).replace(/\.(ts|js|tsx|jsx|mjs|mts|cts)$/i, '.mjs');
1694
1825
  const importerOutDir = importerOutRel ? path.posix.dirname(importerOutRel) : '';
1695
1826
  const ensureRel = (p) => (p.startsWith('.') ? p : `./${p}`);
1827
+ const isNsSfcSpecifier = (spec) => /^(?:https?:\/\/[^/]+)?\/ns\/sfc(?:\/\d+)?(?:\/|$)/.test(spec.replace(PAT.QUERY_PATTERN, ''));
1696
1828
  // Normalize all @nativescript/core imports to the unified HTTP ESM core bridge to guarantee a single realm on device
1697
1829
  try {
1698
1830
  let coreAliasIdx = 0;
@@ -1835,6 +1967,16 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1835
1967
  return `${prefix}${stub}${suffix}`;
1836
1968
  }
1837
1969
  spec = normalizeNativeScriptCoreSpecifier(spec);
1970
+ // Route Vite virtual modules (/@solid-refresh, etc.) through /ns/m/ so their
1971
+ // internal imports (e.g. solid-js) get vendor-rewritten by our pipeline.
1972
+ // Skip known Vite internals (/@vite/, /@id/, /@fs/) which are handled elsewhere.
1973
+ if (spec.startsWith('/@') && !/^\/@(?:vite|id|fs)\//.test(spec)) {
1974
+ const out = `/ns/m${spec}`;
1975
+ if (httpOriginSafe) {
1976
+ return `${prefix}${httpOriginSafe}${out}${suffix}`;
1977
+ }
1978
+ return `${prefix}${out}${suffix}`;
1979
+ }
1838
1980
  // Route internal NS endpoints to absolute HTTP origin for device
1839
1981
  if (spec.startsWith('/ns/')) {
1840
1982
  if (httpOriginSafe) {
@@ -1846,20 +1988,50 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1846
1988
  return `${prefix}${spec}${suffix}`;
1847
1989
  }
1848
1990
  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}`;
1991
+ const normalizedRuntimePluginSpec = nodeModulesSpecifier || spec.replace(PAT.QUERY_PATTERN, '').replace(/^\/+/, '');
1992
+ if (normalizedRuntimePluginSpec && mixedRuntimePluginHttpRootPackages.size > 0) {
1993
+ const { packageName } = resolveNodeModulesPackageBoundary(normalizedRuntimePluginSpec, projectRoot);
1994
+ if (packageName && mixedRuntimePluginHttpRootPackages.has(packageName)) {
1995
+ const httpNodeModulesSpecifier = nodeModulesSpecifier || normalizedRuntimePluginSpec;
1996
+ const httpSpec = `/ns/m/node_modules/${httpNodeModulesSpecifier}`;
1997
+ if (httpOriginSafe) {
1998
+ return `${prefix}${httpOriginSafe}${httpSpec}${suffix}`;
1999
+ }
2000
+ return `${prefix}${httpSpec}${suffix}`;
1854
2001
  }
1855
- return `${prefix}${spec.replace(PAT.QUERY_PATTERN, '')}${suffix}`;
1856
2002
  }
1857
- if (isNativeScriptPluginModule(candidateNativeScriptSpec)) {
1858
- const bareSpecifier = candidateNativeScriptSpec.replace(PAT.QUERY_PATTERN, '');
1859
- return `${prefix}${bareSpecifier}${suffix}`;
2003
+ if (shouldPreserveBareRuntimePluginSubpathImport(spec, projectRoot)) {
2004
+ const httpSpec = `/ns/m/node_modules/${spec.replace(PAT.QUERY_PATTERN, '').replace(/^\/+/, '')}`;
2005
+ if (httpOriginSafe) {
2006
+ return `${prefix}${httpOriginSafe}${httpSpec}${suffix}`;
2007
+ }
2008
+ return `${prefix}${httpSpec}${suffix}`;
1860
2009
  }
2010
+ const candidateNativeScriptSpec = nodeModulesSpecifier ?? spec;
2011
+ // ── Node modules routing ──────────────────────────────────────
2012
+ // Uses the package's own package.json exports field to determine
2013
+ // whether an import is the main entry (→ vendor bridge) or a
2014
+ // subpath entry (→ HTTP). This replaces the old heuristic-based
2015
+ // approach that tried to guess from file paths.
1861
2016
  if (nodeModulesSpecifier) {
1862
- return `${prefix}${nodeModulesSpecifier}${suffix}`;
2017
+ const vendorRouting = resolveVendorRouting(nodeModulesSpecifier, projectRoot);
2018
+ if (vendorRouting) {
2019
+ if (vendorRouting.route === 'vendor') {
2020
+ return `${prefix}${vendorRouting.bareSpec}${suffix}`;
2021
+ }
2022
+ // Vendor package but subpath/platform-specific → HTTP
2023
+ const httpSpec = `/ns/m/node_modules/${nodeModulesSpecifier}`;
2024
+ if (httpOriginSafe) {
2025
+ return `${prefix}${httpOriginSafe}${httpSpec}${suffix}`;
2026
+ }
2027
+ return `${prefix}${httpSpec}${suffix}`;
2028
+ }
2029
+ // Not a vendor package → serve via HTTP from Vite dev server
2030
+ const httpSpec = `/ns/m/node_modules/${nodeModulesSpecifier}`;
2031
+ if (httpOriginSafe) {
2032
+ return `${prefix}${httpOriginSafe}${httpSpec}${suffix}`;
2033
+ }
2034
+ return `${prefix}${httpSpec}${suffix}`;
1863
2035
  }
1864
2036
  // Handle .vue imports
1865
2037
  if (PAT.VUE_FILE_PATTERN.test(spec)) {
@@ -1883,7 +2055,7 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1883
2055
  return `${prefix}${out}${suffix}`;
1884
2056
  }
1885
2057
  // Case B: plain .vue module → rewrite to SFC endpoint or local artifact
1886
- const vueKey = resolveVueKey(spec.replace(PAT.QUERY_PATTERN, ''));
2058
+ const vueKey = resolveVueKey(spec.replace(PAT.QUERY_PATTERN, '')) || '';
1887
2059
  if (vueKey) {
1888
2060
  if (true) {
1889
2061
  const absVue = vueKey.startsWith('/') ? vueKey : '/' + vueKey;
@@ -1913,9 +2085,25 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1913
2085
  // Rewrite relative application imports to HTTP for served modules
1914
2086
  if (spec.startsWith('./') || spec.startsWith('../')) {
1915
2087
  const absMaybe = normalizeImportPath(spec, importerDir);
2088
+ const nodeModulesHttpSpec = absMaybe ? toNodeModulesHttpModuleId(absMaybe) : null;
2089
+ if (nodeModulesHttpSpec) {
2090
+ if (isDynamicImportPrefix(prefix)) {
2091
+ if (verbose)
2092
+ console.log(`[rewrite][http] dynamic relative node_modules import → ${nodeModulesHttpSpec} (from ${spec})`);
2093
+ return `__nsDynamicHmrImport(${JSON.stringify(nodeModulesHttpSpec)})`;
2094
+ }
2095
+ if (verbose)
2096
+ console.log(`[rewrite][http] relative node_modules import → ${nodeModulesHttpSpec} (from ${spec})`);
2097
+ return `${prefix}${nodeModulesHttpSpec}${suffix}`;
2098
+ }
1916
2099
  const baseId = absMaybe ? toAppModuleBaseId(absMaybe, projectRoot) : null; // e.g. /src/foo.mjs
1917
2100
  if (baseId) {
1918
2101
  const httpSpec = `/ns/m${baseId}`;
2102
+ if (isDynamicImportPrefix(prefix)) {
2103
+ if (verbose)
2104
+ console.log(`[rewrite][http] dynamic relative app import → ${httpSpec} (from ${spec})`);
2105
+ return `__nsDynamicHmrImport(${JSON.stringify(httpSpec)})`;
2106
+ }
1919
2107
  if (verbose)
1920
2108
  console.log(`[rewrite][http] relative app import → ${httpSpec} (from ${spec})`);
1921
2109
  return `${prefix}${httpSpec}${suffix}`;
@@ -1928,6 +2116,11 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1928
2116
  const baseId = toAppModuleBaseId(spec, projectRoot);
1929
2117
  if (baseId) {
1930
2118
  const httpSpec = `/ns/m${baseId}`;
2119
+ if (isDynamicImportPrefix(prefix)) {
2120
+ if (verbose)
2121
+ console.log(`[rewrite][http] dynamic app import → ${httpSpec} (from ${spec})`);
2122
+ return `__nsDynamicHmrImport(${JSON.stringify(httpSpec)})`;
2123
+ }
1931
2124
  if (verbose)
1932
2125
  console.log(`[rewrite][http] absolute app import → ${httpSpec} (from ${spec})`);
1933
2126
  return `${prefix}${httpSpec}${suffix}`;
@@ -1959,6 +2152,10 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1959
2152
  result = result.replace(PAT.IMPORT_PATTERN_2, replaceVueImport);
1960
2153
  result = result.replace(PAT.EXPORT_PATTERN, replaceVueImport);
1961
2154
  result = result.replace(PAT.IMPORT_PATTERN_3, replaceVueImport);
2155
+ // Side-effect imports (import "spec") — must run AFTER named-import patterns
2156
+ // since IMPORT_PATTERN_1 already handles `import ... from "spec"`.
2157
+ result = result.replace(PAT.IMPORT_PATTERN_SIDE_EFFECT, replaceVueImport);
2158
+ result = ensureDynamicHmrImportHelper(result);
1962
2159
  // Extra guard: map any lingering dynamic import('@') to a safe stub module path
1963
2160
  // to prevent device runtime normalization errors.
1964
2161
  // Example matched: import('@') or import("@") with optional whitespace before closing paren
@@ -1999,6 +2196,11 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
1999
2196
  // In HTTP mode, skip legacy local-path rewrite to avoid mixing module origins
2000
2197
  result = result.replace(PAT.VUE_FILE_IMPORT, (_m, p1, spec, p3) => {
2001
2198
  if (httpOrigin) {
2199
+ if (isNsSfcSpecifier(spec)) {
2200
+ if (verbose)
2201
+ console.log(`[rewrite] .vue already routed (VUE_FILE_IMPORT http): ${spec}`);
2202
+ return `${p1}${spec}${p3}`;
2203
+ }
2002
2204
  // Route via /ns/sfc with full query preserved
2003
2205
  try {
2004
2206
  let base = spec;
@@ -2048,6 +2250,13 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
2048
2250
  console.log(`[rewrite][http] internal ns import (dynamic) → ${spec} via import.meta.url`);
2049
2251
  return expr;
2050
2252
  }
2253
+ const nodeModulesHttpSpec = toNodeModulesHttpModuleId(spec);
2254
+ if (nodeModulesHttpSpec) {
2255
+ const expr = `import(new URL('${nodeModulesHttpSpec}', import.meta.url).href)`;
2256
+ if (verbose)
2257
+ console.log(`[rewrite][http] absolute dynamic node_modules import → ${nodeModulesHttpSpec} via import.meta.url (from ${spec})`);
2258
+ return expr;
2259
+ }
2051
2260
  const baseId = toAppModuleBaseId(spec, projectRoot);
2052
2261
  if (!baseId)
2053
2262
  return match;
@@ -2064,6 +2273,8 @@ function rewriteImports(code, importerPath, sfcFileMap, depFileMap, projectRoot,
2064
2273
  function createHmrWebSocketPlugin(opts) {
2065
2274
  const verbose = !!opts.verbose;
2066
2275
  let wss = null;
2276
+ let sharedTransformRequest;
2277
+ const pendingAngularReloadSuppressions = new Map();
2067
2278
  const sfcFileMap = new Map();
2068
2279
  const depFileMap = new Map();
2069
2280
  // Generic module manifest (spec -> emitted relative .mjs path)
@@ -2078,10 +2289,27 @@ function createHmrWebSocketPlugin(opts) {
2078
2289
  // Transactional HMR batches: map graphVersion -> ordered list of changed ids for that version
2079
2290
  const txnBatches = new Map();
2080
2291
  const graph = new Map();
2292
+ function rememberAngularReloadSuppression(root, file, ttlMs = 3000) {
2293
+ const absPath = normalizeHotReloadMatchPath(file);
2294
+ const relPath = normalizeHotReloadMatchPath(file, root);
2295
+ pendingAngularReloadSuppressions.set(absPath, {
2296
+ absPath,
2297
+ relPath,
2298
+ expiresAt: Date.now() + ttlMs,
2299
+ });
2300
+ }
2301
+ function pruneAngularReloadSuppressions(now = Date.now()) {
2302
+ for (const [key, entry] of pendingAngularReloadSuppressions) {
2303
+ if (!entry || entry.expiresAt <= now) {
2304
+ pendingAngularReloadSuppressions.delete(key);
2305
+ }
2306
+ }
2307
+ }
2081
2308
  // Compute a dependency-closed, topologically sorted list of modules for a given set of changed ids.
2082
2309
  // Only include application modules we can serve (e.g., under /src and known .vue/.ts/.js entries in the graph).
2083
2310
  function computeTxnOrderForChanged(changedIds) {
2084
- const includeExt = (id) => ACTIVE_STRATEGY.matchesFile(id) || /\.(ts|js|mjs|tsx|jsx)$/i.test(id);
2311
+ const includeAppModule = (id) => matchesRuntimeGraphModuleId(id, APP_VIRTUAL_WITH_SLASH, /\.(ts|js|mjs|tsx|jsx)$/i);
2312
+ const includeExt = (id) => ACTIVE_STRATEGY.matchesFile(id) || includeAppModule(id);
2085
2313
  const isApp = (id) => id.startsWith(APP_VIRTUAL_WITH_SLASH);
2086
2314
  const roots = changedIds.map(normalizeGraphId).filter((id) => graph.has(id) && (isApp(id) || ACTIVE_STRATEGY.matchesFile(id)) && includeExt(id));
2087
2315
  const toVisit = new Set();
@@ -2220,7 +2448,7 @@ function createHmrWebSocketPlugin(opts) {
2220
2448
  catch { }
2221
2449
  });
2222
2450
  }
2223
- function upsertGraphModule(rawId, code, deps) {
2451
+ function upsertGraphModule(rawId, code, deps, options) {
2224
2452
  const id = normalizeGraphId(rawId);
2225
2453
  const normDeps = deps
2226
2454
  .map((d) => normalizeGraphId(d))
@@ -2229,19 +2457,23 @@ function createHmrWebSocketPlugin(opts) {
2229
2457
  .sort();
2230
2458
  const hash = computeHash(code);
2231
2459
  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
2460
+ const classification = classifyGraphUpsert(existing, hash, normDeps);
2461
+ if (classification === 'unchanged')
2462
+ return existing;
2234
2463
  graphVersion++;
2235
2464
  const gm = { id, deps: normDeps, hash };
2236
2465
  graph.set(id, gm);
2237
2466
  if (verbose) {
2238
2467
  try {
2239
- console.log('[hmr-ws][graph] upsert', { id, deps: normDeps, hash, graphVersion });
2468
+ console.log('[hmr-ws][graph] upsert', { id, deps: normDeps, hash, graphVersion, classification });
2240
2469
  console.log('[hmr-ws][graph] size', graph.size);
2241
2470
  }
2242
2471
  catch { }
2243
2472
  }
2244
- emitDelta([gm], []);
2473
+ if (shouldBroadcastGraphUpsertDelta(classification, options?.emitDeltaOnInsert === true, options?.broadcastDelta !== false)) {
2474
+ emitDelta([gm], []);
2475
+ }
2476
+ return gm;
2245
2477
  }
2246
2478
  function isTypescriptFlavor() {
2247
2479
  try {
@@ -2278,15 +2510,15 @@ function createHmrWebSocketPlugin(opts) {
2278
2510
  }
2279
2511
  async function walk(dir) {
2280
2512
  for (const name of fs.readdirSync(dir)) {
2281
- const full = pathMod.join(dir, name);
2282
- if (name === 'node_modules' || name.startsWith('.'))
2513
+ if (name === 'node_modules' || name.startsWith('.') || shouldSkipRuntimeGraphDirectoryName(name))
2283
2514
  continue;
2515
+ const full = pathMod.join(dir, name);
2284
2516
  try {
2285
2517
  const stat = fs.statSync(full);
2286
2518
  if (stat.isDirectory())
2287
2519
  await walk(full);
2288
2520
  else if (stat.isFile()) {
2289
- if (/\.(vue|ts|js|mjs|tsx|jsx)$/.test(name)) {
2521
+ if (shouldIncludeRuntimeGraphFile(full, /\.(vue|ts|js|mjs|tsx|jsx)$/i)) {
2290
2522
  const rel = '/' + pathMod.relative(root, full).split(pathMod.sep).join('/');
2291
2523
  // Transform via Vite to gather deps (ignore failures)
2292
2524
  try {
@@ -2323,6 +2555,41 @@ function createHmrWebSocketPlugin(opts) {
2323
2555
  const httpServer = server.httpServer;
2324
2556
  if (!httpServer)
2325
2557
  return;
2558
+ const wsAny = server.ws;
2559
+ if (!wsAny.__NS_ANGULAR_FULL_RELOAD_FILTER_INSTALLED__) {
2560
+ const originalSend = server.ws.send.bind(server.ws);
2561
+ wsAny.__NS_ANGULAR_FULL_RELOAD_FILTER_INSTALLED__ = true;
2562
+ server.ws.send = ((payload, ...rest) => {
2563
+ pruneAngularReloadSuppressions();
2564
+ if (shouldSuppressViteFullReloadPayload({
2565
+ payload,
2566
+ pendingEntries: pendingAngularReloadSuppressions.values(),
2567
+ root: pluginRoot,
2568
+ })) {
2569
+ if (verbose) {
2570
+ console.log('[hmr-ws][angular] suppressed vite full-reload payload', payload);
2571
+ }
2572
+ return;
2573
+ }
2574
+ return originalSend(payload, ...rest);
2575
+ });
2576
+ }
2577
+ // Default to serialized transform execution for deterministic HTTP HMR startup.
2578
+ // Higher fan-out can be re-enabled explicitly via NS_VITE_HMR_TRANSFORM_CONCURRENCY.
2579
+ const configuredTransformConcurrency = Number.parseInt(process.env.NS_VITE_HMR_TRANSFORM_CONCURRENCY || '1', 10);
2580
+ const transformConcurrency = Number.isFinite(configuredTransformConcurrency) && configuredTransformConcurrency > 0 ? configuredTransformConcurrency : 1;
2581
+ const configuredTransformCacheMs = Number.parseInt(process.env.NS_VITE_HMR_TRANSFORM_CACHE_MS || '15000', 10);
2582
+ const transformCacheMs = Number.isFinite(configuredTransformCacheMs) && configuredTransformCacheMs >= 0 ? configuredTransformCacheMs : 15000;
2583
+ sharedTransformRequest = createSharedTransformRequestRunner((url) => server.transformRequest(url), (url, timeoutMs) => {
2584
+ try {
2585
+ console.warn('[ns:m] slow transformRequest for', url, '(>' + timeoutMs + 'ms)');
2586
+ }
2587
+ catch { }
2588
+ }, {
2589
+ maxConcurrent: transformConcurrency,
2590
+ resultCacheTtlMs: transformCacheMs,
2591
+ getResultCacheKey: (url) => canonicalizeTransformRequestCacheKey(url, pluginRoot || process.cwd()),
2592
+ });
2326
2593
  // Attempt early vendor manifest bootstrap once per server.
2327
2594
  if (!vendorBootstrapDone) {
2328
2595
  vendorBootstrapDone = true;
@@ -2366,14 +2633,26 @@ function createHmrWebSocketPlugin(opts) {
2366
2633
  });
2367
2634
  // Additional connection diagnostics
2368
2635
  wss.on('connection', (ws, req) => {
2636
+ const role = getHmrSocketRoleFromRequestUrl(req.url);
2637
+ ws.__nsHmrClientRole = role;
2369
2638
  try {
2370
2639
  if (verbose) {
2371
2640
  const ra = req.socket?.remoteAddress;
2372
2641
  const rp = req.socket?.remotePort;
2373
- console.log('[hmr-ws] Client connected', ra + (rp ? ':' + rp : ''));
2642
+ console.log('[hmr-ws] Client connected', { role, remote: ra + (rp ? ':' + rp : '') });
2374
2643
  }
2375
2644
  }
2376
2645
  catch { }
2646
+ ws.on('close', () => {
2647
+ try {
2648
+ if (verbose) {
2649
+ const ra = req.socket?.remoteAddress;
2650
+ const rp = req.socket?.remotePort;
2651
+ console.log('[hmr-ws] Client disconnected', { role, remote: ra + (rp ? ':' + rp : '') });
2652
+ }
2653
+ }
2654
+ catch { }
2655
+ });
2377
2656
  });
2378
2657
  wss.on('error', (err) => {
2379
2658
  try {
@@ -2381,6 +2660,40 @@ function createHmrWebSocketPlugin(opts) {
2381
2660
  }
2382
2661
  catch { }
2383
2662
  });
2663
+ // Import map endpoint: GET /ns/import-map.json
2664
+ // Returns the import map + runtime config for __nsConfigureRuntime()
2665
+ server.middlewares.use(async (req, res, next) => {
2666
+ try {
2667
+ const urlObj = new URL(req.url || '', 'http://localhost');
2668
+ if (urlObj.pathname !== '/ns/import-map.json')
2669
+ return next();
2670
+ res.setHeader('Access-Control-Allow-Origin', '*');
2671
+ res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
2672
+ if (req.method === 'OPTIONS') {
2673
+ res.statusCode = 204;
2674
+ res.end();
2675
+ return;
2676
+ }
2677
+ // Determine origin from request headers or server config
2678
+ const host = req.headers.host || 'localhost:5173';
2679
+ const protocol = 'http';
2680
+ const origin = `${protocol}://${host}`;
2681
+ const runtimeConfig = buildRuntimeConfig({
2682
+ origin,
2683
+ flavor: ACTIVE_STRATEGY?.flavor || 'typescript',
2684
+ });
2685
+ res.setHeader('Content-Type', 'application/json');
2686
+ res.end(JSON.stringify({
2687
+ importMap: JSON.parse(runtimeConfig.importMap),
2688
+ volatilePatterns: runtimeConfig.volatilePatterns,
2689
+ }, null, 2));
2690
+ }
2691
+ catch (err) {
2692
+ console.error('[import-map] error generating import map:', err?.message || err);
2693
+ res.statusCode = 500;
2694
+ res.end(JSON.stringify({ error: 'Failed to generate import map' }));
2695
+ }
2696
+ });
2384
2697
  // Dev-only HTTP ESM loader endpoint for device clients
2385
2698
  // 1) Legacy JSON module endpoint (kept temporarily): GET /ns-module?path=/abs -> { path, code, additionalFiles }
2386
2699
  server.middlewares.use(async (req, res, next) => {
@@ -2415,13 +2728,15 @@ function createHmrWebSocketPlugin(opts) {
2415
2728
  // Transform via Vite with variant resolution (same as ws ns:fetch-module)
2416
2729
  const hasExt = /\.(ts|tsx|js|jsx|mjs|mts|cts|vue)$/i.test(spec);
2417
2730
  const baseNoExt = hasExt ? spec.replace(/\.(ts|tsx|js|jsx|mjs|mts|cts)$/i, '') : spec;
2731
+ const transformRoot = server.config?.root || process.cwd();
2418
2732
  const candidates = [];
2419
2733
  if (hasExt)
2420
2734
  candidates.push(spec);
2421
2735
  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');
2736
+ const transformCandidates = filterExistingNodeModulesTransformCandidates(spec, candidates, transformRoot);
2422
2737
  let transformed = null;
2423
2738
  let resolvedCandidate = null;
2424
- for (const cand of candidates) {
2739
+ for (const cand of transformCandidates) {
2425
2740
  try {
2426
2741
  const r = await server.transformRequest(cand);
2427
2742
  if (r?.code) {
@@ -2443,7 +2758,10 @@ function createHmrWebSocketPlugin(opts) {
2443
2758
  code = REQUIRE_GUARD_SNIPPET + code;
2444
2759
  // Apply same sanitation/rewrite pipeline used for WS path
2445
2760
  code = cleanCode(code);
2446
- code = processCodeForDevice(code, false);
2761
+ // preserveVendorImports=true: vendor imports stay as bare specifiers
2762
+ // for the device-side import map (ns-vendor://) instead of being
2763
+ // transformed to __nsVendorRequire calls with fragile __nsPick lookups.
2764
+ code = processCodeForDevice(code, false, true, /(?:^|\/)node_modules\//.test(resolvedCandidate || spec), resolvedCandidate || spec);
2447
2765
  code = rewriteImports(code, spec, sfcFileMap, depFileMap, server.config?.root || process.cwd(), !!verbose, undefined, getServerOrigin(server));
2448
2766
  code = ensureVariableDynamicImportHelper(code);
2449
2767
  // Enforce upstream guarantee: no optimized deps or virtual ids remain
@@ -2455,18 +2773,6 @@ function createHmrWebSocketPlugin(opts) {
2455
2773
  res.setHeader('Content-Type', 'application/json');
2456
2774
  return void res.end(JSON.stringify({ error: e?.message || String(e) }));
2457
2775
  }
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
2776
  try {
2471
2777
  const origin = getServerOrigin(server);
2472
2778
  code = ensureVersionedRtImports(code, origin, graphVersion);
@@ -2499,7 +2805,7 @@ function createHmrWebSocketPlugin(opts) {
2499
2805
  if (seen.has(depBase))
2500
2806
  continue;
2501
2807
  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'];
2808
+ 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);
2503
2809
  let depTrans = null;
2504
2810
  let depResolved = null;
2505
2811
  for (const c of depCandidates) {
@@ -2516,7 +2822,7 @@ function createHmrWebSocketPlugin(opts) {
2516
2822
  if (depTrans?.code && depResolved) {
2517
2823
  let depCode = depTrans.code;
2518
2824
  depCode = cleanCode(depCode);
2519
- depCode = processCodeForDevice(depCode, false);
2825
+ depCode = processCodeForDevice(depCode, false, true, /(?:^|\/)node_modules\//.test(depResolved), depResolved);
2520
2826
  depCode = rewriteImports(depCode, depResolved, sfcFileMap, depFileMap, server.config?.root || process.cwd(), !!verbose, undefined, getServerOrigin(server));
2521
2827
  depCode = ensureVariableDynamicImportHelper(depCode);
2522
2828
  try {
@@ -2567,7 +2873,8 @@ function createHmrWebSocketPlugin(opts) {
2567
2873
  // Support both query (?path=/abs) and path-style (/ns/m/abs)
2568
2874
  let spec = urlObj.searchParams.get('path') || '';
2569
2875
  // Optional graph version pin for deterministic boot
2570
- const forcedVer = urlObj.searchParams.get('v');
2876
+ let forcedVer = urlObj.searchParams.get('v');
2877
+ let bootTaggedRequest = false;
2571
2878
  if (!spec) {
2572
2879
  const base = '/ns/m';
2573
2880
  let rest = urlObj.pathname.slice(base.length);
@@ -2585,22 +2892,25 @@ function createHmrWebSocketPlugin(opts) {
2585
2892
  res.end('export {}\n');
2586
2893
  return;
2587
2894
  }
2895
+ const serverRoot = (server.config?.root || process.cwd());
2588
2896
  spec = spec.replace(/[?#].*$/, '');
2589
- // Accept path-based HMR cache-busting: /ns/m/__ns_hmr__/<tag>/<real-spec>
2897
+ // Accept path-based boot/HMR prefixes:
2898
+ // /ns/m/__ns_boot__/b1/<real-spec>
2899
+ // /ns/m/__ns_hmr__/<tag>/<real-spec>
2900
+ // /ns/m/__ns_boot__/b1/__ns_hmr__/<tag>/<real-spec>
2590
2901
  // The iOS HTTP ESM loader canonicalizes cache keys by stripping query params,
2591
2902
  // so we must carry the cache-buster in the path.
2592
2903
  try {
2593
- const m = spec.match(/^\/?__ns_hmr__\/[^\/]+(\/.*)?$/);
2594
- if (m) {
2595
- spec = m[1] || '/';
2596
- }
2904
+ const decorated = stripDecoratedServePrefixes(spec);
2905
+ spec = decorated.cleanedSpec;
2906
+ bootTaggedRequest = decorated.bootTaggedRequest;
2907
+ forcedVer || (forcedVer = decorated.forcedVer);
2597
2908
  }
2598
2909
  catch { }
2599
2910
  // Normalize absolute filesystem paths back to project-relative ids (e.g. /src/app.ts)
2600
2911
  try {
2601
- const projectRoot = (server.config?.root || process.cwd());
2602
2912
  const toPosix = (p) => p.replace(/\\/g, '/');
2603
- const rootPosix = toPosix(projectRoot);
2913
+ const rootPosix = toPosix(serverRoot);
2604
2914
  const specPosix = toPosix(spec);
2605
2915
  // If spec is an absolute path under the project root, convert to '/'+relative
2606
2916
  const isAbsFs = /^\//.test(specPosix) || /^[A-Za-z]:\//.test(spec); // posix or win drive
@@ -2615,27 +2925,78 @@ function createHmrWebSocketPlugin(opts) {
2615
2925
  }
2616
2926
  }
2617
2927
  catch { }
2928
+ // Serve Vite virtual modules (/@id/ prefix). These are internal
2929
+ // virtual modules (e.g., \0nsvite:nsconfig-json for ~/package.json)
2930
+ // that don't exist on disk. Decode the ID and load via plugin container.
2931
+ if (spec.startsWith('/@id/')) {
2932
+ try {
2933
+ // First try Vite's transform pipeline directly
2934
+ const vr = await sharedTransformRequest(spec);
2935
+ if (vr?.code) {
2936
+ res.statusCode = 200;
2937
+ res.end(vr.code);
2938
+ return;
2939
+ }
2940
+ }
2941
+ catch { }
2942
+ try {
2943
+ // Fallback: decode the virtual module ID (__x00__ → \0) and
2944
+ // load through the plugin container directly
2945
+ const rawId = spec.slice('/@id/'.length).replace(/__x00__/g, '\0');
2946
+ const loadResult = await server.pluginContainer.load(rawId);
2947
+ if (loadResult) {
2948
+ const code = typeof loadResult === 'string' ? loadResult : loadResult.code;
2949
+ if (code) {
2950
+ res.statusCode = 200;
2951
+ res.end(code);
2952
+ return;
2953
+ }
2954
+ }
2955
+ }
2956
+ catch { }
2957
+ }
2618
2958
  if (spec.startsWith('@/'))
2619
2959
  spec = APP_VIRTUAL_WITH_SLASH + spec.slice(2);
2620
2960
  if (spec.startsWith('./'))
2621
2961
  spec = spec.slice(1);
2962
+ const blockedNodeModulesReason = getBlockedDeviceNodeModulesReason(spec);
2963
+ if (blockedNodeModulesReason) {
2964
+ res.statusCode = 404;
2965
+ res.end(`// [ns:m] blocked device import\nthrow new Error(${JSON.stringify(`[ns/m] ${blockedNodeModulesReason}`)});\nexport {};\n`);
2966
+ return;
2967
+ }
2622
2968
  if (!spec.startsWith('/'))
2623
2969
  spec = '/' + spec;
2624
2970
  const hasExt = /\.(ts|tsx|js|jsx|mjs|mts|cts|vue)$/i.test(spec);
2625
2971
  const baseNoExt = hasExt ? spec.replace(/\.(ts|tsx|js|jsx|mjs|mts|cts)$/i, '') : spec;
2626
2972
  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'];
2973
+ const transformCandidates = filterExistingNodeModulesTransformCandidates(spec, candidates, serverRoot);
2627
2974
  let transformed = null;
2628
2975
  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;
2976
+ const rawExplicitModule = tryReadRawExplicitJavaScriptModule(spec, serverRoot);
2977
+ if (rawExplicitModule) {
2978
+ transformed = { code: rawExplicitModule.code };
2979
+ resolvedCandidate = rawExplicitModule.resolvedId;
2980
+ }
2981
+ // Queue and dedupe transformRequest calls so heavy app graphs do not
2982
+ // overwhelm Vite with concurrent work. Slow-transform warnings start only
2983
+ // when the transform actually begins executing, and requests stay pending
2984
+ // until Vite returns a real result.
2985
+ const transformWithTimeout = (url, timeoutMs = 120000) => {
2986
+ return sharedTransformRequest(url, timeoutMs);
2987
+ };
2988
+ if (!transformed?.code) {
2989
+ for (const cand of transformCandidates) {
2990
+ try {
2991
+ const r = await transformWithTimeout(cand);
2992
+ if (r?.code) {
2993
+ transformed = r;
2994
+ resolvedCandidate = cand;
2995
+ break;
2996
+ }
2636
2997
  }
2998
+ catch { }
2637
2999
  }
2638
- catch { }
2639
3000
  }
2640
3001
  // Fallback 1: ask Vite to resolve the id, then transform the resolved id (handles aliases and virtual ids)
2641
3002
  if (!transformed?.code) {
@@ -2643,7 +3004,7 @@ function createHmrWebSocketPlugin(opts) {
2643
3004
  const rid = await server.pluginContainer?.resolveId?.(spec, undefined);
2644
3005
  const ridStr = typeof rid === 'string' ? rid : rid?.id || null;
2645
3006
  if (ridStr) {
2646
- const r = await server.transformRequest(ridStr);
3007
+ const r = await transformWithTimeout(ridStr);
2647
3008
  if (r?.code) {
2648
3009
  transformed = r;
2649
3010
  resolvedCandidate = ridStr;
@@ -2652,27 +3013,49 @@ function createHmrWebSocketPlugin(opts) {
2652
3013
  }
2653
3014
  catch { }
2654
3015
  }
3016
+ // Fallback 1b: if spec is a /node_modules/ path, extract bare specifier
3017
+ // and try resolveId with that. This handles package.json "exports" field
3018
+ // resolution (e.g., solid-js/jsx-runtime → solid-js/dist/solid.js).
3019
+ if (!transformed?.code && spec.includes('/node_modules/')) {
3020
+ try {
3021
+ const nmIdx = spec.lastIndexOf('/node_modules/');
3022
+ const bare = spec.slice(nmIdx + '/node_modules/'.length);
3023
+ if (bare && !bare.startsWith('.')) {
3024
+ const rid = await server.pluginContainer?.resolveId?.(bare, undefined);
3025
+ const ridStr = typeof rid === 'string' ? rid : rid?.id || null;
3026
+ if (ridStr) {
3027
+ const r = await sharedTransformRequest(ridStr);
3028
+ if (r?.code) {
3029
+ transformed = r;
3030
+ resolvedCandidate = ridStr;
3031
+ }
3032
+ }
3033
+ }
3034
+ }
3035
+ catch { }
3036
+ }
2655
3037
  // Fallback 2: try /@fs absolute path under project root (Vite file system alias)
2656
3038
  if (!transformed?.code) {
2657
3039
  try {
2658
- const projectRoot = (server.config?.root || process.cwd());
2659
3040
  const toPosix = (p) => p.replace(/\\/g, '/');
2660
- const rootPosix = toPosix(projectRoot).replace(/\/$/, '');
3041
+ const rootPosix = toPosix(serverRoot).replace(/\/$/, '');
2661
3042
  const absPosix = `${rootPosix}${spec.startsWith('/') ? '' : '/'}${spec}`;
2662
3043
  const fsId = `/@fs${absPosix}`;
2663
- const r = await server.transformRequest(fsId);
2664
- if (r?.code) {
2665
- transformed = r;
2666
- resolvedCandidate = fsId;
3044
+ if (resolveCandidateFilePath(fsId, serverRoot)) {
3045
+ const r = await transformWithTimeout(fsId);
3046
+ if (r?.code) {
3047
+ transformed = r;
3048
+ resolvedCandidate = fsId;
3049
+ }
2667
3050
  }
2668
3051
  }
2669
3052
  catch { }
2670
3053
  }
2671
3054
  // Fallback 3: try adding ?import to hint Vite's transform pipeline
2672
3055
  if (!transformed?.code) {
2673
- for (const cand of candidates) {
3056
+ for (const cand of transformCandidates) {
2674
3057
  try {
2675
- const r = await server.transformRequest(`${cand}${cand.includes('?') ? '&' : '?'}import`);
3058
+ const r = await transformWithTimeout(`${cand}${cand.includes('?') ? '&' : '?'}import`);
2676
3059
  if (r?.code) {
2677
3060
  transformed = r;
2678
3061
  resolvedCandidate = `${cand}?import`;
@@ -2682,39 +3065,67 @@ function createHmrWebSocketPlugin(opts) {
2682
3065
  catch { }
2683
3066
  }
2684
3067
  }
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.
3068
+ // Solid HMR: patch @@solid-refresh's $$refreshESM to do inline patching
3069
+ // during module re-evaluation instead of deferring to hot.accept() callback.
3070
+ // In NativeScript's HTTP ESM environment, accept callbacks are registered
3071
+ // but not invoked by the HMR client. By adding a direct patchRegistry()
3072
+ // call when hot.data already has a stored registry, component updates
3073
+ // apply immediately when the module re-evaluates.
2688
3074
  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);
3075
+ if (transformed?.code && ACTIVE_STRATEGY?.flavor === 'solid' && (resolvedCandidate || spec || '').includes('@solid-refresh')) {
3076
+ const PATCH_SENTINEL = '/* __ns_solid_refresh_patched__ */';
3077
+ const alreadyPatched = transformed.code.includes(PATCH_SENTINEL);
3078
+ console.log('[hmr-ws][solid] @solid-refresh patch check:', { spec: resolvedCandidate || spec, alreadyPatched, codeLen: transformed.code.length });
3079
+ if (!alreadyPatched) {
3080
+ let patchedCode = transformed.code;
3081
+ // Patch 1: Bypass shouldWarnAndDecline() the vendor-bundled solid-js
3082
+ // may not have the 'development' condition active, making DEV empty/undefined.
3083
+ // In NativeScript HMR mode we are always in dev, so force it to return false.
3084
+ const declineCheck = 'function shouldWarnAndDecline() {';
3085
+ if (patchedCode.includes(declineCheck)) {
3086
+ patchedCode = patchedCode.replace(declineCheck, `${PATCH_SENTINEL}\nfunction shouldWarnAndDecline() { return false; /* NS HMR: always allow refresh */ }\nfunction __original_shouldWarnAndDecline() {`);
3087
+ console.log('[hmr-ws][solid] bypassed shouldWarnAndDecline() for NativeScript HMR');
2702
3088
  }
2703
- catch {
2704
- return p;
3089
+ // Patch 2: Force createMemo path in createProxy.
3090
+ // Without the 'development' condition, $DEVCOMP is not set on components,
3091
+ // so createProxy falls through to `return s(props)` — a direct call with
3092
+ // no reactive subscription. When patchComponent fires update() (the signal
3093
+ // setter), nobody is listening. By forcing the createMemo path, HMRComp
3094
+ // subscribes to the signal and re-renders when the component changes.
3095
+ const proxyCondition = 'if (!s || $DEVCOMP in s) {';
3096
+ if (patchedCode.includes(proxyCondition)) {
3097
+ patchedCode = patchedCode.replace(proxyCondition, 'if (true) { /* NS HMR: always use createMemo for reactive HMR updates */');
3098
+ console.log('[hmr-ws][solid] forced createMemo path in createProxy for NativeScript HMR');
2705
3099
  }
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;
3100
+ // Patch 3: Inline patchRegistry call so updates apply immediately
3101
+ // on module re-evaluation (accept callbacks are not invoked by the HMR client).
3102
+ const marker = 'hot.data[SOLID_REFRESH] = hot.data[SOLID_REFRESH] || registry;';
3103
+ if (patchedCode.includes(marker)) {
3104
+ const patchCode = [
3105
+ `console.log('[solid-refresh][$$refreshESM] hot.data keys=', hot.data ? Object.keys(hot.data) : 'no-data', 'has=', !!(hot.data && hot.data[SOLID_REFRESH]));`,
3106
+ `if (hot.data[SOLID_REFRESH]) {`,
3107
+ ` console.log('[solid-refresh][$$refreshESM] patching: oldComponents=', hot.data[SOLID_REFRESH].components ? hot.data[SOLID_REFRESH].components.size : 0, 'newComponents=', registry.components ? registry.components.size : 0);`,
3108
+ ` var _shouldInvalidate = patchRegistry(hot.data[SOLID_REFRESH], registry);`,
3109
+ ` console.log('[solid-refresh][$$refreshESM] patchRegistry result: shouldInvalidate=', _shouldInvalidate);`,
3110
+ `} else {`,
3111
+ ` console.log('[solid-refresh][$$refreshESM] first load — creating registry, components=', registry.components ? registry.components.size : 0);`,
3112
+ `}`,
3113
+ ].join('\n ');
3114
+ patchedCode = patchedCode.replace(marker, `${patchCode}\n ${marker}`);
3115
+ console.log('[hmr-ws][solid] added inline patchRegistry for NativeScript HMR');
3116
+ }
3117
+ // Work on a copy to avoid mutating Vite's cached TransformResult
3118
+ transformed = { ...transformed, code: patchedCode };
3119
+ }
3120
+ }
3121
+ }
3122
+ catch { }
3123
+ // NOTE: Path-based cache busting for /ns/m/* imports is applied in the
3124
+ // finalize step below (after rewriteImports adds the /ns/m/ prefix).
3125
+ // The block here only handles TypeScript-specific graph population.
3126
+ try {
3127
+ if (transformed?.code) {
3128
+ const code = transformed.code;
2718
3129
  // TypeScript-specific graph population: when TS flavor is active
2719
3130
  // and this is an application module under the virtual app root,
2720
3131
  // upsert it into the HMR graph so ns:hmr-full-graph is non-empty.
@@ -2723,7 +3134,7 @@ function createHmrWebSocketPlugin(opts) {
2723
3134
  const id = (resolvedCandidate || spec).replace(/[?#].*$/, '');
2724
3135
  // Only track app modules (under APP_VIRTUAL_WITH_SLASH) and ts/js/tsx/jsx/mjs.
2725
3136
  const isApp = id.startsWith(APP_VIRTUAL_WITH_SLASH) || id.startsWith('/app/');
2726
- if (isApp && /\.(ts|tsx|js|jsx|mjs|mts|cts)$/i.test(id)) {
3137
+ if (isApp && /\.(ts|tsx|js|jsx|mjs|mts|cts)$/i.test(id) && !isRuntimeGraphExcludedPath(id)) {
2727
3138
  const deps = Array.from(collectImportDependencies(code, id));
2728
3139
  if (verbose) {
2729
3140
  try {
@@ -2843,7 +3254,7 @@ export const piniaSymbol = p.piniaSymbol;
2843
3254
  if (!transformed?.code) {
2844
3255
  // Emit a module that throws with context for easier on-device debugging
2845
3256
  try {
2846
- const tried = Array.from(new Set(candidates)).slice(0, 12);
3257
+ const tried = Array.from(new Set(transformCandidates.length > 0 ? transformCandidates : candidates)).slice(0, 12);
2847
3258
  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
3259
  res.statusCode = 404;
2849
3260
  res.end(out);
@@ -2860,8 +3271,33 @@ export const piniaSymbol = p.piniaSymbol;
2860
3271
  // Prepend guard to capture any URL-based require attempts
2861
3272
  code = REQUIRE_GUARD_SNIPPET + code;
2862
3273
  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));
3274
+ const isNodeMod = /(?:^|\/)node_modules\//.test(resolvedCandidate || spec || '');
3275
+ code = processCodeForDevice(code, false, true, isNodeMod, resolvedCandidate || spec);
3276
+ // Solid HMR: The NativeScript iOS/Android runtime provides import.meta.hot
3277
+ // natively (via InitializeImportMetaHot in HMRSupport.mm) with C++-backed
3278
+ // persistent hot.data that survives across module re-evaluations.
3279
+ // cleanCode() strips Vite's __vite__createHotContext assignment, which is
3280
+ // correct — the runtime's native hot context is better.
3281
+ const projectRoot = server.config?.root || process.cwd();
3282
+ const serverOrigin = getServerOrigin(server);
3283
+ if (ACTIVE_STRATEGY?.flavor === 'angular') {
3284
+ code = prepareAngularEntryForDevice(code, resolvedCandidate || spec, sfcFileMap, depFileMap, projectRoot, !!verbose, undefined, serverOrigin, true);
3285
+ }
3286
+ else {
3287
+ code = rewriteImports(code, resolvedCandidate || spec, sfcFileMap, depFileMap, projectRoot, !!verbose, undefined, serverOrigin, true);
3288
+ }
3289
+ // Expand `export * from "url"` into explicit named re-exports.
3290
+ // NativeScript's HTTP ESM loader may not propagate star-re-exports across
3291
+ // HTTP module boundaries (the namespace object gets direct exports but
3292
+ // misses re-exported names). By expanding to `export { a, b } from "url"`,
3293
+ // the engine sees explicit named exports and resolves them correctly.
3294
+ try {
3295
+ code = await expandStarExports(code, server, server.config?.root || process.cwd(), verbose);
3296
+ }
3297
+ catch (e) {
3298
+ if (verbose)
3299
+ console.warn('[ns/m] export* expansion failed:', e?.message);
3300
+ }
2865
3301
  // Dedupe any /ns/rt named imports that duplicate destructured bindings off default /ns/rt
2866
3302
  try {
2867
3303
  code = dedupeRtNamedImportsAgainstDestructures(code);
@@ -2888,6 +3324,28 @@ export const piniaSymbol = p.piniaSymbol;
2888
3324
  }
2889
3325
  }
2890
3326
  catch { }
3327
+ // Final pass: deduplicate/resolve any bare-specifier imports that slipped
3328
+ // through the pipeline (e.g., extracted from JSDoc comments by import-splitting
3329
+ // regexes, or injected by the Angular linker on already-resolved code).
3330
+ try {
3331
+ code = deduplicateLinkerImports(code);
3332
+ }
3333
+ catch { }
3334
+ // CJS/UMD wrapping: if a module uses module.exports but has no ESM export default,
3335
+ // wrap it with CJS shims so the device HTTP ESM loader can consume it.
3336
+ // This handles npm packages that use CommonJS but aren't pre-bundled by Vite.
3337
+ //
3338
+ // Key constraints this must handle:
3339
+ // - CJS modules often declare local vars with the same names as their exports
3340
+ // (e.g. `function createLTTB() {...}; exports.createLTTB = createLTTB;`)
3341
+ // so `export var { createLTTB }` would cause a duplicate declaration.
3342
+ // - UMD modules reference `this` at top level (undefined in ESM) but
3343
+ // typically fall back to `self` or `globalThis`.
3344
+ // - `module`, `exports` must be shims since they don't exist in ESM.
3345
+ try {
3346
+ code = wrapCommonJsModuleForDevice(code);
3347
+ }
3348
+ catch { }
2891
3349
  try {
2892
3350
  assertNoOptimizedArtifacts(code, `NS M ${resolvedCandidate || spec}`);
2893
3351
  }
@@ -2908,29 +3366,43 @@ export const piniaSymbol = p.piniaSymbol;
2908
3366
  }
2909
3367
  catch { }
2910
3368
  try {
2911
- const verNum = Number(forcedVer || graphVersion || 0);
3369
+ const verNum = getNumericServeVersionTag(forcedVer, Number(graphVersion || 0));
2912
3370
  code = ensureVersionedRtImports(code, getServerOrigin(server), verNum);
2913
3371
  code = ACTIVE_STRATEGY.ensureVersionedImports(code, getServerOrigin(server), verNum);
2914
3372
  code = ensureVersionedCoreImports(code, getServerOrigin(server), verNum);
2915
3373
  }
2916
3374
  catch { }
2917
- // Finalize: also stamp all internal /ns/m imports with ?v=<ver> after all rewrites
3375
+ // Finalize: stamp all internal /ns/m imports with PATH-based cache busting.
3376
+ // IMPORTANT: use path prefix (not ?v= query) because the iOS HTTP ESM loader
3377
+ // strips query params when computing module cache keys, so ?v= doesn't bust the V8 cache.
2918
3378
  try {
2919
- const ver = String(forcedVer || graphVersion || 0);
3379
+ const ver = (() => {
3380
+ const raw = String(forcedVer || '').trim();
3381
+ if (raw) {
3382
+ if (raw === 'live' || /^n\d+$/i.test(raw) || /^v[^/]+$/i.test(raw)) {
3383
+ return raw;
3384
+ }
3385
+ if (/^\d+$/.test(raw)) {
3386
+ return `v${raw}`;
3387
+ }
3388
+ }
3389
+ return `v${String(graphVersion || 0)}`;
3390
+ })();
2920
3391
  const origin = getServerOrigin(server);
3392
+ const rewritePath = (p) => rewriteNsMImportPathForHmr(p, ver, bootTaggedRequest);
2921
3393
  // 1) Static imports: import ... from "/ns/m/..."
2922
- code = code.replace(/(from\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, `$1$2?v=${ver}$3`);
3394
+ code = code.replace(/(from\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
2923
3395
  // 2) Side-effect imports: import "/ns/m/..."
2924
- code = code.replace(/(import\s*(?!\()\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, `$1$2?v=${ver}$3`);
3396
+ code = code.replace(/(import\s*(?!\()\s*["'])(\/ns\/m\/[^"'?]+)(["'])/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
2925
3397
  // 3) Dynamic imports: import("/ns/m/...")
2926
- code = code.replace(/(import\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*\))/g, `$1$2?v=${ver}$3`);
3398
+ code = code.replace(/(import\(\s*["'])(\/ns\/m\/[^"'?]+)(["']\s*\))/g, (_m, a, p, b) => `${a}${rewritePath(p)}${b}`);
2927
3399
  // 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`);
3400
+ 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
3401
  // 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/..."
3402
+ 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}`);
3403
+ // 6) Force absolute HTTP for new URL('/ns/m/...', import.meta.url).href → "${origin}/ns/m/__ns_hmr__/..."
2932
3404
  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}`)}`);
3405
+ 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
3406
  }
2935
3407
  catch { }
2936
3408
  // 7) Also fix SFC new URL('/ns/sfc/...', import.meta.url).href → "${origin}/ns/sfc/<ver>/..."
@@ -2946,13 +3418,25 @@ export const piniaSymbol = p.piniaSymbol;
2946
3418
  code = ensureDestructureCoreImports(code);
2947
3419
  }
2948
3420
  catch { }
3421
+ // Boot-time module graph progress: while the app is still replacing the
3422
+ // placeholder, emit lightweight progress updates as /ns/m modules begin
3423
+ // evaluating. This keeps the overlay moving during large initial graphs.
3424
+ try {
3425
+ if (bootTaggedRequest) {
3426
+ const bootModuleLabel = String(spec || '').replace(/\\/g, '/');
3427
+ const bootProgressSnippet = buildBootProgressSnippet(bootModuleLabel);
3428
+ code = bootProgressSnippet + code;
3429
+ code = hoistTopLevelStaticImports(code);
3430
+ }
3431
+ }
3432
+ catch { }
2949
3433
  // Dev-only: link-check static imports to surface missing bindings early
2950
3434
  try {
2951
3435
  const devCheck = process.env.NODE_ENV !== 'production';
2952
3436
  if (devCheck) {
2953
3437
  const ast = babelParse(code, {
2954
3438
  sourceType: 'module',
2955
- plugins: ['typescript', 'importMeta'],
3439
+ plugins: MODULE_IMPORT_ANALYSIS_PLUGINS,
2956
3440
  });
2957
3441
  const imports = [];
2958
3442
  babelTraverse(ast, {
@@ -3036,6 +3520,16 @@ export const piniaSymbol = p.piniaSymbol;
3036
3520
  continue;
3037
3521
  const hasDefault = /\bexport\s+default\b/.test(targetCode) || /export\s*\{\s*default\s*(?:as\s*default)?\s*\}/.test(targetCode);
3038
3522
  if (!hasDefault) {
3523
+ // CJS/UMD modules won't have `export default` — they get CJS-wrapped
3524
+ // by the serving pipeline. Only warn, don't fatally block the importer.
3525
+ const hasCjsPattern = /\bmodule\s*\.\s*exports\b/.test(targetCode) || /\bexports\s*\.\s*\w/.test(targetCode);
3526
+ if (hasCjsPattern) {
3527
+ try {
3528
+ console.warn(`[ns:m][link-check] CJS module without export default: ${u.pathname} (will be CJS-wrapped at serve time)`);
3529
+ }
3530
+ catch { }
3531
+ continue;
3532
+ }
3039
3533
  const msg = `[link-check] Missing default export in ${u.pathname}${u.search} (imported by ${resolvedCandidate || spec})`;
3040
3534
  // Emit a module that throws to surface the exact offender
3041
3535
  res.statusCode = 200;
@@ -3251,27 +3745,92 @@ export const piniaSymbol = p.piniaSymbol;
3251
3745
  server.middlewares.use(async (req, res, next) => {
3252
3746
  try {
3253
3747
  const urlObj = new URL(req.url || '', 'http://localhost');
3254
- if (!(urlObj.pathname === '/ns/core' || /^\/ns\/core\/[\d]+$/.test(urlObj.pathname)))
3748
+ const coreRequest = parseCoreBridgeRequest(urlObj.pathname, urlObj.searchParams, Number(graphVersion || 0));
3749
+ if (!coreRequest)
3255
3750
  return next();
3256
3751
  res.setHeader('Access-Control-Allow-Origin', '*');
3257
3752
  res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
3258
3753
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
3259
3754
  res.setHeader('Pragma', 'no-cache');
3260
3755
  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`;
3756
+ const { hasExplicitVersion, key, normalizedSub, sub, ver } = coreRequest;
3757
+ // Any @nativescript/core subpath import (including shallow ones like
3758
+ // `utils`) may expose exports that are not available from the root
3759
+ // vendor bundle namespace. Serve the actual transformed module content
3760
+ // instead of the lightweight proxy bridge.
3761
+ if (sub) {
3762
+ try {
3763
+ const resolvedSubpath = normalizedSub || sub;
3764
+ const projectRoot = server.config?.root || process.cwd();
3765
+ const resolveModuleId = async (moduleId) => {
3766
+ const resolved = await server.pluginContainer?.resolveId?.(moduleId, undefined);
3767
+ return typeof resolved === 'string' ? resolved : resolved?.id || null;
3768
+ };
3769
+ const resolvedId = await resolveRuntimeCoreModulePath(resolvedSubpath, resolveModuleId);
3770
+ const modulePath = resolvedId || `/node_modules/@nativescript/core/${resolvedSubpath}`;
3771
+ const transformed = await sharedTransformRequest(modulePath);
3772
+ if (!hasExplicitVersion) {
3773
+ if (transformed?.code) {
3774
+ const expandedModuleCode = await expandStarExports(transformed.code, server, projectRoot, verbose);
3775
+ res.statusCode = 200;
3776
+ res.end(buildVersionedCoreSubpathAliasModule(resolvedSubpath, ver, extractExportedNames(expandedModuleCode), hasModuleDefaultExport(expandedModuleCode)));
3777
+ return;
3778
+ }
3779
+ res.statusCode = 200;
3780
+ res.end(buildVersionedCoreSubpathAliasModule(resolvedSubpath, ver));
3781
+ return;
3782
+ }
3783
+ if (transformed?.code) {
3784
+ // Minimal pipeline: Vite already produces correct ESM.
3785
+ // ONLY rewrite specifier strings to device-fetchable URLs.
3786
+ // Do NOT run processCodeForDevice, rewriteImports, or any
3787
+ // other heavy transform — those mangle newlines, eat exports,
3788
+ // and cause cascading "does not provide an export" failures.
3789
+ const moduleCode = rewriteSpecifiersForDevice(transformed.code, getServerOrigin(server), Number(ver));
3790
+ res.statusCode = 200;
3791
+ res.end(moduleCode);
3792
+ return;
3793
+ }
3794
+ }
3795
+ catch (e) {
3796
+ try {
3797
+ console.warn('[ns-core-bridge] deep subpath serve failed:', sub, e?.message);
3798
+ }
3799
+ catch { }
3800
+ }
3801
+ }
3802
+ // Main entry or shallow subpath: use proxy bridge
3803
+ let code = buildVersionedCoreMainBridgeModule(key, ver);
3804
+ if (!sub) {
3805
+ try {
3806
+ const projectRoot = server.config?.root || process.cwd();
3807
+ const coreSpecifier = '@nativescript/core';
3808
+ const resolved = await server.pluginContainer?.resolveId?.(coreSpecifier, undefined);
3809
+ const resolvedId = typeof resolved === 'string' ? resolved : resolved?.id || null;
3810
+ const modulePath = resolvedId || '/node_modules/@nativescript/core/index.js';
3811
+ const staticExportNames = collectStaticExportNamesFromFile(modulePath);
3812
+ const staticExportOrigins = await normalizeCoreExportOriginsForRuntime(collectStaticExportOriginsFromFile(modulePath), async (moduleId) => {
3813
+ const nextResolved = await server.pluginContainer?.resolveId?.(moduleId, undefined);
3814
+ return typeof nextResolved === 'string' ? nextResolved : nextResolved?.id || null;
3815
+ }, modulePath);
3816
+ if (staticExportNames.length) {
3817
+ code = buildVersionedCoreMainBridgeModule(key, ver, staticExportNames, staticExportOrigins);
3818
+ }
3819
+ else {
3820
+ const transformed = await sharedTransformRequest(modulePath);
3821
+ if (transformed?.code) {
3822
+ const expandedModuleCode = await expandStarExports(transformed.code, server, projectRoot, verbose);
3823
+ code = buildVersionedCoreMainBridgeModule(key, ver, extractExportedNames(expandedModuleCode));
3824
+ }
3825
+ }
3826
+ }
3827
+ catch (e) {
3828
+ try {
3829
+ console.warn('[ns-core-bridge] main bridge export discovery failed:', e?.message);
3830
+ }
3831
+ catch { }
3832
+ }
3833
+ }
3275
3834
  res.statusCode = 200;
3276
3835
  res.end(code);
3277
3836
  }
@@ -3300,18 +3859,40 @@ export const piniaSymbol = p.piniaSymbol;
3300
3859
  res.setHeader('Expires', '0');
3301
3860
  let content = '';
3302
3861
  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');
3862
+ const _req = createRequire(import.meta.url);
3863
+ const entryRtPath = _req.resolve('@nativescript/vite/hmr/entry-runtime.js');
3864
+ content = readFileSync(entryRtPath, 'utf-8');
3307
3865
  }
3308
3866
  catch (e) {
3309
- content = 'export default async function start(){ console.error("[/ns/entry-rt] not found"); }\n';
3867
+ // .js not found (source tree without build) — transform .ts on the fly
3868
+ try {
3869
+ const tsPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', 'entry-runtime.ts');
3870
+ if (existsSync(tsPath)) {
3871
+ const tsSource = readFileSync(tsPath, 'utf-8');
3872
+ const result = babelCore.transformSync(tsSource, {
3873
+ filename: tsPath,
3874
+ plugins: [[pluginTransformTypescript, { isTSX: false, allowDeclareFields: true }]],
3875
+ sourceType: 'module',
3876
+ });
3877
+ if (result?.code) {
3878
+ content = result.code;
3879
+ }
3880
+ }
3881
+ }
3882
+ catch (e2) {
3883
+ if (verbose)
3884
+ console.warn('[hmr-http] entry-runtime.ts transform failed', e2);
3885
+ }
3886
+ if (!content) {
3887
+ content = 'export default async function start(){ console.error("[/ns/entry-rt] not found"); }\n';
3888
+ }
3310
3889
  }
3890
+ console.log('[hmr-http] /ns/entry-rt serving', content.length, 'bytes');
3311
3891
  res.statusCode = 200;
3312
3892
  res.end(content);
3313
3893
  }
3314
3894
  catch (e) {
3895
+ console.warn('[hmr-http] /ns/entry-rt error', e);
3315
3896
  next();
3316
3897
  }
3317
3898
  });
@@ -3741,7 +4322,7 @@ export const piniaSymbol = p.piniaSymbol;
3741
4322
  code = outCode;
3742
4323
  }
3743
4324
  catch { }
3744
- code = processCodeForDevice(code, false);
4325
+ code = processCodeForDevice(code, false, true, /(?:^|\/)node_modules\//.test(fullSpec), fullSpec);
3745
4326
  // Transform static .vue imports into static imports from the assembler (no TLA) via AST
3746
4327
  try {
3747
4328
  const importerPath = fullSpec.replace(/[?#].*$/, '');
@@ -4293,11 +4874,10 @@ export const piniaSymbol = p.piniaSymbol;
4293
4874
  parts.push(scriptTransformed);
4294
4875
  parts.push(renderDecl);
4295
4876
  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
4877
  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
4878
  parts.push(`export default __ns_sfc__`);
4299
4879
  let inlineCode = parts.filter(Boolean).join('\n');
4300
- inlineCode = processCodeForDevice(inlineCode, false);
4880
+ inlineCode = processCodeForDevice(inlineCode, false, true);
4301
4881
  try {
4302
4882
  inlineCode = ensureVersionedCoreImports(inlineCode, getServerOrigin(server), Number(ver));
4303
4883
  }
@@ -4372,12 +4952,11 @@ export const piniaSymbol = p.piniaSymbol;
4372
4952
  outParts.push(renderDecl);
4373
4953
  }
4374
4954
  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
4955
  // Export named render as a function that resolves lazily
4377
4956
  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
4957
  outParts.push('export default __ns_sfc__');
4379
4958
  let inlineCode2 = outParts.filter(Boolean).join('\n');
4380
- inlineCode2 = processCodeForDevice(inlineCode2, false);
4959
+ inlineCode2 = processCodeForDevice(inlineCode2, false, true);
4381
4960
  try {
4382
4961
  inlineCode2 = ensureVersionedCoreImports(inlineCode2, getServerOrigin(server), Number(ver));
4383
4962
  }
@@ -4673,12 +5252,11 @@ export const piniaSymbol = p.piniaSymbol;
4673
5252
  }
4674
5253
  let asm;
4675
5254
  if (inlineOk) {
4676
- const diagLine = `// diagnostic:inlineOk ver=${ver} inlineBlock=${!!(inlineBlock && inlineBlock.trim())} helperBindingsLen=${helperBindings.length} renderDeclLen=${renderDecl.length}`;
4677
5255
  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');
5256
+ 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
5257
  }
4680
5258
  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');
5259
+ 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
5260
  }
4683
5261
  }
4684
5262
  else {
@@ -4689,7 +5267,7 @@ export const piniaSymbol = p.piniaSymbol;
4689
5267
  }
4690
5268
  // Run full device processing so helper aliasing and globals are consistent in this path too
4691
5269
  let code = REQUIRE_GUARD_SNIPPET + asm;
4692
- code = processCodeForDevice(code, false);
5270
+ code = processCodeForDevice(code, false, true, /(?:^|\/)node_modules\//.test(base), base);
4693
5271
  try {
4694
5272
  code = ensureVersionedCoreImports(code, getServerOrigin(server), Number(ver));
4695
5273
  }
@@ -4862,7 +5440,7 @@ export const piniaSymbol = p.piniaSymbol;
4862
5440
  let code = transformed.code;
4863
5441
  // Reuse existing sanitation chain (lightweight)
4864
5442
  code = cleanCode(code);
4865
- code = processCodeForDevice(code, false);
5443
+ code = processCodeForDevice(code, false, true, /(?:^|\/)node_modules\//.test(resolvedCandidate || spec), resolvedCandidate || spec);
4866
5444
  try {
4867
5445
  code = ensureVersionedCoreImports(code, getServerOrigin(server), graphVersion);
4868
5446
  }
@@ -4917,7 +5495,7 @@ export const piniaSymbol = p.piniaSymbol;
4917
5495
  if (depTrans?.code && depResolved) {
4918
5496
  let depCode = depTrans.code;
4919
5497
  depCode = cleanCode(depCode);
4920
- depCode = processCodeForDevice(depCode, false);
5498
+ depCode = processCodeForDevice(depCode, false, true, /(?:^|\/)node_modules\//.test(depResolved), depResolved);
4921
5499
  try {
4922
5500
  depCode = ensureVersionedCoreImports(depCode, getServerOrigin(server), graphVersion);
4923
5501
  }
@@ -4950,8 +5528,8 @@ export const piniaSymbol = p.piniaSymbol;
4950
5528
  ts: Date.now(),
4951
5529
  delta: true,
4952
5530
  };
4953
- wss.clients.forEach((c) => {
4954
- if (c.readyState === c.OPEN) {
5531
+ wss?.clients.forEach((c) => {
5532
+ if (isSocketClientOpen(c)) {
4955
5533
  try {
4956
5534
  c.send(JSON.stringify(single));
4957
5535
  }
@@ -4990,32 +5568,33 @@ export const piniaSymbol = p.piniaSymbol;
4990
5568
  if (verbose)
4991
5569
  console.warn('[hmr-ws][graph] initial population failed', e);
4992
5570
  }
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
- }
5571
+ // Send SFC registry on every connection (not just the first).
5572
+ // When the NativeScript app restarts (e.g. CLI auto-reload), the new
5573
+ // JS context has an empty sfcArtifactMap. Without the registry the
5574
+ // rescue-mount cannot find the root .vue component.
5575
+ try {
5576
+ await ACTIVE_STRATEGY.buildRegistry({
5577
+ server,
5578
+ sfcFileMap,
5579
+ depFileMap,
5580
+ wss: wss,
5581
+ verbose,
5582
+ helpers: {
5583
+ cleanCode,
5584
+ collectImportDependencies,
5585
+ isCoreGlobalsReference,
5586
+ isNativeScriptCoreModule,
5587
+ isNativeScriptPluginModule,
5588
+ resolveVendorFromCandidate,
5589
+ createHash: (value) => createHash('md5').update(value).digest('hex'),
5590
+ rewriteImports,
5591
+ processSfcCode,
5592
+ },
5593
+ });
5594
+ registrySent = true;
5595
+ }
5596
+ catch (error) {
5597
+ console.warn('[hmr-ws] Failed to send registry:', error);
5019
5598
  }
5020
5599
  emitFullGraph(ws);
5021
5600
  // After sending registry & graph also send current module manifest if any
@@ -5038,16 +5617,38 @@ export const piniaSymbol = p.piniaSymbol;
5038
5617
  if (!wss) {
5039
5618
  return;
5040
5619
  }
5620
+ if (isRuntimeGraphExcludedPath(file)) {
5621
+ return;
5622
+ }
5041
5623
  // Graph update for this file change (wrapped to avoid aborting rest of handler)
5042
5624
  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);
5625
+ const skipAngularHtmlGraphUpdate = ACTIVE_STRATEGY.flavor === 'angular' && /\.(html|htm)$/i.test(file);
5626
+ if (!skipAngularHtmlGraphUpdate) {
5627
+ const graphTargets = collectGraphUpdateModulesForHotUpdate({
5628
+ file,
5629
+ flavor: ACTIVE_STRATEGY.flavor,
5630
+ modules: ctx.modules,
5631
+ getModuleById: (id) => server.moduleGraph.getModuleById(id),
5632
+ });
5633
+ for (const mod of graphTargets) {
5634
+ if (!mod?.id)
5635
+ continue;
5636
+ try {
5637
+ const deps = Array.from(mod.importedModules || [])
5638
+ .map((m) => (m.id || '').replace(/\?.*$/, ''))
5639
+ .filter(Boolean);
5640
+ const transformed = await server.transformRequest(mod.id);
5641
+ const code = transformed?.code || '';
5642
+ upsertGraphModule((mod.id || '').replace(/\?.*$/, ''), code, deps, {
5643
+ emitDeltaOnInsert: true,
5644
+ broadcastDelta: ACTIVE_STRATEGY.flavor !== 'angular',
5645
+ });
5646
+ }
5647
+ catch (error) {
5648
+ if (verbose)
5649
+ console.warn('[hmr-ws][v2] failed graph update target', mod.id, error);
5650
+ }
5651
+ }
5051
5652
  }
5052
5653
  }
5053
5654
  catch (e) {
@@ -5084,7 +5685,7 @@ export const piniaSymbol = p.piniaSymbol;
5084
5685
  ],
5085
5686
  };
5086
5687
  wss.clients.forEach((client) => {
5087
- if (client.readyState === client.OPEN) {
5688
+ if (isSocketClientOpen(client)) {
5088
5689
  client.send(JSON.stringify(msg));
5089
5690
  }
5090
5691
  });
@@ -5099,20 +5700,103 @@ export const piniaSymbol = p.piniaSymbol;
5099
5700
  // For Angular, react to component TS or external template HTML changes under /src
5100
5701
  const isHtml = file.endsWith('.html');
5101
5702
  const isTs = file.endsWith('.ts');
5703
+ const angularHotUpdateRoots = collectAngularHotUpdateRoots({
5704
+ file,
5705
+ modules: ctx.modules,
5706
+ getModuleById: (id) => server.moduleGraph.getModuleById(id),
5707
+ getModulesByFile: (targetFile) => server.moduleGraph.getModulesByFile?.(targetFile),
5708
+ });
5102
5709
  if (!(isHtml || isTs))
5103
5710
  return;
5711
+ if (angularHotUpdateRoots.length) {
5712
+ for (const mod of angularHotUpdateRoots) {
5713
+ try {
5714
+ server.moduleGraph.invalidateModule(mod);
5715
+ }
5716
+ catch (invalidationError) {
5717
+ if (verbose) {
5718
+ console.warn('[hmr-ws][angular] hot-update root invalidation failed', mod?.id, invalidationError);
5719
+ }
5720
+ }
5721
+ }
5722
+ if (verbose) {
5723
+ console.log('[hmr-ws][angular] invalidated hot-update root modules:', angularHotUpdateRoots.length);
5724
+ }
5725
+ }
5726
+ const angularTransitiveInvalidationRoots = (angularHotUpdateRoots.length ? angularHotUpdateRoots : ctx.modules);
5727
+ if (shouldInvalidateAngularTransitiveImporters({ flavor: ACTIVE_STRATEGY.flavor, file })) {
5728
+ try {
5729
+ const transitiveImporters = collectAngularTransitiveImportersForInvalidation({
5730
+ modules: angularTransitiveInvalidationRoots,
5731
+ isExcluded: (id) => id.includes('/node_modules/'),
5732
+ maxDepth: 16,
5733
+ });
5734
+ for (const mod of transitiveImporters) {
5735
+ try {
5736
+ server.moduleGraph.invalidateModule(mod);
5737
+ }
5738
+ catch (invalidationError) {
5739
+ if (verbose) {
5740
+ console.warn('[hmr-ws][angular] transitive importer invalidation failed', mod?.id, invalidationError);
5741
+ }
5742
+ }
5743
+ }
5744
+ if (verbose && transitiveImporters.length) {
5745
+ console.log('[hmr-ws][angular] invalidated transitive importers:', transitiveImporters.length);
5746
+ }
5747
+ }
5748
+ catch (error) {
5749
+ if (verbose)
5750
+ console.warn('[hmr-ws][angular] transitive importer collection failed', error);
5751
+ }
5752
+ }
5753
+ try {
5754
+ const transitiveImporters = shouldInvalidateAngularTransitiveImporters({ flavor: ACTIVE_STRATEGY.flavor, file })
5755
+ ? collectAngularTransitiveImportersForInvalidation({
5756
+ modules: angularTransitiveInvalidationRoots,
5757
+ isExcluded: (id) => id.includes('/node_modules/'),
5758
+ maxDepth: 16,
5759
+ })
5760
+ : [];
5761
+ const transformCacheInvalidationUrls = new Set(collectAngularTransformCacheInvalidationUrls({
5762
+ file,
5763
+ isTs,
5764
+ hotUpdateRoots: angularHotUpdateRoots,
5765
+ transitiveImporters,
5766
+ projectRoot: server.config.root || process.cwd(),
5767
+ }));
5768
+ if (transformCacheInvalidationUrls.size) {
5769
+ sharedTransformRequest.invalidateMany(transformCacheInvalidationUrls);
5770
+ if (verbose) {
5771
+ console.log('[hmr-ws][angular] purged shared transform cache entries:', transformCacheInvalidationUrls.size);
5772
+ }
5773
+ }
5774
+ }
5775
+ catch (error) {
5776
+ if (verbose)
5777
+ console.warn('[hmr-ws][angular] shared transform cache purge failed', error);
5778
+ }
5104
5779
  try {
5105
5780
  const root = server.config.root || process.cwd();
5106
5781
  const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
5782
+ rememberAngularReloadSuppression(root, file);
5107
5783
  const origin = getServerOrigin(server);
5108
5784
  const msg = {
5109
5785
  type: 'ns:angular-update',
5110
5786
  origin,
5111
5787
  path: rel,
5788
+ version: graphVersion,
5112
5789
  timestamp: Date.now(),
5113
5790
  };
5791
+ if (verbose) {
5792
+ console.log('[hmr-ws][angular] broadcasting update', Array.from(wss.clients || []).map((client) => ({
5793
+ role: getHmrSocketRole(client),
5794
+ readyState: client.readyState,
5795
+ openState: client.OPEN,
5796
+ })));
5797
+ }
5114
5798
  wss.clients.forEach((client) => {
5115
- if (client.readyState === client.OPEN) {
5799
+ if (isSocketClientOpen(client)) {
5116
5800
  client.send(JSON.stringify(msg));
5117
5801
  }
5118
5802
  });
@@ -5120,6 +5804,9 @@ export const piniaSymbol = p.piniaSymbol;
5120
5804
  catch (error) {
5121
5805
  console.warn('[hmr-ws][angular] update failed:', error);
5122
5806
  }
5807
+ if (shouldSuppressDefaultViteHotUpdate({ flavor: ACTIVE_STRATEGY.flavor, file })) {
5808
+ return [];
5809
+ }
5123
5810
  return;
5124
5811
  }
5125
5812
  // TypeScript flavor: emit generic graph delta for app XML/TS/style changes
@@ -5131,10 +5818,7 @@ export const piniaSymbol = p.piniaSymbol;
5131
5818
  // Treat the changed file itself as a graph module with no deps. We only
5132
5819
  // care that its hash/identity changes so the client sees a delta and can
5133
5820
  // 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], []);
5821
+ upsertGraphModule(rel, '', [], { emitDeltaOnInsert: true });
5138
5822
  }
5139
5823
  catch (e) {
5140
5824
  if (verbose)
@@ -5142,6 +5826,43 @@ export const piniaSymbol = p.piniaSymbol;
5142
5826
  }
5143
5827
  return;
5144
5828
  }
5829
+ // Solid flavor: emit graph delta for app TSX/TS/JSX file changes.
5830
+ // The common graph-update block above (moduleGraph lookup) may have
5831
+ // already emitted a delta if the file was in Vite's module graph.
5832
+ // This handler ensures a delta is emitted even if the module wasn't
5833
+ // found (e.g. new file, or moduleGraph mismatch), and provides
5834
+ // Solid-specific logging. The client-side processQueue handles
5835
+ // propagation from non-component .ts files to .tsx component boundaries.
5836
+ if (ACTIVE_STRATEGY.flavor === 'solid') {
5837
+ const isSolidFile = /\.(tsx?|jsx?)$/i.test(file);
5838
+ if (!isSolidFile)
5839
+ return;
5840
+ try {
5841
+ const rel = '/' + path.posix.normalize(path.relative(root, file)).split(path.sep).join('/');
5842
+ if (verbose)
5843
+ console.log('[hmr-ws][solid] app file hot update', { file, rel });
5844
+ // If the common block already upserted (hash changed), this will
5845
+ // detect unchanged hash and no-op. If the common block missed it
5846
+ // (module not in Vite's graph), this forces the delta emission.
5847
+ const normalizedId = normalizeGraphId(rel);
5848
+ const existing = graph.get(normalizedId);
5849
+ if (!existing) {
5850
+ // Module not in graph yet — force upsert with timestamp-based
5851
+ // hash so the client sees a change.
5852
+ upsertGraphModule(rel, `/* solid-hmr ${Date.now()} */`, [], { emitDeltaOnInsert: true });
5853
+ }
5854
+ // Log what we're sending so devs can trace the flow on the server side.
5855
+ if (verbose) {
5856
+ const gm = graph.get(normalizedId);
5857
+ console.log('[hmr-ws][solid] delta module', { id: gm?.id, hash: gm?.hash });
5858
+ }
5859
+ }
5860
+ catch (e) {
5861
+ if (verbose)
5862
+ console.warn('[hmr-ws][solid] failed to handle hot update for', file, e);
5863
+ }
5864
+ return;
5865
+ }
5145
5866
  // Handle .vue file updates
5146
5867
  if (!file.endsWith('.vue')) {
5147
5868
  if (verbose)
@@ -5249,6 +5970,7 @@ export const piniaSymbol = p.piniaSymbol;
5249
5970
  // Rewrite ONLY .vue imports (everything else is now inlined)
5250
5971
  const projectRoot = server.config.root || process.cwd();
5251
5972
  code = rewriteImports(code, rel, sfcFileMap, depFileMap, projectRoot, opts.verbose, undefined);
5973
+ upsertGraphModule(rel, code, [...deps, ...vueDeps]);
5252
5974
  // Add HMR runtime prelude (CRITICAL for runtime)
5253
5975
  const hmrPrelude = `
5254
5976
  // Embedded HMR Runtime for NativeScript runtime
@@ -5324,7 +6046,7 @@ if (typeof __VUE_HMR_RUNTIME__ === 'undefined') {
5324
6046
  version: graphVersion,
5325
6047
  };
5326
6048
  wss.clients.forEach((client) => {
5327
- if (client.readyState === client.OPEN) {
6049
+ if (isSocketClientOpen(client)) {
5328
6050
  client.send(JSON.stringify(registryUpdateMsg));
5329
6051
  }
5330
6052
  });
@@ -5495,4 +6217,6 @@ function getServerOrigin(server) {
5495
6217
  // Test-only export: allow unit tests to run the sanitizer on snippets without booting a server
5496
6218
  // Safe in production builds; this is a named export that tests can import explicitly.
5497
6219
  export const __test_processCodeForDevice = processCodeForDevice;
6220
+ export const __test_resolveVendorRouting = resolveVendorRouting;
6221
+ export const __test_getBlockedDeviceNodeModulesReason = getBlockedDeviceNodeModulesReason;
5498
6222
  //# sourceMappingURL=websocket.js.map