diff --git a/src/index.spec.ts b/src/index.spec.ts index 9e6cd05..a1a9a13 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -6,6 +6,7 @@ import { stringify, pathToRegexp, TokenData, + PathError, } from "./index.js"; import { PARSER_TESTS, @@ -18,44 +19,62 @@ import { * Dynamically generate the entire test suite. */ describe("path-to-regexp", () => { + describe("ParseError", () => { + it("should contain original path and debug url", () => { + const error = new PathError( + "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 PathError( + "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 PathError("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 PathError("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 PathError("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 PathError("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 PathError("Unterminated quote at index 2", '/:"foo'), ); }); }); @@ -105,9 +124,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 PathError('Missing text before "bar" param', "/:foo:bar"), ); }); @@ -119,11 +136,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 PathError('Missing text before "b" param', undefined)); }); it("should throw with `originalPath` when missing text between params using TokenData", () => { @@ -137,11 +150,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 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 4f70fbe..0df51f9 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 PathError 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 PathError(`Unterminated quote at index ${pos}`, str); } } if (!value) { - throw new TypeError( - errorMessage(`Missing parameter name at index ${index}`, str), - ); + throw new PathError(`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 PathError( + `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 PathError( + `Missing text before "${token.name}" ${token.type}`, + originalPath, ); }