Skip to content

Commit b25a26b

Browse files
committed
Add original path to error messages
1 parent 19c5659 commit b25a26b

File tree

3 files changed

+119
-65
lines changed

3 files changed

+119
-65
lines changed

src/cases.spec.ts

Lines changed: 58 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -41,64 +41,88 @@ export interface MatchTestSet {
4141
export const PARSER_TESTS: ParserTestSet[] = [
4242
{
4343
path: "/",
44-
expected: new TokenData([{ type: "text", value: "/" }]),
44+
expected: new TokenData([{ type: "text", value: "/" }], "/"),
4545
},
4646
{
4747
path: "/:test",
48-
expected: new TokenData([
49-
{ type: "text", value: "/" },
50-
{ type: "param", name: "test" },
51-
]),
48+
expected: new TokenData(
49+
[
50+
{ type: "text", value: "/" },
51+
{ type: "param", name: "test" },
52+
],
53+
"/:test",
54+
),
5255
},
5356
{
5457
path: '/:"0"',
55-
expected: new TokenData([
56-
{ type: "text", value: "/" },
57-
{ type: "param", name: "0" },
58-
]),
58+
expected: new TokenData(
59+
[
60+
{ type: "text", value: "/" },
61+
{ type: "param", name: "0" },
62+
],
63+
'/:"0"',
64+
),
5965
},
6066
{
6167
path: "/:_",
62-
expected: new TokenData([
63-
{ type: "text", value: "/" },
64-
{ type: "param", name: "_" },
65-
]),
68+
expected: new TokenData(
69+
[
70+
{ type: "text", value: "/" },
71+
{ type: "param", name: "_" },
72+
],
73+
"/:_",
74+
),
6675
},
6776
{
6877
path: "/:café",
69-
expected: new TokenData([
70-
{ type: "text", value: "/" },
71-
{ type: "param", name: "café" },
72-
]),
78+
expected: new TokenData(
79+
[
80+
{ type: "text", value: "/" },
81+
{ type: "param", name: "café" },
82+
],
83+
"/:café",
84+
),
7385
},
7486
{
7587
path: '/:"123"',
76-
expected: new TokenData([
77-
{ type: "text", value: "/" },
78-
{ type: "param", name: "123" },
79-
]),
88+
expected: new TokenData(
89+
[
90+
{ type: "text", value: "/" },
91+
{ type: "param", name: "123" },
92+
],
93+
'/:"123"',
94+
),
8095
},
8196
{
8297
path: '/:"1\\"\\2\\"3"',
83-
expected: new TokenData([
84-
{ type: "text", value: "/" },
85-
{ type: "param", name: '1"2"3' },
86-
]),
98+
expected: new TokenData(
99+
[
100+
{ type: "text", value: "/" },
101+
{ type: "param", name: '1"2"3' },
102+
],
103+
'/:"1\\"\\2\\"3"',
104+
),
87105
},
88106
{
89107
path: "/*path",
90-
expected: new TokenData([
91-
{ type: "text", value: "/" },
92-
{ type: "wildcard", name: "path" },
93-
]),
108+
expected: new TokenData(
109+
[
110+
{ type: "text", value: "/" },
111+
{ type: "wildcard", name: "path" },
112+
],
113+
"/*path",
114+
),
94115
},
95116
{
96117
path: '/:"test"stuff',
97-
expected: new TokenData([
98-
{ type: "text", value: "/" },
99-
{ type: "param", name: "test" },
100-
{ type: "text", value: "stuff" },
101-
]),
118+
expected: new TokenData(
119+
[
120+
{ type: "text", value: "/" },
121+
{ type: "param", name: "test" },
122+
{ type: "text", value: "stuff" },
123+
],
124+
'/:"test"stuff',
125+
),
102126
},
103127
];
104128

src/index.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,38 +15,38 @@ describe("path-to-regexp", () => {
1515
it("should throw on unbalanced group", () => {
1616
expect(() => parse("/{:foo,")).toThrow(
1717
new TypeError(
18-
"Unexpected END at 7, expected }: https://git.new/pathToRegexpError",
18+
"Unexpected END at index 7, expected }: /{:foo,; visit https://git.new/pathToRegexpError for more info",
1919
),
2020
);
2121
});
2222
it("should throw on nested unbalanced group", () => {
2323
expect(() => parse("/{:foo/{x,y}")).toThrow(
2424
new TypeError(
25-
"Unexpected END at 12, expected }: https://git.new/pathToRegexpError",
25+
"Unexpected END at index 12, expected }: /{:foo/{x,y}; visit https://git.new/pathToRegexpError for more info",
2626
),
2727
);
2828
});
2929

3030
it("should throw on missing param name", () => {
3131
expect(() => parse("/:/")).toThrow(
3232
new TypeError(
33-
"Missing parameter name at 2: https://git.new/pathToRegexpError",
33+
"Missing parameter name at index 2: /:/; visit https://git.new/pathToRegexpError for more info",
3434
),
3535
);
3636
});
3737

3838
it("should throw on missing wildcard name", () => {
3939
expect(() => parse("/*/")).toThrow(
4040
new TypeError(
41-
"Missing parameter name at 2: https://git.new/pathToRegexpError",
41+
"Missing parameter name at index 2: /*/; visit https://git.new/pathToRegexpError for more info",
4242
),
4343
);
4444
});
4545

4646
it("should throw on unterminated quote", () => {
4747
expect(() => parse('/:"foo')).toThrow(
4848
new TypeError(
49-
"Unterminated quote at 2: https://git.new/pathToRegexpError",
49+
'Unterminated quote at index 2: /:"foo; visit https://git.new/pathToRegexpError for more info',
5050
),
5151
);
5252
});

src/index.ts

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,16 @@ function escape(str: string) {
112112
return str.replace(/[.+*?^${}()[\]|/\\]/g, "\\$&");
113113
}
114114

115+
/**
116+
* Format error so it's easier to debug.
117+
*/
118+
function errorMessage(message: string, originalPath: string | undefined) {
119+
if (originalPath) {
120+
return `${message}: ${originalPath}; visit ${DEBUG_URL} for more info`;
121+
}
122+
return `${message}; visit ${DEBUG_URL} for more info`;
123+
}
124+
115125
/**
116126
* Tokenize input string.
117127
*/
@@ -145,12 +155,16 @@ function* lexer(str: string): Generator<LexToken, LexToken> {
145155
}
146156

147157
if (pos) {
148-
throw new TypeError(`Unterminated quote at ${pos}: ${DEBUG_URL}`);
158+
throw new TypeError(
159+
errorMessage(`Unterminated quote at index ${pos}`, str),
160+
);
149161
}
150162
}
151163

152164
if (!value) {
153-
throw new TypeError(`Missing parameter name at ${i}: ${DEBUG_URL}`);
165+
throw new TypeError(
166+
errorMessage(`Missing parameter name at index ${i}`, str),
167+
);
154168
}
155169

156170
return value;
@@ -180,12 +194,15 @@ function* lexer(str: string): Generator<LexToken, LexToken> {
180194

181195
class Iter {
182196
private _peek?: LexToken;
197+
private _tokens: Generator<LexToken, LexToken>;
183198

184-
constructor(private tokens: Generator<LexToken, LexToken>) {}
199+
constructor(private originalPath: string) {
200+
this._tokens = lexer(originalPath);
201+
}
185202

186203
peek(): LexToken {
187204
if (!this._peek) {
188-
const next = this.tokens.next();
205+
const next = this._tokens.next();
189206
this._peek = next.value;
190207
}
191208
return this._peek;
@@ -203,7 +220,10 @@ class Iter {
203220
if (value !== undefined) return value;
204221
const { type: nextType, index } = this.peek();
205222
throw new TypeError(
206-
`Unexpected ${nextType} at ${index}, expected ${type}: ${DEBUG_URL}`,
223+
errorMessage(
224+
`Unexpected ${nextType} at index ${index}, expected ${type}`,
225+
this.originalPath,
226+
),
207227
);
208228
}
209229

@@ -268,15 +288,18 @@ export type Token = Text | Parameter | Wildcard | Group;
268288
* Tokenized path instance.
269289
*/
270290
export class TokenData {
271-
constructor(public readonly tokens: Token[]) {}
291+
constructor(
292+
public readonly tokens: Token[],
293+
public readonly originalPath?: string,
294+
) {}
272295
}
273296

274297
/**
275298
* Parse a string for the raw tokens.
276299
*/
277300
export function parse(str: string, options: ParseOptions = {}): TokenData {
278301
const { encodePath = NOOP_VALUE } = options;
279-
const it = new Iter(lexer(str));
302+
const it = new Iter(str);
280303

281304
function consume(endType: TokenType): Token[] {
282305
const tokens: Token[] = [];
@@ -318,7 +341,7 @@ export function parse(str: string, options: ParseOptions = {}): TokenData {
318341
}
319342

320343
const tokens = consume("END");
321-
return new TokenData(tokens);
344+
return new TokenData(tokens, str);
322345
}
323346

324347
/**
@@ -496,12 +519,8 @@ export function pathToRegexp(
496519
trailing = true,
497520
} = options;
498521
const keys: Keys = [];
499-
const sources: string[] = [];
500522
const flags = sensitive ? "" : "i";
501-
502-
for (const seq of flat(path, options)) {
503-
sources.push(toRegExp(seq, delimiter, keys));
504-
}
523+
const sources = Array.from(toRegExps(path, delimiter, keys, options));
505524

506525
let pattern = `^(?:${sources.join("|")})`;
507526
if (trailing) pattern += `(?:${escape(delimiter)}$)?`;
@@ -511,35 +530,39 @@ export function pathToRegexp(
511530
return { regexp, keys };
512531
}
513532

514-
/**
515-
* Flattened token set.
516-
*/
517-
type Flattened = Text | Parameter | Wildcard;
518-
519533
/**
520534
* Path or array of paths to normalize.
521535
*/
522-
function* flat(
536+
function* toRegExps(
523537
path: Path | Path[],
538+
delimiter: string,
539+
keys: Keys,
524540
options: ParseOptions,
525-
): Generator<Flattened[]> {
541+
): Generator<string> {
526542
if (Array.isArray(path)) {
527-
for (const p of path) yield* flat(p, options);
543+
for (const p of path) yield* toRegExps(p, delimiter, keys, options);
528544
return;
529545
}
530546

531547
const data = path instanceof TokenData ? path : parse(path, options);
532-
yield* flatten(data.tokens, 0, []);
548+
for (const tokens of flatten(data.tokens, 0, [])) {
549+
yield toRegExp(tokens, delimiter, keys, data.originalPath);
550+
}
533551
}
534552

553+
/**
554+
* Flattened token set.
555+
*/
556+
type FlatToken = Text | Parameter | Wildcard;
557+
535558
/**
536559
* Generate a flat list of sequence tokens from the given tokens.
537560
*/
538561
function* flatten(
539562
tokens: Token[],
540563
index: number,
541-
init: Flattened[],
542-
): Generator<Flattened[]> {
564+
init: FlatToken[],
565+
): Generator<FlatToken[]> {
543566
if (index === tokens.length) {
544567
return yield init;
545568
}
@@ -560,7 +583,12 @@ function* flatten(
560583
/**
561584
* Transform a flat sequence of tokens into a regular expression.
562585
*/
563-
function toRegExp(tokens: Flattened[], delimiter: string, keys: Keys) {
586+
function toRegExp(
587+
tokens: FlatToken[],
588+
delimiter: string,
589+
keys: Keys,
590+
originalPath: string | undefined,
591+
) {
564592
let result = "";
565593
let backtrack = "";
566594
let isSafeSegmentParam = true;
@@ -575,7 +603,9 @@ function toRegExp(tokens: Flattened[], delimiter: string, keys: Keys) {
575603

576604
if (token.type === "param" || token.type === "wildcard") {
577605
if (!isSafeSegmentParam && !backtrack) {
578-
throw new TypeError(`Missing text after "${token.name}": ${DEBUG_URL}`);
606+
throw new TypeError(
607+
errorMessage(`Missing text after "${token.name}"`, originalPath),
608+
);
579609
}
580610

581611
if (token.type === "param") {

0 commit comments

Comments
 (0)