Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
58 changes: 53 additions & 5 deletions NativeScript/runtime/BuiltinLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include "Caches.h"
#include "Helpers.h"
#include "NsBuiltinModules.h"

using namespace v8;

Expand All @@ -18,14 +19,55 @@ std::vector<uint8_t> builtinCache[static_cast<unsigned>(BuiltinId::kCount)];

// Every builtin is compiled as a function body receiving these fixed
// parameters, mirroring Node's module wrapper: a file exports through
// `module.exports`/`exports`, natives arrive as properties of the `binding`
// bag (Node's internalBinding idiom) and intrinsics as properties of
// `primordials`; each file destructures what it needs.
// `module.exports`/`exports`, reaches sibling builtin modules through
// `require`, natives arrive as properties of the `binding` bag (Node's
// internalBinding idiom) and intrinsics as properties of `primordials`; each
// file destructures what it needs.
constexpr const char* kExportsParamName = "exports";
constexpr const char* kRequireParamName = "require";
constexpr const char* kModuleParamName = "module";
constexpr const char* kBindingParamName = "binding";
constexpr const char* kPrimordialsParamName = "primordials";
constexpr int kParamCount = 4;
constexpr int kParamCount = 5;

// The `require` every builtin receives: builtin specifiers only, so a builtin
// can never reach application code or the filesystem.
void BuiltinRequireCallback(const FunctionCallbackInfo<Value>& info) {
Isolate* isolate = info.GetIsolate();
if (info.Length() < 1 || !info[0]->IsString()) {
isolate->ThrowException(Exception::TypeError(
tns::ToV8String(isolate, "require() expects a specifier string")));
return;
}

Local<Context> context = isolate->GetCurrentContext();
std::string specifier = tns::ToString(isolate, info[0].As<v8::String>());
Local<Object> exports;
if (NsBuiltinModules::GetExports(context, specifier).ToLocal(&exports)) {
info.GetReturnValue().Set(exports);
} else if (!NsBuiltinModules::IsRegistered(specifier)) {
isolate->ThrowException(Exception::Error(tns::ToV8String(
isolate, NsBuiltinModules::NotFoundMessage(specifier))));
}
}

MaybeLocal<v8::Function> GetBuiltinRequire(Local<Context> context) {
Isolate* isolate = v8::Isolate::GetCurrent();
std::shared_ptr<Caches> cache = Caches::Get(isolate);
if (cache->BuiltinRequire != nullptr) {
return cache->BuiltinRequire->Get(isolate);
}

Local<v8::Function> require;
if (!v8::Function::New(context, BuiltinRequireCallback, Local<Value>(), 1,
ConstructorBehavior::kThrow)
.ToLocal(&require)) {
return MaybeLocal<v8::Function>();
}
cache->BuiltinRequire =
std::make_unique<Persistent<v8::Function>>(isolate, require);
return require;
}

MaybeLocal<v8::Function> CompileBuiltin(Local<Context> context, BuiltinId id) {
Isolate* isolate = v8::Isolate::GetCurrent();
Expand Down Expand Up @@ -54,6 +96,7 @@ MaybeLocal<v8::Function> CompileBuiltin(Local<Context> context, BuiltinId id) {
isolate, builtin.source, static_cast<int>(builtin.length));
Local<v8::String> params[] = {
tns::ToV8String(isolate, kExportsParamName),
tns::ToV8String(isolate, kRequireParamName),
tns::ToV8String(isolate, kModuleParamName),
tns::ToV8String(isolate, kBindingParamName),
tns::ToV8String(isolate, kPrimordialsParamName)};
Expand Down Expand Up @@ -105,6 +148,11 @@ MaybeLocal<Value> CallBuiltin(Local<Context> context, BuiltinId id,
return MaybeLocal<Value>();
}

Local<v8::Function> require;
if (!GetBuiltinRequire(context).ToLocal(&require)) {
return MaybeLocal<Value>();
}

Local<Object> exportsObj = Object::New(isolate);
Local<Object> moduleObj = Object::New(isolate);
Local<v8::String> exportsKey = tns::ToV8String(isolate, kExportsParamName);
Expand All @@ -113,7 +161,7 @@ MaybeLocal<Value> CallBuiltin(Local<Context> context, BuiltinId id,
}

Local<Value> args[] = {
exportsObj, moduleObj,
exportsObj, require, moduleObj,
binding.IsEmpty() ? v8::Undefined(isolate).As<Value>() : binding,
primordials};
if (fn->Call(context, v8::Undefined(isolate), kParamCount, args).IsEmpty()) {
Expand Down
10 changes: 6 additions & 4 deletions NativeScript/runtime/BuiltinLoader.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ namespace tns {
class BuiltinLoader {
public:
// Compiles the builtin identified by id as a function body with the fixed
// parameters `exports`, `module`, `binding` (Node's module wrapper plus its
// internalBinding idiom) and `primordials`, calls it with the given bag of
// natives (or undefined when omitted) plus this isolate's frozen intrinsics
// snapshot, and returns the resulting `module.exports`. The snapshot is
// parameters `exports`, `require`, `module`, `binding` (Node's module wrapper
// plus its internalBinding idiom) and `primordials`, calls it with the given
// bag of natives (or undefined when omitted) plus this isolate's frozen
// intrinsics snapshot, and returns the resulting `module.exports`. `require`
// reaches the builtin modules (NsBuiltinModules) and nothing else. The
// snapshot is
// produced by the kPrimordials builtin on first use and cached per isolate,
// so it is taken before any user code can replace a global. Scripts carry
// an "internal/<name>.js" origin so runtime
Expand Down
20 changes: 20 additions & 0 deletions NativeScript/runtime/Caches.h
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ class Caches {
// console formatter (internal/inspect.js), initialized by Console::Init.
std::unique_ptr<v8::Persistent<v8::Function>> InspectFunc =
std::unique_ptr<v8::Persistent<v8::Function>>(nullptr);
// ns:util's format, used by console.* for %-substitution.
std::unique_ptr<v8::Persistent<v8::Function>> FormatFunc =
std::unique_ptr<v8::Persistent<v8::Function>>(nullptr);
bool FormatFuncUnavailable = false;
std::unique_ptr<v8::Persistent<v8::Function>> InteropReferenceCtorFunc =
std::unique_ptr<v8::Persistent<v8::Function>>(nullptr);
std::unique_ptr<v8::Persistent<v8::Function>> PointerCtorFunc =
Expand All @@ -164,6 +168,22 @@ class Caches {
std::unique_ptr<v8::Persistent<v8::Function>> UnmanagedTypeCtorFunc =
std::unique_ptr<v8::Persistent<v8::Function>>(nullptr);

// `ns:`/`node:` builtin modules (NsBuiltinModules), keyed by specifier. Both
// are per isolate: a builtin module is a singleton per realm, so workers get
// their own exports objects and their own synthetic modules.
robin_hood::unordered_map<std::string,
std::unique_ptr<v8::Persistent<v8::Object>>>
BuiltinModuleExports;
robin_hood::unordered_map<std::string,
std::unique_ptr<v8::Persistent<v8::Module>>>
BuiltinModules;
// Specifiers currently being built, so a shim requiring back into the module
// that is loading it fails instead of recursing.
robin_hood::unordered_set<std::string> BuiltinModulesInProgress;
// The `require` handed to every builtin, resolving builtin specifiers only.
std::unique_ptr<v8::Persistent<v8::Function>> BuiltinRequire =
std::unique_ptr<v8::Persistent<v8::Function>>(nullptr);

// Frozen intrinsics snapshot returned by internal/primordials.js, passed to
// every builtin as its second fixed parameter (BuiltinLoader::RunBuiltin).
// Per isolate, so workers snapshot their own realm's intrinsics.
Expand Down
30 changes: 30 additions & 0 deletions NativeScript/runtime/Console.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "DataWrapper.h"
#include "Helpers.h"
#include "NativeScriptException.h"
#include "NsBuiltinModules.h"
#include "RuntimeConfig.h"
// #include "v8-log-agent-impl.h"
#include <sstream>
Expand Down Expand Up @@ -272,6 +273,30 @@ std::string Console::BuildStringFromArgs(
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
int argLen = args.Length();

// console.* follows Node: the arguments go through util.format, so the first
// one may carry %-substitutions and the rest are appended space-separated.
Local<v8::Function> format = argLen > startingIndex
? NsBuiltinModules::GetFormatFunc(context)
: Local<v8::Function>();
if (!format.IsEmpty()) {
std::vector<Local<Value>> formatArgs;
formatArgs.reserve(argLen - startingIndex);
for (int i = startingIndex; i < argLen; i++) {
formatArgs.push_back(args[i]);
}
TryCatch tc(isolate);
Local<Value> result;
if (format
->Call(context, v8::Undefined(isolate),
static_cast<int>(formatArgs.size()), formatArgs.data())
.ToLocal(&result) &&
result->IsString()) {
return tns::ToString(isolate, result.As<v8::String>());
}
}

// ns:util unavailable or the formatter threw: per-argument rendering.
std::stringstream ss;

if (argLen > 0) {
Expand Down Expand Up @@ -327,6 +352,11 @@ static void GetNativeWrapperHintCallback(

void Console::InitInspect(Local<Context> context) {
Isolate* isolate = v8::Isolate::GetCurrent();
if (Caches::Get(isolate)->InspectFunc != nullptr) {
// inspect.js installs a non-configurable global.__inspect, so a second run
// in the same realm would throw.
return;
}

Local<v8::Function> hintFunc;
if (!v8::Function::New(context, GetNativeWrapperHintCallback)
Expand Down
4 changes: 3 additions & 1 deletion NativeScript/runtime/Console.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ class Console {
static void AttachInspectorClient(
v8_inspector::JsV8InspectorClient* inspector);
static void DetachInspectorClient();
// Builds this realm's inspect function (Caches::InspectFunc) if it isn't
// there yet. Public so ns:util can re-export the same instance.
static void InitInspect(v8::Local<v8::Context> context);

private:
using ConsoleAPIType = v8_inspector::ConsoleAPIType;
Expand All @@ -34,7 +37,6 @@ class Console {
static v8::Local<v8::String> InspectValue(v8::Local<v8::Context> context,
const v8::Local<v8::Value>& val,
int depth = -1);
static void InitInspect(v8::Local<v8::Context> context);
static ConsoleAPIType VerbosityToInspectorMethod(const std::string level);

static void SendToDevToolsFrontEnd(
Expand Down
20 changes: 20 additions & 0 deletions NativeScript/runtime/ModuleInternal.mm
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "Helpers.h"
#include "ModuleInternalCallbacks.h" // for ResolveModuleCallback
#include "NativeScriptException.h"
#include "NsBuiltinModules.h"
#include "Runtime.h" // for GetAppConfigValue
#include "RuntimeConfig.h"

Expand Down Expand Up @@ -214,6 +215,25 @@ bool IsESModule(const std::string& path) {
void ModuleInternal::RequireCallback(const FunctionCallbackInfo<Value>& info) {
Isolate* isolate = info.GetIsolate();

// Builtin modules resolve before any path handling, so they can never be
// shadowed by a file or a package, and an unknown one fails as a missing
// builtin rather than as a missing file. Only prefixed specifiers get here:
// a bare `util` still resolves through npm.
if (info.Length() > 0 && info[0]->IsString()) {
std::string specifier = tns::ToString(isolate, info[0].As<v8::String>());
if (NsBuiltinModules::IsBuiltinScheme(specifier)) {
Local<Context> context = isolate->GetCurrentContext();
Local<Object> exports;
if (NsBuiltinModules::GetExports(context, specifier).ToLocal(&exports)) {
info.GetReturnValue().Set(exports);
} else if (!NsBuiltinModules::IsRegistered(specifier)) {
isolate->ThrowException(Exception::Error(
tns::ToV8String(isolate, NsBuiltinModules::NotFoundMessage(specifier))));
}
return;
}
}

Comment on lines +218 to +236

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect IsBuiltinScheme / IsNsScheme / IsRegistered definitions and the node:url legacy polyfill.
rg -n -B2 -A15 'bool.*IsBuiltinScheme|bool.*IsNsScheme|bool.*IsRegistered' NativeScript/runtime/NsBuiltinModules.h NativeScript/runtime/NsBuiltinModules.cpp 2>/dev/null
rg -n 'node:url|node:util' NativeScript/runtime/NsBuiltinModules.cpp docs/ns-builtin-modules.md 2>/dev/null

Repository: NativeScript/ios

Length of output: 4970


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ModuleInternal RequireCallback context =="
sed -n '190,250p' NativeScript/runtime/ModuleInternal.mm 2>/dev/null || true

echo
echo "== ModuleInternalCallbacks relevant resolution callbacks =="
rg -n -B8 -A22 'ResolveModuleCallback|ImportModuleDynamicallyCallback|legacy|node:url|node:util|RequireCallback' NativeScript/runtime/ModuleInternalCallbacks.mm 2>/dev/null || true

echo
echo "== NsBuiltinModules constants and prefix check =="
rg -n -B4 -A6 'kNodePrefix|kNsPrefix|HasPrefix' NativeScript/runtime/NsBuiltinModules.cpp 2>/dev/null || true

echo
echo "== Deterministic predicate/probe from source text =="
python3 - <<'PY'
from pathlib import Path
import re

ns_cpp = Path('NativeScript/runtime/NsBuiltinModules.cpp').read_text(errors='replace')
m = re.search(r'bool NsBuiltinModules::IsBuiltinScheme\(const std::string& specifier\) \{\s*(.*?)\n\}', ns_cpp, re.S)
print('IsBuiltinScheme body:', m.group(0).strip() if m else 'NOT_FOUND')

m = re.search(r'bool NsBuiltinModules::IsNsScheme\(const std::string& specifier\) \{\s*(.*?)\n\}', ns_cpp, re.S)
print('IsNsScheme body:', m.group(0).strip() if m else 'NOT_FOUND')

m = re.search(r'bool.+kNodePrefix.+=(.+?);|kNodePrefix\s*=\s*([^;]+);', \
    '\n'.join(re.findall(r'k[a-zA-Z0-9_]+Prefix\s*=[^;]+;', ns_cpp)), re.S)
prefixes = re.findall(r'k[Nn]ode[Nn]Prefix(?:\s*=\s*\"([^\"]+)\"|:\s*\"([^\"]+)")', ns_cpp)
print('prefixes text:', prefixes)

def has_prefix(specifier, prefix):
    return specifier.startswith(prefix)
def predicate_new(specifier):
    # Derived from current IsBuiltinScheme body
    return has_prefix(specifier, 'ns:') or has_prefix(specifier, 'node:')
def predicate_esm_condition(specifier, registered):
    # Derived from IsRegistered(rawSpec) || IsNsScheme(rawSpec) branch
    return registered or has_prefix(specifier, 'ns:')

for specifier in ['node:url', 'node:util', 'ns:url', 'util', 'app/builtin']:
    registered = specifier in {'node:url', 'node:util'}  # registry lookup not provided; use documented presence
    print(specifier, 'new_predicate=', predicate_new(specifier),
          'esm_condition=', predicate_esm_condition(specifier, registered),
          'registered=', registered)
PY

Repository: NativeScript/ios

Length of output: 33628


Align RequireCallback builtin-prefixed resolution with ESM paths.

RequireCallback now treats every ns: or node: specifier as builtin and rejects unregistered keys as missing modules. ESM resolution only takes the new builtin path for registered nodes and ns:-prefixed specifiers, leaving unregistered node: specifiers to the legacy node:url fallback. Use or document the same branch predicate for both paths so CommonJS and ESM handle the same node: specifiers consistently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@NativeScript/runtime/ModuleInternal.mm` around lines 218 - 236, Update the
builtin-resolution predicate in RequireCallback to match ESM behavior: use the
new builtin path for registered modules and ns:-prefixed specifiers, while
allowing unregistered node:-prefixed specifiers to continue through the legacy
node:url fallback. Reuse the existing ESM branch predicate or document and
centralize the shared condition so CommonJS and ESM resolve node: specifiers
consistently.

// Declare these outside try block so they're available in catch
std::string moduleName;
std::string callingModuleDirName;
Expand Down
46 changes: 41 additions & 5 deletions NativeScript/runtime/ModuleInternalCallbacks.mm
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "Helpers.h" // for tns::Exists
#include "ModuleInternal.h" // for LoadScript(...)
#include "NativeScriptException.h"
#include "NsBuiltinModules.h"
#include "Runtime.h" // for GetAppConfigValue
#include "RuntimeConfig.h"

Expand Down Expand Up @@ -715,6 +716,20 @@ static bool IsDocumentsPath(const std::string& path) {
return v8::MaybeLocal<v8::Module>();
}

// Builtin modules resolve before any path handling. Unshimmed "node:" names
// fall through to the legacy node:url polyfill below.
if (NsBuiltinModules::IsRegistered(rawSpec) || NsBuiltinModules::IsNsScheme(rawSpec)) {
v8::Local<v8::Module> builtin;
if (NsBuiltinModules::GetModule(context, rawSpec).ToLocal(&builtin)) {
return v8::MaybeLocal<v8::Module>(builtin);
}
if (!NsBuiltinModules::IsRegistered(rawSpec)) {
isolate->ThrowException(v8::Exception::Error(
tns::ToV8String(isolate, NsBuiltinModules::NotFoundMessage(rawSpec))));
}
return v8::MaybeLocal<v8::Module>();
}

std::string normalizedSpec = rawSpec;

// Normalize malformed HTTP(S) schemes that sometimes appear as 'http:/host' (single slash)
Expand Down Expand Up @@ -1344,11 +1359,9 @@ static bool IsDocumentsPath(const std::string& path) {
" return new URL('file://' + encoded);\n"
"}\n";
} else {
// Generic polyfill for other Node.js built-in modules
polyfillContent = "// In-memory polyfill for node:" + builtinName + "\n" +
"console.warn('Node.js built-in module \\'node:" + builtinName +
"\\' is not fully supported in NativeScript');\n" +
"export default {};\n";
isolate->ThrowException(v8::Exception::Error(
tns::ToV8String(isolate, NsBuiltinModules::NotFoundMessage(spec))));
return v8::MaybeLocal<v8::Module>();
}

v8::MaybeLocal<v8::Module> m =
Expand Down Expand Up @@ -1862,6 +1875,29 @@ static bool IsDocumentsPath(const std::string& path) {
// Normalize spec: expand '@/'; only strip ?query/hash for non-HTTP specs so SFC HTTP keys keep
// version tags
std::string rawSpec = cSpec ? std::string(cSpec) : std::string();

// Builtin modules never reach the loader below; the namespace comes straight
// from the realm's synthetic module.
if (NsBuiltinModules::IsRegistered(rawSpec) || NsBuiltinModules::IsNsScheme(rawSpec)) {
v8::EscapableHandleScope builtinScope(isolate);
v8::Local<v8::Promise::Resolver> builtinResolver;
if (!v8::Promise::Resolver::New(context).ToLocal(&builtinResolver)) {
return v8::MaybeLocal<v8::Promise>();
}
v8::TryCatch tc(isolate);
v8::Local<v8::Module> builtin;
if (NsBuiltinModules::GetModule(context, rawSpec).ToLocal(&builtin)) {
builtinResolver->Resolve(context, builtin->GetModuleNamespace()).FromMaybe(false);
} else {
v8::Local<v8::Value> error = tc.HasCaught()
? tc.Exception()
: v8::Exception::Error(tns::ToV8String(
isolate, NsBuiltinModules::NotFoundMessage(rawSpec)));
builtinResolver->Reject(context, error).FromMaybe(false);
}
return builtinScope.Escape(builtinResolver->GetPromise());
}

std::string normalizedSpec = rawSpec;
// remove query/hash ONLY for non-HTTP specs
bool isHttpLike = (!normalizedSpec.empty() && (StartsWith(normalizedSpec, "http://") ||
Expand Down
Loading