fulmine.js 5.19.9 → 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/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 +22 -2
- package/src/create.js +248 -0
- package/src/index.js +22 -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.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
|
-
"
|
|
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 */
|
|
@@ -910,13 +920,23 @@ function main(argv) {
|
|
|
910
920
|
if (command === "angular") {
|
|
911
921
|
return angular(argv.slice(1));
|
|
912
922
|
}
|
|
923
|
+
if (command === "create") {
|
|
924
|
+
return create(argv.slice(1));
|
|
925
|
+
}
|
|
926
|
+
if (command === "pnpm") {
|
|
927
|
+
return pnpm(argv.slice(1));
|
|
928
|
+
}
|
|
913
929
|
if (command !== "migrate") {
|
|
914
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
|
|
915
933
|
npx ${TO} migrate [dir] rewrite require("${FROM}") and import from "${FROM}" to "${TO}"
|
|
916
934
|
npx ${TO} override [dir] answer ${FROM} with this package for the whole dependency tree, for
|
|
917
935
|
when a framework requires ${FROM} in its own code and not in yours
|
|
918
936
|
npx ${TO} angular [dir] declare this package external in angular.json's server build, which
|
|
919
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
|
|
920
940
|
npx ${TO} profile [entry] load an application without listening and print what compiling
|
|
921
941
|
its routes decided, route by route
|
|
922
942
|
npx ${TO} explain <route> what happens when a request for that route arrives
|
|
@@ -924,7 +944,7 @@ function main(argv) {
|
|
|
924
944
|
npx ${TO} differences print what behaves differently, without changing anything
|
|
925
945
|
|
|
926
946
|
Options:
|
|
927
|
-
--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`);
|
|
928
948
|
return command ? 1 : 0;
|
|
929
949
|
}
|
|
930
950
|
|
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/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");
|