@pirxpilot/router 1.1.0 → 2.0.1

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/lib/layer.js CHANGED
@@ -1,60 +1,17 @@
1
- /*!
2
- * router
3
- * Copyright(c) 2013 Roman Shtylman
4
- * Copyright(c) 2014-2022 Douglas Christopher Wilson
5
- * MIT Licensed
6
- */
7
-
8
- 'use strict'
9
-
10
- /**
11
- * Module dependencies.
12
- * @private
13
- */
14
-
15
- const pathRegexp = require('path-to-regexp')
16
-
17
- /**
18
- * Module variables.
19
- * @private
20
- */
21
-
22
- const TRAILING_SLASH_REGEXP = /\/+$/
23
- const MATCHING_GROUP_REGEXP = /\((?:\?<(.*?)>)?(?!\?)/g
24
-
25
- /**
26
- * Expose `Layer`.
27
- */
1
+ import matcher from './matcher.js';
28
2
 
29
- module.exports = Layer
30
-
31
- function Layer (path, opts = {}, fn, method) {
3
+ export default function Layer(path, opts = {}, fn, method) {
32
4
  if (typeof fn !== 'function') {
33
- throw new TypeError('argument handler must be a function')
5
+ throw new TypeError('argument handler must be a function');
34
6
  }
35
- this.handle = fn
36
- this.keys = []
37
- this.name = fn.name || '<anonymous>'
38
- this.params = undefined
39
- this.path = undefined
40
- this.slash = path === '/' && opts.end === false
41
- this.matchers = this.slash
42
- ? []
43
- : Array.isArray(path)
44
- ? path.map(p => matcher(p, opts))
45
- : [matcher(path, opts)]
46
- this.method = method
47
- }
48
-
49
- function matcher (path, { sensitive, end, strict }) {
50
- return path instanceof RegExp
51
- ? createRegexMatcher(path)
52
- : pathRegexp.match((strict ? path : loosen(path)), {
53
- sensitive,
54
- end,
55
- trailing: !strict,
56
- decode: decodeParam
57
- })
7
+ this.handle = fn;
8
+ this.keys = [];
9
+ this.name = fn.name || '<anonymous>';
10
+ this.params = undefined;
11
+ this.path = undefined;
12
+ this.slash = path === '/' && opts.end === false;
13
+ this.matchers = this.slash ? [] : Array.isArray(path) ? path.map(p => matcher(p, opts)) : [matcher(path, opts)];
14
+ this.method = method;
58
15
  }
59
16
 
60
17
  /**
@@ -67,26 +24,26 @@ function matcher (path, { sensitive, end, strict }) {
67
24
  * @api private
68
25
  */
69
26
 
70
- Layer.prototype.handleError = function handleError (error, req, res, next) {
71
- const fn = this.handle
27
+ Layer.prototype.handleError = function handleError(error, req, res, next) {
28
+ const fn = this.handle;
72
29
 
73
30
  if (fn.length !== 4) {
74
31
  // not a standard error handler
75
- return next(error)
32
+ return next(error);
76
33
  }
77
34
 
78
35
  try {
79
36
  // invoke function
80
- const ret = fn(error, req, res, next)
37
+ const ret = fn(error, req, res, next);
81
38
 
82
39
  // wait for returned promise
83
40
  if (ret instanceof Promise) {
84
- ret.catch((error = new Error('Rejected promise')) => next(error))
41
+ ret.catch((error = new Error('Rejected promise')) => next(error));
85
42
  }
86
43
  } catch (err) {
87
- next(err)
44
+ next(err);
88
45
  }
89
- }
46
+ };
90
47
 
91
48
  /**
92
49
  * Handle the request for the layer.
@@ -97,26 +54,26 @@ Layer.prototype.handleError = function handleError (error, req, res, next) {
97
54
  * @api private
98
55
  */
99
56
 
100
- Layer.prototype.handleRequest = function handleRequest (req, res, next) {
101
- const fn = this.handle
57
+ Layer.prototype.handleRequest = function handleRequest(req, res, next) {
58
+ const fn = this.handle;
102
59
 
103
60
  if (fn.length > 3) {
104
61
  // not a standard request handler
105
- return next()
62
+ return next();
106
63
  }
107
64
 
108
65
  try {
109
66
  // invoke function
110
- const ret = fn(req, res, next)
67
+ const ret = fn(req, res, next);
111
68
 
112
69
  // wait for returned promise
113
70
  if (ret instanceof Promise) {
114
- ret.catch((error = new Error('Rejected promise')) => next(error))
71
+ ret.catch((error = new Error('Rejected promise')) => next(error));
115
72
  }
116
73
  } catch (err) {
117
- next(err)
74
+ next(err);
118
75
  }
119
- }
76
+ };
120
77
 
121
78
  /**
122
79
  * Check if this route matches `path`, if so
@@ -127,101 +84,28 @@ Layer.prototype.handleRequest = function handleRequest (req, res, next) {
127
84
  * @api private
128
85
  */
129
86
 
130
- Layer.prototype.match = function match (path) {
87
+ Layer.prototype.match = function match(path) {
131
88
  if (path != null) {
132
89
  // fast path non-ending match for / (any path matches)
133
90
  if (this.slash) {
134
- this.params = {}
135
- this.path = ''
136
- return true
91
+ this.params = {};
92
+ this.path = '';
93
+ return true;
137
94
  }
138
95
 
139
96
  for (const matcher of this.matchers) {
140
- const matched = matcher(path)
97
+ const matched = matcher(path);
141
98
  if (matched) {
142
99
  // store values
143
- this.params = matched.params
144
- this.path = matched.path
145
- this.keys = Object.keys(matched.params)
146
- return true
100
+ this.params = matched.params;
101
+ this.path = matched.path;
102
+ this.keys = matched.keys;
103
+ return true;
147
104
  }
148
105
  }
149
106
  }
150
107
 
151
- this.params = undefined
152
- this.path = undefined
153
- return false
154
- }
155
-
156
- function createRegexMatcher (path) {
157
- const keys = []
158
- let name = 0
159
-
160
- for (const m of path.source.matchAll(MATCHING_GROUP_REGEXP)) {
161
- keys.push({
162
- name: m[1] || name++,
163
- offset: m.index
164
- })
165
- }
166
-
167
- return function regexpMatcher (p) {
168
- const match = path.exec(p)
169
- if (!match) {
170
- return false
171
- }
172
-
173
- const params = {}
174
- for (let i = 1; i < match.length; i++) {
175
- const key = keys[i - 1]
176
- const prop = key.name
177
- const val = decodeParam(match[i])
178
-
179
- if (val !== undefined) {
180
- params[prop] = val
181
- }
182
- }
183
-
184
- return {
185
- params,
186
- path: match[0]
187
- }
188
- }
189
- }
190
-
191
- /**
192
- * Decode param value.
193
- *
194
- * @param {string} val
195
- * @return {string}
196
- * @private
197
- */
198
-
199
- function decodeParam (val) {
200
- if (typeof val !== 'string' || val.length === 0) {
201
- return val
202
- }
203
-
204
- try {
205
- return decodeURIComponent(val)
206
- } catch (err) {
207
- if (err instanceof URIError) {
208
- err.message = `Failed to decode param '${val}'`
209
- err.status = 400
210
- }
211
-
212
- throw err
213
- }
214
- }
215
-
216
- /**
217
- * Loosens the given path for path-to-regexp matching.
218
- */
219
- function loosen (path) {
220
- if (path instanceof RegExp || path === '/') {
221
- return path
222
- }
223
-
224
- return Array.isArray(path)
225
- ? path.map(p => loosen(p))
226
- : String(path).replace(TRAILING_SLASH_REGEXP, '')
227
- }
108
+ this.params = undefined;
109
+ this.path = undefined;
110
+ return false;
111
+ };
package/lib/matcher.js ADDED
@@ -0,0 +1,147 @@
1
+ const MATCHING_GROUP_REGEXP = /\((?:\?<(.*?)>)?(?!\?)/g;
2
+
3
+ export default function matcher(path, { sensitive = false, end = true, strict = false } = {}) {
4
+ return path instanceof RegExp
5
+ ? createRegexMatcher(path)
6
+ : createURLPatternMatcher(path, {
7
+ sensitive,
8
+ end,
9
+ strict
10
+ });
11
+ }
12
+
13
+ export function createRegexMatcher(path) {
14
+ const regexKeys = [];
15
+ let name = 0;
16
+
17
+ for (const m of path.source.matchAll(MATCHING_GROUP_REGEXP)) {
18
+ regexKeys.push({
19
+ name: m[1] || name++,
20
+ offset: m.index
21
+ });
22
+ }
23
+
24
+ return function regexpMatcher(p) {
25
+ const match = path.exec(p);
26
+ if (!match) {
27
+ return false;
28
+ }
29
+
30
+ const params = {};
31
+ const keys = [];
32
+ for (let i = 1; i < match.length; i++) {
33
+ const { name } = regexKeys[i - 1];
34
+ const val = decodeParam(match[i]);
35
+
36
+ if (val !== undefined) {
37
+ keys.push(name);
38
+ params[name] = val;
39
+ }
40
+ }
41
+
42
+ return {
43
+ params,
44
+ keys,
45
+ path: match[0]
46
+ };
47
+ };
48
+ }
49
+
50
+ const parameterRegex = /:([a-zA-Z_]\w*)/g;
51
+
52
+ export function createURLPatternMatcher(pathname, { sensitive, end, strict }) {
53
+ // Extract parameter names from the pattern in order
54
+ const parameterOrder = new Set(pathname.matchAll(parameterRegex).map(m => m[1]));
55
+
56
+ // Build the pattern:
57
+ // - For end=false (prefix matching): add {/*}? to match optional trailing content
58
+ // - For end=true with trailing=true: add {/}? to match optional trailing slash
59
+
60
+ if (!end) {
61
+ if (pathname.endsWith('/')) pathname = pathname.slice(0, -1);
62
+ pathname += '{/*}?';
63
+ } else if (!strict) {
64
+ if (pathname.endsWith('/')) pathname = pathname.slice(0, -1);
65
+ pathname += '{/}?';
66
+ }
67
+
68
+ const pattern = new URLPattern({ pathname }, { ignoreCase: !sensitive });
69
+
70
+ return function urlPatternMatcher(p) {
71
+ const match = pattern.exec(p);
72
+ if (!match) {
73
+ return false;
74
+ }
75
+
76
+ const params = {};
77
+ const keys = [];
78
+
79
+ // Process parameters in the order they appear in the pattern
80
+ // Use the preserved parameter order
81
+ for (const key of parameterOrder) {
82
+ const value = match.pathname.groups[key];
83
+ if (value !== undefined) {
84
+ keys.push(key);
85
+ params[key] = decodeParam(value);
86
+ }
87
+ }
88
+
89
+ // Also handle any additional groups not in parameterOrder (e.g., numbered groups)
90
+ for (const [key, value] of Object.entries(match.pathname.groups)) {
91
+ // Skip the default wildcard capture group '0'
92
+ if (key === '0') continue;
93
+ // Skip if we already processed this key
94
+ if (parameterOrder.has(key)) continue;
95
+ if (value !== undefined) {
96
+ keys.push(key);
97
+ params[key] = decodeParam(value);
98
+ }
99
+ }
100
+
101
+ // For prefix matching, calculate the matched path length
102
+ let matchedPath = match.pathname.input;
103
+ if (!end) {
104
+ // Remove the wildcard matched portion to get just the prefix
105
+ const wildcardMatch = match.pathname.groups['0'];
106
+ if (wildcardMatch !== undefined && wildcardMatch !== '') {
107
+ // The wildcard captures content after the /, so we need to remove "/" + wildcard
108
+ matchedPath = matchedPath.slice(0, -(wildcardMatch.length + 1));
109
+ } else if (wildcardMatch === '') {
110
+ // Empty string means we matched a trailing slash: /foo/ with pattern /foo{/*}?
111
+ // Remove the trailing slash to normalize
112
+ matchedPath = matchedPath.slice(0, -1);
113
+ }
114
+ }
115
+
116
+ return {
117
+ params,
118
+ keys,
119
+ path: matchedPath
120
+ };
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Decode param value.
126
+ *
127
+ * @param {string} val
128
+ * @return {string}
129
+ * @private
130
+ */
131
+
132
+ export function decodeParam(val) {
133
+ if (typeof val !== 'string' || val.length === 0) {
134
+ return val;
135
+ }
136
+
137
+ try {
138
+ return decodeURIComponent(val);
139
+ } catch (err) {
140
+ if (err instanceof URIError) {
141
+ err.message = `Failed to decode param '${val}'`;
142
+ err.status = 400;
143
+ }
144
+
145
+ throw err;
146
+ }
147
+ }
package/lib/route.js CHANGED
@@ -1,30 +1,5 @@
1
- /*!
2
- * router
3
- * Copyright(c) 2013 Roman Shtylman
4
- * Copyright(c) 2014-2022 Douglas Christopher Wilson
5
- * MIT Licensed
6
- */
7
-
8
- 'use strict'
9
-
10
- /**
11
- * Module dependencies.
12
- * @private
13
- */
14
-
15
- const { METHODS } = require('node:http')
16
- const Layer = require('./layer')
17
-
18
- /**
19
- * Module variables.
20
- * @private
21
- */
22
-
23
- /**
24
- * Expose `Route`.
25
- */
26
-
27
- module.exports = Route
1
+ import { METHODS } from 'node:http';
2
+ import Layer from './layer.js';
28
3
 
29
4
  /**
30
5
  * Initialize `Route` with the given `path`,
@@ -33,45 +8,41 @@ module.exports = Route
33
8
  * @api private
34
9
  */
35
10
 
36
- function Route (path) {
37
- this.path = path
38
- this.stack = []
11
+ export default function Route(path) {
12
+ this.path = path;
13
+ this.stack = [];
39
14
 
40
15
  // route handlers for various http methods
41
- this.methods = new Set()
16
+ this.methods = new Set();
42
17
  }
43
18
 
44
- /**
45
- * @private
46
- */
47
-
48
- Route.prototype._handlesMethod = function _handlesMethod (method) {
19
+ Route.prototype._handlesMethod = function _handlesMethod(method) {
49
20
  if (this.methods._all) {
50
- return true
21
+ return true;
51
22
  }
52
23
 
53
24
  if (method === 'HEAD' && !this.methods.has('HEAD')) {
54
- method = 'GET'
25
+ method = 'GET';
55
26
  }
56
27
 
57
- return this.methods.has(method)
58
- }
28
+ return this.methods.has(method);
29
+ };
59
30
 
60
31
  /**
61
32
  * @return {array} supported HTTP methods
62
33
  * @private
63
34
  */
64
35
 
65
- Route.prototype._methods = function _methods () {
66
- const methods = [...this.methods]
36
+ Route.prototype._methods = function _methods() {
37
+ const methods = [...this.methods];
67
38
 
68
39
  // append automatic head
69
40
  if (this.methods.has('GET') && !this.methods.has('HEAD')) {
70
- methods.push('HEAD')
41
+ methods.push('HEAD');
71
42
  }
72
43
 
73
- return methods
74
- }
44
+ return methods;
45
+ };
75
46
 
76
47
  /**
77
48
  * dispatch req, res into this route
@@ -79,71 +50,71 @@ Route.prototype._methods = function _methods () {
79
50
  * @private
80
51
  */
81
52
 
82
- Route.prototype.dispatch = function dispatch (req, res, done) {
83
- let idx = 0
84
- const stack = this.stack
85
- let sync = 0
53
+ Route.prototype.dispatch = function dispatch(req, res, done) {
54
+ let idx = 0;
55
+ const stack = this.stack;
56
+ let sync = 0;
86
57
 
87
58
  if (stack.length === 0) {
88
- return done()
59
+ return done();
89
60
  }
90
61
 
91
- let { method } = req
62
+ let { method } = req;
92
63
  if (method === 'HEAD' && !this.methods.has('HEAD')) {
93
- method = 'GET'
64
+ method = 'GET';
94
65
  }
95
66
 
96
- req.route = this
67
+ req.route = this;
97
68
 
98
- next()
69
+ next();
99
70
 
100
- function next (err) {
71
+ function next(err) {
101
72
  // signal to exit route
102
73
  if (err && err === 'route') {
103
- return done()
74
+ return done();
104
75
  }
105
76
 
106
77
  // signal to exit router
107
78
  if (err && err === 'router') {
108
- return done(err)
79
+ return done(err);
109
80
  }
110
81
 
111
82
  // no more matching layers
112
83
  if (idx >= stack.length) {
113
- return done(err)
84
+ return done(err);
114
85
  }
115
86
 
116
87
  // max sync stack
117
88
  if (++sync > 100) {
118
- return setImmediate(next, err)
89
+ return setImmediate(next, err);
119
90
  }
120
91
 
121
- let layer
122
- let match
92
+ let layer;
93
+ let match;
123
94
 
124
95
  // find next matching layer
125
96
  while (idx < stack.length) {
126
- layer = stack[idx++]
127
- match = !layer.method || layer.method === method
97
+ layer = stack[idx++];
98
+ match = !layer.method || layer.method === method;
128
99
  if (match) {
129
- break
100
+ break;
130
101
  }
131
102
  }
132
103
 
133
104
  // no match
134
105
  if (match !== true) {
135
- return done(err)
106
+ return done(err);
136
107
  }
137
108
 
138
109
  if (err) {
139
- layer.handleError(err, req, res, next)
110
+ layer.handleError(err, req, res, next);
140
111
  } else {
141
- layer.handleRequest(req, res, next)
112
+ layer.handleRequest(req, res, next);
142
113
  }
143
114
 
144
- sync = 0
115
+ sync = 0;
145
116
  }
146
- }
117
+ };
147
118
 
148
119
  /**
149
120
  * Add a handler for all HTTP verbs to this route.
@@ -173,31 +144,30 @@ Route.prototype.dispatch = function dispatch (req, res, done) {
173
144
  * @api public
174
145
  */
175
146
 
176
- Route.prototype.all = function all (...args) {
177
- const callbacks = args.flat(Infinity)
147
+ Route.prototype.all = function all(...args) {
148
+ const callbacks = args.flat(Number.POSITIVE_INFINITY);
178
149
 
179
150
  if (callbacks.length === 0) {
180
- throw new TypeError('argument handler is required')
151
+ throw new TypeError('argument handler is required');
181
152
  }
182
153
 
183
- this.methods._all = true
184
- this.stack.push(...callbacks.map(fn => new Layer('/', {}, fn)))
154
+ this.methods._all = true;
155
+ this.stack.push(...callbacks.map(fn => new Layer('/', {}, fn)));
185
156
 
186
- return this
187
- }
157
+ return this;
158
+ };
188
159
 
189
- METHODS
190
- .forEach(method => {
191
- Route.prototype[method.toLowerCase()] = function (...args) {
192
- const callbacks = args.flat(Infinity)
160
+ METHODS.forEach(method => {
161
+ Route.prototype[method.toLowerCase()] = function (...args) {
162
+ const callbacks = args.flat(Number.POSITIVE_INFINITY);
193
163
 
194
- if (callbacks.length === 0) {
195
- throw new TypeError('argument handler is required')
196
- }
164
+ if (callbacks.length === 0) {
165
+ throw new TypeError('argument handler is required');
166
+ }
197
167
 
198
- this.methods.add(method)
199
- this.stack.push(...callbacks.map(fn => new Layer('/', {}, fn, method)))
168
+ this.methods.add(method);
169
+ this.stack.push(...callbacks.map(fn => new Layer('/', {}, fn, method)));
200
170
 
201
- return this
202
- }
203
- })
171
+ return this;
172
+ };
173
+ });
package/package.json CHANGED
@@ -1,35 +1,32 @@
1
1
  {
2
2
  "name": "@pirxpilot/router",
3
3
  "description": "Simple middleware-style router",
4
- "version": "1.1.0",
4
+ "version": "2.0.1",
5
5
  "author": "Douglas Christopher Wilson <doug@somethingdoug.com>",
6
6
  "contributors": [
7
7
  "Blake Embrey <hello@blakeembrey.com>"
8
8
  ],
9
9
  "license": "MIT",
10
+ "exports": "./index.js",
11
+ "type": "module",
10
12
  "repository": {
11
13
  "type": "git",
12
14
  "url": "git+https://github.com/pirxpilot/router.git"
13
15
  },
14
16
  "dependencies": {
15
- "parseurl": "~1",
16
- "path-to-regexp": "~8"
17
+ "parseurl": "~1"
17
18
  },
18
19
  "devDependencies": {
19
- "finalhandler": "~1",
20
- "standard": "~17",
20
+ "@biomejs/biome": "2.3.1",
21
+ "finalhandler": "~2",
21
22
  "supertest": "~7"
22
23
  },
23
24
  "files": [
24
25
  "lib/",
25
- "LICENSE",
26
- "HISTORY.md",
27
- "README.md",
28
- "SECURITY.md",
29
26
  "index.js"
30
27
  ],
31
28
  "engines": {
32
- "node": ">= 0.10"
29
+ "node": ">= 23.8.0"
33
30
  },
34
31
  "scripts": {
35
32
  "test": "make check"