fulmine.js 5.19.8 → 5.20.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.8",
4
- "description": "Drop-in Express 5 replacement on uWebSockets.js. Your existing middleware keeps working.",
3
+ "version": "5.20.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
+ };
@@ -47,7 +47,7 @@ const cpuCount = os.cpus().length;
47
47
  // mounted sub-app knows it may inherit the parent's
48
48
  const trustProxyDefaultSymbol = "@@symbol:trust_proxy_default";
49
49
 
50
- const workers = [];
50
+ const workers = /** @type {FSWorker[]} */ ([]);
51
51
  let taskKey = 0;
52
52
  const workerTasks = new NullObject();
53
53
 
@@ -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));
@@ -133,7 +136,7 @@ class Application extends Router {
133
136
  becomeSupervisor();
134
137
  }
135
138
  if (settings.uwsApp) {
136
- this.uwsApp = settings.uwsApp;
139
+ this.uwsApp = /** @type {import("uWebSockets.js").TemplatedApp} */ (settings.uwsApp);
137
140
  } else if (settings.http3) {
138
141
  // uWS.H3App exists in the pinned build but its QUIC stack does not: the constructor
139
142
  // segfaults on Linux and hangs forever on Windows before serving a single request,
@@ -401,7 +404,7 @@ class Application extends Router {
401
404
  if (value != null && (!Array.isArray(value) || value.some((m) => typeof m !== "string"))) {
402
405
  throw new TypeError('"etag methods" wants an array of method names, or null for all of them');
403
406
  }
404
- value = value == null ? undefined : value.map((m) => m.toUpperCase());
407
+ value = value == null ? undefined : value.map((/** @type {string} */ m) => m.toUpperCase());
405
408
  } else if (key === "etag") {
406
409
  // The skips are not taken back here. They used to be, because send consults freshness,
407
410
  // but that branch reads if-none-match, if-modified-since and cache-control by name
@@ -594,7 +597,7 @@ class Application extends Router {
594
597
  // uWS runs this handler from inside its own listen(), so everything it hands back to the
595
598
  // caller is deferred a tick. Express binds synchronously too but reports through events,
596
599
  // and node emits both 'listening' and 'error' from a process.nextTick.
597
- const onListen = (socket) => {
600
+ const onListen = (/** @type {import("uWebSockets.js").us_listen_socket|false} */ socket) => {
598
601
  if (!socket) {
599
602
  /** @type {NodeJS.ErrnoException} */
600
603
  const err = new Error("listen EADDRINUSE: address already in use :::" + port);
@@ -926,6 +929,7 @@ class Application extends Router {
926
929
  // Tried once before and reverted the same day, because a callable app broke supertest: `request(app)`
927
930
  // reads `typeof app === "function"` and wraps what it finds in http.createServer, and there was
928
931
  // nothing underneath that could serve node's IncomingMessage. src/node-shim.js closes that hole.
932
+ /** @param {object} [options] the settings express() takes, see the Application constructor */
929
933
  module.exports = function (options) {
930
934
  return new Application(options)._asCallable();
931
935
  };
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 */
@@ -214,6 +224,7 @@ function findSpecifiersTypeScript(source, fileName, ts, seen) {
214
224
  }
215
225
  };
216
226
 
227
+ /** @param {import("typescript").Node} node */
217
228
  const visit = (node) => {
218
229
  // import express from "express", import type { Request } from "express", export * from it.
219
230
  // A type-only import is rewritten too: the types come from the new package as well.
@@ -465,7 +476,7 @@ function findEntry(given) {
465
476
  * owns listen.
466
477
  *
467
478
  * @param {string} entry
468
- * @returns {object[]} the prototypes to stub, this command's copy first
479
+ * @returns {Application[]} the prototypes to stub, this command's copy first
469
480
  */
470
481
  function listenOwners(entry) {
471
482
  const builds = new Set([require("./index.js")]);
@@ -477,6 +488,7 @@ function listenOwners(entry) {
477
488
  }
478
489
  }
479
490
 
491
+ /** @type {Application[]} */
480
492
  const owners = [];
481
493
  for (const build of builds) {
482
494
  if (typeof build !== "function") {
@@ -531,6 +543,7 @@ function loadApps(argv, command) {
531
543
  return null;
532
544
  }
533
545
 
546
+ /** @type {Application[]} */
534
547
  const listened = [];
535
548
  const real = owners.map((proto) => proto.listen);
536
549
  for (const proto of owners) {
@@ -907,13 +920,23 @@ function main(argv) {
907
920
  if (command === "angular") {
908
921
  return angular(argv.slice(1));
909
922
  }
923
+ if (command === "create") {
924
+ return create(argv.slice(1));
925
+ }
926
+ if (command === "pnpm") {
927
+ return pnpm(argv.slice(1));
928
+ }
910
929
  if (command !== "migrate") {
911
930
  console.log(`Usage:
931
+ npx ${TO} create <dir> start a new project: a server, a package.json and a Dockerfile that
932
+ works, --ts for TypeScript, --pnpm for the two lines pnpm needs
912
933
  npx ${TO} migrate [dir] rewrite require("${FROM}") and import from "${FROM}" to "${TO}"
913
934
  npx ${TO} override [dir] answer ${FROM} with this package for the whole dependency tree, for
914
935
  when a framework requires ${FROM} in its own code and not in yours
915
936
  npx ${TO} angular [dir] declare this package external in angular.json's server build, which
916
937
  esbuild otherwise tries to inline a native binary into
938
+ npx ${TO} pnpm [dir] make a pnpm project install this: pnpm 10.26 and later refuse a git
939
+ dependency of a dependency, and µWebSockets.js is one
917
940
  npx ${TO} profile [entry] load an application without listening and print what compiling
918
941
  its routes decided, route by route
919
942
  npx ${TO} explain <route> what happens when a request for that route arrives
@@ -921,7 +944,7 @@ function main(argv) {
921
944
  npx ${TO} differences print what behaves differently, without changing anything
922
945
 
923
946
  Options:
924
- --dry-run migrate, override, angular: say what would change and change nothing`);
947
+ --dry-run migrate, override, angular, pnpm: say what would change and change nothing`);
925
948
  return command ? 1 : 0;
926
949
  }
927
950
 
@@ -161,6 +161,7 @@ function reusableCompressor(create, finishFlag, oneShot) {
161
161
  return oneShot;
162
162
  };
163
163
 
164
+ /** @param {Buffer} body */
164
165
  const compress = (body) => {
165
166
  if (broken || busy) {
166
167
  return oneShot(body);
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 };
@@ -26,6 +26,7 @@ const uWSAny = /** @type {any} */ (uWS);
26
26
  const statuses = require("statuses");
27
27
 
28
28
  /** @typedef {import("./application.js").Application} Application */
29
+ /** @typedef {import("./router.js")} Router */
29
30
 
30
31
  const parser = acorn.Parser;
31
32
 
@@ -45,7 +46,11 @@ const allowedResMethods = [
45
46
 
46
47
  const allowedIdentifiers = ["query", "params", ...allowedResMethods];
47
48
 
48
- /** What res.type(x) sets the content type to. A lookup on a literal. */
49
+ /**
50
+ * What res.type(x) sets the content type to. A lookup on a literal.
51
+ *
52
+ * @param {string} type
53
+ */
49
54
  const typeValueOf = (type) => (type.indexOf("/") === -1 ? contentTypeFor(type) : type);
50
55
 
51
56
  // what one instruction of a declarative response can carry, since uWS writes its length as a u16
@@ -307,7 +312,7 @@ function readStatusAndHeaders(callExprs, headers) {
307
312
  * @param {any[]} callExprs the res calls, in run order, as readStatusAndHeaders takes them
308
313
  * @param {[string, string][]} headers the headers read so far, written to
309
314
  * @param {any[]} body the body parts, written to; loose because a literal's value is kept as it is
310
- * @param {Application} app the application, for the json settings
315
+ * @param {Application|Router} app the application or router the route hangs on, for the json settings
311
316
  * @param {string[]} queries names bound by a destructured req.query
312
317
  * @param {string[]} params names bound by a destructured req.params
313
318
  * @returns {{sendUsed: boolean, bodyFromSend: boolean}|null}
@@ -438,6 +443,7 @@ function readBody(callExprs, headers, body, app, queries, params) {
438
443
  }
439
444
  body.push({ type: arg.object.property.name, value: arg.property.name });
440
445
  } else if (arg.type === "BinaryExpression") {
446
+ /** @type {any[]} the parts, in the same loose shape as body */
441
447
  const stuff = [];
442
448
  /**
443
449
  * Reads a chain of string concatenations right to left. Each side must be a literal or a
@@ -768,6 +774,10 @@ function identifiersAllowed(fn, args, names) {
768
774
  // - doesnt create variables
769
775
  // - only uses req.query and req.params
770
776
  // basically, its only simple, static responses
777
+ /**
778
+ * @param {Function} cb the handler
779
+ * @param {Application|Router} app the application or router the route hangs on, for the json settings
780
+ */
771
781
  module.exports = function compileDeclarative(cb, app) {
772
782
  try {
773
783
  const handler = readHandler(cb);
@@ -791,8 +801,8 @@ module.exports = function compileDeclarative(cb, app) {
791
801
  return false;
792
802
  }
793
803
 
804
+ /** @type {[string, string][]} */
794
805
  const headers = [];
795
- const body = [];
796
806
 
797
807
  const status = readStatusAndHeaders(callExprs, headers);
798
808
  if (status === null) {
@@ -800,6 +810,8 @@ module.exports = function compileDeclarative(cb, app) {
800
810
  }
801
811
  const { statusCode, sendStatusUsed } = status;
802
812
 
813
+ /** @type {any[]} loose because a literal's value is kept as it is, see readBody */
814
+ const body = [];
803
815
  const read = readBody(callExprs, headers, body, app, queries, params);
804
816
  if (read === null) {
805
817
  return false;