Skip to content

Commit 8625d84

Browse files
committed
feat: expose inspect and format as the ns:util builtin module
Runtime-provided modules now resolve under the URL-style `ns:` scheme, ahead of any filesystem or npm resolution, with `node:` compatibility shims served from the same registry. v1 ships `ns:util` (inspect, format) and a `node:util` shim that shares its members but is a distinct, frozen module object. format() is Node's util.format (%s %d %i %f %j %o %O %%, extras appended space-separated), and console.* routes its arguments through it, so `console.log("%d apples", 3)` works while unknown and dangling percent signs stay verbatim. Resolution is wired into the CommonJS require path, the ES module resolve callback and the dynamic-import callback; ESM consumption is served by a per-realm synthetic module. An unknown name in either scheme fails with `No such built-in module: <specifier>`, which replaces the warn-and-export- nothing polyfill previously handed to unshimmed `node:` imports. Bare specifiers are untouched. docs/ns-builtin-modules.md is the cross-runtime contract both runtimes implement.
1 parent f59753f commit 8625d84

17 files changed

Lines changed: 941 additions & 7 deletions

NativeScript/runtime/Caches.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ class Caches {
155155
// console formatter (internal/inspect.js), initialized by Console::Init.
156156
std::unique_ptr<v8::Persistent<v8::Function>> InspectFunc =
157157
std::unique_ptr<v8::Persistent<v8::Function>>(nullptr);
158+
// ns:util's format, used by console.* for %-substitution.
159+
std::unique_ptr<v8::Persistent<v8::Function>> FormatFunc =
160+
std::unique_ptr<v8::Persistent<v8::Function>>(nullptr);
161+
bool FormatFuncUnavailable = false;
158162
std::unique_ptr<v8::Persistent<v8::Function>> InteropReferenceCtorFunc =
159163
std::unique_ptr<v8::Persistent<v8::Function>>(nullptr);
160164
std::unique_ptr<v8::Persistent<v8::Function>> PointerCtorFunc =
@@ -164,6 +168,16 @@ class Caches {
164168
std::unique_ptr<v8::Persistent<v8::Function>> UnmanagedTypeCtorFunc =
165169
std::unique_ptr<v8::Persistent<v8::Function>>(nullptr);
166170

171+
// `ns:`/`node:` builtin modules (NsBuiltinModules), keyed by specifier. Both
172+
// are per isolate: a builtin module is a singleton per realm, so workers get
173+
// their own exports objects and their own synthetic modules.
174+
robin_hood::unordered_map<std::string,
175+
std::unique_ptr<v8::Persistent<v8::Object>>>
176+
BuiltinModuleExports;
177+
robin_hood::unordered_map<std::string,
178+
std::unique_ptr<v8::Persistent<v8::Module>>>
179+
BuiltinModules;
180+
167181
// Frozen intrinsics snapshot returned by internal/primordials.js, passed to
168182
// every builtin as its second fixed parameter (BuiltinLoader::RunBuiltin).
169183
// Per isolate, so workers snapshot their own realm's intrinsics.

NativeScript/runtime/Console.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "DataWrapper.h"
1212
#include "Helpers.h"
1313
#include "NativeScriptException.h"
14+
#include "NsBuiltinModules.h"
1415
#include "RuntimeConfig.h"
1516
// #include "v8-log-agent-impl.h"
1617
#include <sstream>
@@ -272,6 +273,30 @@ std::string Console::BuildStringFromArgs(
272273
Isolate* isolate = args.GetIsolate();
273274
Local<Context> context = isolate->GetCurrentContext();
274275
int argLen = args.Length();
276+
277+
// console.* follows Node: the arguments go through util.format, so the first
278+
// one may carry %-substitutions and the rest are appended space-separated.
279+
Local<v8::Function> format = argLen > startingIndex
280+
? NsBuiltinModules::GetFormatFunc(context)
281+
: Local<v8::Function>();
282+
if (!format.IsEmpty()) {
283+
std::vector<Local<Value>> formatArgs;
284+
formatArgs.reserve(argLen - startingIndex);
285+
for (int i = startingIndex; i < argLen; i++) {
286+
formatArgs.push_back(args[i]);
287+
}
288+
TryCatch tc(isolate);
289+
Local<Value> result;
290+
if (format
291+
->Call(context, v8::Undefined(isolate),
292+
static_cast<int>(formatArgs.size()), formatArgs.data())
293+
.ToLocal(&result) &&
294+
result->IsString()) {
295+
return tns::ToString(isolate, result.As<v8::String>());
296+
}
297+
}
298+
299+
// ns:util unavailable or the formatter threw: per-argument rendering.
275300
std::stringstream ss;
276301

277302
if (argLen > 0) {
@@ -327,6 +352,11 @@ static void GetNativeWrapperHintCallback(
327352

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

331361
Local<v8::Function> hintFunc;
332362
if (!v8::Function::New(context, GetNativeWrapperHintCallback)

NativeScript/runtime/Console.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ class Console {
1414
static void AttachInspectorClient(
1515
v8_inspector::JsV8InspectorClient* inspector);
1616
static void DetachInspectorClient();
17+
// Builds this realm's inspect function (Caches::InspectFunc) if it isn't
18+
// there yet. Public so ns:util can re-export the same instance.
19+
static void InitInspect(v8::Local<v8::Context> context);
1720

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

4042
static void SendToDevToolsFrontEnd(

NativeScript/runtime/ModuleInternal.mm

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "Helpers.h"
1212
#include "ModuleInternalCallbacks.h" // for ResolveModuleCallback
1313
#include "NativeScriptException.h"
14+
#include "NsBuiltinModules.h"
1415
#include "Runtime.h" // for GetAppConfigValue
1516
#include "RuntimeConfig.h"
1617

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

218+
// Builtin modules resolve before any path handling, so they can never be
219+
// shadowed by a file or a package, and an unknown one fails as a missing
220+
// builtin rather than as a missing file. Only prefixed specifiers get here:
221+
// a bare `util` still resolves through npm.
222+
if (info.Length() > 0 && info[0]->IsString()) {
223+
std::string specifier = tns::ToString(isolate, info[0].As<v8::String>());
224+
if (NsBuiltinModules::IsBuiltinScheme(specifier)) {
225+
Local<Context> context = isolate->GetCurrentContext();
226+
Local<Object> exports;
227+
if (NsBuiltinModules::GetExports(context, specifier).ToLocal(&exports)) {
228+
info.GetReturnValue().Set(exports);
229+
} else if (!NsBuiltinModules::IsRegistered(specifier)) {
230+
isolate->ThrowException(Exception::Error(
231+
tns::ToV8String(isolate, NsBuiltinModules::NotFoundMessage(specifier))));
232+
}
233+
return;
234+
}
235+
}
236+
217237
// Declare these outside try block so they're available in catch
218238
std::string moduleName;
219239
std::string callingModuleDirName;

NativeScript/runtime/ModuleInternalCallbacks.mm

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "Helpers.h" // for tns::Exists
1717
#include "ModuleInternal.h" // for LoadScript(...)
1818
#include "NativeScriptException.h"
19+
#include "NsBuiltinModules.h"
1920
#include "Runtime.h" // for GetAppConfigValue
2021
#include "RuntimeConfig.h"
2122

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

719+
// Builtin modules resolve before any path handling. Unshimmed "node:" names
720+
// fall through to the legacy node:url polyfill below.
721+
if (NsBuiltinModules::IsRegistered(rawSpec) || NsBuiltinModules::IsNsScheme(rawSpec)) {
722+
v8::Local<v8::Module> builtin;
723+
if (NsBuiltinModules::GetModule(context, rawSpec).ToLocal(&builtin)) {
724+
return v8::MaybeLocal<v8::Module>(builtin);
725+
}
726+
if (!NsBuiltinModules::IsRegistered(rawSpec)) {
727+
isolate->ThrowException(v8::Exception::Error(
728+
tns::ToV8String(isolate, NsBuiltinModules::NotFoundMessage(rawSpec))));
729+
}
730+
return v8::MaybeLocal<v8::Module>();
731+
}
732+
718733
std::string normalizedSpec = rawSpec;
719734

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

13541367
v8::MaybeLocal<v8::Module> m =
@@ -1862,6 +1875,29 @@ static bool IsDocumentsPath(const std::string& path) {
18621875
// Normalize spec: expand '@/'; only strip ?query/hash for non-HTTP specs so SFC HTTP keys keep
18631876
// version tags
18641877
std::string rawSpec = cSpec ? std::string(cSpec) : std::string();
1878+
1879+
// Builtin modules never reach the loader below; the namespace comes straight
1880+
// from the realm's synthetic module.
1881+
if (NsBuiltinModules::IsRegistered(rawSpec) || NsBuiltinModules::IsNsScheme(rawSpec)) {
1882+
v8::EscapableHandleScope builtinScope(isolate);
1883+
v8::Local<v8::Promise::Resolver> builtinResolver;
1884+
if (!v8::Promise::Resolver::New(context).ToLocal(&builtinResolver)) {
1885+
return v8::MaybeLocal<v8::Promise>();
1886+
}
1887+
v8::TryCatch tc(isolate);
1888+
v8::Local<v8::Module> builtin;
1889+
if (NsBuiltinModules::GetModule(context, rawSpec).ToLocal(&builtin)) {
1890+
builtinResolver->Resolve(context, builtin->GetModuleNamespace()).FromMaybe(false);
1891+
} else {
1892+
v8::Local<v8::Value> error = tc.HasCaught()
1893+
? tc.Exception()
1894+
: v8::Exception::Error(tns::ToV8String(
1895+
isolate, NsBuiltinModules::NotFoundMessage(rawSpec)));
1896+
builtinResolver->Reject(context, error).FromMaybe(false);
1897+
}
1898+
return builtinScope.Escape(builtinResolver->GetPromise());
1899+
}
1900+
18651901
std::string normalizedSpec = rawSpec;
18661902
// remove query/hash ONLY for non-HTTP specs
18671903
bool isHttpLike = (!normalizedSpec.empty() && (StartsWith(normalizedSpec, "http://") ||

0 commit comments

Comments
 (0)