feat: ns:util builtin module (inspect, format) - #418
Open
edusperoni wants to merge 2 commits into
Open
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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 re-exporting its members from a distinct, frozen
module object.
Each specifier is one source file and one registry entry: a shim is its
own builtin that reaches the `ns:` module it adapts through a new fixed
wrapper parameter, `require`, which resolves builtin specifiers and
nothing else. Shims therefore compile only when first resolved and own
every bit of Node adaptation, keeping compatibility knowledge out of the
standard modules.
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.
…s inputs Builtins stay classic function bodies on both runtimes; cross-builtin sharing, if ever needed, starts with generation-time bundling. The js2c guard turns an accidental .mjs into a build error pointing at the spec.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #416 (inspect). Review only the last commit; the base branch is
feat/inspect.What
Runtime-provided modules get a resolution scheme.
require("ns:util"),import util, { inspect } from "ns:util"andawait import("ns:util")all resolve to the same per-realm module, andconsole.*gains Node's%-substitution.v1 exports:
inspect(value[, options])format(fmt, ...args)util.format:%s %d %i %f %j %o %O %%, extras appended space-separateddocs/ns-builtin-modules.md(added here) is the cross-runtime contract — the same document the Android runtime implements against, so a capability behaves identically on both platforms before it ships in a stable release.Resolution rules
ns:/node:specifiers are intercepted before any filesystem or npm resolution, in all three entry points: the CommonJSrequirepath (ModuleInternal.mm:219), the ES module resolve callback (ModuleInternalCallbacks.mm:719) and the dynamic-import host callback (ModuleInternalCallbacks.mm:1878). A builtin can never be shadowed by a file or a package.v8::Module::CreateSyntheticModule, exporting every member by name plusdefault(the exports object). The module is instantiated, evaluated and cached per realm, so repeated imports return the identical namespace.No such built-in module: <specifier>— synchronously forrequire, as a rejection forimport().Caches, not in a process-global).node:shimsThe same registry serves
node:(spec: "node:compatibility shims"), so npm packages requiring Node builtins by their prefixed name can run where a shim exists.node:utilis its own builtin (node-util.js) that consumesns:utilthrough the internal require and re-exports its members. The shim owns all the Node adaptation — argument shapes, option names, aliases — sons-util.jscontains no compatibility code and does not know a shim exists. This is the reference pattern every futurens:*module and shim copies.node-util.jsis only compiled the first timenode:utilis resolved, so an app that never touches thenode:scheme never pays for it.node:utilis a distinct, separately frozen module object fromns:util, even though every member is re-exported unchanged (nodeUtil.inspect === nsUtil.inspect) — per the spec bullet, mirroring how Bun (bun:*), Deno and Cloudflare (cloudflare:*) keep their own surface apart from their Node compat layer.ns:utilcan grow runtime-specific exports without widening the Node compatibility surface.typeof util.promisify === "undefined"), never present-but-throwing, so feature checks work.require("util")still resolves through npm exactly as before (many apps bundle theutilpolyfill package). Only the explicit prefix reaches the registry. There is a test assertingrequire("util")does not return the builtin.node:ES imports previously got a generic in-memory polyfill (console.warn(...); export default {}) that broke at first use; they now fail withNo such built-in module: node:<name>. The pre-existingnode:urlpolyfill (fileURLToPath/pathToFileURL) is kept as-is and is still import-only; it is noted in the doc's non-normative iOS section.The internal
require(new wrapper parameter)The builtin function wrapper becomes
(exports, require, module, binding, primordials)— Node's order.requireis created once per realm byBuiltinLoaderand resolves builtin specifiers only: no path, no package, no filesystem. An unregistered name throws exactlyNo such built-in module: <specifier>, and requiring a module that is still loading throws instead of recursing (per-realm in-progress guard). It is documented as normative in the spec, since Android needs it to build shims the same way. C++ call sites are unchanged; builtins that don't need it simply ignore the parameter.console integration
Console::BuildStringFromArgscalls the cachedformatwith the full argument list, soconsole.log("%d apples", 3)prints3 appleswhileconsole.log("100%"), unknown specifiers and a dangling%stay verbatim. Non-format calls keep joining with spaces exactly as before. If the builtin is unavailable the old per-argument loop still runs, so logging can never be taken down by the formatter.Implementation
NativeScript/runtime/js/ns-util.js— thens:utilbuiltin, all intrinsic access through primordials (Number,NumberParseInt,NumberParseFloat,StringPrototypeCharCodeAtadded, ESLint lists kept in sync).inspectis passed in viabinding, so the module and console share one formatter instance.NativeScript/runtime/js/node-util.js— the shim, four lines of re-export plus the rule about where adaptation code lives.NativeScript/runtime/NsBuiltinModules.{h,cpp}— the registry (one specifier → oneBuiltinId), lazy per-realm materialization with a cycle guard, and the synthetic-module evaluation steps. Adding a module is one row plus one file.Stability caveat
Verbatim from Node's contract, and repeated in the doc:
inspect's output (and thereforeformat's object rendering) may change between runtime versions for readability. It is for humans and must not be parsed programmatically.Tests
TestRunner/app/tests/NsUtilTests.js(+ an.mjsfixture that statically imports both specifiers, so the resolve callback is covered and not just the dynamic-import fast path): frozen exports, singleton identity, every format specifier,%%/unknown/dangling%,%jon a circular value, extras, non-string first argument,console.log("%d apples", 3), dynamic + static import identity,node:utildistinctness with shared members, unknownns:/node:messages, and the bare-specifier non-change.Full suite on the simulator: 941 tests, 0 failures (922 baseline + 19 new).
formatwas additionally verified against Node's ownutil.formaton the host for every non-object case.