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/README.md +115 -985
- package/package.json +16 -4
- package/src/adopt.js +116 -5
- package/src/application.js +3 -0
- package/src/cli.js +27 -6
- package/src/create.js +248 -0
- package/src/declarative.js +6 -0
- package/src/index.js +22 -0
- package/src/optimizer.js +3 -2
- package/src/testing.js +5 -2
- package/src/utils.js +5 -0
- package/src/verify.js +108 -7
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fulmine.js",
|
|
3
|
-
"version": "5.
|
|
4
|
-
"description": "Drop-in Express 5 replacement on uWebSockets.js.
|
|
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
|
-
"
|
|
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://
|
|
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
|
|
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
|
|
21
|
-
//
|
|
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
|
-
|
|
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
|
+
};
|
package/src/application.js
CHANGED
|
@@ -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
|
|
101
|
-
"
|
|
102
|
-
|
|
103
|
-
'
|
|
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 };
|
package/src/declarative.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|