Visitar URL original
feat: ESM loader overhaul — Web/Node parity, ns:module dev surface, HTTP module loader by NathanWalker · Pull Request #1965 · NativeScript/android · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0e3513d
feat(runtime): ESM resolver hardening and async module-graph loader
NathanWalker Aug 13, 2026
5b080c4
feat(runtime): HMR dev-sessions and a hardened HTTP session loader
NathanWalker Aug 13, 2026
d7b20a5
feat(runtime): expose the dev-loader surface as the ns:module builtin
NathanWalker Aug 13, 2026
b19dbe8
fix(worker): surface entry-script load errors and buffer early messages
NathanWalker Aug 13, 2026
d5d180c
test: cover the ESM loader, remote-module security, and worker behavior
NathanWalker Aug 13, 2026
8faf670
refactor(runtime): drop the require() optional-module placeholder
NathanWalker Aug 13, 2026
b6d3bac
refactor(runtime): rename HMRSupport to HttpLoader and fold DevFlags …
NathanWalker Aug 13, 2026
4e58028
fix(runtime): harden HTTP fetch, extend names, and worker drain retries
NathanWalker Aug 13, 2026
5fa23eb
ci: build
NathanWalker Aug 15, 2026
593dc7f
refactor(runtime): move per-isolate loader state onto RuntimeState slots
edusperoni Aug 19, 2026
781816a
test: add an in-app HTTP module fixture server
edusperoni Aug 19, 2026
f125072
perf(runtime): resolve module referrers through an identity-hash index
edusperoni Aug 19, 2026
76321ed
feat(runtime): category-scoped debug logging behind NS_DEBUG
edusperoni Aug 19, 2026
1bf9852
refactor(runtime): make the ESM resolver compile-and-register only
edusperoni Aug 19, 2026
2d7f3fe
feat(runtime): scheme-agnostic module graph discovery
edusperoni Aug 19, 2026
d56e86f
feat(runtime): one response classifier and a web-strict MIME gate for…
edusperoni Aug 19, 2026
13a6dc9
fix(runtime): clear dynamic-import routing state before settling waiters
edusperoni Aug 19, 2026
9fb3559
feat(runtime): extract module evaluation policies; require(esm) refus…
edusperoni Aug 19, 2026
b0d537a
feat(runtime): Node-parity exports interop for require(esm)
edusperoni Aug 19, 2026
062e9be
feat(runtime): createRequire and createPumpingRequire on ns:module, n…
edusperoni Aug 19, 2026
3cccda2
feat(runtime): import-map scopes, validation, and atomic installation
edusperoni Aug 19, 2026
9771032
refactor(runtime): workers copy the loader vocabulary at spawn
edusperoni Aug 19, 2026
73ebaab
refactor(runtime): drop hardcoded client vocabulary from the loader
edusperoni Aug 19, 2026
332c28c
refactor(runtime): derive boot state natively; setDevBootComplete rem…
edusperoni Aug 19, 2026
fd8ceec
fix(worker): enable the message queue on entry settle, like the web
edusperoni Aug 19, 2026
bfd1682
feat(runtime): ES module app entries and a boot backstop that holds f…
edusperoni Aug 19, 2026
52fdeef
fix(runtime): fail cleanly on an unreadable ES module entry
edusperoni Aug 19, 2026
55ca718
test: port the loader suite - HTTP ESM, createRequire, interop, workers
edusperoni Aug 19, 2026
e3ab969
docs: sync ns-builtin-modules.md with the loader overhaul
edusperoni Aug 19, 2026
6711447
refactor(runtime): reserve the g_ prefix for actual process globals
edusperoni Aug 19, 2026
006de1a
fix(runtime): loader input validation, worker entry resolution, revie…
edusperoni Aug 19, 2026
95c8bae
docs: sync ns-builtin-modules with the iOS module-reference rewrite
edusperoni Aug 19, 2026
da6cf99
fix(runtime): never poll the looper from inside one of its fd callbacks
edusperoni Aug 19, 2026
c167db4
perf(runtime): wait on the event loop's fds instead of sleeping in pumps
edusperoni Aug 19, 2026
47333f4
feat(runtime): one pump primitive that drains due JS timers
edusperoni Aug 20, 2026
d4e98c6
fix(runtime): route pumps through PumpUntil and close the evaluation …
edusperoni Aug 20, 2026
253eef6
fix(runtime): gate the pump's looper-equivalent drain on pumpRunLoop
edusperoni Aug 20, 2026
2ed5343
fix(runtime): JNI hygiene in the sync fetch's failure paths
edusperoni Aug 20, 2026
02044d6
fix(runtime): generate named Java proxies at runtime when not precomp…
NathanWalker Aug 20, 2026
362c826
Merge branch 'main' into feat/hmr-dev-sessions
NathanWalker Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor(runtime): drop hardcoded client vocabulary from the loader
The runtime provides mechanism; the client supplies vocabulary. Removed:
NormalizeViteSpecifier and both of its second-chance import-map lookups
(clients register every rewritten specifier form verbatim), the
underscore-chunk bare-specifier heuristic, the import('@') empty-stub
sentinel complex on all five sites (a bare '@' now fails loudly as an
unresolvable specifier on every route, and can be mapped through the
import map like any other specifier), the default canonicalization
vocabulary (/ns/, /node_modules/.vite/, /@id/, /@fs/, the /@ng/component
preserve rule, and the import/t/v strip params), and the client-URL-shape
diagnostic labels.

Unconfigured canonicalization is purely mechanical - the fragment is
stripped and nothing else; the query is part of the module's identity
until the client teaches the runtime otherwise via
configureLoader({ canonicalization }). The two supported
configure-before-ESM-traffic bootstrap shapes are documented in the
cross-runtime contract.
  • Loading branch information
edusperoni committed Aug 19, 2026
commit 73ebaabc6c57ee800692eb996f7e2e4e92cfb054
37 changes: 21 additions & 16 deletions test-app/app/src/main/assets/app/tests/testNsModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,22 +67,22 @@ describe("HTTP canonical key (ns:module canonicalizeHttpUrlKey)", function () {
expect(typeof canon).toBe("function");
});

it("drops dev cache-busters (t/v/import) but keeps real query params", function () {
checkKey("http://h/ns/core?p=x&t=123&v=9&import=1", "http://h/ns/core?p=x");
// Unconfigured, canonicalization is purely mechanical: the fragment goes
// and the query stays. Which params are cache-busters and which paths are
// dev endpoints is client vocabulary the runtime no longer guesses.
it("unconfigured: strips the fragment and nothing else", function () {
checkKey("http://h/app/foo.js#frag", "http://h/app/foo.js");
checkKey("http://h/app/foo.js?t=123&v=9#frag", "http://h/app/foo.js?t=123&v=9");
});

it("leaves public (non-dev, non-volatile) URLs untouched", function () {
it("unconfigured: leaves every query param in the key", function () {
checkKey("http://h/app/foo.js?t=123&v=9&import=1", "http://h/app/foo.js?t=123&v=9&import=1");
checkKey("https://cdn.example.com/lib.js?token=abc", "https://cdn.example.com/lib.js?token=abc");
});

it("treats module identity as literally the URL — no path-tag collapses", function () {
checkKey("http://h/ns/m/foo.js", "http://h/ns/m/foo.js");
checkKey("http://h/ns/rt", "http://h/ns/rt");
checkKey("http://h/ns/core", "http://h/ns/core");
});

it("ignores URL fragments for dev endpoints", function () {
checkKey("http://h/ns/m/foo.js#frag", "http://h/ns/m/foo.js");
checkKey("http://h/app/m/foo.js", "http://h/app/m/foo.js");
checkKey("http://h/app/rt", "http://h/app/rt");
});

it("honors a client-supplied canonicalization vocabulary via configureLoader", function () {
Expand All @@ -91,15 +91,20 @@ describe("HTTP canonical key (ns:module canonicalizeHttpUrlKey)", function () {
pending("ns:module.canonicalizeHttpUrlKey not exposed (release build)");
return;
}
// Neutral vocabulary: the mechanics under test are the runtime's, the
// strings are the client's to choose.
require("ns:module").configureLoader({
canonicalization: {
stripParams: ["t", "v", "import"],
forPathPrefixes: ["/ns/", "/node_modules/.vite/", "/@id/", "/@fs/"],
preserveQueryFor: ["/@ng/component"],
stripParams: ["cachebust", "rev"],
forPathPrefixes: ["/dev/"],
preserveQueryFor: ["/dev/metadata"],
},
});
expect(canon("http://h/ns/core?p=x&t=123&v=9&import=1")).toBe("http://h/ns/core?p=x");
expect(canon("http://h/ns/m/comp/@ng/component?c=a&t=42")).toBe("http://h/ns/m/comp/@ng/component?c=a&t=42");
expect(canon("https://cdn.example.com/lib.js?token=abc")).toBe("https://cdn.example.com/lib.js?token=abc");
// Under a configured dev prefix, the named params drop and the rest sort.
expect(canon("http://h/dev/core?p=x&cachebust=123&rev=9")).toBe("http://h/dev/core?p=x");
// preserveQueryFor wins over the dev prefix: the query IS the identity.
expect(canon("http://h/dev/metadata?c=a&cachebust=42")).toBe("http://h/dev/metadata?c=a&cachebust=42");
// Outside every configured prefix, the query is untouched.
expect(canon("https://cdn.example.com/lib.js?cachebust=abc")).toBe("https://cdn.example.com/lib.js?cachebust=abc");
});
});
62 changes: 31 additions & 31 deletions test-app/runtime/src/main/cpp/HttpLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -200,37 +200,42 @@ std::string CanonicalizeHttpUrlKey(const std::string& url) {
std::string originAndPath = (qPos == std::string::npos) ? noHash : noHash.substr(0, qPos);
std::string query = (qPos == std::string::npos) ? std::string() : noHash.substr(qPos + 1);

// This key is the module registry/cache key. For general-purpose HTTP
// module loading the query can be part of a module's identity (auth,
// content versioning, routing), so query normalization applies only to the
// endpoints the client names, through the vocabulary it supplies.
//
// `preserveQueryFor` is checked BEFORE the dev-endpoint prefix test, so it
// covers endpoints nested under a dev prefix: for some endpoints the query
// IS the identity, and stripping it would collapse every refetch onto the
// boot-time key.
//
// Until a client supplies that vocabulary, canonicalization is purely
// mechanical: the fragment is gone and the query stays. Which params are
// cache-busters and which paths are dev endpoints is knowledge only the
// client has; guessing would silently collapse two distinct modules onto
// one registry key.
const CanonicalizationConfig* canon = CanonicalizationConfigForCurrentIsolate();
if (canon == nullptr) {
return noHash;
}
{
std::string pathOnly = originAndPath.substr(pathStart);
if (canon) {
for (const auto& p : canon->preserveQueryPrefixes) {
if (!p.empty() && pathOnly.find(p) != std::string::npos) {
return noHash;
}
}
bool isDevEndpoint = false;
for (const auto& p : canon->devPathPrefixes) {
if (!p.empty() && StartsWith(pathOnly, p.c_str())) {
isDevEndpoint = true;
break;
}
}
if (!isDevEndpoint) {
for (const auto& p : canon->preserveQueryPrefixes) {
if (!p.empty() && pathOnly.find(p) != std::string::npos) {
return noHash;
}
} else {
if (pathOnly.find("/@ng/component") != std::string::npos) {
return noHash;
}
const bool isDevEndpoint = StartsWith(pathOnly, "/ns/") ||
StartsWith(pathOnly, "/node_modules/.vite/") ||
StartsWith(pathOnly, "/@id/") ||
StartsWith(pathOnly, "/@fs/");
if (!isDevEndpoint) {
return noHash;
}
bool isDevEndpoint = false;
for (const auto& p : canon->devPathPrefixes) {
if (!p.empty() && StartsWith(pathOnly, p.c_str())) {
isDevEndpoint = true;
break;
}
}
if (!isDevEndpoint) {
return noHash;
}
}

if (query.empty()) return originAndPath;
Expand All @@ -244,13 +249,8 @@ std::string CanonicalizeHttpUrlKey(const std::string& url) {
if (!pair.empty()) {
size_t eq = pair.find('=');
std::string name = (eq == std::string::npos) ? pair : pair.substr(0, eq);
bool drop;
if (canon) {
drop = std::find(canon->stripParams.begin(), canon->stripParams.end(),
name) != canon->stripParams.end();
} else {
drop = (name == "import" || name == "t" || name == "v");
}
const bool drop = std::find(canon->stripParams.begin(), canon->stripParams.end(),
name) != canon->stripParams.end();
if (!drop) kept.push_back(pair);
}
if (amp == std::string::npos) break;
Expand Down
4 changes: 2 additions & 2 deletions test-app/runtime/src/main/cpp/MetadataNode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1866,8 +1866,8 @@ bool MetadataNode::GetExtendLocation(v8::Isolate* isolate, string& extendLocatio
} else {
// srcFileName is not always `file://<APP_ROOT>/<path>.js`:
// HTTP ESM loading (HMR dev workflow) passes a full URL like
// `http://127.0.0.1:5173/ns/core/...` with no `.js` suffix and
// no app-root prefix, so naive scheme/app-root/`.js` stripping
// `http://<dev-host>/<path>` with no `.js` suffix and no
// app-root prefix, so naive scheme/app-root/`.js` stripping
// can yield an empty `fullPathToFile` and crash downstream on
// an empty token list.
string normalized = srcFileName;
Expand Down
Loading