fulmine.js 5.19.9 → 5.21.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fulmine.js",
3
- "version": "5.19.9",
4
- "description": "Drop-in Express 5 replacement on uWebSockets.js. Your existing middleware keeps working.",
3
+ "version": "5.21.0",
4
+ "description": "Drop-in Express 5 replacement on uWebSockets.js, up to 20x faster. Same API, your middleware and framework keep working.",
5
5
  "main": "src/index.js",
6
6
  "exports": {
7
7
  ".": {
@@ -64,16 +64,28 @@
64
64
  "keywords": [
65
65
  "express",
66
66
  "express5",
67
+ "express-alternative",
68
+ "drop-in replacement",
67
69
  "fast",
70
+ "fastest",
71
+ "performance",
72
+ "web framework",
68
73
  "http",
69
74
  "http server",
70
75
  "https",
71
76
  "https server",
77
+ "router",
78
+ "middleware",
79
+ "rest api",
72
80
  "uwebsockets",
73
81
  "uws",
74
82
  "websocket",
75
83
  "websockets",
76
- "performance"
84
+ "nestjs",
85
+ "angular ssr",
86
+ "nextjs",
87
+ "cluster",
88
+ "typescript"
77
89
  ],
78
90
  "types": "src/types.d.ts",
79
91
  "author": "Nigro Simone",
@@ -81,7 +93,7 @@
81
93
  "bugs": {
82
94
  "url": "https://github.com/nigrosimone/fulmine.js/issues"
83
95
  },
84
- "homepage": "https://github.com/nigrosimone/fulmine.js#readme",
96
+ "homepage": "https://fulmine.sndesign.it",
85
97
  "dependencies": {
86
98
  "@types/express": "^5.0.6",
87
99
  "accepts": "^2.0.0",
package/src/adopt.js CHANGED
@@ -14,17 +14,20 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- // The two commands that edit a config file rather than source: `npx fulmine.js override` and
18
- // `npx fulmine.js angular`.
17
+ // The commands that edit a config file rather than source: `npx fulmine.js override`,
18
+ // `npx fulmine.js angular` and `npx fulmine.js pnpm`.
19
19
  //
20
- // `migrate` rewrites `require("express")` in your own files. The two cases it cannot reach are
21
- // both a line in a JSON file:
20
+ // `migrate` rewrites `require("express")` in your own files. The cases it cannot reach are each
21
+ // a line in a config file:
22
22
  //
23
23
  // override A framework built on Express requires it in its own code, so there is no specifier
24
24
  // to rewrite. Every package manager can answer `express` with this package instead,
25
25
  // and each one spells it differently.
26
26
  // angular An Angular server bundle is built with esbuild, which inlines every dependency and
27
27
  // cannot load uWS's native binary. Two names in `externalDependencies` fix it.
28
+ // pnpm pnpm 10.26 and later refuse a git dependency of a dependency, and µWebSockets.js
29
+ // is one. A direct dependency is allowed, so the project takes it on itself and an
30
+ // override drops the copy this package asks for.
28
31
 
29
32
  "use strict";
30
33
 
@@ -37,6 +40,12 @@ const REPLACES = "express";
37
40
  /** The major this package tracks, which is the range an override should ask for. */
38
41
  const MAJOR = require("../package.json").version.split(".")[0];
39
42
 
43
+ const UWS = "uWebSockets.js";
44
+ /** The git spec this package pins, which is what a project taking uWS on itself has to pin too. */
45
+ const UWS_SPEC = /** @type {string} */ (require("../package.json").dependencies[UWS]);
46
+ /** The pnpm override that drops this package's own copy, so the project's direct one is the only one. */
47
+ const UWS_OVERRIDE = `${SELF}>${UWS}`;
48
+
40
49
  /** Where each manager keeps its substitutions, and what to call it when telling someone. */
41
50
  const MANAGERS = {
42
51
  npm: { keys: ["overrides"], reinstall: "npm install" },
@@ -315,4 +324,106 @@ function angular(argv) {
315
324
  return 0;
316
325
  }
317
326
 
318
- module.exports = { override, angular, detectManager, serverBuilds, indentOf };
327
+ /**
328
+ * The pnpm-workspace.yaml with the override in it. Written by hand rather than through a YAML
329
+ * library: the file is small, the block is two lines, and what is already there is kept as is.
330
+ *
331
+ * @param {string} source the file as it is, or "" when there is none
332
+ * @returns {string|undefined} the new file, or nothing when the override is already there
333
+ */
334
+ function withPnpmOverride(source) {
335
+ const line = ` "${UWS_OVERRIDE}": "-"`;
336
+ if (
337
+ new RegExp(
338
+ `^\\s*["']?${SELF.replace(".", "\\.")}>${UWS.replace(".", "\\.")}["']?:\\s*["']?-["']?\\s*$`,
339
+ "m"
340
+ ).test(source)
341
+ ) {
342
+ return undefined;
343
+ }
344
+ const lines = source.length ? source.replace(/\r\n/g, "\n").replace(/\n*$/, "").split("\n") : [];
345
+ const at = lines.findIndex((one) => /^overrides:\s*$/.test(one));
346
+ if (at === -1) {
347
+ return [...lines, ...(lines.length ? [""] : []), "overrides:", line, ""].join("\n");
348
+ }
349
+ lines.splice(at + 1, 0, line);
350
+ return lines.join("\n") + "\n";
351
+ }
352
+
353
+ /**
354
+ * npx fulmine.js pnpm [dir] [--dry-run]
355
+ *
356
+ * Makes a pnpm project install this package, which pnpm 10.26 and later otherwise refuse: the
357
+ * project takes µWebSockets.js as a direct dependency, at the spec this package pins, and an
358
+ * override in pnpm-workspace.yaml drops the copy this package asks for. pnpm 11 reads its
359
+ * settings from that file only, so package.json's `pnpm` field is not where this goes.
360
+ *
361
+ * @param {string[]} argv everything after the command name
362
+ * @returns {number} exit code
363
+ */
364
+ function pnpm(argv) {
365
+ const dryRun = argv.includes("--dry-run");
366
+ const dir = path.resolve(argv.find((arg) => !arg.startsWith("--")) ?? ".");
367
+ const file = path.join(dir, "package.json");
368
+ const workspaceFile = path.join(dir, "pnpm-workspace.yaml");
369
+
370
+ const read = readJson(file);
371
+ if ("error" in read) {
372
+ console.error(read.code === "ENOENT" ? `no package.json in ${dir}` : read.error);
373
+ return 1;
374
+ }
375
+ const { data: pkg, source } = read;
376
+
377
+ let changed = 0;
378
+ const existing = readPath(pkg, ["dependencies", UWS]);
379
+ if (existing === UWS_SPEC) {
380
+ console.log(`dependencies.${UWS} already says ${UWS_SPEC}`);
381
+ } else {
382
+ if (existing !== undefined) {
383
+ console.log(`dependencies.${UWS} says ${JSON.stringify(existing)}, and this package pins ${UWS_SPEC}:`);
384
+ console.log("the pin is what the code here was tested against, so it is the one written.");
385
+ }
386
+ writePath(pkg, ["dependencies", UWS], UWS_SPEC);
387
+ changed++;
388
+ console.log(`${dryRun ? "would add" : "added"} to package.json: "dependencies": { "${UWS}": "${UWS_SPEC}" }`);
389
+ if (!dryRun) fs.writeFileSync(file, JSON.stringify(pkg, null, indentOf(source)) + "\n");
390
+ }
391
+
392
+ let workspace = "";
393
+ try {
394
+ workspace = fs.readFileSync(workspaceFile, "utf8");
395
+ } catch {
396
+ // no workspace file yet, it is written below
397
+ }
398
+ const rewritten = withPnpmOverride(workspace);
399
+ if (rewritten === undefined) {
400
+ console.log(`pnpm-workspace.yaml already overrides ${UWS_OVERRIDE}`);
401
+ } else {
402
+ changed++;
403
+ console.log(`${dryRun ? "would add" : "added"} to pnpm-workspace.yaml: overrides: { "${UWS_OVERRIDE}": "-" }`);
404
+ if (!dryRun) fs.writeFileSync(workspaceFile, rewritten);
405
+ }
406
+
407
+ if (!changed) {
408
+ console.log("\nNothing to change.");
409
+ return 0;
410
+ }
411
+ console.log(`\nThen \`pnpm install\`. µWebSockets.js is now the project's own dependency, so it is fetched as a`);
412
+ console.log("direct one, which pnpm allows, and the copy this package asks for is dropped from the graph.");
413
+ console.log(
414
+ `When this package moves its pin, run this again: \`npx fulmine.js verify\` says when the two differ.\n`
415
+ );
416
+ return 0;
417
+ }
418
+
419
+ module.exports = {
420
+ override,
421
+ angular,
422
+ pnpm,
423
+ detectManager,
424
+ serverBuilds,
425
+ indentOf,
426
+ withPnpmOverride,
427
+ UWS_SPEC,
428
+ UWS_OVERRIDE
429
+ };
@@ -62,6 +62,9 @@ class FSWorker {
62
62
  this.worker = new Worker(path.join(__dirname, "worker.js"));
63
63
 
64
64
  this.worker.on("message", (message) => {
65
+ // node speaks on this channel too: under --watch a worker reports the files it loaded
66
+ // as {"watch:import": [...]}, which carries no key of ours
67
+ if (workerTasks[message.key] === undefined) return;
65
68
  this.busy = false;
66
69
  if (message.err) {
67
70
  workerTasks[message.key].reject(new Error(message.err));
package/src/cli.js CHANGED
@@ -37,6 +37,15 @@ limitations under the License.
37
37
  // The two things a project needs that are a line in a JSON file rather than a specifier in a
38
38
  // source file: the package manager substitution, for a framework that requires express in its own
39
39
  // code, and angular.json's externalDependencies. See src/adopt.js.
40
+ //
41
+ // npx fulmine create <dir>
42
+ //
43
+ // A new project for whoever has nothing to migrate: a server, a package.json and the Dockerfile
44
+ // that works. See src/create.js.
45
+ //
46
+ // npx fulmine pnpm [dir]
47
+ //
48
+ // The two lines a pnpm project needs before it will install this at all. See src/adopt.js.
40
49
 
41
50
  const fs = require("fs");
42
51
  const path = require("path");
@@ -44,7 +53,8 @@ const acorn = require("acorn");
44
53
  // the same walk express.testing asserts on, so the command and the assertions cannot drift
45
54
  const { collectRoutes } = require("./testing.js");
46
55
  const { verify } = require("./verify.js");
47
- const { override, angular } = require("./adopt.js");
56
+ const { override, angular, pnpm } = require("./adopt.js");
57
+ const { create } = require("./create.js");
48
58
 
49
59
  /** @typedef {import("./application.js").Application} Application */
50
60
  /** @typedef {import("./router-utils.js").RouteEntry} RouteEntry */
@@ -97,10 +107,11 @@ const DIFFERENCES = [
97
107
  [
98
108
  "a compiled route keeps its connection header",
99
109
  "A handler simple enough to be read at registration time is answered natively, and a client\n" +
100
- "that sent Connection: close is still told keep-alive, though the socket does close. A body\n" +
101
- "with a piece of the query in it is framed chunked, since its length is not known until the\n" +
102
- "request arrives. A response that would carry a validator is never compiled, so conditional\n" +
103
- 'requests behave as on Express. app.set("declarative responses", false) turns it off.'
110
+ "that sent Connection: close is still told keep-alive, though the socket does close. A\n" +
111
+ "response that would carry a validator is never compiled, so conditional requests behave as\n" +
112
+ 'on Express. app.set("declarative responses", false) turns it off. A body with a piece of\n' +
113
+ 'the query or a route parameter in it is compiled only under app.set("declarative request\n' +
114
+ 'values", true), which takes the value as uWS reads it: undecoded, the first one, or none.'
104
115
  ],
105
116
  [
106
117
  "headers are capped at 4096 bytes by default",
@@ -910,13 +921,23 @@ function main(argv) {
910
921
  if (command === "angular") {
911
922
  return angular(argv.slice(1));
912
923
  }
924
+ if (command === "create") {
925
+ return create(argv.slice(1));
926
+ }
927
+ if (command === "pnpm") {
928
+ return pnpm(argv.slice(1));
929
+ }
913
930
  if (command !== "migrate") {
914
931
  console.log(`Usage:
932
+ npx ${TO} create <dir> start a new project: a server, a package.json and a Dockerfile that
933
+ works, --ts for TypeScript, --pnpm for the two lines pnpm needs
915
934
  npx ${TO} migrate [dir] rewrite require("${FROM}") and import from "${FROM}" to "${TO}"
916
935
  npx ${TO} override [dir] answer ${FROM} with this package for the whole dependency tree, for
917
936
  when a framework requires ${FROM} in its own code and not in yours
918
937
  npx ${TO} angular [dir] declare this package external in angular.json's server build, which
919
938
  esbuild otherwise tries to inline a native binary into
939
+ npx ${TO} pnpm [dir] make a pnpm project install this: pnpm 10.26 and later refuse a git
940
+ dependency of a dependency, and µWebSockets.js is one
920
941
  npx ${TO} profile [entry] load an application without listening and print what compiling
921
942
  its routes decided, route by route
922
943
  npx ${TO} explain <route> what happens when a request for that route arrives
@@ -924,7 +945,7 @@ function main(argv) {
924
945
  npx ${TO} differences print what behaves differently, without changing anything
925
946
 
926
947
  Options:
927
- --dry-run migrate, override, angular: say what would change and change nothing`);
948
+ --dry-run migrate, override, angular, pnpm: say what would change and change nothing`);
928
949
  return command ? 1 : 0;
929
950
  }
930
951
 
package/src/create.js ADDED
@@ -0,0 +1,248 @@
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
+
17
+ // npx fulmine.js create <dir> [--ts] [--pnpm]
18
+ //
19
+ // A new project, for whoever has no Express application to migrate. `migrate` and `override` start
20
+ // from somebody's code; this starts from nothing and writes the few files a first run needs: a
21
+ // server, a package.json, and the Dockerfile that works, since the base image is the one thing a
22
+ // Dockerfile written for Express gets wrong here. Nothing is installed, that is the user's call.
23
+
24
+ "use strict";
25
+
26
+ const fs = require("fs");
27
+ const path = require("path");
28
+ const { withPnpmOverride, UWS_SPEC } = require("./adopt.js");
29
+
30
+ const SELF = "fulmine.js";
31
+
32
+ /** The major this package tracks, which is the range a new project should ask for. */
33
+ const MAJOR = require("../package.json").version.split(".")[0];
34
+
35
+ /**
36
+ * The node major the Dockerfile names: the one running this when it has a µWS binary, which is the
37
+ * even lines, and the newest supported one otherwise.
38
+ *
39
+ * @returns {number}
40
+ */
41
+ function nodeMajor() {
42
+ const running = Number(process.versions.node.split(".")[0]);
43
+ return running >= 22 && running % 2 === 0 ? running : 26;
44
+ }
45
+
46
+ /**
47
+ * The server, in the flavour asked for. The same three routes either way: a page, a JSON answer
48
+ * with a parameter, and a body read by the built-in parser.
49
+ *
50
+ * @param {boolean} ts
51
+ * @returns {string}
52
+ */
53
+ function serverSource(ts) {
54
+ const types = ts ? 'import type { Request, Response } from "fulmine.js";\n' : "";
55
+ const params = ts ? "(req: Request, res: Response)" : "(req, res)";
56
+ return `import express from "${SELF}";
57
+ ${types}
58
+ const app = express();
59
+ const port = Number(process.env.PORT ?? 3000);
60
+
61
+ app.use(express.json());
62
+ app.use(express.static("public"));
63
+
64
+ app.get("/api/hello", ${params} => {
65
+ res.json({ hello: "world" });
66
+ });
67
+
68
+ app.get("/api/items/:id", ${params} => {
69
+ res.json({ id: req.params.id });
70
+ });
71
+
72
+ app.post("/api/items", ${params} => {
73
+ res.status(201).json(req.body);
74
+ });
75
+
76
+ app.listen(port, () => {
77
+ console.log(\`listening on http://localhost:\${port}\`);
78
+ });
79
+ `;
80
+ }
81
+
82
+ /**
83
+ * @param {string} name the package name, from the directory
84
+ * @param {boolean} ts
85
+ * @param {boolean} pnpm the project also owns µWebSockets.js, see `npx fulmine.js pnpm`
86
+ * @returns {string}
87
+ */
88
+ function packageSource(name, ts, pnpm) {
89
+ const pkg = {
90
+ name,
91
+ version: "0.1.0",
92
+ private: true,
93
+ type: "module",
94
+ scripts: ts
95
+ ? {
96
+ build: "tsc",
97
+ start: "node dist/server.js",
98
+ dev: "node --watch --experimental-strip-types src/server.ts"
99
+ }
100
+ : { start: "node server.js", dev: "node --watch server.js" },
101
+ dependencies: { [SELF]: `^${MAJOR}`, ...(pnpm ? { "uWebSockets.js": UWS_SPEC } : {}) },
102
+ ...(ts ? { devDependencies: { "@types/node": `^${nodeMajor()}`, typescript: "^5" } } : {}),
103
+ engines: { node: ">=22" }
104
+ };
105
+ return JSON.stringify(pkg, null, 4) + "\n";
106
+ }
107
+
108
+ const TSCONFIG = `{
109
+ "compilerOptions": {
110
+ "target": "es2022",
111
+ "module": "nodenext",
112
+ "outDir": "dist",
113
+ "rootDir": "src",
114
+ "strict": true,
115
+ "verbatimModuleSyntax": true,
116
+ "skipLibCheck": true,
117
+ "types": ["node"]
118
+ },
119
+ "include": ["src"]
120
+ }
121
+ `;
122
+
123
+ /**
124
+ * Install with the full image, which has git for the µWS fetch, run with the slim one. trixie or
125
+ * newer on both, since bookworm's glibc is too old for the binary and Alpine has no build at all.
126
+ *
127
+ * @param {boolean} ts
128
+ * @param {boolean} pnpm install with pnpm, whose lockfile and workspace file come along
129
+ * @returns {string}
130
+ */
131
+ function dockerfileSource(ts, pnpm) {
132
+ const major = nodeMajor();
133
+ const manifests = pnpm ? "package.json pnpm-lock.yaml pnpm-workspace.yaml" : "package*.json";
134
+ const install = pnpm ? "npm install -g pnpm@11 && pnpm install --frozen-lockfile" : "npm ci";
135
+ const build = ts
136
+ ? `COPY ${manifests} tsconfig.json ./
137
+ RUN ${install}
138
+ COPY src ./src
139
+ RUN ${pnpm ? "pnpm run build && pnpm prune --prod" : "npm run build && npm prune --omit=dev"}`
140
+ : `COPY ${manifests} ./
141
+ RUN ${install}${pnpm ? " --prod" : " --omit=dev"}`;
142
+ const run = ts
143
+ ? `COPY --from=build /app/node_modules ./node_modules
144
+ COPY --from=build /app/dist ./dist
145
+ COPY package.json ./
146
+ COPY public ./public
147
+ EXPOSE 3000
148
+ CMD ["node", "dist/server.js"]`
149
+ : `COPY --from=build /app/node_modules ./node_modules
150
+ COPY . .
151
+ EXPOSE 3000
152
+ CMD ["node", "server.js"]`;
153
+ return `FROM node:${major}-trixie AS build
154
+ WORKDIR /app
155
+ ${build}
156
+
157
+ FROM node:${major}-trixie-slim
158
+ WORKDIR /app
159
+ ENV NODE_ENV=production
160
+ ${run}
161
+ `;
162
+ }
163
+
164
+ const INDEX_HTML = `<!doctype html>
165
+ <meta charset="utf-8">
166
+ <title>fulmine.js</title>
167
+ <p>Served by <code>express.static()</code>. The API answers on <a href="/api/hello">/api/hello</a>.</p>
168
+ `;
169
+
170
+ /**
171
+ * The files a new project is made of, by path.
172
+ *
173
+ * @param {string} name
174
+ * @param {boolean} ts
175
+ * @param {boolean} pnpm
176
+ * @returns {Record<string, string>}
177
+ */
178
+ function projectFiles(name, ts, pnpm) {
179
+ return {
180
+ "package.json": packageSource(name, ts, pnpm),
181
+ [ts ? "src/server.ts" : "server.js"]: serverSource(ts),
182
+ ...(ts ? { "tsconfig.json": TSCONFIG } : {}),
183
+ // pnpm refuses a git dependency of a dependency, so the project owns µWebSockets.js itself
184
+ ...(pnpm ? { "pnpm-workspace.yaml": /** @type {string} */ (withPnpmOverride("")) } : {}),
185
+ "public/index.html": INDEX_HTML,
186
+ Dockerfile: dockerfileSource(ts, pnpm),
187
+ ".dockerignore": "node_modules\n.git\ndist\n",
188
+ ".gitignore": "node_modules\ndist\n"
189
+ };
190
+ }
191
+
192
+ /**
193
+ * `npx fulmine.js create <dir> [--ts] [--pnpm]`
194
+ *
195
+ * pnpm is also read from how this was started: `pnpm dlx fulmine.js create` says so in the user
196
+ * agent npm and pnpm both set, and a project started that way would otherwise fail its first install.
197
+ *
198
+ * @param {string[]} argv everything after the command
199
+ * @returns {number} exit code
200
+ */
201
+ function create(argv) {
202
+ const ts = argv.includes("--ts");
203
+ const pnpm = argv.includes("--pnpm") || /^pnpm\//.test(process.env.npm_config_user_agent ?? "");
204
+ const given = argv.find((arg) => !arg.startsWith("--"));
205
+ if (!given) {
206
+ console.error(`Usage: npx ${SELF} create <dir> [--ts] [--pnpm]`);
207
+ return 1;
208
+ }
209
+ const dir = path.resolve(given);
210
+ // a name npm accepts: what the directory is called, lowercased, anything else a dash
211
+ const name =
212
+ path
213
+ .basename(dir)
214
+ .toLowerCase()
215
+ .replace(/[^a-z0-9._-]+/g, "-") || "app";
216
+
217
+ if (fs.existsSync(dir) && fs.readdirSync(dir).length) {
218
+ console.error(`${dir} is not empty. Nothing was written: this only starts a project, it does not join one.`);
219
+ return 1;
220
+ }
221
+
222
+ const files = projectFiles(name, ts, pnpm);
223
+ for (const [file, content] of Object.entries(files)) {
224
+ const full = path.join(dir, file);
225
+ fs.mkdirSync(path.dirname(full), { recursive: true });
226
+ fs.writeFileSync(full, content);
227
+ console.log(`wrote ${path.join(given, file)}`);
228
+ }
229
+
230
+ const pm = pnpm ? "pnpm" : "npm";
231
+ console.log(`\nNext:\n`);
232
+ console.log(` cd ${given}`);
233
+ console.log(` ${pm} install`);
234
+ console.log(` ${pm} run dev${ts ? ` # or ${pm} run build && ${pm} start` : ""}\n`);
235
+ if (pnpm) {
236
+ console.log("pnpm refuses a git dependency of a dependency, and µWebSockets.js is one, so this project owns");
237
+ console.log(
238
+ "it: the pin in package.json and the override in pnpm-workspace.yaml. See docs/deployment.md#pnpm."
239
+ );
240
+ }
241
+ console.log(
242
+ `The Dockerfile uses node:${nodeMajor()}-trixie, since Alpine and bookworm cannot load µWebSockets.js.`
243
+ );
244
+ console.log(`\`npx ${SELF} verify\` says whether this machine can, before you install anything.\n`);
245
+ return 0;
246
+ }
247
+
248
+ module.exports = { create, projectFiles };
@@ -818,6 +818,12 @@ module.exports = function compileDeclarative(cb, app) {
818
818
  }
819
819
  const { sendUsed, bodyFromSend } = read;
820
820
 
821
+ // a part copied out of the request is written by uWS with its own reading of it, so the
822
+ // route is compiled only where the application asked for that
823
+ if (!app.get("declarative request values") && body.some((part) => part.type !== "text")) {
824
+ return false;
825
+ }
826
+
821
827
  // a handler that never sends is not a response: Express leaves the request waiting, so this
822
828
  // has to fall back instead of answering a bare 200
823
829
  if (!sendUsed && !sendStatusUsed) {
package/src/index.js CHANGED
@@ -21,6 +21,28 @@ limitations under the License.
21
21
  // package ships, so the module is read through a loose alias
22
22
  const uWS = require("uWebSockets.js");
23
23
  const uWSAny = /** @type {any} */ (uWS);
24
+
25
+ // A project on pnpm owns the uWebSockets.js dependency itself, see `npx fulmine.js pnpm`, so the
26
+ // one installed can drift from the one this package pins and was tested against. Said once, at
27
+ // require time, where it reaches every deployment rather than only the ones that run verify.
28
+ {
29
+ const pinned = /#v?([\d.]+)$/.exec(require("../package.json").dependencies["uWebSockets.js"])?.[1];
30
+ /** @type {string|undefined} */
31
+ let installed;
32
+ try {
33
+ // its exports map does not expose package.json, so it is read beside the entry point
34
+ const beside = require("path").join(require.resolve("uWebSockets.js"), "..", "package.json");
35
+ installed = JSON.parse(require("fs").readFileSync(beside, "utf8")).version;
36
+ } catch {
37
+ // nothing to compare against, which is not worth a warning of its own
38
+ }
39
+ if (pinned && installed && installed !== pinned) {
40
+ console.warn(
41
+ `fulmine.js: uWebSockets.js ${installed} is installed, this version was tested with ${pinned}.\n` +
42
+ " On pnpm the pin is the project's own: `npx fulmine.js pnpm` writes the tested one."
43
+ );
44
+ }
45
+ }
24
46
  const Application = require("./application.js");
25
47
  const Router = require("./router.js");
26
48
  const Route = require("./route.js");
package/src/optimizer.js CHANGED
@@ -558,8 +558,9 @@ function registerUwsRoute(router, route, optimizedPath) {
558
558
  route.paramCallbacks.size === 0 && // a param callback has to run, and this answers without running anything
559
559
  // a captured value is decoded when the route runs, and one that cannot be decoded is a
560
560
  // 400 in express and on the ordinary path here. Nothing runs to raise it on a
561
- // declarative response, so GET /a-b%5Ec@d%e came back 200 from app.get("/:p12")
562
- route.optimizedParams === undefined &&
561
+ // declarative response, so GET /a-b%5Ec@d%e came back 200 from app.get("/:p12").
562
+ // "declarative request values" is the application accepting that
563
+ (route.optimizedParams === undefined || router.get("declarative request values")) &&
563
564
  // a declarative response is answered by µWS itself, so no javascript runs and the case
564
565
  // guard could not: a route that needs one has to stay an ordinary handler
565
566
  caseGuards === null &&
package/src/testing.js CHANGED
@@ -187,8 +187,11 @@ function whyNotCompiled(app, entry) {
187
187
  if (!app.get("declarative responses")) {
188
188
  return "answered by µWS, but declarative responses are turned off";
189
189
  }
190
- if (entry.path.includes(":")) {
191
- return "answered by µWS, but the route captures, and nothing runs to decode the value";
190
+ if (entry.path.includes(":") && !app.get("declarative request values")) {
191
+ return (
192
+ "answered by µWS, but the route captures, and nothing runs to decode the value: " +
193
+ 'app.set("declarative request values", true) is what puts a route here'
194
+ );
192
195
  }
193
196
  if (app.get("etag")) {
194
197
  return (
package/src/utils.js CHANGED
@@ -1028,6 +1028,11 @@ const defaultSettings = {
1028
1028
  // The native µWS router matches bytes, so the compiler in _compileOptimizedRoutes only hands
1029
1029
  // it routes whose earlier siblings it can prove agree under either case rule.
1030
1030
  "declarative responses": true,
1031
+ // off: a compiled body copies nothing out of the request. On, res.send(req.query.q) and
1032
+ // res.send(req.params.id) compile too, written by uWS as it reads them and not as Express
1033
+ // does: a query key repeated or missing gives the first value or nothing, a route parameter
1034
+ // goes out undecoded, and one that Express refuses with a 400 is answered
1035
+ "declarative request values": false,
1031
1036
  // on. Off hands every request to the ordinary chain instead of letting uWS match what it can,
1032
1037
  // which is slower and answers the same. Not a tuning knob: it exists so one application can be
1033
1038
  // served both ways and the answers compared, see `npm run fuzz -- --self`. A compiled response