Skip to content

Commit bc97a90

Browse files
anonriglemire
authored andcommittedFeb 2, 2025
url: add URLPattern implementation
Co-authored-by: Daniel Lemire <[email protected]> PR-URL: #56452 Reviewed-By: Daniel Lemire <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Jordan Harband <[email protected]> Reviewed-By: Stephen Belanger <[email protected]>
1 parent e11cda0 commit bc97a90

14 files changed

+1073
-0
lines changed
 

‎doc/api/errors.md

+7
Original file line numberDiff line numberDiff line change
@@ -2116,6 +2116,13 @@ constructor][`new URL(input)`] or the legacy [`url.parse()`][] to be parsed.
21162116
The thrown error object typically has an additional property `'input'` that
21172117
contains the URL that failed to parse.
21182118

2119+
<a id="ERR_INVALID_URL_PATTERN"></a>
2120+
2121+
### `ERR_INVALID_URL_PATTERN`
2122+
2123+
An invalid URLPattern was passed to the [WHATWG][WHATWG URL API] \[`URLPattern`
2124+
constructor]\[`new URLPattern(input)`] to be parsed.
2125+
21192126
<a id="ERR_INVALID_URL_SCHEME"></a>
21202127

21212128
### `ERR_INVALID_URL_SCHEME`

‎doc/api/url.md

+123
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,129 @@ Parses a string as a URL. If `base` is provided, it will be used as the base
714714
URL for the purpose of resolving non-absolute `input` URLs. Returns `null`
715715
if `input` is not a valid.
716716

717+
### Class: `URLPattern`
718+
719+
> Stability: 1 - Experimental
720+
721+
<!-- YAML
722+
added: REPLACEME
723+
-->
724+
725+
The `URLPattern` API provides an interface to match URLs or parts of URLs
726+
against a pattern.
727+
728+
```js
729+
const myPattern = new URLPattern('https://nodejs.org/docs/latest/api/*.html');
730+
console.log(myPattern.exec('https://nodejs.org/docs/latest/api/dns.html'));
731+
// Prints:
732+
// {
733+
// "hash": { "groups": { "0": "" }, "input": "" },
734+
// "hostname": { "groups": {}, "input": "nodejs.org" },
735+
// "inputs": [
736+
// "https://nodejs.org/docs/latest/api/dns.html"
737+
// ],
738+
// "password": { "groups": { "0": "" }, "input": "" },
739+
// "pathname": { "groups": { "0": "dns" }, "input": "/docs/latest/api/dns.html" },
740+
// "port": { "groups": {}, "input": "" },
741+
// "protocol": { "groups": {}, "input": "https" },
742+
// "search": { "groups": { "0": "" }, "input": "" },
743+
// "username": { "groups": { "0": "" }, "input": "" }
744+
// }
745+
746+
console.log(myPattern.test('https://nodejs.org/docs/latest/api/dns.html'));
747+
// Prints: true
748+
```
749+
750+
#### `new URLPattern()`
751+
752+
Instantiate a new empty `URLPattern` object.
753+
754+
#### `new URLPattern(string[, baseURL][, options])`
755+
756+
* `string` {string} A URL string
757+
* `baseURL` {string | undefined} A base URL string
758+
* `options` {Object} Options
759+
760+
Parse the `string` as a URL, and use it to instantiate a new
761+
`URLPattern` object.
762+
763+
If `baseURL` is not specified, it defaults to `undefined`.
764+
765+
An option can have `ignoreCase` boolean attribute which enables
766+
case-insensitive matching if set to true.
767+
768+
The constructor can throw a `TypeError` to indicate parsing failure.
769+
770+
#### `new URLPattern(objg[, baseURL][, options])`
771+
772+
* `obj` {Object} An input pattern
773+
* `baseURL` {string | undefined} A base URL string
774+
* `options` {Object} Options
775+
776+
Parse the `Object` as an input pattern, and use it to instantiate a new
777+
`URLPattern` object. The object members can be any of `protocol`, `username`,
778+
`password`, `hostname`, `port`, `pathname`, `search`, `hash` or `baseURL`.
779+
780+
If `baseURL` is not specified, it defaults to `undefined`.
781+
782+
An option can have `ignoreCase` boolean attribute which enables
783+
case-insensitive matching if set to true.
784+
785+
The constructor can throw a `TypeError` to indicate parsing failure.
786+
787+
#### `urlPattern.exec(input[, baseURL])`
788+
789+
* `input` {string | Object} A URL or URL parts
790+
* `baseURL` {string | undefined} A base URL string
791+
792+
Input can be a string or an object providing the individual URL parts. The
793+
object members can be any of `protocol`, `username`, `password`, `hostname`,
794+
`port`, `pathname`, `search`, `hash` or `baseURL`.
795+
796+
If `baseURL` is not specified, it will default to `undefined`.
797+
798+
Returns an object with an `inputs` key containing the array of arguments
799+
passed into the function and keys of the URL components which contains the
800+
matched input and matched groups.
801+
802+
```js
803+
const myPattern = new URLPattern('https://nodejs.org/docs/latest/api/*.html');
804+
console.log(myPattern.exec('https://nodejs.org/docs/latest/api/dns.html'));
805+
// Prints:
806+
// {
807+
// "hash": { "groups": { "0": "" }, "input": "" },
808+
// "hostname": { "groups": {}, "input": "nodejs.org" },
809+
// "inputs": [
810+
// "https://nodejs.org/docs/latest/api/dns.html"
811+
// ],
812+
// "password": { "groups": { "0": "" }, "input": "" },
813+
// "pathname": { "groups": { "0": "dns" }, "input": "/docs/latest/api/dns.html" },
814+
// "port": { "groups": {}, "input": "" },
815+
// "protocol": { "groups": {}, "input": "https" },
816+
// "search": { "groups": { "0": "" }, "input": "" },
817+
// "username": { "groups": { "0": "" }, "input": "" }
818+
// }
819+
```
820+
821+
#### `urlPattern.test(input[, baseURL])`
822+
823+
* `input` {string | Object} A URL or URL parts
824+
* `baseURL` {string | undefined} A base URL string
825+
826+
Input can be a string or an object providing the individual URL parts. The
827+
object members can be any of `protocol`, `username`, `password`, `hostname`,
828+
`port`, `pathname`, `search`, `hash` or `baseURL`.
829+
830+
If `baseURL` is not specified, it will default to `undefined`.
831+
832+
Returns a boolean indicating if the input matches the current pattern.
833+
834+
```js
835+
const myPattern = new URLPattern('https://nodejs.org/docs/latest/api/*.html');
836+
console.log(myPattern.test('https://nodejs.org/docs/latest/api/dns.html'));
837+
// Prints: true
838+
```
839+
717840
### Class: `URLSearchParams`
718841

719842
<!-- YAML

‎lib/internal/url.js

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const {
3232
decodeURIComponent,
3333
} = primordials;
3434

35+
const { URLPattern } = internalBinding('url_pattern');
3536
const { inspect } = require('internal/util/inspect');
3637
const {
3738
encodeStr,
@@ -1574,6 +1575,7 @@ module.exports = {
15741575
toPathIfFileURL,
15751576
installObjectURLMethods,
15761577
URL,
1578+
URLPattern,
15771579
URLSearchParams,
15781580
URLParse: URL.parse,
15791581
domainToASCII,

‎lib/url.js

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const {
3030
decodeURIComponent,
3131
} = primordials;
3232

33+
const { URLPattern } = internalBinding('url_pattern');
3334
const { toASCII } = internalBinding('encoding_binding');
3435
const { encodeStr, hexTable } = require('internal/querystring');
3536
const querystring = require('querystring');
@@ -1030,6 +1031,7 @@ module.exports = {
10301031

10311032
// WHATWG API
10321033
URL,
1034+
URLPattern,
10331035
URLSearchParams,
10341036
domainToASCII,
10351037
domainToUnicode,

‎node.gyp

+2
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@
146146
'src/node_trace_events.cc',
147147
'src/node_types.cc',
148148
'src/node_url.cc',
149+
'src/node_url_pattern.cc',
149150
'src/node_util.cc',
150151
'src/node_v8.cc',
151152
'src/node_wasi.cc',
@@ -275,6 +276,7 @@
275276
'src/node_stat_watcher.h',
276277
'src/node_union_bytes.h',
277278
'src/node_url.h',
279+
'src/node_url_pattern.h',
278280
'src/node_version.h',
279281
'src/node_v8.h',
280282
'src/node_v8_platform-inl.h',

‎src/env_properties.h

+10
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
V(async_ids_stack_string, "async_ids_stack") \
7979
V(attributes_string, "attributes") \
8080
V(base_string, "base") \
81+
V(base_url_string, "baseURL") \
8182
V(bits_string, "bits") \
8283
V(block_list_string, "blockList") \
8384
V(buffer_string, "buffer") \
@@ -179,20 +180,26 @@
179180
V(get_data_clone_error_string, "_getDataCloneError") \
180181
V(get_shared_array_buffer_id_string, "_getSharedArrayBufferId") \
181182
V(gid_string, "gid") \
183+
V(groups_string, "groups") \
184+
V(has_regexp_groups_string, "hasRegExpGroups") \
185+
V(hash_string, "hash") \
182186
V(h2_string, "h2") \
183187
V(handle_string, "handle") \
184188
V(hash_algorithm_string, "hashAlgorithm") \
185189
V(help_text_string, "helpText") \
186190
V(homedir_string, "homedir") \
187191
V(host_string, "host") \
188192
V(hostmaster_string, "hostmaster") \
193+
V(hostname_string, "hostname") \
189194
V(http_1_1_string, "http/1.1") \
190195
V(id_string, "id") \
191196
V(identity_string, "identity") \
197+
V(ignore_case_string, "ignoreCase") \
192198
V(ignore_string, "ignore") \
193199
V(infoaccess_string, "infoAccess") \
194200
V(inherit_string, "inherit") \
195201
V(input_string, "input") \
202+
V(inputs_string, "inputs") \
196203
V(internal_binding_string, "internalBinding") \
197204
V(internal_string, "internal") \
198205
V(ipv4_string, "IPv4") \
@@ -280,6 +287,7 @@
280287
V(parse_error_string, "Parse Error") \
281288
V(password_string, "password") \
282289
V(path_string, "path") \
290+
V(pathname_string, "pathname") \
283291
V(pending_handle_string, "pendingHandle") \
284292
V(permission_string, "permission") \
285293
V(pid_string, "pid") \
@@ -295,6 +303,7 @@
295303
V(priority_string, "priority") \
296304
V(process_string, "process") \
297305
V(promise_string, "promise") \
306+
V(protocol_string, "protocol") \
298307
V(prototype_string, "prototype") \
299308
V(psk_string, "psk") \
300309
V(pubkey_string, "pubkey") \
@@ -323,6 +332,7 @@
323332
V(scopeid_string, "scopeid") \
324333
V(script_id_string, "scriptId") \
325334
V(script_name_string, "scriptName") \
335+
V(search_string, "search") \
326336
V(serial_number_string, "serialNumber") \
327337
V(serial_string, "serial") \
328338
V(servername_string, "servername") \

‎src/node_binding.cc

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "node_builtins.h"
55
#include "node_errors.h"
66
#include "node_external_reference.h"
7+
#include "node_url_pattern.h"
78
#include "util.h"
89

910
#include <string>
@@ -87,6 +88,7 @@
8788
V(types) \
8889
V(udp_wrap) \
8990
V(url) \
91+
V(url_pattern) \
9092
V(util) \
9193
V(uv) \
9294
V(v8) \

‎src/node_errors.h

+2
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
9090
V(ERR_INVALID_STATE, Error) \
9191
V(ERR_INVALID_THIS, TypeError) \
9292
V(ERR_INVALID_URL, TypeError) \
93+
V(ERR_INVALID_URL_PATTERN, TypeError) \
9394
V(ERR_INVALID_URL_SCHEME, TypeError) \
9495
V(ERR_LOAD_SQLITE_EXTENSION, Error) \
9596
V(ERR_MEMORY_ALLOCATION_FAILED, Error) \
@@ -99,6 +100,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
99100
V(ERR_MISSING_PLATFORM_FOR_WORKER, Error) \
100101
V(ERR_MODULE_NOT_FOUND, Error) \
101102
V(ERR_NON_CONTEXT_AWARE_DISABLED, Error) \
103+
V(ERR_OPERATION_FAILED, TypeError) \
102104
V(ERR_OUT_OF_RANGE, RangeError) \
103105
V(ERR_REQUIRE_ASYNC_MODULE, Error) \
104106
V(ERR_SCRIPT_EXECUTION_INTERRUPTED, Error) \

‎src/node_external_reference.h

+1
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ class ExternalReferenceRegistry {
180180
V(tty_wrap) \
181181
V(udp_wrap) \
182182
V(url) \
183+
V(url_pattern) \
183184
V(util) \
184185
V(pipe_wrap) \
185186
V(sea) \

‎src/node_url_pattern.cc

+787
Large diffs are not rendered by default.

‎src/node_url_pattern.h

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#ifndef SRC_NODE_URL_PATTERN_H_
2+
#define SRC_NODE_URL_PATTERN_H_
3+
4+
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
5+
6+
#include "ada.h"
7+
#include "base_object.h"
8+
#include "util.h"
9+
10+
#include <v8.h>
11+
12+
#include <optional>
13+
#include <string_view>
14+
15+
namespace node::url_pattern {
16+
17+
// By default, ada::url_pattern doesn't ship with any regex library.
18+
// Ada has a std::regex implementation, but it is considered unsafe and does
19+
// not have a fully compliant ecmascript syntax support. Therefore, Ada
20+
// supports passing custom regex provider that conforms to the following
21+
// class and function structure. For more information, please look into
22+
// url_pattern_regex.h inside github.com/ada-url/ada.
23+
class URLPatternRegexProvider {
24+
public:
25+
using regex_type = v8::Global<v8::RegExp>;
26+
static std::optional<regex_type> create_instance(std::string_view pattern,
27+
bool ignore_case);
28+
static std::optional<std::vector<std::optional<std::string>>> regex_search(
29+
std::string_view input, const regex_type& pattern);
30+
static bool regex_match(std::string_view input, const regex_type& pattern);
31+
};
32+
33+
class URLPattern : public BaseObject {
34+
public:
35+
URLPattern(Environment* env,
36+
v8::Local<v8::Object> object,
37+
ada::url_pattern<URLPatternRegexProvider>&& url_pattern);
38+
39+
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
40+
41+
// V8 APIs
42+
// - Functions
43+
static void Exec(const v8::FunctionCallbackInfo<v8::Value>& info);
44+
static void Test(const v8::FunctionCallbackInfo<v8::Value>& info);
45+
// - Getters
46+
static void Hash(const v8::FunctionCallbackInfo<v8::Value>& info);
47+
static void Hostname(const v8::FunctionCallbackInfo<v8::Value>& info);
48+
static void Password(const v8::FunctionCallbackInfo<v8::Value>& info);
49+
static void Pathname(const v8::FunctionCallbackInfo<v8::Value>& info);
50+
static void Port(const v8::FunctionCallbackInfo<v8::Value>& info);
51+
static void Protocol(const v8::FunctionCallbackInfo<v8::Value>& info);
52+
static void Search(const v8::FunctionCallbackInfo<v8::Value>& info);
53+
static void Username(const v8::FunctionCallbackInfo<v8::Value>& info);
54+
static void HasRegexpGroups(const v8::FunctionCallbackInfo<v8::Value>& info);
55+
56+
void MemoryInfo(MemoryTracker* tracker) const override;
57+
SET_MEMORY_INFO_NAME(URLPattern)
58+
SET_SELF_SIZE(URLPattern)
59+
60+
class URLPatternInit {
61+
public:
62+
static ada::url_pattern_init FromJsObject(Environment* env,
63+
v8::Local<v8::Object> obj);
64+
static v8::MaybeLocal<v8::Value> ToJsObject(
65+
Environment* env, const ada::url_pattern_init& init);
66+
};
67+
68+
class URLPatternOptions {
69+
public:
70+
static std::optional<ada::url_pattern_options> FromJsObject(
71+
Environment* env, v8::Local<v8::Object> obj);
72+
};
73+
74+
class URLPatternResult {
75+
public:
76+
static v8::MaybeLocal<v8::Value> ToJSValue(
77+
Environment* env, const ada::url_pattern_result& result);
78+
};
79+
80+
class URLPatternComponentResult {
81+
public:
82+
static v8::MaybeLocal<v8::Object> ToJSObject(
83+
Environment* env, const ada::url_pattern_component_result& result);
84+
};
85+
86+
private:
87+
ada::url_pattern<URLPatternRegexProvider> url_pattern_;
88+
// Getter methods
89+
v8::MaybeLocal<v8::Value> Hash() const;
90+
v8::MaybeLocal<v8::Value> Hostname() const;
91+
v8::MaybeLocal<v8::Value> Password() const;
92+
v8::MaybeLocal<v8::Value> Pathname() const;
93+
v8::MaybeLocal<v8::Value> Port() const;
94+
v8::MaybeLocal<v8::Value> Protocol() const;
95+
v8::MaybeLocal<v8::Value> Search() const;
96+
v8::MaybeLocal<v8::Value> Username() const;
97+
bool HasRegExpGroups() const;
98+
// Public API
99+
v8::MaybeLocal<v8::Value> Exec(
100+
Environment* env,
101+
const ada::url_pattern_input& input,
102+
std::optional<std::string_view>& baseURL); // NOLINT (runtime/references)
103+
bool Test(
104+
Environment* env,
105+
const ada::url_pattern_input& input,
106+
std::optional<std::string_view>& baseURL); // NOLINT (runtime/references)
107+
};
108+
109+
} // namespace node::url_pattern
110+
111+
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
112+
#endif // SRC_NODE_URL_PATTERN_H_

‎test/parallel/test-bootstrap-modules.js

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ expected.beforePreExec = new Set([
7474
'NativeModule internal/querystring',
7575
'NativeModule querystring',
7676
'Internal Binding url',
77+
'Internal Binding url_pattern',
7778
'Internal Binding blob',
7879
'NativeModule internal/url',
7980
'NativeModule util',

‎typings/globals.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { SymbolsBinding } from './internalBinding/symbols';
1414
import { TimersBinding } from './internalBinding/timers';
1515
import { TypesBinding } from './internalBinding/types';
1616
import { URLBinding } from './internalBinding/url';
17+
import { URLPatternBinding } from "./internalBinding/url_pattern";
1718
import { UtilBinding } from './internalBinding/util';
1819
import { WASIBinding } from './internalBinding/wasi';
1920
import { WorkerBinding } from './internalBinding/worker';
@@ -38,6 +39,7 @@ interface InternalBindingMap {
3839
timers: TimersBinding;
3940
types: TypesBinding;
4041
url: URLBinding;
42+
url_pattern: URLPatternBinding;
4143
util: UtilBinding;
4244
wasi: WASIBinding;
4345
worker: WorkerBinding;
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export class URLPattern {
2+
protocol: string
3+
username: string
4+
password: string
5+
hostname: string
6+
port: string
7+
pathname: string
8+
search: string
9+
hash: string
10+
11+
constructor(input: Record<string, string> | string, options?: { ignoreCase: boolean });
12+
constructor(input: Record<string, string> | string, baseUrl?: string, options?: { ignoreCase: boolean });
13+
14+
exec(input: string | Record<string, string>, baseURL?: string): null | Record<string, unknown>;
15+
test(input: string | Record<string, string>, baseURL?: string): boolean;
16+
}
17+
18+
export interface URLPatternBinding {
19+
URLPattern: URLPattern;
20+
}

0 commit comments

Comments
 (0)
Please sign in to comment.