@@ -19,6 +19,11 @@ static inline bool StartsWith(const std::string& s, const char* prefix) {
1919 return s.size () >= n && s.compare (0 , n, prefix) == 0 ;
2020}
2121
22+ static inline bool EndsWith (const std::string& s, const char * suffix) {
23+ size_t n = strlen (suffix);
24+ return s.size () >= n && s.compare (s.size () - n, n, suffix) == 0 ;
25+ }
26+
2227// Per-module hot data and callbacks. Keyed by canonical module path.
2328static std::unordered_map<std::string, v8::Global<v8::Object>> g_hotData;
2429static std::unordered_map<std::string, std::vector<v8::Global<v8::Function>>> g_hotAccept;
@@ -82,9 +87,67 @@ void InitializeImportMetaHot(v8::Isolate* isolate,
8287 // Ensure context scope for property creation
8388 v8::HandleScope scope (isolate);
8489
90+ // Canonicalize key to ensure per-module hot.data persists across HMR URLs.
91+ // Important: this must NOT affect the HTTP loader cache key; otherwise HMR fetches
92+ // can collapse onto an already-evaluated module and no update occurs.
93+ auto canonicalHotKey = [&](const std::string& in) -> std::string {
94+ // Unwrap file://http(s)://...
95+ std::string s = in;
96+ if (StartsWith (s, " file://http://" ) || StartsWith (s, " file://https://" )) {
97+ s = s.substr (strlen (" file://" ));
98+ }
99+
100+ // Drop fragment
101+ size_t hashPos = s.find (' #' );
102+ if (hashPos != std::string::npos) s = s.substr (0 , hashPos);
103+
104+ // Split query (we'll drop it for hot key stability)
105+ size_t qPos = s.find (' ?' );
106+ std::string noQuery = (qPos == std::string::npos) ? s : s.substr (0 , qPos);
107+
108+ // If it's an http(s) URL, normalize only the path portion below.
109+ size_t schemePos = noQuery.find (" ://" );
110+ size_t pathStart = (schemePos == std::string::npos) ? 0 : noQuery.find (' /' , schemePos + 3 );
111+ if (pathStart == std::string::npos) {
112+ // No path; return without query
113+ return noQuery;
114+ }
115+
116+ std::string origin = noQuery.substr (0 , pathStart);
117+ std::string path = noQuery.substr (pathStart);
118+
119+ // Normalize NS HMR virtual module paths:
120+ // /ns/m/__ns_hmr__/<token>/<rest> -> /ns/m/<rest>
121+ const char * hmrPrefix = " /ns/m/__ns_hmr__/" ;
122+ size_t hmrLen = strlen (hmrPrefix);
123+ if (path.compare (0 , hmrLen, hmrPrefix) == 0 ) {
124+ size_t nextSlash = path.find (' /' , hmrLen);
125+ if (nextSlash != std::string::npos) {
126+ path = std::string (" /ns/m/" ) + path.substr (nextSlash + 1 );
127+ }
128+ }
129+
130+ // Normalize common script extensions so `/foo` and `/foo.ts` share hot.data.
131+ const char * exts[] = {" .ts" , " .tsx" , " .js" , " .jsx" , " .mjs" , " .cjs" };
132+ for (auto ext : exts) {
133+ if (EndsWith (path, ext)) {
134+ path = path.substr (0 , path.size () - strlen (ext));
135+ break ;
136+ }
137+ }
138+
139+ // Also drop `.vue`? No — SFC endpoints should stay distinct.
140+ return origin + path;
141+ };
142+
143+ const std::string key = canonicalHotKey (modulePath);
144+ if (tns::IsScriptLoadingLogEnabled () && key != modulePath) {
145+ Log (@" [hmr] canonical key: %s -> %s " , modulePath.c_str (), key.c_str ());
146+ }
147+
85148 // Helper to capture key in function data
86- auto makeKeyData = [&](const std::string& key ) -> Local<Value> {
87- return tns::ToV8String (isolate, key .c_str ());
149+ auto makeKeyData = [&](const std::string& k ) -> Local<Value> {
150+ return tns::ToV8String (isolate, k .c_str ());
88151 };
89152
90153 // accept([deps], cb?) — we register cb if provided; deps ignored for now
@@ -134,22 +197,22 @@ void InitializeImportMetaHot(v8::Isolate* isolate,
134197 Local<Object> hot = Object::New (isolate);
135198 // Stable flags
136199 hot->CreateDataProperty (context, tns::ToV8String (isolate, " data" ),
137- GetOrCreateHotData (isolate, modulePath )).Check ();
200+ GetOrCreateHotData (isolate, key )).Check ();
138201 hot->CreateDataProperty (context, tns::ToV8String (isolate, " prune" ),
139202 v8::Boolean::New (isolate, false )).Check ();
140203 // Methods
141204 hot->CreateDataProperty (
142205 context, tns::ToV8String (isolate, " accept" ),
143- v8::Function::New (context, acceptCb, makeKeyData (modulePath )).ToLocalChecked ()).Check ();
206+ v8::Function::New (context, acceptCb, makeKeyData (key )).ToLocalChecked ()).Check ();
144207 hot->CreateDataProperty (
145208 context, tns::ToV8String (isolate, " dispose" ),
146- v8::Function::New (context, disposeCb, makeKeyData (modulePath )).ToLocalChecked ()).Check ();
209+ v8::Function::New (context, disposeCb, makeKeyData (key )).ToLocalChecked ()).Check ();
147210 hot->CreateDataProperty (
148211 context, tns::ToV8String (isolate, " decline" ),
149- v8::Function::New (context, declineCb, makeKeyData (modulePath )).ToLocalChecked ()).Check ();
212+ v8::Function::New (context, declineCb, makeKeyData (key )).ToLocalChecked ()).Check ();
150213 hot->CreateDataProperty (
151214 context, tns::ToV8String (isolate, " invalidate" ),
152- v8::Function::New (context, invalidateCb, makeKeyData (modulePath )).ToLocalChecked ()).Check ();
215+ v8::Function::New (context, invalidateCb, makeKeyData (key )).ToLocalChecked ()).Check ();
153216
154217 // Attach to import.meta
155218 importMeta->CreateDataProperty (
@@ -158,15 +221,20 @@ void InitializeImportMetaHot(v8::Isolate* isolate,
158221}
159222
160223// ─────────────────────────────────────────────────────────────
161- // Dev HTTP loader helpers
224+ // HTTP loader helpers
162225
163226std::string CanonicalizeHttpUrlKey (const std::string& url) {
164- if (!(StartsWith (url, " http://" ) || StartsWith (url, " https://" ))) {
165- return url;
227+ // Some loaders wrap HTTP module URLs as file://http(s)://...
228+ std::string normalizedUrl = url;
229+ if (StartsWith (normalizedUrl, " file://http://" ) || StartsWith (normalizedUrl, " file://https://" )) {
230+ normalizedUrl = normalizedUrl.substr (strlen (" file://" ));
231+ }
232+ if (!(StartsWith (normalizedUrl, " http://" ) || StartsWith (normalizedUrl, " https://" ))) {
233+ return normalizedUrl;
166234 }
167235 // Drop fragment entirely
168- size_t hashPos = url .find (' #' );
169- std::string noHash = (hashPos == std::string::npos) ? url : url .substr (0 , hashPos);
236+ size_t hashPos = normalizedUrl .find (' #' );
237+ std::string noHash = (hashPos == std::string::npos) ? normalizedUrl : normalizedUrl .substr (0 , hashPos);
170238
171239 // Locate path start and query start
172240 size_t schemePos = noHash.find (" ://" );
@@ -184,10 +252,10 @@ void InitializeImportMetaHot(v8::Isolate* isolate,
184252 std::string originAndPath = (qPos == std::string::npos) ? noHash : noHash.substr (0 , qPos);
185253 std::string query = (qPos == std::string::npos) ? std::string () : noHash.substr (qPos + 1 );
186254
187- // Normalize bridge endpoints to keep a single realm across HMR updates :
255+ // Normalize bridge endpoints to keep a single realm across reloads :
188256 // - /ns/rt/<ver> -> /ns/rt
189257 // - /ns/core/<ver> -> /ns/core
190- // Preserve query params (e.g. /ns/core?p=...) as part of module identity.
258+ // Preserve query params (e.g. /ns/core?p=...), except for internal cache-busters (import, t, v), as part of module identity.
191259 {
192260 std::string pathOnly = originAndPath.substr (pathStart);
193261 auto normalizeBridge = [&](const char * needle) {
@@ -213,9 +281,27 @@ void InitializeImportMetaHot(v8::Isolate* isolate,
213281 normalizeBridge (" /ns/core" );
214282 }
215283
284+ // IMPORTANT: This function is used as an HTTP module registry/cache key.
285+ // For general-purpose HTTP module loading (public internet), the query string
286+ // can be part of the module's identity (auth, content versioning, routing, etc).
287+ // Therefore we only apply query normalization (sorting/dropping) for known
288+ // NativeScript dev endpoints where `t`/`v`/`import` are purely cache busters.
289+ {
290+ std::string pathOnly = originAndPath.substr (pathStart);
291+ const bool isDevEndpoint =
292+ StartsWith (pathOnly, " /ns/" ) ||
293+ StartsWith (pathOnly, " /node_modules/.vite/" ) ||
294+ StartsWith (pathOnly, " /@id/" ) ||
295+ StartsWith (pathOnly, " /@fs/" );
296+ if (!isDevEndpoint) {
297+ // Preserve query as-is (fragment already removed).
298+ return noHash;
299+ }
300+ }
301+
216302 if (query.empty ()) return originAndPath;
217303
218- // Keep all params except Vite's import marker ; sort for stability.
304+ // Keep all params except typical import markers or t/v cache busters ; sort for stability.
219305 std::vector<std::string> kept;
220306 size_t start = 0 ;
221307 while (start <= query.size ()) {
@@ -224,7 +310,8 @@ void InitializeImportMetaHot(v8::Isolate* isolate,
224310 if (!pair.empty ()) {
225311 size_t eq = pair.find (' =' );
226312 std::string name = (eq == std::string::npos) ? pair : pair.substr (0 , eq);
227- if (!(name == " import" )) kept.push_back (pair);
313+ // Drop import marker and common cache-busting stamps.
314+ if (!(name == " import" || name == " t" || name == " v" )) kept.push_back (pair);
228315 }
229316 if (amp == std::string::npos) break ;
230317 start = amp + 1 ;
0 commit comments