Skip to content

Commit 608257d

Browse files
committed
fix: keep vendor ESM across reloadApplication
Flushing the whole module registry remints certain realms. Evict app chunks only; leave vendor/runtime, node_modules, and synthetic namespaces.
1 parent 620c6fa commit 608257d

5 files changed

Lines changed: 67 additions & 1 deletion

File tree

‎NativeScript/runtime/ModuleInternalCallbacks.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ extern std::unordered_map<std::string, v8::Global<v8::Module>> g_moduleRegistry;
1414
// fails
1515
void RemoveModuleFromRegistry(const std::string& canonicalPath);
1616
void ClearModuleRegistry();
17+
// Isolate reload must evict app ESM (bundle.mjs, lazy chunks) so OTA JS
18+
// re-evaluates, but keep webpack vendor/runtime (and node_modules) so
19+
// @angular/core is not minted into a second realm.
20+
void ClearModuleRegistryForApplicationReload();
1721

1822
// Keep a fallback copy of the last evaluated module so could be served while
1923
// reloading if needed

‎NativeScript/runtime/ModuleInternalCallbacks.mm‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,47 @@ void ClearModuleRegistry() {
316316
g_moduleFallbackByRelative.clear();
317317
}
318318

319+
static std::string ModuleRegistryBasename(const std::string& key) {
320+
auto pos = key.find_last_of("/\\");
321+
std::string base = pos == std::string::npos ? key : key.substr(pos + 1);
322+
auto query = base.find('?');
323+
if (query != std::string::npos) {
324+
base = base.substr(0, query);
325+
}
326+
return base;
327+
}
328+
329+
static bool ShouldPreserveModuleOnApplicationReload(const std::string& key) {
330+
if (key.rfind("node:", 0) == 0 || key.rfind("optional:", 0) == 0 || key.rfind("blob:", 0) == 0) {
331+
return true;
332+
}
333+
if (key.find("/node_modules/") != std::string::npos ||
334+
key.find("\\node_modules\\") != std::string::npos) {
335+
return true;
336+
}
337+
const std::string base = ModuleRegistryBasename(key);
338+
return base == "vendor.mjs" || base == "vendor.js" || base == "runtime.mjs" ||
339+
base == "runtime.js";
340+
}
341+
342+
static void ClearRegistryMapForApplicationReload(
343+
std::unordered_map<std::string, v8::Global<v8::Module>>& registry) {
344+
for (auto it = registry.begin(); it != registry.end();) {
345+
if (ShouldPreserveModuleOnApplicationReload(it->first)) {
346+
++it;
347+
continue;
348+
}
349+
it->second.Reset();
350+
it = registry.erase(it);
351+
}
352+
}
353+
354+
void ClearModuleRegistryForApplicationReload() {
355+
ClearRegistryMapForApplicationReload(g_moduleRegistry);
356+
ClearRegistryMapForApplicationReload(g_moduleFallbackRegistry);
357+
ClearRegistryMapForApplicationReload(g_moduleFallbackByRelative);
358+
}
359+
319360
void RemoveModuleFromRegistry(const std::string& canonicalPath) {
320361
// Defensive: never operate on an anomalous/sentinel key.
321362
// This covers the bare "@" anomaly and the special invalid-at stub module used by the dev HTTP

‎NativeScript/runtime/Runtime.mm‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ void DisposeIsolateWhenPossible(Isolate* isolate) {
459459
if (this->moduleInternal_) {
460460
this->moduleInternal_->ClearLoadedModules();
461461
}
462-
ClearModuleRegistry();
462+
ClearModuleRegistryForApplicationReload();
463463
tns::InvokeApplicationReload(isolate);
464464
}
465465

‎TestRunner/app/tests/ReloadApplicationTests.js‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,24 @@ describe("reloadApplication", function () {
5555
expect(second.n).toBe(first.n + 1);
5656
});
5757

58+
it("preserves webpack vendor.mjs across reload so the Angular realm stays", function (done) {
59+
global.__onApplicationReload = function () {};
60+
61+
import("~/vendor.mjs")
62+
.then(function (first) {
63+
expect(NativeScriptRuntime.reloadApplication()).toBe(true);
64+
return import("~/vendor.mjs").then(function (second) {
65+
expect(second).toBe(first);
66+
expect(second.n).toBe(first.n);
67+
done();
68+
});
69+
})
70+
.catch(function (error) {
71+
fail("vendor.mjs should resolve before and after reload: " + error);
72+
done();
73+
});
74+
});
75+
5876
it("keeps JS UIApplicationDelegate IMPs callable from native after reload", function () {
5977
var AppDelegate = UIResponder.extend({
6078
get window() {

‎TestRunner/app/vendor.mjs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Basename vendor.mjs is preserved across reloadApplication so webpack
2+
// isolate-reload can keep @angular/core in the original realm.
3+
export const n = (globalThis.__tnsReloadVendorEvals = (globalThis.__tnsReloadVendorEvals || 0) + 1);

0 commit comments

Comments
 (0)