@peter.naydenov/url-pattern 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/url-pattern.cjs.js +207 -58
- package/dist/url-pattern.es.js +205 -58
- package/dist/url-pattern.umd.js +1 -1
- package/package.json +6 -6
- package/{README.md → readme.md} +26 -10
- package/src/main.js +204 -57
- package/types/main.d.ts +87 -71
package/dist/url-pattern.cjs.js
CHANGED
|
@@ -9,13 +9,15 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* @typedef {Object} UrlPatternOptions
|
|
12
|
-
* @property {string} [escapeChar='\\'] - Character used for escaping
|
|
12
|
+
* @property {string} [escapeChar='\\'] - Character used for escaping. Only escapes regex metacharacters (`^$.*+?()[]{}|\`); for any other character the backslash is treated as a literal.
|
|
13
13
|
* @property {string} [segmentNameStartChar=':'] - Character that starts a named segment
|
|
14
|
-
* @property {string} [
|
|
14
|
+
* @property {string} [segmentNameEndChar] - Character that ends a named segment. When set, the segment name stops at the first occurrence of this character (instead of stopping at the first character outside `segmentNameCharset`).
|
|
15
|
+
* @property {string} [segmentNameCharset='a-zA-Z0-9_'] - Characters allowed in segment names
|
|
15
16
|
* @property {string} [segmentValueCharset='a-zA-Z0-9-_~ %'] - Characters allowed in segment values
|
|
16
17
|
* @property {string} [optionalSegmentStartChar='('] - Character that starts an optional segment
|
|
17
18
|
* @property {string} [optionalSegmentEndChar=')'] - Character that ends an optional segment
|
|
18
|
-
* @property {string} [wildcardChar='*'] - Character that denotes a wildcard
|
|
19
|
+
* @property {string} [wildcardChar='*'] - Character that denotes a wildcard in the pattern
|
|
20
|
+
* @property {string} [wildcardName='_'] - Key under which the wildcard value is stored in the match result
|
|
19
21
|
*/
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -23,6 +25,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
23
25
|
* @property {string} name - Segment name
|
|
24
26
|
* @property {string} type - Segment type ('named' | 'wildcard' | 'literal')
|
|
25
27
|
* @property {boolean} [optional=false] - Whether the segment is optional
|
|
28
|
+
* @property {number} [optionalGroupId] - Identifier of the optional group this segment belongs to; absent for required segments
|
|
26
29
|
* @property {string} regex - Compiled regex string
|
|
27
30
|
*/
|
|
28
31
|
|
|
@@ -52,11 +55,13 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
52
55
|
const DEFAULT_OPTIONS = {
|
|
53
56
|
escapeChar: '\\',
|
|
54
57
|
segmentNameStartChar: ':',
|
|
55
|
-
|
|
58
|
+
segmentNameEndChar: undefined,
|
|
59
|
+
segmentNameCharset: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_',
|
|
56
60
|
segmentValueCharset: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_~ %',
|
|
57
61
|
optionalSegmentStartChar: '(',
|
|
58
62
|
optionalSegmentEndChar: ')',
|
|
59
|
-
wildcardChar: '*'
|
|
63
|
+
wildcardChar: '*',
|
|
64
|
+
wildcardName: '_'
|
|
60
65
|
};
|
|
61
66
|
|
|
62
67
|
/**
|
|
@@ -66,6 +71,26 @@ const DEFAULT_OPTIONS = {
|
|
|
66
71
|
*/
|
|
67
72
|
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
68
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Builds a safe regex character class body from a charset string.
|
|
76
|
+
*
|
|
77
|
+
* The charset is treated as a list of explicit characters — range notation
|
|
78
|
+
* (`a-z`) is NOT interpreted as a range. To match the same set of chars
|
|
79
|
+
* with range syntax, expand it (e.g. `a-z` → `abcdefghijklmnopqrstuvwxyz`).
|
|
80
|
+
*
|
|
81
|
+
* The following chars are escaped because they are special inside `[...]`:
|
|
82
|
+
* `\`, `]`, `^`, and `-`.
|
|
83
|
+
* @param {string} charset - Character class body
|
|
84
|
+
* @returns {string} Safe char class body
|
|
85
|
+
*/
|
|
86
|
+
const escapeCharClass = (charset) => {
|
|
87
|
+
let escaped = charset.replace(/\\/g, '\\\\');
|
|
88
|
+
escaped = escaped.replace(/\]/g, '\\]');
|
|
89
|
+
escaped = escaped.replace(/\^/g, '\\^');
|
|
90
|
+
escaped = escaped.replace(/-/g, '\\-');
|
|
91
|
+
return escaped;
|
|
92
|
+
};
|
|
93
|
+
|
|
69
94
|
/**
|
|
70
95
|
* Merges default options with user provided options
|
|
71
96
|
* @param {UrlPatternOptions} [userOptions={}] - User provided options
|
|
@@ -116,29 +141,63 @@ const parsePattern = (pattern, options) => {
|
|
|
116
141
|
const segments = [];
|
|
117
142
|
let i = 0;
|
|
118
143
|
let inOptional = false;
|
|
144
|
+
let optionalGroupId = 0;
|
|
145
|
+
let parenDepth = 0;
|
|
119
146
|
|
|
120
147
|
while (i < pattern.length) {
|
|
121
148
|
const char = pattern[i];
|
|
122
149
|
|
|
123
|
-
if (char === options.escapeChar
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
150
|
+
if (char === options.escapeChar) {
|
|
151
|
+
// Throw when the escape char is at the end of the pattern — nothing follows
|
|
152
|
+
// it to escape, and the resulting regex would be broken.
|
|
153
|
+
if (i + 1 >= pattern.length) {
|
|
154
|
+
throw new Error(`Invalid pattern: '\\' at position ${i} has nothing to escape`);
|
|
155
|
+
}
|
|
156
|
+
const nextChar = pattern[i + 1];
|
|
157
|
+
// Only treat \ as an escape when followed by a regex metacharacter.
|
|
158
|
+
// For any other character the backslash is literal (e.g. '\:' in a URL
|
|
159
|
+
// is just a backslash followed by ':'). This means `:` cannot be escaped
|
|
160
|
+
// — it has no special meaning in regex, so the escape adds nothing.
|
|
161
|
+
const regexMetachars = '^$.*+?()[]{}|\\';
|
|
162
|
+
if (!regexMetachars.includes(nextChar)) {
|
|
163
|
+
// Not a regex metachar: treat the backslash as a literal character,
|
|
164
|
+
// advance past it so the next character is processed normally.
|
|
165
|
+
segments.push({
|
|
166
|
+
type: 'literal',
|
|
167
|
+
name: '\\',
|
|
168
|
+
regex: '\\\\',
|
|
169
|
+
optional: inOptional,
|
|
170
|
+
optionalGroupId: inOptional ? optionalGroupId : undefined
|
|
171
|
+
});
|
|
172
|
+
i += 1;
|
|
173
|
+
continue;
|
|
174
|
+
} else {
|
|
175
|
+
segments.push({
|
|
176
|
+
type: 'literal',
|
|
177
|
+
name: nextChar,
|
|
178
|
+
regex: escapeRegex(nextChar),
|
|
179
|
+
optional: inOptional,
|
|
180
|
+
optionalGroupId: inOptional ? optionalGroupId : undefined
|
|
181
|
+
});
|
|
182
|
+
i += 2;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
132
185
|
}
|
|
133
186
|
|
|
134
187
|
if (char === options.optionalSegmentStartChar) {
|
|
135
188
|
inOptional = true;
|
|
189
|
+
optionalGroupId++;
|
|
190
|
+
parenDepth++;
|
|
136
191
|
i++;
|
|
137
192
|
continue;
|
|
138
193
|
}
|
|
139
194
|
|
|
140
195
|
if (char === options.optionalSegmentEndChar) {
|
|
196
|
+
if (parenDepth === 0) {
|
|
197
|
+
throw new Error(`Invalid pattern: unmatched '${char}' at position ${i}`);
|
|
198
|
+
}
|
|
141
199
|
inOptional = false;
|
|
200
|
+
parenDepth--;
|
|
142
201
|
i++;
|
|
143
202
|
continue;
|
|
144
203
|
}
|
|
@@ -146,9 +205,10 @@ const parsePattern = (pattern, options) => {
|
|
|
146
205
|
if (char === options.wildcardChar) {
|
|
147
206
|
segments.push({
|
|
148
207
|
type: 'wildcard',
|
|
149
|
-
name:
|
|
208
|
+
name: options.wildcardName,
|
|
150
209
|
regex: '.*',
|
|
151
|
-
optional: inOptional
|
|
210
|
+
optional: inOptional,
|
|
211
|
+
optionalGroupId: inOptional ? optionalGroupId : undefined
|
|
152
212
|
});
|
|
153
213
|
i++;
|
|
154
214
|
continue;
|
|
@@ -158,8 +218,12 @@ const parsePattern = (pattern, options) => {
|
|
|
158
218
|
const remaining = pattern.slice(i + 1);
|
|
159
219
|
let nameEnd = 0;
|
|
160
220
|
const charset = options.segmentNameCharset || '';
|
|
161
|
-
|
|
221
|
+
const endChar = options.segmentNameEndChar;
|
|
222
|
+
|
|
162
223
|
for (let j = 0; j < remaining.length; j++) {
|
|
224
|
+
if (endChar && remaining[j] === endChar) {
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
163
227
|
if (!charset.includes(remaining[j])) {
|
|
164
228
|
break;
|
|
165
229
|
}
|
|
@@ -167,54 +231,77 @@ const parsePattern = (pattern, options) => {
|
|
|
167
231
|
}
|
|
168
232
|
|
|
169
233
|
const name = remaining.slice(0, nameEnd);
|
|
170
|
-
|
|
234
|
+
const consumeEndChar = !!(endChar && remaining[nameEnd] === endChar);
|
|
235
|
+
|
|
171
236
|
if (name.length > 0) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
valueCharset = valueCharset.replace(/-/g, '');
|
|
175
|
-
valueCharset += '-';
|
|
176
|
-
}
|
|
177
|
-
const escapedValueCharset = valueCharset.replace(/\]/g, '\\]');
|
|
237
|
+
const valueCharset = options.segmentValueCharset || '';
|
|
238
|
+
const escapedValueCharset = escapeCharClass(valueCharset);
|
|
178
239
|
const valueRegex = `([${escapedValueCharset}]+)`;
|
|
179
|
-
|
|
240
|
+
|
|
180
241
|
segments.push({
|
|
181
242
|
type: 'named',
|
|
182
243
|
name,
|
|
183
244
|
regex: valueRegex,
|
|
184
|
-
optional: inOptional
|
|
245
|
+
optional: inOptional,
|
|
246
|
+
optionalGroupId: inOptional ? optionalGroupId : undefined
|
|
185
247
|
});
|
|
186
|
-
i += 1 + nameEnd;
|
|
248
|
+
i += 1 + nameEnd + (consumeEndChar ? 1 : 0);
|
|
187
249
|
continue;
|
|
188
250
|
}
|
|
251
|
+
|
|
252
|
+
// segmentNameStartChar at end of pattern, or followed by a non-charset
|
|
253
|
+
// character that is also a special char (e.g. ':)').
|
|
254
|
+
throw new Error(`Invalid pattern: '${char}' at position ${i} has no segment name`);
|
|
189
255
|
}
|
|
190
256
|
|
|
191
257
|
const literalEnd = findNextSpecialChar(pattern, i, options);
|
|
192
|
-
|
|
258
|
+
|
|
193
259
|
if (literalEnd > i) {
|
|
194
260
|
const literal = pattern.slice(i, literalEnd);
|
|
195
261
|
segments.push({
|
|
196
262
|
type: 'literal',
|
|
197
263
|
name: literal,
|
|
198
264
|
regex: escapeRegex(literal),
|
|
199
|
-
optional: inOptional
|
|
265
|
+
optional: inOptional,
|
|
266
|
+
optionalGroupId: inOptional ? optionalGroupId : undefined
|
|
200
267
|
});
|
|
201
268
|
i = literalEnd;
|
|
202
269
|
continue;
|
|
203
270
|
}
|
|
204
271
|
|
|
205
|
-
i
|
|
272
|
+
// literalEnd === i: current char itself is a special char with no
|
|
273
|
+
// name/value following. Throw for clarity.
|
|
274
|
+
throw new Error(`Invalid pattern: '${char}' at position ${i} has no segment name`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (parenDepth !== 0) {
|
|
278
|
+
throw new Error(`Invalid pattern: unclosed '${options.optionalSegmentStartChar}'`);
|
|
206
279
|
}
|
|
207
280
|
|
|
208
281
|
return segments;
|
|
209
282
|
};
|
|
210
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Returns true when a value should be treated as "absent" for an optional segment.
|
|
286
|
+
* @param {*} val - Value to inspect
|
|
287
|
+
* @returns {boolean}
|
|
288
|
+
*/
|
|
289
|
+
const isAbsentValue = (val) => {
|
|
290
|
+
if (val === undefined || val === null || val === '') return true;
|
|
291
|
+
// Number.isNaN catches numeric NaN values that would otherwise stringify to
|
|
292
|
+
// the literal string "NaN" in a generated URL.
|
|
293
|
+
if (typeof val === 'number' && Number.isNaN(val)) return true;
|
|
294
|
+
if (Array.isArray(val) && val.length === 0) return true;
|
|
295
|
+
return false;
|
|
296
|
+
};
|
|
297
|
+
|
|
211
298
|
/**
|
|
212
299
|
* Compiles segments into a regex pattern
|
|
213
300
|
* @param {Array<ParsedSegment>} segments - Parsed segments
|
|
214
301
|
* @param {UrlPatternOptions} options - Options
|
|
215
302
|
* @returns {{regex: string, segmentNames: Array<SegmentName>}} Compiled regex and segment names
|
|
216
303
|
*/
|
|
217
|
-
const compileRegex = (segments,
|
|
304
|
+
const compileRegex = (segments, options) => {
|
|
218
305
|
let regex = '^';
|
|
219
306
|
let groupIndex = 0;
|
|
220
307
|
/** @type {Array<SegmentName>} */
|
|
@@ -227,13 +314,14 @@ const compileRegex = (segments, _options) => {
|
|
|
227
314
|
if (segment.optional) {
|
|
228
315
|
let optionalPart = '';
|
|
229
316
|
let j = i;
|
|
230
|
-
|
|
231
|
-
|
|
317
|
+
const currentGroupId = segment.optionalGroupId;
|
|
318
|
+
|
|
319
|
+
while (j < segments.length && segments[j].optional && segments[j].optionalGroupId === currentGroupId) {
|
|
232
320
|
const seg = segments[j];
|
|
233
321
|
|
|
234
322
|
if (seg.type === 'wildcard') {
|
|
235
323
|
optionalPart += '(.*)';
|
|
236
|
-
segmentNames.push({ name:
|
|
324
|
+
segmentNames.push({ name: options.wildcardName, index: groupIndex, type: 'wildcard' });
|
|
237
325
|
groupIndex++;
|
|
238
326
|
} else if (seg.type === 'named') {
|
|
239
327
|
optionalPart += seg.regex;
|
|
@@ -253,7 +341,7 @@ const compileRegex = (segments, _options) => {
|
|
|
253
341
|
|
|
254
342
|
if (segment.type === 'wildcard') {
|
|
255
343
|
regex += '(.*)';
|
|
256
|
-
segmentNames.push({ name:
|
|
344
|
+
segmentNames.push({ name: options.wildcardName, index: groupIndex, type: 'wildcard' });
|
|
257
345
|
groupIndex++;
|
|
258
346
|
} else if (segment.type === 'named') {
|
|
259
347
|
regex += segment.regex;
|
|
@@ -280,7 +368,7 @@ const compileRegex = (segments, _options) => {
|
|
|
280
368
|
const makePattern = (pattern, options = {}) => {
|
|
281
369
|
const mergedOptions = mergeOptions(options);
|
|
282
370
|
const segments = parsePattern(pattern, mergedOptions);
|
|
283
|
-
const { regex, segmentNames } = compileRegex(segments);
|
|
371
|
+
const { regex, segmentNames } = compileRegex(segments, mergedOptions);
|
|
284
372
|
|
|
285
373
|
return {
|
|
286
374
|
regex,
|
|
@@ -300,9 +388,20 @@ const makePattern = (pattern, options = {}) => {
|
|
|
300
388
|
* @returns {CompiledPattern} Compiled pattern
|
|
301
389
|
*/
|
|
302
390
|
const makePatternFromRegex = (regex, keys = []) => {
|
|
391
|
+
// Strip the `g` and `y` flags. The `g` flag makes `RegExp.prototype.exec`
|
|
392
|
+
// advance `lastIndex` between calls, which would cause subsequent matches
|
|
393
|
+
// to start from the wrong position. The `y` (sticky) flag requires a match
|
|
394
|
+
// starting exactly at `lastIndex`, which is incompatible with our
|
|
395
|
+
// anchored, single-match contract. Other flags (`i`, `m`, `s`, `d`, `u`)
|
|
396
|
+
// are preserved.
|
|
397
|
+
const safeFlags = regex.flags.replace(/[gy]/g, '');
|
|
398
|
+
const regexObj = safeFlags === regex.flags
|
|
399
|
+
? regex
|
|
400
|
+
: new RegExp(regex.source, safeFlags);
|
|
401
|
+
|
|
303
402
|
return {
|
|
304
403
|
regex: regex.source,
|
|
305
|
-
regexObj
|
|
404
|
+
regexObj,
|
|
306
405
|
segments: [],
|
|
307
406
|
segmentNames: keys.map((name, index) => ({ name, index, type: 'named' })),
|
|
308
407
|
options: DEFAULT_OPTIONS,
|
|
@@ -318,6 +417,14 @@ const makePatternFromRegex = (regex, keys = []) => {
|
|
|
318
417
|
* @returns {Object|null} Extracted values or null if no match
|
|
319
418
|
*/
|
|
320
419
|
const match = (compiled, str) => {
|
|
420
|
+
// Guard against non-string input. `RegExp.prototype.exec` coerces with
|
|
421
|
+
// String(), so `match(123)` would silently try to match "123" — almost
|
|
422
|
+
// certainly not what the caller meant. Throw a TypeError so the misuse
|
|
423
|
+
// surfaces immediately.
|
|
424
|
+
if (typeof str !== 'string') {
|
|
425
|
+
throw new TypeError(`pattern.match() requires a string, got ${typeof str}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
321
428
|
const matchResult = compiled.regexObj.exec(str);
|
|
322
429
|
|
|
323
430
|
if (!matchResult) {
|
|
@@ -329,20 +436,40 @@ const match = (compiled, str) => {
|
|
|
329
436
|
const result = {};
|
|
330
437
|
compiled.keys.forEach((key, index) => {
|
|
331
438
|
const val = matchResult[index + 1];
|
|
332
|
-
|
|
439
|
+
// Undefined means the optional group didn't participate — omit the key
|
|
440
|
+
// so this is consistent with string-pattern behaviour.
|
|
441
|
+
if (val !== undefined) {
|
|
442
|
+
result[key] = val;
|
|
443
|
+
}
|
|
333
444
|
});
|
|
334
445
|
return result;
|
|
335
446
|
}
|
|
447
|
+
// No keys, no groups → return the captured values as an array. If the
|
|
448
|
+
// regex had no capture groups, this is an empty array; we keep the
|
|
449
|
+
// existing behaviour (documented in the README's regex example) rather
|
|
450
|
+
// than coercing to `{}` and breaking callers that rely on the array.
|
|
336
451
|
return matchResult.slice(1);
|
|
337
452
|
}
|
|
338
453
|
|
|
339
454
|
const result = {};
|
|
340
455
|
const usedNames = new Set();
|
|
341
456
|
|
|
457
|
+
// Track which segment names are wildcards so their empty-string captures survive
|
|
458
|
+
// the cleanup loop. An empty wildcard result (e.g. '/files/*' on '/files/')
|
|
459
|
+
// is a valid match result.
|
|
460
|
+
const wildcardNames = new Set();
|
|
461
|
+
for (const seg of compiled.segmentNames) {
|
|
462
|
+
if (seg.type === 'wildcard') wildcardNames.add(seg.name);
|
|
463
|
+
}
|
|
464
|
+
|
|
342
465
|
for (let i = 0; i < compiled.segmentNames.length; i++) {
|
|
343
466
|
const segInfo = compiled.segmentNames[i];
|
|
344
|
-
const
|
|
345
|
-
|
|
467
|
+
const hasCapture = segInfo.index + 1 in matchResult;
|
|
468
|
+
// When an optional group didn't participate, the regex returns undefined.
|
|
469
|
+
// Normalize to '' so both non-isRegex and isRegex paths behave the same.
|
|
470
|
+
const rawValue = hasCapture ? matchResult[segInfo.index + 1] : '';
|
|
471
|
+
const value = rawValue === undefined ? '' : rawValue;
|
|
472
|
+
|
|
346
473
|
if (usedNames.has(segInfo.name)) {
|
|
347
474
|
if (!Array.isArray(result[segInfo.name])) {
|
|
348
475
|
result[segInfo.name] = [result[segInfo.name]];
|
|
@@ -354,8 +481,19 @@ const match = (compiled, str) => {
|
|
|
354
481
|
}
|
|
355
482
|
}
|
|
356
483
|
|
|
484
|
+
// Delete empty-string entries, but preserve wildcards — an empty capture is
|
|
485
|
+
// a valid match result for wildcards. Also clean null/undefined from arrays.
|
|
357
486
|
for (const key in result) {
|
|
358
|
-
|
|
487
|
+
const val = result[key];
|
|
488
|
+
if (Array.isArray(val)) {
|
|
489
|
+
// Filter out empty-string entries from arrays (but preserve wildcards).
|
|
490
|
+
const filtered = val.filter(v => v !== '' || wildcardNames.has(key));
|
|
491
|
+
if (filtered.length === 0) {
|
|
492
|
+
delete result[key];
|
|
493
|
+
} else {
|
|
494
|
+
result[key] = filtered;
|
|
495
|
+
}
|
|
496
|
+
} else if (val === '' && !wildcardNames.has(key)) {
|
|
359
497
|
delete result[key];
|
|
360
498
|
}
|
|
361
499
|
}
|
|
@@ -384,37 +522,38 @@ const stringify = (compiled, values = {}) => {
|
|
|
384
522
|
if (segment.optional) {
|
|
385
523
|
let optionalPart = '';
|
|
386
524
|
let j = i;
|
|
387
|
-
|
|
388
|
-
|
|
525
|
+
const currentGroupId = segment.optionalGroupId;
|
|
526
|
+
|
|
527
|
+
while (j < compiled.segments.length && compiled.segments[j].optional && compiled.segments[j].optionalGroupId === currentGroupId) {
|
|
389
528
|
const seg = compiled.segments[j];
|
|
390
|
-
|
|
529
|
+
|
|
391
530
|
if (seg.type === 'literal') {
|
|
392
531
|
optionalPart += seg.name;
|
|
393
532
|
} else if (seg.type === 'named') {
|
|
394
533
|
const val = values[seg.name];
|
|
395
|
-
if (val
|
|
396
|
-
optionalPart += Array.isArray(val) ? val.join('/') : val;
|
|
397
|
-
} else {
|
|
534
|
+
if (isAbsentValue(val)) {
|
|
398
535
|
optionalPart = '';
|
|
399
536
|
break;
|
|
400
537
|
}
|
|
538
|
+
optionalPart += Array.isArray(val) ? val.join('/') : val;
|
|
401
539
|
} else if (seg.type === 'wildcard') {
|
|
402
|
-
const val = values.
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
540
|
+
const val = values[compiled.options.wildcardName];
|
|
541
|
+
// Wildcards inside an optional group still consume values[wildcardName]
|
|
542
|
+
// when present; if absent, the whole group is wiped.
|
|
543
|
+
if (isAbsentValue(val)) {
|
|
406
544
|
optionalPart = '';
|
|
407
545
|
break;
|
|
408
546
|
}
|
|
547
|
+
optionalPart += Array.isArray(val) ? val.join('/') : val;
|
|
409
548
|
}
|
|
410
|
-
|
|
549
|
+
|
|
411
550
|
j++;
|
|
412
551
|
}
|
|
413
|
-
|
|
552
|
+
|
|
414
553
|
if (optionalPart !== '') {
|
|
415
554
|
result += optionalPart;
|
|
416
555
|
}
|
|
417
|
-
|
|
556
|
+
|
|
418
557
|
if (i === j) {
|
|
419
558
|
i++;
|
|
420
559
|
} else {
|
|
@@ -427,13 +566,15 @@ const stringify = (compiled, values = {}) => {
|
|
|
427
566
|
result += segment.name;
|
|
428
567
|
} else if (segment.type === 'named') {
|
|
429
568
|
const value = values[segment.name];
|
|
430
|
-
if (value
|
|
569
|
+
if (isAbsentValue(value)) {
|
|
431
570
|
throw new Error(`Missing required value for segment: ${segment.name}`);
|
|
432
571
|
}
|
|
433
572
|
result += Array.isArray(value) ? value.join('/') : value;
|
|
434
573
|
} else if (segment.type === 'wildcard') {
|
|
435
|
-
const value = values.
|
|
436
|
-
|
|
574
|
+
const value = values[compiled.options.wildcardName];
|
|
575
|
+
// Empty string is a valid wildcard match result (e.g. '/files/*' on '/files/')
|
|
576
|
+
// and should be allowed without throwing.
|
|
577
|
+
if (value === undefined || value === null) {
|
|
437
578
|
throw new Error('Missing required wildcard value');
|
|
438
579
|
}
|
|
439
580
|
result += Array.isArray(value) ? value.join('/') : value;
|
|
@@ -462,6 +603,12 @@ class UrlPattern {
|
|
|
462
603
|
/** @type {CompiledPattern} */
|
|
463
604
|
this.compiled = makePattern(pattern, /** @type {UrlPatternOptions} */ (options));
|
|
464
605
|
}
|
|
606
|
+
// The compiled state is exposed as a read-only introspection field; freeze
|
|
607
|
+
// it (and the nested `options` object) so accidental mutation fails loudly
|
|
608
|
+
// instead of silently desynchronising the cached regex. `Object.freeze`
|
|
609
|
+
// is shallow, so we explicitly freeze `options` too.
|
|
610
|
+
Object.freeze(this.compiled.options);
|
|
611
|
+
Object.freeze(this.compiled);
|
|
465
612
|
}
|
|
466
613
|
|
|
467
614
|
/**
|
|
@@ -495,9 +642,11 @@ const urlPattern = (pattern, options = {}) => {
|
|
|
495
642
|
|
|
496
643
|
exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
|
|
497
644
|
exports.UrlPattern = UrlPattern;
|
|
498
|
-
exports.default =
|
|
645
|
+
exports.default = urlPattern;
|
|
499
646
|
exports.makePattern = makePattern;
|
|
500
647
|
exports.makePatternFromRegex = makePatternFromRegex;
|
|
501
648
|
exports.match = match;
|
|
502
649
|
exports.stringify = stringify;
|
|
503
650
|
exports.urlPattern = urlPattern;
|
|
651
|
+
|
|
652
|
+
module.exports = Object.assign(module.exports.default, module.exports);
|