├── .husky ├── pre-commit └── commit-msg ├── test ├── fixtures │ ├── broken.js │ ├── index.html │ ├── simple.js │ ├── warning.js │ ├── immutable.js │ ├── bar.js │ ├── foo.js │ ├── webpack.error.config.js │ ├── webpack.immutable.config.js │ ├── webpack.simple.config.js │ ├── webpack.watch-options.config.js │ ├── webpack.config.js │ ├── webpack.querystring.config.js │ ├── webpack.warning.config.js │ ├── webpack.no-stats.config.js │ ├── webpack.public-path.config.js │ ├── webpack.stats-false.config.js │ ├── webpack.stats-none.config.js │ ├── webpack.stats-true.config.js │ ├── webpack.stats-minimal.config.js │ ├── webpack.stats-verbose.config.js │ ├── webpack.stats-object.config.js │ ├── webpack.stats-colors-false.config.js │ ├── webpack.stats-colors-true.config.js │ ├── webpack.array.error.config.js │ ├── webpack.array.config.js │ ├── webpack.client.server.config.js │ ├── webpack.array.dev-server-false.js │ ├── webpack.array.watch-options.config.js │ ├── webpack.array.warning.config.js │ ├── webpack.array.one-error-one-warning-one-no.js │ ├── webpack.array.one-error-one-warning-one-success.js │ ├── webpack.array.one-error-one-warning-one-object.js │ ├── webpack.array.one-error-one-warning-one-success-with-names.js │ └── svg.svg ├── utils │ ├── __snapshots__ │ │ ├── setupHooks.test.js.snap.webpack5 │ │ └── setupWriteToDisk.test.js.snap.webpack5 │ ├── escapeHtml.test.js │ ├── setupOutputFileSystem.test.js │ ├── ready.test.js │ ├── setupWriteToDisk.test.js │ └── setupHooks.test.js ├── helpers │ ├── getCompilerHooks.js │ ├── getCompiler.js │ ├── listenAndCompile.js │ ├── snapshotResolver.js │ └── runner.js ├── validation-options.test.js └── __snapshots__ │ └── validation-options.test.js.snap.webpack5 ├── setupTest.js ├── .prettierignore ├── .gitattributes ├── types ├── utils │ ├── escapeHtml.d.ts │ ├── parseTokenList.d.ts │ ├── etag.d.ts │ ├── memorize.d.ts │ ├── ready.d.ts │ ├── setupOutputFileSystem.d.ts │ ├── setupWriteToDisk.d.ts │ ├── getPaths.d.ts │ ├── getFilenameFromUrl.d.ts │ ├── setupHooks.d.ts │ └── compatibleAPI.d.ts ├── middleware.d.ts └── index.d.ts ├── lint-staged.config.js ├── scripts └── globalSetup.js ├── commitlint.config.js ├── .editorconfig ├── .gitignore ├── tsconfig.json ├── eslint.config.mjs ├── .github ├── dependabot.yml └── workflows │ ├── dependency-review.yml │ └── nodejs.yml ├── babel.config.js ├── jest.config.js ├── .cspell.json ├── src ├── utils │ ├── ready.js │ ├── parseTokenList.js │ ├── memorize.js │ ├── escapeHtml.js │ ├── getPaths.js │ ├── setupOutputFileSystem.js │ ├── etag.js │ ├── setupWriteToDisk.js │ ├── getFilenameFromUrl.js │ ├── setupHooks.js │ └── compatibleAPI.js ├── options.json └── index.js ├── LICENSE ├── package.json ├── CONTRIBUTING.md └── CHANGELOG.md /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | lint-staged 2 | -------------------------------------------------------------------------------- /test/fixtures/broken.js: -------------------------------------------------------------------------------- 1 | 1()2()3() 2 | -------------------------------------------------------------------------------- /test/fixtures/index.html: -------------------------------------------------------------------------------- 1 | My Index. 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | commitlint --edit $1 2 | -------------------------------------------------------------------------------- /test/fixtures/simple.js: -------------------------------------------------------------------------------- 1 | console.log('foo'); 2 | -------------------------------------------------------------------------------- /test/fixtures/warning.js: -------------------------------------------------------------------------------- 1 | console.log('foo'); 2 | -------------------------------------------------------------------------------- /setupTest.js: -------------------------------------------------------------------------------- 1 | /* global jest */ 2 | 3 | jest.setTimeout(20000); 4 | -------------------------------------------------------------------------------- /test/fixtures/immutable.js: -------------------------------------------------------------------------------- 1 | new URL("./svg.svg", import.meta.url); 2 | -------------------------------------------------------------------------------- /test/fixtures/bar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | console.log('Bar'); // eslint-disable-line no-console 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /node_modules 4 | /test/fixtures 5 | /test/outputs 6 | CHANGELOG.md 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | test/fixtures/** eol=lf 3 | bin/* eol=lf 4 | yarn.lock -diff 5 | package-lock.json -diff 6 | -------------------------------------------------------------------------------- /test/fixtures/foo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./svg.svg'); 4 | require('./index.html'); 5 | 6 | console.log('Hey.'); // eslint-disable-line no-console 7 | -------------------------------------------------------------------------------- /types/utils/escapeHtml.d.ts: -------------------------------------------------------------------------------- 1 | export = escapeHtml; 2 | /** 3 | * @param {string} string raw HTML 4 | * @returns {string} escaped HTML 5 | */ 6 | declare function escapeHtml(string: string): string; 7 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*": [ 3 | "prettier --cache --write --ignore-unknown", 4 | "cspell --cache --no-must-find-files", 5 | ], 6 | "*.js": ["eslint --cache --fix"], 7 | }; 8 | -------------------------------------------------------------------------------- /scripts/globalSetup.js: -------------------------------------------------------------------------------- 1 | const { version } = require("webpack"); 2 | 3 | module.exports = () => 4 | // eslint-disable-next-line no-console 5 | console.log(`\n Running tests for webpack @${version} \n`); 6 | -------------------------------------------------------------------------------- /types/utils/parseTokenList.d.ts: -------------------------------------------------------------------------------- 1 | export = parseTokenList; 2 | /** 3 | * Parse a HTTP token list. 4 | * @param {string} str str 5 | * @returns {string[]} tokens 6 | */ 7 | declare function parseTokenList(str: string): string[]; 8 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | rules: { 4 | "header-max-length": [0], 5 | "body-max-line-length": [0], 6 | "footer-max-line-length": [0], 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | insert_final_newline = true 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .idea/ 3 | /coverage 4 | logs 5 | *.log 6 | npm-debug.log* 7 | .eslintcache 8 | .cspellcache 9 | /dist 10 | /local 11 | /reports 12 | /test/outputs 13 | .DS_Store 14 | Thumbs.db 15 | .idea 16 | *.iml 17 | .vscode 18 | *.sublime-project 19 | *.sublime-workspace 20 | yarn.lock 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "allowJs": true, 6 | "checkJs": true, 7 | "strict": true, 8 | "types": ["node"], 9 | "resolveJsonModule": true, 10 | "allowSyntheticDefaultImports": true 11 | }, 12 | "include": ["./src/**/*"] 13 | } 14 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "eslint/config"; 2 | import configs from "eslint-config-webpack/configs.js"; 3 | 4 | export default defineConfig([ 5 | { 6 | extends: [configs["recommended-dirty"]], 7 | }, 8 | { 9 | files: ["test/helpers/runner.js"], 10 | rules: { 11 | "n/hashbang": "off", 12 | }, 13 | }, 14 | ]); 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | timezone: Europe/Berlin 9 | open-pull-requests-limit: 10 10 | versioning-strategy: lockfile-only 11 | groups: 12 | dependencies: 13 | patterns: 14 | - "*" 15 | -------------------------------------------------------------------------------- /test/fixtures/webpack.error.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | context: path.resolve(__dirname), 8 | entry: './broken.js', 9 | output: { 10 | filename: 'bundle.js', 11 | path: path.resolve(__dirname, '../outputs/error'), 12 | }, 13 | stats: 'errors-warnings' 14 | }; 15 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const MIN_BABEL_VERSION = 7; 2 | 3 | module.exports = (api) => { 4 | api.assertVersion(MIN_BABEL_VERSION); 5 | api.cache(true); 6 | 7 | return { 8 | presets: [ 9 | [ 10 | "@babel/preset-env", 11 | { 12 | targets: { 13 | node: "14.15.0", 14 | }, 15 | }, 16 | ], 17 | ], 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | name: "Dependency Review" 2 | on: [pull_request] 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | dependency-review: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: "Checkout Repository" 12 | uses: actions/checkout@v4 13 | - name: "Dependency Review" 14 | uses: actions/dependency-review-action@v4 15 | -------------------------------------------------------------------------------- /test/utils/__snapshots__/setupHooks.test.js.snap.webpack5: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing 2 | 3 | exports[`setupHooks handles multi compiler 1`] = `[]`; 4 | 5 | exports[`setupHooks handles multi compiler 2`] = `[]`; 6 | 7 | exports[`setupHooks handles multi compiler 3`] = `[]`; 8 | 9 | exports[`setupHooks sets state, then logs stats and handles callbacks on nextTick from done hook 1`] = `[]`; 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "node", 3 | collectCoverage: false, 4 | coveragePathIgnorePatterns: ["test", "/node_modules"], 5 | moduleFileExtensions: ["js", "json"], 6 | testMatch: ["**/test/**/*.test.js"], 7 | setupFilesAfterEnv: ["/setupTest.js"], 8 | globalSetup: "/scripts/globalSetup.js", 9 | snapshotResolver: "./test/helpers/snapshotResolver.js", 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/webpack.immutable.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | context: path.resolve(__dirname), 8 | entry: './immutable.js', 9 | output: { 10 | publicPath: "/static/", 11 | path: path.resolve(__dirname, '../outputs/basic'), 12 | }, 13 | infrastructureLogging: { 14 | level: 'none' 15 | }, 16 | stats: 'normal' 17 | }; 18 | -------------------------------------------------------------------------------- /test/fixtures/webpack.simple.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | context: path.resolve(__dirname), 8 | entry: './simple.js', 9 | output: { 10 | filename: 'bundle.js', 11 | path: path.resolve(__dirname, '../outputs/simple'), 12 | }, 13 | infrastructureLogging: { 14 | level: 'none' 15 | }, 16 | stats: 'errors-warnings' 17 | }; 18 | -------------------------------------------------------------------------------- /test/helpers/getCompilerHooks.js: -------------------------------------------------------------------------------- 1 | export default (compiler) => { 2 | const result = {}; 3 | 4 | for (const hook of Object.keys(compiler.hooks)) { 5 | for (const tap of compiler.hooks[hook].taps) { 6 | if (tap.name === "webpack-dev-middleware") { 7 | if (!result[hook]) { 8 | result[hook] = []; 9 | } 10 | 11 | result[hook].push(tap); 12 | } 13 | } 14 | } 15 | 16 | return result; 17 | }; 18 | -------------------------------------------------------------------------------- /test/helpers/getCompiler.js: -------------------------------------------------------------------------------- 1 | import webpack from "webpack"; 2 | 3 | import defaultConfig from "../fixtures/webpack.config"; 4 | 5 | /** @typedef {import("webpack").Configuration} Configuration */ 6 | /** @typedef {import("webpack").Compiler} Compiler */ 7 | 8 | /** 9 | * @param {Configuration} config config 10 | * @returns {Compiler} compiler 11 | */ 12 | function getCompiler(config) { 13 | return webpack(config || defaultConfig); 14 | } 15 | 16 | export default getCompiler; 17 | -------------------------------------------------------------------------------- /types/utils/etag.d.ts: -------------------------------------------------------------------------------- 1 | export = etag; 2 | /** 3 | * Create a simple ETag. 4 | * @param {Buffer | ReadStream | Stats} entity entity 5 | * @returns {Promise<{ hash: string, buffer?: Buffer }>} etag 6 | */ 7 | declare function etag(entity: Buffer | ReadStream | Stats): Promise<{ 8 | hash: string; 9 | buffer?: Buffer; 10 | }>; 11 | declare namespace etag { 12 | export { Stats, ReadStream }; 13 | } 14 | type Stats = import("fs").Stats; 15 | type ReadStream = import("fs").ReadStream; 16 | -------------------------------------------------------------------------------- /test/utils/escapeHtml.test.js: -------------------------------------------------------------------------------- 1 | import escapeHtml from "../../src/utils/escapeHtml"; 2 | 3 | describe("escapeHtml", () => { 4 | it("should work", () => { 5 | expect(escapeHtml("")).toBe(""); 6 | expect(escapeHtml("test")).toBe("test"); 7 | expect(escapeHtml("\"&'<>test")).toBe(""&'<>test"); 8 | expect(escapeHtml("\"&'test<>")).toBe(""&'test<>"); 9 | expect(escapeHtml("test\"&'<>")).toBe("test"&'<>"); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/fixtures/webpack.watch-options.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | context: path.resolve(__dirname), 8 | entry: './simple.js', 9 | output: { 10 | filename: 'bundle.js', 11 | path: path.resolve(__dirname, '../outputs/watch-options'), 12 | }, 13 | watchOptions: { 14 | aggregateTimeout: 300, 15 | poll: true, 16 | }, 17 | infrastructureLogging: { 18 | level: 'none' 19 | }, 20 | stats: 'errors-warnings' 21 | }; 22 | -------------------------------------------------------------------------------- /test/fixtures/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | context: path.resolve(__dirname), 8 | entry: './foo.js', 9 | output: { 10 | filename: 'bundle.js', 11 | path: path.resolve(__dirname, '../outputs/basic'), 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(svg|html)$/, 17 | loader: 'file-loader', 18 | options: { name: '[name].[ext]' }, 19 | }, 20 | ], 21 | }, 22 | infrastructureLogging: { 23 | level: 'none' 24 | }, 25 | stats: 'normal' 26 | }; 27 | -------------------------------------------------------------------------------- /test/helpers/listenAndCompile.js: -------------------------------------------------------------------------------- 1 | export default (app, compiler, done) => { 2 | let complete = 0; 3 | // wait until the app is listening and the done hook is called 4 | const progress = () => { 5 | complete += 1; 6 | if (complete === 2) { 7 | done(); 8 | } 9 | }; 10 | 11 | const listen = app.listen((error) => { 12 | if (error) { 13 | // if there is an error, don't wait for the compilation to finish 14 | return done(error); 15 | } 16 | 17 | return progress(); 18 | }); 19 | 20 | compiler.hooks.done.tap("wdm-test", () => progress()); 21 | 22 | return listen; 23 | }; 24 | -------------------------------------------------------------------------------- /test/fixtures/webpack.querystring.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | context: path.resolve(__dirname), 8 | entry: './foo.js', 9 | output: { 10 | filename: 'bundle.js?[contenthash]', 11 | path: path.resolve(__dirname, '../outputs/querystring'), 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(svg|html)$/, 17 | loader: 'file-loader', 18 | options: { name: '[name].[ext]' }, 19 | }, 20 | ], 21 | }, 22 | infrastructureLogging: { 23 | level: 'none' 24 | }, 25 | stats: 'errors-warnings' 26 | }; 27 | -------------------------------------------------------------------------------- /test/fixtures/webpack.warning.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | context: path.resolve(__dirname), 8 | entry: './warning.js', 9 | output: { 10 | filename: 'bundle.js', 11 | path: path.resolve(__dirname, '../outputs/warning'), 12 | }, 13 | plugins: [ 14 | { 15 | apply(compiler) { 16 | compiler.hooks.emit.tapAsync('WarningPlugin', (compilation, done) => { 17 | compilation.warnings.push(new Error('Warning')); 18 | 19 | done(); 20 | }) 21 | }, 22 | } 23 | ], 24 | stats: 'errors-warnings' 25 | }; 26 | -------------------------------------------------------------------------------- /test/fixtures/webpack.no-stats.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | context: path.resolve(__dirname), 8 | entry: './foo.js', 9 | output: { 10 | filename: 'bundle.js', 11 | path: path.resolve(__dirname, '../outputs/basic'), 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(svg|html)$/, 17 | loader: 'file-loader', 18 | options: { name: '[name].[ext]' }, 19 | }, 20 | ], 21 | }, 22 | infrastructureLogging: { 23 | level: 'none' 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /test/fixtures/webpack.public-path.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | context: path.resolve(__dirname), 8 | entry: './foo.js', 9 | output: { 10 | filename: 'bundle.js', 11 | path: path.resolve(__dirname, '../outputs/public-path'), 12 | publicPath: '/public/path/', 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.(svg|html)$/, 18 | loader: 'file-loader', 19 | options: { name: '[name].[ext]' }, 20 | }, 21 | ], 22 | }, 23 | infrastructureLogging: { 24 | level: 'none' 25 | }, 26 | stats: 'errors-warnings' 27 | }; 28 | -------------------------------------------------------------------------------- /test/fixtures/webpack.stats-false.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | context: path.resolve(__dirname), 8 | entry: './foo.js', 9 | output: { 10 | filename: 'bundle.js', 11 | path: path.resolve(__dirname, '../outputs/basic'), 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(svg|html)$/, 17 | loader: 'file-loader', 18 | options: { name: '[name].[ext]' }, 19 | }, 20 | ], 21 | }, 22 | infrastructureLogging: { 23 | level: 'none' 24 | }, 25 | stats: false 26 | }; 27 | -------------------------------------------------------------------------------- /test/fixtures/webpack.stats-none.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | context: path.resolve(__dirname), 8 | entry: './foo.js', 9 | output: { 10 | filename: 'bundle.js', 11 | path: path.resolve(__dirname, '../outputs/basic'), 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(svg|html)$/, 17 | loader: 'file-loader', 18 | options: { name: '[name].[ext]' }, 19 | }, 20 | ], 21 | }, 22 | infrastructureLogging: { 23 | level: 'none' 24 | }, 25 | stats: 'none' 26 | }; 27 | -------------------------------------------------------------------------------- /test/fixtures/webpack.stats-true.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | context: path.resolve(__dirname), 8 | entry: './foo.js', 9 | output: { 10 | filename: 'bundle.js', 11 | path: path.resolve(__dirname, '../outputs/basic'), 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(svg|html)$/, 17 | loader: 'file-loader', 18 | options: { name: '[name].[ext]' }, 19 | }, 20 | ], 21 | }, 22 | infrastructureLogging: { 23 | level: 'none' 24 | }, 25 | stats: true 26 | }; 27 | -------------------------------------------------------------------------------- /test/fixtures/webpack.stats-minimal.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | context: path.resolve(__dirname), 8 | entry: './foo.js', 9 | output: { 10 | filename: 'bundle.js', 11 | path: path.resolve(__dirname, '../outputs/basic'), 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(svg|html)$/, 17 | loader: 'file-loader', 18 | options: { name: '[name].[ext]' }, 19 | }, 20 | ], 21 | }, 22 | infrastructureLogging: { 23 | level: 'none' 24 | }, 25 | stats: 'minimal' 26 | }; 27 | -------------------------------------------------------------------------------- /test/fixtures/webpack.stats-verbose.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | context: path.resolve(__dirname), 8 | entry: './foo.js', 9 | output: { 10 | filename: 'bundle.js', 11 | path: path.resolve(__dirname, '../outputs/basic'), 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(svg|html)$/, 17 | loader: 'file-loader', 18 | options: { name: '[name].[ext]' }, 19 | }, 20 | ], 21 | }, 22 | infrastructureLogging: { 23 | level: 'none' 24 | }, 25 | stats: 'verbose' 26 | }; 27 | -------------------------------------------------------------------------------- /test/fixtures/webpack.stats-object.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | context: path.resolve(__dirname), 8 | entry: './foo.js', 9 | output: { 10 | filename: 'bundle.js', 11 | path: path.resolve(__dirname, '../outputs/basic'), 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(svg|html)$/, 17 | loader: 'file-loader', 18 | options: { name: '[name].[ext]' }, 19 | }, 20 | ], 21 | }, 22 | infrastructureLogging: { 23 | level: 'none' 24 | }, 25 | stats: { all: false, assets: true } 26 | }; 27 | -------------------------------------------------------------------------------- /test/fixtures/webpack.stats-colors-false.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | context: path.resolve(__dirname), 8 | entry: './foo.js', 9 | output: { 10 | filename: 'bundle.js', 11 | path: path.resolve(__dirname, '../outputs/basic'), 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(svg|html)$/, 17 | loader: 'file-loader', 18 | options: { name: '[name].[ext]' }, 19 | }, 20 | ], 21 | }, 22 | infrastructureLogging: { 23 | level: 'none' 24 | }, 25 | stats: { 26 | colors: false 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /test/fixtures/webpack.stats-colors-true.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | context: path.resolve(__dirname), 8 | entry: './foo.js', 9 | output: { 10 | filename: 'bundle.js', 11 | path: path.resolve(__dirname, '../outputs/basic'), 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(svg|html)$/, 17 | loader: 'file-loader', 18 | options: { name: '[name].[ext]' }, 19 | }, 20 | ], 21 | }, 22 | infrastructureLogging: { 23 | level: 'none' 24 | }, 25 | stats: { 26 | colors: true 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /test/helpers/snapshotResolver.js: -------------------------------------------------------------------------------- 1 | const path = require("node:path"); 2 | 3 | const webpack = require("webpack"); 4 | 5 | const [webpackVersion] = webpack.version; 6 | const snapshotExtension = `.snap.webpack${webpackVersion}`; 7 | 8 | module.exports = { 9 | resolveSnapshotPath: (testPath) => 10 | path.join( 11 | path.dirname(testPath), 12 | "__snapshots__", 13 | `${path.basename(testPath)}${snapshotExtension}`, 14 | ), 15 | resolveTestPath: (snapshotPath) => 16 | snapshotPath 17 | .replace(`${path.sep}__snapshots__`, "") 18 | .slice(0, -snapshotExtension.length), 19 | testPathForConsistencyCheck: path.join( 20 | "consistency_check", 21 | "__tests__", 22 | "example.test.js", 23 | ), 24 | }; 25 | -------------------------------------------------------------------------------- /test/fixtures/webpack.array.error.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = [ 6 | { 7 | mode: 'development', 8 | context: path.resolve(__dirname), 9 | entry: './broken.js', 10 | output: { 11 | filename: 'bundle.js', 12 | path: path.resolve(__dirname, '../outputs/array-error'), 13 | publicPath: '/static-one/', 14 | }, 15 | stats: 'errors-warnings' 16 | }, 17 | { 18 | mode: 'development', 19 | context: path.resolve(__dirname), 20 | entry: './broken.js', 21 | output: { 22 | filename: 'bundle.js', 23 | path: path.resolve(__dirname, '../outputs/array-error'), 24 | publicPath: '/static-two/', 25 | }, 26 | stats: 'errors-warnings' 27 | } 28 | ]; 29 | -------------------------------------------------------------------------------- /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "language": "en,en-gb", 4 | "words": [ 5 | "memfs", 6 | "colorette", 7 | "noextension", 8 | "fullhash", 9 | "execa", 10 | "deepmerge", 11 | "fastify", 12 | "contextify", 13 | "middie", 14 | "cexoso", 15 | "usdz", 16 | "leadinghash", 17 | "myhtml", 18 | "configurated", 19 | "mycustom", 20 | "commitlint", 21 | "nosniff", 22 | "deoptimize", 23 | "etag", 24 | "cachable", 25 | "finalhandler", 26 | "hono", 27 | "rspack" 28 | ], 29 | "ignorePaths": [ 30 | "CHANGELOG.md", 31 | "package.json", 32 | "dist/**", 33 | "**/__snapshots__/**", 34 | "package-lock.json", 35 | "node_modules", 36 | "coverage", 37 | "*.log", 38 | "./test/fixtures/**", 39 | "./test/outputs/**" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/ready.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ 2 | /** @typedef {import("../index.js").ServerResponse} ServerResponse */ 3 | /** @typedef {import("../index.js").Callback} Callback */ 4 | 5 | /** 6 | * @template {IncomingMessage} Request 7 | * @template {ServerResponse} Response 8 | * @param {import("../index.js").FilledContext} context context 9 | * @param {Callback} callback callback 10 | * @param {Request=} req req 11 | * @returns {void} 12 | */ 13 | function ready(context, callback, req) { 14 | if (context.state) { 15 | callback(context.stats); 16 | 17 | return; 18 | } 19 | 20 | const name = (req && req.url) || callback.name; 21 | 22 | context.logger.info(`wait until bundle finished${name ? `: ${name}` : ""}`); 23 | context.callbacks.push(callback); 24 | } 25 | 26 | module.exports = ready; 27 | -------------------------------------------------------------------------------- /src/utils/parseTokenList.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse a HTTP token list. 3 | * @param {string} str str 4 | * @returns {string[]} tokens 5 | */ 6 | function parseTokenList(str) { 7 | let end = 0; 8 | let start = 0; 9 | 10 | const list = []; 11 | 12 | // gather tokens 13 | for (let i = 0, len = str.length; i < len; i++) { 14 | switch (str.charCodeAt(i)) { 15 | case 0x20 /* */: 16 | if (start === end) { 17 | end = i + 1; 18 | start = end; 19 | } 20 | break; 21 | case 0x2c /* , */: 22 | if (start !== end) { 23 | list.push(str.slice(start, end)); 24 | } 25 | end = i + 1; 26 | start = end; 27 | break; 28 | default: 29 | end = i + 1; 30 | break; 31 | } 32 | } 33 | 34 | // final token 35 | if (start !== end) { 36 | list.push(str.slice(start, end)); 37 | } 38 | 39 | return list; 40 | } 41 | 42 | module.exports = parseTokenList; 43 | -------------------------------------------------------------------------------- /types/utils/memorize.d.ts: -------------------------------------------------------------------------------- 1 | export = memorize; 2 | /** 3 | * @template T 4 | * @typedef {(...args: EXPECTED_ANY) => T} FunctionReturning 5 | */ 6 | /** 7 | * @template T 8 | * @param {FunctionReturning} fn memorized function 9 | * @param {({ cache?: Map } | undefined)=} cache cache 10 | * @param {((value: T) => T)=} callback callback 11 | * @returns {FunctionReturning} new function 12 | */ 13 | declare function memorize( 14 | fn: FunctionReturning, 15 | { 16 | cache, 17 | }?: 18 | | ( 19 | | { 20 | cache?: Map< 21 | string, 22 | { 23 | data: T; 24 | } 25 | >; 26 | } 27 | | undefined 28 | ) 29 | | undefined, 30 | callback?: ((value: T) => T) | undefined, 31 | ): FunctionReturning; 32 | declare namespace memorize { 33 | export { FunctionReturning, EXPECTED_ANY }; 34 | } 35 | type FunctionReturning = (...args: EXPECTED_ANY) => T; 36 | type EXPECTED_ANY = import("../index").EXPECTED_ANY; 37 | -------------------------------------------------------------------------------- /types/utils/ready.d.ts: -------------------------------------------------------------------------------- 1 | export = ready; 2 | /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ 3 | /** @typedef {import("../index.js").ServerResponse} ServerResponse */ 4 | /** @typedef {import("../index.js").Callback} Callback */ 5 | /** 6 | * @template {IncomingMessage} Request 7 | * @template {ServerResponse} Response 8 | * @param {import("../index.js").FilledContext} context context 9 | * @param {Callback} callback callback 10 | * @param {Request=} req req 11 | * @returns {void} 12 | */ 13 | declare function ready< 14 | Request extends IncomingMessage, 15 | Response extends ServerResponse, 16 | >( 17 | context: import("../index.js").FilledContext, 18 | callback: Callback, 19 | req?: Request | undefined, 20 | ): void; 21 | declare namespace ready { 22 | export { IncomingMessage, ServerResponse, Callback }; 23 | } 24 | type IncomingMessage = import("../index.js").IncomingMessage; 25 | type ServerResponse = import("../index.js").ServerResponse; 26 | type Callback = import("../index.js").Callback; 27 | -------------------------------------------------------------------------------- /test/fixtures/webpack.array.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = [ 6 | { 7 | mode: 'development', 8 | context: path.resolve(__dirname), 9 | entry: './foo.js', 10 | output: { 11 | filename: 'bundle.js', 12 | path: path.resolve(__dirname, '../outputs/array/js1'), 13 | publicPath: '/static-one/', 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(svg|html)$/, 19 | loader: 'file-loader', 20 | options: { name: '[name].[ext]' }, 21 | }, 22 | ], 23 | }, 24 | infrastructureLogging: { 25 | level: 'none' 26 | }, 27 | stats: 'normal' 28 | }, 29 | { 30 | mode: 'development', 31 | context: path.resolve(__dirname), 32 | entry: './bar.js', 33 | output: { 34 | filename: 'bundle.js', 35 | path: path.resolve(__dirname, '../outputs/array/js2'), 36 | publicPath: '/static-two/', 37 | }, 38 | infrastructureLogging: { 39 | level: 'none' 40 | }, 41 | stats: 'normal' 42 | }, 43 | ]; 44 | -------------------------------------------------------------------------------- /test/fixtures/webpack.client.server.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = [ 6 | { 7 | mode: 'development', 8 | context: path.resolve(__dirname), 9 | entry: './foo.js', 10 | output: { 11 | filename: 'bundle.js', 12 | path: path.resolve(__dirname, '../outputs/client-server/client'), 13 | publicPath: '/static/', 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(svg|html)$/, 19 | loader: 'file-loader', 20 | options: { name: '[name].[ext]' }, 21 | }, 22 | ], 23 | }, 24 | infrastructureLogging: { 25 | level: 'none' 26 | }, 27 | stats: 'errors-warnings' 28 | }, 29 | { 30 | mode: 'development', 31 | context: path.resolve(__dirname), 32 | entry: './bar.js', 33 | target: 'node', 34 | output: { 35 | filename: 'bundle.js', 36 | path: path.resolve(__dirname, '../outputs/client-server/server'), 37 | }, 38 | infrastructureLogging: { 39 | level: 'none' 40 | }, 41 | stats: 'errors-warnings' 42 | }, 43 | ]; 44 | -------------------------------------------------------------------------------- /types/utils/setupOutputFileSystem.d.ts: -------------------------------------------------------------------------------- 1 | export = setupOutputFileSystem; 2 | /** @typedef {import("webpack").MultiCompiler} MultiCompiler */ 3 | /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ 4 | /** @typedef {import("../index.js").ServerResponse} ServerResponse */ 5 | /** 6 | * @template {IncomingMessage} Request 7 | * @template {ServerResponse} Response 8 | * @param {import("../index.js").WithOptional, "watching" | "outputFileSystem">} context context 9 | */ 10 | declare function setupOutputFileSystem< 11 | Request extends IncomingMessage, 12 | Response extends ServerResponse, 13 | >( 14 | context: import("../index.js").WithOptional< 15 | import("../index.js").Context, 16 | "watching" | "outputFileSystem" 17 | >, 18 | ): void; 19 | declare namespace setupOutputFileSystem { 20 | export { MultiCompiler, IncomingMessage, ServerResponse }; 21 | } 22 | type MultiCompiler = import("webpack").MultiCompiler; 23 | type IncomingMessage = import("../index.js").IncomingMessage; 24 | type ServerResponse = import("../index.js").ServerResponse; 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/fixtures/webpack.array.dev-server-false.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = [ 6 | { 7 | mode: 'development', 8 | context: path.resolve(__dirname), 9 | entry: './bar.js', 10 | output: { 11 | filename: 'bundle.js', 12 | path: path.resolve(__dirname, '../outputs/dev-server-false/js3'), 13 | publicPath: '/static-two/', 14 | }, 15 | infrastructureLogging: { 16 | level: 'none' 17 | }, 18 | stats: 'normal', 19 | devServer: false, 20 | }, 21 | { 22 | mode: 'development', 23 | context: path.resolve(__dirname), 24 | entry: './foo.js', 25 | output: { 26 | filename: 'bundle.js', 27 | path: path.resolve(__dirname, '../outputs/dev-server-false/js4'), 28 | publicPath: '/static-one/', 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.(svg|html)$/, 34 | loader: 'file-loader', 35 | options: { name: '[name].[ext]' }, 36 | }, 37 | ], 38 | }, 39 | infrastructureLogging: { 40 | level: 'none' 41 | }, 42 | stats: 'normal' 43 | } 44 | ]; 45 | -------------------------------------------------------------------------------- /src/utils/memorize.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("../index").EXPECTED_ANY} EXPECTED_ANY */ 2 | 3 | const cacheStore = new WeakMap(); 4 | 5 | /** 6 | * @template T 7 | * @typedef {(...args: EXPECTED_ANY) => T} FunctionReturning 8 | */ 9 | 10 | /** 11 | * @template T 12 | * @param {FunctionReturning} fn memorized function 13 | * @param {({ cache?: Map } | undefined)=} cache cache 14 | * @param {((value: T) => T)=} callback callback 15 | * @returns {FunctionReturning} new function 16 | */ 17 | function memorize(fn, { cache = new Map() } = {}, callback = undefined) { 18 | /** 19 | * @param {EXPECTED_ANY[]} arguments_ args 20 | * @returns {EXPECTED_ANY} result 21 | */ 22 | const memoized = (...arguments_) => { 23 | const [key] = arguments_; 24 | const cacheItem = cache.get(key); 25 | 26 | if (cacheItem) { 27 | return cacheItem.data; 28 | } 29 | 30 | // @ts-expect-error 31 | let result = fn.apply(this, arguments_); 32 | 33 | if (callback) { 34 | result = callback(result); 35 | } 36 | 37 | cache.set(key, { 38 | data: result, 39 | }); 40 | 41 | return result; 42 | }; 43 | 44 | cacheStore.set(memoized, cache); 45 | 46 | return memoized; 47 | } 48 | 49 | module.exports = memorize; 50 | -------------------------------------------------------------------------------- /test/fixtures/webpack.array.watch-options.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = [ 6 | { 7 | mode: 'development', 8 | context: path.resolve(__dirname), 9 | entry: './foo.js', 10 | output: { 11 | filename: 'bundle.js', 12 | path: path.resolve(__dirname, '../outputs/array-watch-options/js1'), 13 | publicPath: '/static-one/', 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(svg|html)$/, 19 | loader: 'file-loader', 20 | options: { name: '[name].[ext]' }, 21 | }, 22 | ], 23 | }, 24 | infrastructureLogging: { 25 | level: 'none' 26 | }, 27 | watchOptions: { 28 | aggregateTimeout: 800, 29 | poll: false, 30 | }, 31 | }, 32 | { 33 | mode: 'development', 34 | context: path.resolve(__dirname), 35 | entry: './bar.js', 36 | output: { 37 | filename: 'bundle.js', 38 | path: path.resolve(__dirname, '../outputs/array-watch-options/js2'), 39 | publicPath: '/static-two/', 40 | }, 41 | infrastructureLogging: { 42 | level: 'none' 43 | }, 44 | watchOptions: { 45 | aggregateTimeout: 300, 46 | poll: true, 47 | }, 48 | stats: 'errors-warnings' 49 | }, 50 | ]; 51 | -------------------------------------------------------------------------------- /test/fixtures/webpack.array.warning.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = [ 6 | { 7 | mode: 'development', 8 | context: path.resolve(__dirname), 9 | entry: './warning.js', 10 | output: { 11 | filename: 'bundle.js', 12 | path: path.resolve(__dirname, '../../outputs/array-warning/js1'), 13 | publicPath: '/static-one/', 14 | }, 15 | plugins: [ 16 | { 17 | apply(compiler) { 18 | compiler.hooks.emit.tapAsync('WarningPlugin', (compilation, done) => { 19 | compilation.warnings.push(new Error('Warning')); 20 | 21 | done(); 22 | }) 23 | }, 24 | } 25 | ], 26 | stats: 'errors-warnings' 27 | }, 28 | { 29 | mode: 'development', 30 | context: path.resolve(__dirname), 31 | entry: './warning.js', 32 | output: { 33 | filename: 'bundle.js', 34 | path: path.resolve(__dirname, '../outputs/array-warning/js2'), 35 | publicPath: '/static-two/', 36 | }, 37 | plugins: [ 38 | { 39 | apply(compiler) { 40 | compiler.hooks.emit.tapAsync('WarningPlugin', (compilation, done) => { 41 | compilation.warnings.push(new Error('Warning')); 42 | 43 | done(); 44 | }) 45 | }, 46 | } 47 | ], 48 | stats: 'errors-warnings' 49 | }, 50 | ]; 51 | -------------------------------------------------------------------------------- /types/utils/setupWriteToDisk.d.ts: -------------------------------------------------------------------------------- 1 | export = setupWriteToDisk; 2 | /** @typedef {import("webpack").Compiler} Compiler */ 3 | /** @typedef {import("webpack").MultiCompiler} MultiCompiler */ 4 | /** @typedef {import("webpack").Compilation} Compilation */ 5 | /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ 6 | /** @typedef {import("../index.js").ServerResponse} ServerResponse */ 7 | /** 8 | * @template {IncomingMessage} Request 9 | * @template {ServerResponse} Response 10 | * @param {import("../index.js").WithOptional, "watching" | "outputFileSystem">} context context 11 | */ 12 | declare function setupWriteToDisk< 13 | Request extends IncomingMessage, 14 | Response extends ServerResponse, 15 | >( 16 | context: import("../index.js").WithOptional< 17 | import("../index.js").Context, 18 | "watching" | "outputFileSystem" 19 | >, 20 | ): void; 21 | declare namespace setupWriteToDisk { 22 | export { 23 | Compiler, 24 | MultiCompiler, 25 | Compilation, 26 | IncomingMessage, 27 | ServerResponse, 28 | }; 29 | } 30 | type Compiler = import("webpack").Compiler; 31 | type MultiCompiler = import("webpack").MultiCompiler; 32 | type Compilation = import("webpack").Compilation; 33 | type IncomingMessage = import("../index.js").IncomingMessage; 34 | type ServerResponse = import("../index.js").ServerResponse; 35 | -------------------------------------------------------------------------------- /src/utils/escapeHtml.js: -------------------------------------------------------------------------------- 1 | const matchHtmlRegExp = /["'&<>]/; 2 | 3 | /** 4 | * @param {string} string raw HTML 5 | * @returns {string} escaped HTML 6 | */ 7 | function escapeHtml(string) { 8 | const str = `${string}`; 9 | const match = matchHtmlRegExp.exec(str); 10 | 11 | if (!match) { 12 | return str; 13 | } 14 | 15 | let escape; 16 | let html = ""; 17 | let index = 0; 18 | let lastIndex = 0; 19 | 20 | for ({ index } = match; index < str.length; index++) { 21 | switch (str.charCodeAt(index)) { 22 | // " 23 | case 34: 24 | escape = """; 25 | break; 26 | // & 27 | case 38: 28 | escape = "&"; 29 | break; 30 | // ' 31 | case 39: 32 | escape = "'"; 33 | break; 34 | // < 35 | case 60: 36 | escape = "<"; 37 | break; 38 | // > 39 | case 62: 40 | escape = ">"; 41 | break; 42 | default: 43 | continue; 44 | } 45 | 46 | if (lastIndex !== index) { 47 | // eslint-disable-next-line unicorn/prefer-string-slice 48 | html += str.substring(lastIndex, index); 49 | } 50 | 51 | lastIndex = index + 1; 52 | html += escape; 53 | } 54 | 55 | // eslint-disable-next-line unicorn/prefer-string-slice 56 | return lastIndex !== index ? html + str.substring(lastIndex, index) : html; 57 | } 58 | 59 | module.exports = escapeHtml; 60 | -------------------------------------------------------------------------------- /types/utils/getPaths.d.ts: -------------------------------------------------------------------------------- 1 | export = getPaths; 2 | /** @typedef {import("webpack").Compiler} Compiler */ 3 | /** @typedef {import("webpack").Stats} Stats */ 4 | /** @typedef {import("webpack").MultiStats} MultiStats */ 5 | /** @typedef {import("webpack").Asset} Asset */ 6 | /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ 7 | /** @typedef {import("../index.js").ServerResponse} ServerResponse */ 8 | /** 9 | * @template {IncomingMessage} Request 10 | * @template {ServerResponse} Response 11 | * @param {import("../index.js").FilledContext} context context 12 | * @returns {{ outputPath: string, publicPath: string, assetsInfo: Asset["info"] }[]} paths 13 | */ 14 | declare function getPaths< 15 | Request extends IncomingMessage, 16 | Response extends ServerResponse, 17 | >( 18 | context: import("../index.js").FilledContext, 19 | ): { 20 | outputPath: string; 21 | publicPath: string; 22 | assetsInfo: Asset["info"]; 23 | }[]; 24 | declare namespace getPaths { 25 | export { 26 | Compiler, 27 | Stats, 28 | MultiStats, 29 | Asset, 30 | IncomingMessage, 31 | ServerResponse, 32 | }; 33 | } 34 | type Compiler = import("webpack").Compiler; 35 | type Stats = import("webpack").Stats; 36 | type MultiStats = import("webpack").MultiStats; 37 | type Asset = import("webpack").Asset; 38 | type IncomingMessage = import("../index.js").IncomingMessage; 39 | type ServerResponse = import("../index.js").ServerResponse; 40 | -------------------------------------------------------------------------------- /types/utils/getFilenameFromUrl.d.ts: -------------------------------------------------------------------------------- 1 | export = getFilenameFromUrl; 2 | /** 3 | * @typedef {object} Extra 4 | * @property {import("fs").Stats=} stats stats 5 | * @property {number=} errorCode error code 6 | * @property {boolean=} immutable true when immutable, otherwise false 7 | */ 8 | /** 9 | * decodeURIComponent. 10 | * 11 | * Allows V8 to only deoptimize this fn instead of all of send(). 12 | * @param {string} input 13 | * @returns {string} 14 | */ 15 | /** 16 | * @template {IncomingMessage} Request 17 | * @template {ServerResponse} Response 18 | * @param {import("../index.js").FilledContext} context context 19 | * @param {string} url url 20 | * @param {Extra=} extra extra 21 | * @returns {string | undefined} filename 22 | */ 23 | declare function getFilenameFromUrl< 24 | Request extends IncomingMessage, 25 | Response extends ServerResponse, 26 | >( 27 | context: import("../index.js").FilledContext, 28 | url: string, 29 | extra?: Extra | undefined, 30 | ): string | undefined; 31 | declare namespace getFilenameFromUrl { 32 | export { IncomingMessage, ServerResponse, Extra }; 33 | } 34 | type IncomingMessage = import("../index.js").IncomingMessage; 35 | type ServerResponse = import("../index.js").ServerResponse; 36 | type Extra = { 37 | /** 38 | * stats 39 | */ 40 | stats?: import("fs").Stats | undefined; 41 | /** 42 | * error code 43 | */ 44 | errorCode?: number | undefined; 45 | /** 46 | * true when immutable, otherwise false 47 | */ 48 | immutable?: boolean | undefined; 49 | }; 50 | -------------------------------------------------------------------------------- /test/utils/setupOutputFileSystem.test.js: -------------------------------------------------------------------------------- 1 | import memfs from "memfs"; 2 | 3 | import setupOutputFileSystem from "../../src/utils/setupOutputFileSystem"; 4 | 5 | const createFsFromVolume = jest.spyOn(memfs, "createFsFromVolume"); 6 | 7 | createFsFromVolume.mockImplementation(() => ({ 8 | testFs: true, 9 | })); 10 | 11 | describe("setupOutputFileSystem", () => { 12 | afterEach(() => { 13 | createFsFromVolume.mockClear(); 14 | }); 15 | 16 | it("should create default fs if not provided", () => { 17 | const context = { 18 | compiler: { options: {} }, 19 | options: {}, 20 | }; 21 | 22 | setupOutputFileSystem(context); 23 | 24 | // make sure that this is the default fs created 25 | expect(context.compiler.outputFileSystem.testFs).toBeTruthy(); 26 | expect(context.outputFileSystem.testFs).toBeTruthy(); 27 | expect(createFsFromVolume).toHaveBeenCalledTimes(1); 28 | }); 29 | 30 | it("should set fs for multi compiler", () => { 31 | const context = { 32 | compiler: { 33 | compilers: [{ options: {} }, { options: {} }], 34 | }, 35 | options: {}, 36 | }; 37 | 38 | setupOutputFileSystem(context); 39 | 40 | for (const comp of context.compiler.compilers) { 41 | expect(comp.outputFileSystem).toBeTruthy(); 42 | } 43 | }); 44 | 45 | it("should use provided fs with correct methods", () => { 46 | const context = { 47 | compiler: { options: {} }, 48 | options: { 49 | outputFileSystem: { 50 | join: () => {}, 51 | mkdirp: () => {}, 52 | }, 53 | }, 54 | }; 55 | 56 | setupOutputFileSystem(context); 57 | 58 | expect(context.outputFileSystem).toEqual(context.options.outputFileSystem); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/utils/__snapshots__/setupWriteToDisk.test.js.snap.webpack5: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing 2 | 3 | exports[`setupWriteToDisk tries to create directories and write file if not filtered out with mkdir error 1`] = ` 4 | [ 5 | [ 6 | "Child "name": Unable to write "/target/path" directory to disk: 7 | error1", 8 | ], 9 | ] 10 | `; 11 | 12 | exports[`setupWriteToDisk tries to create directories and write file if not filtered out with mkdir error 2`] = `[]`; 13 | 14 | exports[`setupWriteToDisk tries to create directories and write file if not filtered out with mkdir error 3`] = ` 15 | [ 16 | [ 17 | "error1", 18 | ], 19 | ] 20 | `; 21 | 22 | exports[`setupWriteToDisk tries to create directories and write file if not filtered out with no write errors 1`] = `[]`; 23 | 24 | exports[`setupWriteToDisk tries to create directories and write file if not filtered out with no write errors 2`] = ` 25 | [ 26 | [ 27 | "Child "name": Asset written to disk: "/target/path/file"", 28 | ], 29 | ] 30 | `; 31 | 32 | exports[`setupWriteToDisk tries to create directories and write file if not filtered out with no write errors 3`] = ` 33 | [ 34 | [], 35 | ] 36 | `; 37 | 38 | exports[`setupWriteToDisk tries to create directories and write file if not filtered out with writeFile error 1`] = ` 39 | [ 40 | [ 41 | "Child "name": Unable to write "/target/path/file" asset to disk: 42 | error2", 43 | ], 44 | ] 45 | `; 46 | 47 | exports[`setupWriteToDisk tries to create directories and write file if not filtered out with writeFile error 2`] = `[]`; 48 | 49 | exports[`setupWriteToDisk tries to create directories and write file if not filtered out with writeFile error 3`] = ` 50 | [ 51 | [ 52 | "error2", 53 | ], 54 | ] 55 | `; 56 | -------------------------------------------------------------------------------- /test/fixtures/webpack.array.one-error-one-warning-one-no.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = [ 6 | { 7 | mode: 'development', 8 | context: path.resolve(__dirname), 9 | entry: './broken.js', 10 | output: { 11 | filename: 'bundle.js', 12 | path: path.resolve(__dirname, '../outputs/one-error-one-warning-one-success/js1'), 13 | publicPath: '/static-one/', 14 | }, 15 | infrastructureLogging: { 16 | level: 'none' 17 | }, 18 | stats: 'normal' 19 | }, 20 | { 21 | mode: 'development', 22 | context: path.resolve(__dirname), 23 | entry: './warning.js', 24 | output: { 25 | filename: 'bundle.js', 26 | path: path.resolve(__dirname, '../outputs/one-error-one-warning-one-success/js2'), 27 | publicPath: '/static-two/', 28 | }, 29 | plugins: [ 30 | { 31 | apply(compiler) { 32 | compiler.hooks.emit.tapAsync('WarningPlugin', (compilation, done) => { 33 | compilation.warnings.push(new Error('Warning')); 34 | 35 | done(); 36 | }) 37 | }, 38 | } 39 | ], 40 | infrastructureLogging: { 41 | level: 'none' 42 | }, 43 | stats: 'normal' 44 | }, 45 | { 46 | mode: 'development', 47 | context: path.resolve(__dirname), 48 | entry: './foo.js', 49 | output: { 50 | filename: 'bundle.js', 51 | path: path.resolve(__dirname, 'js3'), 52 | publicPath: '/static-three/', 53 | }, 54 | module: { 55 | rules: [ 56 | { 57 | test: /\.(svg|html)$/, 58 | loader: 'file-loader', 59 | options: { name: '[name].[ext]' }, 60 | }, 61 | ], 62 | }, 63 | infrastructureLogging: { 64 | level: 'none' 65 | }, 66 | }, 67 | ]; 68 | -------------------------------------------------------------------------------- /test/utils/ready.test.js: -------------------------------------------------------------------------------- 1 | import ready from "../../src/utils/ready"; 2 | 3 | describe("ready", () => { 4 | it("should call callback if state is true", () => { 5 | const cb = jest.fn(); 6 | const context = { 7 | state: true, 8 | stats: "stats", 9 | }; 10 | ready(context, cb); 11 | 12 | expect(cb).toHaveBeenCalledTimes(1); 13 | expect(cb.mock.calls[0]).toEqual(["stats"]); 14 | }); 15 | 16 | it("should save callback and log req.url if state is false with req.url set", () => { 17 | const cb = jest.fn(); 18 | const context = { 19 | state: false, 20 | stats: "stats", 21 | logger: { 22 | info: jest.fn(), 23 | }, 24 | callbacks: [], 25 | }; 26 | const req = { 27 | url: "url", 28 | }; 29 | ready(context, cb, req); 30 | 31 | expect(cb).not.toHaveBeenCalled(); 32 | expect(context.logger.info).toHaveBeenCalledTimes(1); 33 | expect(context.logger.info.mock.calls[0]).toEqual([ 34 | "wait until bundle finished: url", 35 | ]); 36 | expect(context.callbacks).toEqual([cb]); 37 | }); 38 | 39 | it("should save callback and log callback.name if state is false with req.url not set", () => { 40 | const cb = jest.fn(); 41 | const context = { 42 | state: false, 43 | stats: "stats", 44 | logger: { 45 | info: jest.fn(), 46 | }, 47 | callbacks: [], 48 | }; 49 | ready(context, cb); 50 | 51 | expect(cb).not.toHaveBeenCalled(); 52 | expect(context.logger.info).toHaveBeenCalledTimes(1); 53 | // mockConstructor is the name of the jest.fn() function 54 | expect(context.logger.info.mock.calls[0]).toEqual([ 55 | "wait until bundle finished: mockConstructor", 56 | ]); 57 | expect(context.callbacks).toEqual([cb]); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/fixtures/webpack.array.one-error-one-warning-one-success.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = [ 6 | { 7 | mode: 'development', 8 | context: path.resolve(__dirname), 9 | entry: './broken.js', 10 | output: { 11 | filename: 'bundle.js', 12 | path: path.resolve(__dirname, '../outputs/one-error-one-warning-one-success/js1'), 13 | publicPath: '/static-one/', 14 | }, 15 | infrastructureLogging: { 16 | level: 'none' 17 | }, 18 | stats: 'normal' 19 | }, 20 | { 21 | mode: 'development', 22 | context: path.resolve(__dirname), 23 | entry: './warning.js', 24 | output: { 25 | filename: 'bundle.js', 26 | path: path.resolve(__dirname, '../outputs/one-error-one-warning-one-success/js2'), 27 | publicPath: '/static-two/', 28 | }, 29 | plugins: [ 30 | { 31 | apply(compiler) { 32 | compiler.hooks.emit.tapAsync('WarningPlugin', (compilation, done) => { 33 | compilation.warnings.push(new Error('Warning')); 34 | 35 | done(); 36 | }) 37 | }, 38 | } 39 | ], 40 | infrastructureLogging: { 41 | level: 'none' 42 | }, 43 | stats: 'normal' 44 | }, 45 | { 46 | mode: 'development', 47 | context: path.resolve(__dirname), 48 | entry: './foo.js', 49 | output: { 50 | filename: 'bundle.js', 51 | path: path.resolve(__dirname, 'js3'), 52 | publicPath: '/static-three/', 53 | }, 54 | module: { 55 | rules: [ 56 | { 57 | test: /\.(svg|html)$/, 58 | loader: 'file-loader', 59 | options: { name: '[name].[ext]' }, 60 | }, 61 | ], 62 | }, 63 | infrastructureLogging: { 64 | level: 'none' 65 | }, 66 | stats: 'normal' 67 | }, 68 | ]; 69 | -------------------------------------------------------------------------------- /test/fixtures/webpack.array.one-error-one-warning-one-object.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = [ 6 | { 7 | mode: 'development', 8 | context: path.resolve(__dirname), 9 | entry: './broken.js', 10 | output: { 11 | filename: 'bundle.js', 12 | path: path.resolve(__dirname, '../outputs/one-error-one-warning-one-success/js1'), 13 | publicPath: '/static-one/', 14 | }, 15 | infrastructureLogging: { 16 | level: 'none' 17 | }, 18 | stats: 'normal' 19 | }, 20 | { 21 | mode: 'development', 22 | context: path.resolve(__dirname), 23 | entry: './warning.js', 24 | output: { 25 | filename: 'bundle.js', 26 | path: path.resolve(__dirname, '../outputs/one-error-one-warning-one-success/js2'), 27 | publicPath: '/static-two/', 28 | }, 29 | plugins: [ 30 | { 31 | apply(compiler) { 32 | compiler.hooks.emit.tapAsync('WarningPlugin', (compilation, done) => { 33 | compilation.warnings.push(new Error('Warning')); 34 | 35 | done(); 36 | }) 37 | }, 38 | } 39 | ], 40 | infrastructureLogging: { 41 | level: 'none' 42 | }, 43 | stats: 'normal' 44 | }, 45 | { 46 | mode: 'development', 47 | context: path.resolve(__dirname), 48 | entry: './foo.js', 49 | output: { 50 | filename: 'bundle.js', 51 | path: path.resolve(__dirname, 'js3'), 52 | publicPath: '/static-three/', 53 | }, 54 | module: { 55 | rules: [ 56 | { 57 | test: /\.(svg|html)$/, 58 | loader: 'file-loader', 59 | options: { name: '[name].[ext]' }, 60 | }, 61 | ], 62 | }, 63 | infrastructureLogging: { 64 | level: 'none' 65 | }, 66 | stats: { 67 | all: false, 68 | assets: true, 69 | } 70 | }, 71 | ]; 72 | -------------------------------------------------------------------------------- /types/middleware.d.ts: -------------------------------------------------------------------------------- 1 | export = wrapper; 2 | /** 3 | * @template {IncomingMessage} Request 4 | * @template {ServerResponse} Response 5 | * @typedef {object} SendErrorOptions send error options 6 | * @property {Record=} headers headers 7 | * @property {import("./index").ModifyResponseData=} modifyResponseData modify response data callback 8 | */ 9 | /** 10 | * @template {IncomingMessage} Request 11 | * @template {ServerResponse} Response 12 | * @param {import("./index.js").FilledContext} context context 13 | * @returns {import("./index.js").Middleware} wrapper 14 | */ 15 | declare function wrapper< 16 | Request extends IncomingMessage, 17 | Response extends ServerResponse, 18 | >( 19 | context: import("./index.js").FilledContext, 20 | ): import("./index.js").Middleware; 21 | declare namespace wrapper { 22 | export { 23 | SendErrorOptions, 24 | NextFunction, 25 | IncomingMessage, 26 | ServerResponse, 27 | NormalizedHeaders, 28 | ReadStream, 29 | }; 30 | } 31 | /** 32 | * send error options 33 | */ 34 | type SendErrorOptions< 35 | Request extends IncomingMessage, 36 | Response extends ServerResponse, 37 | > = { 38 | /** 39 | * headers 40 | */ 41 | headers?: Record | undefined; 42 | /** 43 | * modify response data callback 44 | */ 45 | modifyResponseData?: 46 | | import("./index").ModifyResponseData 47 | | undefined; 48 | }; 49 | type NextFunction = import("./index.js").NextFunction; 50 | type IncomingMessage = import("./index.js").IncomingMessage; 51 | type ServerResponse = import("./index.js").ServerResponse; 52 | type NormalizedHeaders = import("./index.js").NormalizedHeaders; 53 | type ReadStream = import("fs").ReadStream; 54 | -------------------------------------------------------------------------------- /src/utils/getPaths.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("webpack").Compiler} Compiler */ 2 | /** @typedef {import("webpack").Stats} Stats */ 3 | /** @typedef {import("webpack").MultiStats} MultiStats */ 4 | /** @typedef {import("webpack").Asset} Asset */ 5 | /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ 6 | /** @typedef {import("../index.js").ServerResponse} ServerResponse */ 7 | 8 | /** 9 | * @template {IncomingMessage} Request 10 | * @template {ServerResponse} Response 11 | * @param {import("../index.js").FilledContext} context context 12 | * @returns {{ outputPath: string, publicPath: string, assetsInfo: Asset["info"] }[]} paths 13 | */ 14 | function getPaths(context) { 15 | const { stats, options } = context; 16 | /* eslint-disable unicorn/prefer-logical-operator-over-ternary */ 17 | /** @type {Stats[]} */ 18 | const childStats = 19 | /** @type {MultiStats} */ 20 | (stats).stats 21 | ? /** @type {MultiStats} */ (stats).stats 22 | : [/** @type {Stats} */ (stats)]; 23 | /** @type {{ outputPath: string, publicPath: string, assetsInfo: Asset["info"] }[]} */ 24 | const publicPaths = []; 25 | 26 | for (const { compilation } of childStats) { 27 | if (compilation.options.devServer === false) { 28 | continue; 29 | } 30 | 31 | // The `output.path` is always present and always absolute 32 | const outputPath = compilation.getPath( 33 | compilation.outputOptions.path || "", 34 | ); 35 | const publicPath = options.publicPath 36 | ? compilation.getPath(options.publicPath) 37 | : compilation.outputOptions.publicPath 38 | ? compilation.getPath(compilation.outputOptions.publicPath) 39 | : ""; 40 | 41 | publicPaths.push({ 42 | outputPath, 43 | publicPath, 44 | assetsInfo: compilation.assetsInfo, 45 | }); 46 | } 47 | 48 | return publicPaths; 49 | } 50 | 51 | module.exports = getPaths; 52 | -------------------------------------------------------------------------------- /test/fixtures/webpack.array.one-error-one-warning-one-success-with-names.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = [ 6 | { 7 | name: "broken", 8 | mode: 'development', 9 | context: path.resolve(__dirname), 10 | entry: './broken.js', 11 | output: { 12 | filename: 'bundle.js', 13 | path: path.resolve(__dirname, '../outputs/one-error-one-warning-one-success-with-names/js1'), 14 | publicPath: '/static-one/', 15 | }, 16 | infrastructureLogging: { 17 | level: 'none' 18 | }, 19 | stats: 'normal' 20 | }, 21 | { 22 | name: "warning", 23 | mode: 'development', 24 | context: path.resolve(__dirname), 25 | entry: './warning.js', 26 | output: { 27 | filename: 'bundle.js', 28 | path: path.resolve(__dirname, '../outputs/one-error-one-warning-one-success-with-names/js2'), 29 | publicPath: '/static-two/', 30 | }, 31 | plugins: [ 32 | { 33 | apply(compiler) { 34 | compiler.hooks.emit.tapAsync('WarningPlugin', (compilation, done) => { 35 | compilation.warnings.push(new Error('Warning')); 36 | 37 | done(); 38 | }) 39 | }, 40 | } 41 | ], 42 | infrastructureLogging: { 43 | level: 'none' 44 | }, 45 | stats: 'normal' 46 | }, 47 | { 48 | name: "success", 49 | mode: 'development', 50 | context: path.resolve(__dirname), 51 | entry: './foo.js', 52 | output: { 53 | filename: 'bundle.js', 54 | path: path.resolve(__dirname, '../outputs/one-error-one-warning-one-success-with-names/js3'), 55 | publicPath: '/static-three/', 56 | }, 57 | module: { 58 | rules: [ 59 | { 60 | test: /\.(svg|html)$/, 61 | loader: 'file-loader', 62 | options: { name: '[name].[ext]' }, 63 | }, 64 | ], 65 | }, 66 | infrastructureLogging: { 67 | level: 'none' 68 | }, 69 | stats: 'normal' 70 | }, 71 | ]; 72 | -------------------------------------------------------------------------------- /types/utils/setupHooks.d.ts: -------------------------------------------------------------------------------- 1 | export = setupHooks; 2 | /** @typedef {import("webpack").Configuration} Configuration */ 3 | /** @typedef {import("webpack").Compiler} Compiler */ 4 | /** @typedef {import("webpack").MultiCompiler} MultiCompiler */ 5 | /** @typedef {import("webpack").Stats} Stats */ 6 | /** @typedef {import("webpack").MultiStats} MultiStats */ 7 | /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ 8 | /** @typedef {import("../index.js").ServerResponse} ServerResponse */ 9 | /** @typedef {Configuration["stats"]} StatsOptions */ 10 | /** @typedef {{ children: Configuration["stats"][] }} MultiStatsOptions */ 11 | /** @typedef {Exclude} StatsObjectOptions */ 12 | /** 13 | * @template {IncomingMessage} Request 14 | * @template {ServerResponse} Response 15 | * @param {import("../index.js").WithOptional, "watching" | "outputFileSystem">} context context 16 | */ 17 | declare function setupHooks< 18 | Request extends IncomingMessage, 19 | Response extends ServerResponse, 20 | >( 21 | context: import("../index.js").WithOptional< 22 | import("../index.js").Context, 23 | "watching" | "outputFileSystem" 24 | >, 25 | ): void; 26 | declare namespace setupHooks { 27 | export { 28 | Configuration, 29 | Compiler, 30 | MultiCompiler, 31 | Stats, 32 | MultiStats, 33 | IncomingMessage, 34 | ServerResponse, 35 | StatsOptions, 36 | MultiStatsOptions, 37 | StatsObjectOptions, 38 | }; 39 | } 40 | type Configuration = import("webpack").Configuration; 41 | type Compiler = import("webpack").Compiler; 42 | type MultiCompiler = import("webpack").MultiCompiler; 43 | type Stats = import("webpack").Stats; 44 | type MultiStats = import("webpack").MultiStats; 45 | type IncomingMessage = import("../index.js").IncomingMessage; 46 | type ServerResponse = import("../index.js").ServerResponse; 47 | type StatsOptions = Configuration["stats"]; 48 | type MultiStatsOptions = { 49 | children: Configuration["stats"][]; 50 | }; 51 | type StatsObjectOptions = Exclude< 52 | Configuration["stats"], 53 | boolean | string | undefined 54 | >; 55 | -------------------------------------------------------------------------------- /src/utils/setupOutputFileSystem.js: -------------------------------------------------------------------------------- 1 | const memfs = require("memfs"); 2 | 3 | /** @typedef {import("webpack").MultiCompiler} MultiCompiler */ 4 | /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ 5 | /** @typedef {import("../index.js").ServerResponse} ServerResponse */ 6 | 7 | /** 8 | * @template {IncomingMessage} Request 9 | * @template {ServerResponse} Response 10 | * @param {import("../index.js").WithOptional, "watching" | "outputFileSystem">} context context 11 | */ 12 | function setupOutputFileSystem(context) { 13 | let outputFileSystem; 14 | 15 | if (context.options.outputFileSystem) { 16 | const { outputFileSystem: outputFileSystemFromOptions } = context.options; 17 | 18 | outputFileSystem = outputFileSystemFromOptions; 19 | } 20 | // Don't use `memfs` when developer wants to write everything to a disk, because it doesn't make sense. 21 | else if (context.options.writeToDisk !== true) { 22 | outputFileSystem = memfs.createFsFromVolume(new memfs.Volume()); 23 | } else { 24 | const isMultiCompiler = 25 | /** @type {MultiCompiler} */ 26 | (context.compiler).compilers; 27 | 28 | if (isMultiCompiler) { 29 | // Prefer compiler with `devServer` option or fallback on the first 30 | // TODO we need to support webpack-dev-server as a plugin or revisit it 31 | const compiler = 32 | /** @type {MultiCompiler} */ 33 | (context.compiler).compilers.find( 34 | (item) => 35 | Object.hasOwn(item.options, "devServer") && 36 | item.options.devServer !== false, 37 | ); 38 | 39 | ({ outputFileSystem } = 40 | compiler || 41 | /** @type {MultiCompiler} */ 42 | (context.compiler).compilers[0]); 43 | } else { 44 | ({ outputFileSystem } = context.compiler); 45 | } 46 | } 47 | 48 | const compilers = 49 | /** @type {MultiCompiler} */ 50 | (context.compiler).compilers || [context.compiler]; 51 | 52 | for (const compiler of compilers) { 53 | if (compiler.options.devServer === false) { 54 | continue; 55 | } 56 | 57 | // @ts-expect-error 58 | compiler.outputFileSystem = outputFileSystem; 59 | } 60 | 61 | // @ts-expect-error 62 | context.outputFileSystem = outputFileSystem; 63 | } 64 | 65 | module.exports = setupOutputFileSystem; 66 | -------------------------------------------------------------------------------- /src/utils/etag.js: -------------------------------------------------------------------------------- 1 | const crypto = require("node:crypto"); 2 | 3 | /** @typedef {import("fs").Stats} Stats */ 4 | /** @typedef {import("fs").ReadStream} ReadStream */ 5 | 6 | /** 7 | * Generate a tag for a stat. 8 | * @param {Stats} stats stats 9 | * @returns {{ hash: string, buffer?: Buffer }} etag 10 | */ 11 | function statTag(stats) { 12 | const mtime = stats.mtime.getTime().toString(16); 13 | const size = stats.size.toString(16); 14 | 15 | return { hash: `W/"${size}-${mtime}"` }; 16 | } 17 | 18 | /** 19 | * Generate an entity tag. 20 | * @param {Buffer | ReadStream} entity entity 21 | * @returns {Promise<{ hash: string, buffer?: Buffer }>} etag 22 | */ 23 | async function entityTag(entity) { 24 | const sha1 = crypto.createHash("sha1"); 25 | 26 | if (!Buffer.isBuffer(entity)) { 27 | let byteLength = 0; 28 | 29 | /** @type {Buffer[]} */ 30 | const buffers = []; 31 | 32 | await new Promise((resolve, reject) => { 33 | entity 34 | .on("data", (chunk) => { 35 | sha1.update(chunk); 36 | buffers.push(/** @type {Buffer} */ (chunk)); 37 | byteLength += /** @type {Buffer} */ (chunk).byteLength; 38 | }) 39 | .on("end", () => { 40 | resolve(sha1); 41 | }) 42 | .on("error", reject); 43 | }); 44 | 45 | return { 46 | buffer: Buffer.concat(buffers), 47 | hash: `"${byteLength.toString(16)}-${sha1.digest("base64").slice(0, 27)}"`, 48 | }; 49 | } 50 | 51 | if (entity.byteLength === 0) { 52 | // Fast-path empty 53 | return { hash: '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"' }; 54 | } 55 | 56 | // Compute hash of entity 57 | const hash = sha1.update(entity).digest("base64").slice(0, 27); 58 | 59 | // Compute length of entity 60 | const { byteLength } = entity; 61 | 62 | return { hash: `"${byteLength.toString(16)}-${hash}"` }; 63 | } 64 | 65 | /** 66 | * Create a simple ETag. 67 | * @param {Buffer | ReadStream | Stats} entity entity 68 | * @returns {Promise<{ hash: string, buffer?: Buffer }>} etag 69 | */ 70 | async function etag(entity) { 71 | const isStrong = 72 | Buffer.isBuffer(entity) || 73 | typeof (/** @type {ReadStream} */ (entity).pipe) === "function"; 74 | 75 | return isStrong 76 | ? entityTag(/** @type {Buffer | ReadStream} */ (entity)) 77 | : statTag(/** @type {import("fs").Stats} */ (entity)); 78 | } 79 | 80 | module.exports = etag; 81 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: webpack-dev-middleware 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | pull_request: 9 | branches: 10 | - main 11 | - next 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | lint: 18 | name: Lint - ${{ matrix.os }} - Node v${{ matrix.node-version }} 19 | 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest] 26 | node-version: [lts/*] 27 | 28 | runs-on: ${{ matrix.os }} 29 | 30 | concurrency: 31 | group: lint-${{ matrix.os }}-v${{ matrix.node-version }}-${{ github.ref }} 32 | cancel-in-progress: true 33 | steps: 34 | - uses: actions/checkout@v4 35 | with: 36 | fetch-depth: 0 37 | 38 | - name: Use Node.js ${{ matrix.node-version }} 39 | uses: actions/setup-node@v4 40 | with: 41 | node-version: ${{ matrix.node-version }} 42 | cache: "npm" 43 | 44 | - name: Install dependencies 45 | run: npm ci 46 | 47 | - name: Lint 48 | run: npm run lint 49 | 50 | - name: Build types 51 | run: npm run build:types 52 | 53 | - name: Check types 54 | 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 55 | 56 | - name: Security audit 57 | run: npm run security 58 | 59 | - name: Validate PR commits with commitlint 60 | if: github.event_name == 'pull_request' 61 | run: npx commitlint --from ${{ github.event.pull_request.head.sha }}~${{ github.event.pull_request.commits }} --to ${{ github.event.pull_request.head.sha }} --verbose 62 | 63 | test: 64 | name: Test - ${{ matrix.os }} - Node v${{ matrix.node-version }}, Webpack ${{ matrix.webpack-version }} 65 | 66 | strategy: 67 | matrix: 68 | os: [ubuntu-latest, windows-latest, macos-latest] 69 | node-version: [18.x, 20.x, 22.x, 24.x] 70 | webpack-version: [latest] 71 | 72 | runs-on: ${{ matrix.os }} 73 | 74 | concurrency: 75 | group: test-${{ matrix.os }}-v${{ matrix.node-version }}-${{ matrix.webpack-version }}-${{ github.ref }} 76 | cancel-in-progress: true 77 | 78 | steps: 79 | - uses: actions/checkout@v4 80 | 81 | - name: Use Node.js ${{ matrix.node-version }} 82 | uses: actions/setup-node@v4 83 | with: 84 | node-version: ${{ matrix.node-version }} 85 | cache: "npm" 86 | 87 | - name: Install dependencies 88 | run: npm ci 89 | 90 | - name: Run tests for webpack version ${{ matrix.webpack-version }} 91 | run: npm run test:coverage -- --ci 92 | 93 | - name: Submit coverage data to codecov 94 | uses: codecov/codecov-action@v5 95 | with: 96 | token: ${{ secrets.CODECOV_TOKEN }} 97 | -------------------------------------------------------------------------------- /src/utils/setupWriteToDisk.js: -------------------------------------------------------------------------------- 1 | const fs = require("node:fs"); 2 | const path = require("node:path"); 3 | 4 | /** @typedef {import("webpack").Compiler} Compiler */ 5 | /** @typedef {import("webpack").MultiCompiler} MultiCompiler */ 6 | /** @typedef {import("webpack").Compilation} Compilation */ 7 | /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ 8 | /** @typedef {import("../index.js").ServerResponse} ServerResponse */ 9 | 10 | /** 11 | * @template {IncomingMessage} Request 12 | * @template {ServerResponse} Response 13 | * @param {import("../index.js").WithOptional, "watching" | "outputFileSystem">} context context 14 | */ 15 | function setupWriteToDisk(context) { 16 | /** 17 | * @type {Compiler[]} 18 | */ 19 | const compilers = 20 | /** @type {MultiCompiler} */ 21 | (context.compiler).compilers || [context.compiler]; 22 | 23 | for (const compiler of compilers) { 24 | if (compiler.options.devServer === false) { 25 | continue; 26 | } 27 | 28 | compiler.hooks.emit.tap("DevMiddleware", () => { 29 | // @ts-expect-error 30 | if (compiler.hasWebpackDevMiddlewareAssetEmittedCallback) { 31 | return; 32 | } 33 | 34 | compiler.hooks.assetEmitted.tapAsync( 35 | "DevMiddleware", 36 | (file, info, callback) => { 37 | const { targetPath, content } = info; 38 | const { writeToDisk: filter } = context.options; 39 | const allowWrite = 40 | filter && typeof filter === "function" ? filter(targetPath) : true; 41 | 42 | if (!allowWrite) { 43 | return callback(); 44 | } 45 | 46 | const dir = path.dirname(targetPath); 47 | const name = compiler.options.name 48 | ? `Child "${compiler.options.name}": ` 49 | : ""; 50 | 51 | return fs.mkdir(dir, { recursive: true }, (mkdirError) => { 52 | if (mkdirError) { 53 | context.logger.error( 54 | `${name}Unable to write "${dir}" directory to disk:\n${mkdirError}`, 55 | ); 56 | 57 | return callback(mkdirError); 58 | } 59 | 60 | return fs.writeFile(targetPath, content, (writeFileError) => { 61 | if (writeFileError) { 62 | context.logger.error( 63 | `${name}Unable to write "${targetPath}" asset to disk:\n${writeFileError}`, 64 | ); 65 | 66 | return callback(writeFileError); 67 | } 68 | 69 | context.logger.log( 70 | `${name}Asset written to disk: "${targetPath}"`, 71 | ); 72 | 73 | return callback(); 74 | }); 75 | }); 76 | }, 77 | ); 78 | 79 | // @ts-expect-error 80 | compiler.hasWebpackDevMiddlewareAssetEmittedCallback = true; 81 | }); 82 | } 83 | } 84 | 85 | module.exports = setupWriteToDisk; 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-dev-middleware", 3 | "version": "7.4.5", 4 | "description": "A development middleware for webpack", 5 | "keywords": [ 6 | "webpack", 7 | "middleware", 8 | "development" 9 | ], 10 | "homepage": "https://github.com/webpack/webpack-dev-middleware", 11 | "bugs": "https://github.com/webpack/webpack-dev-middleware/issues", 12 | "repository": "webpack/webpack-dev-middleware", 13 | "funding": { 14 | "type": "opencollective", 15 | "url": "https://opencollective.com/webpack" 16 | }, 17 | "license": "MIT", 18 | "author": "Tobias Koppers @sokra", 19 | "main": "dist/index.js", 20 | "types": "types/index.d.ts", 21 | "files": [ 22 | "dist", 23 | "types" 24 | ], 25 | "scripts": { 26 | "commitlint": "commitlint --from=main", 27 | "security": "npm audit --production", 28 | "lint:prettier": "prettier --cache --list-different .", 29 | "lint:code": "eslint --cache .", 30 | "lint:spelling": "cspell --cache --no-must-find-files --quiet \"**/*.*\"", 31 | "lint:types": "tsc --pretty --noEmit", 32 | "lint": "npm-run-all -l -p \"lint:**\"", 33 | "fix:js": "npm run lint:code -- --fix", 34 | "fix:prettier": "npm run lint:prettier -- --write", 35 | "fix": "npm-run-all -l fix:js fix:prettier", 36 | "clean": "del-cli dist types", 37 | "prebuild": "npm run clean", 38 | "build:types": "tsc --declaration --emitDeclarationOnly --outDir types && prettier \"types/**/*.ts\" --write", 39 | "build:code": "babel src -d dist --copy-files", 40 | "build": "npm-run-all -p \"build:**\"", 41 | "test:only": "jest", 42 | "test:watch": "npm run test:only -- --watch", 43 | "test:coverage": "npm run test:only -- --collectCoverageFrom=\"src/**/*.js\" --coverage", 44 | "pretest": "npm run lint", 45 | "test": "npm run test:coverage", 46 | "prepare": "husky && npm run build", 47 | "release": "standard-version" 48 | }, 49 | "dependencies": { 50 | "colorette": "^2.0.10", 51 | "memfs": "^4.43.1", 52 | "mime-types": "^3.0.1", 53 | "on-finished": "^2.4.1", 54 | "range-parser": "^1.2.1", 55 | "schema-utils": "^4.0.0" 56 | }, 57 | "devDependencies": { 58 | "@babel/cli": "^7.16.7", 59 | "@babel/core": "^7.16.7", 60 | "@babel/preset-env": "^7.16.7", 61 | "@eslint/js": "^9.28.0", 62 | "@eslint/markdown": "^7.1.0", 63 | "@commitlint/cli": "^20.2.0", 64 | "@commitlint/config-conventional": "^20.2.0", 65 | "@fastify/express": "^4.0.2", 66 | "@hapi/hapi": "^21.3.7", 67 | "@hono/node-server": "^1.12.0", 68 | "@stylistic/eslint-plugin": "^5.3.1", 69 | "@types/connect": "^3.4.35", 70 | "@types/express": "^5.0.2", 71 | "@types/mime-types": "^3.0.1", 72 | "@types/node": "^22.3.0", 73 | "@types/on-finished": "^2.3.4", 74 | "babel-jest": "^30.1.2", 75 | "connect": "^3.7.0", 76 | "cspell": "^8.3.2", 77 | "deepmerge": "^4.2.2", 78 | "del-cli": "^6.0.0", 79 | "globals": "^16.2.0", 80 | "eslint": "^9.28.0", 81 | "eslint-config-webpack": "^4.5.0", 82 | "eslint-config-prettier": "^10.1.5", 83 | "eslint-plugin-import": "^2.31.0", 84 | "eslint-plugin-jest": "^29.0.1", 85 | "eslint-plugin-jsdoc": "^61.5.0", 86 | "eslint-plugin-n": "^17.19.0", 87 | "eslint-plugin-prettier": "^5.4.1", 88 | "eslint-plugin-unicorn": "^62.0.0", 89 | "execa": "^5.1.1", 90 | "express-4": "npm:express@^4", 91 | "express": "^5.1.0", 92 | "fastify": "^5.2.1", 93 | "file-loader": "^6.2.0", 94 | "finalhandler": "^2.1.0", 95 | "hono": "^4.4.13", 96 | "husky": "^9.1.3", 97 | "jest": "^30.1.3", 98 | "koa": "^3.0.0", 99 | "lint-staged": "^15.2.0", 100 | "npm-run-all": "^4.1.5", 101 | "prettier": "^3.6.0", 102 | "router": "^2.2.0", 103 | "standard-version": "^9.3.0", 104 | "supertest": "^7.0.0", 105 | "typescript": "^5.3.3", 106 | "webpack": "^5.101.0" 107 | }, 108 | "peerDependencies": { 109 | "webpack": "^5.0.0" 110 | }, 111 | "peerDependenciesMeta": { 112 | "webpack": { 113 | "optional": true 114 | } 115 | }, 116 | "engines": { 117 | "node": ">= 18.12.0" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /test/validation-options.test.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | import { Volume, createFsFromVolume } from "memfs"; 4 | 5 | import middleware from "../src"; 6 | 7 | import getCompiler from "./helpers/getCompiler"; 8 | 9 | // Suppress unnecessary stats output 10 | jest.spyOn(globalThis.console, "log").mockImplementation(); 11 | 12 | const configuredFs = createFsFromVolume(new Volume()); 13 | 14 | configuredFs.join = path.join.bind(path); 15 | 16 | describe("validation", () => { 17 | const cases = { 18 | mimeTypes: { 19 | success: [{ phtml: ["text/html"] }], 20 | failure: ["foo"], 21 | }, 22 | writeToDisk: { 23 | success: [true, false, () => {}], 24 | failure: [{}], 25 | }, 26 | methods: { 27 | success: [["GET", "HEAD"]], 28 | failure: [{}, true], 29 | }, 30 | headers: { 31 | success: [ 32 | { "X-Custom-Header": "yes" }, 33 | () => {}, 34 | [{ key: "foo", value: "bar" }], 35 | ], 36 | failure: [true, 1, [], [{ foo: "bar" }]], 37 | }, 38 | publicPath: { 39 | success: ["/foo", "", "auto", () => "/public/path"], 40 | failure: [false], 41 | }, 42 | serverSideRender: { 43 | success: [true], 44 | failure: ["foo", 0], 45 | }, 46 | outputFileSystem: { 47 | success: [configuredFs], 48 | failure: [false], 49 | }, 50 | index: { 51 | success: [true, false, "foo"], 52 | failure: [0, {}], 53 | }, 54 | stats: { 55 | success: [true, false, "normal", "verbose", { all: false, assets: true }], 56 | failure: [0], 57 | }, 58 | mimeTypeDefault: { 59 | success: ["text/plain"], 60 | failure: [0], 61 | }, 62 | modifyResponseData: { 63 | success: [(_ignore, _ignore1, foo, bar) => ({ foo, bar })], 64 | failure: [true], 65 | }, 66 | etag: { 67 | success: ["weak", "strong"], 68 | failure: ["foo", 0], 69 | }, 70 | lastModified: { 71 | success: [true, false], 72 | failure: ["foo", 0], 73 | }, 74 | cacheControl: { 75 | success: [ 76 | true, 77 | false, 78 | 10000, 79 | "max-age=100", 80 | { immutable: true, maxAge: 10000 }, 81 | ], 82 | failure: [{ unknown: true, maxAge: 10000 }], 83 | }, 84 | cacheImmutable: { 85 | success: [true, false], 86 | failure: ["foo", 0], 87 | }, 88 | }; 89 | 90 | // eslint-disable-next-line jsdoc/reject-any-type 91 | /** @typedef {any} EXPECTED_ANY */ 92 | 93 | /** 94 | * @param {EXPECTED_ANY} value value 95 | * @returns {string} stringified value 96 | */ 97 | function stringifyValue(value) { 98 | if ( 99 | Array.isArray(value) || 100 | (value && typeof value === "object" && value.constructor === Object) 101 | ) { 102 | return JSON.stringify(value); 103 | } 104 | 105 | return value; 106 | } 107 | 108 | /** 109 | * @param {string} key key 110 | * @param {EXPECTED_ANY} value value 111 | * @param {"success" | "failure"} type type 112 | */ 113 | function createTestCase(key, value, type) { 114 | it(`should ${ 115 | type === "success" ? "successfully validate" : "throw an error on" 116 | } the "${key}" option with "${stringifyValue(value)}" value`, (done) => { 117 | const compiler = getCompiler(); 118 | 119 | let webpackDevMiddleware; 120 | let error; 121 | 122 | try { 123 | webpackDevMiddleware = middleware(compiler, { [key]: value }); 124 | } catch (err) { 125 | if (err.name !== "ValidationError") { 126 | throw err; 127 | } 128 | 129 | error = err; 130 | } finally { 131 | if (type === "success") { 132 | expect(error).toBeUndefined(); 133 | } else if (type === "failure") { 134 | expect(() => { 135 | throw error; 136 | }).toThrowErrorMatchingSnapshot(); 137 | } 138 | 139 | if (webpackDevMiddleware) { 140 | webpackDevMiddleware.waitUntilValid(() => { 141 | webpackDevMiddleware.close(() => { 142 | done(); 143 | }); 144 | }); 145 | } else { 146 | done(); 147 | } 148 | } 149 | }); 150 | } 151 | 152 | for (const [key, values] of Object.entries(cases)) { 153 | for (const type of Object.keys(values)) { 154 | for (const value of values[type]) { 155 | createTestCase(key, value, type); 156 | } 157 | } 158 | } 159 | }); 160 | -------------------------------------------------------------------------------- /test/helpers/runner.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const merge = require("deepmerge"); 4 | const express = require("express"); 5 | const webpack = require("webpack"); 6 | 7 | const middleware = require("../../dist"); 8 | const defaultConfig = require("../fixtures/webpack.config"); 9 | 10 | const configEntries = []; 11 | const configMiddlewareEntries = []; 12 | 13 | /** 14 | * @param {string} NSKey NSKey 15 | * @param {string[]} accumulator accumulator 16 | */ 17 | function fillConfigEntries(NSKey, accumulator) { 18 | for (const key of Object.keys(process.env).filter( 19 | (key) => key.indexOf(NSKey) === 0, 20 | )) { 21 | let value = process.env[key]; 22 | const keys = key.replace(NSKey, "").split("_"); 23 | 24 | value = value === "true" ? true : value === "false" ? false : value; 25 | 26 | keys.push(value); 27 | accumulator.push(keys); 28 | } 29 | } 30 | 31 | fillConfigEntries("WCF_", configEntries); 32 | fillConfigEntries("WMC_", configMiddlewareEntries); 33 | 34 | /** 35 | * @param {string} name name 36 | * @returns {import("webpack").Configuration | import("webpack").Configuration[]} configuration 37 | */ 38 | function getWebpackConfig(name) { 39 | try { 40 | return require(`../fixtures/${name}`); 41 | } catch { 42 | return require("../fixtures/webpack.config"); 43 | } 44 | } 45 | 46 | /** 47 | * @param {import("webpack").Configuration[]} data data 48 | * @returns {import("webpack").Configuration} configuration 49 | */ 50 | function createConfig(data) { 51 | /** 52 | * @param {string} entry entry 53 | * @returns {{ [string]: string }} object 54 | */ 55 | function getObject(entry) { 56 | return { [entry[0]]: entry[1] }; 57 | } 58 | 59 | /** 60 | * @param {import("webpack").Configuration[]} arr arr 61 | * @returns {import("webpack").Configuration} result 62 | */ 63 | function reduceObject(arr) { 64 | if (arr.length > 1) { 65 | const temp = []; 66 | temp.push(arr.pop()); 67 | temp.push(arr.pop()); 68 | 69 | return reduceObject([...arr, getObject(temp.reverse())]); 70 | } 71 | 72 | return arr[0]; 73 | } 74 | 75 | const result = data.map((el) => reduceObject([...el])); 76 | 77 | return merge.all(result); 78 | } 79 | 80 | const createdConfig = createConfig(configEntries); 81 | const unionConfig = 82 | Object.keys(createdConfig).length > 0 83 | ? merge(getWebpackConfig(process.env.WEBPACK_CONFIG), createdConfig) 84 | : getWebpackConfig(process.env.WEBPACK_CONFIG); 85 | const configMiddleware = createConfig(configMiddlewareEntries); 86 | const config = unionConfig || defaultConfig; 87 | 88 | if (Array.isArray(config)) { 89 | config.parallelism = 1; 90 | } 91 | 92 | const compiler = webpack(config); 93 | 94 | if (process.env.WEBPACK_BREAK_WATCH) { 95 | compiler.watch = function watch() { 96 | const error = new Error("Watch error"); 97 | error.code = "watch error"; 98 | 99 | throw error; 100 | }; 101 | } 102 | 103 | compiler.hooks.done.tap("plugin-test", () => { 104 | process.stdout.write("compiled-for-tests"); 105 | }); 106 | 107 | switch (process.env.WEBPACK_DEV_MIDDLEWARE_STATS) { 108 | case "object": 109 | configMiddleware.stats = { all: false, assets: true }; 110 | break; 111 | case "object_colors_true": 112 | configMiddleware.stats = { all: false, assets: true, colors: true }; 113 | break; 114 | case "object_colors_false": 115 | configMiddleware.stats = { all: false, assets: true, colors: false }; 116 | break; 117 | default: 118 | // Nothing 119 | } 120 | 121 | const instance = middleware(compiler, configMiddleware); 122 | const app = express(); 123 | 124 | app.use(instance); 125 | app.listen((error) => { 126 | if (error) { 127 | throw error; 128 | } 129 | 130 | let commands = []; 131 | let incompleteCommand = ""; 132 | 133 | process.stdin.on("data", (chunk) => { 134 | const entries = chunk.toString().split("|"); 135 | 136 | incompleteCommand += entries.shift(); 137 | commands.push(incompleteCommand); 138 | incompleteCommand = entries.pop(); 139 | commands = [...commands, ...entries]; 140 | 141 | while (commands.length > 0) { 142 | switch (commands.shift()) { 143 | // case 'invalidate': 144 | // stdinInput = ''; 145 | // instance.waitUntilValid(() => { 146 | // instance.invalidate(); 147 | // }); 148 | // break; 149 | case "exit": 150 | // eslint-disable-next-line n/no-process-exit 151 | process.exit(); 152 | break; 153 | } 154 | } 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /test/fixtures/svg.svg: -------------------------------------------------------------------------------- 1 | 2 | SVG Logo 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | SVG 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/utils/setupWriteToDisk.test.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | 3 | import setupWriteToDisk from "../../src/utils/setupWriteToDisk"; 4 | 5 | const mkdirSpy = jest.spyOn(fs, "mkdir"); 6 | const writeFileSpy = jest.spyOn(fs, "writeFile"); 7 | 8 | describe("setupWriteToDisk", () => { 9 | let context; 10 | const emitHook = jest.fn(); 11 | const assetEmittedHook = jest.fn(); 12 | const getPath = jest.fn((outputPath) => outputPath); 13 | 14 | beforeEach(() => { 15 | context = { 16 | compiler: { 17 | hooks: { 18 | emit: { 19 | tap: emitHook, 20 | }, 21 | assetEmitted: { 22 | tapAsync: assetEmittedHook, 23 | }, 24 | }, 25 | outputPath: "/output/path/", 26 | options: { 27 | name: "name", 28 | }, 29 | }, 30 | logger: { 31 | error: jest.fn(), 32 | log: jest.fn(), 33 | }, 34 | }; 35 | }); 36 | 37 | afterEach(() => { 38 | emitHook.mockClear(); 39 | assetEmittedHook.mockClear(); 40 | getPath.mockClear(); 41 | mkdirSpy.mockClear(); 42 | writeFileSpy.mockClear(); 43 | }); 44 | 45 | const runAssetEmitted = (...args) => { 46 | // calls the emit hook callback 47 | emitHook.mock.calls[0][1]({ 48 | getPath, 49 | }); 50 | // calls the asset emitted hook 51 | assetEmittedHook.mock.calls[0][1](...args); 52 | }; 53 | 54 | it("will not tap assetEmitted twice for compiler", () => { 55 | setupWriteToDisk(context); 56 | // this simulates the emit hook being called twice 57 | emitHook.mock.calls[0][1](); 58 | emitHook.mock.calls[0][1](); 59 | expect(assetEmittedHook).toHaveBeenCalledTimes(1); 60 | }); 61 | 62 | it("filters out unwanted emits with writeToDisk", () => { 63 | const filter = jest.fn(() => false); 64 | context.options = { 65 | writeToDisk: filter, 66 | }; 67 | setupWriteToDisk(context); 68 | const cb = jest.fn(); 69 | // webpack@5 info style 70 | runAssetEmitted( 71 | null, 72 | { 73 | compilation: {}, 74 | targetPath: "targetPath", 75 | }, 76 | cb, 77 | ); 78 | 79 | // the getPath helper is not needed for webpack@5 80 | expect(getPath).not.toHaveBeenCalled(); 81 | 82 | expect(filter).toHaveBeenCalledTimes(1); 83 | expect(filter.mock.calls[0][0]).toBe("targetPath"); 84 | // the callback should always be called 85 | expect(cb).toHaveBeenCalledTimes(1); 86 | // the filter prevents a directory from being made 87 | expect(mkdirSpy).not.toHaveBeenCalled(); 88 | }); 89 | 90 | const writeErrors = [ 91 | { 92 | title: "with no write errors", 93 | mkdirError: null, 94 | writeFileError: null, 95 | }, 96 | { 97 | title: "with mkdir error", 98 | mkdirError: "error1", 99 | writeFileError: null, 100 | }, 101 | { 102 | title: "with writeFile error", 103 | mkdirError: null, 104 | writeFileError: "error2", 105 | }, 106 | ]; 107 | 108 | for (const writeError of writeErrors) { 109 | // eslint-disable-next-line no-loop-func 110 | it(`tries to create directories and write file if not filtered out ${writeError.title}`, () => { 111 | context.options = {}; 112 | setupWriteToDisk(context); 113 | const cb = jest.fn(); 114 | // webpack@5 info style 115 | runAssetEmitted( 116 | null, 117 | { 118 | compilation: {}, 119 | targetPath: "/target/path/file", 120 | content: "content", 121 | }, 122 | cb, 123 | ); 124 | 125 | // the getPath helper is not needed for webpack@5 126 | expect(getPath).not.toHaveBeenCalled(); 127 | 128 | expect(mkdirSpy).toHaveBeenCalledTimes(1); 129 | expect(mkdirSpy.mock.calls[0][0]).toBe("/target/path"); 130 | 131 | // simulates the mkdir callback being called 132 | mkdirSpy.mock.calls[0][2](writeError.mkdirError); 133 | 134 | if (writeError.mkdirError) { 135 | expect(writeFileSpy).not.toHaveBeenCalled(); 136 | } else { 137 | expect(writeFileSpy).toHaveBeenCalledTimes(1); 138 | expect(writeFileSpy.mock.calls[0][0]).toBe("/target/path/file"); 139 | expect(writeFileSpy.mock.calls[0][1]).toBe("content"); 140 | 141 | // simulates the writeFile callback being called 142 | writeFileSpy.mock.calls[0][2](writeError.writeFileError); 143 | } 144 | 145 | // expected logs based on errors 146 | expect(context.logger.error.mock.calls).toMatchSnapshot(); 147 | expect(context.logger.log.mock.calls).toMatchSnapshot(); 148 | 149 | // the callback should always be called 150 | expect(cb).toHaveBeenCalledTimes(1); 151 | // no errors are expected 152 | expect(cb.mock.calls).toMatchSnapshot(); 153 | }); 154 | } 155 | }); 156 | -------------------------------------------------------------------------------- /src/utils/getFilenameFromUrl.js: -------------------------------------------------------------------------------- 1 | const path = require("node:path"); 2 | const querystring = require("node:querystring"); 3 | // eslint-disable-next-line n/no-deprecated-api 4 | const { parse } = require("node:url"); 5 | 6 | const getPaths = require("./getPaths"); 7 | const memorize = require("./memorize"); 8 | 9 | /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ 10 | /** @typedef {import("../index.js").ServerResponse} ServerResponse */ 11 | 12 | /** 13 | * @param {string} input input 14 | * @returns {string} unescape input 15 | */ 16 | function decode(input) { 17 | return querystring.unescape(input); 18 | } 19 | 20 | const memoizedParse = memorize(parse, undefined, (value) => { 21 | if (value.pathname) { 22 | value.pathname = decode(value.pathname); 23 | } 24 | 25 | return value; 26 | }); 27 | 28 | const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/; 29 | 30 | /** 31 | * @typedef {object} Extra 32 | * @property {import("fs").Stats=} stats stats 33 | * @property {number=} errorCode error code 34 | * @property {boolean=} immutable true when immutable, otherwise false 35 | */ 36 | 37 | /** 38 | * decodeURIComponent. 39 | * 40 | * Allows V8 to only deoptimize this fn instead of all of send(). 41 | * @param {string} input 42 | * @returns {string} 43 | */ 44 | 45 | // TODO refactor me in the next major release, this function should return `{ filename, stats, error }` 46 | // TODO fix redirect logic when `/` at the end, like https://github.com/pillarjs/send/blob/master/index.js#L586 47 | /** 48 | * @template {IncomingMessage} Request 49 | * @template {ServerResponse} Response 50 | * @param {import("../index.js").FilledContext} context context 51 | * @param {string} url url 52 | * @param {Extra=} extra extra 53 | * @returns {string | undefined} filename 54 | */ 55 | function getFilenameFromUrl(context, url, extra = {}) { 56 | const { options } = context; 57 | const paths = getPaths(context); 58 | 59 | /** @type {string | undefined} */ 60 | let foundFilename; 61 | /** @type {import("node:url").Url} */ 62 | let urlObject; 63 | 64 | try { 65 | // The `url` property of the `request` is contains only `pathname`, `search` and `hash` 66 | urlObject = memoizedParse(url, false, true); 67 | } catch { 68 | return; 69 | } 70 | 71 | for (const { publicPath, outputPath, assetsInfo } of paths) { 72 | /** @type {string | undefined} */ 73 | let filename; 74 | /** @type {import("node:url").Url} */ 75 | let publicPathObject; 76 | 77 | try { 78 | publicPathObject = memoizedParse( 79 | publicPath !== "auto" && publicPath ? publicPath : "/", 80 | false, 81 | true, 82 | ); 83 | } catch { 84 | continue; 85 | } 86 | 87 | const { pathname } = urlObject; 88 | const { pathname: publicPathPathname } = publicPathObject; 89 | 90 | if ( 91 | pathname && 92 | publicPathPathname && 93 | pathname.startsWith(publicPathPathname) 94 | ) { 95 | // Null byte(s) 96 | if (pathname.includes("\0")) { 97 | extra.errorCode = 400; 98 | 99 | return; 100 | } 101 | 102 | // ".." is malicious 103 | if (UP_PATH_REGEXP.test(path.normalize(`./${pathname}`))) { 104 | extra.errorCode = 403; 105 | 106 | return; 107 | } 108 | 109 | // Strip the `pathname` property from the `publicPath` option from the start of requested url 110 | // `/complex/foo.js` => `foo.js` 111 | // and add outputPath 112 | // `foo.js` => `/home/user/my-project/dist/foo.js` 113 | filename = path.join( 114 | outputPath, 115 | pathname.slice(publicPathPathname.length), 116 | ); 117 | 118 | try { 119 | extra.stats = context.outputFileSystem.statSync(filename); 120 | } catch { 121 | continue; 122 | } 123 | 124 | if (extra.stats.isFile()) { 125 | foundFilename = filename; 126 | 127 | // Rspack does not yet support `assetsInfo`, so we need to check if `assetsInfo` exists here 128 | if (assetsInfo) { 129 | const assetInfo = assetsInfo.get( 130 | pathname.slice(publicPathPathname.length), 131 | ); 132 | 133 | extra.immutable = assetInfo ? assetInfo.immutable : false; 134 | } 135 | 136 | break; 137 | } else if ( 138 | extra.stats.isDirectory() && 139 | (typeof options.index === "undefined" || options.index) 140 | ) { 141 | const indexValue = 142 | typeof options.index === "undefined" || 143 | typeof options.index === "boolean" 144 | ? "index.html" 145 | : options.index; 146 | 147 | filename = path.join(filename, indexValue); 148 | 149 | try { 150 | extra.stats = context.outputFileSystem.statSync(filename); 151 | } catch { 152 | continue; 153 | } 154 | 155 | if (extra.stats.isFile()) { 156 | foundFilename = filename; 157 | 158 | break; 159 | } 160 | } 161 | } 162 | } 163 | 164 | return foundFilename; 165 | } 166 | 167 | module.exports = getFilenameFromUrl; 168 | -------------------------------------------------------------------------------- /test/utils/setupHooks.test.js: -------------------------------------------------------------------------------- 1 | import setupHooks from "../../src/utils/setupHooks"; 2 | 3 | // Suppress unnecessary stats output 4 | jest.spyOn(globalThis.console, "log").mockImplementation(); 5 | 6 | describe("setupHooks", () => { 7 | let context; 8 | const watchRunHook = jest.fn(); 9 | const invalidHook = jest.fn(); 10 | const doneHook = jest.fn(); 11 | const loggerLog = jest.fn(); 12 | const loggerInfo = jest.fn(); 13 | const loggerWarn = jest.fn(); 14 | const loggerError = jest.fn(); 15 | let nextTick; 16 | 17 | const cb1 = jest.fn(); 18 | const cb2 = jest.fn(); 19 | 20 | beforeEach(() => { 21 | nextTick = jest.spyOn(process, "nextTick").mockImplementation(() => {}); 22 | context = { 23 | options: {}, 24 | compiler: { 25 | hooks: { 26 | watchRun: { 27 | tap: watchRunHook, 28 | }, 29 | invalid: { 30 | tap: invalidHook, 31 | }, 32 | done: { 33 | tap: doneHook, 34 | }, 35 | }, 36 | options: { stats: {} }, 37 | }, 38 | logger: { 39 | log: loggerLog, 40 | info: loggerInfo, 41 | warn: loggerWarn, 42 | error: loggerError, 43 | }, 44 | callbacks: [cb1, cb2], 45 | }; 46 | }); 47 | 48 | afterEach(() => { 49 | watchRunHook.mockClear(); 50 | invalidHook.mockClear(); 51 | doneHook.mockClear(); 52 | loggerInfo.mockClear(); 53 | loggerWarn.mockClear(); 54 | loggerError.mockClear(); 55 | nextTick.mockClear(); 56 | cb1.mockClear(); 57 | cb2.mockClear(); 58 | }); 59 | 60 | it("taps watchRun, invalid, and done", () => { 61 | setupHooks(context); 62 | expect(watchRunHook).toHaveBeenCalledTimes(1); 63 | expect(invalidHook).toHaveBeenCalledTimes(1); 64 | expect(doneHook).toHaveBeenCalledTimes(1); 65 | }); 66 | 67 | it("watchRun hook invalidates", () => { 68 | setupHooks(context); 69 | // this calls invalidate 70 | watchRunHook.mock.calls[0][1](); 71 | expect(context.state).toBe(false); 72 | expect(context.stats).toBeUndefined(); 73 | expect(loggerInfo).not.toHaveBeenCalled(); 74 | }); 75 | 76 | it("invalid hook invalidates", () => { 77 | setupHooks(context); 78 | // this calls invalidate 79 | invalidHook.mock.calls[0][1](); 80 | expect(context.state).toBe(false); 81 | expect(context.stats).toBeUndefined(); 82 | expect(loggerInfo).not.toHaveBeenCalled(); 83 | }); 84 | 85 | it("logs if state is set on invalidate", () => { 86 | context.state = true; 87 | setupHooks(context); 88 | // this calls invalidate 89 | invalidHook.mock.calls[0][1](); 90 | expect(context.state).toBe(false); 91 | expect(context.stats).toBeUndefined(); 92 | expect(loggerLog.mock.calls[0][0]).toBe("Compilation starting..."); 93 | }); 94 | 95 | it("sets state, then logs stats and handles callbacks on nextTick from done hook", () => { 96 | setupHooks(context); 97 | doneHook.mock.calls[0][1]({ 98 | toString: jest.fn(() => "statsString"), 99 | hasErrors: jest.fn(() => false), 100 | hasWarnings: jest.fn(() => false), 101 | }); 102 | expect(context.stats).toBeTruthy(); 103 | expect(context.state).toBeTruthy(); 104 | expect(nextTick).toHaveBeenCalledTimes(1); 105 | 106 | nextTick.mock.calls[0][0](); 107 | expect(loggerInfo.mock.calls).toMatchSnapshot(); 108 | expect(loggerError).not.toHaveBeenCalled(); 109 | expect(loggerWarn).not.toHaveBeenCalled(); 110 | 111 | expect(cb1.mock.calls[0][0]).toEqual(context.stats); 112 | expect(cb2.mock.calls[0][0]).toEqual(context.stats); 113 | }); 114 | 115 | it("stops on done if invalidated before nextTick", () => { 116 | setupHooks(context); 117 | doneHook.mock.calls[0][1]("stats"); 118 | expect(context.stats).toBe("stats"); 119 | expect(context.state).toBeTruthy(); 120 | expect(nextTick).toHaveBeenCalledTimes(1); 121 | context.state = false; 122 | nextTick.mock.calls[0][0](); 123 | expect(loggerInfo).not.toHaveBeenCalled(); 124 | }); 125 | 126 | it("handles multi compiler", () => { 127 | context.compiler.compilers = [ 128 | { 129 | options: { 130 | name: "comp1", 131 | stats: {}, 132 | }, 133 | }, 134 | { 135 | options: { 136 | name: "comp2", 137 | stats: {}, 138 | }, 139 | }, 140 | ]; 141 | setupHooks(context); 142 | doneHook.mock.calls[0][1]({ 143 | stats: [ 144 | { 145 | toString: jest.fn(() => "statsString1"), 146 | hasErrors: jest.fn(() => true), 147 | hasWarnings: jest.fn(() => false), 148 | }, 149 | { 150 | toString: jest.fn(() => "statsString2"), 151 | hasErrors: jest.fn(() => false), 152 | hasWarnings: jest.fn(() => true), 153 | }, 154 | ], 155 | }); 156 | expect(context.stats).toBeTruthy(); 157 | expect(context.state).toBeTruthy(); 158 | expect(nextTick).toHaveBeenCalledTimes(1); 159 | 160 | nextTick.mock.calls[0][0](); 161 | expect(loggerInfo.mock.calls).toMatchSnapshot(); 162 | expect(loggerError.mock.calls).toMatchSnapshot(); 163 | expect(loggerWarn.mock.calls).toMatchSnapshot(); 164 | 165 | expect(cb1.mock.calls[0][0]).toEqual(context.stats); 166 | expect(cb2.mock.calls[0][0]).toEqual(context.stats); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /src/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "mimeTypes": { 5 | "description": "Allows a user to register custom mime types or extension mappings.", 6 | "link": "https://github.com/webpack/webpack-dev-middleware#mimetypes", 7 | "type": "object" 8 | }, 9 | "mimeTypeDefault": { 10 | "description": "Allows a user to register a default mime type when we can't determine the content type.", 11 | "link": "https://github.com/webpack/webpack-dev-middleware#mimetypedefault", 12 | "type": "string" 13 | }, 14 | "writeToDisk": { 15 | "description": "Allows to write generated files on disk.", 16 | "link": "https://github.com/webpack/webpack-dev-middleware#writetodisk", 17 | "anyOf": [ 18 | { 19 | "type": "boolean" 20 | }, 21 | { 22 | "instanceof": "Function" 23 | } 24 | ] 25 | }, 26 | "methods": { 27 | "description": "Allows to pass the list of HTTP request methods accepted by the middleware.", 28 | "link": "https://github.com/webpack/webpack-dev-middleware#methods", 29 | "type": "array", 30 | "items": { 31 | "type": "string", 32 | "minLength": 1 33 | } 34 | }, 35 | "headers": { 36 | "anyOf": [ 37 | { 38 | "type": "array", 39 | "items": { 40 | "type": "object", 41 | "additionalProperties": false, 42 | "properties": { 43 | "key": { 44 | "description": "key of header.", 45 | "type": "string" 46 | }, 47 | "value": { 48 | "description": "value of header.", 49 | "type": "string" 50 | } 51 | } 52 | }, 53 | "minItems": 1 54 | }, 55 | { 56 | "type": "object" 57 | }, 58 | { 59 | "instanceof": "Function" 60 | } 61 | ], 62 | "description": "Allows to pass custom HTTP headers on each request", 63 | "link": "https://github.com/webpack/webpack-dev-middleware#headers" 64 | }, 65 | "publicPath": { 66 | "description": "The `publicPath` specifies the public URL address of the output files when referenced in a browser.", 67 | "link": "https://github.com/webpack/webpack-dev-middleware#publicpath", 68 | "anyOf": [ 69 | { 70 | "enum": ["auto"] 71 | }, 72 | { 73 | "type": "string" 74 | }, 75 | { 76 | "instanceof": "Function" 77 | } 78 | ] 79 | }, 80 | "stats": { 81 | "description": "Stats options object or preset name.", 82 | "link": "https://github.com/webpack/webpack-dev-middleware#stats", 83 | "anyOf": [ 84 | { 85 | "enum": [ 86 | "none", 87 | "summary", 88 | "errors-only", 89 | "errors-warnings", 90 | "minimal", 91 | "normal", 92 | "detailed", 93 | "verbose" 94 | ] 95 | }, 96 | { 97 | "type": "boolean" 98 | }, 99 | { 100 | "type": "object", 101 | "additionalProperties": true 102 | } 103 | ] 104 | }, 105 | "serverSideRender": { 106 | "description": "Instructs the module to enable or disable the server-side rendering mode.", 107 | "link": "https://github.com/webpack/webpack-dev-middleware#serversiderender", 108 | "type": "boolean" 109 | }, 110 | "outputFileSystem": { 111 | "description": "Set the default file system which will be used by webpack as primary destination of generated files.", 112 | "link": "https://github.com/webpack/webpack-dev-middleware#outputfilesystem", 113 | "type": "object" 114 | }, 115 | "index": { 116 | "description": "Allows to serve an index of the directory.", 117 | "link": "https://github.com/webpack/webpack-dev-middleware#index", 118 | "anyOf": [ 119 | { 120 | "type": "boolean" 121 | }, 122 | { 123 | "type": "string", 124 | "minLength": 1 125 | } 126 | ] 127 | }, 128 | "modifyResponseData": { 129 | "description": "Allows to set up a callback to change the response data.", 130 | "link": "https://github.com/webpack/webpack-dev-middleware#modifyresponsedata", 131 | "instanceof": "Function" 132 | }, 133 | "etag": { 134 | "description": "Enable or disable etag generation.", 135 | "link": "https://github.com/webpack/webpack-dev-middleware#etag", 136 | "enum": ["weak", "strong"] 137 | }, 138 | "lastModified": { 139 | "description": "Enable or disable `Last-Modified` header. Uses the file system's last modified value.", 140 | "link": "https://github.com/webpack/webpack-dev-middleware#lastmodified", 141 | "type": "boolean" 142 | }, 143 | "cacheControl": { 144 | "description": "Enable or disable setting `Cache-Control` response header.", 145 | "link": "https://github.com/webpack/webpack-dev-middleware#cachecontrol", 146 | "anyOf": [ 147 | { 148 | "type": "boolean" 149 | }, 150 | { 151 | "type": "number" 152 | }, 153 | { 154 | "type": "string", 155 | "minLength": 1 156 | }, 157 | { 158 | "type": "object", 159 | "properties": { 160 | "maxAge": { 161 | "type": "number" 162 | }, 163 | "immutable": { 164 | "type": "boolean" 165 | } 166 | }, 167 | "additionalProperties": false 168 | } 169 | ] 170 | }, 171 | "cacheImmutable": { 172 | "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`).", 173 | "link": "https://github.com/webpack/webpack-dev-middleware#cacheimmutable", 174 | "type": "boolean" 175 | } 176 | }, 177 | "additionalProperties": false 178 | } 179 | -------------------------------------------------------------------------------- /src/utils/setupHooks.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("webpack").Configuration} Configuration */ 2 | /** @typedef {import("webpack").Compiler} Compiler */ 3 | /** @typedef {import("webpack").MultiCompiler} MultiCompiler */ 4 | /** @typedef {import("webpack").Stats} Stats */ 5 | /** @typedef {import("webpack").MultiStats} MultiStats */ 6 | /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ 7 | /** @typedef {import("../index.js").ServerResponse} ServerResponse */ 8 | 9 | /** @typedef {Configuration["stats"]} StatsOptions */ 10 | /** @typedef {{ children: Configuration["stats"][] }} MultiStatsOptions */ 11 | /** @typedef {Exclude} StatsObjectOptions */ 12 | 13 | /** 14 | * @template {IncomingMessage} Request 15 | * @template {ServerResponse} Response 16 | * @param {import("../index.js").WithOptional, "watching" | "outputFileSystem">} context context 17 | */ 18 | function setupHooks(context) { 19 | /** 20 | * @returns {void} 21 | */ 22 | function invalid() { 23 | if (context.state) { 24 | context.logger.log("Compilation starting..."); 25 | } 26 | 27 | // We are now in invalid state 28 | 29 | context.state = false; 30 | 31 | context.stats = undefined; 32 | } 33 | 34 | /** 35 | * @param {StatsOptions} statsOptions stats options 36 | * @returns {StatsObjectOptions} object stats options 37 | */ 38 | function normalizeStatsOptions(statsOptions) { 39 | if (typeof statsOptions === "undefined") { 40 | statsOptions = { preset: "normal" }; 41 | } else if (typeof statsOptions === "boolean") { 42 | statsOptions = statsOptions ? { preset: "normal" } : { preset: "none" }; 43 | } else if (typeof statsOptions === "string") { 44 | statsOptions = { preset: statsOptions }; 45 | } 46 | 47 | return statsOptions; 48 | } 49 | 50 | /** 51 | * @param {Stats | MultiStats} stats stats 52 | */ 53 | function done(stats) { 54 | // We are now on valid state 55 | 56 | context.state = true; 57 | 58 | context.stats = stats; 59 | 60 | // Do the stuff in nextTick, because bundle may be invalidated if a change happened while compiling 61 | process.nextTick(() => { 62 | const { compiler, logger, options, state, callbacks } = context; 63 | 64 | // Check if still in valid state 65 | if (!state) { 66 | return; 67 | } 68 | 69 | logger.log("Compilation finished"); 70 | 71 | const isMultiCompilerMode = Boolean( 72 | /** @type {MultiCompiler} */ 73 | (compiler).compilers, 74 | ); 75 | 76 | /** 77 | * @type {StatsOptions | MultiStatsOptions | undefined} 78 | */ 79 | let statsOptions; 80 | 81 | if (typeof options.stats !== "undefined") { 82 | statsOptions = isMultiCompilerMode 83 | ? { 84 | children: 85 | /** @type {MultiCompiler} */ 86 | (compiler).compilers.map(() => options.stats), 87 | } 88 | : options.stats; 89 | } else { 90 | statsOptions = isMultiCompilerMode 91 | ? { 92 | children: 93 | /** @type {MultiCompiler} */ 94 | (compiler).compilers.map((child) => child.options.stats), 95 | } 96 | : /** @type {Compiler} */ (compiler).options.stats; 97 | } 98 | 99 | if (isMultiCompilerMode) { 100 | /** @type {MultiStatsOptions} */ 101 | (statsOptions).children = 102 | /** @type {MultiStatsOptions} */ 103 | (statsOptions).children.map( 104 | /** 105 | * @param {StatsOptions} childStatsOptions child stats options 106 | * @returns {StatsObjectOptions} object child stats options 107 | */ 108 | (childStatsOptions) => { 109 | childStatsOptions = normalizeStatsOptions(childStatsOptions); 110 | 111 | if (typeof childStatsOptions.colors === "undefined") { 112 | const [firstCompiler] = 113 | /** @type {MultiCompiler} */ 114 | (compiler).compilers; 115 | 116 | // TODO remove `colorette` and set minimum supported webpack version is `5.101.0` 117 | childStatsOptions.colors = 118 | typeof firstCompiler.webpack !== "undefined" && 119 | typeof firstCompiler.webpack.cli !== "undefined" && 120 | typeof firstCompiler.webpack.cli.isColorSupported === 121 | "function" 122 | ? firstCompiler.webpack.cli.isColorSupported() 123 | : require("colorette").isColorSupported; 124 | } 125 | 126 | return childStatsOptions; 127 | }, 128 | ); 129 | } else { 130 | statsOptions = normalizeStatsOptions( 131 | /** @type {StatsOptions} */ (statsOptions), 132 | ); 133 | 134 | if (typeof statsOptions.colors === "undefined") { 135 | const { compiler } = /** @type {{ compiler: Compiler }} */ (context); 136 | // TODO remove `colorette` and set minimum supported webpack version is `5.101.0` 137 | statsOptions.colors = 138 | typeof compiler.webpack !== "undefined" && 139 | typeof compiler.webpack.cli !== "undefined" && 140 | typeof compiler.webpack.cli.isColorSupported === "function" 141 | ? compiler.webpack.cli.isColorSupported() 142 | : require("colorette").isColorSupported; 143 | } 144 | } 145 | 146 | const printedStats = stats.toString( 147 | /** @type {StatsObjectOptions} */ 148 | (statsOptions), 149 | ); 150 | 151 | // Avoid extra empty line when `stats: 'none'` 152 | if (printedStats) { 153 | // eslint-disable-next-line no-console 154 | console.log(printedStats); 155 | } 156 | 157 | context.callbacks = []; 158 | 159 | // Execute callback that are delayed 160 | for (const callback of callbacks) { 161 | callback(stats); 162 | } 163 | }); 164 | } 165 | 166 | // eslint-disable-next-line prefer-destructuring 167 | const compiler = 168 | /** @type {import("../index.js").Context} */ 169 | (context).compiler; 170 | 171 | compiler.hooks.watchRun.tap("webpack-dev-middleware", invalid); 172 | compiler.hooks.invalid.tap("webpack-dev-middleware", invalid); 173 | compiler.hooks.done.tap("webpack-dev-middleware", done); 174 | } 175 | 176 | module.exports = setupHooks; 177 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing in webpack-dev-middleware 2 | 3 | We'd always love contributions to further improve the webpack / webpack-contrib ecosystem! 4 | Here are the guidelines we'd like you to follow: 5 | 6 | - [Questions and Problems](#question) 7 | - [Issues and Bugs](#issue) 8 | - [Feature Requests](#feature) 9 | - [Pull Request Submission Guidelines](#submit-pr) 10 | - [Commit Message Conventions](#commit) 11 | 12 | ## Got a Question or Problem? 13 | 14 | Please submit support requests and questions to StackOverflow using the tag [[webpack]](http://stackoverflow.com/tags/webpack). 15 | StackOverflow is better suited for this kind of support though you may also inquire in [Webpack discussions](https://github.com/webpack/webpack/discussions). 16 | The issue tracker is for bug reports and feature discussions. 17 | 18 | ## Found an Issue or Bug? 19 | 20 | Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available. 21 | 22 | We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs, we ask that you to provide a minimal reproduction scenario (github repo or failing test case). Having a live, reproducible scenario gives us a wealth of important information without going back & forth to you with additional questions like: 23 | 24 | - version of Webpack used 25 | - version of the loader / plugin you are creating a bug report for 26 | - the use-case that fails 27 | 28 | A minimal reproduce scenario allows us to quickly confirm a bug (or point out config problems) as well as confirm that we are fixing the right problem. 29 | 30 | We will be insisting on a minimal reproduce scenario in order to save maintainers time and ultimately be able to fix more bugs. We understand that sometimes it might be hard to extract essentials bits of code from a larger code-base but we really need to isolate the problem before we can fix it. 31 | 32 | Unfortunately, we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that doesn't have enough info to be reproduced. 33 | 34 | ## Feature Requests? 35 | 36 | You can _request_ a new feature by creating an issue on Github. 37 | 38 | If you would like to _implement_ a new feature, please submit an issue with a proposal for your work `first`, to be sure that particular makes sense for the project. 39 | 40 | ## Pull Request Submission Guidelines 41 | 42 | Before you submit your Pull Request (PR) consider the following guidelines: 43 | 44 | - Search Github for an open or closed PR that relates to your submission. You don't want to duplicate effort. 45 | - Commit your changes using a descriptive commit message that follows our [commit message conventions](#commit). Adherence to these conventions is necessary because release notes are automatically generated from these messages. 46 | - Fill out our `Pull Request Template`. Your pull request will not be considered if it is ignored. 47 | - Please sign the `Contributor License Agreement (CLA)` when a pull request is opened. We cannot accept your pull request without this. Make sure you sign with the primary email address associated with your local / github account. 48 | 49 | ### Webpack Contrib Commit Conventions 50 | 51 | Each commit message consists of a **header**, a **body** and a **footer**. The header has a special 52 | format that includes a **type**, a **scope** and a **subject**: 53 | 54 | ``` 55 | (): 56 | 57 | 58 | 59 |