-
-
Notifications
You must be signed in to change notification settings - Fork 142
feat: ESM resolver hardening, http loader, dev-mode globals #1965
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
NathanWalker
wants to merge
4
commits into
main
Choose a base branch
from
feat/hmr-dev-sessions
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
c43a108
feat: HMR dev-sessions, ESM resolver hardening, dev-mode runtime globals
NathanWalker 269fa36
feat: HMR robustness and additional tests
NathanWalker 696d4fc
refactor(runtime): reduce surface to a mechanism-only dev-loader cont…
NathanWalker d49581c
test: restore shared runtime tests submodule to main's pointer
NathanWalker File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| // Fixture for testNsDevBoundary: reports whether the runtime attached a | ||
| // native `import.meta.hot` (it must NOT — hot contexts are injected by the | ||
| // JS dev client via source rewrite, never by the runtime). | ||
| export const hasHot = typeof import.meta.hot !== "undefined"; | ||
| export const hotValue = import.meta.hot; |
77 changes: 77 additions & 0 deletions
77
test-app/app/src/main/assets/app/tests/testHttpCanonicalKey.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| // HTTP canonical-key identity tests. | ||
| // | ||
| // Pins the behavior of the native CanonicalizeHttpUrlKey (the loader/registry | ||
| // key) via the debug-only __NS_DEV__.canonicalizeHttpUrlKey diagnostic. Pure | ||
| // string logic — no dev server required. | ||
| // | ||
| // Module identity IS the canonical URL: the dev server serves every module | ||
| // under one URL and freshness is handled by __NS_DEV__.invalidateModules | ||
| // (registry + prewarm-cache evict + fetch nonce), never by URL variation. | ||
| // There is deliberately no path-tag vocabulary (__ns_boot__/__ns_hmr__) | ||
| // to collapse, and no versioned-bridge-endpoint normalization. | ||
|
|
||
| describe("HTTP canonical key", function () { | ||
| function canonFn() { | ||
| const dev = globalThis.__NS_DEV__; | ||
| return dev && typeof dev.canonicalizeHttpUrlKey === "function" | ||
| ? dev.canonicalizeHttpUrlKey | ||
| : null; | ||
| } | ||
| function canon(url) { | ||
| return canonFn()(url); | ||
| } | ||
|
|
||
| it("is available in dev builds", function () { | ||
| if (!canonFn()) { | ||
| pending("__NS_DEV__.canonicalizeHttpUrlKey not available (release build?)"); | ||
| return; | ||
| } | ||
| expect(typeof canonFn()).toBe("function"); | ||
| }); | ||
|
|
||
| it("drops the fragment and unwraps file://http wrappers", function () { | ||
| if (!canonFn()) { pending("release"); return; } | ||
| expect(canon("http://h/ns/m/foo.js#frag")).toBe("http://h/ns/m/foo.js"); | ||
| expect(canon("file://http://h/x.js")).toBe("http://h/x.js"); | ||
| }); | ||
|
|
||
| it("does NOT collapse versioned endpoint paths or path tags", function () { | ||
| if (!canonFn()) { pending("release"); return; } | ||
| // Path is identity — no /ns/rt/<v> → /ns/rt collapse, no | ||
| // __ns_hmr__/__ns_boot__ tag folding. | ||
| expect(canon("http://h/ns/rt/42")).toBe("http://h/ns/rt/42"); | ||
| expect(canon("http://h/ns/rt/42/x.js")).toBe("http://h/ns/rt/42/x.js"); | ||
| expect(canon("http://h/ns/m/__ns_hmr__/v7/foo.js")) | ||
| .toBe("http://h/ns/m/__ns_hmr__/v7/foo.js"); | ||
| }); | ||
|
|
||
| it("strips import/t/v markers and sorts remaining params on dev endpoints", function () { | ||
| if (!canonFn()) { pending("release"); return; } | ||
| expect(canon("http://h/ns/m/a?import=1&b=2&a=3")).toBe("http://h/ns/m/a?a=3&b=2"); | ||
| expect(canon("http://h/ns/m/a?b=2&a=1")).toBe("http://h/ns/m/a?a=1&b=2"); | ||
| expect(canon("http://h/ns/core?import=1")).toBe("http://h/ns/core"); | ||
| expect(canon("http://h/ns/m/a?t=123&v=abc&x=1")).toBe("http://h/ns/m/a?x=1"); | ||
| }); | ||
|
|
||
| it("preserves the query verbatim on non-dev endpoints", function () { | ||
| if (!canonFn()) { pending("release"); return; } | ||
| // Public-internet module URLs: the query can be part of identity | ||
| // (auth, content versioning, routing) — only the fragment is dropped. | ||
| expect(canon("http://h/a?import=1&b=2&a=3")).toBe("http://h/a?import=1&b=2&a=3"); | ||
| expect(canon("https://cdn.example.com/pkg.js?token=x#frag")) | ||
| .toBe("https://cdn.example.com/pkg.js?token=x"); | ||
| }); | ||
|
|
||
| it("preserves the t param on @ng/component endpoints", function () { | ||
| if (!canonFn()) { pending("release"); return; } | ||
| // Angular HMR component-update endpoint: `t` identifies a specific | ||
| // recompile and must remain a distinct registry entry. | ||
| expect(canon("http://h/ns/m/app/@ng/component?c=x&t=111")) | ||
| .toBe("http://h/ns/m/app/@ng/component?c=x&t=111"); | ||
| }); | ||
|
|
||
| it("leaves a non-http(s) specifier unchanged", function () { | ||
| if (!canonFn()) { pending("release"); return; } | ||
| expect(canon("~/local/foo.js")).toBe("~/local/foo.js"); | ||
| }); | ||
| }); |
128 changes: 128 additions & 0 deletions
128
test-app/app/src/main/assets/app/tests/testNodeBuiltinsAndOptionalModules.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| // Tests the ESM resolver's synthetic-module paths: | ||
| // - node: built-in polyfills (in-memory ES modules) | ||
| // - bare-specifier optional-module placeholders | ||
| // - ns-vendor:// vendor-registry resolution via configureRuntime importMap | ||
| // - blob: URL module re-use across imports | ||
| // | ||
| // The Android resolver's Node-builtin polyfill set is broader than iOS's; | ||
| // only the explicitly-tested specifiers are asserted here. | ||
|
|
||
| describe("Node built-in and optional module resolution", function () { | ||
| it("provides an in-memory polyfill for node:url", async function () { | ||
| const mod = await import("node:url"); | ||
| const modAgain = await import("node:url"); | ||
|
|
||
| expect(mod).toBeDefined(); | ||
| expect(modAgain).toBe(mod); | ||
| expect(typeof mod.fileURLToPath).toBe("function"); | ||
| expect(typeof mod.pathToFileURL).toBe("function"); | ||
|
|
||
| const p = mod.fileURLToPath("file:///foo/bar.txt"); | ||
| expect(p === "/foo/bar.txt" || p === "foo/bar.txt").toBe(true); | ||
|
|
||
| const u = mod.pathToFileURL("/foo/bar.txt"); | ||
| expect(u instanceof URL).toBe(true); | ||
| expect(u.protocol).toBe("file:"); | ||
| }); | ||
|
|
||
| it("creates an in-memory placeholder for likely-optional modules", async function () { | ||
| // Use a name that IsLikelyOptionalModule will treat as optional | ||
| // (no slashes, no extension, no scope prefix). | ||
| const mod = await import("__ns_optional_test_module__"); | ||
| const modAgain = await import("__ns_optional_test_module__"); | ||
|
|
||
| expect(mod).toBeDefined(); | ||
| expect(modAgain).toBe(mod); | ||
| expect(typeof mod.default).toBe("object"); | ||
|
|
||
| let threw = false; | ||
| try { | ||
| // eslint-disable-next-line no-unused-expressions | ||
| mod.default.someProperty; | ||
| } catch (e) { | ||
| threw = true; | ||
| } | ||
| expect(threw).toBe(true); | ||
| }); | ||
|
|
||
| it("resolves import-map vendor modules through the explicit vendor registry", async function () { | ||
| const dev = globalThis.__NS_DEV__; | ||
| const configureRuntime = dev && dev.configureRuntime; | ||
| if (typeof configureRuntime !== "function") { | ||
| pending("__NS_DEV__.configureRuntime not available"); | ||
| return; | ||
| } | ||
|
|
||
| const previousRegistry = globalThis.__nsVendorRegistry; | ||
| const vendorRegistry = new Map(); | ||
| globalThis.__nsVendorRegistry = vendorRegistry; | ||
| vendorRegistry.set("__ns_test_vendor__", { | ||
| default: { source: "vendor-default" }, | ||
| namedValue: 7, | ||
| makeValue() { | ||
| return "vendor-named"; | ||
| }, | ||
| }); | ||
|
|
||
| try { | ||
| configureRuntime({ | ||
| importMap: { | ||
| imports: { | ||
| __ns_test_vendor__: "ns-vendor://__ns_test_vendor__", | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| const mod = await import("__ns_test_vendor__"); | ||
| const modAgain = await import("__ns_test_vendor__"); | ||
|
|
||
| expect(mod).toBeDefined(); | ||
| expect(modAgain).toBe(mod); | ||
| expect(mod.default).toEqual({ source: "vendor-default" }); | ||
| expect(mod.namedValue).toBe(7); | ||
| expect(mod.makeValue()).toBe("vendor-named"); | ||
| } finally { | ||
| configureRuntime({ importMap: { imports: {} } }); | ||
| if (typeof previousRegistry === "undefined") { | ||
| delete globalThis.__nsVendorRegistry; | ||
| } else { | ||
| globalThis.__nsVendorRegistry = previousRegistry; | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| it("reuses blob URL modules across concurrent and repeated imports", async function () { | ||
| delete globalThis.__nsBlobEvalCount; | ||
|
|
||
| const blobSource = [ | ||
| "globalThis.__nsBlobEvalCount = (globalThis.__nsBlobEvalCount || 0) + 1;", | ||
| "export const evalCount = globalThis.__nsBlobEvalCount;", | ||
| "export const kind = 'blob-module';", | ||
| "export default { evalCount, kind };", | ||
| ].join("\n"); | ||
|
|
||
| const url = URL.createObjectURL(new Blob([blobSource], { type: "text/javascript" }), { | ||
| ext: ".mjs", | ||
| }); | ||
|
|
||
| expect(typeof url).toBe("string"); | ||
| expect(url.indexOf("blob:nativescript/")).toBe(0); | ||
|
|
||
| try { | ||
| const [first, second] = await Promise.all([import(url), import(url)]); | ||
| const third = await import(url); | ||
|
|
||
| expect(first).toBeDefined(); | ||
| expect(second).toBe(first); | ||
| expect(third).toBe(first); | ||
| expect(first.evalCount).toBe(1); | ||
| expect(second.evalCount).toBe(1); | ||
| expect(third.evalCount).toBe(1); | ||
| expect(first.kind).toBe("blob-module"); | ||
| expect(globalThis.__nsBlobEvalCount).toBe(1); | ||
| } finally { | ||
| URL.revokeObjectURL(url); | ||
| delete globalThis.__nsBlobEvalCount; | ||
| } | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Dex cache file path in doc omits thumb suffix.
Doc says the dex is written to
<dexDir>/<name>.dex, butDexFactory.getDexFileactually appends the thumb:dexDir + "/" + classToProxyFile + "-" + dexThumb + ".dex". Since this section doubles as a troubleshooting reference (see "What to look at when this breaks"), the actual on-disk filename pattern is worth stating precisely.✏️ Suggested wording fix
📝 Committable suggestion
🤖 Prompt for AI Agents