From 3ad3795df4ebde4814b01f8d0ea1ebd908fa290d Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Mon, 19 Jan 2026 10:26:01 -0500 Subject: [PATCH 01/13] chore: remove colorette dependency and update minimum webpack version to 5.101.0 (#2238) --- .cspell.json | 1 - package-lock.json | 4 ++-- package.json | 3 +-- src/utils/setupHooks.js | 16 ++-------------- test/utils/setupHooks.test.js | 16 ++++++++++++++++ 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/.cspell.json b/.cspell.json index 7722ca977..fb32a57f0 100644 --- a/.cspell.json +++ b/.cspell.json @@ -3,7 +3,6 @@ "language": "en,en-gb", "words": [ "memfs", - "colorette", "noextension", "fullhash", "execa", diff --git a/package-lock.json b/package-lock.json index 5b2ed65cb..c34544e7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "7.4.5", "license": "MIT", "dependencies": { - "colorette": "^2.0.10", "memfs": "^4.43.1", "mime-types": "^3.0.1", "on-finished": "^2.4.1", @@ -64,7 +63,7 @@ "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^5.0.0" + "webpack": "^5.101.0" }, "peerDependenciesMeta": { "webpack": { @@ -7753,6 +7752,7 @@ "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, "license": "MIT" }, "node_modules/combined-stream": { diff --git a/package.json b/package.json index 4ec83b262..0d0b7d3b4 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "release": "standard-version" }, "dependencies": { - "colorette": "^2.0.10", "memfs": "^4.43.1", "mime-types": "^3.0.1", "on-finished": "^2.4.1", @@ -95,7 +94,7 @@ "webpack": "^5.101.0" }, "peerDependencies": { - "webpack": "^5.0.0" + "webpack": "^5.101.0" }, "peerDependenciesMeta": { "webpack": { diff --git a/src/utils/setupHooks.js b/src/utils/setupHooks.js index f9b14c8de..50c55a0f8 100644 --- a/src/utils/setupHooks.js +++ b/src/utils/setupHooks.js @@ -113,14 +113,8 @@ function setupHooks(context) { /** @type {MultiCompiler} */ (compiler).compilers; - // TODO remove `colorette` and set minimum supported webpack version is `5.101.0` childStatsOptions.colors = - typeof firstCompiler.webpack !== "undefined" && - typeof firstCompiler.webpack.cli !== "undefined" && - typeof firstCompiler.webpack.cli.isColorSupported === - "function" - ? firstCompiler.webpack.cli.isColorSupported() - : require("colorette").isColorSupported; + firstCompiler.webpack.cli.isColorSupported(); } return childStatsOptions; @@ -133,13 +127,7 @@ function setupHooks(context) { if (typeof statsOptions.colors === "undefined") { const { compiler } = /** @type {{ compiler: Compiler }} */ (context); - // TODO remove `colorette` and set minimum supported webpack version is `5.101.0` - statsOptions.colors = - typeof compiler.webpack !== "undefined" && - typeof compiler.webpack.cli !== "undefined" && - typeof compiler.webpack.cli.isColorSupported === "function" - ? compiler.webpack.cli.isColorSupported() - : require("colorette").isColorSupported; + statsOptions.colors = compiler.webpack.cli.isColorSupported(); } } diff --git a/test/utils/setupHooks.test.js b/test/utils/setupHooks.test.js index 8aaaa263f..92f437aef 100644 --- a/test/utils/setupHooks.test.js +++ b/test/utils/setupHooks.test.js @@ -12,6 +12,7 @@ describe("setupHooks", () => { const loggerInfo = jest.fn(); const loggerWarn = jest.fn(); const loggerError = jest.fn(); + const colorSupport = jest.fn(); let nextTick; const cb1 = jest.fn(); @@ -22,6 +23,11 @@ describe("setupHooks", () => { context = { options: {}, compiler: { + webpack: { + cli: { + isColorSupported: colorSupport, + }, + }, hooks: { watchRun: { tap: watchRunHook, @@ -126,12 +132,22 @@ describe("setupHooks", () => { it("handles multi compiler", () => { context.compiler.compilers = [ { + webpack: { + cli: { + isColorSupported: colorSupport, + }, + }, options: { name: "comp1", stats: {}, }, }, { + webpack: { + cli: { + isColorSupported: colorSupport, + }, + }, options: { name: "comp2", stats: {}, From bf4167d8ca8bb47662545c8f0139d7fef839dbfc Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 20 Jan 2026 06:36:41 -0500 Subject: [PATCH 02/13] feat!: drop support for node <20.9 (#2248) --- .github/workflows/nodejs.yml | 3 ++- package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 1d61d2f5e..878bf751e 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -64,9 +64,10 @@ jobs: name: Test - ${{ matrix.os }} - Node v${{ matrix.node-version }}, Webpack ${{ matrix.webpack-version }} strategy: + fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - node-version: [18.x, 20.x, 22.x, 24.x] + node-version: [20.x, 22.x, 24.x] webpack-version: [latest] runs-on: ${{ matrix.os }} diff --git a/package.json b/package.json index 0d0b7d3b4..e5db22d89 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,6 @@ } }, "engines": { - "node": ">= 18.12.0" + "node": ">= 20.9.0" } } From 327d5bf2c2b76d68f2ad8b9b96a840530f252b33 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Mon, 26 Jan 2026 13:03:19 -0500 Subject: [PATCH 03/13] feat: enable cacheImmutable by default (#2247) * feat: enable cacheImmutable by default * fix: thoses files shouldn't immutable * fix: update cache control logic becauses undefined !== false (true) * test: add tests for cacheControl option behavior with cacheImmutable set to false * refactor: normalize cacheControl handling in response headers * refactor: simplify maxAge calculation in cacheControl handling --- src/middleware.js | 58 ++++--- test/middleware.test.js | 329 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 354 insertions(+), 33 deletions(-) diff --git a/src/middleware.js b/src/middleware.js index 7b38ea99b..ba47666a9 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -571,38 +571,34 @@ function wrapper(context) { } if (!getResponseHeader(res, "Cache-Control")) { - // TODO enable the `cacheImmutable` by default for the next major release - const cacheControl = - context.options.cacheImmutable && extra.immutable - ? { immutable: true } - : context.options.cacheControl; - - if (cacheControl) { - let cacheControlValue; - - if (typeof cacheControl === "boolean") { - cacheControlValue = "public, max-age=31536000"; - } else if (typeof cacheControl === "number") { - const maxAge = Math.floor( - Math.min(Math.max(0, cacheControl), MAX_MAX_AGE) / 1000, - ); + const hasCacheImmutable = + context.options.cacheImmutable === undefined + ? true + : context.options.cacheImmutable; + + let { cacheControl } = context.options; + + // Normalize cacheControl to object + if (typeof cacheControl === "string") { + setResponseHeader(res, "Cache-Control", cacheControl); + } else if (hasCacheImmutable && extra.immutable) { + cacheControl = { immutable: true }; + } else if (typeof cacheControl === "boolean") { + cacheControl = { maxAge: MAX_MAX_AGE }; + } else if (typeof cacheControl === "number") { + cacheControl = { maxAge: cacheControl }; + } - cacheControlValue = `public, max-age=${maxAge}`; - } else if (typeof cacheControl === "string") { - cacheControlValue = cacheControl; - } else { - const maxAge = cacheControl.maxAge - ? Math.floor( - Math.min(Math.max(0, cacheControl.maxAge), MAX_MAX_AGE) / - 1000, - ) - : MAX_MAX_AGE / 1000; - - cacheControlValue = `public, max-age=${maxAge}`; - - if (cacheControl.immutable) { - cacheControlValue += ", immutable"; - } + if (cacheControl && typeof cacheControl === "object") { + const maxAge = + cacheControl.maxAge !== undefined + ? Math.min(Math.max(0, cacheControl.maxAge), MAX_MAX_AGE) + : MAX_MAX_AGE; + + let cacheControlValue = `public, max-age=${Math.floor(maxAge / 1000)}`; + + if (cacheControl.immutable && hasCacheImmutable) { + cacheControlValue += ", immutable"; } setResponseHeader(res, "Cache-Control", cacheControlValue); diff --git a/test/middleware.test.js b/test/middleware.test.js index 8d676b43d..db400db40 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -5964,7 +5964,7 @@ describe.each([ await close(server, instance); }); - it('should return the "200" code for the "GET" request to the bundle file and don\'t generate `Cache-Control` header', async () => { + it('should return the "200" code for the "GET" request to the bundle file and generate `Cache-Control` header', async () => { const response = await req.get("/bundle.js"); expect(response.statusCode).toBe(200); @@ -6129,7 +6129,6 @@ describe.each([ name, framework, compiler, - { cacheImmutable: true }, ); }); @@ -6235,6 +6234,332 @@ describe.each([ ); }); }); + + describe("should not generate `Cache-Control` header for immutable assets when cacheImmutable is false", () => { + beforeEach(async () => { + const compiler = getCompiler({ + ...webpackConfigImmutable, + output: { + path: path.resolve(__dirname, "./outputs/basic"), + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { cacheImmutable: false }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and don\'t generate `Cache-Control` header', async () => { + const response = await req.get("/main.js"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeUndefined(); + }); + + it('should return the "200" code for the "GET" request to the immutable asset and don\'t generate `Cache-Control` header', async () => { + const response = await req.get("/6076fc274f403ebb2d09.svg"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeUndefined(); + }); + }); + + describe("should use cacheControl option when cacheImmutable is false even for immutable assets", () => { + beforeEach(async () => { + const compiler = getCompiler({ + ...webpackConfigImmutable, + output: { + path: path.resolve(__dirname, "./outputs/basic"), + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { cacheImmutable: false, cacheControl: 1000000 }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and generate `Cache-Control` header from cacheControl option', async () => { + const response = await req.get("/main.js"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=1000", + ); + }); + + it('should return the "200" code for the "GET" request to the immutable asset and generate `Cache-Control` header from cacheControl option (not immutable)', async () => { + const response = await req.get("/6076fc274f403ebb2d09.svg"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=1000", + ); + }); + }); + + describe("should use cacheControl string option when cacheImmutable is false", () => { + beforeEach(async () => { + const compiler = getCompiler({ + ...webpackConfigImmutable, + output: { + path: path.resolve(__dirname, "./outputs/basic"), + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { cacheImmutable: false, cacheControl: "max-age=500" }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and generate `Cache-Control` header from cacheControl string option without immutable', async () => { + const response = await req.get("/main.js"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe("max-age=500"); + }); + + it('should return the "200" code for the "GET" request to the immutable asset and generate `Cache-Control` header from cacheControl string option without immutable', async () => { + const response = await req.get("/6076fc274f403ebb2d09.svg"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe("max-age=500"); + }); + }); + + describe("should use cacheControl object option when cacheImmutable is false", () => { + beforeEach(async () => { + const compiler = getCompiler({ + ...webpackConfigImmutable, + output: { + path: path.resolve(__dirname, "./outputs/basic"), + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { cacheImmutable: false, cacheControl: { maxAge: 2000000 } }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and generate `Cache-Control` header from cacheControl object option without immutable', async () => { + const response = await req.get("/main.js"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=2000", + ); + }); + + it('should return the "200" code for the "GET" request to the immutable asset and generate `Cache-Control` header from cacheControl object option without immutable', async () => { + const response = await req.get("/6076fc274f403ebb2d09.svg"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=2000", + ); + }); + }); + + describe("should use cacheControl object option (with only immutable: true) when cacheImmutable is false, and not add 'immutable' to Cache-Control header", () => { + beforeEach(async () => { + const compiler = getCompiler({ + ...webpackConfigImmutable, + output: { + path: path.resolve(__dirname, "./outputs/basic"), + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { cacheImmutable: false, cacheControl: { immutable: true } }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and generate `Cache-Control` header from cacheControl object option without immutable', async () => { + const response = await req.get("/main.js"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=31536000", + ); + }); + + it('should return the "200" code for the "GET" request to the immutable asset and generate `Cache-Control` header from cacheControl object option without immutable', async () => { + const response = await req.get("/6076fc274f403ebb2d09.svg"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=31536000", + ); + }); + }); + + describe("should use cacheControl object option with explicit immutable false", () => { + beforeEach(async () => { + const compiler = getCompiler({ + ...webpackConfigImmutable, + output: { + path: path.resolve(__dirname, "./outputs/basic"), + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { cacheControl: { maxAge: 3000000, immutable: false } }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and generate `Cache-Control` header without immutable when explicitly set to false', async () => { + const response = await req.get("/main.js"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=3000", + ); + }); + + it('should return the "200" code for the "GET" request to the immutable asset and generate `Cache-Control` header without immutable when explicitly set to false', async () => { + const response = await req.get("/6076fc274f403ebb2d09.svg"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=31536000, immutable", + ); + }); + }); + + describe("should use cacheControl boolean option when cacheImmutable is false", () => { + beforeEach(async () => { + const compiler = getCompiler({ + ...webpackConfigImmutable, + output: { + path: path.resolve(__dirname, "./outputs/basic"), + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { cacheImmutable: false, cacheControl: true }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and generate `Cache-Control` header from cacheControl boolean option without immutable', async () => { + const response = await req.get("/main.js"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=31536000", + ); + }); + + it('should return the "200" code for the "GET" request to the immutable asset and generate `Cache-Control` header from cacheControl boolean option without immutable', async () => { + const response = await req.get("/6076fc274f403ebb2d09.svg"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=31536000", + ); + }); + }); + + describe("should use cacheControl number option when cacheImmutable is false without immutable", () => { + beforeEach(async () => { + const compiler = getCompiler({ + ...webpackConfigImmutable, + output: { + path: path.resolve(__dirname, "./outputs/basic"), + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { cacheImmutable: false, cacheControl: 5000000 }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and generate `Cache-Control` header from cacheControl number option without immutable', async () => { + const response = await req.get("/main.js"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=5000", + ); + }); + + it('should return the "200" code for the "GET" request to the immutable asset and generate `Cache-Control` header from cacheControl number option without immutable', async () => { + const response = await req.get("/6076fc274f403ebb2d09.svg"); + + expect(response.statusCode).toBe(200); + expect(response.headers["cache-control"]).toBeDefined(); + expect(response.headers["cache-control"]).toBe( + "public, max-age=5000", + ); + }); + }); }); }); }); From cced49038d30978a3cd8ed58b9d9fbda3eb7138d Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:39:34 +0300 Subject: [PATCH 04/13] refactor: logic for cacheControl and cacheImmutable (#2254) --- README.md | 2 +- src/middleware.js | 38 +++++++++++++++++++------------------- test/middleware.test.js | 6 +++--- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index ce8a7feda..4a7cfbbf2 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ See [below](#other-servers) for an example of use with fastify. | **[`etag`](#tag)** | `boolean\| "weak"\| "strong"` | `undefined` | Enable or disable etag generation. | | **[`lastModified`](#lastmodified)** | `boolean` | `undefined` | Enable or disable `Last-Modified` header. Uses the file system's last modified value. | | **[`cacheControl`](#cachecontrol)** | `boolean\|number\|string\|Object` | `undefined` | Enable or disable setting `Cache-Control` response header. | -| **[`cacheImmutable`](#cacheimmutable)** | `boolean\` | `undefined` | Enable or disable setting `Cache-Control: public, max-age=31536000, immutable` response header for immutable assets. | +| **[`cacheImmutable`](#cacheimmutable)** | `boolean` | `undefined` | Enable or disable setting `Cache-Control: public, max-age=31536000, immutable` response header for immutable assets. | | **[`publicPath`](#publicpath)** | `string` | `undefined` | The public path that the middleware is bound to. | | **[`stats`](#stats)** | `boolean\|string\|Object` | `stats` (from a configuration) | Stats options object or preset name. | | **[`serverSideRender`](#serversiderender)** | `boolean` | `undefined` | Instructs the module to enable or disable the server-side rendering mode. | diff --git a/src/middleware.js b/src/middleware.js index ba47666a9..3441f3285 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -571,36 +571,36 @@ function wrapper(context) { } if (!getResponseHeader(res, "Cache-Control")) { - const hasCacheImmutable = - context.options.cacheImmutable === undefined - ? true - : context.options.cacheImmutable; - - let { cacheControl } = context.options; - - // Normalize cacheControl to object - if (typeof cacheControl === "string") { - setResponseHeader(res, "Cache-Control", cacheControl); - } else if (hasCacheImmutable && extra.immutable) { - cacheControl = { immutable: true }; + const { cacheControl, cacheImmutable } = context.options; + + let cacheControlValue; + + if ( + (cacheImmutable === undefined || cacheImmutable) && + extra.immutable + ) { + cacheControlValue = `public, max-age=${Math.floor(MAX_MAX_AGE / 1000)}, immutable`; } else if (typeof cacheControl === "boolean") { - cacheControl = { maxAge: MAX_MAX_AGE }; + cacheControlValue = `public, max-age=${Math.floor(MAX_MAX_AGE / 1000)}`; } else if (typeof cacheControl === "number") { - cacheControl = { maxAge: cacheControl }; - } - - if (cacheControl && typeof cacheControl === "object") { + const maxAge = Math.min(Math.max(0, cacheControl), MAX_MAX_AGE); + cacheControlValue = `public, max-age=${Math.floor(maxAge / 1000)}`; + } else if (typeof cacheControl === "string") { + cacheControlValue = cacheControl; + } else if (cacheControl) { const maxAge = cacheControl.maxAge !== undefined ? Math.min(Math.max(0, cacheControl.maxAge), MAX_MAX_AGE) : MAX_MAX_AGE; - let cacheControlValue = `public, max-age=${Math.floor(maxAge / 1000)}`; + cacheControlValue = `public, max-age=${Math.floor(maxAge / 1000)}`; - if (cacheControl.immutable && hasCacheImmutable) { + if (cacheControl.immutable) { cacheControlValue += ", immutable"; } + } + if (cacheControlValue) { setResponseHeader(res, "Cache-Control", cacheControlValue); } } diff --git a/test/middleware.test.js b/test/middleware.test.js index db400db40..740546689 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -6393,7 +6393,7 @@ describe.each([ }); }); - describe("should use cacheControl object option (with only immutable: true) when cacheImmutable is false, and not add 'immutable' to Cache-Control header", () => { + describe("should use cacheControl object option (with only immutable: true) when cacheImmutable is false, and add 'immutable' to Cache-Control header", () => { beforeEach(async () => { const compiler = getCompiler({ ...webpackConfigImmutable, @@ -6420,7 +6420,7 @@ describe.each([ expect(response.statusCode).toBe(200); expect(response.headers["cache-control"]).toBeDefined(); expect(response.headers["cache-control"]).toBe( - "public, max-age=31536000", + "public, max-age=31536000, immutable", ); }); @@ -6430,7 +6430,7 @@ describe.each([ expect(response.statusCode).toBe(200); expect(response.headers["cache-control"]).toBeDefined(); expect(response.headers["cache-control"]).toBe( - "public, max-age=31536000", + "public, max-age=31536000, immutable", ); }); }); From d4e111822d4ab9aafa6d79d3776c0aa9b81294df Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 27 Jan 2026 14:26:15 -0500 Subject: [PATCH 05/13] feat!: update `getFilenameFromUrl` to throw errors and return `undefined` when not found --- .cspell.json | 3 +- README.md | 13 +++++- src/index.js | 14 +++--- src/middleware.js | 46 ++++++++++++-------- src/utils/getFilenameFromUrl.js | 67 ++++++++++++++++------------- test/middleware.test.js | 48 ++++++++++++--------- types/index.d.ts | 21 ++++----- types/middleware.d.ts | 4 ++ types/utils/getFilenameFromUrl.d.ts | 52 ++++++++++++---------- 9 files changed, 157 insertions(+), 111 deletions(-) diff --git a/.cspell.json b/.cspell.json index fb32a57f0..ecd7d2e23 100644 --- a/.cspell.json +++ b/.cspell.json @@ -23,7 +23,8 @@ "cachable", "finalhandler", "hono", - "rspack" + "rspack", + "malformed" ], "ignorePaths": [ "CHANGELOG.md", diff --git a/README.md b/README.md index 4a7cfbbf2..113fdc2e5 100644 --- a/README.md +++ b/README.md @@ -459,7 +459,18 @@ const app = new express(); app.use(instance); instance.waitUntilValid(() => { - const filename = instance.getFilenameFromUrl("/bundle.js"); + let resolver; + + try { + resolved = instance.getFilenameFromUrl("/bundle.js"); + } catch (err) { + console.log(`Error: ${err}`); + } + + if (!resolved) { + console.log("Not found"); + return; + } console.log(`Filename is ${filename}`); }); diff --git a/src/index.js b/src/index.js index 8cc01b439..6c7e7a05c 100644 --- a/src/index.js +++ b/src/index.js @@ -131,9 +131,9 @@ const noop = () => {}; * @template {IncomingMessage} [RequestInternal=IncomingMessage] * @template {ServerResponse} [ResponseInternal=ServerResponse] * @callback Middleware - * @param {RequestInternal} req - * @param {ResponseInternal} res - * @param {NextFunction} next + * @param {RequestInternal} req request + * @param {ResponseInternal} res response + * @param {NextFunction} next next function * @returns {Promise} */ @@ -141,9 +141,8 @@ const noop = () => {}; /** * @callback GetFilenameFromUrl - * @param {string} url - * @param {Extra=} extra - * @returns {string | undefined} + * @param {string} url request URL + * @returns {{ filename: string, extra: Extra } | undefined} a filename with additional information, or `undefined` if nothing is found */ /** @@ -278,8 +277,7 @@ function wdm(compiler, options = {}) { (middleware(filledContext)); // API - instance.getFilenameFromUrl = (url, extra) => - getFilenameFromUrl(filledContext, url, extra); + instance.getFilenameFromUrl = (url) => getFilenameFromUrl(filledContext, url); instance.waitUntilValid = (callback = noop) => { ready(filledContext, callback); diff --git a/src/middleware.js b/src/middleware.js index 3441f3285..078bbead7 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -31,6 +31,8 @@ const ready = require("./utils/ready"); /** @typedef {import("./index.js").IncomingMessage} IncomingMessage */ /** @typedef {import("./index.js").ServerResponse} ServerResponse */ /** @typedef {import("./index.js").NormalizedHeaders} NormalizedHeaders */ +/** @typedef {import("./utils/getFilenameFromUrl.js").FilenameError} FilenameError */ +/** @typedef {import("./utils/getFilenameFromUrl.js").Extra} Extra */ /** @typedef {import("fs").ReadStream} ReadStream */ const BYTES_RANGE_REGEXP = /^ *bytes/i; @@ -498,22 +500,29 @@ function wrapper(context) { */ async function processRequest() { // Pipe and SendFile - /** @type {import("./utils/getFilenameFromUrl").Extra} */ - const extra = {}; - const filename = getFilenameFromUrl( - context, - /** @type {string} */ (getRequestURL(req)), - extra, - ); - - if (extra.errorCode) { - if (extra.errorCode === 403) { - context.logger.error(`Malicious path "${filename}".`); + /** @type {{ filename: string, extra: Extra } | undefined} */ + let resolved; + + const requestUrl = /** @type {string} */ (getRequestURL(req)); + + try { + resolved = getFilenameFromUrl(context, requestUrl); + } catch (err) { + // Fallback to 403 for unknown errors + const errorCode = + typeof err === "object" && + err !== null && + typeof (/** @type {FilenameError} */ (err).code) !== "undefined" + ? /** @type {FilenameError} */ (err).code + : 403; + + if (errorCode === 403) { + context.logger.error(`Malicious path "${requestUrl}".`); } await sendError( - extra.errorCode === 400 ? "Bad Request" : "Forbidden", - extra.errorCode, + errorCode === 400 ? "Bad Request" : "Forbidden", + errorCode, { modifyResponseData: context.options.modifyResponseData, }, @@ -521,7 +530,7 @@ function wrapper(context) { return; } - if (!filename) { + if (!resolved) { await goNext(); return; } @@ -531,7 +540,8 @@ function wrapper(context) { return; } - const { size } = /** @type {import("fs").Stats} */ (extra.stats); + const { extra, filename } = resolved; + const { size } = extra.stats; let len = size; let offset = 0; @@ -609,9 +619,7 @@ function wrapper(context) { context.options.lastModified && !getResponseHeader(res, "Last-Modified") ) { - const modified = - /** @type {import("fs").Stats} */ - (extra.stats).mtime.toUTCString(); + const modified = extra.stats.mtime.toUTCString(); setResponseHeader(res, "Last-Modified", modified); } @@ -667,7 +675,7 @@ function wrapper(context) { const result = await getETag()( isStrongETag ? /** @type {Buffer | ReadStream} */ (bufferOrStream) - : /** @type {import("fs").Stats} */ (extra.stats), + : extra.stats, ); // Because we already read stream, we can cache buffer to avoid extra read from fs diff --git a/src/utils/getFilenameFromUrl.js b/src/utils/getFilenameFromUrl.js index 44c7b7bd6..d54bd749a 100644 --- a/src/utils/getFilenameFromUrl.js +++ b/src/utils/getFilenameFromUrl.js @@ -1,7 +1,5 @@ const path = require("node:path"); const querystring = require("node:querystring"); -// eslint-disable-next-line n/no-deprecated-api -const { parse } = require("node:url"); const getPaths = require("./getPaths"); const memorize = require("./memorize"); @@ -17,20 +15,18 @@ function decode(input) { return querystring.unescape(input); } -const memoizedParse = memorize(parse, undefined, (value) => { - if (value.pathname) { - value.pathname = decode(value.pathname); - } +const memoizedParse = memorize((url) => { + const urlObject = new URL(url, "http://localhost"); - return value; -}); + // We can't change pathname in URL object directly because don't decode correctly + return { ...urlObject, pathname: decode(urlObject.pathname) }; +}, undefined); const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/; /** * @typedef {object} Extra - * @property {import("fs").Stats=} stats stats - * @property {number=} errorCode error code + * @property {import("fs").Stats} stats stats * @property {boolean=} immutable true when immutable, otherwise false */ @@ -42,43 +38,55 @@ const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/; * @returns {string} */ -// TODO refactor me in the next major release, this function should return `{ filename, stats, error }` +class FilenameError extends Error { + /** + * @param {string} message message + * @param {number=} code error code + */ + constructor(message, code) { + super(message); + this.name = "FilenameError"; + this.code = code; + } +} + // TODO fix redirect logic when `/` at the end, like https://github.com/pillarjs/send/blob/master/index.js#L586 /** * @template {IncomingMessage} Request * @template {ServerResponse} Response * @param {import("../index.js").FilledContext} context context * @param {string} url url - * @param {Extra=} extra extra - * @returns {string | undefined} filename + * @returns {{ filename: string, extra: Extra } | undefined} result of get filename from url */ -function getFilenameFromUrl(context, url, extra = {}) { - const { options } = context; - const paths = getPaths(context); +function getFilenameFromUrl(context, url) { + /** @type {URL} */ + let urlObject; /** @type {string | undefined} */ let foundFilename; - /** @type {import("node:url").Url} */ - let urlObject; try { // The `url` property of the `request` is contains only `pathname`, `search` and `hash` - urlObject = memoizedParse(url, false, true); + urlObject = memoizedParse(url); } catch { return; } + const { options } = context; + const paths = getPaths(context); + + /** @type {Extra} */ + const extra = {}; + for (const { publicPath, outputPath, assetsInfo } of paths) { /** @type {string | undefined} */ let filename; - /** @type {import("node:url").Url} */ + /** @type {URL} */ let publicPathObject; try { publicPathObject = memoizedParse( publicPath !== "auto" && publicPath ? publicPath : "/", - false, - true, ); } catch { continue; @@ -94,16 +102,12 @@ function getFilenameFromUrl(context, url, extra = {}) { ) { // Null byte(s) if (pathname.includes("\0")) { - extra.errorCode = 400; - - return; + throw new FilenameError("Bad Request", 400); } // ".." is malicious if (UP_PATH_REGEXP.test(path.normalize(`./${pathname}`))) { - extra.errorCode = 403; - - return; + throw new FilenameError("Forbidden", 403); } // Strip the `pathname` property from the `publicPath` option from the start of requested url @@ -161,7 +165,12 @@ function getFilenameFromUrl(context, url, extra = {}) { } } - return foundFilename; + if (!foundFilename) { + return; + } + + return { filename: foundFilename, extra }; } module.exports = getFilenameFromUrl; +module.exports.FilenameError = FilenameError; diff --git a/test/middleware.test.js b/test/middleware.test.js index 740546689..28723a5f2 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -573,16 +573,16 @@ describe.each([ it("should work", (done) => { instance.waitUntilValid(() => { - expect(instance.getFilenameFromUrl("/bundle.js")).toBe( + expect(instance.getFilenameFromUrl("/bundle.js").filename).toBe( path.join(webpackConfig.output.path, "/bundle.js"), ); - expect(instance.getFilenameFromUrl("/")).toBe( + expect(instance.getFilenameFromUrl("/").filename).toBe( path.join(webpackConfig.output.path, "/index.html"), ); - expect(instance.getFilenameFromUrl("/index.html")).toBe( + expect(instance.getFilenameFromUrl("/index.html").filename).toBe( path.join(webpackConfig.output.path, "/index.html"), ); - expect(instance.getFilenameFromUrl("/svg.svg")).toBe( + expect(instance.getFilenameFromUrl("/svg.svg").filename).toBe( path.join(webpackConfig.output.path, "/svg.svg"), ); expect( @@ -617,15 +617,15 @@ describe.each([ it("should work", (done) => { instance.waitUntilValid(() => { - expect(instance.getFilenameFromUrl("/bundle.js")).toBe( + expect(instance.getFilenameFromUrl("/bundle.js").filename).toBe( path.join(webpackConfig.output.path, "/bundle.js"), ); expect(instance.getFilenameFromUrl("/")).toBeUndefined(); - expect(instance.getFilenameFromUrl("/index.html")).toBe( + expect(instance.getFilenameFromUrl("/index.html").filename).toBe( path.join(webpackConfig.output.path, "/index.html"), ); - expect(instance.getFilenameFromUrl("/svg.svg")).toBe( + expect(instance.getFilenameFromUrl("/svg.svg").filename).toBe( path.join(webpackConfig.output.path, "/svg.svg"), ); expect( @@ -658,19 +658,23 @@ describe.each([ it("should work", (done) => { instance.waitUntilValid(() => { expect( - instance.getFilenameFromUrl("/public/path/bundle.js"), + instance.getFilenameFromUrl("/public/path/bundle.js").filename, ).toBe( path.join(webpackPublicPathConfig.output.path, "/bundle.js"), ); - expect(instance.getFilenameFromUrl("/public/path/")).toBe( + expect( + instance.getFilenameFromUrl("/public/path/").filename, + ).toBe( path.join(webpackPublicPathConfig.output.path, "/index.html"), ); expect( - instance.getFilenameFromUrl("/public/path/index.html"), + instance.getFilenameFromUrl("/public/path/index.html").filename, ).toBe( path.join(webpackPublicPathConfig.output.path, "/index.html"), ); - expect(instance.getFilenameFromUrl("/public/path/svg.svg")).toBe( + expect( + instance.getFilenameFromUrl("/public/path/svg.svg").filename, + ).toBe( path.join(webpackPublicPathConfig.output.path, "/svg.svg"), ); @@ -704,20 +708,22 @@ describe.each([ it("should work", (done) => { instance.waitUntilValid(() => { - expect(instance.getFilenameFromUrl("/static-one/bundle.js")).toBe( + expect( + instance.getFilenameFromUrl("/static-one/bundle.js").filename, + ).toBe( path.join(webpackMultiConfig[0].output.path, "/bundle.js"), ); - expect(instance.getFilenameFromUrl("/static-one/")).toBe( + expect(instance.getFilenameFromUrl("/static-one/").filename).toBe( path.join(webpackMultiConfig[0].output.path, "/index.html"), ); expect( - instance.getFilenameFromUrl("/static-one/index.html"), + instance.getFilenameFromUrl("/static-one/index.html").filename, ).toBe( path.join(webpackMultiConfig[0].output.path, "/index.html"), ); - expect(instance.getFilenameFromUrl("/static-one/svg.svg")).toBe( - path.join(webpackMultiConfig[0].output.path, "/svg.svg"), - ); + expect( + instance.getFilenameFromUrl("/static-one/svg.svg").filename, + ).toBe(path.join(webpackMultiConfig[0].output.path, "/svg.svg")); expect( instance.getFilenameFromUrl("/static-one/unknown.unknown"), ).toBeUndefined(); @@ -727,7 +733,9 @@ describe.each([ ), ).toBeUndefined(); - expect(instance.getFilenameFromUrl("/static-two/bundle.js")).toBe( + expect( + instance.getFilenameFromUrl("/static-two/bundle.js").filename, + ).toBe( path.join(webpackMultiConfig[1].output.path, "/bundle.js"), ); expect( @@ -1294,7 +1302,7 @@ describe.each([ }); }); - describe('should not work with the broken "publicPath" option', () => { + describe('should work with the broken "publicPath" option (malformed URI parsed as "/")', () => { let compiler; const outputPath = path.resolve(__dirname, "./outputs/basic"); @@ -1305,7 +1313,7 @@ describe.each([ output: { filename: "bundle.js", path: outputPath, - publicPath: "https://test:malfor%5Med@test.example.com", + publicPath: "http/s://test:malformed%5Med@test.example.com", }, }); diff --git a/types/index.d.ts b/types/index.d.ts index 140425b45..1ba81cd6c 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -99,17 +99,16 @@ export = wdm; * @template {IncomingMessage} [RequestInternal=IncomingMessage] * @template {ServerResponse} [ResponseInternal=ServerResponse] * @callback Middleware - * @param {RequestInternal} req - * @param {ResponseInternal} res - * @param {NextFunction} next + * @param {RequestInternal} req request + * @param {ResponseInternal} res response + * @param {NextFunction} next next function * @returns {Promise} */ /** @typedef {import("./utils/getFilenameFromUrl").Extra} Extra */ /** * @callback GetFilenameFromUrl - * @param {string} url - * @param {Extra=} extra - * @returns {string | undefined} + * @param {string} url request URL + * @returns {{ filename: string, extra: Extra } | undefined} a filename with additional information, or `undefined` if nothing is found */ /** * @callback WaitUntilValid @@ -460,10 +459,12 @@ type Middleware< next: NextFunction, ) => Promise; type Extra = import("./utils/getFilenameFromUrl").Extra; -type GetFilenameFromUrl = ( - url: string, - extra?: Extra | undefined, -) => string | undefined; +type GetFilenameFromUrl = (url: string) => + | { + filename: string; + extra: Extra; + } + | undefined; type WaitUntilValid = (callback: Callback) => any; type Invalidate = (callback: Callback) => any; type Close = (callback: (err: Error | null | undefined) => void) => any; diff --git a/types/middleware.d.ts b/types/middleware.d.ts index cce5820ac..2be44b1de 100644 --- a/types/middleware.d.ts +++ b/types/middleware.d.ts @@ -25,6 +25,8 @@ declare namespace wrapper { IncomingMessage, ServerResponse, NormalizedHeaders, + FilenameError, + Extra, ReadStream, }; } @@ -50,4 +52,6 @@ type NextFunction = import("./index.js").NextFunction; type IncomingMessage = import("./index.js").IncomingMessage; type ServerResponse = import("./index.js").ServerResponse; type NormalizedHeaders = import("./index.js").NormalizedHeaders; +type FilenameError = import("./utils/getFilenameFromUrl.js").FilenameError; +type Extra = import("./utils/getFilenameFromUrl.js").Extra; type ReadStream = import("fs").ReadStream; diff --git a/types/utils/getFilenameFromUrl.d.ts b/types/utils/getFilenameFromUrl.d.ts index be28a0ae0..2385f8646 100644 --- a/types/utils/getFilenameFromUrl.d.ts +++ b/types/utils/getFilenameFromUrl.d.ts @@ -1,24 +1,10 @@ export = getFilenameFromUrl; -/** - * @typedef {object} Extra - * @property {import("fs").Stats=} stats stats - * @property {number=} errorCode error code - * @property {boolean=} immutable true when immutable, otherwise false - */ -/** - * decodeURIComponent. - * - * Allows V8 to only deoptimize this fn instead of all of send(). - * @param {string} input - * @returns {string} - */ /** * @template {IncomingMessage} Request * @template {ServerResponse} Response * @param {import("../index.js").FilledContext} context context * @param {string} url url - * @param {Extra=} extra extra - * @returns {string | undefined} filename + * @returns {{ filename: string, extra: Extra } | undefined} result of get filename from url */ declare function getFilenameFromUrl< Request extends IncomingMessage, @@ -26,10 +12,34 @@ declare function getFilenameFromUrl< >( context: import("../index.js").FilledContext, url: string, - extra?: Extra | undefined, -): string | undefined; +): + | { + filename: string; + extra: Extra; + } + | undefined; declare namespace getFilenameFromUrl { - export { IncomingMessage, ServerResponse, Extra }; + export { FilenameError, IncomingMessage, ServerResponse, Extra }; +} +/** + * @typedef {object} Extra + * @property {import("fs").Stats} stats stats + * @property {boolean=} immutable true when immutable, otherwise false + */ +/** + * decodeURIComponent. + * + * Allows V8 to only deoptimize this fn instead of all of send(). + * @param {string} input + * @returns {string} + */ +declare class FilenameError extends Error { + /** + * @param {string} message message + * @param {number=} code error code + */ + constructor(message: string, code?: number | undefined); + code: number | undefined; } type IncomingMessage = import("../index.js").IncomingMessage; type ServerResponse = import("../index.js").ServerResponse; @@ -37,11 +47,7 @@ type Extra = { /** * stats */ - stats?: import("fs").Stats | undefined; - /** - * error code - */ - errorCode?: number | undefined; + stats: import("fs").Stats; /** * true when immutable, otherwise false */ From 055687c5b9eb8bc23e2888b33a5e50f6b1578f61 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Wed, 28 Jan 2026 19:19:43 -0500 Subject: [PATCH 06/13] chore: update dependencies for memfs and mime-types (#2258) --- package-lock.json | 8 ++++---- package.json | 6 +++--- test/middleware.test.js | 9 ++------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index c34544e7d..1f043dbe2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,11 @@ "version": "7.4.5", "license": "MIT", "dependencies": { - "memfs": "^4.43.1", - "mime-types": "^3.0.1", + "memfs": "^4.56.10", + "mime-types": "^3.0.2", "on-finished": "^2.4.1", "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" + "schema-utils": "^4.3.3" }, "devDependencies": { "@babel/cli": "^7.16.7", @@ -56,7 +56,7 @@ "webpack": "^5.101.0" }, "engines": { - "node": ">= 18.12.0" + "node": ">= 20.9.0" }, "funding": { "type": "opencollective", diff --git a/package.json b/package.json index e5db22d89..df1b8d900 100644 --- a/package.json +++ b/package.json @@ -47,11 +47,11 @@ "release": "standard-version" }, "dependencies": { - "memfs": "^4.43.1", - "mime-types": "^3.0.1", + "memfs": "^4.56.10", + "mime-types": "^3.0.2", "on-finished": "^2.4.1", "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" + "schema-utils": "^4.3.3" }, "devDependencies": { "@babel/cli": "^7.16.7", diff --git a/test/middleware.test.js b/test/middleware.test.js index 28723a5f2..eac6b7d3b 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -1252,13 +1252,8 @@ describe.each([ expect(response.statusCode).toBe(200); - // todo bug on node.js@22 and memfs - const [major] = process.versions.node.split(".").map(Number); - - if (major < 22) { - expect(response.text).toBe("\u00BD + \u00BC = \u00BE"); - expect(response.headers["content-length"]).toBe("12"); - } + expect(response.text).toBe("\u00BD + \u00BC = \u00BE"); + expect(response.headers["content-length"]).toBe("12"); expect(response.headers["content-type"]).toBe( "text/html; charset=utf-8", From 1fccf27143a850d038322c04f29ede944347cec7 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 22 Feb 2026 13:01:21 -0500 Subject: [PATCH 07/13] feat: add `forwardError` option to enable error forwarding to next middleware (#2259) * feat: add `forwardError` option to enable error forwarding to next middleware * feat: add error middleware support in frameworkFactory for improved error handling * feat: enhance error handling by removing response headers before forwarding errors * feat: remove error middleware option from frameworkFactory for cleaner middleware setup * test: update middleware tests to reflect changes in error handling with forwardError option * test: update middleware tests to handle error forwarding for hapi framework * feat: implement error forwarding in koa middleware and update tests accordingly * feat: add error forwarding support in hono middleware and update tests accordingly * feat: add forwardError option to README with usage example --- README.md | 30 ++++++ src/index.js | 15 +++ src/middleware.js | 15 ++- src/options.json | 5 + test/middleware.test.js | 230 +++++++++++++++++++++++++++++++++++++++- types/index.d.ts | 5 + 6 files changed, 296 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 113fdc2e5..46a54691c 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ See [below](#other-servers) for an example of use with fastify. | **[`writeToDisk`](#writetodisk)** | `boolean\|Function` | `false` | Instructs the module to write files to the configured location on disk as specified in your `webpack` configuration. | | **[`outputFileSystem`](#outputfilesystem)** | `Object` | [`memfs`](https://github.com/streamich/memfs) | Set the default file system which will be used by webpack as primary destination of generated files. | | **[`modifyResponseData`](#modifyresponsedata)** | `Function` | `undefined` | Allows to set up a callback to change the response data. | +| **[`forwardError`](#forwarderror)** | `boolean` | `false` | Enable or disable forwarding errors to the next middleware. | The middleware accepts an `options` Object. The following is a property reference for the Object. @@ -476,6 +477,35 @@ instance.waitUntilValid(() => { }); ``` +### `forwardError` + +Type: `boolean` +Default: `false` + +Enable or disable forwarding errors to the next middleware. If `true`, errors will be forwarded to the next middleware, otherwise, they will be handled by `webpack-dev-middleware` and a response will be handled case by case. + +This option don't work with hono, koa and hapi, because of the differences in error handling between these frameworks and express. + +```js +const express = require("express"); +const webpack = require("webpack"); +const middleware = require("webpack-dev-middleware"); + +const compiler = webpack({ + /* Webpack configuration */ +}); + +const instance = middleware(compiler, { forwardError: true }); + +const app = express(); +app.use(instance); + +app.use((err, req, res, next) => { + console.log(`Error: ${err}`); + res.status(500).send("Something broke!"); +}); +``` + ## FAQ ### Avoid blocking requests to non-webpack resources. diff --git a/src/index.js b/src/index.js index 6c7e7a05c..c0c3b38bc 100644 --- a/src/index.js +++ b/src/index.js @@ -125,6 +125,7 @@ const noop = () => {}; * @property {boolean=} lastModified options to generate last modified header * @property {(boolean | number | string | { maxAge?: number, immutable?: boolean })=} cacheControl options to generate cache headers * @property {boolean=} cacheImmutable is cache immutable + * @property {boolean=} forwardError forward error to next middleware */ /** @@ -441,6 +442,7 @@ function koaWrapper(compiler, options) { ctx.body = stream; isFinished = true; + resolve(); }; /** @@ -479,6 +481,13 @@ function koaWrapper(compiler, options) { }, ); } catch (err) { + if (options?.forwardError) { + await next(); + + // need the return for prevent to execute the code below and override the status and body set by user in the next middleware + return; + } + ctx.status = /** @type {Error & { statusCode: number }} */ (err).statusCode || /** @type {Error & { status: number }} */ (err).status || @@ -653,6 +662,12 @@ function honoWrapper(compiler, options) { }, ); } catch (err) { + if (options?.forwardError) { + await next(); + + // need the return for prevent to execute the code below and override the status and body set by user in the next middleware + return; + } context.status(500); return context.json({ message: /** @type {Error} */ (err).message }); diff --git a/src/middleware.js b/src/middleware.js index 078bbead7..946d4d73f 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -161,8 +161,6 @@ function wrapper(context) { } const acceptedMethods = context.options.methods || ["GET", "HEAD"]; - // TODO do we need an option here? - const forwardError = false; initState(res); @@ -180,13 +178,24 @@ function wrapper(context) { * @returns {Promise} */ async function sendError(message, status, options) { - if (forwardError) { + if (context.options.forwardError) { + if (!getHeadersSent(res)) { + const headers = getResponseHeaders(res); + + for (let i = 0; i < headers.length; i++) { + removeResponseHeader(res, headers[i]); + } + } + const error = /** @type {Error & { statusCode: number }} */ (new Error(message)); error.statusCode = status; await goNext(error); + + // need the return for prevent to execute the code below and override the status and body set by user in the next middleware + return; } const escapeHtml = getEscapeHtml(); diff --git a/src/options.json b/src/options.json index 0a55b69c9..1e83adaa1 100644 --- a/src/options.json +++ b/src/options.json @@ -172,6 +172,11 @@ "description": "Enable or disable setting `Cache-Control: public, max-age=31536000, immutable` response header for immutable assets (i.e. asset with a hash in file name like `image.a4c12bde.jpg`).", "link": "https://github.com/webpack/webpack-dev-middleware#cacheimmutable", "type": "boolean" + }, + "forwardError": { + "description": "Enable or disable forwarding errors to next middleware.", + "link": "https://github.com/webpack/webpack-dev-middleware#forwarderrors", + "type": "boolean" } }, "additionalProperties": false diff --git a/test/middleware.test.js b/test/middleware.test.js index eac6b7d3b..683684dfe 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -146,7 +146,6 @@ async function frameworkFactory( app.use(item); } } - return [server, req, instance.devMiddleware]; } default: { @@ -3610,6 +3609,235 @@ describe.each([ }); }); + describe("should call the next middleware for finished or errored requests when forwardError is enabled", () => { + let compiler; + + const outputPath = path.resolve( + __dirname, + "./outputs/basic-test-errors-headers-sent", + ); + + let nextWasCalled = false; + + beforeAll(async () => { + compiler = getCompiler({ + ...webpackConfig, + output: { + filename: "bundle.js", + path: outputPath, + }, + }); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + etag: "weak", + lastModified: true, + forwardError: true, + }, + { + setupMiddlewares: (middlewares) => { + if (name === "hapi") { + // There's no such thing as "the next route handler" in hapi. One request is matched to one or no route handlers. + } else if (name === "koa") { + middlewares.push(async (ctx) => { + nextWasCalled = true; + ctx.status = 500; + ctx.body = "error"; + }); + } else if (name === "hono") { + middlewares.push(async (ctx) => { + nextWasCalled = true; + ctx.status(500); + return ctx.text("error"); + }); + } else { + middlewares.push((_error, _req, res, _next) => { + nextWasCalled = true; + res.statusCode = 500; + res.end("error"); + }); + } + + return middlewares; + }, + }, + ); + + instance.context.outputFileSystem.mkdirSync(outputPath, { + recursive: true, + }); + instance.context.outputFileSystem.writeFileSync( + path.resolve(outputPath, "index.html"), + "HTML", + ); + instance.context.outputFileSystem.writeFileSync( + path.resolve(outputPath, "image.svg"), + "svg image", + ); + instance.context.outputFileSystem.writeFileSync( + path.resolve(outputPath, "file.text"), + "text", + ); + + const originalMethod = + instance.context.outputFileSystem.createReadStream; + + instance.context.outputFileSystem.createReadStream = + function createReadStream(...args) { + if (args[0].endsWith("image.svg")) { + const brokenStream = new this.ReadStream(...args); + + brokenStream._read = function _read() { + const error = new Error("test"); + error.code = "ENAMETOOLONG"; + this.emit("error", error); + this.end(); + this.destroy(); + }; + + return brokenStream; + } + + return originalMethod(...args); + }; + }); + + beforeEach(() => { + nextWasCalled = false; + }); + + afterAll(async () => { + await close(server, instance); + }); + + it("should work with piping stream", async () => { + const response1 = await req.get("/file.text"); + + expect(response1.statusCode).toBe(200); + expect(nextWasCalled).toBe(false); + }); + + it('should return the "500" code for requests above root', async () => { + const response = await req.get("/public/..%2f../middleware.test.js"); + + expect(response.statusCode).toBe(500); + if (name !== "hapi") { + expect(response.text).toBe("error"); + expect(nextWasCalled).toBe(true); + } else { + expect(nextWasCalled).toBe(false); + } + }); + + it('should return the "500" code for the "GET" request to the bundle file with etag and wrong "if-match" header', async () => { + const response1 = await req.get("/file.text"); + + expect(response1.statusCode).toBe(200); + expect(response1.headers.etag).toBeDefined(); + expect(response1.headers.etag.startsWith("W/")).toBe(true); + + const response2 = await req.get("/file.text").set("if-match", "test"); + + expect(response2.statusCode).toBe(500); + if (name !== "hapi") { + expect(response2.text).toBe("error"); + expect(nextWasCalled).toBe(true); + } else { + expect(nextWasCalled).toBe(false); + } + }); + + it('should return the "500" code for the "GET" request with the invalid range header', async () => { + const response = await req + .get("/file.text") + .set("Range", "bytes=9999999-"); + + expect(response.statusCode).toBe(500); + if (name !== "hapi") { + expect(response.text).toBe("error"); + expect(nextWasCalled).toBe(true); + } else { + expect(nextWasCalled).toBe(false); + } + }); + + // TODO: why koa and hono don't catch for their error handling when stream emit error? + (name === "koa" || name === "hono" ? it.skip : it)( + 'should return the "500" code for the "GET" request to the "image.svg" file when it throws a reading error', + async () => { + const response = await req.get("/image.svg"); + + // eslint-disable-next-line jest/no-standalone-expect + expect(response.statusCode).toBe(500); + if (name !== "hapi") { + // eslint-disable-next-line jest/no-standalone-expect + expect(nextWasCalled).toBe(true); + } else { + // eslint-disable-next-line jest/no-standalone-expect + expect(nextWasCalled).toBe(false); + } + }, + ); + + it('should return the "200" code for the "HEAD" request to the bundle file', async () => { + const response = await req.head("/file.text"); + + expect(response.statusCode).toBe(200); + expect(response.text).toBeUndefined(); + expect(nextWasCalled).toBe(false); + }); + + it('should return the "304" code for the "GET" request to the bundle file with etag and "if-none-match"', async () => { + const response1 = await req.get("/file.text"); + + expect(response1.statusCode).toBe(200); + expect(response1.headers.etag).toBeDefined(); + expect(response1.headers.etag.startsWith("W/")).toBe(true); + + const response2 = await req + .get("/file.text") + .set("if-none-match", response1.headers.etag); + + expect(response2.statusCode).toBe(304); + expect(response2.headers.etag).toBeDefined(); + expect(response2.headers.etag.startsWith("W/")).toBe(true); + + const response3 = await req + .get("/file.text") + .set("if-none-match", response1.headers.etag); + + expect(response3.statusCode).toBe(304); + expect(response3.headers.etag).toBeDefined(); + expect(response3.headers.etag.startsWith("W/")).toBe(true); + expect(nextWasCalled).toBe(false); + }); + + it('should return the "304" code for the "GET" request to the bundle file with lastModified and "if-modified-since" header', async () => { + const response1 = await req.get("/file.text"); + + expect(response1.statusCode).toBe(200); + expect(response1.headers["last-modified"]).toBeDefined(); + + const response2 = await req + .get("/file.text") + .set("if-modified-since", response1.headers["last-modified"]); + + expect(response2.statusCode).toBe(304); + expect(response2.headers["last-modified"]).toBeDefined(); + + const response3 = await req + .get("/file.text") + .set("if-modified-since", response2.headers["last-modified"]); + + expect(response3.statusCode).toBe(304); + expect(response3.headers["last-modified"]).toBeDefined(); + expect(nextWasCalled).toBe(false); + }); + }); + describe("should fallthrough for not found files", () => { let compiler; diff --git a/types/index.d.ts b/types/index.d.ts index 1ba81cd6c..e3fb18505 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -94,6 +94,7 @@ export = wdm; * @property {boolean=} lastModified options to generate last modified header * @property {(boolean | number | string | { maxAge?: number, immutable?: boolean })=} cacheControl options to generate cache headers * @property {boolean=} cacheImmutable is cache immutable + * @property {boolean=} forwardError forward error to next middleware */ /** * @template {IncomingMessage} [RequestInternal=IncomingMessage] @@ -449,6 +450,10 @@ type Options< * is cache immutable */ cacheImmutable?: boolean | undefined; + /** + * forward error to next middleware + */ + forwardError?: boolean | undefined; }; type Middleware< RequestInternal extends IncomingMessage = import("http").IncomingMessage, From 2146256dffb94637c9182e952bc6fbdc50e38c25 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Wed, 11 Mar 2026 08:04:33 -0500 Subject: [PATCH 08/13] feat: added support for plugin usage --- README.md | 69 +- package-lock.json | 34 +- src/index.js | 104 ++- src/middleware.js | 1 + src/utils/setupHooks.js | 139 ++-- .../logging.test.js.snap.webpack5 | 728 ++++++++++++++++-- .../webpack.array.dev-server-false-logging.js | 44 ++ .../webpack.array.dev-server-false.js | 4 +- test/fixtures/webpack.array.logging.config.js | 43 ++ test/helpers/runner.js | 188 +++-- test/logging.test.js | 105 +-- test/middleware.test.js | 192 +++-- types/index.d.ts | 9 +- types/utils/setupHooks.d.ts | 2 + 14 files changed, 1283 insertions(+), 379 deletions(-) create mode 100644 test/fixtures/webpack.array.dev-server-false-logging.js create mode 100644 test/fixtures/webpack.array.logging.config.js diff --git a/README.md b/README.md index 46a54691c..823f108fa 100644 --- a/README.md +++ b/README.md @@ -477,6 +477,58 @@ instance.waitUntilValid(() => { }); ``` +### `plugin(compiler, options)` + +Creates middleware instance in plugin mode. + +In plugin mode, stats output is written through custom code (i.e. in callback for `watch` or where you are calling `stats.toString(options)`) instead of `console.log`. +In this case, the `stats` option is not supported because `webpack-dev-middleware` does not have access to the code where the stats will be output. +You will also need to manually run the `watch` method. + +Why do you need this mode? In some cases, you may want to have multiple dev servers or run only one dev server when you have multiple configurations, and this is suitable for you. + +```js +const webpack = require("webpack"); +const middleware = require("webpack-dev-middleware"); + +const compiler = webpack({ + plugins: [ + { + apply(compiler) { + const devMiddleware = middleware( + compiler, + { + /* webpack-dev-middleware options */ + }, + true, + ); + }, + }, + ], + /* Webpack configuration */ +}); + +compiler.watch((err, stats) => { + if (err) { + console.error(err); + return; + } + + console.log(stats.toString()); +}); +``` + +### Plugin wrappers + +The following wrappers enable plugin mode for framework integrations: + +- `middleware(compiler, options, true)` (connect/express like middleware) +- `middleware.koaWrapper(compiler, options, true)` +- `middleware.hapiWrapper(true)` +- `middleware.honoWrapper(compiler, options, true)` + +They are equivalent to `koaWrapper`/`hapiWrapper`/`honoWrapper`, but use plugin mode logging behavior. + ### `forwardError` Type: `boolean` @@ -722,6 +774,8 @@ const devMiddlewareOptions = { const app = new Koa(); app.use(middleware.koaWrapper(compiler, devMiddlewareOptions)); +// Alternative usage (when you want to use as a plugin, i.e. all stats will be printed by other code): +// app.use(middleware.koaWrapper(compiler, devMiddlewareOptions, true)); app.listen(3000); ``` @@ -740,7 +794,7 @@ const devMiddlewareOptions = {}; const server = Hapi.server({ port: 3000, host: "localhost" }); await server.register({ - plugin: devMiddleware.hapiPlugin(), + plugin: devMiddleware.hapiWrapper(), options: { // The `compiler` option is required compiler, @@ -748,6 +802,16 @@ await server.register({ }, }); +// Alternative usage (when you want to use as a plugin, i.e. all stats will be printed by other code): +// await server.register({ +// plugin: devMiddleware.hapiWrapper(true), +// options: { +// // The `compiler` option is required +// compiler, +// ...devMiddlewareOptions, +// }, +// }); + await server.start(); console.log("Server running on %s", server.info.uri); @@ -796,6 +860,9 @@ const app = new Hono(); app.use(devMiddleware.honoWrapper(compiler, devMiddlewareOptions)); +// Alternative usage (when you want to use as a plugin, i.e. all stats will be printed by other code): +// const honoDevMiddleware = devMiddleware.honoWrapper(compiler, devMiddlewareOptions, true) + serve(app); ``` diff --git a/package-lock.json b/package-lock.json index 1f043dbe2..1cf0b248c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -132,6 +132,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2273,6 +2274,7 @@ "integrity": "sha512-Tdfx4eH2uS+gv9V9NCr3Rz+c7RSS6ntXp3Blliud18ibRUlRxO9dTaOjG4iv4x0nAmMeedP1ORkEpeXSkh2QiQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=20" } @@ -2354,7 +2356,8 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-4.0.19.tgz", "integrity": "sha512-VYHtPnZt/Zd/ATbW3rtexWpBnHUohUrQOHff/2JBhsVgxOrksAxJnLAO43Q1ayLJBJUUwNVo+RU0sx0aaysZfg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@cspell/dict-dart": { "version": "2.3.2", @@ -2494,14 +2497,16 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.14.tgz", "integrity": "sha512-2bf7n+kS92g+cMKV0wr9o/Oq9n8JzU7CcrB96gIh2GHgnF+0xDOqO2W/1KeFAqOfqosoOVE48t+4dnEMkkoJ2Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@cspell/dict-html-symbol-entities": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@cspell/dict-html-symbol-entities/-/dict-html-symbol-entities-4.0.5.tgz", "integrity": "sha512-429alTD4cE0FIwpMucvSN35Ld87HCyuM8mF731KU5Rm4Je2SG6hmVx7nkBsLyrmH3sQukTcr1GaiZsiEg8svPA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@cspell/dict-java": { "version": "5.0.12", @@ -2699,7 +2704,8 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-typescript/-/dict-typescript-3.2.3.tgz", "integrity": "sha512-zXh1wYsNljQZfWWdSPYwQhpwiuW0KPW1dSd8idjMRvSD0aSvWWHoWlrMsmZeRl4qM4QCEAjua8+cjflm41cQBg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@cspell/dict-vue": { "version": "3.0.5", @@ -5542,6 +5548,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -5674,6 +5681,7 @@ "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -5767,6 +5775,7 @@ "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.54.0", @@ -5806,6 +5815,7 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -6508,6 +6518,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6550,6 +6561,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7233,6 +7245,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -10207,6 +10220,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10296,6 +10310,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -11759,6 +11774,7 @@ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -13138,6 +13154,7 @@ "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -14159,6 +14176,7 @@ "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.3.0", "@jest/types": "30.3.0", @@ -18422,6 +18440,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -20805,6 +20824,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -20984,7 +21004,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsscmp": { "version": "1.0.6", @@ -21138,6 +21159,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -21507,6 +21529,7 @@ "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -22074,6 +22097,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/index.js b/src/index.js index c0c3b38bc..7e845342e 100644 --- a/src/index.js +++ b/src/index.js @@ -195,9 +195,10 @@ const noop = () => {}; * @template {ServerResponse} [ResponseInternal=ServerResponse] * @param {Compiler | MultiCompiler} compiler compiler * @param {Options=} options options + * @param {boolean} isPlugin true when will use as a plugin, otherwise false * @returns {API} webpack dev middleware */ -function wdm(compiler, options = {}) { +function wdm(compiler, options = {}, isPlugin = false) { validate(/** @type {Schema} */ (schema), options, { name: "Dev Middleware", baseDataPath: "options", @@ -219,7 +220,6 @@ function wdm(compiler, options = {}) { */ const context = { state: false, - stats: undefined, callbacks: [], options, @@ -227,7 +227,7 @@ function wdm(compiler, options = {}) { logger: compiler.getInfrastructureLogger("webpack-dev-middleware"), }; - setupHooks(context); + setupHooks(context, isPlugin); if (typeof options.writeToDisk === "function") { setupWriteToDisk(context); @@ -236,36 +236,38 @@ function wdm(compiler, options = {}) { setupOutputFileSystem(context); // Start watching - if (/** @type {Compiler} */ (context.compiler).watching) { - context.watching = /** @type {Compiler} */ (context.compiler).watching; - } else { - /** - * @param {Error | null | undefined} error error - */ - const errorHandler = (error) => { - if (error) { - // TODO: improve that in future - // For example - `writeToDisk` can throw an error and right now it is ends watching. - // We can improve that and keep watching active, but it is require API on webpack side. - // Let's implement that in webpack@5 because it is rare case. - context.logger.error(error); - } - }; + if (!isPlugin) { + if (/** @type {Compiler} */ (context.compiler).watching) { + context.watching = /** @type {Compiler} */ (context.compiler).watching; + } else { + /** + * @param {Error | null | undefined} error error + */ + const errorHandler = (error) => { + if (error) { + // TODO: improve that in future + // For example - `writeToDisk` can throw an error and right now it is ends watching. + // We can improve that and keep watching active, but it is require API on webpack side. + // Let's implement that in webpack@5 because it is rare case. + context.logger.error(error); + } + }; - if ( - Array.isArray(/** @type {MultiCompiler} */ (context.compiler).compilers) - ) { - const compilers = /** @type {MultiCompiler} */ (context.compiler); - const watchOptions = compilers.compilers.map( - (childCompiler) => childCompiler.options.watchOptions || {}, - ); + if ( + Array.isArray(/** @type {MultiCompiler} */ (context.compiler).compilers) + ) { + const compilers = /** @type {MultiCompiler} */ (context.compiler); + const watchOptions = compilers.compilers.map( + (childCompiler) => childCompiler.options.watchOptions || {}, + ); - context.watching = compiler.watch(watchOptions, errorHandler); - } else { - const oneCompiler = /** @type {Compiler} */ (context.compiler); - const watchOptions = oneCompiler.options.watchOptions || {}; + context.watching = compiler.watch(watchOptions, errorHandler); + } else { + const oneCompiler = /** @type {Compiler} */ (context.compiler); + const watchOptions = oneCompiler.options.watchOptions || {}; - context.watching = compiler.watch(watchOptions, errorHandler); + context.watching = compiler.watch(watchOptions, errorHandler); + } } } @@ -319,9 +321,10 @@ function wdm(compiler, options = {}) { /** * @template HapiServer * @template {HapiOptions} HapiOptionsInternal + * @param {boolean=} usePlugin true when need to use as a plugin, otherwise false * @returns {HapiPlugin} hapi wrapper */ -function hapiWrapper() { +function hapiWrapper(usePlugin = false) { return { pkg: { name: "webpack-dev-middleware", @@ -335,7 +338,7 @@ function hapiWrapper() { throw new Error("The compiler options is required."); } - const devMiddleware = wdm(compiler, rest); + const devMiddleware = wdm(compiler, rest, usePlugin); // @ts-expect-error if (!server.decorations.server.includes("webpackDevMiddleware")) { @@ -394,10 +397,11 @@ wdm.hapiWrapper = hapiWrapper; * @template {ServerResponse} [ResponseInternal=ServerResponse] * @param {Compiler | MultiCompiler} compiler compiler * @param {Options=} options options + * @param {boolean=} usePlugin whether to use as webpack plugin * @returns {(ctx: EXPECTED_ANY, next: EXPECTED_FUNCTION) => Promise | void} kow wrapper */ -function koaWrapper(compiler, options) { - const devMiddleware = wdm(compiler, options); +function koaWrapper(compiler, options, usePlugin) { + const devMiddleware = wdm(compiler, options, usePlugin); /** * @param {{ req: RequestInternal, res: ResponseInternal & import("./utils/compatibleAPI").ExpectedServerResponse, status: number, body: string | Buffer | import("fs").ReadStream | { message: string }, state: object }} ctx context @@ -439,11 +443,32 @@ function koaWrapper(compiler, options) { * @param {import("fs").ReadStream} stream readable stream */ res.stream = (stream) => { - ctx.body = stream; + let resolved = false; - isFinished = true; + /** + * @param {Error=} err error + */ + const onEvent = (err) => { + if (resolved) return; + resolved = true; - resolve(); + stream.removeListener("error", onEvent); + stream.removeListener("readable", onEvent); + + if (err) { + reject(err); + return; + } + + ctx.body = stream; + isFinished = true; + resolve(); + }; + + stream.once("error", onEvent); + stream.once("readable", onEvent); + // Empty stream + stream.once("end", onEvent); }; /** * @param {string | Buffer} data data @@ -515,10 +540,11 @@ wdm.koaWrapper = koaWrapper; * @template {ServerResponse} [ResponseInternal=ServerResponse] * @param {Compiler | MultiCompiler} compiler compiler * @param {Options=} options options + * @param {boolean=} usePlugin true when need to use as a plugin, otherwise false * @returns {(ctx: EXPECTED_ANY, next: EXPECTED_FUNCTION) => Promise | void} hono wrapper */ -function honoWrapper(compiler, options) { - const devMiddleware = wdm(compiler, options); +function honoWrapper(compiler, options, usePlugin) { + const devMiddleware = wdm(compiler, options, usePlugin); /** * @param {{ env: EXPECTED_ANY, body: EXPECTED_ANY, json: EXPECTED_ANY, status: EXPECTED_ANY, set: EXPECTED_ANY, req: RequestInternal & import("./utils/compatibleAPI").ExpectedIncomingMessage & { header: (name: string) => string }, res: ResponseInternal & import("./utils/compatibleAPI").ExpectedServerResponse & { headers: EXPECTED_ANY, status: EXPECTED_ANY } }} context context diff --git a/src/middleware.js b/src/middleware.js index 946d4d73f..063f672ab 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -876,6 +876,7 @@ function wrapper(context) { // Error handling /** @type {import("fs").ReadStream} */ (bufferOrStream).on("error", (error) => { + context.logger.error("Stream error:", error); // clean up stream early cleanup(); errorHandler(error); diff --git a/src/utils/setupHooks.js b/src/utils/setupHooks.js index 50c55a0f8..e09638023 100644 --- a/src/utils/setupHooks.js +++ b/src/utils/setupHooks.js @@ -14,8 +14,9 @@ * @template {IncomingMessage} Request * @template {ServerResponse} Response * @param {import("../index.js").WithOptional, "watching" | "outputFileSystem">} context context + * @param {boolean=} isPlugin true when it is a plugin usage, otherwise false */ -function setupHooks(context) { +function setupHooks(context, isPlugin) { /** * @returns {void} */ @@ -54,7 +55,6 @@ function setupHooks(context) { // We are now on valid state context.state = true; - context.stats = stats; // Do the stuff in nextTick, because bundle may be invalidated if a change happened while compiling @@ -66,80 +66,85 @@ function setupHooks(context) { return; } - logger.log("Compilation finished"); - - const isMultiCompilerMode = Boolean( - /** @type {MultiCompiler} */ - (compiler).compilers, - ); - - /** - * @type {StatsOptions | MultiStatsOptions | undefined} - */ - let statsOptions; - - if (typeof options.stats !== "undefined") { - statsOptions = isMultiCompilerMode - ? { - children: - /** @type {MultiCompiler} */ - (compiler).compilers.map(() => options.stats), - } - : options.stats; - } else { - statsOptions = isMultiCompilerMode - ? { - children: - /** @type {MultiCompiler} */ - (compiler).compilers.map((child) => child.options.stats), - } - : /** @type {Compiler} */ (compiler).options.stats; - } + // For plugin support we should print nothing, because webpack/webpack-cli/webpack-dev-server will print them on using `stats.toString()` + if (!isPlugin) { + logger.log("Compilation finished"); - if (isMultiCompilerMode) { - /** @type {MultiStatsOptions} */ - (statsOptions).children = - /** @type {MultiStatsOptions} */ - (statsOptions).children.map( - /** - * @param {StatsOptions} childStatsOptions child stats options - * @returns {StatsObjectOptions} object child stats options - */ - (childStatsOptions) => { - childStatsOptions = normalizeStatsOptions(childStatsOptions); - - if (typeof childStatsOptions.colors === "undefined") { - const [firstCompiler] = - /** @type {MultiCompiler} */ - (compiler).compilers; + const isMultiCompilerMode = Boolean( + /** @type {MultiCompiler} */ + (compiler).compilers, + ); + + /** + * @type {StatsOptions | MultiStatsOptions | undefined} + */ + let statsOptions; - childStatsOptions.colors = - firstCompiler.webpack.cli.isColorSupported(); + if (typeof options.stats !== "undefined") { + statsOptions = isMultiCompilerMode + ? { + children: + /** @type {MultiCompiler} */ + (compiler).compilers.map(() => options.stats), } + : options.stats; + } else { + statsOptions = isMultiCompilerMode + ? { + children: + /** @type {MultiCompiler} */ + (compiler).compilers.map((child) => child.options.stats), + } + : /** @type {Compiler} */ (compiler).options.stats; + } - return childStatsOptions; - }, + if (isMultiCompilerMode) { + /** @type {MultiStatsOptions} */ + (statsOptions).children = + /** @type {MultiStatsOptions} */ + (statsOptions).children.map( + /** + * @param {StatsOptions} childStatsOptions child stats options + * @returns {StatsObjectOptions} object child stats options + */ + (childStatsOptions) => { + childStatsOptions = normalizeStatsOptions(childStatsOptions); + + if (typeof childStatsOptions.colors === "undefined") { + const [firstCompiler] = + /** @type {MultiCompiler} */ + (compiler).compilers; + + childStatsOptions.colors = + firstCompiler.webpack.cli.isColorSupported(); + } + + return childStatsOptions; + }, + ); + } else { + statsOptions = normalizeStatsOptions( + /** @type {StatsOptions} */ (statsOptions), ); - } else { - statsOptions = normalizeStatsOptions( - /** @type {StatsOptions} */ (statsOptions), - ); - if (typeof statsOptions.colors === "undefined") { - const { compiler } = /** @type {{ compiler: Compiler }} */ (context); - statsOptions.colors = compiler.webpack.cli.isColorSupported(); + if (typeof statsOptions.colors === "undefined") { + const { compiler } = /** @type {{ compiler: Compiler }} */ ( + context + ); + statsOptions.colors = compiler.webpack.cli.isColorSupported(); + } } - } - const printedStats = stats.toString( - /** @type {StatsObjectOptions} */ - (statsOptions), - ); + const printedStats = stats.toString( + /** @type {StatsObjectOptions} */ + (statsOptions), + ); - // Avoid extra empty line when `stats: 'none'` - if (printedStats) { - // eslint-disable-next-line no-console - console.log(printedStats); + // Avoid extra empty line when `stats: 'none'` + if (printedStats) { + // eslint-disable-next-line no-console + console.log(printedStats); + } } context.callbacks = []; diff --git a/test/__snapshots__/logging.test.js.snap.webpack5 b/test/__snapshots__/logging.test.js.snap.webpack5 index e5a367df7..1bc9bec7a 100644 --- a/test/__snapshots__/logging.test.js.snap.webpack5 +++ b/test/__snapshots__/logging.test.js.snap.webpack5 @@ -1,18 +1,18 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`logging should logging an error in "watch" method: stderr 1`] = `"Error: Watch error"`; +exports[`logging plugin should logging an error in "watch" method: stderr 1`] = `"Error: Watch error"`; -exports[`logging should logging an warning: stderr 1`] = `""`; +exports[`logging plugin should logging an warning: stderr 1`] = `""`; -exports[`logging should logging an warning: stdout 1`] = ` +exports[`logging plugin should logging an warning: stdout 1`] = ` "WARNING in Warning webpack compiled with 1 warning" `; -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #2: stderr 1`] = `""`; +exports[`logging plugin should logging in multi-compiler and respect the "stats" option from configuration #2: stderr 1`] = `""`; -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #2: stdout 1`] = ` +exports[`logging plugin should logging in multi-compiler and respect the "stats" option from configuration #2: stdout 1`] = ` "broken: asset bundle.js x KiB [emitted] (name: main) ./broken.js x bytes [built] [code generated] [1 error] @@ -45,9 +45,9 @@ cacheable modules x bytes success (webpack x.x.x) compiled successfully in x ms" `; -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #3: stderr 1`] = `""`; +exports[`logging plugin should logging in multi-compiler and respect the "stats" option from configuration #3: stderr 1`] = `""`; -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #3: stdout 1`] = ` +exports[`logging plugin should logging in multi-compiler and respect the "stats" option from configuration #3: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) ./broken.js x bytes [built] [code generated] [1 error] @@ -77,9 +77,9 @@ cacheable modules x bytes webpack x.x.x compiled successfully in x ms" `; -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #4: stderr 1`] = `""`; +exports[`logging plugin should logging in multi-compiler and respect the "stats" option from configuration #4: stderr 1`] = `""`; -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #4: stdout 1`] = ` +exports[`logging plugin should logging in multi-compiler and respect the "stats" option from configuration #4: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) ./broken.js x bytes [built] [code generated] [1 error] @@ -103,9 +103,9 @@ asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main)" `; -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #5: stderr 1`] = `""`; +exports[`logging plugin should logging in multi-compiler and respect the "stats" option from configuration #5: stderr 1`] = `""`; -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #5: stdout 1`] = ` +exports[`logging plugin should logging in multi-compiler and respect the "stats" option from configuration #5: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) ./bar.js x bytes [built] [code generated] webpack x.x.x compiled successfully in x ms @@ -121,9 +121,9 @@ cacheable modules x bytes webpack x.x.x compiled successfully in x ms" `; -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration: stderr 1`] = `""`; +exports[`logging plugin should logging in multi-compiler and respect the "stats" option from configuration: stderr 1`] = `""`; -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration: stdout 1`] = ` +exports[`logging plugin should logging in multi-compiler and respect the "stats" option from configuration: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) ./broken.js x bytes [built] [code generated] [1 error] @@ -153,9 +153,9 @@ cacheable modules x bytes webpack x.x.x compiled successfully in x ms" `; -exports[`logging should logging on successfully build and respect colors #2: stderr 1`] = `""`; +exports[`logging plugin should logging on successfully build and respect colors #2: stderr 1`] = `""`; -exports[`logging should logging on successfully build and respect colors #2: stdout 1`] = ` +exports[`logging plugin should logging on successfully build and respect colors #2: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) @@ -167,9 +167,9 @@ cacheable modules x bytes webpack x.x.x compiled successfully in x ms" `; -exports[`logging should logging on successfully build and respect colors: stderr 1`] = `""`; +exports[`logging plugin should logging on successfully build and respect colors: stderr 1`] = `""`; -exports[`logging should logging on successfully build and respect colors: stdout 1`] = ` +exports[`logging plugin should logging on successfully build and respect colors: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) @@ -181,9 +181,9 @@ cacheable modules x bytes webpack x.x.x compiled successfully in x ms" `; -exports[`logging should logging on successfully build and respect the "NO_COLOR" env: stderr 1`] = `""`; +exports[`logging plugin should logging on successfully build and respect the "NO_COLOR" env: stderr 1`] = `""`; -exports[`logging should logging on successfully build and respect the "NO_COLOR" env: stdout 1`] = ` +exports[`logging plugin should logging on successfully build and respect the "NO_COLOR" env: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) @@ -195,33 +195,33 @@ cacheable modules x bytes webpack x.x.x compiled successfully in x ms" `; -exports[`logging should logging on successfully build and respect the "stats" option from configuration with custom object value: stderr 1`] = `""`; +exports[`logging plugin should logging on successfully build and respect the "stats" option from configuration with custom object value: stderr 1`] = `""`; -exports[`logging should logging on successfully build and respect the "stats" option from configuration with custom object value: stdout 1`] = ` +exports[`logging plugin should logging on successfully build and respect the "stats" option from configuration with custom object value: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main)" `; -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "false" value: stderr 1`] = `""`; +exports[`logging plugin should logging on successfully build and respect the "stats" option from configuration with the "false" value: stderr 1`] = `""`; -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "false" value: stdout 1`] = `""`; +exports[`logging plugin should logging on successfully build and respect the "stats" option from configuration with the "false" value: stdout 1`] = `""`; -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "minimal" value: stderr 1`] = `""`; +exports[`logging plugin should logging on successfully build and respect the "stats" option from configuration with the "minimal" value: stderr 1`] = `""`; -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "minimal" value: stdout 1`] = ` +exports[`logging plugin should logging on successfully build and respect the "stats" option from configuration with the "minimal" value: stdout 1`] = ` "x assets x modules webpack x.x.x compiled successfully in x ms" `; -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "none" value: stderr 1`] = `""`; +exports[`logging plugin should logging on successfully build and respect the "stats" option from configuration with the "none" value: stderr 1`] = `""`; -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "none" value: stdout 1`] = `""`; +exports[`logging plugin should logging on successfully build and respect the "stats" option from configuration with the "none" value: stdout 1`] = `""`; -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "true" value: stderr 1`] = `""`; +exports[`logging plugin should logging on successfully build and respect the "stats" option from configuration with the "true" value: stderr 1`] = `""`; -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "true" value: stdout 1`] = ` +exports[`logging plugin should logging on successfully build and respect the "stats" option from configuration with the "true" value: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) @@ -233,9 +233,9 @@ cacheable modules x bytes webpack x.x.x compiled successfully in x ms" `; -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "verbose" value: stderr 1`] = `""`; +exports[`logging plugin should logging on successfully build and respect the "stats" option from configuration with the "verbose" value: stderr 1`] = `""`; -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "verbose" value: stdout 1`] = ` +exports[`logging plugin should logging on successfully build and respect the "stats" option from configuration with the "verbose" value: stdout 1`] = ` "PublicPath: auto asset bundle.js x KiB {main} [emitted] (name: main) asset svg.svg x KiB ({main}) [emitted] [from: svg.svg] (auxiliary name: main) @@ -276,9 +276,9 @@ cjs require ./svg.svg [./foo.js] 3:0-20 LOG from xxx" `; -exports[`logging should logging on successfully build in multi-compiler mode: stderr 1`] = `""`; +exports[`logging plugin should logging on successfully build in multi-compiler mode: stderr 1`] = `""`; -exports[`logging should logging on successfully build in multi-compiler mode: stdout 1`] = ` +exports[`logging plugin should logging on successfully build in multi-compiler mode: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) @@ -294,33 +294,619 @@ asset bundle.js x KiB [emitted] (name: main) webpack x.x.x compiled successfully in x ms" `; -exports[`logging should logging on successfully build using the "stats" option for middleware with object value and no colors: stderr 1`] = `""`; +exports[`logging plugin should logging on successfully build using the "stats" option for middleware with object value and no colors: stderr 1`] = `""`; -exports[`logging should logging on successfully build using the "stats" option for middleware with object value and no colors: stdout 1`] = ` +exports[`logging plugin should logging on successfully build using the "stats" option for middleware with object value and no colors: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x KiB x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging plugin should logging on successfully build using the "stats" option for middleware with object value: stderr 1`] = `""`; + +exports[`logging plugin should logging on successfully build using the "stats" option for middleware with object value: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x KiB x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging plugin should logging on successfully build using the "stats" option for middleware with the "false" value: stderr 1`] = `""`; + +exports[`logging plugin should logging on successfully build using the "stats" option for middleware with the "false" value: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x KiB x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging plugin should logging on successfully build using the "stats" option for middleware with the "none" value: stderr 1`] = `""`; + +exports[`logging plugin should logging on successfully build using the "stats" option for middleware with the "none" value: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x KiB x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging plugin should logging on successfully build using the "stats" option for middleware with the "normal" value: stderr 1`] = `""`; + +exports[`logging plugin should logging on successfully build using the "stats" option for middleware with the "normal" value: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x KiB x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging plugin should logging on successfully build using the "stats" option for middleware with the "true" value: stderr 1`] = `""`; + +exports[`logging plugin should logging on successfully build using the "stats" option for middleware with the "true" value: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x KiB x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging plugin should logging on successfully build using the "stats" option for middleware with the "verbose" value: stderr 1`] = `""`; + +exports[`logging plugin should logging on successfully build using the "stats" option for middleware with the "verbose" value: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x KiB x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging plugin should logging on successfully build using the "stats" option for middleware with the object value and colors: stderr 1`] = `""`; + +exports[`logging plugin should logging on successfully build using the "stats" option for middleware with the object value and colors: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x KiB x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging plugin should logging on successfully build when the 'stats' doesn't exist: stderr 1`] = `""`; + +exports[`logging plugin should logging on successfully build when the 'stats' doesn't exist: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x KiB x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging plugin should logging on successfully build: stderr 1`] = `""`; + +exports[`logging plugin should logging on successfully build: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x KiB x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging plugin should logging on successfully multi-compiler build using the "stats" option for middleware with object value and colors: stderr 1`] = `""`; + +exports[`logging plugin should logging on successfully multi-compiler build using the "stats" option for middleware with object value and colors: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x bytes x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms + +asset bundle.js x KiB [emitted] (name: main) +./bar.js x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging plugin should logging on successfully multi-compiler build using the "stats" option for middleware with object value and no colors: stderr 1`] = `""`; + +exports[`logging plugin should logging on successfully multi-compiler build using the "stats" option for middleware with object value and no colors: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x bytes x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms + +asset bundle.js x KiB [emitted] (name: main) +./bar.js x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging plugin should logging on successfully multi-compiler build using the "stats" option for middleware with the "false" value: stderr 1`] = `""`; + +exports[`logging plugin should logging on successfully multi-compiler build using the "stats" option for middleware with the "false" value: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x bytes x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms + +asset bundle.js x KiB [emitted] (name: main) +./bar.js x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging plugin should logging on successfully multi-compiler build using the "stats" option for middleware with the "normal" value: stderr 1`] = `""`; + +exports[`logging plugin should logging on successfully multi-compiler build using the "stats" option for middleware with the "normal" value: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x bytes x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms + +asset bundle.js x KiB [emitted] (name: main) +./bar.js x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging plugin should logging on successfully multi-compiler build using the "stats" option for middleware with the "true" value: stderr 1`] = `""`; + +exports[`logging plugin should logging on successfully multi-compiler build using the "stats" option for middleware with the "true" value: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x bytes x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms + +asset bundle.js x KiB [emitted] (name: main) +./bar.js x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging plugin should logging on successfully multi-compiler build using the "stats" option for middleware with the object value: stderr 1`] = `""`; + +exports[`logging plugin should logging on successfully multi-compiler build using the "stats" option for middleware with the object value: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x bytes x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms + +asset bundle.js x KiB [emitted] (name: main) +./bar.js x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging plugin should logging on unsuccessful build in multi-compiler: stderr 1`] = `""`; + +exports[`logging plugin should logging on unsuccessful build in multi-compiler: stdout 1`] = ` +"ERROR in ./broken.js 1:3 +Module parse failed: Unexpected token (1:3) +You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders +> 1()2()3() +| + +webpack compiled with 1 error + +ERROR in ./broken.js 1:3 +Module parse failed: Unexpected token (1:3) +You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders +> 1()2()3() +| + +webpack compiled with 1 error" +`; + +exports[`logging plugin should logging on unsuccessful build: stderr 1`] = `""`; + +exports[`logging plugin should logging on unsuccessful build: stdout 1`] = ` +"ERROR in ./broken.js 1:3 +Module parse failed: Unexpected token (1:3) +You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders +> 1()2()3() +| + +webpack compiled with 1 error" +`; + +exports[`logging plugin should logging warnings in multi-compiler mode: stderr 1`] = `""`; + +exports[`logging plugin should logging warnings in multi-compiler mode: stdout 1`] = ` +"WARNING in Warning + +webpack compiled with 1 warning + +WARNING in Warning + +webpack compiled with 1 warning" +`; + +exports[`logging standalone should logging an error in "watch" method: stderr 1`] = `"Error: Watch error"`; + +exports[`logging standalone should logging an warning: stderr 1`] = `""`; + +exports[`logging standalone should logging an warning: stdout 1`] = ` +"WARNING in Warning + +webpack compiled with 1 warning" +`; + +exports[`logging standalone should logging in multi-compiler and respect the "stats" option from configuration #2: stderr 1`] = `""`; + +exports[`logging standalone should logging in multi-compiler and respect the "stats" option from configuration #2: stdout 1`] = ` +"broken: +asset bundle.js x KiB [emitted] (name: main) +./broken.js x bytes [built] [code generated] [1 error] + +ERROR in ./broken.js 1:3 +Module parse failed: Unexpected token (1:3) +You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders +> 1()2()3() +| + +broken (webpack x.x.x) compiled with 1 error in x ms + +warning: +asset bundle.js x KiB [emitted] (name: main) +./warning.js x bytes [built] [code generated] + +WARNING in Warning + +warning (webpack x.x.x) compiled with 1 warning in x ms + +success: +asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x bytes x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +success (webpack x.x.x) compiled successfully in x ms" +`; + +exports[`logging standalone should logging in multi-compiler and respect the "stats" option from configuration #3: stderr 1`] = `""`; + +exports[`logging standalone should logging in multi-compiler and respect the "stats" option from configuration #3: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +./broken.js x bytes [built] [code generated] [1 error] + +ERROR in ./broken.js 1:3 +Module parse failed: Unexpected token (1:3) +You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders +> 1()2()3() +| + +webpack x.x.x compiled with 1 error in x ms + +asset bundle.js x KiB [emitted] (name: main) +./warning.js x bytes [built] [code generated] + +WARNING in Warning + +webpack x.x.x compiled with 1 warning in x ms + +asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x bytes x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging standalone should logging in multi-compiler and respect the "stats" option from configuration #4: stderr 1`] = `""`; + +exports[`logging standalone should logging in multi-compiler and respect the "stats" option from configuration #4: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +./broken.js x bytes [built] [code generated] [1 error] + +ERROR in ./broken.js 1:3 +Module parse failed: Unexpected token (1:3) +You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders +> 1()2()3() +| + +webpack x.x.x compiled with 1 error in x ms + +asset bundle.js x KiB [emitted] (name: main) +./warning.js x bytes [built] [code generated] + +WARNING in Warning + +webpack x.x.x compiled with 1 warning in x ms + +asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main)" +`; + +exports[`logging standalone should logging in multi-compiler and respect the "stats" option from configuration #5: stderr 1`] = `""`; + +exports[`logging standalone should logging in multi-compiler and respect the "stats" option from configuration #5: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +./bar.js x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms + +asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x bytes x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging standalone should logging in multi-compiler and respect the "stats" option from configuration: stderr 1`] = `""`; + +exports[`logging standalone should logging in multi-compiler and respect the "stats" option from configuration: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +./broken.js x bytes [built] [code generated] [1 error] + +ERROR in ./broken.js 1:3 +Module parse failed: Unexpected token (1:3) +You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders +> 1()2()3() +| + +webpack x.x.x compiled with 1 error in x ms + +asset bundle.js x KiB [emitted] (name: main) +./warning.js x bytes [built] [code generated] + +WARNING in Warning + +webpack x.x.x compiled with 1 warning in x ms + +asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x bytes x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging standalone should logging on successfully build and respect colors #2: stderr 1`] = `""`; + +exports[`logging standalone should logging on successfully build and respect colors #2: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x KiB x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging standalone should logging on successfully build and respect colors: stderr 1`] = `""`; + +exports[`logging standalone should logging on successfully build and respect colors: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x KiB x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging standalone should logging on successfully build and respect the "NO_COLOR" env: stderr 1`] = `""`; + +exports[`logging standalone should logging on successfully build and respect the "NO_COLOR" env: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x KiB x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging standalone should logging on successfully build and respect the "stats" option from configuration with custom object value: stderr 1`] = `""`; + +exports[`logging standalone should logging on successfully build and respect the "stats" option from configuration with custom object value: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main)" +`; + +exports[`logging standalone should logging on successfully build and respect the "stats" option from configuration with the "false" value: stderr 1`] = `""`; + +exports[`logging standalone should logging on successfully build and respect the "stats" option from configuration with the "false" value: stdout 1`] = `""`; + +exports[`logging standalone should logging on successfully build and respect the "stats" option from configuration with the "minimal" value: stderr 1`] = `""`; + +exports[`logging standalone should logging on successfully build and respect the "stats" option from configuration with the "minimal" value: stdout 1`] = ` +"x assets +x modules +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging standalone should logging on successfully build and respect the "stats" option from configuration with the "none" value: stderr 1`] = `""`; + +exports[`logging standalone should logging on successfully build and respect the "stats" option from configuration with the "none" value: stdout 1`] = `""`; + +exports[`logging standalone should logging on successfully build and respect the "stats" option from configuration with the "true" value: stderr 1`] = `""`; + +exports[`logging standalone should logging on successfully build and respect the "stats" option from configuration with the "true" value: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x KiB x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging standalone should logging on successfully build and respect the "stats" option from configuration with the "verbose" value: stderr 1`] = `""`; + +exports[`logging standalone should logging on successfully build and respect the "stats" option from configuration with the "verbose" value: stdout 1`] = ` +"PublicPath: auto +asset bundle.js x KiB {main} [emitted] (name: main) +asset svg.svg x KiB ({main}) [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes ({main}) [emitted] [from: index.html] (auxiliary name: main) +Entrypoint main x KiB (x KiB) = bundle.js 2 auxiliary assets +chunk {main} (runtime: main) bundle.js (xxxx) x bytes (xxxx) x KiB (xxxx) [entry] [rendered] +> ./foo.js main +runtime modules x KiB +webpack/runtime/define property getters x bytes {main} [code generated] +[no exports] +[used exports unknown] +webpack/runtime/global x bytes {main} [code generated] +[no exports] +[used exports unknown] +webpack/runtime/hasOwnProperty shorthand x bytes {main} [code generated] +[no exports] +[used exports unknown] +webpack/runtime/make namespace object x bytes {main} [code generated] +[no exports] +[used exports unknown] +webpack/runtime/publicPath x KiB {main} [code generated] +[no exports] +[used exports unknown] +cacheable modules x bytes +./foo.js x bytes {main} [depth 0] [built] [code generated] +[used exports unknown] +entry ./foo.js main +./index.html x bytes {main} [depth 1] [dependent] [built] [code generated] +[exports: default] +[used exports unknown] +cjs require ./index.html [./foo.js] 4:0-23 +./svg.svg x bytes {main} [depth 1] [dependent] [built] [code generated] +[exports: default] +[used exports unknown] +cjs require ./svg.svg [./foo.js] 3:0-20 + + +LOG from xxx" +`; + +exports[`logging standalone should logging on successfully build in multi-compiler mode: stderr 1`] = `""`; + +exports[`logging standalone should logging on successfully build in multi-compiler mode: stdout 1`] = ` +"asset bundle.js x KiB [emitted] (name: main) +asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) +asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) +runtime modules x bytes x modules +cacheable modules x bytes +./foo.js x bytes [built] [code generated] +./svg.svg x bytes [built] [code generated] +./index.html x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms + +asset bundle.js x KiB [emitted] (name: main) +./bar.js x bytes [built] [code generated] +webpack x.x.x compiled successfully in x ms" +`; + +exports[`logging standalone should logging on successfully build using the "stats" option for middleware with object value and no colors: stderr 1`] = `""`; + +exports[`logging standalone should logging on successfully build using the "stats" option for middleware with object value and no colors: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main)" `; -exports[`logging should logging on successfully build using the "stats" option for middleware with object value: stderr 1`] = `""`; +exports[`logging standalone should logging on successfully build using the "stats" option for middleware with object value: stderr 1`] = `""`; -exports[`logging should logging on successfully build using the "stats" option for middleware with object value: stdout 1`] = ` +exports[`logging standalone should logging on successfully build using the "stats" option for middleware with object value: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main)" `; -exports[`logging should logging on successfully build using the "stats" option for middleware with the "false" value: stderr 1`] = `""`; +exports[`logging standalone should logging on successfully build using the "stats" option for middleware with the "false" value: stderr 1`] = `""`; -exports[`logging should logging on successfully build using the "stats" option for middleware with the "false" value: stdout 1`] = `""`; +exports[`logging standalone should logging on successfully build using the "stats" option for middleware with the "false" value: stdout 1`] = `""`; -exports[`logging should logging on successfully build using the "stats" option for middleware with the "none" value: stderr 1`] = `""`; +exports[`logging standalone should logging on successfully build using the "stats" option for middleware with the "none" value: stderr 1`] = `""`; -exports[`logging should logging on successfully build using the "stats" option for middleware with the "none" value: stdout 1`] = `""`; +exports[`logging standalone should logging on successfully build using the "stats" option for middleware with the "none" value: stdout 1`] = `""`; -exports[`logging should logging on successfully build using the "stats" option for middleware with the "normal" value: stderr 1`] = `""`; +exports[`logging standalone should logging on successfully build using the "stats" option for middleware with the "normal" value: stderr 1`] = `""`; -exports[`logging should logging on successfully build using the "stats" option for middleware with the "normal" value: stdout 1`] = ` +exports[`logging standalone should logging on successfully build using the "stats" option for middleware with the "normal" value: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) @@ -332,9 +918,9 @@ cacheable modules x bytes webpack x.x.x compiled successfully in x ms" `; -exports[`logging should logging on successfully build using the "stats" option for middleware with the "true" value: stderr 1`] = `""`; +exports[`logging standalone should logging on successfully build using the "stats" option for middleware with the "true" value: stderr 1`] = `""`; -exports[`logging should logging on successfully build using the "stats" option for middleware with the "true" value: stdout 1`] = ` +exports[`logging standalone should logging on successfully build using the "stats" option for middleware with the "true" value: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) @@ -346,9 +932,9 @@ cacheable modules x bytes webpack x.x.x compiled successfully in x ms" `; -exports[`logging should logging on successfully build using the "stats" option for middleware with the "verbose" value: stderr 1`] = `""`; +exports[`logging standalone should logging on successfully build using the "stats" option for middleware with the "verbose" value: stderr 1`] = `""`; -exports[`logging should logging on successfully build using the "stats" option for middleware with the "verbose" value: stdout 1`] = ` +exports[`logging standalone should logging on successfully build using the "stats" option for middleware with the "verbose" value: stdout 1`] = ` "PublicPath: auto asset bundle.js x KiB {main} [emitted] (name: main) asset svg.svg x KiB ({main}) [emitted] [from: svg.svg] (auxiliary name: main) @@ -389,17 +975,17 @@ cjs require ./svg.svg [./foo.js] 3:0-20 LOG from xxx" `; -exports[`logging should logging on successfully build using the "stats" option for middleware with the object value and colors: stderr 1`] = `""`; +exports[`logging standalone should logging on successfully build using the "stats" option for middleware with the object value and colors: stderr 1`] = `""`; -exports[`logging should logging on successfully build using the "stats" option for middleware with the object value and colors: stdout 1`] = ` +exports[`logging standalone should logging on successfully build using the "stats" option for middleware with the object value and colors: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main)" `; -exports[`logging should logging on successfully build when the 'stats' doesn't exist: stderr 1`] = `""`; +exports[`logging standalone should logging on successfully build when the 'stats' doesn't exist: stderr 1`] = `""`; -exports[`logging should logging on successfully build when the 'stats' doesn't exist: stdout 1`] = ` +exports[`logging standalone should logging on successfully build when the 'stats' doesn't exist: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) @@ -411,9 +997,9 @@ cacheable modules x bytes webpack x.x.x compiled successfully in x ms" `; -exports[`logging should logging on successfully build: stderr 1`] = `""`; +exports[`logging standalone should logging on successfully build: stderr 1`] = `""`; -exports[`logging should logging on successfully build: stdout 1`] = ` +exports[`logging standalone should logging on successfully build: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) @@ -425,9 +1011,9 @@ cacheable modules x bytes webpack x.x.x compiled successfully in x ms" `; -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with object value and colors: stderr 1`] = `""`; +exports[`logging standalone should logging on successfully multi-compiler build using the "stats" option for middleware with object value and colors: stderr 1`] = `""`; -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with object value and colors: stdout 1`] = ` +exports[`logging standalone should logging on successfully multi-compiler build using the "stats" option for middleware with object value and colors: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) @@ -435,9 +1021,9 @@ asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) asset bundle.js x KiB [emitted] (name: main)" `; -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with object value and no colors: stderr 1`] = `""`; +exports[`logging standalone should logging on successfully multi-compiler build using the "stats" option for middleware with object value and no colors: stderr 1`] = `""`; -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with object value and no colors: stdout 1`] = ` +exports[`logging standalone should logging on successfully multi-compiler build using the "stats" option for middleware with object value and no colors: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) @@ -445,13 +1031,13 @@ asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) asset bundle.js x KiB [emitted] (name: main)" `; -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the "false" value: stderr 1`] = `""`; +exports[`logging standalone should logging on successfully multi-compiler build using the "stats" option for middleware with the "false" value: stderr 1`] = `""`; -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the "false" value: stdout 1`] = `""`; +exports[`logging standalone should logging on successfully multi-compiler build using the "stats" option for middleware with the "false" value: stdout 1`] = `""`; -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the "normal" value: stderr 1`] = `""`; +exports[`logging standalone should logging on successfully multi-compiler build using the "stats" option for middleware with the "normal" value: stderr 1`] = `""`; -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the "normal" value: stdout 1`] = ` +exports[`logging standalone should logging on successfully multi-compiler build using the "stats" option for middleware with the "normal" value: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) @@ -467,9 +1053,9 @@ asset bundle.js x KiB [emitted] (name: main) webpack x.x.x compiled successfully in x ms" `; -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the "true" value: stderr 1`] = `""`; +exports[`logging standalone should logging on successfully multi-compiler build using the "stats" option for middleware with the "true" value: stderr 1`] = `""`; -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the "true" value: stdout 1`] = ` +exports[`logging standalone should logging on successfully multi-compiler build using the "stats" option for middleware with the "true" value: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) @@ -485,9 +1071,9 @@ asset bundle.js x KiB [emitted] (name: main) webpack x.x.x compiled successfully in x ms" `; -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the object value: stderr 1`] = `""`; +exports[`logging standalone should logging on successfully multi-compiler build using the "stats" option for middleware with the object value: stderr 1`] = `""`; -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the object value: stdout 1`] = ` +exports[`logging standalone should logging on successfully multi-compiler build using the "stats" option for middleware with the object value: stdout 1`] = ` "asset bundle.js x KiB [emitted] (name: main) asset svg.svg x KiB [emitted] [from: svg.svg] (auxiliary name: main) asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) @@ -495,9 +1081,9 @@ asset index.html x bytes [emitted] [from: index.html] (auxiliary name: main) asset bundle.js x KiB [emitted] (name: main)" `; -exports[`logging should logging on unsuccessful build in multi-compiler: stderr 1`] = `""`; +exports[`logging standalone should logging on unsuccessful build in multi-compiler: stderr 1`] = `""`; -exports[`logging should logging on unsuccessful build in multi-compiler: stdout 1`] = ` +exports[`logging standalone should logging on unsuccessful build in multi-compiler: stdout 1`] = ` "ERROR in ./broken.js 1:3 Module parse failed: Unexpected token (1:3) You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders @@ -515,9 +1101,9 @@ You may need an appropriate loader to handle this file type, currently no loader webpack compiled with 1 error" `; -exports[`logging should logging on unsuccessful build: stderr 1`] = `""`; +exports[`logging standalone should logging on unsuccessful build: stderr 1`] = `""`; -exports[`logging should logging on unsuccessful build: stdout 1`] = ` +exports[`logging standalone should logging on unsuccessful build: stdout 1`] = ` "ERROR in ./broken.js 1:3 Module parse failed: Unexpected token (1:3) You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders @@ -527,9 +1113,9 @@ You may need an appropriate loader to handle this file type, currently no loader webpack compiled with 1 error" `; -exports[`logging should logging warnings in multi-compiler mode: stderr 1`] = `""`; +exports[`logging standalone should logging warnings in multi-compiler mode: stderr 1`] = `""`; -exports[`logging should logging warnings in multi-compiler mode: stdout 1`] = ` +exports[`logging standalone should logging warnings in multi-compiler mode: stdout 1`] = ` "WARNING in Warning webpack compiled with 1 warning diff --git a/test/fixtures/webpack.array.dev-server-false-logging.js b/test/fixtures/webpack.array.dev-server-false-logging.js new file mode 100644 index 000000000..9210eafaa --- /dev/null +++ b/test/fixtures/webpack.array.dev-server-false-logging.js @@ -0,0 +1,44 @@ +'use strict'; + +const path = require('path'); + +module.exports = [ + { + mode: 'development', + context: path.resolve(__dirname), + entry: './bar.js', + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, '../outputs/dev-server-false-logging/js3'), + publicPath: '/static-one/', + }, + infrastructureLogging: { + level: 'none' + }, + stats: 'normal', + devServer: false, + }, + { + mode: 'development', + context: path.resolve(__dirname), + entry: './foo.js', + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, '../outputs/dev-server-false-logging/js4'), + publicPath: '/static-two/', + }, + module: { + rules: [ + { + test: /\.(svg|html)$/, + loader: 'file-loader', + options: { name: '[name].[ext]' }, + }, + ], + }, + infrastructureLogging: { + level: 'none' + }, + stats: 'normal' + } +]; diff --git a/test/fixtures/webpack.array.dev-server-false.js b/test/fixtures/webpack.array.dev-server-false.js index e23d44a2b..4687ecfea 100644 --- a/test/fixtures/webpack.array.dev-server-false.js +++ b/test/fixtures/webpack.array.dev-server-false.js @@ -10,7 +10,7 @@ module.exports = [ output: { filename: 'bundle.js', path: path.resolve(__dirname, '../outputs/dev-server-false/js3'), - publicPath: '/static-two/', + publicPath: '/static-one/', }, infrastructureLogging: { level: 'none' @@ -25,7 +25,7 @@ module.exports = [ output: { filename: 'bundle.js', path: path.resolve(__dirname, '../outputs/dev-server-false/js4'), - publicPath: '/static-one/', + publicPath: '/static-two/', }, module: { rules: [ diff --git a/test/fixtures/webpack.array.logging.config.js b/test/fixtures/webpack.array.logging.config.js new file mode 100644 index 000000000..62d8265a6 --- /dev/null +++ b/test/fixtures/webpack.array.logging.config.js @@ -0,0 +1,43 @@ +'use strict'; + +const path = require('path'); + +module.exports = [ + { + mode: 'development', + context: path.resolve(__dirname), + entry: './foo.js', + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, '../outputs/array-logging/js1'), + publicPath: '/static-one/', + }, + module: { + rules: [ + { + test: /\.(svg|html)$/, + loader: 'file-loader', + options: { name: '[name].[ext]' }, + }, + ], + }, + infrastructureLogging: { + level: 'none' + }, + stats: 'normal' + }, + { + mode: 'development', + context: path.resolve(__dirname), + entry: './bar.js', + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, '../outputs/array-logging/js2'), + publicPath: '/static-two/', + }, + infrastructureLogging: { + level: 'none' + }, + stats: 'normal' + }, +]; diff --git a/test/helpers/runner.js b/test/helpers/runner.js index 13fba0cae..a21554efb 100755 --- a/test/helpers/runner.js +++ b/test/helpers/runner.js @@ -10,6 +10,8 @@ const defaultConfig = require("../fixtures/webpack.config"); const configEntries = []; const configMiddlewareEntries = []; +const isPlugin = process.argv.includes("--plugin"); + /** * @param {string} NSKey NSKey * @param {string[]} accumulator accumulator @@ -89,21 +91,6 @@ if (Array.isArray(config)) { config.parallelism = 1; } -const compiler = webpack(config); - -if (process.env.WEBPACK_BREAK_WATCH) { - compiler.watch = function watch() { - const error = new Error("Watch error"); - error.code = "watch error"; - - throw error; - }; -} - -compiler.hooks.done.tap("plugin-test", () => { - process.stdout.write("compiled-for-tests"); -}); - switch (process.env.WEBPACK_DEV_MIDDLEWARE_STATS) { case "object": configMiddleware.stats = { all: false, assets: true }; @@ -118,39 +105,148 @@ switch (process.env.WEBPACK_DEV_MIDDLEWARE_STATS) { // Nothing } -const instance = middleware(compiler, configMiddleware); -const app = express(); +let commands = []; +let incompleteCommand = ""; + +const handleStdin = (chunk) => { + const entries = chunk.toString().split("|"); + + incompleteCommand += entries.shift(); + commands.push(incompleteCommand); + incompleteCommand = entries.pop(); + commands = [...commands, ...entries]; + + while (commands.length > 0) { + switch (commands.shift()) { + // case 'invalidate': + // stdinInput = ''; + // instance.waitUntilValid(() => { + // instance.invalidate(); + // }); + // break; + case "exit": + // eslint-disable-next-line n/no-process-exit + process.exit(); + break; + } + } +}; + +/** + * @param {import("webpack").Compiler} compiler compiler + * @param {import("webpack").StatsOptions} statsOptions stats options + * @returns {{ preset: string }} normalized stats + */ +function normalizeStatsOptions(compiler, statsOptions) { + if (typeof statsOptions === "undefined") { + statsOptions = { preset: "normal" }; + } else if (typeof statsOptions === "boolean") { + statsOptions = statsOptions ? { preset: "normal" } : { preset: "none" }; + } else if (typeof statsOptions === "string") { + statsOptions = { preset: statsOptions }; + } + + if (typeof statsOptions.colors === "undefined") { + statsOptions.colors = compiler.webpack.cli.isColorSupported(); + } -app.use(instance); -app.listen((error) => { - if (error) { - throw error; + // Just for test, in the real world webpack-dev-middleware doesn't support stats options as a plugin + switch (process.env.WEBPACK_DEV_MIDDLEWARE_STATS) { + case "object_colors_true": + statsOptions.colors = true; + break; + case "object_colors_false": + statsOptions.colors = false; + break; } - let commands = []; - let incompleteCommand = ""; - - process.stdin.on("data", (chunk) => { - const entries = chunk.toString().split("|"); - - incompleteCommand += entries.shift(); - commands.push(incompleteCommand); - incompleteCommand = entries.pop(); - commands = [...commands, ...entries]; - - while (commands.length > 0) { - switch (commands.shift()) { - // case 'invalidate': - // stdinInput = ''; - // instance.waitUntilValid(() => { - // instance.invalidate(); - // }); - // break; - case "exit": - // eslint-disable-next-line n/no-process-exit - process.exit(); - break; - } + return statsOptions; +} + +/** + * @param {import("webpack").Configuration} config configuration + * @returns {import("webpack").Configuration} configuration with the test plugin + */ +function addPlugin(config) { + if (!config.plugins) config.plugins = []; + + config.plugins.push({ + apply(compiler) { + let app; + + compiler.hooks.done.tap("webpack-dev-middleware-test", () => { + const instance = middleware(compiler, configMiddleware, true); + + app = express(); + app.use(instance); + app.listen((error) => { + if (error) { + throw error; + } + + process.stdin.on("data", handleStdin); + }); + }); + }, + }); + + return config; +} + +if (isPlugin) { + const isMultiCompiler = Array.isArray(config); + + const compiler = webpack( + isMultiCompiler ? config.map((item) => addPlugin(item)) : addPlugin(config), + ); + + compiler.watch({}, (err, stats) => { + if (err) { + throw err; + } + + if (process.env.WEBPACK_BREAK_WATCH) { + const error = new Error("Watch error"); + error.code = "watch error"; + + throw error; } + + const statsOptions = isMultiCompiler + ? { + children: config.map((config, idx) => + normalizeStatsOptions(compiler.compilers[idx], config.stats), + ), + } + : normalizeStatsOptions(compiler, config.stats); + + process.stdout.write("compiled-for-tests"); + process.stdout.write(stats.toString(statsOptions)); }); -}); +} else { + const compiler = webpack(config); + + if (process.env.WEBPACK_BREAK_WATCH) { + compiler.watch = function watch() { + const error = new Error("Watch error"); + error.code = "watch error"; + + throw error; + }; + } + + compiler.hooks.done.tap("plugin-test", () => { + process.stdout.write("compiled-for-tests"); + }); + const instance = middleware(compiler, configMiddleware); + const app = express(); + + app.use(instance); + app.listen((error) => { + if (error) { + throw error; + } + + process.stdin.on("data", handleStdin); + }); +} diff --git a/test/logging.test.js b/test/logging.test.js index aa9109818..117ef1d32 100644 --- a/test/logging.test.js +++ b/test/logging.test.js @@ -1,8 +1,8 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; - import { stripVTControlCharacters } from "node:util"; + import execa from "execa"; function extractErrorEntry(string) { @@ -65,9 +65,20 @@ function stderrToSnapshot(stderr) { const runner = path.resolve(__dirname, "./helpers/runner.js"); -describe("logging", () => { +const scenarios = [ + { + name: "standalone", + args: [], + }, + { + name: "plugin", + args: ["--plugin"], + }, +]; + +describe.each(scenarios)("logging $name", ({ args }) => { it("should logging on successfully build", (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.config", @@ -105,7 +116,7 @@ describe("logging", () => { }); it("should logging on successfully build and respect colors", (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.stats-colors-true.config.js", @@ -143,7 +154,7 @@ describe("logging", () => { }); it("should logging on successfully build and respect colors #2", (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.stats-colors-false.config.js", @@ -181,7 +192,7 @@ describe("logging", () => { }); it("should logging on successfully build when the 'stats' doesn't exist", (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.no-stats.config.js", @@ -219,7 +230,7 @@ describe("logging", () => { }); it('should logging on successfully build and respect the "stats" option from configuration with the "none" value', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.stats-none.config.js", @@ -255,7 +266,7 @@ describe("logging", () => { }); it('should logging on successfully build and respect the "stats" option from configuration with the "minimal" value', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.stats-minimal.config", @@ -293,7 +304,7 @@ describe("logging", () => { }); it('should logging on successfully build and respect the "stats" option from configuration with the "verbose" value', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.stats-verbose.config", @@ -331,7 +342,7 @@ describe("logging", () => { }); it('should logging on successfully build and respect the "stats" option from configuration with the "true" value', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.stats-true.config", @@ -369,7 +380,7 @@ describe("logging", () => { }); it('should logging on successfully build and respect the "stats" option from configuration with the "false" value', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.stats-false.config", @@ -405,7 +416,7 @@ describe("logging", () => { }); it('should logging on successfully build and respect the "stats" option from configuration with custom object value', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.stats-object.config", @@ -443,10 +454,10 @@ describe("logging", () => { }); it("should logging on successfully build in multi-compiler mode", (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { - WEBPACK_CONFIG: "webpack.array.config", + WEBPACK_CONFIG: "webpack.array.logging.config.js", FORCE_COLOR: true, }, }); @@ -481,7 +492,7 @@ describe("logging", () => { }); it("should logging on unsuccessful build", (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.error.config", @@ -519,7 +530,7 @@ describe("logging", () => { }); it("should logging on unsuccessful build in multi-compiler", (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.array.error.config", @@ -557,7 +568,7 @@ describe("logging", () => { }); it("should logging an warning", (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.warning.config", @@ -595,7 +606,7 @@ describe("logging", () => { }); it("should logging warnings in multi-compiler mode", (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.array.warning.config", @@ -633,7 +644,7 @@ describe("logging", () => { }); it('should logging in multi-compiler and respect the "stats" option from configuration', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.array.one-error-one-warning-one-success", @@ -671,7 +682,7 @@ describe("logging", () => { }); it('should logging in multi-compiler and respect the "stats" option from configuration #2', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: @@ -710,7 +721,7 @@ describe("logging", () => { }); it('should logging in multi-compiler and respect the "stats" option from configuration #3', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.array.one-error-one-warning-one-no", @@ -748,7 +759,7 @@ describe("logging", () => { }); it('should logging in multi-compiler and respect the "stats" option from configuration #4', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.array.one-error-one-warning-one-object", @@ -786,10 +797,10 @@ describe("logging", () => { }); it('should logging in multi-compiler and respect the "stats" option from configuration #5', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { - WEBPACK_CONFIG: "webpack.array.dev-server-false", + WEBPACK_CONFIG: "webpack.array.dev-server-false-logging", FORCE_COLOR: true, }, }); @@ -824,7 +835,7 @@ describe("logging", () => { }); it('should logging an error in "watch" method', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_BREAK_WATCH: true, @@ -859,7 +870,7 @@ describe("logging", () => { fs.mkdirSync(outputDir, { recursive: true }); fs.chmodSync(outputDir, 0o400); - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.simple.config", @@ -893,7 +904,7 @@ describe("logging", () => { } it('should logging on successfully build using the "stats" option for middleware with the "true" value', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.config", @@ -932,7 +943,7 @@ describe("logging", () => { }); it('should logging on successfully build using the "stats" option for middleware with the "false" value', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.config", @@ -970,7 +981,7 @@ describe("logging", () => { }); it('should logging on successfully build using the "stats" option for middleware with the "none" value', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.config", @@ -1008,7 +1019,7 @@ describe("logging", () => { }); it('should logging on successfully build using the "stats" option for middleware with the "normal" value', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.config", @@ -1047,7 +1058,7 @@ describe("logging", () => { }); it('should logging on successfully build using the "stats" option for middleware with the "verbose" value', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.config", @@ -1086,7 +1097,7 @@ describe("logging", () => { }); it('should logging on successfully build using the "stats" option for middleware with object value', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.config", @@ -1125,7 +1136,7 @@ describe("logging", () => { }); it('should logging on successfully build using the "stats" option for middleware with the object value and colors', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.config", @@ -1164,7 +1175,7 @@ describe("logging", () => { }); it('should logging on successfully build using the "stats" option for middleware with object value and no colors', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.config", @@ -1203,10 +1214,10 @@ describe("logging", () => { }); it('should logging on successfully multi-compiler build using the "stats" option for middleware with the "true" value', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { - WEBPACK_CONFIG: "webpack.array.config", + WEBPACK_CONFIG: "webpack.array.logging.config.js", WMC_stats: true, FORCE_COLOR: true, }, @@ -1242,10 +1253,10 @@ describe("logging", () => { }); it('should logging on successfully multi-compiler build using the "stats" option for middleware with the "false" value', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { - WEBPACK_CONFIG: "webpack.array.config", + WEBPACK_CONFIG: "webpack.array.logging.config.js", WMC_stats: false, FORCE_COLOR: true, }, @@ -1280,10 +1291,10 @@ describe("logging", () => { }); it('should logging on successfully multi-compiler build using the "stats" option for middleware with the "normal" value', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { - WEBPACK_CONFIG: "webpack.array.config", + WEBPACK_CONFIG: "webpack.array.logging.config.js", WMC_stats: "normal", FORCE_COLOR: true, }, @@ -1319,10 +1330,10 @@ describe("logging", () => { }); it('should logging on successfully multi-compiler build using the "stats" option for middleware with the object value', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { - WEBPACK_CONFIG: "webpack.array.config", + WEBPACK_CONFIG: "webpack.array.logging.config.js", WEBPACK_DEV_MIDDLEWARE_STATS: "object", FORCE_COLOR: true, }, @@ -1358,10 +1369,10 @@ describe("logging", () => { }); it('should logging on successfully multi-compiler build using the "stats" option for middleware with object value and colors', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { - WEBPACK_CONFIG: "webpack.array.config", + WEBPACK_CONFIG: "webpack.array.logging.config.js", WEBPACK_DEV_MIDDLEWARE_STATS: "object_colors_true", FORCE_COLOR: true, }, @@ -1397,10 +1408,10 @@ describe("logging", () => { }); it('should logging on successfully multi-compiler build using the "stats" option for middleware with object value and no colors', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { - WEBPACK_CONFIG: "webpack.array.config", + WEBPACK_CONFIG: "webpack.array.logging.config.js", WEBPACK_DEV_MIDDLEWARE_STATS: "object_colors_false", FORCE_COLOR: true, }, @@ -1436,7 +1447,7 @@ describe("logging", () => { }); it('should logging on successfully build and respect the "NO_COLOR" env', (done) => { - const proc = execa(runner, [], { + const proc = execa(runner, args, { stdio: "pipe", env: { WEBPACK_CONFIG: "webpack.config", diff --git a/test/middleware.test.js b/test/middleware.test.js index 683684dfe..36e8f6091 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -1450,19 +1450,20 @@ describe.each([ await close(server, instance); }); - it('should return "200" code for GET request to the bundle file for the first compiler', async () => { + it('should return "404" code for GET request to the bundle file for the first compiler', async () => { const bundlePath = path.resolve( __dirname, - "./outputs/dev-server-false/js4/", - ); - - expect(fs.existsSync(path.resolve(bundlePath, "bundle.js"))).toBe( - false, + "./outputs/dev-server-false/js3/", ); const response = await req.get("/static-one/bundle.js"); - expect(response.statusCode).toBe(200); + expect(response.statusCode).toBe(404); + + // Stored in the real fs + expect(fs.existsSync(path.resolve(bundlePath, "bundle.js"))).toBe( + true, + ); }); it('should return "404" code for GET request to a non existing file for the first compiler', async () => { @@ -1471,37 +1472,32 @@ describe.each([ expect(response.statusCode).toBe(404); }); - it('should return "200" code for GET request to the "public" path for the first compiler', async () => { + it('should return "404" code for GET request to the "public" path for the first compiler', async () => { const response = await req.get("/static-one/"); - expect(response.statusCode).toBe(200); - expect(response.headers["content-type"]).toBe( - "text/html; charset=utf-8", - ); + expect(response.statusCode).toBe(404); }); - it('should return "200" code for GET request to the "index" option for the first compiler', async () => { + it('should return "404" code for GET request to the "index" option for the first compiler', async () => { const response = await req.get("/static-one/index.html"); - expect(response.statusCode).toBe(200); - expect(response.headers["content-type"]).toBe( - "text/html; charset=utf-8", - ); + expect(response.statusCode).toBe(404); }); it('should return "200" code for GET request for the bundle file for the second compiler', async () => { const bundlePath = path.resolve( __dirname, - "./outputs/dev-server-false/js3/", - ); - - expect(fs.existsSync(path.resolve(bundlePath, "bundle.js"))).toBe( - true, + "./outputs/dev-server-false/js4/", ); const response = await req.get("/static-two/bundle.js"); - expect(response.statusCode).toBe(404); + expect(response.statusCode).toBe(200); + + // stored in memory + expect(fs.existsSync(path.resolve(bundlePath, "bundle.js"))).toBe( + false, + ); }); it('should return "404" code for GET request to a non existing file for the second compiler', async () => { @@ -1510,16 +1506,22 @@ describe.each([ expect(response.statusCode).toBe(404); }); - it('should return "404" code for GET request to the "public" path for the second compiler', async () => { + it('should return "200" code for GET request to the "public" path for the second compiler', async () => { const response = await req.get("/static-two/"); - expect(response.statusCode).toBe(404); + expect(response.statusCode).toBe(200); + expect(response.headers["content-type"]).toBe( + "text/html; charset=utf-8", + ); }); - it('should return "404" code for GET request to the "index" option for the second compiler', async () => { + it('should return "200" code for GET request to the "index" option for the second compiler', async () => { const response = await req.get("/static-two/index.html"); - expect(response.statusCode).toBe(404); + expect(response.statusCode).toBe(200); + expect(response.headers["content-type"]).toBe( + "text/html; charset=utf-8", + ); }); it('should return "404" code for GET request to the non-public path', async () => { @@ -3764,23 +3766,18 @@ describe.each([ } }); - // TODO: why koa and hono don't catch for their error handling when stream emit error? - (name === "koa" || name === "hono" ? it.skip : it)( - 'should return the "500" code for the "GET" request to the "image.svg" file when it throws a reading error', - async () => { - const response = await req.get("/image.svg"); - - // eslint-disable-next-line jest/no-standalone-expect - expect(response.statusCode).toBe(500); - if (name !== "hapi") { - // eslint-disable-next-line jest/no-standalone-expect - expect(nextWasCalled).toBe(true); - } else { - // eslint-disable-next-line jest/no-standalone-expect - expect(nextWasCalled).toBe(false); - } - }, - ); + it('should return the "500" code for the "GET" request to the "image.svg" file when it throws a reading error', async () => { + const response = await req.get("/image.svg"); + + expect(response.statusCode).toBe(500); + + // hapi and hono don't support passthrough errors + if (name === "hapi" || name === "hono") { + expect(nextWasCalled).toBe(false); + } else { + expect(nextWasCalled).toBe(true); + } + }); it('should return the "200" code for the "HEAD" request to the bundle file', async () => { const response = await req.head("/file.text"); @@ -4236,52 +4233,50 @@ describe.each([ }); describe("writeToDisk option", () => { - (name === "hono" ? describe.skip : describe)( - 'should work with "true" value', - () => { - let compiler; - - const outputPath = path.resolve( - __dirname, - "./outputs/write-to-disk-true", - ); + describe('should work with "true" value', () => { + let compiler; - beforeAll(async () => { - compiler = getCompiler({ - ...webpackConfig, - output: { - filename: "bundle.js", - path: outputPath, - publicPath: "/public/", - }, - }); + const outputPath = path.resolve( + __dirname, + "./outputs/write-to-disk-true", + ); - [server, req, instance] = await frameworkFactory( - name, - framework, - compiler, - { writeToDisk: true }, - ); + beforeAll(async () => { + compiler = getCompiler({ + ...webpackConfig, + output: { + filename: "bundle.js", + path: outputPath, + publicPath: "/public/", + }, }); - afterAll(async () => { - await fs.promises.rm(outputPath, { - recursive: true, - force: true, - }); - await close(server, instance); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { writeToDisk: true }, + ); + }); + + afterAll(async () => { + await fs.promises.rm(outputPath, { + recursive: true, + force: true, }); + await close(server, instance); + }); - it("should find the bundle file on disk", (done) => { - req.get("/public/bundle.js").expect(200, (error) => { - if (error) { - return done(error); - } + it("should find the bundle file on disk", (done) => { + req.get("/public/bundle.js").expect(200, (error) => { + if (error) { + return done(error); + } - const bundlePath = path.resolve( - __dirname, - "./outputs/write-to-disk-true/bundle.js", - ); + const bundlePath = path.resolve( + __dirname, + "./outputs/write-to-disk-true/bundle.js", + ); expect( compiler.hooks.assetEmitted.taps.filter( @@ -4290,7 +4285,7 @@ describe.each([ ).toHaveLength(0); expect(fs.existsSync(bundlePath)).toBe(true); - instance.invalidate(); + instance.invalidate(); return compiler.hooks.done.tap( "DevMiddlewareWriteToDiskTest", @@ -4301,22 +4296,20 @@ describe.each([ ), ).toHaveLength(0); - done(); - }, - ); - }); + done(); + }, + ); }); + }); - it("should not allow to get files above root", async () => { - const response = await req.get( - "/public/..%2f../middleware.test.js", - ); + it("should not allow to get files above root", async () => { + const response = await req.get("/public/..%2f../middleware.test.js"); - expect(response.statusCode).toBe(403); - expect(response.headers["content-type"]).toBe( - "text/html; charset=utf-8", - ); - expect(response.text).toBe(` + expect(response.statusCode).toBe(403); + expect(response.headers["content-type"]).toBe( + "text/html; charset=utf-8", + ); + expect(response.text).toBe(` @@ -4326,9 +4319,8 @@ describe.each([
Forbidden
`); - }); - }, - ); + }); + }); describe('should work with "true" value when the `output.clean` is `true`', () => { const outputPath = path.resolve( diff --git a/types/index.d.ts b/types/index.d.ts index e3fb18505..df53752f7 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -153,6 +153,7 @@ export = wdm; * @template {ServerResponse} [ResponseInternal=ServerResponse] * @param {Compiler | MultiCompiler} compiler compiler * @param {Options=} options options + * @param {boolean} isPlugin true when will use as a plugin, otherwise false * @returns {API} webpack dev middleware */ declare function wdm< @@ -161,6 +162,7 @@ declare function wdm< >( compiler: Compiler | MultiCompiler, options?: Options | undefined, + isPlugin?: boolean, ): API; declare namespace wdm { export { @@ -225,17 +227,19 @@ declare namespace wdm { /** * @template HapiServer * @template {HapiOptions} HapiOptionsInternal + * @param {boolean=} usePlugin true when need to use as a plugin, otherwise false * @returns {HapiPlugin} hapi wrapper */ declare function hapiWrapper< HapiServer, HapiOptionsInternal extends HapiOptions, ->(): HapiPlugin; +>(usePlugin?: boolean | undefined): HapiPlugin; /** * @template {IncomingMessage} [RequestInternal=IncomingMessage] * @template {ServerResponse} [ResponseInternal=ServerResponse] * @param {Compiler | MultiCompiler} compiler compiler * @param {Options=} options options + * @param {boolean=} usePlugin whether to use as webpack plugin * @returns {(ctx: EXPECTED_ANY, next: EXPECTED_FUNCTION) => Promise | void} kow wrapper */ declare function koaWrapper< @@ -244,12 +248,14 @@ declare function koaWrapper< >( compiler: Compiler | MultiCompiler, options?: Options | undefined, + usePlugin?: boolean | undefined, ): (ctx: EXPECTED_ANY, next: EXPECTED_FUNCTION) => Promise | void; /** * @template {IncomingMessage} [RequestInternal=IncomingMessage] * @template {ServerResponse} [ResponseInternal=ServerResponse] * @param {Compiler | MultiCompiler} compiler compiler * @param {Options=} options options + * @param {boolean=} usePlugin true when need to use as a plugin, otherwise false * @returns {(ctx: EXPECTED_ANY, next: EXPECTED_FUNCTION) => Promise | void} hono wrapper */ declare function honoWrapper< @@ -258,6 +264,7 @@ declare function honoWrapper< >( compiler: Compiler | MultiCompiler, options?: Options | undefined, + usePlugin?: boolean | undefined, ): (ctx: EXPECTED_ANY, next: EXPECTED_FUNCTION) => Promise | void; type Schema = import("schema-utils/declarations/validate").Schema; type Compiler = import("webpack").Compiler; diff --git a/types/utils/setupHooks.d.ts b/types/utils/setupHooks.d.ts index 1ec0e489b..512582e48 100644 --- a/types/utils/setupHooks.d.ts +++ b/types/utils/setupHooks.d.ts @@ -13,6 +13,7 @@ export = setupHooks; * @template {IncomingMessage} Request * @template {ServerResponse} Response * @param {import("../index.js").WithOptional, "watching" | "outputFileSystem">} context context + * @param {boolean=} isPlugin true when it is a plugin usage, otherwise false */ declare function setupHooks< Request extends IncomingMessage, @@ -22,6 +23,7 @@ declare function setupHooks< import("../index.js").Context, "watching" | "outputFileSystem" >, + isPlugin?: boolean | undefined, ): void; declare namespace setupHooks { export { From 9f3ceedd2547bb14bfd32e95d333bcbf976140a8 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Wed, 11 Mar 2026 16:11:35 +0300 Subject: [PATCH 09/13] refactor: rebase --- test/middleware.test.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/test/middleware.test.js b/test/middleware.test.js index 36e8f6091..9547d6371 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -4278,23 +4278,23 @@ describe.each([ "./outputs/write-to-disk-true/bundle.js", ); - expect( - compiler.hooks.assetEmitted.taps.filter( - (hook) => hook.name === "DevMiddleware", - ), - ).toHaveLength(0); - expect(fs.existsSync(bundlePath)).toBe(true); + expect( + compiler.hooks.assetEmitted.taps.filter( + (hook) => hook.name === "DevMiddleware", + ), + ).toHaveLength(0); + expect(fs.existsSync(bundlePath)).toBe(true); instance.invalidate(); - return compiler.hooks.done.tap( - "DevMiddlewareWriteToDiskTest", - () => { - expect( - compiler.hooks.assetEmitted.taps.filter( - (hook) => hook.name === "DevMiddleware", - ), - ).toHaveLength(0); + return compiler.hooks.done.tap( + "DevMiddlewareWriteToDiskTest", + () => { + expect( + compiler.hooks.assetEmitted.taps.filter( + (hook) => hook.name === "DevMiddleware", + ), + ).toHaveLength(0); done(); }, From f991e2adfbe109ba5c5791cda7400a039bbc3dda Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Wed, 11 Mar 2026 16:24:20 +0300 Subject: [PATCH 10/13] ci: update --- .github/workflows/dependency-review.yml | 4 ++-- .github/workflows/nodejs.yml | 15 ++++++--------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index ee0524fd6..3e183e1f0 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -9,6 +9,6 @@ jobs: runs-on: ubuntu-latest steps: - name: "Checkout Repository" - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: "Dependency Review" - uses: actions/dependency-review-action@v4 + uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 878bf751e..897bbf71c 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -31,12 +31,12 @@ jobs: group: lint-${{ matrix.os }}-v${{ matrix.node-version }}-${{ github.ref }} cancel-in-progress: true steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: ${{ matrix.node-version }} cache: "npm" @@ -53,9 +53,6 @@ jobs: - name: Check types run: if [ -n "$(git status types --porcelain)" ]; then echo "Missing types. Update types by running 'npm run build:types'"; exit 1; else echo "All types are valid"; fi - - name: Security audit - run: npm run security - - name: Validate PR commits with commitlint if: github.event_name == 'pull_request' run: npx commitlint --from ${{ github.event.pull_request.head.sha }}~${{ github.event.pull_request.commits }} --to ${{ github.event.pull_request.head.sha }} --verbose @@ -67,7 +64,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - node-version: [20.x, 22.x, 24.x] + node-version: [20.x, 22.x, 24.x, 25.x] webpack-version: [latest] runs-on: ${{ matrix.os }} @@ -77,10 +74,10 @@ jobs: cancel-in-progress: true steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: ${{ matrix.node-version }} cache: "npm" @@ -92,6 +89,6 @@ jobs: run: npm run test:coverage -- --ci - name: Submit coverage data to codecov - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 with: token: ${{ secrets.CODECOV_TOKEN }} From 3049ea5afdbd06b56f81eedcf2de1593ded56068 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Wed, 11 Mar 2026 16:39:51 +0300 Subject: [PATCH 11/13] refactor: safe stream for hono --- src/index.js | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index 7e845342e..4e544f015 100644 --- a/src/index.js +++ b/src/index.js @@ -639,10 +639,34 @@ function honoWrapper(compiler, options, usePlugin) { * @param {import("fs").ReadStream} stream readable stream */ res.stream = (stream) => { - body = stream; + let isResolved = false; - isFinished = true; - resolve(); + /** + * @param {Error=} err err + */ + const done = (err) => { + if (isResolved) return; + isResolved = true; + + stream.removeListener("error", done); + stream.removeListener("readable", done); + stream.removeListener("end", done); + + if (err) { + stream.destroy(); + reject(err); + return; + } + + body = stream; + isFinished = true; + resolve(); + }; + + stream.once("error", done); + stream.once("readable", done); + // Empty stream + stream.once("end", done); }; /** From 48a021bdca3e30ed08e0f49bdd58d34b5d85ce56 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Wed, 11 Mar 2026 17:00:40 +0300 Subject: [PATCH 12/13] test: fix --- test/middleware.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/middleware.test.js b/test/middleware.test.js index 9547d6371..02fda1cf6 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -3771,8 +3771,8 @@ describe.each([ expect(response.statusCode).toBe(500); - // hapi and hono don't support passthrough errors - if (name === "hapi" || name === "hono") { + // hapi doesn't support passthrough errors? + if (name === "hapi") { expect(nextWasCalled).toBe(false); } else { expect(nextWasCalled).toBe(true); From 6cfc4f12fcbf7203a13f790b52f0eefd40f69481 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Wed, 11 Mar 2026 17:06:37 +0300 Subject: [PATCH 13/13] test: debug --- src/index.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/index.js b/src/index.js index 4e544f015..49b1fd222 100644 --- a/src/index.js +++ b/src/index.js @@ -644,13 +644,13 @@ function honoWrapper(compiler, options, usePlugin) { /** * @param {Error=} err err */ - const done = (err) => { + const onEvent = (err) => { if (isResolved) return; isResolved = true; - stream.removeListener("error", done); - stream.removeListener("readable", done); - stream.removeListener("end", done); + stream.removeListener("error", onEvent); + stream.removeListener("readable", onEvent); + stream.removeListener("end", onEvent); if (err) { stream.destroy(); @@ -663,10 +663,14 @@ function honoWrapper(compiler, options, usePlugin) { resolve(); }; - stream.once("error", done); - stream.once("readable", done); + stream.once("error", onEvent); + stream.once("readable", onEvent); // Empty stream - stream.once("end", done); + stream.once("end", onEvent); + + if (stream.pending === false) { + onEvent(); + } }; /**