extreme-router 1.1.0 → 1.2.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/CHANGELOG.MD +15 -0
- package/README.md +661 -660
- package/dist/bundle-size.json +12 -12
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/docs/error-types.md +35 -35
- package/docs/examples/browser.md +130 -130
- package/docs/examples/server.bun.md +73 -73
- package/docs/examples/server.deno.md +71 -71
- package/docs/examples/server.node.md +102 -102
- package/docs/optional-parameters-priority.md +64 -64
- package/package.json +9 -9
|
@@ -1,102 +1,102 @@
|
|
|
1
|
-
```javascript
|
|
2
|
-
// server.node.mjs
|
|
3
|
-
import http from 'node:http';
|
|
4
|
-
import Extreme, { param, wildcard } from 'extreme-router';
|
|
5
|
-
|
|
6
|
-
// Define the type for your route store, mapping methods to handlers
|
|
7
|
-
type MethodHandler = (req: http.IncomingMessage, res: http.ServerResponse, params?: Record<string, string>) => void;
|
|
8
|
-
type RouteStore = {
|
|
9
|
-
[method: string]: MethodHandler; // e.g., GET, POST
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
// Initialize the router
|
|
13
|
-
const router = new Extreme<RouteStore>();
|
|
14
|
-
|
|
15
|
-
// Register plugins (chaining supported)
|
|
16
|
-
router.use(param)
|
|
17
|
-
.use(wildcard);
|
|
18
|
-
|
|
19
|
-
// --- Define Handlers ---
|
|
20
|
-
const homeHandler: MethodHandler = (req, res) => {
|
|
21
|
-
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
22
|
-
res.end('Welcome Home! (GET)');
|
|
23
|
-
};
|
|
24
|
-
const createUserHandler: MethodHandler = (req, res) => {
|
|
25
|
-
let body = '';
|
|
26
|
-
req.on('data', (chunk) => {
|
|
27
|
-
body += chunk.toString();
|
|
28
|
-
});
|
|
29
|
-
req.on('end', () => {
|
|
30
|
-
res.writeHead(201, { 'Content-Type': 'text/plain' });
|
|
31
|
-
res.end(`Creating user... (POST) Data: ${body}`);
|
|
32
|
-
});
|
|
33
|
-
};
|
|
34
|
-
const userHandler: MethodHandler = (req, res, params) => {
|
|
35
|
-
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
36
|
-
res.end(`User ID: ${params?.userId} (GET)`);
|
|
37
|
-
};
|
|
38
|
-
const updateUserHandler: MethodHandler = (req, res, params) => {
|
|
39
|
-
let body = '';
|
|
40
|
-
req.on('data', (chunk) => {
|
|
41
|
-
body += chunk.toString();
|
|
42
|
-
});
|
|
43
|
-
req.on('end', () => {
|
|
44
|
-
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
45
|
-
res.end(`Updating user ${params?.userId}... (PUT) Data: ${body}`);
|
|
46
|
-
});
|
|
47
|
-
};
|
|
48
|
-
const fileHandler: MethodHandler = (req, res, params) => {
|
|
49
|
-
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
50
|
-
res.end(`File path: ${params?.['*']} (GET)`);
|
|
51
|
-
};
|
|
52
|
-
const notFoundHandler: MethodHandler = (req, res) => {
|
|
53
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
54
|
-
res.end('Not Found');
|
|
55
|
-
};
|
|
56
|
-
const methodNotAllowedHandler: MethodHandler = (req, res) => {
|
|
57
|
-
res.writeHead(405, { 'Content-Type': 'text/plain' });
|
|
58
|
-
res.end('Method Not Allowed');
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// --- Register Routes and Methods ---
|
|
62
|
-
router.register('/').GET = homeHandler;
|
|
63
|
-
|
|
64
|
-
const userRoute = router.register('/users/:userId');
|
|
65
|
-
userRoute.GET = userHandler;
|
|
66
|
-
userRoute.PUT = updateUserHandler;
|
|
67
|
-
|
|
68
|
-
router.register('/users').POST = createUserHandler;
|
|
69
|
-
|
|
70
|
-
router.register('/files/*').GET = fileHandler;
|
|
71
|
-
|
|
72
|
-
// Create Node.js server
|
|
73
|
-
const server = http.createServer((req, res) => {
|
|
74
|
-
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
|
|
75
|
-
const match = router.match(url.pathname);
|
|
76
|
-
const method = req.method ?? 'GET'; // Default to GET if method is undefined
|
|
77
|
-
|
|
78
|
-
if (match) {
|
|
79
|
-
// Check if a handler exists for the request method
|
|
80
|
-
const handler = match[method as keyof RouteStore];
|
|
81
|
-
if (handler) {
|
|
82
|
-
if ('params' in match && match.params) {
|
|
83
|
-
// Dynamic route match
|
|
84
|
-
handler(req, res, match.params);
|
|
85
|
-
} else {
|
|
86
|
-
// Static route match
|
|
87
|
-
handler(req, res);
|
|
88
|
-
}
|
|
89
|
-
} else {
|
|
90
|
-
// Path matched, but method not allowed
|
|
91
|
-
methodNotAllowedHandler(req, res);
|
|
92
|
-
}
|
|
93
|
-
} else {
|
|
94
|
-
// No path match found
|
|
95
|
-
notFoundHandler(req, res);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
server.listen(3000, () => {
|
|
100
|
-
console.log('Node.js server listening on http://localhost:3000');
|
|
101
|
-
});
|
|
102
|
-
```
|
|
1
|
+
```javascript
|
|
2
|
+
// server.node.mjs
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import Extreme, { param, wildcard } from 'extreme-router';
|
|
5
|
+
|
|
6
|
+
// Define the type for your route store, mapping methods to handlers
|
|
7
|
+
type MethodHandler = (req: http.IncomingMessage, res: http.ServerResponse, params?: Record<string, string>) => void;
|
|
8
|
+
type RouteStore = {
|
|
9
|
+
[method: string]: MethodHandler; // e.g., GET, POST
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Initialize the router
|
|
13
|
+
const router = new Extreme<RouteStore>();
|
|
14
|
+
|
|
15
|
+
// Register plugins (chaining supported)
|
|
16
|
+
router.use(param)
|
|
17
|
+
.use(wildcard);
|
|
18
|
+
|
|
19
|
+
// --- Define Handlers ---
|
|
20
|
+
const homeHandler: MethodHandler = (req, res) => {
|
|
21
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
22
|
+
res.end('Welcome Home! (GET)');
|
|
23
|
+
};
|
|
24
|
+
const createUserHandler: MethodHandler = (req, res) => {
|
|
25
|
+
let body = '';
|
|
26
|
+
req.on('data', (chunk) => {
|
|
27
|
+
body += chunk.toString();
|
|
28
|
+
});
|
|
29
|
+
req.on('end', () => {
|
|
30
|
+
res.writeHead(201, { 'Content-Type': 'text/plain' });
|
|
31
|
+
res.end(`Creating user... (POST) Data: ${body}`);
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
const userHandler: MethodHandler = (req, res, params) => {
|
|
35
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
36
|
+
res.end(`User ID: ${params?.userId} (GET)`);
|
|
37
|
+
};
|
|
38
|
+
const updateUserHandler: MethodHandler = (req, res, params) => {
|
|
39
|
+
let body = '';
|
|
40
|
+
req.on('data', (chunk) => {
|
|
41
|
+
body += chunk.toString();
|
|
42
|
+
});
|
|
43
|
+
req.on('end', () => {
|
|
44
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
45
|
+
res.end(`Updating user ${params?.userId}... (PUT) Data: ${body}`);
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const fileHandler: MethodHandler = (req, res, params) => {
|
|
49
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
50
|
+
res.end(`File path: ${params?.['*']} (GET)`);
|
|
51
|
+
};
|
|
52
|
+
const notFoundHandler: MethodHandler = (req, res) => {
|
|
53
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
54
|
+
res.end('Not Found');
|
|
55
|
+
};
|
|
56
|
+
const methodNotAllowedHandler: MethodHandler = (req, res) => {
|
|
57
|
+
res.writeHead(405, { 'Content-Type': 'text/plain' });
|
|
58
|
+
res.end('Method Not Allowed');
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// --- Register Routes and Methods ---
|
|
62
|
+
router.register('/').GET = homeHandler;
|
|
63
|
+
|
|
64
|
+
const userRoute = router.register('/users/:userId');
|
|
65
|
+
userRoute.GET = userHandler;
|
|
66
|
+
userRoute.PUT = updateUserHandler;
|
|
67
|
+
|
|
68
|
+
router.register('/users').POST = createUserHandler;
|
|
69
|
+
|
|
70
|
+
router.register('/files/*').GET = fileHandler;
|
|
71
|
+
|
|
72
|
+
// Create Node.js server
|
|
73
|
+
const server = http.createServer((req, res) => {
|
|
74
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
|
|
75
|
+
const match = router.match(url.pathname);
|
|
76
|
+
const method = req.method ?? 'GET'; // Default to GET if method is undefined
|
|
77
|
+
|
|
78
|
+
if (match) {
|
|
79
|
+
// Check if a handler exists for the request method
|
|
80
|
+
const handler = match[method as keyof RouteStore];
|
|
81
|
+
if (handler) {
|
|
82
|
+
if ('params' in match && match.params) {
|
|
83
|
+
// Dynamic route match
|
|
84
|
+
handler(req, res, match.params);
|
|
85
|
+
} else {
|
|
86
|
+
// Static route match
|
|
87
|
+
handler(req, res);
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
// Path matched, but method not allowed
|
|
91
|
+
methodNotAllowedHandler(req, res);
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
// No path match found
|
|
95
|
+
notFoundHandler(req, res);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
server.listen(3000, () => {
|
|
100
|
+
console.log('Node.js server listening on http://localhost:3000');
|
|
101
|
+
});
|
|
102
|
+
```
|
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
### Note on Optional Parameters and Priority
|
|
2
|
-
|
|
3
|
-
The interaction between optional parameters (`:name?`) and subsequent segments is governed by plugin priorities during the matching process. Consider a route like `/prefix/:optional?/nextSegment` and a URL like `/prefix/value`.
|
|
4
|
-
|
|
5
|
-
When the router encounters the `value` segment after `/prefix`, it looks at the potential nodes registered at that point:
|
|
6
|
-
|
|
7
|
-
1. A node representing the optional segment (`:optional?`), handled by the `optionalParam` plugin (priority 600).
|
|
8
|
-
2. A node representing the path _without_ the optional segment (leading directly to `nextSegment`). If `nextSegment` is static, it has the highest priority. If it's dynamic (e.g., `:nextSegment`), it's handled by its corresponding plugin (e.g., `param` with priority 700).
|
|
9
|
-
|
|
10
|
-
The router compares the priorities of the plugins handling these potential next steps:
|
|
11
|
-
|
|
12
|
-
- **Case 1: `nextSegment` has higher priority (lower number) than `:optional?`**
|
|
13
|
-
|
|
14
|
-
- The router first attempts to match `value` against `nextSegment`.
|
|
15
|
-
- If it matches, the path taken is `/prefix/nextSegment`, effectively skipping the optional parameter logic for this segment.
|
|
16
|
-
- If it _doesn't_ match, the router then attempts to match `value` using the `optionalParam` plugin.
|
|
17
|
-
|
|
18
|
-
- **Case 2: `:optional?` has higher priority (lower number) than `nextSegment`** (This is the case with built-in `optionalParam` (600) vs `param` (700) or `wildcard` (800)).
|
|
19
|
-
- The router first attempts to match `value` using the higher-priority `optionalParam` plugin.
|
|
20
|
-
- Crucially, the built-in `optionalParam` plugin's matching logic is designed to **always succeed** for the segment it checks. It consumes `value` and assigns it to the `:optional` parameter.
|
|
21
|
-
- The router then proceeds _down the path that includes the optional segment_.
|
|
22
|
-
- It then attempts to match the _next_ segment in the URL against the node _following_ the optional one (i.e., the `nextSegment` node that is a child of the `:optional?` node).
|
|
23
|
-
- If the rest of the URL matches the structure defined _after_ the optional segment, the match succeeds.
|
|
24
|
-
- If the rest of the URL _doesn't_ match (e.g., the URL ends after `value` but the route expected `nextSegment`), the match fails.
|
|
25
|
-
|
|
26
|
-
**Key Takeaway:** Because the built-in `optionalParam` (priority 600) has a higher priority than `param` (700) and `wildcard` (800), and its matching function always consumes the segment, it will generally "win" the priority check against a subsequent standard parameter or wildcard. The match then _must_ follow the path structure _including_ the optional parameter.
|
|
27
|
-
|
|
28
|
-
**Revisiting the Example:**
|
|
29
|
-
|
|
30
|
-
Let's re-analyze the example `/products/:category?/:productId` with this understanding:
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
// ... (import and setup) ...
|
|
34
|
-
|
|
35
|
-
// Register plugins (lower priority number = higher precedence)
|
|
36
|
-
// Order of registration does not matter for the built-in plugins, but it's good practice to register them in a logical order.
|
|
37
|
-
router.use(optionalParam); // Priority 600
|
|
38
|
-
.use(param); // Priority 700
|
|
39
|
-
|
|
40
|
-
// Route: /products/:category?/:productId
|
|
41
|
-
router.register('/products/:category?/:productId').name = 'GetProduct';
|
|
42
|
-
|
|
43
|
-
// Matching /products/books/987:
|
|
44
|
-
// - After /products, encounter 'books'.
|
|
45
|
-
// - Potential next nodes: ':category?' (optionalParam, 600) and ':productId' (param, 700 - representing the path without the optional).
|
|
46
|
-
// - optionalParam (600) has higher priority.
|
|
47
|
-
// - optionalParam matches and consumes 'books' for :category.
|
|
48
|
-
// - Router proceeds down the optional path. Expects ':productId' next.
|
|
49
|
-
// - Encounters '987'. Matches ':productId' (using param plugin).
|
|
50
|
-
// - End of URL, end of path. Success.
|
|
51
|
-
console.log(router.match('/products/books/987'));
|
|
52
|
-
// { name: 'GetProduct', params: { category: 'books', productId: '987' } } // Correct based on priority logic.
|
|
53
|
-
|
|
54
|
-
// Matching /products/654:
|
|
55
|
-
// - After /products, encounter '654'.
|
|
56
|
-
// - Potential next nodes: ':category?' (optionalParam, 600) and ':productId' (param, 700).
|
|
57
|
-
// - optionalParam (600) has higher priority.
|
|
58
|
-
// - optionalParam matches and consumes '654' for :category.
|
|
59
|
-
// - Router proceeds down the optional path. Expects ':productId' next.
|
|
60
|
-
// - No more segments in the URL.
|
|
61
|
-
// - Match fails because the path requires ':productId' after ':category'.
|
|
62
|
-
console.log(router.match('/products/654'));
|
|
63
|
-
// null // Corrected: optionalParam (higher priority) consumes '654' for :category, then fails as :productId is missing.
|
|
64
|
-
```
|
|
1
|
+
### Note on Optional Parameters and Priority
|
|
2
|
+
|
|
3
|
+
The interaction between optional parameters (`:name?`) and subsequent segments is governed by plugin priorities during the matching process. Consider a route like `/prefix/:optional?/nextSegment` and a URL like `/prefix/value`.
|
|
4
|
+
|
|
5
|
+
When the router encounters the `value` segment after `/prefix`, it looks at the potential nodes registered at that point:
|
|
6
|
+
|
|
7
|
+
1. A node representing the optional segment (`:optional?`), handled by the `optionalParam` plugin (priority 600).
|
|
8
|
+
2. A node representing the path _without_ the optional segment (leading directly to `nextSegment`). If `nextSegment` is static, it has the highest priority. If it's dynamic (e.g., `:nextSegment`), it's handled by its corresponding plugin (e.g., `param` with priority 700).
|
|
9
|
+
|
|
10
|
+
The router compares the priorities of the plugins handling these potential next steps:
|
|
11
|
+
|
|
12
|
+
- **Case 1: `nextSegment` has higher priority (lower number) than `:optional?`**
|
|
13
|
+
|
|
14
|
+
- The router first attempts to match `value` against `nextSegment`.
|
|
15
|
+
- If it matches, the path taken is `/prefix/nextSegment`, effectively skipping the optional parameter logic for this segment.
|
|
16
|
+
- If it _doesn't_ match, the router then attempts to match `value` using the `optionalParam` plugin.
|
|
17
|
+
|
|
18
|
+
- **Case 2: `:optional?` has higher priority (lower number) than `nextSegment`** (This is the case with built-in `optionalParam` (600) vs `param` (700) or `wildcard` (800)).
|
|
19
|
+
- The router first attempts to match `value` using the higher-priority `optionalParam` plugin.
|
|
20
|
+
- Crucially, the built-in `optionalParam` plugin's matching logic is designed to **always succeed** for the segment it checks. It consumes `value` and assigns it to the `:optional` parameter.
|
|
21
|
+
- The router then proceeds _down the path that includes the optional segment_.
|
|
22
|
+
- It then attempts to match the _next_ segment in the URL against the node _following_ the optional one (i.e., the `nextSegment` node that is a child of the `:optional?` node).
|
|
23
|
+
- If the rest of the URL matches the structure defined _after_ the optional segment, the match succeeds.
|
|
24
|
+
- If the rest of the URL _doesn't_ match (e.g., the URL ends after `value` but the route expected `nextSegment`), the match fails.
|
|
25
|
+
|
|
26
|
+
**Key Takeaway:** Because the built-in `optionalParam` (priority 600) has a higher priority than `param` (700) and `wildcard` (800), and its matching function always consumes the segment, it will generally "win" the priority check against a subsequent standard parameter or wildcard. The match then _must_ follow the path structure _including_ the optional parameter.
|
|
27
|
+
|
|
28
|
+
**Revisiting the Example:**
|
|
29
|
+
|
|
30
|
+
Let's re-analyze the example `/products/:category?/:productId` with this understanding:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// ... (import and setup) ...
|
|
34
|
+
|
|
35
|
+
// Register plugins (lower priority number = higher precedence)
|
|
36
|
+
// Order of registration does not matter for the built-in plugins, but it's good practice to register them in a logical order.
|
|
37
|
+
router.use(optionalParam); // Priority 600
|
|
38
|
+
.use(param); // Priority 700
|
|
39
|
+
|
|
40
|
+
// Route: /products/:category?/:productId
|
|
41
|
+
router.register('/products/:category?/:productId').name = 'GetProduct';
|
|
42
|
+
|
|
43
|
+
// Matching /products/books/987:
|
|
44
|
+
// - After /products, encounter 'books'.
|
|
45
|
+
// - Potential next nodes: ':category?' (optionalParam, 600) and ':productId' (param, 700 - representing the path without the optional).
|
|
46
|
+
// - optionalParam (600) has higher priority.
|
|
47
|
+
// - optionalParam matches and consumes 'books' for :category.
|
|
48
|
+
// - Router proceeds down the optional path. Expects ':productId' next.
|
|
49
|
+
// - Encounters '987'. Matches ':productId' (using param plugin).
|
|
50
|
+
// - End of URL, end of path. Success.
|
|
51
|
+
console.log(router.match('/products/books/987'));
|
|
52
|
+
// { name: 'GetProduct', params: { category: 'books', productId: '987' } } // Correct based on priority logic.
|
|
53
|
+
|
|
54
|
+
// Matching /products/654:
|
|
55
|
+
// - After /products, encounter '654'.
|
|
56
|
+
// - Potential next nodes: ':category?' (optionalParam, 600) and ':productId' (param, 700).
|
|
57
|
+
// - optionalParam (600) has higher priority.
|
|
58
|
+
// - optionalParam matches and consumes '654' for :category.
|
|
59
|
+
// - Router proceeds down the optional path. Expects ':productId' next.
|
|
60
|
+
// - No more segments in the URL.
|
|
61
|
+
// - Match fails because the path requires ':productId' after ':category'.
|
|
62
|
+
console.log(router.match('/products/654'));
|
|
63
|
+
// null // Corrected: optionalParam (higher priority) consumes '654' for :category, then fails as :productId is missing.
|
|
64
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "extreme-router",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A high-performance, tree-based router for JavaScript and TypeScript, featuring a powerful plugin system for extreme extensibility",
|
|
5
5
|
"author": "lior cohen",
|
|
6
6
|
"homepage": "https://github.com/liorcodev/extreme-router#readme",
|
|
@@ -38,21 +38,21 @@
|
|
|
38
38
|
"parameters"
|
|
39
39
|
],
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@eslint/js": "^9.
|
|
41
|
+
"@eslint/js": "^9.39.2",
|
|
42
42
|
"@types/benchmark": "^2.1.5",
|
|
43
43
|
"@types/bun": "latest",
|
|
44
44
|
"@vitest/coverage-v8": "3.1.2",
|
|
45
45
|
"benchmark": "^2.1.4",
|
|
46
|
-
"chalk": "^5.
|
|
47
|
-
"eslint": "^9.
|
|
48
|
-
"globals": "^16.
|
|
46
|
+
"chalk": "^5.6.2",
|
|
47
|
+
"eslint": "^9.39.2",
|
|
48
|
+
"globals": "^16.5.0",
|
|
49
49
|
"husky": "^9.1.7",
|
|
50
50
|
"lint-staged": "^15.5.2",
|
|
51
51
|
"minimist": "^1.2.8",
|
|
52
|
-
"prettier": "^3.
|
|
53
|
-
"tsup": "^8.5.
|
|
54
|
-
"typescript-eslint": "^8.
|
|
55
|
-
"vitest": "^3.
|
|
52
|
+
"prettier": "^3.7.4",
|
|
53
|
+
"tsup": "^8.5.1",
|
|
54
|
+
"typescript-eslint": "^8.50.1",
|
|
55
|
+
"vitest": "^3.2.4"
|
|
56
56
|
},
|
|
57
57
|
"files": [
|
|
58
58
|
"dist",
|