Skip to content

feat(runtime): ns:url builtin module mirroring node:url, with URL types - #497

Open
edusperoni wants to merge 2 commits into
feat/ns-worker-modulefrom
feat/ns-url-module
Open

edusperoni wants to merge 2 commits into
feat/ns-worker-modulefrom
feat/ns-url-module

Conversation

@edusperoni

@edusperoni edusperoni commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #494 (base branch feat/ns-worker-module), which is stacked on #493; only the last two commits are this PR.

What

A new ns:url builtin whose surface is exactly node:url's: the runtime's own URL and URLSearchParams constructors plus the fileURLToPath/pathToFileURL converters. The standard module now owns the converters (moved verbatim from node-url.js, same Node error messages), and node:url becomes a thin re-export shim over it on a distinct, separately frozen module object, the way node:util sits over ns:util.

import { URL, fileURLToPath, pathToFileURL } from "ns:url";

pathToFileURL("/app/my file.js").href; // "file:///app/my%20file.js"
fileURLToPath(new URL("file:///app/a.js?v=2")); // "/app/a.js"

Why a module

URL and URLSearchParams are globals whose types come from whichever lib a program includes. A specifier gives typed code one fixed place to name them. The binding takes both constructors from the templates URLImpl::Init / URLSearchParamsImpl::Init installed, held per isolate in a Caches::StateFor slot exactly like Worker::Constructor in #494. V8 instantiates a template once per context, so the module hands out the very functions the globals were created with, even after an app reassigns globalThis.URL.

Scope decision

The export set is URL, URLSearchParams, fileURLToPath, pathToFileURL, identical to node:url's, so the shim re-exports all four unchanged (nodeUrl.fileURLToPath === nsUrl.fileURLToPath). As a result require("node:url") gains URL and URLSearchParams, matching Node's node:url; the converters behave exactly as before. URLPattern stays a global only: Node's node:url has no counterpart, and nothing in the module needs it. It can join ns:url later the same way ns:util grew TextEncoder.

Types

types/ns-url.d.ts declares the module with standalone URL and URLSearchParams interfaces limited to the members this runtime implements (no toJSON, no static URL.parse; createObjectURL/revokeObjectURL and searchParams are included because blob-url.js installs them), plus the two converters. At script level it does what @types/node does for its own globals:

  • interface URL extends NsURL {} / interface URLSearchParams extends NsURLSearchParams {} merges, written inside the module's global {} block with a self-import, since an interface cannot extend an import() type;
  • declare var URL / declare var URLSearchParams through the typeof globalThis extends { onmessage: any; URL: infer T } probe, so a program with a DOM lib keeps the lib's declarations and one without gets the module's constructors.

Verified with tsc --strict against a DOM program and a non-DOM program, each constructing both classes through the module and through the globals, with @ts-expect-error checks on wrong argument types: both are clean. The non-DOM program no longer reports the two pre-existing Cannot find name 'URL' errors in ns-module.d.ts, because the global URL interface now exists there.

The DOM program also assigns pathToFileURL()'s result to the global URL type, which holds because of the toJSON the second commit adds.

Runtime gaps closed (second commit)

  • URL gains toJSON, registered next to toString with the same callback, so JSON.stringify(url) gives the href string as WHATWG requires, and a module URL is assignable to lib.dom's URL.
  • The searchParams sync wrapper in blob-url.js forwarded only the name to the native delete, so url.searchParams.delete(name, value) removed every pair with that name. It now forwards the value whenever one was passed (by arity, so an explicit undefined still reaches the native coercion).

Both have specs, in URL.js and UrlSearchParamsTests.js.

Docs and tests

docs/ns-builtin-modules.md gains the ns:url reference section (marked experimental, iOS-only until Android ships it; it now carries the converter semantics and error-message table), the public-row count goes to nine, and the node:url shims row says it re-exports all four members from ns:url, still without legacy url.parse/format/resolve or URLPattern.

NsUrlTests.js pins the frozen export set, identity with the globals, URLPattern staying out, the per-realm singleton, the converters returning the module's own URL, the shim being a distinct frozen object whose every member is the ns:url one, and, through a worker fixture that reassigns globalThis.URL before its first require("ns:url"), that the module still exports the original constructor. The existing node:url export-set assertion grows the two names.

Full suite on a dedicated iOS 18.5 simulator, with both commits: 1764 test cases, 0 failures.

@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 31 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 51caa810-4e29-4a80-ad9a-3f8ffb686ac9
📥 Commits

Reviewing files that changed from the base of the PR and between a38ff54 and aae08b4.

📒 Files selected for processing (18)
  • NativeScript/runtime/NsBuiltinModules.cpp
  • NativeScript/runtime/URLImpl.cpp
  • NativeScript/runtime/URLImpl.h
  • NativeScript/runtime/URLSearchParamsImpl.cpp
  • NativeScript/runtime/URLSearchParamsImpl.h
  • NativeScript/runtime/js/blob-url.js
  • NativeScript/runtime/js/node-url.js
  • NativeScript/runtime/js/ns-url.js
  • TestRunner/app/tests/NodeBuiltinsAndOptionalModulesTests.mjs
  • TestRunner/app/tests/NsUrlTests.js
  • TestRunner/app/tests/URL.js
  • TestRunner/app/tests/UrlSearchParamsTests.js
  • TestRunner/app/tests/index.js
  • TestRunner/app/tests/nsUrlIdentityWorker.js
  • docs/ns-builtin-modules.md
  • tools/js2c-inputs.xcfilelist
  • types/index.d.ts
  • types/ns-url.d.ts
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@edusperoni
edusperoni added this pull request to stack #495 October 8, 2026 19:29
@edusperoni
edusperoni marked this pull request as ready for review October 8, 2026 19:33
Adds `ns:url`, exporting the runtime's own URL and URLSearchParams plus the
`fileURLToPath`/`pathToFileURL` converters, so typed code can name the URL
classes by specifier instead of through whatever `globalThis.URL` resolves to
in the program's type environment. Its surface is exactly `node:url`'s: the
standard module now owns the converters, and `node:url` becomes a re-export
shim over it on a distinct, separately frozen module object, the way
`node:util` sits over `ns:util`. `require("node:url")` behaves as before and
additionally exports URL and URLSearchParams, as Node's does. URLPattern stays
a global only; Node's `node:url` has no counterpart and nothing here needs it.

The binding takes both constructors from the templates URLImpl::Init and
URLSearchParamsImpl::Init installed, held per isolate the way Worker's is.
V8 instantiates a template once per context, so the module hands out the very
functions the globals were created with even after an app reassigns them.

types/ns-url.d.ts declares the module and, like @types/node does for its own
globals, merges the module's interfaces into the global URL and
URLSearchParams and declares the global variables through the `onmessage`
probe, so a program with a DOM lib keeps the lib's declarations and one
without gets the module's constructors. Verified with tsc --strict against a
DOM and a non-DOM program: both are clean, and the non-DOM one no longer
reports the two missing-`URL` errors ns-module.d.ts used to produce there.

Experimental, iOS-only until the Android runtime ships the module.
WHATWG URL defines toJSON as the href serialization, so JSON.stringify(url)
yields the href string. The runtime's URL lacked it, which also kept a
module URL from being assignable to lib.dom's URL in typed code. It is
registered next to toString and shares its callback, so both return the
same serialization. types/ns-url.d.ts declares it; tsc against the DOM and
the non-DOM program stays clean, and the DOM program now assigns
pathToFileURL()'s result to the global URL type.

The searchParams wrapper that keeps a URL's query in sync forwarded only the
name to the native delete, so url.searchParams.delete(name, value) removed
every pair with that name. It now forwards the value whenever the caller
passed one, by arity, so an explicit undefined still reaches the native
coercion.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant