fulmine.js 5.19.0 → 5.19.2

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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/package.json +7 -5
  3. package/src/cli.js +79 -8
package/README.md CHANGED
@@ -32,7 +32,7 @@ See [Migrating](#migrating) for what it handles and what it deliberately does no
32
32
 
33
33
  [![npm version](https://img.shields.io/npm/v/fulmine.js)](https://www.npmjs.com/package/fulmine.js)
34
34
  [![Node.js 22 | 24 | 26](https://img.shields.io/badge/Node.js-22%20%7C%2024%20%7C%2026-green)](https://nodejs.org)
35
- [![HTTP Arena](https://img.shields.io/endpoint?url=https://www.http-arena.com/badge/fulmine/h1.json)](https://www.http-arena.com/#tuned=0)
35
+ [![HTTP Arena](https://img.shields.io/endpoint?url=https://www.http-arena.com/badge/fulmine.js/h1.json)](https://www.http-arena.com/#tuned=0)
36
36
  [![Coverage Status](https://coveralls.io/repos/github/nigrosimone/fulmine.js/badge.svg?branch=main)](https://coveralls.io/github/nigrosimone/fulmine.js?branch=main)
37
37
  [![CodeQL](https://github.com/nigrosimone/fulmine.js/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/nigrosimone/fulmine.js/actions/workflows/codeql.yml)
38
38
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/nigrosimone/fulmine.js/badge)](https://scorecard.dev/viewer/?uri=github.com/nigrosimone/fulmine.js)
@@ -624,6 +624,8 @@ app.listen(3000, () => console.log(`worker ${process.pid} listening`));
624
624
 
625
625
  Anything held per process is now held per worker: an in-memory cache, a rate-limit counter, a session store or a `Map` of connected sockets is not shared, and needs Redis or something like it to be. `app.close()` in the primary stops the workers, and a `SIGTERM` or `SIGINT` that reaches only the primary, which is what a container sends, is passed on to them. Runnable: [`examples/cluster.js`](./examples/cluster.js).
626
626
 
627
+ 10. `app.set("connection headers", false)` stops `Connection: keep-alive` and `Keep-Alive: timeout=10` going out on every response. Express sends both, so Fulmine sends both by default. An HTTP/1.1 connection stays open without being told, so to an HTTP/1.1 client the two headers say nothing it does not know already, and they cost 46 bytes and two header writes per response. A request that asked for `Connection: close` still gets `Connection: close`, and the connection is closed. Turn it off for an API behind a proxy or serving HTTP/1.1 clients; keep the default where a client or a proxy relies on the header to keep the connection open. Worth 2% to 3.5% here on a route that is not compiled, plus the bytes.
628
+
627
629
  ## WebSockets
628
630
 
629
631
  `app.ws()` registers a WebSocket route, served by µWS itself. The upgrade never reaches node, so `server.on("upgrade")` and the libraries built on it have nothing to hear; this is the replacement.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fulmine.js",
3
- "version": "5.19.0",
3
+ "version": "5.19.2",
4
4
  "description": "Drop-in Express 5 replacement on uWebSockets.js. Your existing middleware keeps working.",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -119,9 +119,9 @@
119
119
  "@commitlint/cli": "^21.2.1",
120
120
  "@commitlint/config-conventional": "^21.2.0",
121
121
  "@eslint/js": "^10.0.1",
122
- "@nestjs/common": "^11.2.1",
123
- "@nestjs/core": "^11.2.1",
124
- "@nestjs/platform-express": "^11.2.1",
122
+ "@nestjs/common": "^12.0.1",
123
+ "@nestjs/core": "^12.0.1",
124
+ "@nestjs/platform-express": "^12.0.1",
125
125
  "@release-it/conventional-changelog": "^12.0.0",
126
126
  "@types/accepts": "^1.3.7",
127
127
  "@types/bytes": "^3.1.5",
@@ -182,7 +182,7 @@
182
182
  "pako": "^3.0.1",
183
183
  "passport": "^0.7.0",
184
184
  "passport-local": "^1.0.0",
185
- "pkg-pr-new": "^0.0.87",
185
+ "pkg-pr-new": "^0.0.88",
186
186
  "prettier": "^3.9.6",
187
187
  "pug": "^3.0.4",
188
188
  "reflect-metadata": "^0.2.2",
@@ -198,6 +198,8 @@
198
198
  "swagger-ui-express": "^5.0.1",
199
199
  "swig": "^1.4.2",
200
200
  "tsd": "^0.33.0",
201
+ "typescript": "^6.0.3",
202
+ "typescript7": "npm:typescript@^7.0.2",
201
203
  "vhost": "^3.0.2"
202
204
  },
203
205
  "contributors": [
package/src/cli.js CHANGED
@@ -143,19 +143,42 @@ function collectFiles(dir) {
143
143
  }
144
144
 
145
145
  /**
146
- * The TypeScript compiler belonging to the project being migrated, or null when it has none.
146
+ * A reader for the .ts files of the project being migrated, or null when it has no TypeScript.
147
147
  *
148
148
  * acorn cannot read TypeScript, and shipping a parser that can would put megabytes into this
149
149
  * package for a command most people run once. A TypeScript project already has the compiler, so
150
150
  * it is resolved from there. A project without one is told its .ts files were left alone rather
151
151
  * than having them quietly skipped, which is what happened before they were looked at at all.
152
152
  *
153
+ * typescript 7 is the compiler rewritten in Go, and it publishes no JavaScript parser any more:
154
+ * require("typescript") gives back a version number and nothing else. Its scanner survives, on an
155
+ * ESM-only subpath that require() reads on every node this package supports, so 7 gets the token
156
+ * walk and 6 keeps the tree.
157
+ *
153
158
  * @param {string} target directory being migrated
154
- * @returns {any|null}
159
+ * @returns {((source: string, fileName: string, seen?: Set<string>) => {start: number, end: number}[])|null}
155
160
  */
156
161
  function loadTypeScript(target) {
162
+ /** @param {string} name */
163
+ const load = (name) => require(require.resolve(name, { paths: [target, process.cwd()] }));
164
+
157
165
  try {
158
- return require(require.resolve("typescript", { paths: [target, process.cwd()] }));
166
+ const ts = load("typescript");
167
+ if (typeof ts.createSourceFile === "function") {
168
+ return (source, fileName, seen) => findSpecifiersTypeScript(source, fileName, ts, seen);
169
+ }
170
+ } catch {
171
+ return null;
172
+ }
173
+
174
+ try {
175
+ const { createScanner } = load("typescript/unstable/ast/scanner");
176
+ const { LanguageVariant, SyntaxKind } = load("typescript/unstable/ast");
177
+ // an unstable subpath, so the name the token loop stops on is checked here rather than read
178
+ // as undefined and scanned past the end of the file forever
179
+ if (SyntaxKind.EndOfFile === undefined) return null;
180
+ const ts = { createScanner, LanguageVariant, SyntaxKind };
181
+ return (source, fileName, seen) => findSpecifiersScanner(source, fileName, ts, seen);
159
182
  } catch {
160
183
  return null;
161
184
  }
@@ -222,6 +245,53 @@ function findSpecifiersTypeScript(source, fileName, ts, seen) {
222
245
  return found;
223
246
  }
224
247
 
248
+ /**
249
+ * The same specifiers again, read from the token stream instead of from a tree. typescript 7 has
250
+ * no parser to give a tree, and its scanner already does the part that a search over the text gets
251
+ * wrong: comments, template literals and strings that only look like imports are not tokens here.
252
+ *
253
+ * @param {string} source
254
+ * @param {string} fileName decides whether JSX is allowed, as above
255
+ * @param {any} ts the scanner and the two enums loadTypeScript kept
256
+ * @param {Set<string>} [seen] as in findSpecifiers
257
+ * @returns {{start: number, end: number}[]}
258
+ */
259
+ function findSpecifiersScanner(source, fileName, ts, seen) {
260
+ const kind = ts.SyntaxKind;
261
+ const scanner = ts.createScanner(
262
+ true,
263
+ fileName.endsWith(".tsx") ? ts.LanguageVariant.JSX : ts.LanguageVariant.Standard,
264
+ source
265
+ );
266
+
267
+ /** @type {{start: number, end: number}[]} */
268
+ const found = [];
269
+ // the three tokens before the one in hand, newest first
270
+ let back1 = -1;
271
+ let back2 = -1;
272
+ let back3 = -1;
273
+
274
+ for (let token = scanner.scan(); token !== kind.EndOfFile; token = scanner.scan()) {
275
+ // from "express", and import("express") or require("express") as a call. The dot rules out
276
+ // obj.require("express"), which is somebody else's require and not a module specifier
277
+ const isFrom = back1 === kind.FromKeyword;
278
+ const isRequire = back2 === kind.RequireKeyword && back3 !== kind.DotToken && back3 !== kind.QuestionDotToken;
279
+ const isCall = back1 === kind.OpenParenToken && (back2 === kind.ImportKeyword || isRequire);
280
+ if (token === kind.StringLiteral && (isFrom || isCall)) {
281
+ const text = scanner.getTokenValue();
282
+ if (text === FROM) {
283
+ found.push({ start: scanner.getTokenStart(), end: scanner.getTokenEnd() });
284
+ } else if (seen && BUILT_IN_INSTEAD[text]) {
285
+ seen.add(text);
286
+ }
287
+ }
288
+ back3 = back2;
289
+ back2 = back1;
290
+ back1 = token;
291
+ }
292
+ return found;
293
+ }
294
+
225
295
  /**
226
296
  * The string literals naming the module, found through the parser rather than by searching the
227
297
  * text. "express" appears inside express-session, inside comments and inside strings that are not
@@ -874,7 +944,7 @@ Options:
874
944
 
875
945
  // resolved once, and only if there is anything to use it on
876
946
  const hasTypeScriptFiles = files.some((file) => TYPESCRIPT_EXTENSIONS.has(path.extname(file)));
877
- const ts = hasTypeScriptFiles ? loadTypeScript(target) : null;
947
+ const readTypeScript = hasTypeScriptFiles ? loadTypeScript(target) : null;
878
948
 
879
949
  for (const file of files) {
880
950
  const source = fs.readFileSync(file, "utf8");
@@ -883,14 +953,15 @@ Options:
883
953
  if (!source.includes(FROM) && !Object.keys(BUILT_IN_INSTEAD).some((name) => source.includes(name))) continue;
884
954
 
885
955
  const isTypeScript = TYPESCRIPT_EXTENSIONS.has(path.extname(file));
886
- if (isTypeScript && !ts) {
956
+ if (isTypeScript && !readTypeScript) {
887
957
  needTypeScript.push(path.relative(target, file));
888
958
  continue;
889
959
  }
890
960
 
891
- const specifiers = isTypeScript
892
- ? findSpecifiersTypeScript(source, file, ts, builtInInstead)
893
- : findSpecifiers(source, builtInInstead);
961
+ const specifiers =
962
+ isTypeScript && readTypeScript
963
+ ? readTypeScript(source, file, builtInInstead)
964
+ : findSpecifiers(source, builtInInstead);
894
965
  if (specifiers === null) {
895
966
  unparsed.push(path.relative(target, file));
896
967
  continue;