Skip to content

Commit b47313d

Browse files
committed
url: preserve property order in URLPattern results
Use each compiled component's group name list when materializing URLPattern capture groups instead of iterating an unordered map. Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com>
1 parent d822e10 commit b47313d

3 files changed

Lines changed: 58 additions & 21 deletions

File tree

src/node_url_pattern.cc

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -409,22 +409,30 @@ std::optional<ada::url_pattern_init> URLPattern::URLPatternInit::FromJsObject(
409409
}
410410

411411
MaybeLocal<Object> URLPattern::URLPatternComponentResult::ToJSObject(
412-
Environment* env, const ada::url_pattern_component_result& result) {
412+
Environment* env,
413+
const ada::url_pattern_component_result& result,
414+
const std::vector<std::string>& ordered_group_names) {
413415
auto isolate = env->isolate();
414416
auto context = env->context();
415-
LocalVector<Name> group_names(isolate);
417+
LocalVector<Name> js_group_names(isolate);
416418
LocalVector<Value> group_values(isolate);
417-
group_names.reserve(result.groups.size());
419+
js_group_names.reserve(result.groups.size());
418420
group_values.reserve(result.groups.size());
419-
for (const auto& [group_key, group_value] : result.groups) {
421+
// The result uses an unordered map, so follow the component's group name
422+
// list to preserve capture declaration order.
423+
for (const auto& group_key : ordered_group_names) {
424+
const auto group = result.groups.find(group_key);
425+
if (group == result.groups.end()) {
426+
continue;
427+
}
420428
Local<Value> key;
421429
if (!ToV8Value(context, group_key).ToLocal(&key)) {
422430
return {};
423431
}
424-
group_names.push_back(key.As<Name>());
432+
js_group_names.push_back(key.As<Name>());
425433
Local<Value> value;
426-
if (group_value) {
427-
if (!ToV8Value(env->context(), *group_value).ToLocal(&value)) {
434+
if (group->second) {
435+
if (!ToV8Value(env->context(), *group->second).ToLocal(&value)) {
428436
return {};
429437
}
430438
} else {
@@ -434,9 +442,9 @@ MaybeLocal<Object> URLPattern::URLPatternComponentResult::ToJSObject(
434442
}
435443
auto parsed_group = Object::New(isolate,
436444
Object::New(isolate),
437-
group_names.data(),
445+
js_group_names.data(),
438446
group_values.data(),
439-
group_names.size());
447+
js_group_names.size());
440448

441449
Local<Value> input;
442450
if (!ToV8Value(env->context(), result.input).ToLocal(&input)) {
@@ -457,7 +465,9 @@ MaybeLocal<Object> URLPattern::URLPatternComponentResult::ToJSObject(
457465
}
458466

459467
MaybeLocal<Value> URLPattern::URLPatternResult::ToJSValue(
460-
Environment* env, const ada::url_pattern_result& result) {
468+
Environment* env,
469+
const ada::url_pattern_result& result,
470+
const ada::url_pattern<URLPatternRegexProvider>& url_pattern) {
461471
auto isolate = env->isolate();
462472

463473
auto tmpl = env->urlpatternresult_template();
@@ -479,8 +489,10 @@ MaybeLocal<Value> URLPattern::URLPatternResult::ToJSValue(
479489

480490
size_t index = 0;
481491
MaybeLocal<Value> vals[] = {
482-
URLPatternComponentResult::ToJSObject(env, result.hash),
483-
URLPatternComponentResult::ToJSObject(env, result.hostname),
492+
URLPatternComponentResult::ToJSObject(
493+
env, result.hash, url_pattern.hash_component.group_name_list),
494+
URLPatternComponentResult::ToJSObject(
495+
env, result.hostname, url_pattern.hostname_component.group_name_list),
484496
Array::New(env->context(),
485497
result.inputs.size(),
486498
[&index, &inputs = result.inputs, env]() {
@@ -495,12 +507,20 @@ MaybeLocal<Value> URLPattern::URLPatternResult::ToJSValue(
495507
return URLPatternInit::ToJsObject(env, init);
496508
}
497509
}),
498-
URLPatternComponentResult::ToJSObject(env, result.password),
499-
URLPatternComponentResult::ToJSObject(env, result.pathname),
500-
URLPatternComponentResult::ToJSObject(env, result.port),
501-
URLPatternComponentResult::ToJSObject(env, result.protocol),
502-
URLPatternComponentResult::ToJSObject(env, result.search),
503-
URLPatternComponentResult::ToJSObject(env, result.username)};
510+
URLPatternComponentResult::ToJSObject(
511+
env, result.password, url_pattern.password_component.group_name_list),
512+
URLPatternComponentResult::ToJSObject(
513+
env, result.pathname, url_pattern.pathname_component.group_name_list),
514+
URLPatternComponentResult::ToJSObject(
515+
env, result.port, url_pattern.port_component.group_name_list),
516+
URLPatternComponentResult::ToJSObject(
517+
env, result.protocol, url_pattern.protocol_component.group_name_list),
518+
URLPatternComponentResult::ToJSObject(
519+
env, result.search, url_pattern.search_component.group_name_list),
520+
URLPatternComponentResult::ToJSObject(
521+
env,
522+
result.username,
523+
url_pattern.username_component.group_name_list)};
504524
return NewDictionaryInstanceNullProto(env->context(), tmpl, vals);
505525
}
506526

@@ -552,7 +572,7 @@ MaybeLocal<Value> URLPattern::Exec(Environment* env,
552572
std::optional<std::string_view>& baseURL) {
553573
if (auto result = url_pattern_.exec(input, baseURL ? &*baseURL : nullptr)) {
554574
if (result->has_value()) {
555-
return URLPatternResult::ToJSValue(env, result->value());
575+
return URLPatternResult::ToJSValue(env, result->value(), url_pattern_);
556576
}
557577
return Null(env->isolate());
558578
}

src/node_url_pattern.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
#include <v8.h>
1111

1212
#include <optional>
13+
#include <string>
1314
#include <string_view>
15+
#include <vector>
1416

1517
namespace node::url_pattern {
1618

@@ -81,13 +83,17 @@ class URLPattern : public BaseObject {
8183
class URLPatternResult {
8284
public:
8385
static v8::MaybeLocal<v8::Value> ToJSValue(
84-
Environment* env, const ada::url_pattern_result& result);
86+
Environment* env,
87+
const ada::url_pattern_result& result,
88+
const ada::url_pattern<URLPatternRegexProvider>& url_pattern);
8589
};
8690

8791
class URLPatternComponentResult {
8892
public:
8993
static v8::MaybeLocal<v8::Object> ToJSObject(
90-
Environment* env, const ada::url_pattern_component_result& result);
94+
Environment* env,
95+
const ada::url_pattern_component_result& result,
96+
const std::vector<std::string>& ordered_group_names);
9197
};
9298

9399
private:

test/parallel/test-urlpattern.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,14 @@ assert.throws(() => {
5050
assert.strictEqual(result.pathname.input, '/test');
5151
assert.strictEqual(result.pathname.groups.value, 'test');
5252
}
53+
54+
{
55+
const result = new URLPattern({ pathname: '/:one/:two/:three' })
56+
.exec('https://example.com/a/b/c');
57+
58+
assert.deepStrictEqual(Object.entries(result.pathname.groups), [
59+
['one', 'a'],
60+
['two', 'b'],
61+
['three', 'c'],
62+
]);
63+
}

0 commit comments

Comments
 (0)