fulmine.js 5.2.0 → 5.3.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/NOTICE +29 -2
- package/README.md +90 -61
- package/package.json +6 -2
- package/src/application.js +67 -34
- package/src/cli.js +302 -5
- package/src/declarative.js +19 -0
- package/src/index.js +2 -0
- package/src/middlewares.js +67 -6
- package/src/node-shim.js +5 -3
- package/src/options.d.ts +16 -0
- package/src/parse-query.js +19 -2
- package/src/request.js +275 -31
- package/src/response.js +52 -7
- package/src/router.js +632 -159
- package/src/types.d.ts +16 -0
- package/src/usage.js +16 -0
- package/src/utils.js +267 -36
- package/src/view.js +2 -0
- package/src/websocket.js +16 -0
- package/src/worker.js +2 -0
package/src/types.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2026 Nigro Simone
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
1
17
|
declare module "fulmine.js" {
|
|
2
18
|
import e from "express";
|
|
3
19
|
import uWS from "uWebSockets.js";
|
package/src/usage.js
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2026 Nigro Simone
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
1
17
|
"use strict";
|
|
2
18
|
|
|
3
19
|
const acorn = require("acorn");
|
package/src/utils.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Copyright 2024 dimden.dev
|
|
3
3
|
Copyright 2026 Nigro Simone
|
|
4
4
|
|
|
5
|
+
This file is derived from Ultimate Express and has been modified.
|
|
6
|
+
|
|
5
7
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
8
|
you may not use this file except in compliance with the License.
|
|
7
9
|
You may obtain a copy of the License at
|
|
@@ -26,6 +28,12 @@ const { Stats } = require("fs");
|
|
|
26
28
|
|
|
27
29
|
const EMPTY_REGEX = new RegExp(``);
|
|
28
30
|
|
|
31
|
+
// what express hands qs for a query string. allowPrototypes keeps a key named "constructor" or
|
|
32
|
+
// "toString" instead of dropping it, and it is safe here for the reason it is safe there: the
|
|
33
|
+
// result below sits on a null prototype, so such a key is inert data. A caller with options of its
|
|
34
|
+
// own, the extended body parser, passes them and does not get these.
|
|
35
|
+
const QUERY_QS_OPTIONS = { allowPrototypes: true };
|
|
36
|
+
|
|
29
37
|
/**
|
|
30
38
|
* Parses a query string the way the "extended" parser is expected to, which means qs and its
|
|
31
39
|
* support for nested keys, but without paying for qs on the strings that cannot need it. A short
|
|
@@ -45,12 +53,29 @@ function fastQueryParse(query, options) {
|
|
|
45
53
|
return Object.create(null);
|
|
46
54
|
}
|
|
47
55
|
if (len <= 128) {
|
|
48
|
-
|
|
56
|
+
// An empty name is the other place the two disagree: "=v" and "a=1&=2" are a pair under
|
|
57
|
+
// the empty key to fast-querystring, and nothing at all to qs. A query carrying one goes
|
|
58
|
+
// the slow way, which is what makes the shortcut above safe to take for the rest.
|
|
59
|
+
if (
|
|
60
|
+
!query.includes("[") &&
|
|
61
|
+
!query.includes("%5B") &&
|
|
62
|
+
!query.includes(".") &&
|
|
63
|
+
!query.includes("%2E") &&
|
|
64
|
+
query.charCodeAt(0) !== 0x3d &&
|
|
65
|
+
!query.includes("&=")
|
|
66
|
+
) {
|
|
49
67
|
// already on a bare null prototype, no copy needed, see parse-query.js
|
|
50
|
-
|
|
68
|
+
const parsed = parseQuery(query);
|
|
69
|
+
// qs drops a "__proto__" key whatever allowPrototypes says, and a null prototype makes
|
|
70
|
+
// this one an ordinary own property rather than the setter, so reading it is a plain
|
|
71
|
+
// load and the encoded spelling is covered too: the name is decoded before it lands
|
|
72
|
+
if (parsed.__proto__ !== undefined) {
|
|
73
|
+
delete parsed.__proto__;
|
|
74
|
+
}
|
|
75
|
+
return parsed;
|
|
51
76
|
}
|
|
52
77
|
}
|
|
53
|
-
return Object.assign(Object.create(null), qs.parse(query, options));
|
|
78
|
+
return Object.assign(Object.create(null), qs.parse(query, options ?? QUERY_QS_OPTIONS));
|
|
54
79
|
}
|
|
55
80
|
|
|
56
81
|
/**
|
|
@@ -64,6 +89,69 @@ function removeDuplicateSlashes(path) {
|
|
|
64
89
|
return path.replace(/\/{2,}/g, "/");
|
|
65
90
|
}
|
|
66
91
|
|
|
92
|
+
// What path-to-regexp takes as a parameter name, which is a javascript identifier: /:café and
|
|
93
|
+
// /:año are names to express and were a dead route here, because \w stops at ASCII. A named
|
|
94
|
+
// capture group accepts the same spellings, so the compiled pattern still carries the name.
|
|
95
|
+
const ID_START = /[$_\p{ID_Start}]/u;
|
|
96
|
+
// the two joiners are written as escapes on purpose: as themselves they are invisible here
|
|
97
|
+
const ID_CONTINUE = /[$\u200c\u200d\p{ID_Continue}]/u;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reads a parameter name out of a pattern. Walks code points rather than code units, so a name
|
|
101
|
+
* starting outside the basic plane is read whole instead of as half a surrogate pair.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} text the pattern, or the contents of one optional group
|
|
104
|
+
* @param {number} from the index just past the ":" or the "*"
|
|
105
|
+
* @returns {{name: string, next: number}} the name, empty when there is none, and where it ended
|
|
106
|
+
*/
|
|
107
|
+
function readParamName(text, from) {
|
|
108
|
+
let i = from;
|
|
109
|
+
let name = "";
|
|
110
|
+
while (i < text.length) {
|
|
111
|
+
const char = String.fromCodePoint(/** @type {number} */ (text.codePointAt(i)));
|
|
112
|
+
if (!(name === "" ? ID_START : ID_CONTINUE).test(char)) {
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
name += char;
|
|
116
|
+
i += char.length;
|
|
117
|
+
}
|
|
118
|
+
return { name, next: i };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Escapes a string for use inside a regular expression, as path-to-regexp escapes it.
|
|
123
|
+
* @param {string} str
|
|
124
|
+
* @returns {string}
|
|
125
|
+
*/
|
|
126
|
+
function escapeRe(str) {
|
|
127
|
+
return str.replace(/[.+*?^${}()[\]|/\\]/g, "\\$&");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A character class that matches anything except what these two strings could start, which is how
|
|
132
|
+
* path-to-regexp stops a capture from backtracking over the literal text that follows it. Ported
|
|
133
|
+
* from its negate(), so the patterns compiled here agree with the ones express matches.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} a
|
|
136
|
+
* @param {string} b
|
|
137
|
+
* @returns {string}
|
|
138
|
+
*/
|
|
139
|
+
function negate(a, b) {
|
|
140
|
+
if (b.length > a.length) {
|
|
141
|
+
return negate(b, a);
|
|
142
|
+
}
|
|
143
|
+
if (a === b) {
|
|
144
|
+
b = "";
|
|
145
|
+
}
|
|
146
|
+
if (b.length > 1) {
|
|
147
|
+
return `(?:(?!${escapeRe(a)}|${escapeRe(b)})[^])`;
|
|
148
|
+
}
|
|
149
|
+
if (a.length > 1) {
|
|
150
|
+
return `(?:(?!${escapeRe(a)})[^${escapeRe(b)}])`;
|
|
151
|
+
}
|
|
152
|
+
return `[^${escapeRe(a + b)}]`;
|
|
153
|
+
}
|
|
154
|
+
|
|
67
155
|
// the opening of a named capture group, which is how the parameter names are read back out of a
|
|
68
156
|
// finished pattern
|
|
69
157
|
const NAMED_GROUP = /\(\?<([^>]+)>/g;
|
|
@@ -73,7 +161,7 @@ const NAMED_GROUP = /\(\?<([^>]+)>/g;
|
|
|
73
161
|
* into an array. Here and not on the regex itself, because one own property takes a RegExp off V8's
|
|
74
162
|
* fast path: replace() went from 37ns to 808ns and test() from 22ns to 78ns.
|
|
75
163
|
*
|
|
76
|
-
* @typedef {{wildcardNames: string[], paramNames: string[], isWildcard: boolean[]}} PatternMeta
|
|
164
|
+
* @typedef {{wildcardNames: string[], paramNames: string[], outputNames: string[], isWildcard: boolean[]}} PatternMeta
|
|
77
165
|
*/
|
|
78
166
|
const patternMeta = new WeakMap();
|
|
79
167
|
|
|
@@ -103,7 +191,7 @@ function getPatternMeta(pattern) {
|
|
|
103
191
|
* throw: a route that quietly stops matching is worse than one that fails at startup. The names it
|
|
104
192
|
* captures go in a WeakMap beside the regex, see PatternMeta.
|
|
105
193
|
*/
|
|
106
|
-
function patternToRegex(pattern, isPrefix = false, caseSensitive = true) {
|
|
194
|
+
function patternToRegex(pattern, isPrefix = false, caseSensitive = true, strict = false) {
|
|
107
195
|
if (pattern instanceof RegExp) {
|
|
108
196
|
// the application's own RegExp matches as written, whatever the routing setting says,
|
|
109
197
|
// which is what express does with one
|
|
@@ -117,15 +205,122 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true) {
|
|
|
117
205
|
let i = 0;
|
|
118
206
|
const len = pattern.length;
|
|
119
207
|
const wildcardNames = [];
|
|
208
|
+
// express takes /:a/:a, and two capture groups cannot share a name, so a repeat is compiled
|
|
209
|
+
// under a spelling of its own and mapped back when the parameters are read out. Reading them
|
|
210
|
+
// in order then leaves the last occurrence in place, which is the value express reports
|
|
211
|
+
const groupOutputName = new Map();
|
|
212
|
+
const uniqueGroupName = (name) => {
|
|
213
|
+
if (!groupOutputName.has(name)) {
|
|
214
|
+
groupOutputName.set(name, name);
|
|
215
|
+
return name;
|
|
216
|
+
}
|
|
217
|
+
let n = 2;
|
|
218
|
+
while (groupOutputName.has(name + "$" + n)) n++;
|
|
219
|
+
const group = name + "$" + n;
|
|
220
|
+
groupOutputName.set(group, name);
|
|
221
|
+
return group;
|
|
222
|
+
};
|
|
120
223
|
// whether the token just emitted was a :parameter, which decides how greedy the next
|
|
121
224
|
// optional group is allowed to be. see the comment where it is read
|
|
122
225
|
let lastTokenWasParam = false;
|
|
226
|
+
// What path-to-regexp calls the wildcard backtrack: the literal text written since the last
|
|
227
|
+
// wildcard. Once a wildcard has eaten slashes, a later one in the same path is held to a single
|
|
228
|
+
// segment, or the two would divide the path between them in more than one way and the regex
|
|
229
|
+
// would have to backtrack to find out which. /*a/*b against /x/y/ is the case that shows it:
|
|
230
|
+
// express refuses it under strict routing, and a second greedy wildcard accepts it.
|
|
231
|
+
// the text written since the last capture of any kind, and the text written since the last
|
|
232
|
+
// wildcard, which are the two path-to-regexp weighs
|
|
233
|
+
let backtrack = "";
|
|
234
|
+
let wildcardBacktrack = "";
|
|
235
|
+
let lastCaptureWasWildcard = false;
|
|
236
|
+
let wildcardInSegment = false;
|
|
237
|
+
let paramInSegment = false;
|
|
238
|
+
/** Records literal text as it is emitted, which is what the rules above are written against. */
|
|
239
|
+
const literal = (text) => {
|
|
240
|
+
backtrack += text;
|
|
241
|
+
if (lastCaptureWasWildcard) {
|
|
242
|
+
wildcardBacktrack += text;
|
|
243
|
+
}
|
|
244
|
+
if (text.includes("/")) {
|
|
245
|
+
wildcardInSegment = false;
|
|
246
|
+
paramInSegment = false;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* Whether a wildcard is still to come in the segment being written, which makes the parameters
|
|
251
|
+
* before it give ground so that it has something left to match.
|
|
252
|
+
*
|
|
253
|
+
* @param {number} from where to look from
|
|
254
|
+
* @returns {boolean}
|
|
255
|
+
*/
|
|
256
|
+
const wildcardLaterInSegment = (from) => {
|
|
257
|
+
for (let j = from; j < len; j++) {
|
|
258
|
+
const c = pattern[j];
|
|
259
|
+
if (c === "\\") {
|
|
260
|
+
j++;
|
|
261
|
+
} else if (c === "/") {
|
|
262
|
+
return false;
|
|
263
|
+
} else if (c === "*") {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
};
|
|
269
|
+
/**
|
|
270
|
+
* The literal text written immediately after this point, which is what the capture before it
|
|
271
|
+
* must not swallow.
|
|
272
|
+
*
|
|
273
|
+
* @param {number} from
|
|
274
|
+
* @returns {string}
|
|
275
|
+
*/
|
|
276
|
+
const textAfter = (from) => {
|
|
277
|
+
let out = "";
|
|
278
|
+
for (let j = from; j < len; j++) {
|
|
279
|
+
const c = pattern[j];
|
|
280
|
+
if (c === "\\") {
|
|
281
|
+
out += pattern[++j] ?? "";
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (":*{}".includes(c)) {
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
out += c;
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
};
|
|
291
|
+
/** A :parameter ends the run of text and stops the wildcard one from growing. */
|
|
292
|
+
const noteParam = () => {
|
|
293
|
+
backtrack = "";
|
|
294
|
+
lastCaptureWasWildcard = false;
|
|
295
|
+
paramInSegment = true;
|
|
296
|
+
lastTokenWasParam = true;
|
|
297
|
+
};
|
|
298
|
+
/**
|
|
299
|
+
* What a wildcard is allowed to match here, and the bookkeeping that goes with emitting one.
|
|
300
|
+
* The first wildcard of a path takes everything; one sharing a segment with an earlier wildcard
|
|
301
|
+
* stops at the text between them; and one in a later segment is held to a single segment.
|
|
302
|
+
*
|
|
303
|
+
* @returns {string} the body of the capture group
|
|
304
|
+
*/
|
|
305
|
+
const wildcardClass = () => {
|
|
306
|
+
const body = wildcardInSegment
|
|
307
|
+
? `${negate(backtrack, "")}+`
|
|
308
|
+
: wildcardBacktrack
|
|
309
|
+
? `${negate(wildcardBacktrack, "")}+|${negate("/", "")}+`
|
|
310
|
+
: "[^]+";
|
|
311
|
+
backtrack = "";
|
|
312
|
+
wildcardBacktrack = "";
|
|
313
|
+
lastCaptureWasWildcard = true;
|
|
314
|
+
wildcardInSegment = true;
|
|
315
|
+
return body;
|
|
316
|
+
};
|
|
123
317
|
|
|
124
318
|
while (i < len) {
|
|
125
319
|
const ch = pattern[i];
|
|
126
320
|
|
|
127
321
|
if (ch === "\\" && i + 1 < len) {
|
|
128
322
|
regexPattern += "\\" + pattern[i + 1];
|
|
323
|
+
literal(pattern[i + 1]);
|
|
129
324
|
i += 2;
|
|
130
325
|
continue;
|
|
131
326
|
}
|
|
@@ -134,16 +329,17 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true) {
|
|
|
134
329
|
// boundary, so /te*st is literal "/te" followed by a wildcard named "st"
|
|
135
330
|
if (ch === "*") {
|
|
136
331
|
const at = i;
|
|
137
|
-
i
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
name += pattern[i++];
|
|
141
|
-
}
|
|
332
|
+
const splat = readParamName(pattern, i + 1);
|
|
333
|
+
const name = splat.name;
|
|
334
|
+
i = splat.next;
|
|
142
335
|
if (!name) {
|
|
143
|
-
throw new Error(
|
|
336
|
+
throw new Error(
|
|
337
|
+
`Missing parameter name at index ${at + 1}: ${pattern}; visit https://git.new/pathToRegexpError for info`
|
|
338
|
+
);
|
|
144
339
|
}
|
|
145
|
-
|
|
146
|
-
|
|
340
|
+
const splatGroup = uniqueGroupName(name);
|
|
341
|
+
wildcardNames.push(splatGroup);
|
|
342
|
+
regexPattern += `(?<${splatGroup}>${wildcardClass()})`;
|
|
147
343
|
lastTokenWasParam = false;
|
|
148
344
|
continue;
|
|
149
345
|
}
|
|
@@ -160,14 +356,23 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true) {
|
|
|
160
356
|
if (!name) {
|
|
161
357
|
throw new Error(`Wildcard must be named in Express 5: use {*splat} (in "${pattern}")`);
|
|
162
358
|
}
|
|
163
|
-
|
|
359
|
+
const optionalGroup = uniqueGroupName(name);
|
|
360
|
+
wildcardNames.push(optionalGroup);
|
|
164
361
|
if (regexPattern.endsWith("/") || regexPattern.endsWith("\\/")) {
|
|
165
|
-
// the slash
|
|
362
|
+
// the slash is part of the alternative rather than optional on its own: a
|
|
363
|
+
// mount at /a/{*w} answers /a/ and /a/x, and not /a, which is what express
|
|
364
|
+
// compiles it to
|
|
166
365
|
regexPattern = regexPattern.slice(0, regexPattern.endsWith("\\/") ? -2 : -1);
|
|
167
|
-
|
|
366
|
+
// under strict routing the slash is part of the alternative, so /a/{*w}
|
|
367
|
+
// answers /a/ and /a/x and not /a. Without it express loosens the pattern and
|
|
368
|
+
// allows a trailing slash instead, which is the shape the path arrives in here
|
|
369
|
+
regexPattern += strict
|
|
370
|
+
? `(?:/(?<${optionalGroup}>${wildcardClass()})|/)`
|
|
371
|
+
: `(?:/(?<${optionalGroup}>${wildcardClass()}))?/?`;
|
|
168
372
|
} else {
|
|
169
|
-
regexPattern += `(?<${
|
|
373
|
+
regexPattern += `(?<${optionalGroup}>${wildcardClass()}|)`;
|
|
170
374
|
}
|
|
375
|
+
lastTokenWasParam = false;
|
|
171
376
|
continue;
|
|
172
377
|
}
|
|
173
378
|
|
|
@@ -197,12 +402,10 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true) {
|
|
|
197
402
|
let gi = 0;
|
|
198
403
|
while (gi < groupContent.length) {
|
|
199
404
|
if (groupContent[gi] === ":") {
|
|
200
|
-
gi
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
groupRegex += `(?<${paramName}>${groupParamClass})`;
|
|
405
|
+
const inner = readParamName(groupContent, gi + 1);
|
|
406
|
+
const paramName = inner.name;
|
|
407
|
+
gi = inner.next;
|
|
408
|
+
groupRegex += `(?<${uniqueGroupName(paramName)}>${groupParamClass})`;
|
|
206
409
|
} else if (groupContent[gi] === ".") {
|
|
207
410
|
groupRegex += "\\.";
|
|
208
411
|
gi++;
|
|
@@ -215,22 +418,43 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true) {
|
|
|
215
418
|
}
|
|
216
419
|
}
|
|
217
420
|
regexPattern += `(?:${groupRegex})?`;
|
|
421
|
+
literal(groupContent);
|
|
218
422
|
lastTokenWasParam = false;
|
|
219
423
|
continue;
|
|
220
424
|
}
|
|
221
425
|
|
|
222
426
|
if (ch === ":") {
|
|
223
|
-
i
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
name += pattern[i++];
|
|
227
|
-
}
|
|
427
|
+
const named = readParamName(pattern, i + 1);
|
|
428
|
+
const name = named.name;
|
|
429
|
+
i = named.next;
|
|
228
430
|
if (!name) {
|
|
229
|
-
throw new Error(
|
|
431
|
+
throw new Error(
|
|
432
|
+
`Missing parameter name at index ${i}: ${pattern}; visit https://git.new/pathToRegexpError for info`
|
|
433
|
+
);
|
|
230
434
|
}
|
|
231
435
|
// a following optional group needs room to match, so the parameter gives ground
|
|
232
|
-
|
|
233
|
-
|
|
436
|
+
const paramGroup = uniqueGroupName(name);
|
|
437
|
+
// How much of its segment a parameter may take, which path-to-regexp decides by what
|
|
438
|
+
// else shares the segment with it. Alone it takes everything up to the next slash; with
|
|
439
|
+
// a wildcard beside it, before or after, it stops at the text that separates the two,
|
|
440
|
+
// so the wildcard has something left to match; and a second parameter in the segment
|
|
441
|
+
// may also be exactly that separating text, which is the alternative below.
|
|
442
|
+
let head;
|
|
443
|
+
let alternative = "";
|
|
444
|
+
if (wildcardInSegment) {
|
|
445
|
+
head = negate("/", backtrack);
|
|
446
|
+
} else if (wildcardLaterInSegment(i)) {
|
|
447
|
+
head = negate("/", textAfter(i));
|
|
448
|
+
} else if (paramInSegment) {
|
|
449
|
+
head = negate("/", backtrack);
|
|
450
|
+
alternative = "|" + escapeRe(backtrack);
|
|
451
|
+
} else {
|
|
452
|
+
head = "[^/]";
|
|
453
|
+
}
|
|
454
|
+
// a following optional group needs room to match, so the parameter gives ground
|
|
455
|
+
const lazy = i < len && pattern[i] === "{" ? "?" : "";
|
|
456
|
+
regexPattern += `(?<${paramGroup}>${head}+${lazy}${alternative})`;
|
|
457
|
+
noteParam();
|
|
234
458
|
continue;
|
|
235
459
|
}
|
|
236
460
|
|
|
@@ -246,19 +470,24 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true) {
|
|
|
246
470
|
} else {
|
|
247
471
|
regexPattern += ch;
|
|
248
472
|
}
|
|
473
|
+
literal(ch);
|
|
249
474
|
lastTokenWasParam = false;
|
|
250
475
|
i++;
|
|
251
476
|
}
|
|
252
477
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
478
|
+
// Without strict routing express allows one trailing slash at the end of the pattern
|
|
479
|
+
// rather than taking it off the path, which is the only way /things and /things/ can be the
|
|
480
|
+
// same route while // and /things/ stay different paths. A mount ends at a segment boundary
|
|
481
|
+
// instead, and the slash after it belongs to what follows.
|
|
482
|
+
const ending = isPrefix ? "(?=$|/)" : strict ? "$" : "/?$";
|
|
483
|
+
const regex = /** @type {PathRegExp} */ (new RegExp(`^${regexPattern}${ending}`, caseSensitive ? "" : "i"));
|
|
256
484
|
// read back out of the finished pattern, so the list cannot disagree with the regex. Asking for
|
|
257
485
|
// each name in turn beats walking match.groups with for-in: 176ns against 349
|
|
258
486
|
const paramNames = [...regexPattern.matchAll(NAMED_GROUP)].map((m) => m[1]);
|
|
259
487
|
patternMeta.set(regex, {
|
|
260
488
|
wildcardNames,
|
|
261
489
|
paramNames,
|
|
490
|
+
outputNames: paramNames.map((name) => groupOutputName.get(name) ?? name),
|
|
262
491
|
isWildcard: paramNames.map((name) => wildcardNames.includes(name))
|
|
263
492
|
});
|
|
264
493
|
return regex;
|
|
@@ -307,7 +536,7 @@ function canBeOptimized(pattern) {
|
|
|
307
536
|
|
|
308
537
|
// a parameter that is the whole segment, which is the only shape µWS matches the same way Express
|
|
309
538
|
// does. "/flights/:from-:to" is one segment to µWS and two parameters to Express.
|
|
310
|
-
const WHOLE_SEGMENT_PARAM =
|
|
539
|
+
const WHOLE_SEGMENT_PARAM = /^:[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*$/u;
|
|
311
540
|
|
|
312
541
|
/**
|
|
313
542
|
* Whether µWS's own router matches this path exactly as Express would, parameters included.
|
|
@@ -704,7 +933,9 @@ function decodeParam(value) {
|
|
|
704
933
|
try {
|
|
705
934
|
return decodeURIComponent(value);
|
|
706
935
|
} catch {
|
|
707
|
-
|
|
936
|
+
// a URIError, not an Error: express throws what decodeURIComponent threw, so an error
|
|
937
|
+
// handler written as `err instanceof URIError` has to keep working here
|
|
938
|
+
const err = /** @type {any} */ (new URIError(`Failed to decode param '${value}'`));
|
|
708
939
|
err.status = 400;
|
|
709
940
|
err.statusCode = 400;
|
|
710
941
|
err.expose = true;
|
package/src/view.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Copyright 2024 dimden.dev
|
|
3
3
|
Copyright 2026 Nigro Simone
|
|
4
4
|
|
|
5
|
+
This file is derived from Ultimate Express and has been modified.
|
|
6
|
+
|
|
5
7
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
8
|
you may not use this file except in compliance with the License.
|
|
7
9
|
You may obtain a copy of the License at
|
package/src/websocket.js
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2026 Nigro Simone
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
1
17
|
"use strict";
|
|
2
18
|
|
|
3
19
|
const { canBeOptimizedWithParams, decodeParam, NullObject } = require("./utils.js");
|
package/src/worker.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Copyright 2024 dimden.dev
|
|
3
3
|
Copyright 2026 Nigro Simone
|
|
4
4
|
|
|
5
|
+
This file is derived from Ultimate Express and has been modified.
|
|
6
|
+
|
|
5
7
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
8
|
you may not use this file except in compliance with the License.
|
|
7
9
|
You may obtain a copy of the License at
|