From 964404d35f208c3979cbcb816362ea75d58bb76c Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 18 Aug 2025 17:50:14 -0700 Subject: [PATCH 1/2] Add custom ParseError class --- src/index.spec.ts | 66 ++++++++++++++++++++++++++++------------------- src/index.ts | 47 +++++++++++++++++---------------- 2 files changed, 62 insertions(+), 51 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 9e6cd05..001b8f6 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -6,6 +6,7 @@ import { stringify, pathToRegexp, TokenData, + ParseError, } from "./index.js"; import { PARSER_TESTS, @@ -18,44 +19,65 @@ import { * Dynamically generate the entire test suite. */ describe("path-to-regexp", () => { + describe("ParseError", () => { + it("should contain original path and debug url", () => { + const error = new ParseError( + "Unexpected END at index 7, expected }", + "/{:foo,", + ); + + expect(error).toBeInstanceOf(TypeError); + expect(error.message).toBe( + "Unexpected END at index 7, expected }: /{:foo,; visit https://git.new/pathToRegexpError for info", + ); + expect(error.originalPath).toBe("/{:foo,"); + }); + + it("should omit original url when undefined", () => { + const error = new ParseError( + "Unexpected END at index 7, expected }", + undefined, + ); + + expect(error).toBeInstanceOf(TypeError); + expect(error.message).toBe( + "Unexpected END at index 7, expected }; visit https://git.new/pathToRegexpError for info", + ); + expect(error.originalPath).toBeUndefined(); + }); + }); + describe("parse errors", () => { it("should throw on unbalanced group", () => { expect(() => parse("/{:foo,")).toThrow( - new TypeError( - "Unexpected END at index 7, expected }: /{:foo,; visit https://git.new/pathToRegexpError for info", - ), + new ParseError("Unexpected END at index 7, expected }", "/{:foo,"), ); }); it("should throw on nested unbalanced group", () => { expect(() => parse("/{:foo/{x,y}")).toThrow( - new TypeError( - "Unexpected END at index 12, expected }: /{:foo/{x,y}; visit https://git.new/pathToRegexpError for info", + new ParseError( + "Unexpected END at index 12, expected }", + "/{:foo/{x,y}", ), ); }); it("should throw on missing param name", () => { expect(() => parse("/:/")).toThrow( - new TypeError( - "Missing parameter name at index 2: /:/; visit https://git.new/pathToRegexpError for info", - ), + new ParseError("Missing parameter name at index 2", "/:/"), ); }); it("should throw on missing wildcard name", () => { expect(() => parse("/*/")).toThrow( - new TypeError( - "Missing parameter name at index 2: /*/; visit https://git.new/pathToRegexpError for info", - ), + new ParseError("Missing parameter name at index 2", "/*/"), ); }); it("should throw on unterminated quote", () => { expect(() => parse('/:"foo')).toThrow( - new TypeError( - 'Unterminated quote at index 2: /:"foo; visit https://git.new/pathToRegexpError for info', - ), + new ParseError("Unterminated quote at index 2", '/:"foo'), ); }); }); @@ -105,9 +127,7 @@ describe("path-to-regexp", () => { describe("pathToRegexp errors", () => { it("should throw when missing text between params", () => { expect(() => pathToRegexp("/:foo:bar")).toThrow( - new TypeError( - 'Missing text before "bar": /:foo:bar; visit https://git.new/pathToRegexpError for info', - ), + new ParseError('Missing text before "bar" param', "/:foo:bar"), ); }); @@ -119,11 +139,7 @@ describe("path-to-regexp", () => { { type: "param", name: "b" }, ]), ), - ).toThrow( - new TypeError( - 'Missing text before "b"; visit https://git.new/pathToRegexpError for info', - ), - ); + ).toThrow(new ParseError('Missing text before "b" param', undefined)); }); it("should throw with `originalPath` when missing text between params using TokenData", () => { @@ -137,11 +153,7 @@ describe("path-to-regexp", () => { "/[a][b]", ), ), - ).toThrow( - new TypeError( - 'Missing text before "b": /[a][b]; visit https://git.new/pathToRegexpError for info', - ), - ); + ).toThrow(new ParseError('Missing text before "b" param', "/[a][b]")); }); it("should contain the error line", () => { diff --git a/src/index.ts b/src/index.ts index 4f70fbe..42b034f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,6 @@ const DEFAULT_DELIMITER = "/"; const NOOP_VALUE = (value: string) => value; const ID_START = /^[$_\p{ID_Start}]$/u; const ID_CONTINUE = /^[$\u200c\u200d\p{ID_Continue}]$/u; -const DEBUG_URL = "https://git.new/pathToRegexpError"; /** * Encode a string into another string. @@ -112,16 +111,6 @@ function escape(str: string) { return str.replace(/[.+*?^${}()[\]|/\\]/g, "\\$&"); } -/** - * Format error so it's easier to debug. - */ -function errorMessage(text: string, originalPath: string | undefined) { - let message = text; - if (originalPath !== undefined) message += `: ${originalPath}`; - message += `; visit ${DEBUG_URL} for info`; - return message; -} - /** * Plain text. */ @@ -179,6 +168,21 @@ export class TokenData { ) {} } +/** + * ParseError is thrown when there is an error processing the path. + */ +export class ParseError extends TypeError { + constructor( + message: string, + public readonly originalPath: string | undefined, + ) { + let text = message; + if (originalPath) text += `: ${originalPath}`; + text += `; visit https://git.new/pathToRegexpError for info`; + super(text); + } +} + /** * Parse a string for the raw tokens. */ @@ -215,16 +219,12 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { } if (pos) { - throw new TypeError( - errorMessage(`Unterminated quote at index ${pos}`, str), - ); + throw new ParseError(`Unterminated quote at index ${pos}`, str); } } if (!value) { - throw new TypeError( - errorMessage(`Missing parameter name at index ${index}`, str), - ); + throw new ParseError(`Missing parameter name at index ${index}`, str); } return value; @@ -292,11 +292,9 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { continue; } - throw new TypeError( - errorMessage( - `Unexpected ${type} at index ${index}, expected ${endType}`, - str, - ), + throw new ParseError( + `Unexpected ${type} at index ${index}, expected ${endType}`, + str, ); } @@ -564,8 +562,9 @@ function toRegExpSource( if (token.type === "param" || token.type === "wildcard") { if (!isSafeSegmentParam && !backtrack) { - throw new TypeError( - errorMessage(`Missing text before "${token.name}"`, originalPath), + throw new ParseError( + `Missing text before "${token.name}" ${token.type}`, + originalPath, ); } From 667d91f38f9746564148b72fdbc17f727f95fef5 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Tue, 19 Aug 2025 16:25:43 -0700 Subject: [PATCH 2/2] Rename to PathError --- src/index.spec.ts | 25 +++++++++++-------------- src/index.ts | 10 +++++----- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 001b8f6..a1a9a13 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -6,7 +6,7 @@ import { stringify, pathToRegexp, TokenData, - ParseError, + PathError, } from "./index.js"; import { PARSER_TESTS, @@ -21,7 +21,7 @@ import { describe("path-to-regexp", () => { describe("ParseError", () => { it("should contain original path and debug url", () => { - const error = new ParseError( + const error = new PathError( "Unexpected END at index 7, expected }", "/{:foo,", ); @@ -34,7 +34,7 @@ describe("path-to-regexp", () => { }); it("should omit original url when undefined", () => { - const error = new ParseError( + const error = new PathError( "Unexpected END at index 7, expected }", undefined, ); @@ -50,34 +50,31 @@ describe("path-to-regexp", () => { describe("parse errors", () => { it("should throw on unbalanced group", () => { expect(() => parse("/{:foo,")).toThrow( - new ParseError("Unexpected END at index 7, expected }", "/{:foo,"), + new PathError("Unexpected END at index 7, expected }", "/{:foo,"), ); }); it("should throw on nested unbalanced group", () => { expect(() => parse("/{:foo/{x,y}")).toThrow( - new ParseError( - "Unexpected END at index 12, expected }", - "/{:foo/{x,y}", - ), + new PathError("Unexpected END at index 12, expected }", "/{:foo/{x,y}"), ); }); it("should throw on missing param name", () => { expect(() => parse("/:/")).toThrow( - new ParseError("Missing parameter name at index 2", "/:/"), + new PathError("Missing parameter name at index 2", "/:/"), ); }); it("should throw on missing wildcard name", () => { expect(() => parse("/*/")).toThrow( - new ParseError("Missing parameter name at index 2", "/*/"), + new PathError("Missing parameter name at index 2", "/*/"), ); }); it("should throw on unterminated quote", () => { expect(() => parse('/:"foo')).toThrow( - new ParseError("Unterminated quote at index 2", '/:"foo'), + new PathError("Unterminated quote at index 2", '/:"foo'), ); }); }); @@ -127,7 +124,7 @@ describe("path-to-regexp", () => { describe("pathToRegexp errors", () => { it("should throw when missing text between params", () => { expect(() => pathToRegexp("/:foo:bar")).toThrow( - new ParseError('Missing text before "bar" param', "/:foo:bar"), + new PathError('Missing text before "bar" param', "/:foo:bar"), ); }); @@ -139,7 +136,7 @@ describe("path-to-regexp", () => { { type: "param", name: "b" }, ]), ), - ).toThrow(new ParseError('Missing text before "b" param', undefined)); + ).toThrow(new PathError('Missing text before "b" param', undefined)); }); it("should throw with `originalPath` when missing text between params using TokenData", () => { @@ -153,7 +150,7 @@ describe("path-to-regexp", () => { "/[a][b]", ), ), - ).toThrow(new ParseError('Missing text before "b" param', "/[a][b]")); + ).toThrow(new PathError('Missing text before "b" param', "/[a][b]")); }); it("should contain the error line", () => { diff --git a/src/index.ts b/src/index.ts index 42b034f..0df51f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -171,7 +171,7 @@ export class TokenData { /** * ParseError is thrown when there is an error processing the path. */ -export class ParseError extends TypeError { +export class PathError extends TypeError { constructor( message: string, public readonly originalPath: string | undefined, @@ -219,12 +219,12 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { } if (pos) { - throw new ParseError(`Unterminated quote at index ${pos}`, str); + throw new PathError(`Unterminated quote at index ${pos}`, str); } } if (!value) { - throw new ParseError(`Missing parameter name at index ${index}`, str); + throw new PathError(`Missing parameter name at index ${index}`, str); } return value; @@ -292,7 +292,7 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { continue; } - throw new ParseError( + throw new PathError( `Unexpected ${type} at index ${index}, expected ${endType}`, str, ); @@ -562,7 +562,7 @@ function toRegExpSource( if (token.type === "param" || token.type === "wildcard") { if (!isSafeSegmentParam && !backtrack) { - throw new ParseError( + throw new PathError( `Missing text before "${token.name}" ${token.type}`, originalPath, );