extreme-router 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.
@@ -0,0 +1,71 @@
1
+ ```typescript
2
+ // server.deno.ts
3
+ // @deno-types="npm:extreme-router/dist/index.d.ts"
4
+ import Extreme, { param, wildcard } from 'npm:extreme-router';
5
+
6
+ // Define the type for your route store, mapping methods to handlers
7
+ type MethodHandler = (req: Request, params?: Record<string, string>) => Response | Promise<Response>;
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).use(wildcard);
17
+
18
+ // --- Define Handlers ---
19
+ const homeHandler: MethodHandler = () => new Response('Welcome Home! (GET)');
20
+ const createUserHandler: MethodHandler = async (req) => {
21
+ const body = await req.text();
22
+ return new Response(`Creating user... (POST) Data: ${body}`, { status: 201 });
23
+ };
24
+ const userHandler: MethodHandler = (req, params) => new Response(`User ID: ${params?.userId} (GET)`);
25
+ const updateUserHandler: MethodHandler = async (req, params) => {
26
+ const body = await req.text();
27
+ return new Response(`Updating user ${params?.userId}... (PUT) Data: ${body}`);
28
+ };
29
+ const fileHandler: MethodHandler = (req, params) => new Response(`File path: ${params?.['*']} (GET)`);
30
+ const notFoundHandler: MethodHandler = () => new Response('Not Found', { status: 404 });
31
+ const methodNotAllowedHandler: MethodHandler = () => new Response('Method Not Allowed', { status: 405 });
32
+
33
+ // --- Register Routes and Methods ---
34
+ router.register('/').GET = homeHandler;
35
+
36
+ const userRoute = router.register('/users/:userId');
37
+ userRoute.GET = userHandler;
38
+ userRoute.PUT = updateUserHandler;
39
+
40
+ router.register('/users').POST = createUserHandler;
41
+
42
+ router.register('/files/*').GET = fileHandler;
43
+
44
+ // Create Deno server
45
+ Deno.serve({ port: 3000 }, (req) => {
46
+ const url = new URL(req.url);
47
+ const match = router.match(url.pathname);
48
+
49
+ if (match) {
50
+ // Check if a handler exists for the request method
51
+ const handler = match[req.method as keyof RouteStore];
52
+ if (handler) {
53
+ if ('params' in match && match.params) {
54
+ // Dynamic route match
55
+ return handler(req, match.params);
56
+ } else {
57
+ // Static route match
58
+ return handler(req);
59
+ }
60
+ } else {
61
+ // Path matched, but method not allowed
62
+ return methodNotAllowedHandler(req);
63
+ }
64
+ }
65
+
66
+ // No path match found
67
+ return notFoundHandler(req);
68
+ });
69
+
70
+ console.log('Deno server listening on http://localhost:3000');
71
+ ```
@@ -0,0 +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
+ ```
@@ -0,0 +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
+ ```
package/package.json ADDED
@@ -0,0 +1,107 @@
1
+ {
2
+ "name": "extreme-router",
3
+ "version": "1.0.0",
4
+ "description": "A high-performance, tree-based router for JavaScript and TypeScript, featuring a powerful plugin system for extreme extensibility",
5
+ "author": "lior cohen",
6
+ "homepage": "https://github.com/liorcodev/extreme-router#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/liorcodev/extreme-router.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/liorcodev/extreme-router/issues"
13
+ },
14
+ "license": "MIT",
15
+ "keywords": [
16
+ "router",
17
+ "routing",
18
+ "javascript",
19
+ "typescript",
20
+ "node",
21
+ "modular",
22
+ "bun",
23
+ "browser",
24
+ "performance",
25
+ "extensible",
26
+ "plugin",
27
+ "url",
28
+ "path",
29
+ "matching",
30
+ "tree",
31
+ "radix",
32
+ "trie",
33
+ "dynamic",
34
+ "static",
35
+ "regex",
36
+ "wildcard",
37
+ "optional",
38
+ "parameters"
39
+ ],
40
+ "devDependencies": {
41
+ "@eslint/js": "^9.26.0",
42
+ "@types/benchmark": "^2.1.5",
43
+ "@types/bun": "latest",
44
+ "@vitest/coverage-v8": "3.1.2",
45
+ "benchmark": "^2.1.4",
46
+ "chalk": "^5.4.1",
47
+ "eslint": "^9.26.0",
48
+ "globals": "^16.1.0",
49
+ "husky": "^9.1.7",
50
+ "lint-staged": "^15.5.2",
51
+ "minimist": "^1.2.8",
52
+ "prettier": "^3.5.3",
53
+ "tsup": "^8.4.0",
54
+ "typescript-eslint": "^8.32.0",
55
+ "vitest": "^3.1.3"
56
+ },
57
+ "files": [
58
+ "dist",
59
+ "assets",
60
+ "docs",
61
+ "README.md",
62
+ "LICENSE",
63
+ "CHANGELOG.md"
64
+ ],
65
+ "module": "dist/index.js",
66
+ "main": "dist/index.js",
67
+ "types": "dist/index.d.ts",
68
+ "type": "module",
69
+ "exports": {
70
+ ".": {
71
+ "types": "./dist/index.d.ts",
72
+ "import": "./dist/index.js",
73
+ "require": "./dist/index.cjs"
74
+ }
75
+ },
76
+ "engines": {
77
+ "node": ">=18.0.0",
78
+ "bun": ">=1.0.0"
79
+ },
80
+ "scripts": {
81
+ "prepare": "husky",
82
+ "lint": "bun eslint",
83
+ "lint:fix": "bun eslint --fix",
84
+ "lint-staged": "lint-staged",
85
+ "test": "vitest",
86
+ "test:watch": "vitest --watch",
87
+ "test:coverage": "vitest --coverage",
88
+ "build": "tsup",
89
+ "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\" \"benchmark/**/*.js\" \"scripts/**/*.js\"",
90
+ "benchmark": "bun run benchmark/match.benchmark.js --type mixed",
91
+ "benchmark:dynamic": "bun run benchmark/match.benchmark.js --type dynamic",
92
+ "benchmark:static": "bun run benchmark/match.benchmark.js --type static",
93
+ "benchmark:stress": "bun run benchmark/match.stress.js",
94
+ "benchmark:memory": "bun run benchmark/match.memory.js",
95
+ "size": "bun run scripts/bundle-size.js",
96
+ "prepublishOnly": "bun run format && bun run test:coverage && bun run lint:fix && bun run build && bun run size"
97
+ },
98
+ "lint-staged": {
99
+ "*.{ts,tsx}": [
100
+ "eslint --fix",
101
+ "prettier --write"
102
+ ],
103
+ "*.{json,md}": [
104
+ "prettier --write"
105
+ ]
106
+ }
107
+ }