-
-
Notifications
You must be signed in to change notification settings - Fork 37
feat: http loaded es module realms + HMR DX enrichments #312
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
base: main
Are you sure you want to change the base?
Conversation
…k to allow continued development during debug
With Vite, we just include inspector_modules in the bundle when running in debug mode so no need to load separate file. Other bundlers can do the same now.
…ght based on user choice
…l-esm-exception-improvements
…l-esm-consumption
…l-esm-consumption
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…l-esm-consumption
…mption' into feat/hmr-improvements
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.
Pull Request Overview
This PR adds HTTP ESM (ECMAScript Module) loader functionality with Hot Module Replacement (HMR) support to the NativeScript iOS runtime. This enables the runtime to fetch and compile ES modules from a development server over HTTP during development, supporting features like live reload and hot module replacement.
Key Changes
- HTTP-based ES module loading for development builds
- HMR infrastructure with
import.meta.hotsupport - Module registry enhancements for cache management and fallback handling
- Comprehensive test suite for HTTP ESM loader functionality
- App Transport Security configuration updates for HTTP requests
Reviewed Changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| package.json | Version bump to 9.0.0-alpha.9 |
| TestRunner/app/tests/index.js | Added HttpEsmLoaderTests to test suite |
| TestRunner/app/tests/HttpEsmLoaderTests.js | New test suite for HTTP ESM loader features |
| TestRunner/app/tests/esm/*.mjs | Test fixtures for ESM loading scenarios |
| TestRunner/Info.plist | Added NSAppTransportSecurity to allow HTTP requests |
| NativeScript/runtime/Runtime.mm | Integrated HMR support headers and import.meta.hot initialization |
| NativeScript/runtime/ModuleInternalCallbacks.mm | Core HTTP ESM loader implementation with module resolution |
| NativeScript/runtime/ModuleInternal.mm | Path normalization and ESM loading enhancements |
| NativeScript/runtime/HMRSupport.{h,mm} | New HMR support module with HTTP fetching utilities |
| NativeScript/runtime/DevFlags.{h,mm} | New development flags module for logging control |
| NativeScript/NativeScript-Prefix.pch | Version update to match package.json |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| #include "HMRSupport.mm" | ||
| #include "DevFlags.h" | ||
| #include "DevFlags.mm" |
Copilot
AI
Nov 6, 2025
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.
Including .mm implementation files directly in Runtime.mm violates the principle of separate compilation and can lead to multiple definition errors. Implementation files should be compiled separately and linked, not included. Remove lines 33 and 35, and ensure HMRSupport.mm and DevFlags.mm are added to the project's build targets instead.
| #include "HMRSupport.mm" | |
| #include "DevFlags.h" | |
| #include "DevFlags.mm" | |
| #include "DevFlags.h" |
| // Helper function to create a test dev server module (if dev server is running) | ||
| function createTestDevModule() { | ||
| return ` | ||
| // Test ES module for HTTP ESM loader | ||
| export const testValue = "http-esm-loaded"; | ||
| export default function testFunction() { | ||
| return "HTTP ESM loader working"; | ||
| } | ||
| export { testFunction as namedExport }; | ||
| `; | ||
| } |
Copilot
AI
Nov 6, 2025
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.
The helper function createTestDevModule at lines 176-185 is defined but never called or used in any test. This is dead code that should be removed to reduce clutter and prevent confusion.
| // Helper function to create a test dev server module (if dev server is running) | |
| function createTestDevModule() { | |
| return ` | |
| // Test ES module for HTTP ESM loader | |
| export const testValue = "http-esm-loaded"; | |
| export default function testFunction() { | |
| return "HTTP ESM loader working"; | |
| } | |
| export { testFunction as namedExport }; | |
| `; | |
| } |
| // Normalize malformed HTTP(S) schemes that sometimes appear as 'http:/host' (single slash) | ||
| // due to upstream path joins or standardization. This ensures our HTTP loader fast-path | ||
| // is used and avoids filesystem fallback attempts like '/app/http:/host'. | ||
| if (spec.rfind("http:/", 0) == 0 && spec.rfind("http://", 0) != 0) { | ||
| spec.insert(5, "/"); // http:/ -> http:// | ||
| } else if (spec.rfind("https:/", 0) == 0 && spec.rfind("https://", 0) != 0) { | ||
| spec.insert(6, "/"); // https:/ -> https:// | ||
| } | ||
|
|
||
| if (IsScriptLoadingLogEnabled()) { | ||
| Log(@"[resolver][spec] %s", spec.c_str()); | ||
| } | ||
|
|
||
| // Normalize '@/' alias to '/src/' for static imports (mirrors client dynamic import normalization) | ||
| if (spec.rfind("@/", 0) == 0) { | ||
| std::string orig = spec; | ||
| spec = std::string("/src/") + spec.substr(2); | ||
| if (IsScriptLoadingLogEnabled()) { | ||
| Log(@"[resolver][normalize] %@ -> %@", [NSString stringWithUTF8String:orig.c_str()], [NSString stringWithUTF8String:spec.c_str()]); |
Copilot
AI
Nov 6, 2025
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.
This normalization logic mutates the input spec parameter. Consider storing the result in a separate variable to make the function side-effect-free and easier to reason about. For example: std::string normalizedSpec = spec; and then mutate normalizedSpec.
| // Normalize malformed HTTP(S) schemes that sometimes appear as 'http:/host' (single slash) | |
| // due to upstream path joins or standardization. This ensures our HTTP loader fast-path | |
| // is used and avoids filesystem fallback attempts like '/app/http:/host'. | |
| if (spec.rfind("http:/", 0) == 0 && spec.rfind("http://", 0) != 0) { | |
| spec.insert(5, "/"); // http:/ -> http:// | |
| } else if (spec.rfind("https:/", 0) == 0 && spec.rfind("https://", 0) != 0) { | |
| spec.insert(6, "/"); // https:/ -> https:// | |
| } | |
| if (IsScriptLoadingLogEnabled()) { | |
| Log(@"[resolver][spec] %s", spec.c_str()); | |
| } | |
| // Normalize '@/' alias to '/src/' for static imports (mirrors client dynamic import normalization) | |
| if (spec.rfind("@/", 0) == 0) { | |
| std::string orig = spec; | |
| spec = std::string("/src/") + spec.substr(2); | |
| if (IsScriptLoadingLogEnabled()) { | |
| Log(@"[resolver][normalize] %@ -> %@", [NSString stringWithUTF8String:orig.c_str()], [NSString stringWithUTF8String:spec.c_str()]); | |
| // Use a separate variable for normalization to avoid mutating the input spec | |
| std::string normalizedSpec = spec; | |
| // Normalize malformed HTTP(S) schemes that sometimes appear as 'http:/host' (single slash) | |
| // due to upstream path joins or standardization. This ensures our HTTP loader fast-path | |
| // is used and avoids filesystem fallback attempts like '/app/http:/host'. | |
| if (normalizedSpec.rfind("http:/", 0) == 0 && normalizedSpec.rfind("http://", 0) != 0) { | |
| normalizedSpec.insert(5, "/"); // http:/ -> http:// | |
| } else if (normalizedSpec.rfind("https:/", 0) == 0 && normalizedSpec.rfind("https://", 0) != 0) { | |
| normalizedSpec.insert(6, "/"); // https:/ -> https:// | |
| } | |
| if (IsScriptLoadingLogEnabled()) { | |
| Log(@"[resolver][spec] %s", normalizedSpec.c_str()); | |
| } | |
| // Normalize '@/' alias to '/src/' for static imports (mirrors client dynamic import normalization) | |
| if (normalizedSpec.rfind("@/", 0) == 0) { | |
| std::string orig = normalizedSpec; | |
| normalizedSpec = std::string("/src/") + normalizedSpec.substr(2); | |
| if (IsScriptLoadingLogEnabled()) { | |
| Log(@"[resolver][normalize] %@ -> %@", [NSString stringWithUTF8String:orig.c_str()], [NSString stringWithUTF8String:normalizedSpec.c_str()]); |
| g_moduleReentryParents; | ||
| static thread_local std::unordered_map<std::string, std::string> g_modulePrimaryImporters; | ||
| static thread_local std::unordered_set<std::string> g_modulesInFlight; | ||
| static thread_local std::unordered_set<std::string> g_modulesPendingReset; |
Copilot
AI
Nov 6, 2025
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.
The magic number 256 for kMaxModuleReentryCount at line 424 lacks justification. Add a comment explaining why 256 was chosen as the threshold for detecting circular dependencies and what happens when this limit is exceeded.
| static thread_local std::unordered_set<std::string> g_modulesPendingReset; | |
| static thread_local std::unordered_set<std::string> g_modulesPendingReset; | |
| // The threshold for detecting circular dependencies during module resolution. | |
| // 256 was chosen as a high enough value to allow deep but legitimate module graphs, | |
| // but low enough to catch runaway recursion or infinite circular imports. | |
| // If a module is re-entered more than this limit, module loading is aborted and | |
| // an error is reported to prevent stack overflow or infinite loops. |
| gatingDisabled = [gatingFlag boolValue]; | ||
| } | ||
| } | ||
| static std::atomic<size_t> g_hmrModuleGatedCount {0}; |
Copilot
AI
Nov 6, 2025
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.
The variable g_hmrModuleGatedCount at line 1462 is declared but never read or used for any diagnostic or operational purpose. If this is intended for future metrics collection, add a TODO comment. Otherwise, remove it to reduce code complexity.
| static std::atomic<size_t> g_hmrModuleGatedCount {0}; |
| cfg.timeoutIntervalForRequest = 5.0; | ||
| cfg.timeoutIntervalForResource = 5.0; |
Copilot
AI
Nov 6, 2025
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.
The HTTP timeout values of 5 seconds (lines 276-277) are hardcoded. Consider making these configurable via RuntimeConfig or app settings to allow developers to adjust timeouts based on their network conditions and dev server performance.
| }); | ||
|
|
||
| it("should handle network timeouts", function(done) { | ||
| // Attempt to import from an unreachable address to test timeout |
Copilot
AI
Nov 6, 2025
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.
Using the TEST-NET-1 address 192.0.2.1 (RFC 5737) at line 116 is a good practice for documentation and testing. However, add a comment explaining that this is an RFC 5737 documentation address intentionally used to trigger timeout scenarios, which would help future maintainers understand the test's purpose.
| // Attempt to import from an unreachable address to test timeout | |
| // Attempt to import from an unreachable address to test timeout | |
| // 192.0.2.1 is a TEST-NET-1 address reserved by RFC 5737 for documentation and testing purposes. | |
| // It is intentionally used here to trigger a network timeout scenario. |
| if (EndsWith(noExt, ".ts") || EndsWith(noExt, ".js")) { | ||
| noExt = noExt.substr(0, noExt.size() - 3); |
Copilot
AI
Nov 6, 2025
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.
The logic at lines 1202-1204 strips exactly 3 characters assuming .ts or .js extensions, but doesn't handle other common extensions like .tsx, .jsx, .mts, or .cts which have different lengths. This could lead to incorrect path resolution for these file types. Consider using a more robust extension-stripping approach that handles variable-length extensions.
| if (EndsWith(noExt, ".ts") || EndsWith(noExt, ".js")) { | |
| noExt = noExt.substr(0, noExt.size() - 3); | |
| // Handle variable-length extensions: .ts, .js, .tsx, .jsx, .mts, .cts | |
| const std::vector<std::string> knownExts = {".ts", ".js", ".tsx", ".jsx", ".mts", ".cts"}; | |
| for (const auto& ext : knownExts) { | |
| if (EndsWith(noExt, ext)) { | |
| noExt = noExt.substr(0, noExt.size() - ext.size()); | |
| break; | |
| } |
This pull request introduces significant improvements to the NativeScript runtime, focusing on better support for ES modules, Hot Module Replacement (HMR), and developer ergonomics. The key changes include the addition of a new HMR support module, centralized development flags, enhanced path normalization for module resolution, and stricter handling of HTTP-based module imports. These updates help improve module loading reliability, enable minimal HMR features, and provide more consistent developer experience.
ESM/HMR Support Enhancements:
HMRSupport.handHMRSupport.mmmodules providing minimal HMR (import.meta.hot) support, including per-module hot data, accept/dispose callback registration, and helpers for dev HTTP ESM loaders. [1] [2]DevFlags.handDevFlags.mm(e.g.,IsScriptLoadingLogEnabled) to control verbose logging and other dev features viapackage.json. [1] [2]Module Resolution Improvements:
NormalizePathhelper to standardize file system paths for consistent lookups in the module registry, reducing path-related bugs during module loading.HTTP Module Import Handling:
require()for HTTP(S) modules, enforcing dynamicimport()for such cases and providing clear error messages.