@@ -16,6 +16,11 @@ static inline bool StartsWith(const std::string& s, const char* prefix) {
1616 return s.size () >= n && s.compare (0 , n, prefix) == 0 ;
1717}
1818
19+ static inline bool EndsWith (const std::string& s, const char * suffix) {
20+ size_t n = strlen (suffix);
21+ return s.size () >= n && s.compare (s.size () - n, n, suffix) == 0 ;
22+ }
23+
1924// Per-module hot data and callbacks. Keyed by canonical module path (file path or URL).
2025static std::unordered_map<std::string, v8::Global<v8::Object>> g_hotData;
2126static std::unordered_map<std::string, std::vector<v8::Global<v8::Function>>> g_hotAccept;
@@ -76,6 +81,63 @@ void InitializeImportMetaHot(v8::Isolate* isolate,
7681
7782 v8::HandleScope scope (isolate);
7883
84+ // Canonicalize key to ensure per-module hot.data persists across HMR URLs.
85+ // Important: this must NOT affect the HTTP loader cache key; otherwise HMR fetches
86+ // can collapse onto an already-evaluated module and no update occurs.
87+ auto canonicalHotKey = [&](const std::string& in) -> std::string {
88+ // Some loaders wrap HTTP module URLs as file://http(s)://...
89+ std::string s = in;
90+ if (StartsWith (s, " file://http://" ) || StartsWith (s, " file://https://" )) {
91+ s = s.substr (strlen (" file://" ));
92+ }
93+
94+ // Drop fragment
95+ size_t hashPos = s.find (' #' );
96+ if (hashPos != std::string::npos) {
97+ s = s.substr (0 , hashPos);
98+ }
99+
100+ // Drop query (we want hot key stability)
101+ size_t qPos = s.find (' ?' );
102+ std::string noQuery = (qPos == std::string::npos) ? s : s.substr (0 , qPos);
103+
104+ // If it's an http(s) URL, normalize only the path portion below.
105+ size_t schemePos = noQuery.find (" ://" );
106+ size_t pathStart = (schemePos == std::string::npos) ? 0 : noQuery.find (' /' , schemePos + 3 );
107+ if (pathStart == std::string::npos) {
108+ // No path; return without query
109+ return noQuery;
110+ }
111+
112+ std::string origin = noQuery.substr (0 , pathStart);
113+ std::string path = noQuery.substr (pathStart);
114+
115+ // Normalize NS HMR virtual module paths:
116+ // /ns/m/__ns_hmr__/<token>/<rest> -> /ns/m/<rest>
117+ const char * hmrPrefix = " /ns/m/__ns_hmr__/" ;
118+ size_t hmrLen = strlen (hmrPrefix);
119+ if (path.compare (0 , hmrLen, hmrPrefix) == 0 ) {
120+ size_t nextSlash = path.find (' /' , hmrLen);
121+ if (nextSlash != std::string::npos) {
122+ path = std::string (" /ns/m/" ) + path.substr (nextSlash + 1 );
123+ }
124+ }
125+
126+ // Normalize common script extensions so `/foo` and `/foo.ts` share hot.data.
127+ const char * exts[] = {" .ts" , " .tsx" , " .js" , " .jsx" , " .mjs" , " .cjs" };
128+ for (auto ext : exts) {
129+ if (EndsWith (path, ext)) {
130+ path = path.substr (0 , path.size () - strlen (ext));
131+ break ;
132+ }
133+ }
134+
135+ // Also drop `.vue`? No — SFC endpoints should stay distinct.
136+ return origin + path;
137+ };
138+
139+ const std::string key = canonicalHotKey (modulePath);
140+
79141 auto makeKeyData = [&](const std::string& key) -> Local<Value> {
80142 return ArgConverter::ConvertToV8String (isolate, key);
81143 };
@@ -121,33 +183,38 @@ void InitializeImportMetaHot(v8::Isolate* isolate,
121183
122184 Local<Object> hot = Object::New (isolate);
123185 hot->CreateDataProperty (context, ArgConverter::ConvertToV8String (isolate, " data" ),
124- GetOrCreateHotData (isolate, modulePath )).Check ();
186+ GetOrCreateHotData (isolate, key )).Check ();
125187 hot->CreateDataProperty (context, ArgConverter::ConvertToV8String (isolate, " prune" ),
126188 v8::Boolean::New (isolate, false )).Check ();
127189 hot->CreateDataProperty (
128190 context, ArgConverter::ConvertToV8String (isolate, " accept" ),
129- v8::Function::New (context, acceptCb, makeKeyData (modulePath )).ToLocalChecked ()).Check ();
191+ v8::Function::New (context, acceptCb, makeKeyData (key )).ToLocalChecked ()).Check ();
130192 hot->CreateDataProperty (
131193 context, ArgConverter::ConvertToV8String (isolate, " dispose" ),
132- v8::Function::New (context, disposeCb, makeKeyData (modulePath )).ToLocalChecked ()).Check ();
194+ v8::Function::New (context, disposeCb, makeKeyData (key )).ToLocalChecked ()).Check ();
133195 hot->CreateDataProperty (
134196 context, ArgConverter::ConvertToV8String (isolate, " decline" ),
135- v8::Function::New (context, declineCb, makeKeyData (modulePath )).ToLocalChecked ()).Check ();
197+ v8::Function::New (context, declineCb, makeKeyData (key )).ToLocalChecked ()).Check ();
136198 hot->CreateDataProperty (
137199 context, ArgConverter::ConvertToV8String (isolate, " invalidate" ),
138- v8::Function::New (context, invalidateCb, makeKeyData (modulePath )).ToLocalChecked ()).Check ();
200+ v8::Function::New (context, invalidateCb, makeKeyData (key )).ToLocalChecked ()).Check ();
139201
140202 importMeta->CreateDataProperty (context, ArgConverter::ConvertToV8String (isolate, " hot" ), hot).Check ();
141203}
142204
143205// Drop fragments and normalize parameters for consistent registry keys.
144206std::string CanonicalizeHttpUrlKey (const std::string& url) {
145- if (!(StartsWith (url, " http://" ) || StartsWith (url, " https://" ))) {
146- return url;
207+ // Some loaders wrap HTTP module URLs as file://http(s)://...
208+ std::string normalizedUrl = url;
209+ if (StartsWith (normalizedUrl, " file://http://" ) || StartsWith (normalizedUrl, " file://https://" )) {
210+ normalizedUrl = normalizedUrl.substr (strlen (" file://" ));
211+ }
212+ if (!(StartsWith (normalizedUrl, " http://" ) || StartsWith (normalizedUrl, " https://" ))) {
213+ return normalizedUrl;
147214 }
148215 // Remove fragment
149- size_t hashPos = url .find (' #' );
150- std::string noHash = (hashPos == std::string::npos) ? url : url .substr (0 , hashPos);
216+ size_t hashPos = normalizedUrl .find (' #' );
217+ std::string noHash = (hashPos == std::string::npos) ? normalizedUrl : normalizedUrl .substr (0 , hashPos);
151218
152219 // Split into origin+path and query
153220 size_t qPos = noHash.find (' ?' );
@@ -158,10 +225,13 @@ std::string CanonicalizeHttpUrlKey(const std::string& url) {
158225 // - /ns/rt/<ver> -> /ns/rt
159226 // - /ns/core/<ver> -> /ns/core
160227 size_t schemePos = originAndPath.find (" ://" );
161- if (schemePos != std::string::npos) {
162- size_t pathStart = originAndPath.find (' /' , schemePos + 3 );
163- if (pathStart != std::string::npos) {
164- std::string pathOnly = originAndPath.substr (pathStart);
228+ size_t pathStart = (schemePos == std::string::npos) ? std::string::npos : originAndPath.find (' /' , schemePos + 3 );
229+ if (pathStart == std::string::npos) {
230+ // No path; preserve query as-is (fragment already removed).
231+ return noHash;
232+ }
233+ {
234+ std::string pathOnly = originAndPath.substr (pathStart);
165235 auto normalizeBridge = [&](const char * needle) {
166236 size_t nlen = strlen (needle);
167237 if (pathOnly.size () <= nlen) return false ;
@@ -180,12 +250,29 @@ std::string CanonicalizeHttpUrlKey(const std::string& url) {
180250 if (!normalizeBridge (" /ns/rt" )) {
181251 normalizeBridge (" /ns/core" );
182252 }
253+ }
254+
255+ // IMPORTANT: This function is used as an HTTP module registry/cache key.
256+ // For general-purpose HTTP module loading (public internet), the query string
257+ // can be part of the module's identity (auth, content versioning, routing, etc).
258+ // Therefore we only apply query normalization (sorting/dropping) for known
259+ // NativeScript dev endpoints where `t`/`v`/`import` are purely cache busters.
260+ {
261+ std::string pathOnly = originAndPath.substr (pathStart);
262+ const bool isDevEndpoint =
263+ StartsWith (pathOnly, " /ns/" ) ||
264+ StartsWith (pathOnly, " /node_modules/.vite/" ) ||
265+ StartsWith (pathOnly, " /@id/" ) ||
266+ StartsWith (pathOnly, " /@fs/" );
267+ if (!isDevEndpoint) {
268+ // Preserve query as-is (fragment already removed).
269+ return noHash;
183270 }
184271 }
185272
186273 if (query.empty ()) return originAndPath;
187274
188- // Strip ?import markers and sort remaining query params for stability
275+ // Strip ?import markers / cache busters and sort remaining query params for stability
189276 std::vector<std::string> kept;
190277 size_t start = 0 ;
191278 while (start <= query.size ()) {
@@ -194,7 +281,7 @@ std::string CanonicalizeHttpUrlKey(const std::string& url) {
194281 if (!pair.empty ()) {
195282 size_t eq = pair.find (' =' );
196283 std::string name = (eq == std::string::npos) ? pair : pair.substr (0 , eq);
197- if (!(name == " import" )) kept.push_back (pair);
284+ if (!(name == " import" || name == " t " || name == " v " )) kept.push_back (pair);
198285 }
199286 if (amp == std::string::npos) break ;
200287 start = amp + 1 ;
0 commit comments