Skip to content

Commit 80475b4

Browse files
authored
feat(runtime): ns:url builtin module mirroring node:url, with URL types (#497)
* feat(runtime): ns:url builtin module mirroring node:url, with URL types 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. * fix(runtime): URL toJSON and value-aware searchParams.delete 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.
1 parent 0819de2 commit 80475b4

18 files changed

Lines changed: 522 additions & 134 deletions

‎NativeScript/runtime/NsBuiltinModules.cpp‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#include "Runtime.h"
1212
#include "StructuredSerialization.h"
1313
#include "TextEncoding.h"
14+
#include "URLImpl.h"
15+
#include "URLSearchParamsImpl.h"
1416
#include "Worker.h"
1517

1618
using namespace v8;
@@ -25,6 +27,7 @@ constexpr const char* kNodePrefix = "node:";
2527
// Defined below, each next to the natives it gathers.
2628
MaybeLocal<Object> NsModuleBinding(Local<Context> context);
2729
MaybeLocal<Object> NsRuntimeBinding(Local<Context> context);
30+
MaybeLocal<Object> NsUrlBinding(Local<Context> context);
2831
MaybeLocal<Object> NsUtilBinding(Local<Context> context);
2932
MaybeLocal<Object> NsWorkerThreadsBinding(Local<Context> context);
3033

@@ -47,6 +50,7 @@ struct Registration {
4750
constexpr Registration kRegistry[] = {
4851
{"ns:module", BuiltinId::kNsModule, NsModuleBinding},
4952
{"ns:runtime", BuiltinId::kNsRuntime, NsRuntimeBinding},
53+
{"ns:url", BuiltinId::kNsUrl, NsUrlBinding},
5054
{"ns:util", BuiltinId::kNsUtil, NsUtilBinding},
5155
{"ns:worker_threads", BuiltinId::kNsWorkerThreads, NsWorkerThreadsBinding},
5256
{"node:module", BuiltinId::kNodeModule, nullptr},
@@ -200,6 +204,27 @@ MaybeLocal<Object> NsRuntimeBinding(Local<Context> context) {
200204
return binding;
201205
}
202206

207+
// URL and URLSearchParams for ns:url: the context's own, from the templates
208+
// their Init installed, rather than whatever the globals name by the time the
209+
// module is first required.
210+
MaybeLocal<Object> NsUrlBinding(Local<Context> context) {
211+
Isolate* isolate = v8::Isolate::GetCurrent();
212+
Local<Object> binding = Object::New(isolate);
213+
Local<v8::Function> url;
214+
Local<v8::Function> searchParams;
215+
if (!URLImpl::Constructor(context).ToLocal(&url) ||
216+
!URLSearchParamsImpl::Constructor(context).ToLocal(&searchParams) ||
217+
!binding->Set(context, tns::ToV8String(isolate, "URL"), url)
218+
.FromMaybe(false) ||
219+
!binding
220+
->Set(context, tns::ToV8String(isolate, "URLSearchParams"),
221+
searchParams)
222+
.FromMaybe(false)) {
223+
return MaybeLocal<Object>();
224+
}
225+
return binding;
226+
}
227+
203228
// The Worker constructor for ns:worker_threads: the context's own, from the
204229
// template Worker::Init installed, rather than whatever `globalThis.Worker`
205230
// names by the time the module is first required.

‎NativeScript/runtime/URLImpl.cpp‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,24 @@
44

55
#include "URLImpl.h"
66

7+
#include "Caches.h"
78
#include "Helpers.h"
89
#include "ModuleBinding.hpp"
910

1011
using namespace tns;
1112
using namespace ada;
1213

14+
namespace {
15+
16+
// The constructor template Init put on the global template. A template's
17+
// function is cached per context, so instantiating it again yields the very
18+
// function the global was created with.
19+
struct URLConstructorState {
20+
v8::Global<v8::FunctionTemplate> constructor;
21+
};
22+
23+
} // namespace
24+
1325
URLImpl::URLImpl(url_aggregator url) : url_(url) {}
1426

1527
void URLImpl::Init(v8::Isolate* isolate,
@@ -18,6 +30,20 @@ void URLImpl::Init(v8::Isolate* isolate,
1830

1931
v8::Local<v8::String> urlPropertyName = ToV8String(isolate, "URL");
2032
globalTemplate->Set(urlPropertyName, URLTemplate);
33+
34+
if (auto* state = Caches::StateFor<URLConstructorState>(isolate)) {
35+
state->constructor.Reset(isolate, URLTemplate);
36+
}
37+
}
38+
39+
v8::MaybeLocal<v8::Function> URLImpl::Constructor(
40+
v8::Local<v8::Context> context) {
41+
v8::Isolate* isolate = v8::Isolate::GetCurrent();
42+
auto* state = Caches::StateFor<URLConstructorState>(isolate);
43+
if (state == nullptr || state->constructor.IsEmpty()) {
44+
return v8::MaybeLocal<v8::Function>();
45+
}
46+
return state->constructor.Get(isolate)->GetFunction(context);
2147
}
2248

2349
URLImpl* URLImpl::GetPointer(v8::Local<v8::Object> object) {
@@ -67,6 +93,10 @@ v8::Local<v8::FunctionTemplate> URLImpl::GetCtor(v8::Isolate* isolate) {
6793
tmpl->Set(ToV8String(isolate, "toString"),
6894
v8::FunctionTemplate::New(isolate, &ToString));
6995

96+
// WHATWG defines toJSON as the href serialization, the same as toString.
97+
tmpl->Set(ToV8String(isolate, "toJSON"),
98+
v8::FunctionTemplate::New(isolate, &ToString));
99+
70100
ctorTmpl->Set(ToV8String(isolate, "canParse"),
71101
v8::FunctionTemplate::New(isolate, &CanParse));
72102

‎NativeScript/runtime/URLImpl.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ class URLImpl : public IsolateTracked {
2121
static void Init(v8::Isolate* isolate,
2222
v8::Local<v8::ObjectTemplate> globalTemplate);
2323

24+
// The URL constructor of `context`: the function Init's template produces
25+
// for it, which is the object the global of that name was created with,
26+
// whatever `globalThis.URL` names by now. Empty before Init has run for the
27+
// isolate.
28+
static v8::MaybeLocal<v8::Function> Constructor(
29+
v8::Local<v8::Context> context);
30+
2431
static URLImpl* GetPointer(v8::Local<v8::Object> object);
2532

2633
static v8::Local<v8::FunctionTemplate> GetCtor(v8::Isolate* isolate);

‎NativeScript/runtime/URLSearchParamsImpl.cpp‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,24 @@
44

55
#include "URLSearchParamsImpl.h"
66

7+
#include "Caches.h"
78
#include "Helpers.h"
89
#include "ModuleBinding.hpp"
910

1011
using namespace ada;
1112

1213
namespace tns {
14+
15+
namespace {
16+
17+
// The constructor template Init put on the global template; see
18+
// URLConstructorState in URLImpl.cpp.
19+
struct URLSearchParamsConstructorState {
20+
v8::Global<v8::FunctionTemplate> constructor;
21+
};
22+
23+
} // namespace
24+
1325
URLSearchParamsImpl::URLSearchParamsImpl(ada::url_search_params params)
1426
: params_(params) {}
1527

@@ -20,6 +32,21 @@ void URLSearchParamsImpl::Init(v8::Isolate* isolate,
2032
v8::Local<v8::String> urlSearchParamsPropertyName =
2133
ToV8String(isolate, "URLSearchParams");
2234
globalTemplate->Set(urlSearchParamsPropertyName, URLSearchParamsTemplate);
35+
36+
if (auto* state =
37+
Caches::StateFor<URLSearchParamsConstructorState>(isolate)) {
38+
state->constructor.Reset(isolate, URLSearchParamsTemplate);
39+
}
40+
}
41+
42+
v8::MaybeLocal<v8::Function> URLSearchParamsImpl::Constructor(
43+
v8::Local<v8::Context> context) {
44+
v8::Isolate* isolate = v8::Isolate::GetCurrent();
45+
auto* state = Caches::StateFor<URLSearchParamsConstructorState>(isolate);
46+
if (state == nullptr || state->constructor.IsEmpty()) {
47+
return v8::MaybeLocal<v8::Function>();
48+
}
49+
return state->constructor.Get(isolate)->GetFunction(context);
2350
}
2451

2552
URLSearchParamsImpl* URLSearchParamsImpl::GetPointer(

‎NativeScript/runtime/URLSearchParamsImpl.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ class URLSearchParamsImpl : public IsolateTracked {
2222
static void Init(v8::Isolate* isolate,
2323
v8::Local<v8::ObjectTemplate> globalTemplate);
2424

25+
// The URLSearchParams constructor of `context`, as URLImpl::Constructor.
26+
static v8::MaybeLocal<v8::Function> Constructor(
27+
v8::Local<v8::Context> context);
28+
2529
static void Ctor(const v8::FunctionCallbackInfo<v8::Value>& args);
2630

2731
static void Append(const v8::FunctionCallbackInfo<v8::Value>& args);

‎NativeScript/runtime/js/blob-url.js‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,15 @@ ObjectDefineProperty(URL.prototype, 'searchParams', {
105105
writeBack(this);
106106
};
107107
params._delete = params.delete;
108-
params.delete = function (name) {
108+
params.delete = function (name, value) {
109109
resync(this);
110-
this._delete(name);
110+
// An explicitly passed undefined must still reach the native
111+
// coercion, so forward by arity rather than by value.
112+
if (arguments.length > 1) {
113+
this._delete(name, value);
114+
} else {
115+
this._delete(name);
116+
}
111117
writeBack(this);
112118
};
113119
params._set = params.set;
Lines changed: 12 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,16 @@
11
"use strict";
22

3-
// The `node:url` compatibility shim: the two path/URL converters
4-
// (docs/ns-builtin-modules.md). Parsing goes through the URL intrinsic rather
5-
// than a hand-rolled scan, so authority normalization (`file://localhost/x`
6-
// has no host, per the URL spec), percent-decoding and path canonicalization
7-
// all follow the spec instead of an approximation.
3+
// The `node:url` compatibility shim over `ns:url` (docs/ns-builtin-modules.md).
4+
// The two surfaces coincide today, so this only re-exports; any adaptation to
5+
// Node's API belongs here, never in the standard module.
86

9-
const {
10-
decodeURIComponent,
11-
ObjectFreeze,
12-
StringPrototypeCharCodeAt,
13-
StringPrototypeStartsWith,
14-
TypeError,
15-
URL,
16-
} = primordials;
17-
18-
const INVALID_ARG =
19-
'The "path" argument must be of type string or an instance of URL.';
20-
21-
function toUrl(input) {
22-
let href;
23-
if (typeof input === "string") {
24-
href = input;
25-
} else if (input !== null && typeof input === "object" &&
26-
typeof input.href === "string") {
27-
// Duck-typed so a URL from another realm still works.
28-
href = input.href;
29-
} else {
30-
throw new TypeError(INVALID_ARG);
31-
}
32-
33-
try {
34-
return new URL(href);
35-
} catch {
36-
throw new TypeError(INVALID_ARG);
37-
}
38-
}
39-
40-
function fileURLToPath(input) {
41-
const url = toUrl(input);
42-
43-
if (url.protocol !== "file:") {
44-
throw new TypeError("The URL must be of scheme file");
45-
}
46-
// The URL parser already folded a "localhost" authority to the empty host,
47-
// so anything left here is a real remote host and names no local file.
48-
if (url.hostname !== "") {
49-
throw new TypeError('File URL host must be "localhost" or empty');
50-
}
51-
52-
// `pathname` carries neither the query nor the fragment.
53-
const pathname = url.pathname;
54-
for (let i = 0; i < pathname.length; i++) {
55-
if (pathname[i] !== "%") {
56-
continue;
57-
}
58-
// %2F would decode to a separator and silently change the path's shape.
59-
const third = StringPrototypeCharCodeAt(pathname, i + 2) | 0x20;
60-
if (pathname[i + 1] === "2" && third === 102 /* 'f' */) {
61-
throw new TypeError("File URL path must not include encoded / characters");
62-
}
63-
}
7+
const { ObjectFreeze } = primordials;
8+
const { URL, URLSearchParams, fileURLToPath, pathToFileURL } =
9+
require("ns:url");
6410

65-
return decodeURIComponent(pathname);
66-
}
67-
68-
const kHexDigits = "0123456789ABCDEF";
69-
70-
// Percent-encode everything the URL parser would otherwise read as syntax (or
71-
// reject), leaving `/` as the separator it is. Non-ASCII is left alone: the
72-
// parser UTF-8 encodes it correctly on its own.
73-
function encodePathChars(filepath) {
74-
let encoded = "";
75-
for (let i = 0; i < filepath.length; i++) {
76-
const char = filepath[i];
77-
const code = StringPrototypeCharCodeAt(filepath, i);
78-
const mustEncode =
79-
code < 0x21 || code === 0x7f || char === "%" || char === "?" ||
80-
char === "#" || char === "\\" || char === '"' || char === "<" ||
81-
char === ">" || char === "`" || char === "{" || char === "}";
82-
if (mustEncode) {
83-
encoded += "%" + kHexDigits[(code >> 4) & 0xf] + kHexDigits[code & 0xf];
84-
} else {
85-
encoded += char;
86-
}
87-
}
88-
return encoded;
89-
}
90-
91-
function pathToFileURL(filepath) {
92-
if (typeof filepath !== "string") {
93-
throw new TypeError('The "path" argument must be of type string.');
94-
}
95-
// Node resolves a relative path against the process working directory; there
96-
// is no such thing here, so a relative path has no single correct answer.
97-
if (!StringPrototypeStartsWith(filepath, "/")) {
98-
throw new TypeError('The "path" argument must be an absolute path.');
99-
}
100-
101-
return new URL("file://" + encodePathChars(filepath));
102-
}
103-
104-
module.exports = ObjectFreeze({ fileURLToPath, pathToFileURL });
11+
module.exports = ObjectFreeze({
12+
URL,
13+
URLSearchParams,
14+
fileURLToPath,
15+
pathToFileURL,
16+
});

0 commit comments

Comments
 (0)