Skip to content

Commit ff19da9

Browse files
committed
refactor(runtime): drop the require() optional-module placeholder
1 parent e330ca5 commit ff19da9

4 files changed

Lines changed: 17 additions & 164 deletions

File tree

NativeScript/runtime/ModuleInternal.h

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,6 @@ class ModuleInternal {
4646
const std::string& moduleName);
4747
std::string ResolvePathFromPackageJson(const std::string& packageJson,
4848
bool& error);
49-
v8::Local<v8::Object> CreatePlaceholderModule(v8::Isolate* isolate,
50-
const std::string& moduleName,
51-
const std::string& cacheKey);
5249
static v8::ScriptCompiler::CachedData* LoadScriptCache(
5350
const std::string& path);
5451
static void SaveScriptCache(const v8::Local<v8::Script> script,

NativeScript/runtime/ModuleInternal.mm

Lines changed: 0 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -22,46 +22,6 @@
2222

2323
namespace tns {
2424

25-
// require()-path policy only: import() rejects a missing bare specifier outright
26-
// (ESM optionality is `try { await import(x) } catch {}` at the call site, and in
27-
// dev sessions a bare specifier the import map doesn't cover is a config bug that
28-
// must fail loudly — see docs/knowledge/hmr-simplification-pass.md §2).
29-
static bool IsLikelyOptionalModule(const std::string& moduleName) {
30-
// Node built-ins are handled by their own dedicated resolution path; never treat them as
31-
// an optional external module.
32-
if (moduleName.rfind("node:", 0) == 0) {
33-
return false;
34-
}
35-
36-
// Check if it's a bare module name (no path separators) that could be an npm package.
37-
//
38-
// Bare specifiers that end in a recognizable script/data extension (e.g. "foo.js",
39-
// "config.json") are explicit file references, not npm-style package names — real npm
40-
// package names don't carry a file extension. Treating them as "likely optional" would
41-
// swallow a genuine "module not found" failure behind a lazily-throwing placeholder
42-
// instead of letting require()/import() fail immediately, which is what callers (and the
43-
// existing "should throw error if cant find node module" test) expect for those names.
44-
//
45-
// This carve-out is deliberately narrow: a dotted bare name that doesn't end in one of
46-
// these exact extensions (e.g. "lodash.debounce") is still treated as optional, same as
47-
// before. See ModuleInternal.mm/ModuleInternalCallbacks.mm optional-module tests for the
48-
// cases this boundary is expected to hold for.
49-
static const char* kExplicitFileExtensions[] = {".js", ".mjs", ".cjs", ".json", ".node", ".ts"};
50-
51-
if (moduleName.find('/') == std::string::npos && moduleName.find('\\') == std::string::npos &&
52-
moduleName[0] != '.' && moduleName[0] != '~' && moduleName[0] != '/') {
53-
for (const char* ext : kExplicitFileExtensions) {
54-
size_t extLen = strlen(ext);
55-
if (moduleName.size() > extLen &&
56-
moduleName.compare(moduleName.size() - extLen, extLen, ext) == 0) {
57-
return false;
58-
}
59-
}
60-
return true;
61-
}
62-
return false;
63-
}
64-
6525
// Helper function to check if a file path is an ES module (.mjs) but not a source map (.mjs.map)
6626
bool IsESModule(const std::string& path) {
6727
return path.size() >= 4 && path.compare(path.size() - 4, 4, ".mjs") == 0 &&
@@ -624,14 +584,6 @@ static inline void SetOutErrorMessage(std::string* outErrorMessage, const std::s
624584
}
625585

626586
if (path.empty()) {
627-
// A bare specifier shaped like an npm package name resolves to a
628-
// lazily-throwing placeholder, so an app can ship without an optional
629-
// dependency installed and only fail if it actually touches it. A
630-
// specifier resolved against "/" is an explicit absolute path, never an
631-
// optional package, so it always hard-fails.
632-
if (baseDir != "/" && IsLikelyOptionalModule(moduleName)) {
633-
return this->CreatePlaceholderModule(isolate, moduleName, cacheKey);
634-
}
635587
throw NativeScriptException(isolate, "Cannot find module '" + moduleName + "'", "Error");
636588
}
637589

@@ -1615,12 +1567,6 @@ throw NativeScriptException(
16151567
}
16161568

16171569
if (exists == NO) {
1618-
// No path for an optional-looking package: LoadImpl turns the empty result
1619-
// into the placeholder module rather than a hard failure.
1620-
if (IsLikelyOptionalModule(moduleName)) {
1621-
return std::string();
1622-
}
1623-
16241570
// Create a detailed error message with context
16251571
std::string errorMsg = "Cannot find module '" + moduleName + "'";
16261572
errorMsg += "\n Base directory: " + baseDir;
@@ -1724,94 +1670,6 @@ throw NativeScriptException(
17241670
return std::string([[basePath stringByAppendingPathExtension:@"js"] UTF8String]);
17251671
}
17261672

1727-
Local<Object> ModuleInternal::CreatePlaceholderModule(Isolate* isolate,
1728-
const std::string& moduleName,
1729-
const std::string& cacheKey) {
1730-
Local<Context> context = isolate->GetCurrentContext();
1731-
1732-
// Create a module object with exports that throws when accessed
1733-
Local<Object> moduleObj = Object::New(isolate);
1734-
1735-
// Create a Proxy that throws an error when any property is accessed.
1736-
//
1737-
// The message is passed to a constant factory script as a real V8 string
1738-
// (never interpolated into the script source). Interpolation is a JS
1739-
// injection hazard: the message itself contains single quotes
1740-
// ("Module 'zip' is not available...") which terminated the string literal
1741-
// early and made the generated script a guaranteed SyntaxError
1742-
// ("missing ) after argument list") that propagated out of require() with
1743-
// no hint of its origin.
1744-
std::string errorMessage =
1745-
"Module '" + moduleName + "' is not available. This is an optional module.";
1746-
static const char* kProxyFactorySource = "(function(msg) {"
1747-
" const error = new Error(msg);"
1748-
" return new Proxy({}, {"
1749-
" get: function(target, prop) {"
1750-
" throw error;"
1751-
" },"
1752-
" set: function(target, prop, value) {"
1753-
" throw error;"
1754-
" },"
1755-
" has: function(target, prop) {"
1756-
" return false;"
1757-
" },"
1758-
" ownKeys: function(target) {"
1759-
" return [];"
1760-
" },"
1761-
" getPrototypeOf: function(target) {"
1762-
" return null;"
1763-
" }"
1764-
" });"
1765-
"})";
1766-
1767-
TryCatch tc(isolate);
1768-
Local<Script> proxyScript;
1769-
if (Script::Compile(context, tns::ToV8String(isolate, kProxyFactorySource))
1770-
.ToLocal(&proxyScript)) {
1771-
Local<Value> factoryValue;
1772-
if (proxyScript->Run(context).ToLocal(&factoryValue) && factoryValue->IsFunction()) {
1773-
Local<Value> args[]{tns::ToV8String(isolate, errorMessage.c_str())};
1774-
Local<Value> proxyObject;
1775-
if (factoryValue.As<v8::Function>()
1776-
->Call(context, v8::Undefined(isolate), 1, args)
1777-
.ToLocal(&proxyObject)) {
1778-
// Set the exports to the proxy object
1779-
bool success = moduleObj->Set(context, tns::ToV8String(isolate, "exports"), proxyObject)
1780-
.FromMaybe(false);
1781-
if (!success) {
1782-
Log(@"Warning: Failed to set exports property on proxy module object");
1783-
}
1784-
}
1785-
}
1786-
}
1787-
if (tc.HasCaught()) {
1788-
// The placeholder is best-effort. Never let its construction machinery
1789-
// leak an exception that masks the real "module not found" condition.
1790-
Log(@"Warning: placeholder module construction failed for %s", moduleName.c_str());
1791-
}
1792-
1793-
// Set up the module object
1794-
bool success = moduleObj
1795-
->Set(context, tns::ToV8String(isolate, "id"),
1796-
tns::ToV8String(isolate, moduleName.c_str()))
1797-
.FromMaybe(false);
1798-
if (!success) {
1799-
Log(@"Warning: Failed to set id property on module object");
1800-
}
1801-
1802-
success =
1803-
moduleObj->Set(context, tns::ToV8String(isolate, "loaded"), v8::Boolean::New(isolate, true))
1804-
.FromMaybe(false);
1805-
if (!success) {
1806-
Log(@"Warning: Failed to set loaded property on module object");
1807-
}
1808-
1809-
// Cache the placeholder module
1810-
this->loadedModules_[cacheKey] = std::make_shared<Persistent<Object>>(isolate, moduleObj);
1811-
1812-
return moduleObj;
1813-
}
1814-
18151673
ScriptCompiler::CachedData* ModuleInternal::LoadScriptCache(const std::string& path) {
18161674
std::string canonicalPath = NormalizePath(path);
18171675
if (RuntimeConfig.IsDebug) {

TestRunner/app/tests/NodeBuiltinsAndOptionalModulesTests.mjs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,8 @@ describe("Node built-in and optional module resolution", function () {
1717
expect(u.protocol).toBe("file:");
1818
});
1919

20-
// The optional-module placeholder is require()-path policy only (its specs live in
21-
// shared/Require/index.js, pending review — docs/knowledge/optional-module-placeholder.md).
22-
// import() deliberately diverges: a missing bare specifier rejects, package-shaped or not,
23-
// so a dev-session import-map miss fails loudly instead of resolving to a lazily-throwing
24-
// proxy. ESM callers that want optionality can `try { await import(x) } catch {}`.
20+
// Missing bare specifiers fail at the request site on both require() and
21+
// import(). ESM callers that want optionality can `try { await import(x) } catch {}`.
2522
it("rejects a missing bare specifier instead of resolving a placeholder", async function () {
2623
const names = [
2724
"__ns_optional_test_module__",
@@ -39,6 +36,15 @@ describe("Node built-in and optional module resolution", function () {
3936
}
4037
expect(error).not.toBe(null);
4138
expect(String(error)).toContain("Cannot find module");
39+
40+
let requireError = null;
41+
try {
42+
globalThis.require(name);
43+
} catch (e) {
44+
requireError = e;
45+
}
46+
expect(requireError).not.toBe(null);
47+
expect(String(requireError)).toContain("Cannot find module");
4248
}
4349
});
4450

TestRunner/app/tests/NsUtilTests.js

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,23 +35,15 @@ describe("ns:util", function () {
3535

3636
it("leaves bare specifiers to npm resolution", function () {
3737
// Bare "util" must never resolve to the ns:util builtin. In this app
38-
// no npm "util" package exists, so the loader yields an optional-module
39-
// placeholder whose members throw on access — both the require and the
40-
// member read need guarding.
41-
var resolved;
42-
try {
43-
resolved = require("util");
44-
} catch (e) {
45-
resolved = undefined;
46-
}
47-
expect(resolved).not.toBe(util);
48-
var resolvedFormat;
38+
// no npm "util" package exists, so require("util") throws.
39+
var error;
4940
try {
50-
resolvedFormat = resolved && resolved.format;
41+
require("util");
5142
} catch (e) {
52-
resolvedFormat = undefined;
43+
error = e;
5344
}
54-
expect(resolvedFormat).not.toBe(util.format);
45+
expect(error).toBeDefined();
46+
expect(String(error)).toContain("Cannot find module");
5547
});
5648

5749
describe("format", function () {

0 commit comments

Comments
 (0)