route-parser-ts 1.0.0
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/LICENSE +21 -0
- package/README.md +215 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +254 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +149 -0
- package/package.json +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Marko Stijak (https://github.com/mstijak)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# route-parser-ts
|
|
2
|
+
|
|
3
|
+
A TypeScript URL routing library that parses route patterns and matches them against URL paths. Supports named parameters, splat (wildcard) parameters, and optional segments.
|
|
4
|
+
|
|
5
|
+
Based on [route-parser](https://github.com/rcs/route-parser) by Ryan Sorensen.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install route-parser-ts
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import Route from 'route-parser-ts';
|
|
17
|
+
|
|
18
|
+
// Create a route
|
|
19
|
+
const route = Route('/users/:id');
|
|
20
|
+
|
|
21
|
+
// Match a path
|
|
22
|
+
const result = route.match('/users/123');
|
|
23
|
+
// => { id: '123' }
|
|
24
|
+
|
|
25
|
+
// Reverse a route
|
|
26
|
+
const path = route.reverse({ id: '456' });
|
|
27
|
+
// => '/users/456'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Route Patterns
|
|
31
|
+
|
|
32
|
+
### Static Paths
|
|
33
|
+
|
|
34
|
+
Match exact paths:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
const route = Route('/foo');
|
|
38
|
+
|
|
39
|
+
route.match('/foo'); // => {}
|
|
40
|
+
route.match('/foo?query'); // => {} (query strings are ignored)
|
|
41
|
+
route.match('/bar'); // => false
|
|
42
|
+
route.match('/foobar'); // => false
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Named Parameters
|
|
46
|
+
|
|
47
|
+
Capture path segments with `:param` syntax:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
const route = Route('/users/:id');
|
|
51
|
+
|
|
52
|
+
route.match('/users/123'); // => { id: '123' }
|
|
53
|
+
route.match('/users/abc'); // => { id: 'abc' }
|
|
54
|
+
route.match('/users/'); // => false
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Multiple parameters:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
const route = Route('/users/:userId/posts/:postId');
|
|
61
|
+
|
|
62
|
+
route.match('/users/1/posts/42');
|
|
63
|
+
// => { userId: '1', postId: '42' }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Splat Parameters
|
|
67
|
+
|
|
68
|
+
Capture multiple path segments with `*param` syntax:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
const route = Route('/files/*path');
|
|
72
|
+
|
|
73
|
+
route.match('/files/images/photo.jpg');
|
|
74
|
+
// => { path: 'images/photo.jpg' }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Multiple splats:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const route = Route('/*a/foo/*b');
|
|
81
|
+
|
|
82
|
+
route.match('/zoo/woo/foo/bar/baz');
|
|
83
|
+
// => { a: 'zoo/woo', b: 'bar/baz' }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Mixed Parameters
|
|
87
|
+
|
|
88
|
+
Combine splats and named parameters:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
const route = Route('/books/*section/:title');
|
|
92
|
+
|
|
93
|
+
route.match('/books/some/section/last-words-a-memoir');
|
|
94
|
+
// => { section: 'some/section', title: 'last-words-a-memoir' }
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Optional Segments
|
|
98
|
+
|
|
99
|
+
Make parts of the route optional with parentheses:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const route = Route('/users/:id(/style/:style)');
|
|
103
|
+
|
|
104
|
+
route.match('/users/3');
|
|
105
|
+
// => { id: '3', style: undefined }
|
|
106
|
+
|
|
107
|
+
route.match('/users/3/style/pirate');
|
|
108
|
+
// => { id: '3', style: 'pirate' }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Optional segments starting with a word:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
const route = Route('/things/(option/:first)');
|
|
115
|
+
|
|
116
|
+
route.match('/things/option/bar');
|
|
117
|
+
// => { first: 'bar' }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Nested optional segments:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const route = Route('/users/:id(/style/:style(/more/:param))');
|
|
124
|
+
|
|
125
|
+
route.match('/users/3');
|
|
126
|
+
// => { id: '3', style: undefined, param: undefined }
|
|
127
|
+
|
|
128
|
+
route.match('/users/3/style/pirate');
|
|
129
|
+
// => { id: '3', style: 'pirate', param: undefined }
|
|
130
|
+
|
|
131
|
+
route.match('/users/3/style/pirate/more/things');
|
|
132
|
+
// => { id: '3', style: 'pirate', param: 'things' }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## API
|
|
136
|
+
|
|
137
|
+
### `Route(spec: string)`
|
|
138
|
+
|
|
139
|
+
Creates a new route parser. Can be called with or without `new`.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
const route1 = Route('/users/:id');
|
|
143
|
+
const route2 = new Route('/users/:id');
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Throws an error if `spec` is not provided.
|
|
147
|
+
|
|
148
|
+
### `route.match(path: string): object | false`
|
|
149
|
+
|
|
150
|
+
Matches a path against the route pattern. Returns an object with the captured parameters on success, or `false` if the path doesn't match.
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
const route = Route('/users/:id');
|
|
154
|
+
|
|
155
|
+
route.match('/users/123'); // => { id: '123' }
|
|
156
|
+
route.match('/posts/123'); // => false
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### `route.reverse(params?: object): string | false`
|
|
160
|
+
|
|
161
|
+
Generates a path from the route pattern and provided parameters. Returns the path string on success, or `false` if required parameters are missing.
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
const route = Route('/users/:id/posts/:postId');
|
|
165
|
+
|
|
166
|
+
route.reverse({ id: '1', postId: '42' });
|
|
167
|
+
// => '/users/1/posts/42'
|
|
168
|
+
|
|
169
|
+
route.reverse({ id: '1' });
|
|
170
|
+
// => false (missing required parameter)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
With optional segments:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
const route = Route('/things/(option/:first)');
|
|
177
|
+
|
|
178
|
+
route.reverse({ first: 'bar' });
|
|
179
|
+
// => '/things/option/bar'
|
|
180
|
+
|
|
181
|
+
route.reverse();
|
|
182
|
+
// => '/things/' (optional segment omitted)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Falsy values (like `0`) are handled correctly:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const route = Route('/items/:id?page=:page');
|
|
189
|
+
|
|
190
|
+
route.reverse({ id: 1, page: 0 });
|
|
191
|
+
// => '/items/1?page=0'
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## TypeScript
|
|
195
|
+
|
|
196
|
+
The library is written in TypeScript and includes type definitions.
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
import Route, { RouteParams } from 'route-parser-ts';
|
|
200
|
+
|
|
201
|
+
const route = Route('/users/:id');
|
|
202
|
+
const params: RouteParams | false = route.match('/users/123');
|
|
203
|
+
|
|
204
|
+
if (params) {
|
|
205
|
+
console.log(params.id); // '123'
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Acknowledgments
|
|
210
|
+
|
|
211
|
+
This library is a TypeScript reimplementation of [route-parser](https://github.com/rcs/route-parser) by [Ryan Sorensen](https://github.com/rcs). The original library provides the same functionality in JavaScript.
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route Parser - A TypeScript URL routing library
|
|
3
|
+
* Supports named parameters (:param), splats (*param), and optional segments (())
|
|
4
|
+
*/
|
|
5
|
+
export interface RouteParams {
|
|
6
|
+
[key: string]: string | number | undefined;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* RouteParser class
|
|
10
|
+
*/
|
|
11
|
+
declare class RouteParser {
|
|
12
|
+
spec: string;
|
|
13
|
+
private tokens;
|
|
14
|
+
private regex;
|
|
15
|
+
private paramNames;
|
|
16
|
+
constructor(spec: string);
|
|
17
|
+
/**
|
|
18
|
+
* Match a path against this route
|
|
19
|
+
* Returns params object on match, false otherwise
|
|
20
|
+
*/
|
|
21
|
+
match(path: string): RouteParams | false;
|
|
22
|
+
/**
|
|
23
|
+
* Reverse the route with given parameters
|
|
24
|
+
* Returns the path string on success, false otherwise
|
|
25
|
+
*/
|
|
26
|
+
reverse(params?: RouteParams): string | false;
|
|
27
|
+
/**
|
|
28
|
+
* Check if required params (non-optional) can be fulfilled
|
|
29
|
+
*/
|
|
30
|
+
private canFulfillRequired;
|
|
31
|
+
}
|
|
32
|
+
interface RouteParserConstructor {
|
|
33
|
+
new (spec: string): RouteParser;
|
|
34
|
+
(spec: string): RouteParser;
|
|
35
|
+
}
|
|
36
|
+
declare const Route: RouteParserConstructor;
|
|
37
|
+
export default Route;
|
|
38
|
+
export { Route, RouteParser };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route Parser - A TypeScript URL routing library
|
|
3
|
+
* Supports named parameters (:param), splats (*param), and optional segments (())
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Parse a route specification into tokens
|
|
7
|
+
*/
|
|
8
|
+
function tokenize(spec) {
|
|
9
|
+
const tokens = [];
|
|
10
|
+
let i = 0;
|
|
11
|
+
while (i < spec.length) {
|
|
12
|
+
const char = spec[i];
|
|
13
|
+
if (char === ':') {
|
|
14
|
+
// Named parameter
|
|
15
|
+
i++;
|
|
16
|
+
let name = '';
|
|
17
|
+
while (i < spec.length && /[\w]/.test(spec[i])) {
|
|
18
|
+
name += spec[i];
|
|
19
|
+
i++;
|
|
20
|
+
}
|
|
21
|
+
if (name) {
|
|
22
|
+
tokens.push({ type: 'param', value: name });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else if (char === '*') {
|
|
26
|
+
// Splat parameter
|
|
27
|
+
i++;
|
|
28
|
+
let name = '';
|
|
29
|
+
while (i < spec.length && /[\w]/.test(spec[i])) {
|
|
30
|
+
name += spec[i];
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
if (name) {
|
|
34
|
+
tokens.push({ type: 'splat', value: name });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (char === '(') {
|
|
38
|
+
// Optional segment - find matching closing paren
|
|
39
|
+
i++;
|
|
40
|
+
let depth = 1;
|
|
41
|
+
let optionalContent = '';
|
|
42
|
+
while (i < spec.length && depth > 0) {
|
|
43
|
+
if (spec[i] === '(')
|
|
44
|
+
depth++;
|
|
45
|
+
else if (spec[i] === ')')
|
|
46
|
+
depth--;
|
|
47
|
+
if (depth > 0) {
|
|
48
|
+
optionalContent += spec[i];
|
|
49
|
+
}
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
// Recursively parse the optional content
|
|
53
|
+
const children = tokenize(optionalContent);
|
|
54
|
+
tokens.push({ type: 'optional', value: optionalContent, children });
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// Static text - consume until we hit a special character
|
|
58
|
+
let text = '';
|
|
59
|
+
while (i < spec.length && !':*()'.includes(spec[i])) {
|
|
60
|
+
text += spec[i];
|
|
61
|
+
i++;
|
|
62
|
+
}
|
|
63
|
+
if (text) {
|
|
64
|
+
tokens.push({ type: 'static', value: text });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return tokens;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Escape special regex characters in a string
|
|
72
|
+
*/
|
|
73
|
+
function escapeRegex(str) {
|
|
74
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Build a regex pattern from tokens
|
|
78
|
+
*/
|
|
79
|
+
function buildRegex(tokens) {
|
|
80
|
+
let pattern = '';
|
|
81
|
+
const paramNames = [];
|
|
82
|
+
for (const token of tokens) {
|
|
83
|
+
switch (token.type) {
|
|
84
|
+
case 'static':
|
|
85
|
+
pattern += escapeRegex(token.value);
|
|
86
|
+
break;
|
|
87
|
+
case 'param':
|
|
88
|
+
// Named parameter matches one path segment (no slashes)
|
|
89
|
+
pattern += '([^/]+)';
|
|
90
|
+
paramNames.push(token.value);
|
|
91
|
+
break;
|
|
92
|
+
case 'splat':
|
|
93
|
+
// Splat matches any characters including slashes (non-greedy within constraints)
|
|
94
|
+
pattern += '(.+?)';
|
|
95
|
+
paramNames.push(token.value);
|
|
96
|
+
break;
|
|
97
|
+
case 'optional':
|
|
98
|
+
// Optional segment - recursively build and wrap in optional group
|
|
99
|
+
if (token.children) {
|
|
100
|
+
const { pattern: childPattern, paramNames: childNames } = buildRegex(token.children);
|
|
101
|
+
pattern += `(?:${childPattern})?`;
|
|
102
|
+
paramNames.push(...childNames);
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { pattern, paramNames };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Attempt to reverse a route with given parameters
|
|
111
|
+
*/
|
|
112
|
+
function reverseTokens(tokens, params) {
|
|
113
|
+
let result = '';
|
|
114
|
+
for (const token of tokens) {
|
|
115
|
+
switch (token.type) {
|
|
116
|
+
case 'static':
|
|
117
|
+
result += token.value;
|
|
118
|
+
break;
|
|
119
|
+
case 'param':
|
|
120
|
+
case 'splat': {
|
|
121
|
+
const value = params[token.value];
|
|
122
|
+
if (value === undefined || value === null || value === '') {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
result += String(value);
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case 'optional':
|
|
129
|
+
if (token.children) {
|
|
130
|
+
// Try to fill the optional segment
|
|
131
|
+
const optionalResult = reverseTokens(token.children, params);
|
|
132
|
+
if (optionalResult !== false) {
|
|
133
|
+
result += optionalResult;
|
|
134
|
+
}
|
|
135
|
+
// If it fails, just skip the optional part (don't return false)
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Check if tokens can be fulfilled with the given params
|
|
144
|
+
*/
|
|
145
|
+
function canFulfill(tokens, params) {
|
|
146
|
+
for (const token of tokens) {
|
|
147
|
+
if (token.type === 'param' || token.type === 'splat') {
|
|
148
|
+
const value = params[token.value];
|
|
149
|
+
if (value === undefined || value === null || value === '') {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else if (token.type === 'optional' && token.children) {
|
|
154
|
+
// Optional segments don't need to be fulfilled
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* RouteParser class
|
|
162
|
+
*/
|
|
163
|
+
class RouteParser {
|
|
164
|
+
spec;
|
|
165
|
+
tokens;
|
|
166
|
+
regex;
|
|
167
|
+
paramNames;
|
|
168
|
+
constructor(spec) {
|
|
169
|
+
if (!spec) {
|
|
170
|
+
throw new Error('spec is required');
|
|
171
|
+
}
|
|
172
|
+
this.spec = spec;
|
|
173
|
+
this.tokens = tokenize(spec);
|
|
174
|
+
const { pattern, paramNames } = buildRegex(this.tokens);
|
|
175
|
+
// Match from start to end, with optional query string
|
|
176
|
+
this.regex = new RegExp(`^${pattern}(?:\\?.*)?$`);
|
|
177
|
+
this.paramNames = paramNames;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Match a path against this route
|
|
181
|
+
* Returns params object on match, false otherwise
|
|
182
|
+
*/
|
|
183
|
+
match(path) {
|
|
184
|
+
const match = this.regex.exec(path);
|
|
185
|
+
if (!match) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
const params = {};
|
|
189
|
+
for (let i = 0; i < this.paramNames.length; i++) {
|
|
190
|
+
params[this.paramNames[i]] = match[i + 1];
|
|
191
|
+
}
|
|
192
|
+
return params;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Reverse the route with given parameters
|
|
196
|
+
* Returns the path string on success, false otherwise
|
|
197
|
+
*/
|
|
198
|
+
reverse(params = {}) {
|
|
199
|
+
// Check if required (non-optional) params can be fulfilled
|
|
200
|
+
if (!this.canFulfillRequired(this.tokens, params)) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
return reverseTokens(this.tokens, params);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Check if required params (non-optional) can be fulfilled
|
|
207
|
+
*/
|
|
208
|
+
canFulfillRequired(tokens, params) {
|
|
209
|
+
for (const token of tokens) {
|
|
210
|
+
if (token.type === 'param' || token.type === 'splat') {
|
|
211
|
+
const value = params[token.value];
|
|
212
|
+
if (value === undefined || value === null || value === '') {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Optional segments don't need to be checked at top level
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const RouteParserFactory = function (spec) {
|
|
222
|
+
if (this instanceof RouteParser) {
|
|
223
|
+
// Called with 'new'
|
|
224
|
+
return new RouteParser(spec);
|
|
225
|
+
}
|
|
226
|
+
// Called without 'new'
|
|
227
|
+
return new RouteParser(spec);
|
|
228
|
+
};
|
|
229
|
+
// Set the prototype so instanceof works
|
|
230
|
+
RouteParserFactory.prototype = RouteParser.prototype;
|
|
231
|
+
// Re-implement to allow both calling conventions
|
|
232
|
+
function createRouteParser(spec) {
|
|
233
|
+
return new RouteParser(spec);
|
|
234
|
+
}
|
|
235
|
+
// Export a wrapper that supports both calling conventions
|
|
236
|
+
const Route = function (spec) {
|
|
237
|
+
if (!(this instanceof Route)) {
|
|
238
|
+
return new Route(spec);
|
|
239
|
+
}
|
|
240
|
+
if (!spec) {
|
|
241
|
+
throw new Error('spec is required');
|
|
242
|
+
}
|
|
243
|
+
const parser = new RouteParser(spec);
|
|
244
|
+
// Copy properties to this instance
|
|
245
|
+
this.spec = parser.spec;
|
|
246
|
+
this.match = parser.match.bind(parser);
|
|
247
|
+
this.reverse = parser.reverse.bind(parser);
|
|
248
|
+
return this;
|
|
249
|
+
};
|
|
250
|
+
// Ensure prototype chain works for instanceof
|
|
251
|
+
Route.prototype = Object.create(Object.prototype);
|
|
252
|
+
Route.prototype.constructor = Route;
|
|
253
|
+
export default Route;
|
|
254
|
+
export { Route, RouteParser };
|
package/dist/test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/test.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import Route from './index.js';
|
|
4
|
+
describe('Route', () => {
|
|
5
|
+
it('should create', () => {
|
|
6
|
+
assert.ok(Route('/foo'));
|
|
7
|
+
});
|
|
8
|
+
it('should create with new', () => {
|
|
9
|
+
assert.ok(new Route('/foo'));
|
|
10
|
+
});
|
|
11
|
+
it('should have proper prototype', () => {
|
|
12
|
+
const routeInstance = new Route('/foo');
|
|
13
|
+
assert.ok(routeInstance instanceof Route);
|
|
14
|
+
});
|
|
15
|
+
it('should throw on no spec', () => {
|
|
16
|
+
assert.throws(() => { Route(); }, /spec is required/);
|
|
17
|
+
});
|
|
18
|
+
describe('basic', () => {
|
|
19
|
+
it('should match /foo with a path of /foo', () => {
|
|
20
|
+
const route = Route('/foo');
|
|
21
|
+
assert.ok(route.match('/foo'));
|
|
22
|
+
});
|
|
23
|
+
it('should match /foo with a path of /foo?query', () => {
|
|
24
|
+
const route = Route('/foo');
|
|
25
|
+
assert.ok(route.match('/foo?query'));
|
|
26
|
+
});
|
|
27
|
+
it("shouldn't match /foo with a path of /bar/foo", () => {
|
|
28
|
+
const route = Route('/foo');
|
|
29
|
+
assert.strictEqual(route.match('/bar/foo'), false);
|
|
30
|
+
});
|
|
31
|
+
it("shouldn't match /foo with a path of /foobar", () => {
|
|
32
|
+
const route = Route('/foo');
|
|
33
|
+
assert.strictEqual(route.match('/foobar'), false);
|
|
34
|
+
});
|
|
35
|
+
it("shouldn't match /foo with a path of /bar", () => {
|
|
36
|
+
const route = Route('/foo');
|
|
37
|
+
assert.strictEqual(route.match('/bar'), false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('basic parameters', () => {
|
|
41
|
+
it('should match /users/:id with a path of /users/1', () => {
|
|
42
|
+
const route = Route('/users/:id');
|
|
43
|
+
assert.ok(route.match('/users/1'));
|
|
44
|
+
});
|
|
45
|
+
it('should not match /users/:id with a path of /users/', () => {
|
|
46
|
+
const route = Route('/users/:id');
|
|
47
|
+
assert.strictEqual(route.match('/users/'), false);
|
|
48
|
+
});
|
|
49
|
+
it('should match /users/:id with a path of /users/1 and get parameters', () => {
|
|
50
|
+
const route = Route('/users/:id');
|
|
51
|
+
assert.deepStrictEqual(route.match('/users/1'), { id: '1' });
|
|
52
|
+
});
|
|
53
|
+
it('should match deep pathing and get parameters', () => {
|
|
54
|
+
const route = Route('/users/:id/comments/:comment/rating/:rating');
|
|
55
|
+
assert.deepStrictEqual(route.match('/users/1/comments/cats/rating/22222'), { id: '1', comment: 'cats', rating: '22222' });
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('splat parameters', () => {
|
|
59
|
+
it('should handle double splat parameters', () => {
|
|
60
|
+
const route = Route('/*a/foo/*b');
|
|
61
|
+
assert.deepStrictEqual(route.match('/zoo/woo/foo/bar/baz'), { a: 'zoo/woo', b: 'bar/baz' });
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe('mixed', () => {
|
|
65
|
+
it('should handle mixed splat and named parameters', () => {
|
|
66
|
+
const route = Route('/books/*section/:title');
|
|
67
|
+
assert.deepStrictEqual(route.match('/books/some/section/last-words-a-memoir'), { section: 'some/section', title: 'last-words-a-memoir' });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('optional', () => {
|
|
71
|
+
it('should allow and match optional routes without optional part', () => {
|
|
72
|
+
const route = Route('/users/:id(/style/:style)');
|
|
73
|
+
assert.deepStrictEqual(route.match('/users/3'), { id: '3', style: undefined });
|
|
74
|
+
});
|
|
75
|
+
it('should allow and match optional routes with optional part', () => {
|
|
76
|
+
const route = Route('/users/:id(/style/:style)');
|
|
77
|
+
assert.deepStrictEqual(route.match('/users/3/style/pirate'), { id: '3', style: 'pirate' });
|
|
78
|
+
});
|
|
79
|
+
it('allows optional branches that start with a word character', () => {
|
|
80
|
+
const route = Route('/things/(option/:first)');
|
|
81
|
+
assert.deepStrictEqual(route.match('/things/option/bar'), { first: 'bar' });
|
|
82
|
+
});
|
|
83
|
+
describe('nested', () => {
|
|
84
|
+
it('allows nested', () => {
|
|
85
|
+
const route = Route('/users/:id(/style/:style(/more/:param))');
|
|
86
|
+
const result = route.match('/users/3/style/pirate');
|
|
87
|
+
const expected = { id: '3', style: 'pirate', param: undefined };
|
|
88
|
+
assert.deepStrictEqual(result, expected);
|
|
89
|
+
});
|
|
90
|
+
it('fetches the correct params from nested', () => {
|
|
91
|
+
const route = Route('/users/:id(/style/:style(/more/:param))');
|
|
92
|
+
assert.deepStrictEqual(route.match('/users/3/style/pirate/more/things'), { id: '3', style: 'pirate', param: 'things' });
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('tilde prefix', () => {
|
|
97
|
+
it('should match routes starting with ~/', () => {
|
|
98
|
+
const route = Route('~/foo');
|
|
99
|
+
assert.ok(route.match('~/foo'));
|
|
100
|
+
});
|
|
101
|
+
it('should match ~/users/:id with parameters', () => {
|
|
102
|
+
const route = Route('~/users/:id');
|
|
103
|
+
assert.deepStrictEqual(route.match('~/users/123'), { id: '123' });
|
|
104
|
+
});
|
|
105
|
+
it('should reverse routes starting with ~/', () => {
|
|
106
|
+
const route = Route('~/users/:id');
|
|
107
|
+
assert.strictEqual(route.reverse({ id: '456' }), '~/users/456');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('reverse', () => {
|
|
111
|
+
it('reverses routes without params', () => {
|
|
112
|
+
const route = Route('/foo');
|
|
113
|
+
assert.strictEqual(route.reverse(), '/foo');
|
|
114
|
+
});
|
|
115
|
+
it('reverses routes with simple params', () => {
|
|
116
|
+
const route = Route('/:foo/:bar');
|
|
117
|
+
assert.strictEqual(route.reverse({ foo: '1', bar: '2' }), '/1/2');
|
|
118
|
+
});
|
|
119
|
+
it('reverses routes with optional params', () => {
|
|
120
|
+
const route = Route('/things/(option/:first)');
|
|
121
|
+
assert.strictEqual(route.reverse({ first: 'bar' }), '/things/option/bar');
|
|
122
|
+
});
|
|
123
|
+
it('reverses routes with unfilled optional params', () => {
|
|
124
|
+
const route = Route('/things/(option/:first)');
|
|
125
|
+
assert.strictEqual(route.reverse(), '/things/');
|
|
126
|
+
});
|
|
127
|
+
it("reverses routes with optional params that can't fulfill the optional branch", () => {
|
|
128
|
+
const route = Route('/things/(option/:first(/second/:second))');
|
|
129
|
+
assert.strictEqual(route.reverse({ second: 'foo' }), '/things/');
|
|
130
|
+
});
|
|
131
|
+
it("returns false for routes that can't be fulfilled", () => {
|
|
132
|
+
const route = Route('/foo/:bar');
|
|
133
|
+
assert.strictEqual(route.reverse({}), false);
|
|
134
|
+
});
|
|
135
|
+
it("returns false for routes with splat params that can't be fulfilled", () => {
|
|
136
|
+
const route = Route('/foo/*bar');
|
|
137
|
+
assert.strictEqual(route.reverse({}), false);
|
|
138
|
+
});
|
|
139
|
+
it('allows reversing falsy valued params', () => {
|
|
140
|
+
const path = '/account/json/wall/post/:id/comments/?start=:start&max=:max';
|
|
141
|
+
const vars = {
|
|
142
|
+
id: 50,
|
|
143
|
+
start: 0,
|
|
144
|
+
max: 12
|
|
145
|
+
};
|
|
146
|
+
assert.strictEqual(Route(path).reverse(vars), '/account/json/wall/post/50/comments/?start=0&max=12');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "route-parser-ts",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A TypeScript URL routing library with support for named parameters, splats, and optional segments",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"test": "npm run build && node --test dist/test.js",
|
|
11
|
+
"prepublishOnly": "npm run build"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"route",
|
|
15
|
+
"router",
|
|
16
|
+
"url",
|
|
17
|
+
"parser",
|
|
18
|
+
"typescript"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^25.0.3",
|
|
23
|
+
"typescript": "^5.3.0"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
]
|
|
28
|
+
}
|