@rspack/dev-middleware 2.0.0-beta.1 → 2.0.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 CHANGED
@@ -39,9 +39,9 @@ bun add -D @rspack/dev-middleware
39
39
  ## Usage
40
40
 
41
41
  ```js
42
- const { devMiddleware } = require("@rspack/dev-middleware");
43
- const express = require("express");
44
- const { rspack } = require("@rspack/core");
42
+ import { rspack } from "@rspack/core";
43
+ import { devMiddleware } from "@rspack/dev-middleware";
44
+ import express from "express";
45
45
 
46
46
  const compiler = rspack({
47
47
  // Rspack options
@@ -58,7 +58,7 @@ app.use(
58
58
  app.listen(3000, () => console.log("Example app listening on port 3000!"));
59
59
  ```
60
60
 
61
- See [below](#other-servers) for an example of use with fastify.
61
+ See [below](#other-servers) for examples of use with other servers.
62
62
 
63
63
  ## Options
64
64
 
@@ -72,7 +72,7 @@ See [below](#other-servers) for an example of use with fastify.
72
72
  | **[`etag`](#tag)** | `boolean\| "weak"\| "strong"` | `undefined` | Enable or disable etag generation. |
73
73
  | **[`lastModified`](#lastmodified)** | `boolean` | `undefined` | Enable or disable `Last-Modified` header. Uses the file system's last modified value. |
74
74
  | **[`cacheControl`](#cachecontrol)** | `boolean\|number\|string\|Object` | `undefined` | Enable or disable setting `Cache-Control` response header. |
75
- | **[`cacheImmutable`](#cacheimmutable)** | `boolean\` | `undefined` | Enable or disable setting `Cache-Control: public, max-age=31536000, immutable` response header for immutable assets. |
75
+ | **[`cacheImmutable`](#cacheimmutable)** | `boolean` | `true` | Enable or disable setting `Cache-Control: public, max-age=31536000, immutable` response header for immutable assets. |
76
76
  | **[`publicPath`](#publicpath)** | `string` | `undefined` | The public path that the middleware is bound to. |
77
77
  | **[`stats`](#stats)** | `boolean\|string\|Object` | `stats` (from a configuration) | Stats options object or preset name. |
78
78
  | **[`serverSideRender`](#serversiderender)** | `boolean` | `undefined` | Instructs the module to enable or disable the server-side rendering mode. |
@@ -196,21 +196,25 @@ Default: `undefined`
196
196
 
197
197
  Depending on the setting, the following headers will be generated:
198
198
 
199
- - `Boolean` - `Cache-Control: public, max-age=31536000000`
200
- - `Number` - `Cache-Control: public, max-age=YOUR_NUMBER`
199
+ - `Boolean` - `Cache-Control: public, max-age=31536000`
200
+ - `Number` - `Cache-Control: public, max-age=YOUR_NUMBER_IN_SECONDS`
201
201
  - `String` - `Cache-Control: YOUR_STRING`
202
- - `{ maxAge?: number, immutable?: boolean }` - `Cache-Control: public, max-age=YOUR_MAX_AGE_or_31536000000`, also `, immutable` can be added if you set the `immutable` option to `true`
202
+ - `{ maxAge?: number, immutable?: boolean }` - `Cache-Control: public, max-age=YOUR_MAX_AGE_IN_SECONDS_or_31536000`, also `, immutable` is added when you set the `immutable` option to `true`
203
+
204
+ Numeric `cacheControl` and `cacheControl.maxAge` values are interpreted as milliseconds, clamped to `0..31536000000`, and converted to seconds for the response header.
203
205
 
204
206
  Enable or disable setting `Cache-Control` response header.
205
207
 
206
208
  ### cacheImmutable
207
209
 
208
210
  Type: `Boolean`
209
- Default: `undefined`
211
+ Default: `true`
210
212
 
211
213
  Enable or disable setting `Cache-Control: public, max-age=31536000, immutable` response header for immutable assets (i.e. asset with a hash like `image.a4c12bde.jpg`).
212
214
  Immutable assets are assets that have their hash in the file name therefore they can be cached, because if you change their contents the file name will be changed.
213
- Take preference over the `cacheControl` option if the asset was defined as immutable.
215
+ When omitted, immutable assets use this header by default.
216
+ Set `cacheImmutable: false` to fall back to the `cacheControl` option even for immutable assets.
217
+ This takes precedence over the `cacheControl` option only when the asset was defined as immutable and `cacheImmutable` is not `false`.
214
218
 
215
219
  ### publicPath
216
220
 
@@ -249,7 +253,7 @@ This option also accepts a `Function` value, which can be used to filter which f
249
253
  The function follows the same premise as [`Array#filter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) in which a return value of `false` _will not_ write the file, and a return value of `true` _will_ write the file to disk. eg.
250
254
 
251
255
  ```js
252
- const { rspack } = require("@rspack/core");
256
+ import { rspack } from "@rspack/core";
253
257
 
254
258
  const configuration = {
255
259
  /* Rspack configuration */
@@ -269,16 +273,16 @@ Default: [memfs](https://github.com/streamich/memfs)
269
273
  Set the default file system which will be used by Rspack as primary destination of generated files.
270
274
  This option isn't affected by the [writeToDisk](#writeToDisk) option.
271
275
 
272
- This can be done simply by using `path.join`:
276
+ This can be done simply by using `node:path`'s `join`:
273
277
 
274
278
  ```js
275
- const path = require("node:path");
276
- const mkdirp = require("mkdirp");
277
- const myOutputFileSystem = require("my-fs");
278
- const { rspack } = require("@rspack/core");
279
+ import { join } from "node:path";
280
+ import { rspack } from "@rspack/core";
281
+ import mkdirp from "mkdirp";
282
+ import myOutputFileSystem from "my-fs";
279
283
 
280
- myOutputFileSystem.join = path.join.bind(path); // no need to bind
281
- myOutputFileSystem.mkdirp = mkdirp.bind(mkdirp); // no need to bind
284
+ myOutputFileSystem.join = join;
285
+ myOutputFileSystem.mkdirp = mkdirp;
282
286
 
283
287
  const compiler = rspack({
284
288
  /* Rspack configuration */
@@ -292,7 +296,7 @@ devMiddleware(compiler, { outputFileSystem: myOutputFileSystem });
292
296
  Allows to set up a callback to change the response data.
293
297
 
294
298
  ```js
295
- const { rspack } = require("@rspack/core");
299
+ import { rspack } from "@rspack/core";
296
300
 
297
301
  const configuration = {
298
302
  /* Rspack configuration */
@@ -328,9 +332,9 @@ Required: `No`
328
332
  A function executed once the middleware has stopped watching.
329
333
 
330
334
  ```js
331
- const { devMiddleware } = require("@rspack/dev-middleware");
332
- const express = require("express");
333
- const { rspack } = require("@rspack/core");
335
+ import { rspack } from "@rspack/core";
336
+ import { devMiddleware } from "@rspack/dev-middleware";
337
+ import express from "express";
334
338
 
335
339
  const compiler = rspack({
336
340
  /* Rspack configuration */
@@ -338,8 +342,7 @@ const compiler = rspack({
338
342
 
339
343
  const instance = devMiddleware(compiler);
340
344
 
341
- // eslint-disable-next-line new-cap
342
- const app = new express();
345
+ const app = express();
343
346
 
344
347
  app.use(instance);
345
348
 
@@ -363,9 +366,9 @@ Required: `No`
363
366
  A function executed once the middleware has invalidated.
364
367
 
365
368
  ```js
366
- const { devMiddleware } = require("@rspack/dev-middleware");
367
- const express = require("express");
368
- const { rspack } = require("@rspack/core");
369
+ import { rspack } from "@rspack/core";
370
+ import { devMiddleware } from "@rspack/dev-middleware";
371
+ import express from "express";
369
372
 
370
373
  const compiler = rspack({
371
374
  /* Rspack configuration */
@@ -373,8 +376,7 @@ const compiler = rspack({
373
376
 
374
377
  const instance = devMiddleware(compiler);
375
378
 
376
- // eslint-disable-next-line new-cap
377
- const app = new express();
379
+ const app = express();
378
380
 
379
381
  app.use(instance);
380
382
 
@@ -403,9 +405,9 @@ A function executed when the bundle becomes valid.
403
405
  If the bundle is valid at the time of calling, the callback is executed immediately.
404
406
 
405
407
  ```js
406
- const { devMiddleware } = require("@rspack/dev-middleware");
407
- const express = require("express");
408
- const { rspack } = require("@rspack/core");
408
+ import { rspack } from "@rspack/core";
409
+ import { devMiddleware } from "@rspack/dev-middleware";
410
+ import express from "express";
409
411
 
410
412
  const compiler = rspack({
411
413
  /* Rspack configuration */
@@ -413,8 +415,7 @@ const compiler = rspack({
413
415
 
414
416
  const instance = devMiddleware(compiler);
415
417
 
416
- // eslint-disable-next-line new-cap
417
- const app = new express();
418
+ const app = express();
418
419
 
419
420
  app.use(instance);
420
421
 
@@ -437,9 +438,9 @@ Required: `Yes`
437
438
  URL for the requested file.
438
439
 
439
440
  ```js
440
- const { devMiddleware } = require("@rspack/dev-middleware");
441
- const express = require("express");
442
- const { rspack } = require("@rspack/core");
441
+ import { rspack } from "@rspack/core";
442
+ import { devMiddleware } from "@rspack/dev-middleware";
443
+ import express from "express";
443
444
 
444
445
  const compiler = rspack({
445
446
  /* Rspack configuration */
@@ -447,15 +448,26 @@ const compiler = rspack({
447
448
 
448
449
  const instance = devMiddleware(compiler);
449
450
 
450
- // eslint-disable-next-line new-cap
451
- const app = new express();
451
+ const app = express();
452
452
 
453
453
  app.use(instance);
454
454
 
455
455
  instance.waitUntilValid(() => {
456
- const filename = instance.getFilenameFromUrl("/bundle.js");
456
+ let resolved;
457
+
458
+ try {
459
+ resolved = instance.getFilenameFromUrl("/bundle.js");
460
+ } catch (error) {
461
+ console.error(error);
462
+ return;
463
+ }
464
+
465
+ if (!resolved) {
466
+ console.log("Not found");
467
+ return;
468
+ }
457
469
 
458
- console.log(`Filename is ${filename}`);
470
+ console.log(`Filename is ${resolved.filename}`);
459
471
  });
460
472
  ```
461
473
 
@@ -468,9 +480,9 @@ Since `output.publicPath` and `output.filename`/`output.chunkFilename` can be dy
468
480
  But there is a solution to avoid it - mount the middleware to a non-root route, for example:
469
481
 
470
482
  ```js
471
- const { devMiddleware } = require("@rspack/dev-middleware");
472
- const express = require("express");
473
- const { rspack } = require("@rspack/core");
483
+ import { rspack } from "@rspack/core";
484
+ import { devMiddleware } from "@rspack/dev-middleware";
485
+ import express from "express";
474
486
 
475
487
  const compiler = rspack({
476
488
  // Rspack options
@@ -509,17 +521,17 @@ process is finished with server-side rendering enabled._
509
521
  Example Implementation:
510
522
 
511
523
  ```js
512
- const { devMiddleware } = require("@rspack/dev-middleware");
513
- const express = require("express");
514
- const isObject = require("is-object");
515
- const { rspack } = require("@rspack/core");
524
+ import { join } from "node:path";
525
+ import { rspack } from "@rspack/core";
526
+ import { devMiddleware } from "@rspack/dev-middleware";
527
+ import express from "express";
528
+ import isObject from "is-object";
516
529
 
517
530
  const compiler = rspack({
518
531
  /* Rspack configuration */
519
532
  });
520
533
 
521
- // eslint-disable-next-line new-cap
522
- const app = new express();
534
+ const app = express();
523
535
 
524
536
  // This function makes server rendering of asset references consistent with different Rspack chunk/entry configurations
525
537
  function normalizeAssets(assets) {
@@ -547,16 +559,16 @@ app.use((req, res) => {
547
559
  <title>My App</title>
548
560
  <style>
549
561
  ${normalizeAssets(assetsByChunkName.main)
550
- .filter((path) => path.endsWith(".css"))
551
- .map((path) => outputFileSystem.readFileSync(path.join(outputPath, path)))
562
+ .filter((asset) => asset.endsWith(".css"))
563
+ .map((asset) => outputFileSystem.readFileSync(join(outputPath, asset)))
552
564
  .join("\n")}
553
565
  </style>
554
566
  </head>
555
567
  <body>
556
568
  <div id="root"></div>
557
569
  ${normalizeAssets(assetsByChunkName.main)
558
- .filter((path) => path.endsWith(".js"))
559
- .map((path) => `<script src="${path}"></script>`)
570
+ .filter((asset) => asset.endsWith(".js"))
571
+ .map((asset) => `<script src="${asset}"></script>`)
560
572
  .join("\n")}
561
573
  </body>
562
574
  </html>
@@ -568,14 +580,16 @@ app.use((req, res) => {
568
580
 
569
581
  Examples of use with other servers will follow here.
570
582
 
571
- ### Connect
583
+ ### connect-next
584
+
585
+ [connect-next](https://github.com/rstackjs/connect-next) is an actively maintained fork of Connect.
572
586
 
573
587
  ```js
574
- const http = require("node:http");
575
- const { devMiddleware } = require("@rspack/dev-middleware");
576
- const connect = require("connect");
577
- const { rspack } = require("@rspack/core");
578
- const rspackConfig = require("./rspack.config.js");
588
+ import { createServer } from "node:http";
589
+ import { rspack } from "@rspack/core";
590
+ import { devMiddleware } from "@rspack/dev-middleware";
591
+ import { connect } from "connect-next";
592
+ import rspackConfig from "./rspack.config.js";
579
593
 
580
594
  const compiler = rspack(rspackConfig);
581
595
  const devMiddlewareOptions = {
@@ -585,18 +599,18 @@ const app = connect();
585
599
 
586
600
  app.use(devMiddleware(compiler, devMiddlewareOptions));
587
601
 
588
- http.createServer(app).listen(3000);
602
+ createServer(app).listen(3000);
589
603
  ```
590
604
 
591
605
  ### Router
592
606
 
593
607
  ```js
594
- const http = require("node:http");
595
- const { devMiddleware } = require("@rspack/dev-middleware");
596
- const finalhandler = require("finalhandler");
597
- const Router = require("router");
598
- const { rspack } = require("@rspack/core");
599
- const rspackConfig = require("./rspack.config.js");
608
+ import { createServer } from "node:http";
609
+ import { rspack } from "@rspack/core";
610
+ import { devMiddleware } from "@rspack/dev-middleware";
611
+ import finalhandler from "finalhandler";
612
+ import Router from "router";
613
+ import rspackConfig from "./rspack.config.js";
600
614
 
601
615
  const compiler = rspack(rspackConfig);
602
616
  const devMiddlewareOptions = {
@@ -608,7 +622,7 @@ const router = Router();
608
622
 
609
623
  router.use(devMiddleware(compiler, devMiddlewareOptions));
610
624
 
611
- const server = http.createServer((req, res) => {
625
+ const server = createServer((req, res) => {
612
626
  router(req, res, finalhandler(req, res));
613
627
  });
614
628
 
@@ -618,10 +632,10 @@ server.listen(3000);
618
632
  ### Express
619
633
 
620
634
  ```js
621
- const { devMiddleware } = require("@rspack/dev-middleware");
622
- const express = require("express");
623
- const { rspack } = require("@rspack/core");
624
- const rspackConfig = require("./rspack.config.js");
635
+ import { rspack } from "@rspack/core";
636
+ import { devMiddleware } from "@rspack/dev-middleware";
637
+ import express from "express";
638
+ import rspackConfig from "./rspack.config.js";
625
639
 
626
640
  const compiler = rspack(rspackConfig);
627
641
  const devMiddlewareOptions = {
@@ -637,10 +651,10 @@ app.listen(3000, () => console.log("Example app listening on port 3000!"));
637
651
  ### Koa
638
652
 
639
653
  ```js
640
- const { devMiddleware } = require("@rspack/dev-middleware");
641
- const Koa = require("koa");
642
- const { rspack } = require("@rspack/core");
643
- const rspackConfig = require("./rspack.simple.config");
654
+ import { rspack } from "@rspack/core";
655
+ import { devMiddleware } from "@rspack/dev-middleware";
656
+ import Koa from "koa";
657
+ import rspackConfig from "./rspack.simple.config.js";
644
658
 
645
659
  const compiler = rspack(rspackConfig);
646
660
  const devMiddlewareOptions = {
@@ -648,7 +662,7 @@ const devMiddlewareOptions = {
648
662
  };
649
663
  const app = new Koa();
650
664
 
651
- app.use(middleware.koaWrapper(compiler, devMiddlewareOptions));
665
+ app.use(devMiddleware.koaWrapper(compiler, devMiddlewareOptions));
652
666
 
653
667
  app.listen(3000);
654
668
  ```
@@ -656,10 +670,10 @@ app.listen(3000);
656
670
  ### Hapi
657
671
 
658
672
  ```js
659
- const Hapi = require("@hapi/hapi");
660
- const { devMiddleware } = require("@rspack/dev-middleware");
661
- const { rspack } = require("@rspack/core");
662
- const rspackConfig = require("./rspack.config.js");
673
+ import Hapi from "@hapi/hapi";
674
+ import { rspack } from "@rspack/core";
675
+ import { devMiddleware } from "@rspack/dev-middleware";
676
+ import rspackConfig from "./rspack.config.js";
663
677
 
664
678
  const compiler = rspack(rspackConfig);
665
679
  const devMiddlewareOptions = {};
@@ -685,33 +699,13 @@ process.on("unhandledRejection", (err) => {
685
699
  });
686
700
  ```
687
701
 
688
- ### Fastify
689
-
690
- Fastify interop will require the use of `fastify-express` instead of `middie` for providing middleware support. As the authors of `fastify-express` recommend, this should only be used as a stopgap while full Fastify support is worked on.
691
-
692
- ```js
693
- const { devMiddleware } = require("@rspack/dev-middleware");
694
- const fastify = require("fastify")();
695
- const { rspack } = require("@rspack/core");
696
- const rspackConfig = require("./rspack.config.js");
697
-
698
- const compiler = rspack(rspackConfig);
699
- const devMiddlewareOptions = {
700
- // options
701
- };
702
-
703
- await fastify.register(require("@fastify/express"));
704
- await fastify.use(devMiddleware(compiler, devMiddlewareOptions));
705
- await fastify.listen(3000);
706
- ```
707
-
708
702
  ### Hono
709
703
 
710
704
  ```js
705
+ import { rspack } from "@rspack/core";
706
+ import { devMiddleware } from "@rspack/dev-middleware";
711
707
  import { serve } from "@hono/node-server";
712
- import devMiddleware from "@rspack/dev-middleware";
713
708
  import { Hono } from "hono";
714
- import { rspack } from "@rspack/core";
715
709
  import rspackConfig from "./rspack.config.js";
716
710
 
717
711
  const compiler = rspack(rspackConfig);
package/dist/index.js CHANGED
@@ -10,7 +10,6 @@ import { __webpack_require__ } from "./rslib-runtime.js";
10
10
  import node_path from "node:path";
11
11
  import node_crypto from "node:crypto";
12
12
  import node_querystring from "node:querystring";
13
- import { parse } from "node:url";
14
13
  import node_fs from "node:fs";
15
14
  __webpack_require__.add({
16
15
  "./node_modules/.pnpm/@jsonjoy.com+base64@17.67.0_tslib@2.8.1/node_modules/@jsonjoy.com/base64/lib/constants.js" (__unused_rspack_module, exports) {
@@ -8337,7 +8336,6 @@ function getRequestMethod(req) {
8337
8336
  }
8338
8337
  function getRequestURL(req) {
8339
8338
  if ("function" == typeof req.getURL) return req.getURL();
8340
- if (void 0 !== req.originalUrl) return req.originalUrl;
8341
8339
  return req.url;
8342
8340
  }
8343
8341
  function setStatusCode(res, code) {
@@ -8498,8 +8496,10 @@ function getPaths(context) {
8498
8496
  asset.name,
8499
8497
  asset.info
8500
8498
  ]));
8499
+ const { outputFileSystem } = compilation.compiler;
8501
8500
  publicPaths.push({
8502
8501
  outputPath,
8502
+ outputFileSystem,
8503
8503
  publicPath,
8504
8504
  assetsInfo
8505
8505
  });
@@ -8527,70 +8527,113 @@ const utils_memorize = memorize;
8527
8527
  function decode(input) {
8528
8528
  return node_querystring.unescape(input);
8529
8529
  }
8530
- const memoizedParse = utils_memorize(parse, void 0, (value)=>{
8531
- if (value.pathname) value.pathname = decode(value.pathname);
8532
- return value;
8530
+ const memoizedParse = utils_memorize((url)=>{
8531
+ const urlObject = new URL(url, "http://localhost");
8532
+ return {
8533
+ ...urlObject,
8534
+ pathname: decode(urlObject.pathname)
8535
+ };
8533
8536
  });
8534
8537
  const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
8535
- function getFilenameFromUrl(context, url, extra = {}) {
8538
+ class FilenameError extends Error {
8539
+ constructor(message, code){
8540
+ super(message);
8541
+ this.name = "FilenameError";
8542
+ this.statusCode = code;
8543
+ }
8544
+ }
8545
+ function isNotFoundError(error) {
8546
+ switch(error.code){
8547
+ case "ENAMETOOLONG":
8548
+ case "ENOENT":
8549
+ case "ENOTDIR":
8550
+ return true;
8551
+ default:
8552
+ return false;
8553
+ }
8554
+ }
8555
+ function getFilenameFromUrl(context, url) {
8536
8556
  const { options } = context;
8537
8557
  const paths = utils_getPaths(context);
8538
- let foundFilename;
8558
+ const index = false === options.index ? [] : void 0 === options.index || true === options.index ? [
8559
+ "index.html"
8560
+ ] : [
8561
+ options.index
8562
+ ];
8539
8563
  let urlObject;
8540
8564
  try {
8541
- urlObject = memoizedParse(url, false, true);
8565
+ urlObject = memoizedParse(url);
8542
8566
  } catch {
8543
8567
  return;
8544
8568
  }
8545
- for (const { publicPath, outputPath, assetsInfo } of paths){
8569
+ for (const { publicPath, outputPath, assetsInfo, outputFileSystem } of paths){
8546
8570
  let filename;
8547
8571
  let publicPathObject;
8548
8572
  try {
8549
- publicPathObject = memoizedParse("auto" !== publicPath && publicPath ? publicPath : "/", false, true);
8573
+ publicPathObject = memoizedParse("auto" !== publicPath && publicPath ? publicPath : "/");
8550
8574
  } catch {
8551
8575
  continue;
8552
8576
  }
8553
8577
  const { pathname } = urlObject;
8554
8578
  const { pathname: publicPathPathname } = publicPathObject;
8555
8579
  if (pathname && publicPathPathname && pathname.startsWith(publicPathPathname)) {
8556
- if (pathname.includes("\0")) {
8557
- extra.errorCode = 400;
8558
- return;
8559
- }
8560
- if (UP_PATH_REGEXP.test(node_path.normalize(`./${pathname}`))) {
8561
- extra.errorCode = 403;
8562
- return;
8563
- }
8580
+ if (pathname.includes("\0")) throw new FilenameError("Bad Request", 400);
8581
+ if (UP_PATH_REGEXP.test(node_path.normalize(`./${pathname}`))) throw new FilenameError("Forbidden", 403);
8564
8582
  filename = node_path.join(outputPath, pathname.slice(publicPathPathname.length));
8565
- try {
8566
- extra.stats = context.outputFileSystem.statSync(filename);
8567
- } catch {
8568
- continue;
8569
- }
8570
- if (extra.stats.isFile()) {
8571
- foundFilename = filename;
8572
- if (assetsInfo) {
8573
- const assetInfo = assetsInfo.get(pathname.slice(publicPathPathname.length));
8574
- extra.immutable = assetInfo ? assetInfo.immutable : false;
8575
- }
8576
- break;
8577
- }
8578
- if (extra.stats.isDirectory() && (void 0 === options.index || options.index)) {
8579
- const indexValue = void 0 === options.index || "boolean" == typeof options.index ? "index.html" : options.index;
8580
- filename = node_path.join(filename, indexValue);
8583
+ const resolveIndex = (filename, visited = new Set())=>{
8584
+ if (0 === index.length) return;
8585
+ const nextFilename = node_path.join(filename, index[0]);
8586
+ if (visited.has(nextFilename)) return;
8587
+ visited.add(nextFilename);
8588
+ filename = nextFilename;
8589
+ let stats;
8581
8590
  try {
8582
- extra.stats = context.outputFileSystem.statSync(filename);
8583
- } catch {
8584
- continue;
8591
+ stats = outputFileSystem.statSync(filename);
8592
+ } catch (error) {
8593
+ if (isNotFoundError(error)) return;
8594
+ throw error;
8585
8595
  }
8586
- if (extra.stats.isFile()) {
8587
- foundFilename = filename;
8588
- break;
8596
+ if (stats.isDirectory()) return resolveIndex(filename, visited);
8597
+ const extra = {
8598
+ immutable: assetsInfo ? assetsInfo.get(pathname.slice(publicPathPathname.length))?.immutable : false,
8599
+ outputFileSystem,
8600
+ stats: stats
8601
+ };
8602
+ return {
8603
+ filename,
8604
+ extra
8605
+ };
8606
+ };
8607
+ const resolveFile = (filename)=>{
8608
+ let stats;
8609
+ try {
8610
+ stats = outputFileSystem.statSync(filename);
8611
+ } catch (error) {
8612
+ if (isNotFoundError(error)) return;
8613
+ throw error;
8589
8614
  }
8615
+ if (stats.isDirectory()) return resolveIndex(filename);
8616
+ if (filename.endsWith(node_path.sep)) return;
8617
+ const extra = {
8618
+ immutable: assetsInfo ? assetsInfo.get(pathname.slice(publicPathPathname.length))?.immutable : false,
8619
+ outputFileSystem,
8620
+ stats: stats
8621
+ };
8622
+ return {
8623
+ filename,
8624
+ extra
8625
+ };
8626
+ };
8627
+ if (index.length > 0 && pathname.endsWith("/")) {
8628
+ const result = resolveIndex(filename);
8629
+ if (!result) continue;
8630
+ return result;
8590
8631
  }
8632
+ const result = resolveFile(filename);
8633
+ if (!result) continue;
8634
+ return result;
8591
8635
  }
8592
8636
  }
8593
- return foundFilename;
8594
8637
  }
8595
8638
  const utils_getFilenameFromUrl = getFilenameFromUrl;
8596
8639
  function parseTokenList(str) {
@@ -8736,7 +8779,7 @@ function wrapper(context) {
8736
8779
  setResponseHeader(res, "Content-Length", byteLength);
8737
8780
  finish(res, document);
8738
8781
  }
8739
- async function errorHandler(error) {
8782
+ async function errorHandler(error, message, code) {
8740
8783
  switch(error.code){
8741
8784
  case "ENAMETOOLONG":
8742
8785
  case "ENOENT":
@@ -8746,7 +8789,7 @@ function wrapper(context) {
8746
8789
  });
8747
8790
  break;
8748
8791
  default:
8749
- await sendError(error.message, 500, {
8792
+ await sendError(message || error.message, code || 500, {
8750
8793
  modifyResponseData: context.options.modifyResponseData
8751
8794
  });
8752
8795
  break;
@@ -8835,17 +8878,19 @@ function wrapper(context) {
8835
8878
  ];
8836
8879
  }
8837
8880
  async function processRequest() {
8838
- const extra = {};
8839
- const filename = utils_getFilenameFromUrl(context, getRequestURL(req), extra);
8840
- if (extra.errorCode) {
8841
- if (403 === extra.errorCode) context.logger.error(`Malicious path "${filename}".`);
8842
- await sendError(400 === extra.errorCode ? "Bad Request" : "Forbidden", extra.errorCode, {
8843
- modifyResponseData: context.options.modifyResponseData
8844
- });
8881
+ let resolved;
8882
+ const requestUrl = getRequestURL(req);
8883
+ try {
8884
+ resolved = utils_getFilenameFromUrl(context, requestUrl);
8885
+ } catch (error) {
8886
+ const errorCode = "object" == typeof error && null !== error && void 0 !== error.statusCode ? error.statusCode : void 0;
8887
+ if (403 === errorCode) context.logger.error(`Malicious path "${requestUrl}".`);
8888
+ await errorHandler(error, 400 === errorCode ? "Bad Request" : 403 === errorCode ? "Forbidden" : void 0, errorCode);
8845
8889
  return;
8846
8890
  }
8847
- if (!filename) return void await goNext();
8891
+ if (!resolved) return void await goNext();
8848
8892
  if (getHeadersSent(res)) return void await goNext();
8893
+ const { extra, filename } = resolved;
8849
8894
  const { size } = extra.stats;
8850
8895
  let len = size;
8851
8896
  let offset = 0;
@@ -8866,23 +8911,21 @@ function wrapper(context) {
8866
8911
  }
8867
8912
  if (!getResponseHeader(res, "Accept-Ranges")) setResponseHeader(res, "Accept-Ranges", "bytes");
8868
8913
  if (!getResponseHeader(res, "Cache-Control")) {
8869
- const cacheControl = context.options.cacheImmutable && extra.immutable ? {
8870
- immutable: true
8871
- } : context.options.cacheControl;
8872
- if (cacheControl) {
8873
- let cacheControlValue;
8874
- if ("boolean" == typeof cacheControl) cacheControlValue = "public, max-age=31536000";
8875
- else if ("number" == typeof cacheControl) {
8876
- const maxAge = Math.floor(Math.min(Math.max(0, cacheControl), MAX_MAX_AGE) / 1000);
8877
- cacheControlValue = `public, max-age=${maxAge}`;
8878
- } else if ("string" == typeof cacheControl) cacheControlValue = cacheControl;
8879
- else {
8880
- const maxAge = cacheControl.maxAge ? Math.floor(Math.min(Math.max(0, cacheControl.maxAge), MAX_MAX_AGE) / 1000) : MAX_MAX_AGE / 1000;
8881
- cacheControlValue = `public, max-age=${maxAge}`;
8882
- if (cacheControl.immutable) cacheControlValue += ", immutable";
8883
- }
8884
- setResponseHeader(res, "Cache-Control", cacheControlValue);
8885
- }
8914
+ const { cacheControl, cacheImmutable } = context.options;
8915
+ const useImmutableCache = (void 0 === cacheImmutable || cacheImmutable) && extra.immutable;
8916
+ let cacheControlValue;
8917
+ if (useImmutableCache) cacheControlValue = `public, max-age=${Math.floor(MAX_MAX_AGE / 1000)}, immutable`;
8918
+ else if (true === cacheControl) cacheControlValue = `public, max-age=${Math.floor(MAX_MAX_AGE / 1000)}`;
8919
+ else if ("number" == typeof cacheControl) {
8920
+ const maxAge = Math.min(Math.max(0, cacheControl), MAX_MAX_AGE);
8921
+ cacheControlValue = `public, max-age=${Math.floor(maxAge / 1000)}`;
8922
+ } else if ("string" == typeof cacheControl) cacheControlValue = cacheControl;
8923
+ else if (cacheControl) {
8924
+ const maxAge = void 0 !== cacheControl.maxAge ? Math.min(Math.max(0, cacheControl.maxAge), MAX_MAX_AGE) : MAX_MAX_AGE;
8925
+ cacheControlValue = `public, max-age=${Math.floor(maxAge / 1000)}`;
8926
+ if (cacheControl.immutable) cacheControlValue += ", immutable";
8927
+ }
8928
+ if (cacheControlValue) setResponseHeader(res, "Cache-Control", cacheControlValue);
8886
8929
  }
8887
8930
  if (context.options.lastModified && !getResponseHeader(res, "Last-Modified")) {
8888
8931
  const modified = extra.stats.mtime.toUTCString();
@@ -8902,7 +8945,7 @@ function wrapper(context) {
8902
8945
  }
8903
8946
  [start, end] = calcStartAndEnd(offset, len);
8904
8947
  try {
8905
- const result = createReadStreamOrReadFileSync(filename, context.outputFileSystem, start, end);
8948
+ const result = createReadStreamOrReadFileSync(filename, extra.outputFileSystem, start, end);
8906
8949
  ({ bufferOrStream, byteLength } = result);
8907
8950
  } catch (error) {
8908
8951
  await errorHandler(error);
@@ -8964,7 +9007,7 @@ function wrapper(context) {
8964
9007
  if (!bufferOrStream) {
8965
9008
  [start, end] = calcStartAndEnd(offset, len);
8966
9009
  try {
8967
- ({ bufferOrStream, byteLength } = createReadStreamOrReadFileSync(filename, context.outputFileSystem, start, end));
9010
+ ({ bufferOrStream, byteLength } = createReadStreamOrReadFileSync(filename, extra.outputFileSystem, start, end));
8968
9011
  } catch (error) {
8969
9012
  await errorHandler(error);
8970
9013
  return;
@@ -9121,7 +9164,7 @@ function rdm(compiler, options = {}) {
9121
9164
  }
9122
9165
  const filledContext = context;
9123
9166
  const instance = src_middleware(filledContext);
9124
- instance.getFilenameFromUrl = (url, extra)=>utils_getFilenameFromUrl(filledContext, url, extra);
9167
+ instance.getFilenameFromUrl = (url)=>utils_getFilenameFromUrl(filledContext, url);
9125
9168
  instance.waitUntilValid = (callback = noop)=>{
9126
9169
  utils_ready(filledContext, callback);
9127
9170
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rspack/dev-middleware",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.0",
4
4
  "description": "A development middleware for Rspack",
5
5
  "keywords": [
6
6
  "rspack",
@@ -23,6 +23,7 @@
23
23
  "build": "rslib --syntax es2023 && pnpm run build:types",
24
24
  "build:types": "tsc --declaration --emitDeclarationOnly --outDir types && prettier \"types/**/*.ts\" --write",
25
25
  "lint": "npm-run-all -l -p \"lint:**\"",
26
+ "lint:rslint": "rslint",
26
27
  "lint:prettier": "prettier --cache --list-different .",
27
28
  "lint:spelling": "cspell --cache --no-must-find-files --quiet \"**/*.*\"",
28
29
  "lint:types": "tsc --pretty --noEmit",
@@ -32,24 +33,21 @@
32
33
  "bump": "npx bumpp"
33
34
  },
34
35
  "devDependencies": {
35
- "@fastify/express": "^4.0.2",
36
36
  "@hapi/hapi": "^21.3.7",
37
37
  "@hono/node-server": "^1.12.0",
38
38
  "@rslib/core": "^0.20.0",
39
+ "@rslint/core": "^0.3.3",
39
40
  "@rspack/core": "2.0.0-beta.5",
40
41
  "@rstest/core": "0.9.2",
41
- "@types/connect": "^3.4.35",
42
42
  "@types/express": "^5.0.2",
43
43
  "@types/node": "^22.3.0",
44
44
  "@types/on-finished": "^2.3.4",
45
45
  "@types/range-parser": "^1.2.7",
46
- "connect": "^3.7.0",
46
+ "connect-next": "^4.0.0",
47
47
  "cspell": "^9.6.2",
48
48
  "deepmerge": "^4.2.2",
49
49
  "execa": "^5.1.1",
50
50
  "express": "^5.1.0",
51
- "fastify": "^5.2.1",
52
- "file-loader": "^6.2.0",
53
51
  "finalhandler": "^2.1.0",
54
52
  "hono": "^4.4.13",
55
53
  "koa": "^3.0.0",
@@ -61,7 +59,7 @@
61
59
  "range-parser": "^1.2.1",
62
60
  "router": "^2.2.0",
63
61
  "supertest": "^7.0.0",
64
- "typescript": "^5.3.3"
62
+ "typescript": "^6.0.2"
65
63
  },
66
64
  "peerDependencies": {
67
65
  "@rspack/core": "^2.0.0-0"
package/types/index.d.ts CHANGED
@@ -95,23 +95,22 @@
95
95
  * @property {"weak" | "strong"=} etag options to generate etag header
96
96
  * @property {boolean=} lastModified options to generate last modified header
97
97
  * @property {(boolean | number | string | { maxAge?: number, immutable?: boolean })=} cacheControl options to generate cache headers
98
- * @property {boolean=} cacheImmutable is cache immutable
98
+ * @property {boolean=} cacheImmutable enable immutable cache headers for immutable assets (defaults to true when omitted)
99
99
  */
100
100
  /**
101
101
  * @template {IncomingMessage} [RequestInternal=IncomingMessage]
102
102
  * @template {ServerResponse} [ResponseInternal=ServerResponse]
103
103
  * @callback Middleware
104
- * @param {RequestInternal} req
105
- * @param {ResponseInternal} res
106
- * @param {NextFunction} next
104
+ * @param {RequestInternal} req request
105
+ * @param {ResponseInternal} res response
106
+ * @param {NextFunction} next next function
107
107
  * @returns {Promise<void>}
108
108
  */
109
- /** @typedef {import("./utils/getFilenameFromUrl").Extra} Extra */
109
+ /** @typedef {import("./utils/getFilenameFromUrl.js").Extra} Extra */
110
110
  /**
111
111
  * @callback GetFilenameFromUrl
112
- * @param {string} url
113
- * @param {Extra=} extra
114
- * @returns {string | undefined}
112
+ * @param {string} url request URL
113
+ * @returns {{ filename: string, extra: Extra } | undefined} a filename with additional information, or `undefined` if nothing is found
115
114
  */
116
115
  /**
117
116
  * @callback WaitUntilValid
@@ -364,7 +363,7 @@ export type Options<
364
363
  )
365
364
  | undefined;
366
365
  /**
367
- * is cache immutable
366
+ * enable immutable cache headers for immutable assets (defaults to true when omitted)
368
367
  */
369
368
  cacheImmutable?: boolean | undefined;
370
369
  };
@@ -376,11 +375,13 @@ export type Middleware<
376
375
  res: ResponseInternal,
377
376
  next: NextFunction,
378
377
  ) => Promise<void>;
379
- export type Extra = import("./utils/getFilenameFromUrl").Extra;
380
- export type GetFilenameFromUrl = (
381
- url: string,
382
- extra?: Extra | undefined,
383
- ) => string | undefined;
378
+ export type Extra = import("./utils/getFilenameFromUrl.js").Extra;
379
+ export type GetFilenameFromUrl = (url: string) =>
380
+ | {
381
+ filename: string;
382
+ extra: Extra;
383
+ }
384
+ | undefined;
384
385
  export type WaitUntilValid = (callback: Callback) => any;
385
386
  export type Invalidate = (callback: Callback) => any;
386
387
  export type Close = (callback: (err: Error | null | undefined) => void) => any;
@@ -14,20 +14,27 @@ export type SendErrorOptions<
14
14
  * modify response data callback
15
15
  */
16
16
  modifyResponseData?:
17
- | import("./index").ModifyResponseData<Request, Response>
17
+ | import("./index.js").ModifyResponseData<Request, Response>
18
18
  | undefined;
19
19
  };
20
20
  export type NextFunction = import("./index.js").NextFunction;
21
21
  export type IncomingMessage = import("./index.js").IncomingMessage;
22
22
  export type ServerResponse = import("./index.js").ServerResponse;
23
23
  export type NormalizedHeaders = import("./index.js").NormalizedHeaders;
24
+ export type FilenameError =
25
+ import("./utils/getFilenameFromUrl.js").FilenameError;
26
+ export type Extra = import("./utils/getFilenameFromUrl.js").Extra;
24
27
  export type ReadStream = import("fs").ReadStream;
28
+ export type FilenameWithExtra = {
29
+ filename: string;
30
+ extra: Extra;
31
+ };
25
32
  /**
26
33
  * @template {IncomingMessage} Request
27
34
  * @template {ServerResponse} Response
28
35
  * @typedef {object} SendErrorOptions send error options
29
36
  * @property {Record<string, number | string | string[] | undefined>=} headers headers
30
- * @property {import("./index").ModifyResponseData<Request, Response>=} modifyResponseData modify response data callback
37
+ * @property {import("./index.js").ModifyResponseData<Request, Response>=} modifyResponseData modify response data callback
31
38
  */
32
39
  /**
33
40
  * @template {IncomingMessage} Request
@@ -1,7 +1,7 @@
1
- export type IncomingMessage = import("../index").IncomingMessage;
2
- export type ServerResponse = import("../index").ServerResponse;
3
- export type OutputFileSystem = import("../index").OutputFileSystem;
4
- export type EXPECTED_ANY = import("../index").EXPECTED_ANY;
1
+ export type IncomingMessage = import("../index.js").IncomingMessage;
2
+ export type ServerResponse = import("../index.js").ServerResponse;
3
+ export type OutputFileSystem = import("../index.js").OutputFileSystem;
4
+ export type EXPECTED_ANY = import("../index.js").EXPECTED_ANY;
5
5
  export type ExpectedIncomingMessage = {
6
6
  /**
7
7
  * get header extra method
@@ -15,10 +15,6 @@ export type ExpectedIncomingMessage = {
15
15
  * get URL extra method
16
16
  */
17
17
  getURL?: (() => string | undefined) | undefined;
18
- /**
19
- * an extra option for `fastify` (and `@fastify/express`) to get original URL
20
- */
21
- originalUrl?: string | undefined;
22
18
  };
23
19
  export type ExpectedServerResponse = {
24
20
  /**
@@ -115,16 +111,15 @@ export function getHeadersSent<
115
111
  export function getOutgoing<
116
112
  Response extends ServerResponse & ExpectedServerResponse,
117
113
  >(res: Response): Response;
118
- /** @typedef {import("../index").IncomingMessage} IncomingMessage */
119
- /** @typedef {import("../index").ServerResponse} ServerResponse */
120
- /** @typedef {import("../index").OutputFileSystem} OutputFileSystem */
121
- /** @typedef {import("../index").EXPECTED_ANY} EXPECTED_ANY */
114
+ /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */
115
+ /** @typedef {import("../index.js").ServerResponse} ServerResponse */
116
+ /** @typedef {import("../index.js").OutputFileSystem} OutputFileSystem */
117
+ /** @typedef {import("../index.js").EXPECTED_ANY} EXPECTED_ANY */
122
118
  /**
123
119
  * @typedef {object} ExpectedIncomingMessage
124
120
  * @property {((name: string) => string | string[] | undefined)=} getHeader get header extra method
125
121
  * @property {(() => string | undefined)=} getMethod get method extra method
126
122
  * @property {(() => string | undefined)=} getURL get URL extra method
127
- * @property {string=} originalUrl an extra option for `fastify` (and `@fastify/express`) to get original URL
128
123
  */
129
124
  /**
130
125
  * @typedef {object} ExpectedServerResponse
@@ -1,25 +1,31 @@
1
1
  export default getFilenameFromUrl;
2
2
  export type IncomingMessage = import("../index.js").IncomingMessage;
3
+ export type OutputFileSystem = import("../index.js").OutputFileSystem;
3
4
  export type ServerResponse = import("../index.js").ServerResponse;
5
+ export type FSStats = import("fs").Stats;
6
+ export type FilenameWithExtra = {
7
+ filename: string;
8
+ extra: Extra;
9
+ };
4
10
  export type Extra = {
5
11
  /**
6
12
  * stats
7
13
  */
8
- stats?: import("fs").Stats | undefined;
9
- /**
10
- * error code
11
- */
12
- errorCode?: number | undefined;
14
+ stats: FSStats;
13
15
  /**
14
16
  * true when immutable, otherwise false
15
17
  */
16
18
  immutable?: boolean | undefined;
19
+ /**
20
+ * output file system
21
+ */
22
+ outputFileSystem: OutputFileSystem;
17
23
  };
18
24
  /**
19
25
  * @typedef {object} Extra
20
- * @property {import("fs").Stats=} stats stats
21
- * @property {number=} errorCode error code
26
+ * @property {FSStats} stats stats
22
27
  * @property {boolean=} immutable true when immutable, otherwise false
28
+ * @property {OutputFileSystem} outputFileSystem output file system
23
29
  */
24
30
  /**
25
31
  * decodeURIComponent.
@@ -28,13 +34,20 @@ export type Extra = {
28
34
  * @param {string} input
29
35
  * @returns {string}
30
36
  */
37
+ export class FilenameError extends Error {
38
+ /**
39
+ * @param {string} message message
40
+ * @param {number=} code error code
41
+ */
42
+ constructor(message: string, code?: number | undefined);
43
+ statusCode: number | undefined;
44
+ }
31
45
  /**
32
46
  * @template {IncomingMessage} Request
33
47
  * @template {ServerResponse} Response
34
48
  * @param {import("../index.js").FilledContext<Request, Response>} context context
35
49
  * @param {string} url url
36
- * @param {Extra=} extra extra
37
- * @returns {string | undefined} filename
50
+ * @returns {FilenameWithExtra | undefined} result of get filename from url
38
51
  */
39
52
  declare function getFilenameFromUrl<
40
53
  Request extends IncomingMessage,
@@ -42,5 +55,4 @@ declare function getFilenameFromUrl<
42
55
  >(
43
56
  context: import("../index.js").FilledContext<Request, Response>,
44
57
  url: string,
45
- extra?: Extra | undefined,
46
- ): string | undefined;
58
+ ): FilenameWithExtra | undefined;
@@ -5,6 +5,7 @@ export type MultiStats = import("@rspack/core").MultiStats;
5
5
  export type Asset = import("@rspack/core").Asset;
6
6
  export type DevServerOption = import("../index.js").DevServerOption;
7
7
  export type IncomingMessage = import("../index.js").IncomingMessage;
8
+ export type OutputFileSystem = import("../index.js").OutputFileSystem;
8
9
  export type ServerResponse = import("../index.js").ServerResponse;
9
10
  /** @typedef {import("@rspack/core").Compiler} Compiler */
10
11
  /** @typedef {import("@rspack/core").Stats} Stats */
@@ -12,12 +13,13 @@ export type ServerResponse = import("../index.js").ServerResponse;
12
13
  /** @typedef {import("@rspack/core").Asset} Asset */
13
14
  /** @typedef {import("../index.js").DevServerOption} DevServerOption */
14
15
  /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */
16
+ /** @typedef {import("../index.js").OutputFileSystem} OutputFileSystem */
15
17
  /** @typedef {import("../index.js").ServerResponse} ServerResponse */
16
18
  /**
17
19
  * @template {IncomingMessage} Request
18
20
  * @template {ServerResponse} Response
19
21
  * @param {import("../index.js").FilledContext<Request, Response>} context context
20
- * @returns {{ outputPath: string, publicPath: string, assetsInfo: Map<string, Asset["info"]> | undefined }[]} paths
22
+ * @returns {{ outputPath: string, outputFileSystem: OutputFileSystem, publicPath: string, assetsInfo: Map<string, Asset["info"]> | undefined }[]} paths
21
23
  */
22
24
  declare function getPaths<
23
25
  Request extends IncomingMessage,
@@ -26,6 +28,7 @@ declare function getPaths<
26
28
  context: import("../index.js").FilledContext<Request, Response>,
27
29
  ): {
28
30
  outputPath: string;
31
+ outputFileSystem: OutputFileSystem;
29
32
  publicPath: string;
30
33
  assetsInfo: Map<string, Asset["info"]> | undefined;
31
34
  }[];
@@ -1,6 +1,6 @@
1
1
  export default memorize;
2
2
  export type FunctionReturning<T> = (...args: EXPECTED_ANY) => T;
3
- export type EXPECTED_ANY = import("../index").EXPECTED_ANY;
3
+ export type EXPECTED_ANY = import("../index.js").EXPECTED_ANY;
4
4
  /**
5
5
  * @template T
6
6
  * @typedef {(...args: EXPECTED_ANY) => T} FunctionReturning