fulmine.js 5.12.2 → 5.13.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/README.md +83 -29
- package/package.json +33 -3
- package/src/adopt.js +324 -0
- package/src/application.js +2 -2
- package/src/cli.js +25 -6
- package/src/declarative.js +17 -9
- package/src/middlewares.js +11 -4
- package/src/nest.d.ts +39 -0
- package/src/nest.js +119 -0
- package/src/request.js +103 -22
- package/src/response.js +24 -3
- package/src/router.js +72 -13
- package/src/testing.js +27 -3
- package/src/utils.js +22 -2
package/src/router.js
CHANGED
|
@@ -354,6 +354,13 @@ class Walk {
|
|
|
354
354
|
runRoute(continueRoute) {
|
|
355
355
|
const req = this.req;
|
|
356
356
|
const route = this.route;
|
|
357
|
+
// A compiled chain walks into a mount rather than entering it, so the rule above needs
|
|
358
|
+
// saying here as well: everything after this marker is inside the mount, and a mount is
|
|
359
|
+
// stepped over while an error is in flight. Leaving the chain is what running out of it
|
|
360
|
+
// already means, and ordinary routing takes over after the mount.
|
|
361
|
+
if (route.keepMount === true && req._error) {
|
|
362
|
+
return this.dispatch(this.routes.length);
|
|
363
|
+
}
|
|
357
364
|
if (route.use) {
|
|
358
365
|
if (route.mountApp) {
|
|
359
366
|
// optimized chain: normal dispatch swaps req.app when it enters a mounted
|
|
@@ -484,7 +491,12 @@ class Walk {
|
|
|
484
491
|
if (!this.skipCheck && this.skipUntil && this.skipUntil.routeKey >= route.routeKey) {
|
|
485
492
|
return this.step(undefined);
|
|
486
493
|
}
|
|
487
|
-
|
|
494
|
+
// A mounted router or application is stepped over while an error is in flight. Its handle
|
|
495
|
+
// takes three arguments, so express's Layer#handleError hands the error straight on without
|
|
496
|
+
// entering it: what a mount catches is what it raised itself. Entering it ran the error
|
|
497
|
+
// handlers written inside the mount, and left req.app pointing at a mounted application,
|
|
498
|
+
// whose settings then answered. A 500 carried an ETag under app.set("etag", false).
|
|
499
|
+
if (kind === CALLBACK_ROUTER && !req._error) {
|
|
488
500
|
if (callback._isApplication) {
|
|
489
501
|
rememberApp(this, route, req);
|
|
490
502
|
useApp(req, callback);
|
|
@@ -683,10 +695,18 @@ function setMountedPath(req) {
|
|
|
683
695
|
req._opPath = req._consumed === 0 ? req._originalPath : req._originalPath.slice(req._consumed);
|
|
684
696
|
req._opPathLower = null;
|
|
685
697
|
req.url = req._opPath === "" ? "/" + req.urlQuery : req._opPath + req.urlQuery;
|
|
686
|
-
req.
|
|
698
|
+
req._path = req._opPath === "" ? "/" : req._opPath;
|
|
687
699
|
req._lastUrl = req.url;
|
|
688
700
|
}
|
|
689
701
|
|
|
702
|
+
// req.path as the request class declares it, taken off the prototype rather than written out a
|
|
703
|
+
// second time. A request the router adopts is a plain object and gets it defined on itself, see
|
|
704
|
+
// adoptPlainRequest. Enumerable, as express's own is.
|
|
705
|
+
const PATH_PROPERTY = {
|
|
706
|
+
.../** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(Request.prototype, "path")),
|
|
707
|
+
enumerable: true
|
|
708
|
+
};
|
|
709
|
+
|
|
690
710
|
const NO_PARAM_NAMES = [];
|
|
691
711
|
|
|
692
712
|
/**
|
|
@@ -830,7 +850,10 @@ function adoptPlainRequest(req, router) {
|
|
|
830
850
|
const path = queryIndex === -1 ? raw : raw.slice(0, queryIndex);
|
|
831
851
|
req.urlQuery = queryIndex === -1 ? "" : raw.slice(queryIndex);
|
|
832
852
|
req._rawQuery = req.urlQuery.slice(1);
|
|
833
|
-
req.
|
|
853
|
+
req._path = path;
|
|
854
|
+
// an adopted request is a plain object, so it carries no prototype of ours and reads its path
|
|
855
|
+
// off a property of its own. The class's getter itself, so there is one of it
|
|
856
|
+
Object.defineProperty(req, "path", PATH_PROPERTY);
|
|
834
857
|
req.originalUrl = req.originalUrl ?? arrived;
|
|
835
858
|
req._originalPath = path;
|
|
836
859
|
req.endsWithSlash = path.charCodeAt(path.length - 1) === 0x2f;
|
|
@@ -1056,7 +1079,17 @@ function stepsOver(route, req) {
|
|
|
1056
1079
|
if (route.bodyMethods === undefined) {
|
|
1057
1080
|
route.bodyMethods = req.app.get("body methods") ?? null;
|
|
1058
1081
|
}
|
|
1059
|
-
|
|
1082
|
+
if (route.bodyMethods !== null && route.bodyMethods.includes(req.method)) {
|
|
1083
|
+
return false;
|
|
1084
|
+
}
|
|
1085
|
+
// The layer is not entered, so it leaves the one mark it would have left: the parser puts
|
|
1086
|
+
// `body` on the request before it works out that there is nothing to read. A library asks
|
|
1087
|
+
// `"body" in req` to tell "a parser has run" from "none has", and a skip that did not leave
|
|
1088
|
+
// it would answer a GET differently from express. See the same seeding in middlewares.js
|
|
1089
|
+
if (!("body" in req)) {
|
|
1090
|
+
req.body = undefined;
|
|
1091
|
+
}
|
|
1092
|
+
return true;
|
|
1060
1093
|
}
|
|
1061
1094
|
|
|
1062
1095
|
/**
|
|
@@ -1848,6 +1881,16 @@ module.exports = class Router extends EventEmitter {
|
|
|
1848
1881
|
}
|
|
1849
1882
|
}
|
|
1850
1883
|
|
|
1884
|
+
// The same rule as the one just above, which a route of another method reaches by
|
|
1885
|
+
// another road. A mount's chain is inherited by every path under it, and a route that
|
|
1886
|
+
// is not itself a mount answers the mount point rather than the subtree: in the chain
|
|
1887
|
+
// it ran for the whole of it, so router.all("/:p1") answered the /posts/a-b that
|
|
1888
|
+
// belongs to the router mounted at /posts. guardsInside is written for this: only
|
|
1889
|
+
// layers with more segments than the mount path are asked about a leaf.
|
|
1890
|
+
if (route.use && !r.use && typeof route.path === "string" && couldAnswer(r, route.path)) {
|
|
1891
|
+
return false;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1851
1894
|
// a RegExp mount runs only where its match starts the path and breaks on a separator,
|
|
1852
1895
|
// which is decidable here against a literal path and not against one with a parameter
|
|
1853
1896
|
if (r.regexMount) {
|
|
@@ -1874,13 +1917,9 @@ module.exports = class Router extends EventEmitter {
|
|
|
1874
1917
|
|
|
1875
1918
|
// check if the paths match. A route with parameters is excluded from the text test:
|
|
1876
1919
|
// its literal ":name" text would let an earlier regex in on requests it never matches.
|
|
1877
|
-
|
|
1878
|
-
// answers "/x/" as well as "/x", and an earlier pattern that matches only the first is
|
|
1879
|
-
// still an earlier pattern that answers a request this registration would take.
|
|
1920
|
+
const regexCanMatch = r.pattern instanceof RegExp && (!withParams || r.use);
|
|
1880
1921
|
if (
|
|
1881
|
-
(r.pattern
|
|
1882
|
-
(!withParams || r.use) &&
|
|
1883
|
-
(r.pattern.test(route.path) || (!strictHere && r.pattern.test(route.path + "/")))) ||
|
|
1922
|
+
(regexCanMatch && r.pattern.test(route.path)) ||
|
|
1884
1923
|
(typeof r.pattern === "string" &&
|
|
1885
1924
|
(r.pattern === route.path ||
|
|
1886
1925
|
(!caseSensitive && r.pattern.toLowerCase() === routePathFolded) ||
|
|
@@ -1892,6 +1931,15 @@ module.exports = class Router extends EventEmitter {
|
|
|
1892
1931
|
optimizedPath.push(r);
|
|
1893
1932
|
continue;
|
|
1894
1933
|
}
|
|
1934
|
+
// Without strict routing this registration answers "/x/" as well as "/x". An earlier
|
|
1935
|
+
// pattern matching only the second answers part of what the registration takes and not
|
|
1936
|
+
// the rest, which the chain has no way to say: it runs what is in it without matching
|
|
1937
|
+
// again. Both spellings used to put the route in whole, so app.all("/:p0/{:o1}/{:o2}")
|
|
1938
|
+
// answered a GET /list/Mixed that belonged to the route written after it. An ordinary
|
|
1939
|
+
// pattern answers both spellings, so only an optional group or a wildcard reaches here.
|
|
1940
|
+
if (regexCanMatch && !strictHere && r.pattern.test(route.path + "/")) {
|
|
1941
|
+
return false;
|
|
1942
|
+
}
|
|
1895
1943
|
if (!withParams) {
|
|
1896
1944
|
continue;
|
|
1897
1945
|
}
|
|
@@ -1951,6 +1999,13 @@ module.exports = class Router extends EventEmitter {
|
|
|
1951
1999
|
if (!this.uwsApp) {
|
|
1952
2000
|
return;
|
|
1953
2001
|
}
|
|
2002
|
+
// Everything below is what makes this framework fast, and every one of its decisions is a
|
|
2003
|
+
// claim that µWS answering by itself is the same answer the chain would have given. Turned
|
|
2004
|
+
// off, the claim is not made and the chain answers everything. Serving one application both
|
|
2005
|
+
// ways and comparing the answers is what tests those claims: `npm run fuzz -- --self`.
|
|
2006
|
+
if (this.get("native routes") === false) {
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
1954
2009
|
|
|
1955
2010
|
// pathPrefix/chainPrefix accumulate across nested sole-callback mounts, and outerGuards
|
|
1956
2011
|
// carries what was written before them and answers only part of what is under them
|
|
@@ -2135,7 +2190,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
2135
2190
|
*
|
|
2136
2191
|
* @param {any} response
|
|
2137
2192
|
*/
|
|
2138
|
-
|
|
2193
|
+
_refuseRequest(response) {
|
|
2139
2194
|
response.finished = true;
|
|
2140
2195
|
response._res.close();
|
|
2141
2196
|
response.emit("close");
|
|
@@ -2247,8 +2302,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
2247
2302
|
}
|
|
2248
2303
|
const request = this.handleRequest(res, req, preset, skipHolder);
|
|
2249
2304
|
const response = request.res;
|
|
2250
|
-
if (request.
|
|
2251
|
-
return this.
|
|
2305
|
+
if (request._mustRefuse === true) {
|
|
2306
|
+
return this._refuseRequest(response);
|
|
2252
2307
|
}
|
|
2253
2308
|
if (optimizedParams) {
|
|
2254
2309
|
request.optimizedParams = new NullObject();
|
|
@@ -2347,6 +2402,10 @@ module.exports = class Router extends EventEmitter {
|
|
|
2347
2402
|
route.callbacks.length === 1 && // must not have multiple callbacks
|
|
2348
2403
|
typeof route.callbacks[0] === "function" && // must be a function
|
|
2349
2404
|
route.paramCallbacks.size === 0 && // a param callback has to run, and this answers without running anything
|
|
2405
|
+
// a captured value is decoded when the route runs, and one that cannot be decoded is a
|
|
2406
|
+
// 400 in express and on the ordinary path here. Nothing runs to raise it on a
|
|
2407
|
+
// declarative response, so GET /a-b%5Ec@d%e came back 200 from app.get("/:p12")
|
|
2408
|
+
route.optimizedParams === undefined &&
|
|
2350
2409
|
// a declarative response is answered by µWS itself, so no javascript runs and the case
|
|
2351
2410
|
// guard could not: a route that needs one has to stay an ordinary handler
|
|
2352
2411
|
caseGuards === null &&
|
package/src/testing.js
CHANGED
|
@@ -171,6 +171,32 @@ function expectNative(app, patterns) {
|
|
|
171
171
|
);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Why a route µWS already matches is still not compiled into a response.
|
|
176
|
+
*
|
|
177
|
+
* The handler is the last answer, not the first: three refusals come before it, and blaming the
|
|
178
|
+
* handler for one of those sends the reader to rewrite something that was already simple enough.
|
|
179
|
+
*
|
|
180
|
+
* @param {any} app
|
|
181
|
+
* @param {{path: string}} entry
|
|
182
|
+
* @returns {string}
|
|
183
|
+
*/
|
|
184
|
+
function whyNotCompiled(app, entry) {
|
|
185
|
+
if (!app.get("declarative responses")) {
|
|
186
|
+
return "answered by µWS, but declarative responses are turned off";
|
|
187
|
+
}
|
|
188
|
+
if (entry.path.includes(":")) {
|
|
189
|
+
return "answered by µWS, but the route captures, and nothing runs to decode the value";
|
|
190
|
+
}
|
|
191
|
+
if (app.get("etag")) {
|
|
192
|
+
return (
|
|
193
|
+
"answered by µWS, but a response carrying an ETag could never answer the conditional " +
|
|
194
|
+
'request it invites: app.set("etag", false) is what puts a route here'
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return "answered by µWS, but the handler is not simple enough to compile";
|
|
198
|
+
}
|
|
199
|
+
|
|
174
200
|
/**
|
|
175
201
|
* Throws unless every route named is answered from a response written at startup, which is the
|
|
176
202
|
* step past native: µWS answers it without entering javascript at all.
|
|
@@ -189,9 +215,7 @@ function expectDeclarative(app, patterns) {
|
|
|
189
215
|
.map(
|
|
190
216
|
(entry) =>
|
|
191
217
|
` ${entry.method} ${entry.path}\n ` +
|
|
192
|
-
(entry.native
|
|
193
|
-
? "answered by µWS, but the handler is no longer simple enough to compile"
|
|
194
|
-
: entry.reason)
|
|
218
|
+
(entry.native ? whyNotCompiled(app, entry) : entry.reason)
|
|
195
219
|
)
|
|
196
220
|
.join("\n") +
|
|
197
221
|
`\n\nRun \`npx fulmine profile\` to see the whole picture.`
|
package/src/utils.js
CHANGED
|
@@ -409,8 +409,21 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true, strict
|
|
|
409
409
|
// splits /a.b.c against /:file{.:ext} as file=a.b, ext=c, which only works if ext
|
|
410
410
|
// cannot contain a dot. After static text there is nothing to give ground, so the
|
|
411
411
|
// parameter takes everything: /file{.:ext} against /file.tar.gz gives ext=tar.gz.
|
|
412
|
-
|
|
413
|
-
|
|
412
|
+
// The whole of it, not its first character: /:foo{abc:bar} against /123abcabc splits as
|
|
413
|
+
// foo=123 and bar=abc on express, and reading the separator as "a" left bar unable to
|
|
414
|
+
// match its own text, so the group never matched and foo took the segment whole. More
|
|
415
|
+
// than one character cannot go in a class, so it is written as a lookahead, and either
|
|
416
|
+
// way the parameter is still allowed to be exactly the separator, as path-to-regexp
|
|
417
|
+
// writes it.
|
|
418
|
+
const colon = groupContent.indexOf(":");
|
|
419
|
+
const separator = lastTokenWasParam && colon > 0 ? groupContent.slice(0, colon) : "";
|
|
420
|
+
const escapedSeparator = separator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
421
|
+
const groupParamClass =
|
|
422
|
+
separator === ""
|
|
423
|
+
? "[^/]+"
|
|
424
|
+
: separator.length === 1
|
|
425
|
+
? `[^/${escapedSeparator}]+|${escapedSeparator}`
|
|
426
|
+
: `(?:(?!${escapedSeparator})[^/])+|${escapedSeparator}`;
|
|
414
427
|
|
|
415
428
|
let groupRegex = "";
|
|
416
429
|
let gi = 0;
|
|
@@ -961,6 +974,13 @@ const defaultSettings = {
|
|
|
961
974
|
// The native µWS router matches bytes, so the compiler in _compileOptimizedRoutes only hands
|
|
962
975
|
// it routes whose earlier siblings it can prove agree under either case rule.
|
|
963
976
|
"declarative responses": true,
|
|
977
|
+
// on. Off hands every request to the ordinary chain instead of letting µWS match what it can,
|
|
978
|
+
// which is slower and answers the same. Not a tuning knob: it exists so one application can be
|
|
979
|
+
// served both ways and the two sets of answers compared, which tests the optimizer against the
|
|
980
|
+
// rest of the framework without a second framework to compare with. See
|
|
981
|
+
// `npm run fuzz -- --self`. A compiled response needs a native registration to hang on, so this
|
|
982
|
+
// takes "declarative responses" with it.
|
|
983
|
+
"native routes": true,
|
|
964
984
|
// off: with a window set, the size and mtime of a file served by sendFile are remembered for
|
|
965
985
|
// it, which is one syscall less per request and a file that can be served as it was a moment
|
|
966
986
|
// ago. "stat cache ms" is the window in milliseconds, compiled from it by set()
|