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/src/cli.js CHANGED
@@ -20,6 +20,12 @@ limitations under the License.
20
20
  // Rewrites the module specifier and nothing else. An Express 5 app is a Fulmine app already, so
21
21
  // there is no code to translate: what there is instead is a short list of things that behave
22
22
  // differently, printed at the end, because no rewrite can find those for you.
23
+ //
24
+ // npx fulmine profile [entry]
25
+ //
26
+ // Prints what listen() worked out about each route and normally keeps to itself: which ones µWS
27
+ // answers on its own, which ones fell back to the ordinary router and why, and which ones were
28
+ // compiled all the way down to a response written at startup.
23
29
 
24
30
  const fs = require("fs");
25
31
  const path = require("path");
@@ -259,6 +265,285 @@ function walk(node, visit) {
259
265
  }
260
266
  }
261
267
 
268
+ /** Where an application usually is, when the command was given no entry to load. */
269
+ const DEFAULT_ENTRIES = ["server.js", "app.js", "index.js", "src/server.js", "src/app.js", "src/index.js"];
270
+
271
+ /**
272
+ * The file to load, from the argument, or from package.json's main, or from the usual names.
273
+ *
274
+ * @param {string|undefined} given
275
+ * @returns {string|null}
276
+ */
277
+ function findEntry(given) {
278
+ if (given) {
279
+ const resolved = path.resolve(given);
280
+ return fs.existsSync(resolved) ? resolved : null;
281
+ }
282
+ try {
283
+ const pkg = JSON.parse(fs.readFileSync(path.resolve("package.json"), "utf8"));
284
+ if (pkg.main && fs.existsSync(path.resolve(pkg.main))) {
285
+ return path.resolve(pkg.main);
286
+ }
287
+ } catch {
288
+ // no package.json, or one that will not parse: the usual names are still worth trying
289
+ }
290
+ for (const name of DEFAULT_ENTRIES) {
291
+ const resolved = path.resolve(name);
292
+ if (fs.existsSync(resolved)) {
293
+ return resolved;
294
+ }
295
+ }
296
+ return null;
297
+ }
298
+
299
+ /**
300
+ * Every route of an application and of the routers under it, each with the router it belongs to
301
+ * and the path it answers from the outside.
302
+ *
303
+ * @param {any} router
304
+ * @param {string} prefix
305
+ * @param {any[]} [into]
306
+ * @returns {any[]}
307
+ */
308
+ function collectRoutes(router, prefix, into = []) {
309
+ for (const route of router._routes ?? []) {
310
+ const full = prefix + (typeof route.path === "string" ? route.path : String(route.pattern)) || "/";
311
+ into.push({ route, full });
312
+ const mounted = route.callbacks?.[0];
313
+ if (mounted && Array.isArray(mounted._routes)) {
314
+ collectRoutes(mounted, typeof route.path === "string" ? prefix + route.path : prefix, into);
315
+ }
316
+ }
317
+ return into;
318
+ }
319
+
320
+ /**
321
+ * Loads an application without letting it listen, and prints what compiling its routes decided.
322
+ *
323
+ * listen() is where the routes are compiled and also where the port is bound, and only the first
324
+ * of those is wanted here: an application that answered on its port while being profiled would be
325
+ * a surprise, and a second copy of a running service is worse than a surprise. So listen is
326
+ * replaced by the half that matters. The callback it was given is not run, for the same reason.
327
+ *
328
+ * @param {string[]} argv
329
+ * @returns {number} exit code
330
+ */
331
+ function profile(argv) {
332
+ const entry = findEntry(argv.find((arg) => !arg.startsWith("--")));
333
+ if (!entry) {
334
+ console.error(
335
+ "Nothing to profile: name the file that builds the application, or run this from a\n" +
336
+ "directory whose package.json main points at it."
337
+ );
338
+ return 1;
339
+ }
340
+
341
+ // the same module instance the application will load, so patching this prototype patches the
342
+ // application it builds. An app is a callable, so its own prototype is not the one that carries
343
+ // the methods: walk up to whichever link owns listen
344
+ const express = require("./index.js");
345
+ let proto = Object.getPrototypeOf(express());
346
+ while (proto && !Object.prototype.hasOwnProperty.call(proto, "listen")) {
347
+ proto = Object.getPrototypeOf(proto);
348
+ }
349
+ if (!proto) {
350
+ console.error("This build of fulmine has no listen() to stand in for, which should not happen.");
351
+ return 1;
352
+ }
353
+
354
+ const listened = [];
355
+ const realListen = proto.listen;
356
+ proto.listen = function stubbedListen() {
357
+ this._compileOptimizedRoutes();
358
+ listened.push(this);
359
+ return this;
360
+ };
361
+
362
+ try {
363
+ require(entry);
364
+ } catch (e) {
365
+ const error = /** @type {any} */ (e);
366
+ proto.listen = realListen;
367
+ console.error(`${path.relative(process.cwd(), entry)} could not be loaded:\n${error.stack ?? error}`);
368
+ return 1;
369
+ }
370
+ proto.listen = realListen;
371
+
372
+ let apps = listened;
373
+ if (apps.length === 0) {
374
+ // an application that exports itself rather than listening, which is how a testable one is
375
+ // usually written. Compiling it here is the same work listen() would have done
376
+ const exported = require(entry);
377
+ const candidate = exported?.default ?? exported?.app ?? exported;
378
+ if (candidate && Array.isArray(candidate._routes)) {
379
+ candidate._compileOptimizedRoutes();
380
+ apps = [candidate];
381
+ }
382
+ }
383
+
384
+ if (apps.length === 0) {
385
+ console.error(
386
+ `${path.relative(process.cwd(), entry)} built no application: it neither called listen() nor\n` +
387
+ "exported one. Point this at the file that does."
388
+ );
389
+ return 1;
390
+ }
391
+
392
+ for (const app of apps) {
393
+ printProfile(app, apps.length > 1);
394
+ }
395
+ return 0;
396
+ }
397
+
398
+ // what a reason means for whoever wrote the route, when it means anything they can act on. A
399
+ // method µWS does not serve, or a path only a regular expression can match, is not something to
400
+ // go and fix; an ordering that costs the native match is.
401
+ /** @type {[RegExp, (match: RegExpExecArray) => string][]} */
402
+ const ADVICE = [
403
+ [
404
+ /^the parameter route (.+) is written before it$/,
405
+ (match) =>
406
+ `write it above ${match[1]}. Express answers whichever matches first, so the order is` +
407
+ ` already what decides,\n and with the literal first µWS can match it in C++ as well.`
408
+ ],
409
+ [
410
+ /^something before it in the same router overlaps its paths$/,
411
+ () =>
412
+ "something registered earlier answers some of the same paths, so the chain that would" +
413
+ " reach this route\n cannot be worked out ahead of time. Narrowing the earlier path, or moving this one above it, frees it."
414
+ ],
415
+ [
416
+ /^a route after it in the same mounted router could answer the same paths$/,
417
+ () =>
418
+ "a route below it in the same mounted router overlaps it. Inside a mount the later one" +
419
+ " has to be able to win,\n which a precomputed chain cannot express. Narrowing either path frees it."
420
+ ]
421
+ ];
422
+
423
+ /**
424
+ * A summary that says how much of this application the native router carries, and what could be
425
+ * changed to make it carry more.
426
+ *
427
+ * There is no score here on purpose. A percentage of routes is not a percentage of traffic: an
428
+ * application with a thousand cold routes and one hot one that fell back would score well and
429
+ * serve badly. What is printed instead is counted rather than judged, and the advice is only
430
+ * printed for the reasons somebody can actually act on.
431
+ *
432
+ * @param {any[]} routes
433
+ * @param {any[]} native
434
+ * @param {any[]} declarative
435
+ */
436
+ function printSummary(routes, native, declarative) {
437
+ console.log("\nWhat this adds up to\n");
438
+ console.log(` ${native.length} of ${routes.length} route(s) matched by µWS in C++`);
439
+ if (declarative.length > 0) {
440
+ console.log(` ${declarative.length} answered from a response written at startup, running no javascript`);
441
+ }
442
+
443
+ const skipHeaders = native.filter(({ route }) => route._native.skipHeaders).length;
444
+ const skipQuery = native.filter(({ route }) => route._native.skipQuery).length;
445
+ if (skipHeaders || skipQuery) {
446
+ console.log(
447
+ ` ${skipHeaders} copy no request headers, ${skipQuery} read no query: the analysis proved nothing asks for them`
448
+ );
449
+ }
450
+
451
+ if (native.length > 0) {
452
+ const ahead = native.map(({ route }) => route._native.ahead);
453
+ const total = ahead.reduce((sum, n) => sum + n, 0);
454
+ console.log(
455
+ ` layers in front of a compiled handler: ${Math.min(...ahead)} at least, ${Math.max(...ahead)} at most,` +
456
+ ` ${(total / ahead.length).toFixed(1)} on average`
457
+ );
458
+ }
459
+
460
+ // the routes whose reason somebody can do something about
461
+ const worth = [];
462
+ for (const { route, full } of routes) {
463
+ if (route._native || !route._whyGeneric) continue;
464
+ for (const [pattern, say] of ADVICE) {
465
+ const match = pattern.exec(route._whyGeneric);
466
+ if (match) {
467
+ worth.push(` ${route.method} ${full}\n ${say(match)}`);
468
+ break;
469
+ }
470
+ }
471
+ }
472
+ if (worth.length > 0) {
473
+ console.log(`\nWorth changing, if these are routes that carry traffic\n`);
474
+ console.log(worth.join("\n\n"));
475
+ }
476
+ }
477
+
478
+ /**
479
+ * @param {any} app
480
+ * @param {boolean} several whether to say which application this is
481
+ */
482
+ function printProfile(app, several) {
483
+ const entries = collectRoutes(app, "");
484
+ const routes = entries.filter(({ route }) => !route.use);
485
+ const mounts = entries.filter(({ route }) => route.use);
486
+ const native = routes.filter(({ route }) => route._native);
487
+ const declarative = native.filter(({ route }) => route._native.declarative);
488
+
489
+ if (several) {
490
+ console.log(`\n=== an application listening on ${app._listenHost ?? "its own port"} ===`);
491
+ }
492
+ console.log(
493
+ `\n${routes.length} route(s), ${native.length} answered by µWS itself` +
494
+ `${declarative.length ? `, ${declarative.length} of them without running any javascript` : ""}\n`
495
+ );
496
+
497
+ for (const { route, full } of routes) {
498
+ const method = String(route.method).padEnd(7);
499
+ const where = full.padEnd(34);
500
+ if (route._native) {
501
+ const notes = [];
502
+ if (route._native.declarative) notes.push("compiled to a response");
503
+ if (route._native.ahead) notes.push(`${route._native.ahead} in front of it in its chain`);
504
+ if (route._native.guards) notes.push(`${route._native.guards} case guard(s)`);
505
+ if (route._native.skipHeaders) notes.push("copies no request headers");
506
+ if (route._native.skipQuery) notes.push("reads no query");
507
+ console.log(
508
+ ` ${method}${where}µWS ${route._native.path}${notes.length ? ` (${notes.join(", ")})` : ""}`
509
+ );
510
+ } else {
511
+ console.log(` ${method}${where}router: ${route._whyGeneric ?? "it was not eligible"}`);
512
+ }
513
+ }
514
+
515
+ // a mount holding a router is one the compiler could have walked into; anything else is
516
+ // middleware, and listing every helmet and cors as a mount that "was not walked into" says
517
+ // nothing anyone can act on
518
+ const routers = mounts.filter(
519
+ ({ route }) => route.callbacks?.length === 1 && Array.isArray(route.callbacks[0]?._routes)
520
+ );
521
+ const missed = routers.filter(({ route }) => !route._walkedInto);
522
+ if (missed.length > 0) {
523
+ console.log(`\n${missed.length} mounted router(s) the compiler did not walk into:\n`);
524
+ for (const { route, full } of missed) {
525
+ console.log(` ${(full || "/").padEnd(40)}${route._whyGeneric ?? "it was not eligible"}`);
526
+ }
527
+ }
528
+
529
+ const middleware = mounts.length - routers.length;
530
+ if (middleware > 0) {
531
+ console.log(
532
+ `\n${middleware} middleware in front of them. Every request walks the ones whose path it` +
533
+ ` matches,\nand a compiled route walks them from a list worked out at startup rather than by matching.`
534
+ );
535
+ }
536
+
537
+ printSummary(routes, native, declarative);
538
+
539
+ console.log(
540
+ "\nA route answered by µWS is matched in C++ and reaches javascript with its chain already\n" +
541
+ "known. One that fell back is matched here, in order, the way Express does it: correct\n" +
542
+ "either way, and the reason is printed so it can be changed if it is worth changing.\n" +
543
+ "The server was not started and its listen callback was not run."
544
+ );
545
+ }
546
+
262
547
  /**
263
548
  * @param {string[]} argv
264
549
  */
@@ -268,13 +553,18 @@ function main(argv) {
268
553
  printDifferences();
269
554
  return 0;
270
555
  }
556
+ if (command === "profile") {
557
+ return profile(argv.slice(1));
558
+ }
271
559
  if (command !== "migrate") {
272
560
  console.log(`Usage:
273
- npx ${TO} migrate [dir] rewrite require("${FROM}") and import from "${FROM}" to "${TO}"
274
- npx ${TO} differences print what behaves differently, without changing anything
561
+ npx ${TO} migrate [dir] rewrite require("${FROM}") and import from "${FROM}" to "${TO}"
562
+ npx ${TO} profile [entry] load an application without listening and print what compiling
563
+ its routes decided, route by route
564
+ npx ${TO} differences print what behaves differently, without changing anything
275
565
 
276
566
  Options:
277
- --dry-run say what would change and change nothing`);
567
+ --dry-run migrate: say what would change and change nothing`);
278
568
  return command ? 1 : 0;
279
569
  }
280
570
 
@@ -363,7 +653,14 @@ function printDifferences() {
363
653
  }
364
654
 
365
655
  if (require.main === module) {
366
- process.exitCode = main(process.argv.slice(2));
656
+ const code = main(process.argv.slice(2));
657
+ if (process.argv[2] === "profile") {
658
+ // profile loaded somebody's application, and loading it may have opened a database handle,
659
+ // a timer or a µWS app of its own. None of that is ours to unwind, and there is nothing
660
+ // left to print. Only here, so the command itself stays a function a test can call
661
+ process.exit(code);
662
+ }
663
+ process.exitCode = code;
367
664
  }
368
665
 
369
- module.exports = { main, findSpecifiers, collectFiles, DIFFERENCES };
666
+ module.exports = { main, findSpecifiers, collectFiles, findEntry, collectRoutes, profile, DIFFERENCES };
@@ -1,3 +1,22 @@
1
+ /*
2
+ Copyright 2024 dimden.dev
3
+ Copyright 2026 Nigro Simone
4
+
5
+ This file is derived from Ultimate Express and has been modified.
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
18
+ */
19
+
1
20
  const acorn = require("acorn");
2
21
  const { stringify, withDefaultCharset, withUtf8Charset } = require("./utils.js");
3
22
  // H3App, DeclarativeResponse and _cfg all exist at runtime but are missing from the
package/src/index.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
@@ -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
@@ -24,7 +26,7 @@ const qs = require("qs");
24
26
  const parseQuery = require("./parse-query.js");
25
27
  const { kGetSafe } = require("./usage.js");
26
28
  const { AsyncResource } = require("async_hooks");
27
- const { fastQueryParse, NullObject, asStatError, httpError, memoizeByString } = require("./utils.js");
29
+ const { fastQueryParse, NullObject, asStatError, httpError, memoizeByString, containsDotFile } = require("./utils.js");
28
30
 
29
31
  // largest content-length we will allocate a body buffer for up front. above this the body is
30
32
  // collected chunk by chunk instead, so a declared-but-unsent body cannot pin more memory than a
@@ -309,9 +311,13 @@ function serveStatic(root, options) {
309
311
 
310
312
  const iq = req.url.indexOf("?");
311
313
  let url;
314
+ // the path as it was written, before decoding: whether it names a directory is decided on
315
+ // this and not on what the escapes turn into, which is how send decides it. "/a/%2F" asks
316
+ // for a file called "/" inside "a", and not for the index of a directory
317
+ const rawPath = iq !== -1 ? req.url.substring(0, iq) : req.url;
312
318
 
313
319
  try {
314
- url = decodeURIComponent(iq !== -1 ? req.url.substring(0, iq) : req.url);
320
+ url = decodeURIComponent(rawPath);
315
321
  } catch (e) {
316
322
  // 400 and not 404: send answers a path it cannot decode with a Bad Request, since
317
323
  // nothing was asked for that could be missing
@@ -320,8 +326,27 @@ function serveStatic(root, options) {
320
326
  return next(httpError(400));
321
327
  } else return next();
322
328
  }
329
+ // A decoded NUL is a bad request and not a missing file, which is how send reads it too.
330
+ // Without this the byte reaches fs, and what comes back is node's own complaint with the
331
+ // absolute path of the root inside it, so a request could ask the server where it lives.
332
+ if (url.indexOf("\0") !== -1) {
333
+ if (!options.fallthrough) {
334
+ res.status(400);
335
+ return next(httpError(400));
336
+ } else return next();
337
+ }
323
338
  let _path = url;
324
339
  const fullpath = path.resolve(path.join(root, url));
340
+ // What serve-static hands send is this path, except that a bare "/" under a mount the
341
+ // request did not write with one becomes "": without that rule a mount whose root is a file
342
+ // would ask the disk for a directory and could never answer at all.
343
+ //
344
+ // Send then stats `normalize(join(root, path))`, and both of those keep a trailing
345
+ // separator where `resolve` takes it off. The separator is not decoration: the disk refuses
346
+ // a file that is asked for as a directory, and the name inside the error carries it, which
347
+ // is what an error handler prints when fallthrough is off.
348
+ const mountRelative = rawPath === "/" && !req.endsWithSlash ? "" : url;
349
+ const statTarget = mountRelative.endsWith("/") && !fullpath.endsWith(path.sep) ? fullpath + path.sep : fullpath;
325
350
  if (root && !fullpath.startsWith(path.resolve(root))) {
326
351
  if (!options.fallthrough) {
327
352
  res.status(403);
@@ -329,19 +354,55 @@ function serveStatic(root, options) {
329
354
  } else return next();
330
355
  }
331
356
 
357
+ // Before the stat, because send judges the path before it looks at the disk: a hidden
358
+ // segment anywhere in a path that does not exist answers what the dotfiles rule says and
359
+ // not the ENOENT the disk would have given. sendFile applies the same rule below, and
360
+ // reaches it only for paths that do exist.
361
+ // normalized first, as send normalizes before it judges: a ".." segment is not a hidden
362
+ // file, and resolving it away is what tells the two apart
363
+ if (containsDotFile(path.normalize(url).split(/[\\/]/))) {
364
+ const refusal = options.dotfiles === "deny" ? 403 : options.dotfiles === "allow" ? 0 : 404;
365
+ if (refusal !== 0 && !(options.dotfiles === "ignore_files" && !path.basename(url).startsWith("."))) {
366
+ if (!options.fallthrough) {
367
+ res.status(refusal);
368
+ return next(httpError(refusal));
369
+ }
370
+ return next();
371
+ }
372
+ }
373
+
332
374
  let stat;
333
375
  try {
334
- stat = fs.statSync(fullpath);
376
+ stat = fs.statSync(statTarget);
335
377
  } catch (err) {
378
+ // the one to report when nothing is found: send hands each failed attempt to the next
379
+ // one and reports whichever came last, so an extensions option that also missed names
380
+ // the file it looked for and not the bare path
381
+ let statError = err;
382
+ // a path written with a trailing slash asks for a directory, and send answers that by
383
+ // looking for the index inside it. With nothing there, the file it names is that
384
+ // index and not the directory that does not exist either
385
+ if (rawPath.endsWith("/") && options.index) {
386
+ try {
387
+ fs.statSync(path.join(fullpath, options.index));
388
+ } catch (indexError) {
389
+ statError = indexError;
390
+ }
391
+ }
336
392
  const ext = path.extname(fullpath);
337
393
  let i = 0;
338
- if (ext === "" && options.extensions) {
394
+ // a path that resolves to a directory gets no extension hung off it. The test is on the
395
+ // decoded url and not on fullpath, because resolve() has already taken the trailing
396
+ // separator off that one, and not on the raw path either: send tries the extension on
397
+ // what the escapes decoded to, while it looks for an index on the path as written
398
+ if (ext === "" && !url.endsWith("/") && options.extensions) {
339
399
  while (i < options.extensions.length) {
340
400
  try {
341
401
  stat = fs.statSync(fullpath + "." + options.extensions[i]);
342
402
  _path = url + "." + options.extensions[i];
343
403
  break;
344
- } catch (err) {
404
+ } catch (extensionError) {
405
+ statError = extensionError;
345
406
  i++;
346
407
  }
347
408
  }
@@ -353,7 +414,7 @@ function serveStatic(root, options) {
353
414
  // error handler with its errno, code, syscall and path still on it, and an
354
415
  // error handler doing res.send(err) sends those as JSON. Passing the string
355
416
  // sent an HTML page instead.
356
- return next(asStatError(err));
417
+ return next(asStatError(statError));
357
418
  } else return next();
358
419
  }
359
420
  }
package/src/node-shim.js CHANGED
@@ -100,8 +100,10 @@ class NodeHttpRequest {
100
100
  const url = req.url || "/";
101
101
  const question = url.indexOf("?");
102
102
  this._path = question === -1 ? url : url.slice(0, question);
103
- // uWS answers the query without its "?", and an empty string when there is none
104
- this._query = question === -1 ? "" : url.slice(question + 1);
103
+ // uWS answers the query without its "?", undefined when the url carries none and "" when
104
+ // it carries an empty one, and req.url keeps that difference
105
+ /** @type {string|undefined} */
106
+ this._query = question === -1 ? undefined : url.slice(question + 1);
105
107
  }
106
108
 
107
109
  /** The path, without the query, which is what uWS answers here. */
@@ -109,7 +111,7 @@ class NodeHttpRequest {
109
111
  return this._path;
110
112
  }
111
113
 
112
- /** The query string without its "?", empty when there is none, as uWS reports it. */
114
+ /** The query string without its "?", undefined when the url has none, as uWS reports it. */
113
115
  getQuery() {
114
116
  return this._query;
115
117
  }
package/src/options.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
  // The option bags the file-serving and body-parsing paths take, written once and referenced from
2
18
  // the JSDoc of the functions that read them. They live in a declaration file rather than as
3
19
  // @typedef blocks in the sources because those sources export classes, and a typedef hanging off
@@ -1,8 +1,25 @@
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
  /*
4
- The parser from fast-querystring 1.1.x (MIT, Copyright (c) Yagiz Nizipli,
5
- https://github.com/anonrig/fast-querystring), vendored for one change: the result is a bare
20
+ The parser from fast-querystring 1.1.x (https://github.com/anonrig/fast-querystring), Copyright
21
+ (c) 2022 Yagiz Nizipli, MIT, whose permission notice is reproduced in full in NOTICE at the root
22
+ of this package. Vendored for one change: the result is a bare
6
23
  Object.create(null) instead of the library's Empty-constructor trick. The trick is faster to
7
24
  construct but node inspects it as "Empty <[Object: null prototype] {}>", where Express shows
8
25
  "[Object: null prototype]", and matching that used to cost an Object.assign copy of every parse