GeezerVison is showing github.com — fix: normalize "." and ".." in resolved module paths to dedupe modules by adrian-niculescu · Pull Request #1977 · NativeScript/android
Conversation
adrian-niculescu
commented
Jun 24, 2026
•
edited by coderabbitai
Bot
Loading
Uh oh!
There was an error while loading. Please reload this page.
ResolveModuleCallback builds the on-disk path for a relative specifier without collapsing . and .. segments, so the same file imported as ./x from /a/b and as ../x from /a/b/c lands under two different registry keys. stat() resolves the segments so both locate the file, but the differing keys make V8 compile and evaluate the module twice: two instances with separate module state, broken cyclic resolution, and an import.meta.dirname of /a/b/c/.. for the .. spelling.
This normalizes the resolved path before it becomes the registry key, the same way the HTTP branch already canonicalizes through CanonicalizeHttpUrlKey. The pass is lexical (matching how specifiers are joined, not a realpath) and applies to every on-disk candidate kind; it is a no-op for already-canonical paths.
Test: a test-app spec imports one module as ./counter.mjs and ../counter.mjs and asserts they share one instance (===, and a counter incremented to 2). It fails before the change and passes after.
Summary by CodeRabbit
- Bug Fixes
- Improved module loading so the same module is treated consistently even when reached through different relative paths.
- Shared state now updates reliably across multiple import locations, preventing duplicate copies of the same counter.
- Added coverage to verify that imports from different paths observe the same live state.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
ResolveModuleCallback builds the on-disk path for a relative specifier without collapsing "." and ".." segments, so the same file imported as "./x" from /a/b and as "../x" from /a/b/c lands under two different registry keys. stat() resolves the segments so both locate the file, but the differing keys make V8 compile and evaluate the module twice: two instances with separate state, and import.meta.dirname becomes "/a/b/c/.." for the ".." spelling. Normalize the resolved path before it becomes the registry key, the same way the HTTP branch canonicalizes through CanonicalizeHttpUrlKey. The pass is lexical and runs for every on-disk candidate kind, and is a no-op for already-canonical paths. Adds a test-app spec that imports one module as "./counter.mjs" and "../counter.mjs" and asserts a single shared instance.
coderabbitai
Bot
commented
Jun 24, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|

No actionable comments were generated in the recent review. 🎉
ℹ️ Recent review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 322d5fbf-a6ab-471f-be3f-393ae43c2a92
📥 Commits
Reviewing files that changed from the base of the PR and between 92c2654 and e4e84cd.
📒 Files selected for processing (5)
test-app/app/src/main/assets/app/esm-dedup/counter.mjs
test-app/app/src/main/assets/app/esm-dedup/nested/viaParentDir.mjs
test-app/app/src/main/assets/app/esm-dedup/viaSameDir.mjs
test-app/app/src/main/assets/app/tests/testESModules.mjs
test-app/runtime/src/main/cpp/ModuleInternalCallbacks.cpp
📝 Walkthrough
Walkthrough
Adds NormalizeDotSegments canonicalization to ResolveModuleCallback in ModuleInternalCallbacks.cpp so that paths with ./ or ../ segments resolve to the same canonical key. Three new fixture modules (counter.mjs, viaSameDir.mjs, nested/viaParentDir.mjs) and a Jasmine spec verify that two imports using different relative paths share one module instance.
Changes
ESM Deduplication Fix and Tests
| Layer / File(s) |
Summary |
Path canonicalization in ResolveModuleCallback test-app/runtime/src/main/cpp/ModuleInternalCallbacks.cpp |
After a filesystem candidate is confirmed to exist, absPath is rewritten through NormalizeDotSegments so that dot-segment path variants (./x, ../x) map to one canonical path before cache lookup and module registration. |
ESM dedup fixture modules and Jasmine spec test-app/app/src/main/assets/app/esm-dedup/counter.mjs, test-app/app/src/main/assets/app/esm-dedup/viaSameDir.mjs, test-app/app/src/main/assets/app/esm-dedup/nested/viaParentDir.mjs, test-app/app/src/main/assets/app/tests/testESModules.mjs |
counter.mjs exports a shared state object and increment(); viaSameDir.mjs and nested/viaParentDir.mjs each import the counter via different relative paths and call increment() on load; the Jasmine test imports both and asserts object identity of seenState and count === 2. |
Estimated code review effort
🎯 2 (Simple) | ⏱️ ~10 minutes
Possibly related PRs
- NativeScript/android#1976: Directly modifies the same
ModuleInternalCallbacks.cpp module-resolution path, making it a prerequisite or sibling change to the dot-segment normalization applied here.
Suggested reviewers
Poem
🐇 Hop along the module trail,
Two paths once led to tales,
But now one path, one truth to tell —
NormalizeDotSegments rings the bell!
The counter counts to two with glee,
Deduplicated, finally free! 🎉
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
| Check name |
Status |
Explanation |
Resolution |
| Docstring Coverage |
⚠️ Warning |
Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. |
Write docstrings for the functions missing them to satisfy the coverage threshold. |
✅ Passed checks (4 passed)
| Check name |
Status |
Explanation |
| Description Check |
✅ Passed |
Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check |
✅ Passed |
The title clearly matches the main change: normalizing dot segments in resolved module paths to dedupe modules. |
| Linked Issues check |
✅ Passed |
Check skipped because no linked issues were found for this pull request. |
| Out of Scope Changes check |
✅ Passed |
Check skipped because no linked issues were found for this pull request. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
❤️ Share
Comment @coderabbitai help to get the list of available commands.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
@NathanWalker, (and @coderabbitai), I checked the iOS runtime and it doesn't need an equivalent of this change. Vanilla NativeScript/ios already strips . and .. out of a resolved module path before that path becomes the registry key, so the ./x vs ../x double-instantiation this PR fixes on Android can't happen there.
It's all in ModuleInternalCallbacks.mm:
So canonicalization on iOS happens earlier and in more places than the single pass here, and both sides end up keying the registry by the same canonical path. No parity PR needed on iOS.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Tip For best results, initiate chat on the files or code changes.
@adrian-niculescu Thanks for the thorough cross-platform investigation. The breakdown is very clear:
- iOS (
ModuleInternalCallbacks.mm): canonicalization via stringByStandardizingPath happens at multiple points (path normalization helper, candidate base construction, joined relative path, and registry key assignment), and it's filesystem-aware (resolves symlinks, not just lexical).
- Android (this PR): a single lexical
NormalizeDotSegments pass applied to the confirmed on-disk candidate path, just before it becomes the registry key.
Both approaches converge on the same canonical path as the registry key, so ./x and ../x correctly deduplicate on both platforms. No iOS follow-up needed — noted.
✏️ Learnings added
Learnt from: adrian-niculescu
URL: https://github.com/NativeScript/android/pull/1977
Timestamp: 2026-06-24T11:37:48.132Z
Learning: In NativeScript, the iOS runtime (`NativeScript/ios`) already prevents `./x` vs `../x` module double-instantiation without any additional fix. `NormalizePath()` in `NativeScript/runtime/ModuleInternalCallbacks.mm` (around L50-L66) uses `stringByStandardizingPath` which canonicalizes `.` and `..` segments by consulting the real filesystem (folds symlinks too, not just a lexical rewrite). This normalization is applied at multiple call sites: candidate base during resolution (~L1161), joined relative path when building candidates (~L967-976), and the final `absPath` just before it is used as the `g_moduleRegistry` key (~L1317). The Android PR `#1977` achieves the same dedup via a single lexical `NormalizeDotSegments` pass on the confirmed on-disk path before it becomes the registry key. No parity PR is needed on iOS.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
NathanWalker
pushed a commit
that referenced
this pull request
Jul 4, 2026
#1977)
ResolveModuleCallback builds the on-disk path for a relative specifier without collapsing "." and ".." segments, so the same file imported as "./x" from /a/b and as "../x" from /a/b/c lands under two different registry keys. stat() resolves the segments so both locate the file, but the differing keys make V8 compile and evaluate the module twice: two instances with separate state, and import.meta.dirname becomes "/a/b/c/.." for the ".." spelling. Normalize the resolved path before it becomes the registry key, the same way the HTTP branch canonicalizes through CanonicalizeHttpUrlKey. The pass is lexical and runs for every on-disk candidate kind, and is a no-op for already-canonical paths.
Uh oh!
There was an error while loading. Please reload this page.
Assignees
No one assigned
Milestone
No milestone
Development
Successfully merging this pull request may close these issues.
Uh oh!
There was an error while loading. Please reload this page.
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.
|