diff --git a/NativeScript/runtime/BuiltinLoader.cpp b/NativeScript/runtime/BuiltinLoader.cpp index 0c99f3c8..f5c1c909 100644 --- a/NativeScript/runtime/BuiltinLoader.cpp +++ b/NativeScript/runtime/BuiltinLoader.cpp @@ -5,6 +5,7 @@ #include "Caches.h" #include "Helpers.h" +#include "NsBuiltinModules.h" using namespace v8; @@ -18,14 +19,55 @@ std::vector builtinCache[static_cast(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& 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 = isolate->GetCurrentContext(); + std::string specifier = tns::ToString(isolate, info[0].As()); + Local 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 GetBuiltinRequire(Local context) { + Isolate* isolate = v8::Isolate::GetCurrent(); + std::shared_ptr cache = Caches::Get(isolate); + if (cache->BuiltinRequire != nullptr) { + return cache->BuiltinRequire->Get(isolate); + } + + Local require; + if (!v8::Function::New(context, BuiltinRequireCallback, Local(), 1, + ConstructorBehavior::kThrow) + .ToLocal(&require)) { + return MaybeLocal(); + } + cache->BuiltinRequire = + std::make_unique>(isolate, require); + return require; +} MaybeLocal CompileBuiltin(Local context, BuiltinId id) { Isolate* isolate = v8::Isolate::GetCurrent(); @@ -54,6 +96,7 @@ MaybeLocal CompileBuiltin(Local context, BuiltinId id) { isolate, builtin.source, static_cast(builtin.length)); Local params[] = { tns::ToV8String(isolate, kExportsParamName), + tns::ToV8String(isolate, kRequireParamName), tns::ToV8String(isolate, kModuleParamName), tns::ToV8String(isolate, kBindingParamName), tns::ToV8String(isolate, kPrimordialsParamName)}; @@ -105,6 +148,11 @@ MaybeLocal CallBuiltin(Local context, BuiltinId id, return MaybeLocal(); } + Local require; + if (!GetBuiltinRequire(context).ToLocal(&require)) { + return MaybeLocal(); + } + Local exportsObj = Object::New(isolate); Local moduleObj = Object::New(isolate); Local exportsKey = tns::ToV8String(isolate, kExportsParamName); @@ -113,7 +161,7 @@ MaybeLocal CallBuiltin(Local context, BuiltinId id, } Local args[] = { - exportsObj, moduleObj, + exportsObj, require, moduleObj, binding.IsEmpty() ? v8::Undefined(isolate).As() : binding, primordials}; if (fn->Call(context, v8::Undefined(isolate), kParamCount, args).IsEmpty()) { diff --git a/NativeScript/runtime/BuiltinLoader.h b/NativeScript/runtime/BuiltinLoader.h index 56fb7505..fe7ae8c4 100644 --- a/NativeScript/runtime/BuiltinLoader.h +++ b/NativeScript/runtime/BuiltinLoader.h @@ -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/.js" origin so runtime diff --git a/NativeScript/runtime/Caches.h b/NativeScript/runtime/Caches.h index 647b4214..1b62c19b 100644 --- a/NativeScript/runtime/Caches.h +++ b/NativeScript/runtime/Caches.h @@ -155,6 +155,10 @@ class Caches { // console formatter (internal/inspect.js), initialized by Console::Init. std::unique_ptr> InspectFunc = std::unique_ptr>(nullptr); + // ns:util's format, used by console.* for %-substitution. + std::unique_ptr> FormatFunc = + std::unique_ptr>(nullptr); + bool FormatFuncUnavailable = false; std::unique_ptr> InteropReferenceCtorFunc = std::unique_ptr>(nullptr); std::unique_ptr> PointerCtorFunc = @@ -164,6 +168,22 @@ class Caches { std::unique_ptr> UnmanagedTypeCtorFunc = std::unique_ptr>(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>> + BuiltinModuleExports; + robin_hood::unordered_map>> + 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 BuiltinModulesInProgress; + // The `require` handed to every builtin, resolving builtin specifiers only. + std::unique_ptr> BuiltinRequire = + std::unique_ptr>(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. diff --git a/NativeScript/runtime/Console.cpp b/NativeScript/runtime/Console.cpp index 0ca65a89..a0550af8 100644 --- a/NativeScript/runtime/Console.cpp +++ b/NativeScript/runtime/Console.cpp @@ -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 @@ -272,6 +273,30 @@ std::string Console::BuildStringFromArgs( Isolate* isolate = args.GetIsolate(); Local 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 format = argLen > startingIndex + ? NsBuiltinModules::GetFormatFunc(context) + : Local(); + if (!format.IsEmpty()) { + std::vector> formatArgs; + formatArgs.reserve(argLen - startingIndex); + for (int i = startingIndex; i < argLen; i++) { + formatArgs.push_back(args[i]); + } + TryCatch tc(isolate); + Local result; + if (format + ->Call(context, v8::Undefined(isolate), + static_cast(formatArgs.size()), formatArgs.data()) + .ToLocal(&result) && + result->IsString()) { + return tns::ToString(isolate, result.As()); + } + } + + // ns:util unavailable or the formatter threw: per-argument rendering. std::stringstream ss; if (argLen > 0) { @@ -327,6 +352,11 @@ static void GetNativeWrapperHintCallback( void Console::InitInspect(Local 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 hintFunc; if (!v8::Function::New(context, GetNativeWrapperHintCallback) diff --git a/NativeScript/runtime/Console.h b/NativeScript/runtime/Console.h index 0ddb59a8..d0b43504 100644 --- a/NativeScript/runtime/Console.h +++ b/NativeScript/runtime/Console.h @@ -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 context); private: using ConsoleAPIType = v8_inspector::ConsoleAPIType; @@ -34,7 +37,6 @@ class Console { static v8::Local InspectValue(v8::Local context, const v8::Local& val, int depth = -1); - static void InitInspect(v8::Local context); static ConsoleAPIType VerbosityToInspectorMethod(const std::string level); static void SendToDevToolsFrontEnd( diff --git a/NativeScript/runtime/ModuleInternal.mm b/NativeScript/runtime/ModuleInternal.mm index afabf8b2..a864298a 100644 --- a/NativeScript/runtime/ModuleInternal.mm +++ b/NativeScript/runtime/ModuleInternal.mm @@ -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" @@ -214,6 +215,25 @@ bool IsESModule(const std::string& path) { void ModuleInternal::RequireCallback(const FunctionCallbackInfo& 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()); + if (NsBuiltinModules::IsBuiltinScheme(specifier)) { + Local context = isolate->GetCurrentContext(); + Local 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; + } + } + // Declare these outside try block so they're available in catch std::string moduleName; std::string callingModuleDirName; diff --git a/NativeScript/runtime/ModuleInternalCallbacks.mm b/NativeScript/runtime/ModuleInternalCallbacks.mm index 5b0fad3d..25305610 100644 --- a/NativeScript/runtime/ModuleInternalCallbacks.mm +++ b/NativeScript/runtime/ModuleInternalCallbacks.mm @@ -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" @@ -715,6 +716,20 @@ static bool IsDocumentsPath(const std::string& path) { return v8::MaybeLocal(); } + // 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 builtin; + if (NsBuiltinModules::GetModule(context, rawSpec).ToLocal(&builtin)) { + return v8::MaybeLocal(builtin); + } + if (!NsBuiltinModules::IsRegistered(rawSpec)) { + isolate->ThrowException(v8::Exception::Error( + tns::ToV8String(isolate, NsBuiltinModules::NotFoundMessage(rawSpec)))); + } + return v8::MaybeLocal(); + } + std::string normalizedSpec = rawSpec; // Normalize malformed HTTP(S) schemes that sometimes appear as 'http:/host' (single slash) @@ -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::MaybeLocal m = @@ -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 builtinResolver; + if (!v8::Promise::Resolver::New(context).ToLocal(&builtinResolver)) { + return v8::MaybeLocal(); + } + v8::TryCatch tc(isolate); + v8::Local builtin; + if (NsBuiltinModules::GetModule(context, rawSpec).ToLocal(&builtin)) { + builtinResolver->Resolve(context, builtin->GetModuleNamespace()).FromMaybe(false); + } else { + v8::Local 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://") || diff --git a/NativeScript/runtime/NsBuiltinModules.cpp b/NativeScript/runtime/NsBuiltinModules.cpp new file mode 100644 index 00000000..222e0f23 --- /dev/null +++ b/NativeScript/runtime/NsBuiltinModules.cpp @@ -0,0 +1,303 @@ +#include "NsBuiltinModules.h" + +#include + +#include "BuiltinLoader.h" +#include "Caches.h" +#include "Console.h" +#include "Helpers.h" + +using namespace v8; + +namespace tns { + +namespace { + +constexpr const char* kNsPrefix = "ns:"; +constexpr const char* kNodePrefix = "node:"; + +struct Registration { + const char* specifier; + BuiltinId builtin; +}; + +// The v1 registry (docs/ns-builtin-modules.md). One specifier, one source +// file: a `node:` shim is its own builtin that requires the `ns:` module it +// adapts, so the two module objects stay distinct and the standard module +// never carries compatibility code. +constexpr Registration kRegistry[] = { + {"ns:util", BuiltinId::kNsUtil}, + {"node:util", BuiltinId::kNodeUtil}, +}; + +const Registration* Find(const std::string& specifier) { + for (const Registration& registration : kRegistry) { + if (specifier == registration.specifier) { + return ®istration; + } + } + return nullptr; +} + +bool HasPrefix(const std::string& specifier, const char* prefix) { + return specifier.rfind(prefix, 0) == 0; +} + +MaybeLocal BuildBinding(Local context, BuiltinId builtin) { + Isolate* isolate = v8::Isolate::GetCurrent(); + Local binding = Object::New(isolate); + + switch (builtin) { + case BuiltinId::kNsUtil: { + // The console formatter is built once per realm; ns:util re-exports that + // instance instead of creating a second one. + Console::InitInspect(context); + std::shared_ptr cache = Caches::Get(isolate); + if (cache->InspectFunc == nullptr) { + return MaybeLocal(); + } + if (!binding + ->Set(context, tns::ToV8String(isolate, "inspect"), + cache->InspectFunc->Get(isolate)) + .FromMaybe(false)) { + return MaybeLocal(); + } + break; + } + default: + break; + } + + return binding; +} + +// Runs a module's builtin and caches its exports. Always leaves an exception +// pending when it returns false. +bool Instantiate(Local context, const Registration& requested) { + Isolate* isolate = v8::Isolate::GetCurrent(); + std::shared_ptr cache = Caches::Get(isolate); + + // A shim reaches its ns: module through the builtin require, so the graph is + // walked while a module is still being built; a cycle would otherwise + // recurse until the stack runs out. + if (cache->BuiltinModulesInProgress.count(requested.specifier) > 0) { + isolate->ThrowException(Exception::Error( + tns::ToV8String(isolate, "Circular require of built-in module: " + + std::string(requested.specifier)))); + return false; + } + cache->BuiltinModulesInProgress.emplace(requested.specifier); + + TryCatch tc(isolate); + Local binding; + Local result; + bool built = BuildBinding(context, requested.builtin).ToLocal(&binding) && + BuiltinLoader::RunBuiltin(context, requested.builtin, binding) + .ToLocal(&result) && + result->IsObject(); + cache->BuiltinModulesInProgress.erase(requested.specifier); + + if (built) { + cache->BuiltinModuleExports[requested.specifier] = + std::make_unique>(isolate, result.As()); + return true; + } + + if (tc.HasCaught()) { + tc.ReThrow(); + return false; + } + isolate->ThrowException(Exception::Error( + tns::ToV8String(isolate, "Failed to initialize built-in module '" + + std::string(requested.specifier) + "'"))); + return false; +} + +// The exports a synthetic module re-exports by name, in the order used both +// when declaring the export names and when populating them. +MaybeLocal ExportKeys(Local context, + Local exports) { + return exports->GetOwnPropertyNames(context, PropertyFilter::ONLY_ENUMERABLE, + KeyConversionMode::kConvertToString); +} + +MaybeLocal NoDependencies(Local context, + Local specifier, + Local import_attributes, + Local referrer) { + // Synthetic modules never request anything. + return MaybeLocal(); +} + +MaybeLocal PopulateSyntheticModule(Local context, + Local module) { + Isolate* isolate = v8::Isolate::GetCurrent(); + // The specifier was handed to CreateSyntheticModule as the module name, + // which is how these steps find the exports object again. + std::string specifier = tns::ToString(isolate, module->GetResourceName()); + + Local exports; + if (!NsBuiltinModules::GetExports(context, specifier).ToLocal(&exports)) { + return MaybeLocal(); + } + + Local defaultName = tns::ToV8String(isolate, "default"); + if (module->SetSyntheticModuleExport(isolate, defaultName, exports) + .IsNothing()) { + return MaybeLocal(); + } + + Local keys; + if (!ExportKeys(context, exports).ToLocal(&keys)) { + return MaybeLocal(); + } + for (uint32_t i = 0; i < keys->Length(); i++) { + Local key; + Local value; + if (!keys->Get(context, i).ToLocal(&key) || !key->IsString()) { + return MaybeLocal(); + } + if (key.As()->StringEquals(defaultName)) { + continue; + } + if (!exports->Get(context, key).ToLocal(&value) || + module->SetSyntheticModuleExport(isolate, key.As(), value) + .IsNothing()) { + return MaybeLocal(); + } + } + + Local resolver; + if (!Promise::Resolver::New(context).ToLocal(&resolver) || + !resolver->Resolve(context, v8::Undefined(isolate)).FromMaybe(false)) { + return MaybeLocal(); + } + return resolver->GetPromise(); +} + +} // namespace + +bool NsBuiltinModules::IsBuiltinScheme(const std::string& specifier) { + return HasPrefix(specifier, kNsPrefix) || HasPrefix(specifier, kNodePrefix); +} + +bool NsBuiltinModules::IsNsScheme(const std::string& specifier) { + return HasPrefix(specifier, kNsPrefix); +} + +bool NsBuiltinModules::IsRegistered(const std::string& specifier) { + return Find(specifier) != nullptr; +} + +std::string NsBuiltinModules::NotFoundMessage(const std::string& specifier) { + return "No such built-in module: " + specifier; +} + +MaybeLocal NsBuiltinModules::GetExports(Local context, + const std::string& specifier) { + const Registration* registration = Find(specifier); + if (registration == nullptr) { + return MaybeLocal(); + } + + Isolate* isolate = v8::Isolate::GetCurrent(); + std::shared_ptr cache = Caches::Get(isolate); + auto it = cache->BuiltinModuleExports.find(specifier); + if (it == cache->BuiltinModuleExports.end()) { + if (!Instantiate(context, *registration)) { + return MaybeLocal(); + } + it = cache->BuiltinModuleExports.find(specifier); + if (it == cache->BuiltinModuleExports.end()) { + return MaybeLocal(); + } + } + return it->second->Get(isolate); +} + +MaybeLocal NsBuiltinModules::GetModule(Local context, + const std::string& specifier) { + Isolate* isolate = v8::Isolate::GetCurrent(); + std::shared_ptr cache = Caches::Get(isolate); + + auto it = cache->BuiltinModules.find(specifier); + if (it != cache->BuiltinModules.end()) { + Local cached = it->second->Get(isolate); + if (!cached.IsEmpty() && cached->GetStatus() != Module::kErrored) { + return cached; + } + cache->BuiltinModules.erase(it); + } + + Local exports; + if (!GetExports(context, specifier).ToLocal(&exports)) { + return MaybeLocal(); + } + + Local defaultName = tns::ToV8String(isolate, "default"); + std::vector> exportNames{defaultName}; + Local keys; + if (!ExportKeys(context, exports).ToLocal(&keys)) { + return MaybeLocal(); + } + for (uint32_t i = 0; i < keys->Length(); i++) { + Local key; + if (!keys->Get(context, i).ToLocal(&key) || !key->IsString()) { + return MaybeLocal(); + } + if (!key.As()->StringEquals(defaultName)) { + exportNames.push_back(key.As()); + } + } + + Local module = Module::CreateSyntheticModule( + isolate, tns::ToV8String(isolate, specifier), + MemorySpan>(exportNames.data(), + exportNames.size()), + PopulateSyntheticModule); + // No dependencies and no user code, so the module can be driven to its final + // state here; importers then only ever see an evaluated module. + if (!module->InstantiateModule(context, &NoDependencies).FromMaybe(false)) { + return MaybeLocal(); + } + if (module->Evaluate(context).IsEmpty()) { + return MaybeLocal(); + } + + cache->BuiltinModules[specifier] = + std::make_unique>(isolate, module); + return module; +} + +Local NsBuiltinModules::GetFormatFunc(Local context) { + Isolate* isolate = v8::Isolate::GetCurrent(); + std::shared_ptr cache = Caches::Get(isolate); + if (cache->FormatFunc != nullptr) { + return cache->FormatFunc->Get(isolate); + } + if (cache->FormatFuncUnavailable) { + return Local(); + } + + TryCatch tc(isolate); + Local exports; + Local format; + if (!GetExports(context, "ns:util").ToLocal(&exports) || + !exports->Get(context, tns::ToV8String(isolate, "format")) + .ToLocal(&format) || + !format->IsFunction()) { + if (tc.HasCaught()) { + tns::LogError(isolate, tc); + } + // One attempt per realm: a broken builtin must not make every log call + // recompile it. + cache->FormatFuncUnavailable = true; + return Local(); + } + + cache->FormatFunc = std::make_unique>( + isolate, format.As()); + return format.As(); +} + +} // namespace tns diff --git a/NativeScript/runtime/NsBuiltinModules.h b/NativeScript/runtime/NsBuiltinModules.h new file mode 100644 index 00000000..da6edb67 --- /dev/null +++ b/NativeScript/runtime/NsBuiltinModules.h @@ -0,0 +1,50 @@ +#ifndef NsBuiltinModules_h +#define NsBuiltinModules_h + +#include + +#include "Common.h" + +namespace tns { + +// The `ns:` builtin modules and their `node:` compatibility shims, as +// specified by docs/ns-builtin-modules.md. Every module is a per-realm +// singleton built from a RuntimeBuiltins source, so a specifier always yields +// the same frozen exports object and the same synthetic ES module. +class NsBuiltinModules { + public: + // Any `ns:`/`node:` specifier, registered or not. The CommonJS resolver + // claims all of them so a prefixed name can never reach the filesystem; + // bare specifiers (`util`) keep resolving through npm untouched. + static bool IsBuiltinScheme(const std::string& specifier); + + // The `ns:` half of the scheme, whose unknown names must fail rather than + // fall through to any legacy handling. + static bool IsNsScheme(const std::string& specifier); + + // Whether a module of that name exists. + static bool IsRegistered(const std::string& specifier); + + // Frozen exports object of a registered specifier. Empty when the specifier + // is not registered (nothing thrown) or when the module failed to build (an + // exception is pending). + static v8::MaybeLocal GetExports(v8::Local context, + const std::string& specifier); + + // Evaluated synthetic module exporting the same values as GetExports under + // their own names plus `default` (the exports object itself). + static v8::MaybeLocal GetModule(v8::Local context, + const std::string& specifier); + + // What every resolver reports for a specifier in the scheme that names no + // module. + static std::string NotFoundMessage(const std::string& specifier); + + // `ns:util`'s format, used by console.* for %-substitution. Empty when the + // module could not be built; callers degrade instead of failing the log. + static v8::Local GetFormatFunc(v8::Local context); +}; + +} // namespace tns + +#endif /* NsBuiltinModules_h */ diff --git a/NativeScript/runtime/js/README.md b/NativeScript/runtime/js/README.md index 95382b6d..7bb8a47f 100644 --- a/NativeScript/runtime/js/README.md +++ b/NativeScript/runtime/js/README.md @@ -9,17 +9,24 @@ at runtime `BuiltinLoader::RunBuiltin` compiles and executes them with an ## Contract (Node's module wrapper + internalBinding idiom) Every file is compiled as a **function body** via `v8::ScriptCompiler::CompileFunction` -with the fixed parameters `exports`, `module`, `binding` and `primordials`: +with the fixed parameters `exports`, `require`, `module`, `binding` and +`primordials`: ```js const { someNative, anotherNative } = binding; const { ArrayPrototypeSlice, ObjectCreate } = primordials; +const { inspect } = require("ns:util"); module.exports = somethingTheCallSiteNeeds; ``` - `binding` is a plain object of natives built by the C++ call site; a file that needs nothing from C++ simply doesn't mention it. +- `require` resolves **builtin specifiers only** (`ns:util`, `node:util`, …), + never a path or a package; an unknown one throws + `No such built-in module: `. It is how a `node:` shim consumes the + `ns:` module it adapts, and it materializes that module on first use. + Requiring a module that is still loading throws rather than recursing. - `primordials` is the frozen intrinsics snapshot built by `primordials.js` (see below), the same object for every builtin in an isolate. - **`module.exports` is the export channel** — whatever it holds when the file @@ -35,6 +42,10 @@ module.exports = somethingTheCallSiteNeeds; internal `__inspect` global): budgeted output, no getter invocation, tamper-immune via primordials. Console routes all object formatting through it. +- `ns-util.js` is the `ns:util` module app code requires and `node-util.js` the + `node:util` shim: one source file per specifier, the shim owning every bit of + Node compatibility. See `docs/ns-builtin-modules.md` for the cross-runtime + contract. - Destructure `binding` and `primordials` once, at the top of the file, so the file's dependencies are visible and greppable. @@ -46,8 +57,8 @@ module.exports = somethingTheCallSiteNeeds; (`URLSearchParams`, …) capture it into a file-level `const`. - No `import`/`export` — these are classic function bodies, not modules. - ESLint (`eslint.config.mjs` at the repo root, run by lint-staged) declares - `exports`, `module`, `binding`, `primordials` and the reachable native - globals; `no-undef` is the typo net. If a builtin starts using a new native + `exports`, `require`, `module`, `binding`, `primordials` and the reachable + native globals; `no-undef` is the typo net. If a builtin starts using a new native global, add it there. `no-restricted-properties` fails the lint on direct use of the captured statics (`JSON.stringify`, `Object.defineProperty`, …). Uncurried instance methods can't be matched that way, so `list.slice()` diff --git a/NativeScript/runtime/js/node-util.js b/NativeScript/runtime/js/node-util.js new file mode 100644 index 00000000..a89c2826 --- /dev/null +++ b/NativeScript/runtime/js/node-util.js @@ -0,0 +1,16 @@ +"use strict"; + +// The `node:util` compatibility shim: a documented subset of Node's util, +// backed by `ns:util` (docs/ns-builtin-modules.md). Compiled the first time +// `node:util` is resolved, so an app that never touches the `node:` scheme +// never pays for it. +// +// Every adaptation to Node's API belongs in this file — argument shapes, +// option names, aliases, anything that has to track Node rather than the +// runtime. The `ns:` modules stay free of compatibility knowledge, so the two +// surfaces can diverge without either one carrying the other's baggage. + +const { ObjectFreeze } = primordials; +const { inspect, format } = require("ns:util"); + +module.exports = ObjectFreeze({ inspect, format }); diff --git a/NativeScript/runtime/js/ns-util.js b/NativeScript/runtime/js/ns-util.js new file mode 100644 index 00000000..edc47327 --- /dev/null +++ b/NativeScript/runtime/js/ns-util.js @@ -0,0 +1,151 @@ +"use strict"; + +// The `ns:util` builtin module. See docs/ns-builtin-modules.md for the +// cross-runtime contract. `inspect` is created once per realm by +// Console::InitInspect and handed in here, so the module and console share one +// formatter instance. + +const { inspect } = binding; +const { + JSONStringify, + Number, + NumberParseFloat, + NumberParseInt, + ObjectFreeze, + ObjectIs, + String, + StringPrototypeCharCodeAt, + StringPrototypeSlice, +} = primordials; + +const CHAR_PERCENT = 37; +const CHAR_UPPERCASE_O = 79; +const CHAR_LOWERCASE_D = 100; +const CHAR_LOWERCASE_F = 102; +const CHAR_LOWERCASE_I = 105; +const CHAR_LOWERCASE_J = 106; +const CHAR_LOWERCASE_O = 111; +const CHAR_LOWERCASE_S = 115; + +function formatNumber(value) { + return ObjectIs(value, -0) ? "-0" : String(value); +} + +// %d and %i keep a bigint's "n" suffix (%f, which parses a float, does not). +// Symbols refuse numeric conversion, so they render as NaN rather than throw. +function formatInteger(value, toNumber) { + if (typeof value === "symbol") { + return "NaN"; + } + if (typeof value === "bigint") { + return String(value) + "n"; + } + return formatNumber(toNumber(value)); +} + +function stringify(value) { + try { + return JSONStringify(value); + } catch (ignored) { + // Circular structures are the common case; anything else JSON refuses + // (a throwing toJSON) is not worth taking the log call down for. + return "[Circular]"; + } +} + +// A value spliced in for %s: strings go in raw, everything else through +// inspect (which already honors custom toString overrides). +function toDisplayString(value) { + return typeof value === "string" ? value : inspect(value); +} + +function format(...args) { + const first = args[0]; + let str = ""; + let a = 0; + let join = ""; + + if (typeof first === "string") { + if (args.length === 1) { + return first; + } + let lastPos = 0; + // A trailing "%" cannot start a substitution, hence length - 1. + for (let i = 0; i < first.length - 1; i++) { + if (StringPrototypeCharCodeAt(first, i) !== CHAR_PERCENT) { + continue; + } + const nextChar = StringPrototypeCharCodeAt(first, ++i); + if (a + 1 === args.length) { + // Out of arguments: only %% still means something. + if (nextChar === CHAR_PERCENT) { + str += StringPrototypeSlice(first, lastPos, i); + lastPos = i + 1; + } + continue; + } + let replacement; + switch (nextChar) { + case CHAR_LOWERCASE_S: + replacement = toDisplayString(args[++a]); + break; + case CHAR_LOWERCASE_D: { + const value = args[++a]; + replacement = formatInteger(value, Number); + break; + } + case CHAR_LOWERCASE_I: { + const value = args[++a]; + replacement = formatInteger(value, NumberParseInt); + break; + } + case CHAR_LOWERCASE_F: { + const value = args[++a]; + replacement = typeof value === "symbol" ? "NaN" : formatNumber(NumberParseFloat(value)); + break; + } + case CHAR_LOWERCASE_J: + replacement = stringify(args[++a]); + break; + case CHAR_LOWERCASE_O: + case CHAR_UPPERCASE_O: + replacement = inspect(args[++a]); + break; + case CHAR_PERCENT: + str += StringPrototypeSlice(first, lastPos, i); + lastPos = i + 1; + continue; + default: + // Not a placeholder: "100%" and friends survive verbatim. + continue; + } + if (lastPos !== i - 1) { + str += StringPrototypeSlice(first, lastPos, i - 1); + } + str += replacement; + lastPos = i + 1; + } + if (lastPos !== 0) { + // The format string consumed argument 0 and set the separator for the + // extras that follow it. + a++; + join = " "; + if (lastPos < first.length) { + str += StringPrototypeSlice(first, lastPos); + } + } + } + + // Leftovers (or every argument, when there was no format string) are + // appended space-separated. + while (a < args.length) { + const value = args[a]; + str += join + toDisplayString(value); + join = " "; + a++; + } + + return str; +} + +module.exports = ObjectFreeze({ inspect, format }); diff --git a/NativeScript/runtime/js/primordials.js b/NativeScript/runtime/js/primordials.js index e8db4f68..368d2a26 100644 --- a/NativeScript/runtime/js/primordials.js +++ b/NativeScript/runtime/js/primordials.js @@ -23,6 +23,7 @@ const intrinsics = { // Constructors and well-known symbols. Error, Map, + Number, Proxy, Set, String, @@ -36,6 +37,8 @@ const intrinsics = { ArrayBufferIsView: ArrayBuffer.isView, ArrayIsArray: Array.isArray, JSONStringify: JSON.stringify, + NumberParseFloat: Number.parseFloat, + NumberParseInt: Number.parseInt, ObjectAssign: Object.assign, ObjectCreate: Object.create, ObjectDefineProperty: Object.defineProperty, @@ -73,6 +76,7 @@ const intrinsics = { SetPrototypeDelete: uncurryThis(Set.prototype.delete), SetPrototypeHas: uncurryThis(Set.prototype.has), SetPrototypeValues: uncurryThis(Set.prototype.values), + StringPrototypeCharCodeAt: uncurryThis(String.prototype.charCodeAt), StringPrototypeIndexOf: uncurryThis(String.prototype.indexOf), StringPrototypeSlice: uncurryThis(String.prototype.slice), StringPrototypeToLowerCase: uncurryThis(String.prototype.toLowerCase), diff --git a/TestRunner/app/tests/NsUtilTests.js b/TestRunner/app/tests/NsUtilTests.js new file mode 100644 index 00000000..3096d14f --- /dev/null +++ b/TestRunner/app/tests/NsUtilTests.js @@ -0,0 +1,176 @@ +describe("ns:util", function () { + var util = require("ns:util"); + + it("exports a frozen inspect/format pair", function () { + expect(Object.isFrozen(util)).toBe(true); + expect(typeof util.inspect).toBe("function"); + expect(typeof util.format).toBe("function"); + expect(util.inspect({ a: 1 })).toBe("{ a: 1 }"); + }); + + it("is a singleton per realm", function () { + expect(require("ns:util")).toBe(util); + }); + + it("throws for an unknown builtin", function () { + expect(function () { + require("ns:definitely-not-a-module"); + }).toThrow(); + + var error; + try { + require("ns:definitely-not-a-module"); + } catch (e) { + error = e; + } + expect(error instanceof Error).toBe(true); + expect(error.message).toBe("No such built-in module: ns:definitely-not-a-module"); + }); + + it("leaves bare specifiers to npm resolution", function () { + var resolved; + try { + resolved = require("util"); + } catch (e) { + resolved = undefined; + } + expect(resolved).not.toBe(util); + expect(resolved && resolved.format).not.toBe(util.format); + }); + + describe("format", function () { + var format = util.format; + + it("substitutes %s, %d, %i and %f", function () { + expect(format("%s", "a")).toBe("a"); + expect(format("%s", 3)).toBe("3"); + expect(format("%s", { a: 1 })).toBe("{ a: 1 }"); + expect(format("%d apples", 3)).toBe("3 apples"); + expect(format("%d", "4.7")).toBe("4.7"); + expect(format("%i", "4.7")).toBe("4"); + expect(format("%f", "4.7")).toBe("4.7"); + }); + + it("substitutes %j, %o and %O", function () { + expect(format("%j", { a: 1 })).toBe('{"a":1}'); + expect(format("%o", { a: 1 })).toBe("{ a: 1 }"); + expect(format("%O", { a: 1 })).toBe("{ a: 1 }"); + }); + + it("renders a circular value for %j as [Circular]", function () { + var circular = {}; + circular.self = circular; + expect(format("%j", circular)).toBe("[Circular]"); + }); + + it("collapses %% and leaves unknown or dangling % verbatim", function () { + expect(format("100%")).toBe("100%"); + expect(format("100%", "x")).toBe("100% x"); + expect(format("%% done", 1)).toBe("% done 1"); + expect(format("a %x b", 1)).toBe("a %x b 1"); + expect(format("trailing %", 1)).toBe("trailing % 1"); + expect(format("%s %s", "a")).toBe("a %s"); + }); + + it("appends extra arguments space-separated", function () { + expect(format("%s", "a", "b", { c: 1 })).toBe("a b { c: 1 }"); + }); + + it("joins every argument when the first is not a format string", function () { + expect(format({ a: 1 }, "x")).toBe("{ a: 1 } x"); + expect(format(1, 2)).toBe("1 2"); + expect(format()).toBe(""); + }); + }); + + it("backs console.* formatting", function () { + expect(function () { + console.log("%d apples", 3); + console.log("100%"); + }).not.toThrow(); + }); + + it("resolves through dynamic import with the same members", function (done) { + import("ns:util").then(function (ns) { + expect(ns.default).toBe(util); + expect(ns.inspect).toBe(util.inspect); + expect(ns.format).toBe(util.format); + // The synthetic module is cached per realm. + return import("ns:util").then(function (again) { + expect(again).toBe(ns); + done(); + }); + }, function (error) { + fail("import('ns:util') should resolve: " + error); + done(); + }); + }); + + it("rejects a dynamic import of an unknown builtin", function (done) { + import("ns:definitely-not-a-module").then(function () { + fail("import of an unknown builtin should reject"); + done(); + }, function (error) { + expect(error instanceof Error).toBe(true); + expect(error.message).toBe("No such built-in module: ns:definitely-not-a-module"); + done(); + }); + }); + + it("resolves through a static import", function (done) { + import("~/tests/esm/ns-util-import.mjs").then(function (module) { + expect(module.nsDefault).toBe(util); + expect(module.inspect).toBe(util.inspect); + expect(module.format).toBe(util.format); + expect(module.nodeDefault).toBe(require("node:util")); + expect(module.nodeInspect).toBe(util.inspect); + done(); + }, function (error) { + fail("static import of ns:util should resolve: " + error); + done(); + }); + }); +}); + +describe("node:util", function () { + var util = require("ns:util"); + var nodeUtil = require("node:util"); + + it("is a distinct module object sharing ns:util's members", function () { + expect(nodeUtil).not.toBe(util); + expect(Object.isFrozen(nodeUtil)).toBe(true); + expect(nodeUtil.inspect).toBe(util.inspect); + expect(nodeUtil.format).toBe(util.format); + }); + + it("is a singleton per realm", function () { + expect(require("node:util")).toBe(nodeUtil); + }); + + it("only exposes what the shim implements", function () { + expect(typeof nodeUtil.promisify).toBe("undefined"); + }); + + it("throws for a node builtin with no shim", function () { + var error; + try { + require("node:fs"); + } catch (e) { + error = e; + } + expect(error instanceof Error).toBe(true); + expect(error.message).toBe("No such built-in module: node:fs"); + }); + + it("resolves through dynamic import with the same members", function (done) { + import("node:util").then(function (ns) { + expect(ns.default).toBe(nodeUtil); + expect(ns.inspect).toBe(util.inspect); + expect(ns.format).toBe(util.format); + done(); + }, function (error) { + fail("import('node:util') should resolve: " + error); + done(); + }); + }); +}); diff --git a/TestRunner/app/tests/esm/ns-util-import.mjs b/TestRunner/app/tests/esm/ns-util-import.mjs new file mode 100644 index 00000000..00cb7b01 --- /dev/null +++ b/TestRunner/app/tests/esm/ns-util-import.mjs @@ -0,0 +1,6 @@ +// Static `import` of the builtins, so the ES module resolve callback is +// exercised too (dynamic import() has its own fast path). +import nsDefault, { inspect, format } from "ns:util"; +import nodeDefault, { inspect as nodeInspect } from "node:util"; + +export { nsDefault, inspect, format, nodeDefault, nodeInspect }; diff --git a/TestRunner/app/tests/index.js b/TestRunner/app/tests/index.js index 5ad49d89..bc58e816 100644 --- a/TestRunner/app/tests/index.js +++ b/TestRunner/app/tests/index.js @@ -164,6 +164,9 @@ require("./EscapeExceptionTests"); require("./PrimordialsTests"); require("./InspectTests"); +// The ns:/node: builtin modules +require("./NsUtilTests"); + // Tests common for all runtimes (git submodule of NativeScript/common-runtime-tests-app). require("../shared/index").runAllTests(); diff --git a/docs/ns-builtin-modules.md b/docs/ns-builtin-modules.md new file mode 100644 index 00000000..de6634c7 --- /dev/null +++ b/docs/ns-builtin-modules.md @@ -0,0 +1,148 @@ +# `ns:` builtin modules + +Cross-runtime contract for exposing runtime-provided modules to application +code. This document is the specification both the iOS and Android runtimes +implement; a capability must behave identically on both platforms before it +ships in a stable release. + +## The scheme + +Builtin modules live under the URL-style `ns:` scheme, mirroring Node's +`node:` prefix: + +```js +// CommonJS +const util = require("ns:util"); + +// ES modules +import util, { inspect } from "ns:util"; +const util2 = await import("ns:util"); +``` + +Rules: + +- `ns:` specifiers are resolved by the runtime **before any filesystem or + npm resolution**. They can never be shadowed by a file, a path mapping, or + a package — and conversely, a file named `ns:util` is not reachable. +- Resolution of an unknown builtin fails synchronously with an `Error` whose + message is exactly `No such built-in module: ns:` (matching Node's + wording for familiarity). +- A builtin module is a **singleton per JS realm** (main context and each + worker get their own instance). `require("ns:util")` twice returns the same + object; the CJS exports object and the ESM namespace expose the same + underlying values (ESM additionally provides the exports object as + `default`). +- There is no bare-specifier fallback: `require("util")` is **not** an alias + for `require("ns:util")`. The unprefixed name continues to resolve to npm + packages as it always has. +- Builtin exports are frozen. Apps patch behavior by wrapping, not by + mutating the runtime's module. + +## Modules + +### `ns:util` (v1) + +| export | description | +|---|---| +| `inspect(value[, options])` | Formats any value for human consumption: depth-limited, output-capped, cycle-safe, never invokes getters (except a guarded `error.stack` read and custom `toString` overrides, which are honored). `options.depth` (number) overrides the default depth of 2. Other option keys are reserved. | +| `format(fmt, ...args)` | Node-style printf formatting: `%s`, `%d`, `%i`, `%f`, `%j`, `%o`, `%O`, `%%`. Extra arguments are appended space-separated, objects rendered via `inspect`. When `fmt` is not a string or contains no substitutions, all arguments are formatted and joined with spaces. `console.*` routes its arguments through this, so `console.log("%d apples", 3)` works. | + +**Stability caveat (verbatim from Node's contract):** the output of `inspect` +(and therefore `format`'s object rendering) may change between runtime +versions for readability; it is intended for humans and must not be parsed +programmatically. + +## `node:` compatibility shims + +The same registry serves the `node:` scheme with **compatibility shims** so +npm packages that require Node builtins by their prefixed names can run +unmodified where a shim exists: + +- A shim implements a documented **subset** of the corresponding Node module's + API, backed by `ns:` modules. Unimplemented members are simply absent + (so `typeof util.promisify === "function"` feature-checks behave + correctly); they are never present-but-throwing. +- **One source file per specifier.** A shim is its own module that consumes + the `ns:` module it adapts through the internal require, and it owns *all* + the adaptation — argument shapes, option names, aliases, anything that has + to track Node. A standard `ns:` module never contains compatibility code + and never knows a shim exists. +- Shims are **lazy**: a shim's source is only evaluated when its specifier is + first resolved, so an app that never touches the `node:` scheme never pays + for one. +- `node:` modules with no shim fail with the same error shape: + `No such built-in module: node:`. +- **Bare specifiers are untouched**: `require("util")` resolves through npm + as it always has (many apps bundle the `util` polyfill package). Only the + explicit `node:` prefix reaches the shim registry, so existing apps cannot + break. Bundler-level aliases (webpack/rollup) continue to work and take + precedence at build time. +- A shim is always a **distinct module object** from any `ns:` module, even + when every member is re-exported unchanged. `ns:` modules may grow runtime-specific + members freely; a `node:` shim only ever gains members that track Node's + actual API. This mirrors how Bun (`bun:*`), Deno (`Deno.*`/JSR) and + Cloudflare (`cloudflare:*`) all keep their own surface strictly apart from + their `node:` compat layer. +- Shims ship on both runtimes under the same parity rule as `ns:` modules. + +### v1 shims + +| module | exports | notes | +|---|---|---| +| `node:util` | `inspect`, `format` | Re-exports `ns:util`'s members unchanged (`nodeUtil.inspect === nsUtil.inspect`) from a **distinct, separately frozen module object**. Documented as partial. | + +Candidates for future shims, in rough order of ecosystem demand: +`node:events` (EventEmitter), `node:path` (pure JS), `node:buffer`, +`node:process` (subset). Each requires a spec update here first. + +## The internal require + +Builtin modules reach each other — and only each other — through an internal +`require` the runtime provides to every builtin source: + +- It resolves **builtin specifiers only**. A path, a package name or any other + specifier is not reachable from a builtin; an unregistered builtin name + throws the same `No such built-in module: ` an app sees. +- It materializes the target module on first use and returns the realm's + singleton afterwards, which is what makes shims lazy. +- Requiring a module that is still being built throws rather than recursing, + so a dependency cycle between builtins is a loud error and not a hang. + +This is the mechanism shims are built on, so it is normative: both runtimes +provide it. + +## Adding a builtin module + +- The name is a short, lowercase identifier (`ns:util`, `ns:timers`, ...). +- One module, one source file, one registry entry — including shims. +- New modules and new exports require this document to be updated first and + an implementation on both runtimes before a stable release; a module may + ship on one platform behind a documented "experimental, iOS-only" (or + Android-only) note in between. +- Internal runtime machinery must never be reachable through the scheme: + the registry distinguishes public modules from internal builtins, and only + public ones resolve (Node's `canBeRequiredByUsers` split). + +## Source-text modules: deliberately not supported + +Builtins are classic function bodies, not ES modules, on both runtimes. If +cross-builtin code sharing is ever needed, the first answer is bundling at +generation time (author as ESM, emit function bodies); runtime source-text +builtin modules (Node's `kSourceTextModule`) are justified only by a concrete +need for live module semantics (TLA, live bindings, cyclic imports), which no +current or planned builtin has. Revisit here before building either. + +## iOS implementation notes (non-normative) + +Builtin modules are function-body builtins (`NativeScript/runtime/js/`, +see the README there) compiled via the RuntimeBuiltins table. The `ns:` +resolver intercepts specifiers in the CommonJS require path and in the ES +module resolve/dynamic-import callbacks; ESM consumption is served by a +synthetic module whose exports are populated from the same per-realm exports +object. The internal require is a fixed parameter of the builtin function +wrapper (`exports`, `require`, `module`, `binding`, `primordials`). + +iOS also keeps a pre-registry `node:url` polyfill (`fileURLToPath`, +`pathToFileURL`) that predates this document. It is compiled from module +source inside the ES module resolver and is therefore reachable through +`import` only, not through `require()`. diff --git a/eslint.config.mjs b/eslint.config.mjs index b9715242..5eb59bfa 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -15,6 +15,8 @@ const capturedStatics = [ ['Array', 'isArray', 'ArrayIsArray'], ['ArrayBuffer', 'isView', 'ArrayBufferIsView'], ['JSON', 'stringify', 'JSONStringify'], + ['Number', 'parseFloat', 'NumberParseFloat'], + ['Number', 'parseInt', 'NumberParseInt'], ['Object', 'assign', 'ObjectAssign'], ['Object', 'create', 'ObjectCreate'], ['Object', 'defineProperty', 'ObjectDefineProperty'], @@ -38,7 +40,7 @@ const capturedStatics = [ // fills in after init). Array.from has no primordial: copying `arguments` goes // through an index loop instead, because Array.from depends on the tamperable // array iterator protocol. -const restrictedGlobals = ['Error', 'Map', 'Proxy', 'Set', 'String', 'TypeError'].map((name) => ({ +const restrictedGlobals = ['Error', 'Map', 'Number', 'Proxy', 'Set', 'String', 'TypeError'].map((name) => ({ name, message: `Destructure ${name} from primordials — builtins must not read intrinsics off globals user code can replace.`, })); @@ -60,6 +62,7 @@ export default [ globals: { ...globals.es2021, exports: 'readonly', + require: 'readonly', module: 'readonly', binding: 'readonly', primordials: 'readonly', diff --git a/tools/js2c-inputs.xcfilelist b/tools/js2c-inputs.xcfilelist index 5a6c9072..c52b9428 100644 --- a/tools/js2c-inputs.xcfilelist +++ b/tools/js2c-inputs.xcfilelist @@ -6,6 +6,8 @@ $(SRCROOT)/NativeScript/runtime/js/events.js $(SRCROOT)/NativeScript/runtime/js/inline-functions.js $(SRCROOT)/NativeScript/runtime/js/primordials.js $(SRCROOT)/NativeScript/runtime/js/inspect.js +$(SRCROOT)/NativeScript/runtime/js/node-util.js +$(SRCROOT)/NativeScript/runtime/js/ns-util.js $(SRCROOT)/NativeScript/runtime/js/promise-proxy.js $(SRCROOT)/NativeScript/runtime/js/require-factory.js $(SRCROOT)/NativeScript/runtime/js/ts-helpers.js diff --git a/tools/js2c.mjs b/tools/js2c.mjs index fbb0dde7..cbe7964f 100644 --- a/tools/js2c.mjs +++ b/tools/js2c.mjs @@ -77,6 +77,11 @@ for (const file of inputs) { console.error(`error: no such file: ${file}`); process.exit(1); } + if (!file.endsWith('.js')) { + console.error(`error: builtins must be .js function bodies, got: ${file}`); + console.error('(.mjs/source-text modules are deliberately unsupported — see docs/ns-builtin-modules.md)'); + process.exit(1); + } const stem = basename(file).replace(/\.js$/, ''); if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(stem)) { console.error(`error: file name must be kebab-case .js: ${file}`); diff --git a/v8ios.xcodeproj/project.pbxproj b/v8ios.xcodeproj/project.pbxproj index ca428152..67b56f0a 100644 --- a/v8ios.xcodeproj/project.pbxproj +++ b/v8ios.xcodeproj/project.pbxproj @@ -293,6 +293,7 @@ C2DDEB92229EAC8300345BFE /* WeakRef.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C2DDEB69229EAC8100345BFE /* WeakRef.cpp */; }; 4A5C201A2E2B000100000006 /* BuiltinLoader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A5C201A2E2B000100000001 /* BuiltinLoader.cpp */; }; 4A5C201A2E2B000100000007 /* RuntimeBuiltins.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A5C201A2E2B000100000003 /* RuntimeBuiltins.cpp */; }; + 4A5C201A2E2B000200000006 /* NsBuiltinModules.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A5C201A2E2B000200000001 /* NsBuiltinModules.cpp */; }; C2DDEB93229EAC8300345BFE /* ArgConverter.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2DDEB6A229EAC8200345BFE /* ArgConverter.mm */; }; C2DDEB94229EAC8300345BFE /* Console.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C2DDEB6B229EAC8200345BFE /* Console.cpp */; }; C2DDEB95229EAC8300345BFE /* SetTimeout.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C2DDEB6C229EAC8200345BFE /* SetTimeout.cpp */; }; @@ -809,6 +810,8 @@ C2DDEB69229EAC8100345BFE /* WeakRef.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = WeakRef.cpp; sourceTree = ""; }; 4A5C201A2E2B000100000001 /* BuiltinLoader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BuiltinLoader.cpp; sourceTree = ""; }; 4A5C201A2E2B000100000002 /* BuiltinLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BuiltinLoader.h; sourceTree = ""; }; + 4A5C201A2E2B000200000001 /* NsBuiltinModules.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = NsBuiltinModules.cpp; sourceTree = ""; }; + 4A5C201A2E2B000200000002 /* NsBuiltinModules.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NsBuiltinModules.h; sourceTree = ""; }; 4A5C201A2E2B000100000003 /* RuntimeBuiltins.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = RuntimeBuiltins.cpp; path = generated/RuntimeBuiltins.cpp; sourceTree = ""; }; 4A5C201A2E2B000100000004 /* RuntimeBuiltins.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RuntimeBuiltins.h; path = generated/RuntimeBuiltins.h; sourceTree = ""; }; 4A5C201A2E2B000100000005 /* js */ = {isa = PBXFileReference; lastKnownFileType = folder; path = js; sourceTree = ""; }; @@ -1526,6 +1529,8 @@ C2DDEB69229EAC8100345BFE /* WeakRef.cpp */, 4A5C201A2E2B000100000002 /* BuiltinLoader.h */, 4A5C201A2E2B000100000001 /* BuiltinLoader.cpp */, + 4A5C201A2E2B000200000002 /* NsBuiltinModules.h */, + 4A5C201A2E2B000200000001 /* NsBuiltinModules.cpp */, 4A5C201A2E2B000100000004 /* RuntimeBuiltins.h */, 4A5C201A2E2B000100000003 /* RuntimeBuiltins.cpp */, 4A5C201A2E2B000100000005 /* js */, @@ -2282,6 +2287,7 @@ 6573B9CE291FE29F00B0ED7C /* JSIV8ValueConverter.cpp in Sources */, C2DDEB92229EAC8300345BFE /* WeakRef.cpp in Sources */, 4A5C201A2E2B000100000006 /* BuiltinLoader.cpp in Sources */, + 4A5C201A2E2B000200000006 /* NsBuiltinModules.cpp in Sources */, 4A5C201A2E2B000100000007 /* RuntimeBuiltins.cpp in Sources */, C2A5F86A2359AEB600074AFA /* ExtVector.cpp in Sources */, AA8C47B22E27114300649BF5 /* ModuleInternalCallbacks.mm in Sources */,