From d2847872d3fe97b5c8cc9de3f02c2a48491c6b26 Mon Sep 17 00:00:00 2001 From: palirichtarik Date: Thu, 21 Mar 2024 13:26:30 +0100 Subject: [PATCH 01/16] docs: create SECURITY.md (#1787) --- SECURITY.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..0551ac700 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +# Reporting Security Issues + +If you discover a security issue in webpack, please report it by sending an +email to [webpack@opencollective.com](mailto:webpack@opencollective.com). + +This will allow us to assess the risk, and make a fix available before we add a +bug report to the GitHub repository. + +Thanks for helping make webpack safe for everyone. From 880c4ed1ad48d3d5b027b268fb941f73692d13db Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Mon, 25 Mar 2024 17:07:39 +0530 Subject: [PATCH 02/16] ci: update codecov action to v4 (#1788) --- .github/workflows/nodejs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index ff32afc77..eb67e3ce2 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -91,6 +91,6 @@ jobs: run: npm run test:coverage -- --ci - name: Submit coverage data to codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} From 4adea03daba3267dd484d28a02104c796008f2b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Mar 2024 13:02:21 +0300 Subject: [PATCH 03/16] chore(deps-dev): bump express from 4.19.1 to 4.19.2 (#1790) Bumps [express](https://github.com/expressjs/express) from 4.19.1 to 4.19.2. - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/4.19.1...4.19.2) --- updated-dependencies: - dependency-name: express dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 532b58f1c..c871dca7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9071,9 +9071,9 @@ } }, "node_modules/express": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.1.tgz", - "integrity": "sha512-K4w1/Bp7y8iSiVObmCrtq8Cs79XjJc/RU2YYkZQ7wpUu5ZyZ7MtPHkqoMz4pf+mgXfNvo2qft8D9OnrH2ABk9w==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dev": true, "dependencies": { "accepts": "~1.3.8", From 6d6e15863976584c20bd2ea53dc8ce4ed79796a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Mar 2024 13:03:07 +0300 Subject: [PATCH 04/16] chore(deps-dev): bump cspell from 8.6.0 to 8.6.1 (#1789) Bumps [cspell](https://github.com/streetsidesoftware/cspell/tree/HEAD/packages/cspell) from 8.6.0 to 8.6.1. - [Release notes](https://github.com/streetsidesoftware/cspell/releases) - [Changelog](https://github.com/streetsidesoftware/cspell/blob/main/packages/cspell/CHANGELOG.md) - [Commits](https://github.com/streetsidesoftware/cspell/commits/v8.6.1/packages/cspell) --- updated-dependencies: - dependency-name: cspell dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 180 +++++++++++++++++++++++++--------------------- 1 file changed, 97 insertions(+), 83 deletions(-) diff --git a/package-lock.json b/package-lock.json index c871dca7b..e8b0b4847 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2535,9 +2535,9 @@ } }, "node_modules/@cspell/cspell-bundled-dicts": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-8.6.0.tgz", - "integrity": "sha512-hRVvir4G4276Kz/Cru34AJg1FObIw5MrzezAwHkD3obNMwZkof8aX3MEN6AzWusJSVG2ZxZxZAEnYbgqvGr2Fg==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-8.6.1.tgz", + "integrity": "sha512-s6Av1xIgctYLuUiazKZjQ2WRUXc9dU38BOZXwM/lb7y8grQMEuTjST1c+8MOkZkppx48/sO7GHIF3k9rEzD3fg==", "dev": true, "dependencies": { "@cspell/dict-ada": "^4.0.2", @@ -2567,6 +2567,7 @@ "@cspell/dict-html": "^4.0.5", "@cspell/dict-html-symbol-entities": "^4.0.0", "@cspell/dict-java": "^5.0.6", + "@cspell/dict-julia": "^1.0.1", "@cspell/dict-k8s": "^1.0.2", "@cspell/dict-latex": "^4.0.0", "@cspell/dict-lorem-ipsum": "^4.0.0", @@ -2586,6 +2587,7 @@ "@cspell/dict-sql": "^2.1.3", "@cspell/dict-svelte": "^1.0.2", "@cspell/dict-swift": "^2.0.1", + "@cspell/dict-terraform": "^1.0.0", "@cspell/dict-typescript": "^3.1.2", "@cspell/dict-vue": "^3.0.0" }, @@ -2594,30 +2596,30 @@ } }, "node_modules/@cspell/cspell-json-reporter": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-8.6.0.tgz", - "integrity": "sha512-fPpE4a3zpdfwgTyfLgCmxZn4owkZ4IP6A/oL4XLW22IxW5xBIbXEveOSY+uiWAnVfEnqfrMNRLAGj7JoXnJ1Vg==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-8.6.1.tgz", + "integrity": "sha512-75cmJgU9iQgrDnLFIUyvgybySJJi29BPw71z+8ZO9WhNofufxoSjaWepZeYV2nK0nHXM+MbdQG5Mmj/Lv6J1FA==", "dev": true, "dependencies": { - "@cspell/cspell-types": "8.6.0" + "@cspell/cspell-types": "8.6.1" }, "engines": { "node": ">=18" } }, "node_modules/@cspell/cspell-pipe": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-8.6.0.tgz", - "integrity": "sha512-gbAZksz38OHaN8s4fOmmgtgQfie1K8dRGlo9z/uxSx5FIELV48GWTbHn9t1TY2yBXBwJ7+4NF2+r624rtlPoHQ==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-8.6.1.tgz", + "integrity": "sha512-guIlGhhOLQwfqevBSgp26b+SX4I1hCH+puAksWAk93bybKkcGtGpcavAQSN9qvamox4zcHnvGutEPF+UcXuceQ==", "dev": true, "engines": { "node": ">=18" } }, "node_modules/@cspell/cspell-resolver": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@cspell/cspell-resolver/-/cspell-resolver-8.6.0.tgz", - "integrity": "sha512-ARwO6TWKy8fLHNhC/ls5Wo/AK86E1oLVChwWtHdq7eVyEUIykQaXGLqoRThkIT2jyLfGDrhSvaU+yqcXVLE48Q==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/@cspell/cspell-resolver/-/cspell-resolver-8.6.1.tgz", + "integrity": "sha512-ZUbYcvEhfokHG9qfUlIylUqEobG84PiDozCkE8U4h/rTSmYkf/nAD+M6yg+jQ0F2aTFGNbvpKKGFlfXFXveX7A==", "dev": true, "dependencies": { "global-directory": "^4.0.1" @@ -2627,18 +2629,18 @@ } }, "node_modules/@cspell/cspell-service-bus": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@cspell/cspell-service-bus/-/cspell-service-bus-8.6.0.tgz", - "integrity": "sha512-veCGlhlNGmYMgzX/rMiDp8j7ndLxFHIZq3h6DNlIsIoSjP1v5Rk6UcCwEoWYexwKmNXo7c2VooB0GM9LSBcPAQ==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/@cspell/cspell-service-bus/-/cspell-service-bus-8.6.1.tgz", + "integrity": "sha512-WpI3fSW8t00UMetfd6tS8f9+xE3+ElIUO/bQ1YKK95TMIRdEUcH+QDxcHM66pJXEm4WiaN3H/MfWk1fIhGlJ8g==", "dev": true, "engines": { "node": ">=18" } }, "node_modules/@cspell/cspell-types": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-8.6.0.tgz", - "integrity": "sha512-+CU/nuFOpswJAA3IS2TcKGskfM/o/4aNG1IMUVaOEQi1Sc5qZQ4Wj1qDIWJArSHFYW1Q4XFa4U8K1jnVHkAhZQ==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-8.6.1.tgz", + "integrity": "sha512-MXa9v6sXbbwyiNno7v7vczNph6AsMNWnpMRCcW3h/siXNQYRuMssdxqT5sQJ8Kurh3M/Wo7DlKX4n74elKL3iQ==", "dev": true, "engines": { "node": ">=18" @@ -2812,6 +2814,12 @@ "integrity": "sha512-kdE4AHHHrixyZ5p6zyms1SLoYpaJarPxrz8Tveo6gddszBVVwIUZ+JkQE1bWNLK740GWzIXdkznpUfw1hP9nXw==", "dev": true }, + "node_modules/@cspell/dict-julia": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@cspell/dict-julia/-/dict-julia-1.0.1.tgz", + "integrity": "sha512-4JsCLCRhhLMLiaHpmR7zHFjj1qOauzDI5ZzCNQS31TUMfsOo26jAKDfo0jljFAKgw5M2fEG7sKr8IlPpQAYrmQ==", + "dev": true + }, "node_modules/@cspell/dict-k8s": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@cspell/dict-k8s/-/dict-k8s-1.0.2.tgz", @@ -2929,6 +2937,12 @@ "integrity": "sha512-gxrCMUOndOk7xZFmXNtkCEeroZRnS2VbeaIPiymGRHj5H+qfTAzAKxtv7jJbVA3YYvEzWcVE2oKDP4wcbhIERw==", "dev": true }, + "node_modules/@cspell/dict-terraform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@cspell/dict-terraform/-/dict-terraform-1.0.0.tgz", + "integrity": "sha512-Ak+vy4HP/bOgzf06BAMC30+ZvL9mzv21xLM2XtfnBLTDJGdxlk/nK0U6QT8VfFLqJ0ZZSpyOxGsUebWDCTr/zQ==", + "dev": true + }, "node_modules/@cspell/dict-typescript": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@cspell/dict-typescript/-/dict-typescript-3.1.2.tgz", @@ -2942,9 +2956,9 @@ "dev": true }, "node_modules/@cspell/dynamic-import": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@cspell/dynamic-import/-/dynamic-import-8.6.0.tgz", - "integrity": "sha512-yDJZ/uXCpZcAkXwaWa0JcCZHZFxnF3qtiFiq2WG5cEw8tiJiNdawjSCd8/D35dT3QFNaInMP+H3sOf68dNueew==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/@cspell/dynamic-import/-/dynamic-import-8.6.1.tgz", + "integrity": "sha512-Fjvkcb5umIAcHfw/iiciYWgO2mXVuRZzQAWPSub6UFCxxcJlRz39YPXa+3O/m3lnXCeo8ChoaEN8qnuV4ogk6g==", "dev": true, "dependencies": { "import-meta-resolve": "^4.0.0" @@ -2954,9 +2968,9 @@ } }, "node_modules/@cspell/strong-weak-map": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@cspell/strong-weak-map/-/strong-weak-map-8.6.0.tgz", - "integrity": "sha512-QenBOdIT1zRa0kF3Z1mwObcvmdhxn+rzQDdmkxwSyRB/9KsNnib6XXTUo8P+Z/ZKXOYbP9Wmf4FX+vKd3yVX0Q==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/@cspell/strong-weak-map/-/strong-weak-map-8.6.1.tgz", + "integrity": "sha512-X6/7cy+GGVJFXsfrZapxVKn5mtehNTr7hTlg0bVj3iFoNYEPW9zq9l6WIcI4psmaU8G4DSrNsBK7pp87W3u16A==", "dev": true, "engines": { "node": ">=18" @@ -7149,22 +7163,22 @@ } }, "node_modules/cspell": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/cspell/-/cspell-8.6.0.tgz", - "integrity": "sha512-aAaVD3v1105OQePCpcdYkHnHxxkxKxxQzFcfJ4tKsH06dlW04Sp1oQLlsjgWDa3y6cdYTpSYj1eSenavBvfOFg==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/cspell/-/cspell-8.6.1.tgz", + "integrity": "sha512-/Qle15v4IQe7tViSWX0+RCZJ2HJ4HUCZV9Z4uOVasNUz+DWCrxysNR+pfCRYuLX/6lQdqCM9QCR9GZc7a2KIVA==", "dev": true, "dependencies": { - "@cspell/cspell-json-reporter": "8.6.0", - "@cspell/cspell-pipe": "8.6.0", - "@cspell/cspell-types": "8.6.0", - "@cspell/dynamic-import": "8.6.0", + "@cspell/cspell-json-reporter": "8.6.1", + "@cspell/cspell-pipe": "8.6.1", + "@cspell/cspell-types": "8.6.1", + "@cspell/dynamic-import": "8.6.1", "chalk": "^5.3.0", "chalk-template": "^1.1.0", "commander": "^12.0.0", - "cspell-gitignore": "8.6.0", - "cspell-glob": "8.6.0", - "cspell-io": "8.6.0", - "cspell-lib": "8.6.0", + "cspell-gitignore": "8.6.1", + "cspell-glob": "8.6.1", + "cspell-io": "8.6.1", + "cspell-lib": "8.6.1", "fast-glob": "^3.3.2", "fast-json-stable-stringify": "^2.1.0", "file-entry-cache": "^8.0.0", @@ -7185,28 +7199,28 @@ } }, "node_modules/cspell-config-lib": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/cspell-config-lib/-/cspell-config-lib-8.6.0.tgz", - "integrity": "sha512-Q1rvQFUDJTu4hUtxwL6+q83Hjx/a5grEjMS5axxFJzjJuFRbRsXCagncdSCx/YBqLkNM5noBbRP/0rVh7ufqxw==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/cspell-config-lib/-/cspell-config-lib-8.6.1.tgz", + "integrity": "sha512-I6LatgXJb8mxKFzIywO81TlUD/qWnUDrhB6yTUPdP90bwZcXMmGoCsZxhd2Rvl9fz5fWne0T839I1coShfm86g==", "dev": true, "dependencies": { - "@cspell/cspell-types": "8.6.0", + "@cspell/cspell-types": "8.6.1", "comment-json": "^4.2.3", - "yaml": "^2.4.0" + "yaml": "^2.4.1" }, "engines": { "node": ">=18" } }, "node_modules/cspell-dictionary": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/cspell-dictionary/-/cspell-dictionary-8.6.0.tgz", - "integrity": "sha512-ohToeOQznIrb2/z7RfKxX3NID0WiO4sXK3IxKdnbn2viGgdn17tQ8Z2f4Xuy9egjSGRKyr6N25Z5AOes1C8R3w==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/cspell-dictionary/-/cspell-dictionary-8.6.1.tgz", + "integrity": "sha512-0SfKPi1QoWbGpZ/rWMR7Jn0+GaQT9PAMLWjVOu66PUNUXI5f4oCTHpnZE1Xts+5VX8shZC3TAMHEgtgKuQn4RQ==", "dev": true, "dependencies": { - "@cspell/cspell-pipe": "8.6.0", - "@cspell/cspell-types": "8.6.0", - "cspell-trie-lib": "8.6.0", + "@cspell/cspell-pipe": "8.6.1", + "@cspell/cspell-types": "8.6.1", + "cspell-trie-lib": "8.6.1", "fast-equals": "^5.0.1", "gensequence": "^7.0.0" }, @@ -7215,12 +7229,12 @@ } }, "node_modules/cspell-gitignore": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-8.6.0.tgz", - "integrity": "sha512-6INRlNb17iKtQH7NmDM/EsX5OZOD2TzIwHiJnnWci0Y5l10V/zN9WGLDegTjMh9HU3TS6uUuN4I/ffkCs9m+LA==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-8.6.1.tgz", + "integrity": "sha512-3gtt351sSDfN826aMXTqGHVLz2lz9ZHr8uemImUc24Q+676sXkJM9lXzqP8PUqwGhLyt5qSf+9pt0ieNwQy/cA==", "dev": true, "dependencies": { - "cspell-glob": "8.6.0", + "cspell-glob": "8.6.1", "find-up-simple": "^1.0.0" }, "bin": { @@ -7231,9 +7245,9 @@ } }, "node_modules/cspell-glob": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-8.6.0.tgz", - "integrity": "sha512-AyuExc34F8JsEYNl4inx1m1v5VoSRA/cTptREq/AoNTcMTyG5s+wt5J+VWBfvJjEDEEpd9Cb2it0j8TMo/Tpjw==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-8.6.1.tgz", + "integrity": "sha512-QjtngIR0XsUQLmHHDO86hps/JR5sRxSBwCvcsNCEmSdpdofLFc8cuxi3o33JWge7UAPBCQOLGfpA7/Wx31srmw==", "dev": true, "dependencies": { "micromatch": "^4.0.5" @@ -7243,13 +7257,13 @@ } }, "node_modules/cspell-grammar": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-8.6.0.tgz", - "integrity": "sha512-wVpZ4pPOqRoOmzLUc34wyOQnBi/6RsV3Y1KiPn8BNSkObb9XSohb1xJJMJ69unEmgE0snQDMHIeUaLTQH414MA==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-8.6.1.tgz", + "integrity": "sha512-MaG0e/F0b2FnIRULCZ61JxEiJgTP/6rsbUoR5nG9X+WmJYItYmxC1F/FPPrVeTu+jJr/8O4pdnslE20pimHaCw==", "dev": true, "dependencies": { - "@cspell/cspell-pipe": "8.6.0", - "@cspell/cspell-types": "8.6.0" + "@cspell/cspell-pipe": "8.6.1", + "@cspell/cspell-types": "8.6.1" }, "bin": { "cspell-grammar": "bin.mjs" @@ -7259,38 +7273,38 @@ } }, "node_modules/cspell-io": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-8.6.0.tgz", - "integrity": "sha512-jx7ccRpcshqxN6xnOiGnX4VycaqTpmatRjHITn4vLoDmQNfxQeU69YT62bhyjogCBuJsZS9ksjo7GQIsrYBekA==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-8.6.1.tgz", + "integrity": "sha512-ofxBB8QtUPvh/bOwKLYsqU1hwQCet8E98jkn/5f4jtG+/x5Zd80I0Ez+tlbjiBmrrQfOKh+i8ipfzHD8JtoreQ==", "dev": true, "dependencies": { - "@cspell/cspell-service-bus": "8.6.0" + "@cspell/cspell-service-bus": "8.6.1" }, "engines": { "node": ">=18" } }, "node_modules/cspell-lib": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-8.6.0.tgz", - "integrity": "sha512-l1bBxBz8noPOxEIIu1Ahvd4e/j6Re1PNDD9FwZgaRmvMyIPZbupTxzCM0MZWvYz1VymBmrrVEKRwtZ34VocaCw==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-8.6.1.tgz", + "integrity": "sha512-kGeDUypRtThFT81IdUK7yU8eUwO5MYWj8pGQ0N8WFsqbCahJrUdcocceVSpnCX48W3CXu12DkqYG9kv5Umn7Xw==", "dev": true, "dependencies": { - "@cspell/cspell-bundled-dicts": "8.6.0", - "@cspell/cspell-pipe": "8.6.0", - "@cspell/cspell-resolver": "8.6.0", - "@cspell/cspell-types": "8.6.0", - "@cspell/dynamic-import": "8.6.0", - "@cspell/strong-weak-map": "8.6.0", + "@cspell/cspell-bundled-dicts": "8.6.1", + "@cspell/cspell-pipe": "8.6.1", + "@cspell/cspell-resolver": "8.6.1", + "@cspell/cspell-types": "8.6.1", + "@cspell/dynamic-import": "8.6.1", + "@cspell/strong-weak-map": "8.6.1", "clear-module": "^4.1.2", "comment-json": "^4.2.3", "configstore": "^6.0.0", - "cspell-config-lib": "8.6.0", - "cspell-dictionary": "8.6.0", - "cspell-glob": "8.6.0", - "cspell-grammar": "8.6.0", - "cspell-io": "8.6.0", - "cspell-trie-lib": "8.6.0", + "cspell-config-lib": "8.6.1", + "cspell-dictionary": "8.6.1", + "cspell-glob": "8.6.1", + "cspell-grammar": "8.6.1", + "cspell-io": "8.6.1", + "cspell-trie-lib": "8.6.1", "fast-equals": "^5.0.1", "gensequence": "^7.0.0", "import-fresh": "^3.3.0", @@ -7303,13 +7317,13 @@ } }, "node_modules/cspell-trie-lib": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-8.6.0.tgz", - "integrity": "sha512-S8nGCnEJBL1maiKPd3FhI54QG+OgtOkcJ/yUDXGXGrokSruWFdNocioPirlFAHf959ax1GBUVEYNIgnu/EIWNg==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-8.6.1.tgz", + "integrity": "sha512-iuJuAyWoqTH/TpFAR/ISJGQQoW3oiw54GyvXIucPoCJt/jgQONDuzqPW+skiLvcgcTbXCN9dutZTb2gImIkmpw==", "dev": true, "dependencies": { - "@cspell/cspell-pipe": "8.6.0", - "@cspell/cspell-types": "8.6.0", + "@cspell/cspell-pipe": "8.6.1", + "@cspell/cspell-types": "8.6.1", "gensequence": "^7.0.0" }, "engines": { From b3f9126cfb659c95c0cd77d97eed168c7941c8a8 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Tue, 26 Mar 2024 23:07:59 +0300 Subject: [PATCH 05/16] feat: hapi support --- README.md | 83 +- package-lock.json | 373 +++++++++ package.json | 2 + src/index.js | 65 +- test/api.test.js | 93 +-- test/middleware.test.js | 1756 ++++++++++++++++++++++----------------- types/index.d.ts | 47 +- 7 files changed, 1587 insertions(+), 832 deletions(-) diff --git a/README.md b/README.md index bc102ccc2..5a005d1de 100644 --- a/README.md +++ b/README.md @@ -540,6 +540,83 @@ out completely._ Examples of use with other servers will follow here. +### Connect + +```js +const connect = require("connect"); +const http = require("http"); +const webpack = require("webpack"); +const webpackConfig = require("./webpack.config.js"); +const devMiddleware = require("webpack-dev-middleware"); + +const compiler = webpack(webpackConfig); +const devMiddlewareOptions = { + /** Your webpack-dev-middleware-options */ +}; +const app = connect(); + +app.use(devMiddleware(compiler, devMiddlewareOptions)); + +http.createServer(app).listen(3000); +``` + +### Express + +```js +const express = require("express"); +const webpack = require("webpack"); +const webpackConfig = require("./webpack.config.js"); +const devMiddleware = require("webpack-dev-middleware"); + +const compiler = webpack(webpackConfig); +const devMiddlewareOptions = { + /** Your webpack-dev-middleware-options */ +}; +const app = express(); + +app.use(devMiddleware(compiler, devMiddlewareOptions)); + +app.listen(3000, () => console.log("Example app listening on port 3000!")); +``` + +### Hapi + +```js +const Hapi = require("@hapi/hapi"); +const webpack = require("webpack"); +const webpackConfig = require("./webpack.config.js"); +const devMiddleware = require("webpack-dev-middleware"); + +const compiler = webpack(webpackConfig); +const devMiddlewareOptions = {}; + +(async () => { + const server = Hapi.server({ port: 3000, host: "localhost" }); + + await server.register({ + plugin: devMiddleware.hapiPlugin(), + options: { + // The `compiler` option is required + compiler, + ...devMiddlewareOptions, + }, + }); + + await server.start(); + + console.log("Server running on %s", server.info.uri); +})(); + +process.on("unhandledRejection", (err) => { + console.log(err); + process.exit(1); +}); +``` + +### Koa + +Soon... + ### Fastify Fastify interop will require the use of `fastify-express` instead of `middie` for providing middleware support. As the authors of `fastify-express` recommend, this should only be used as a stopgap while full Fastify support is worked on. @@ -551,11 +628,13 @@ const webpackConfig = require("./webpack.config.js"); const devMiddleware = require("webpack-dev-middleware"); const compiler = webpack(webpackConfig); -const { publicPath } = webpackConfig.output; +const devMiddlewareOptions = { + /** Your webpack-dev-middleware-options */ +}; (async () => { await fastify.register(require("fastify-express")); - await fastify.use(devMiddleware(compiler, { publicPath })); + await fastify.use(devMiddleware(compiler, devMiddlewareOptions)); await fastify.listen(3000); })(); ``` diff --git a/package-lock.json b/package-lock.json index e8b0b4847..21597b884 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@babel/preset-env": "^7.16.7", "@commitlint/cli": "^19.0.3", "@commitlint/config-conventional": "^19.0.3", + "@hapi/hapi": "^21.3.7", "@types/connect": "^3.4.35", "@types/express": "^4.17.13", "@types/mime-types": "^2.1.1", @@ -44,6 +45,7 @@ "file-loader": "^6.2.0", "husky": "^9.0.10", "jest": "^29.3.1", + "joi": "^17.12.2", "lint-staged": "^15.2.0", "npm-run-all": "^4.1.5", "prettier": "^3.2.4", @@ -3099,6 +3101,322 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hapi/accept": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@hapi/accept/-/accept-6.0.3.tgz", + "integrity": "sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw==", + "dev": true, + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/ammo": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/ammo/-/ammo-6.0.1.tgz", + "integrity": "sha512-pmL+nPod4g58kXrMcsGLp05O2jF4P2Q3GiL8qYV7nKYEh3cGf+rV4P5Jyi2Uq0agGhVU63GtaSAfBEZOlrJn9w==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/b64": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/b64/-/b64-6.0.1.tgz", + "integrity": "sha512-ZvjX4JQReUmBheeCq+S9YavcnMMHWqx3S0jHNXWIM1kQDxB9cyfSycpVvjfrKcIS8Mh5N3hmu/YKo4Iag9g2Kw==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/boom": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-10.0.1.tgz", + "integrity": "sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/bounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@hapi/bounce/-/bounce-3.0.1.tgz", + "integrity": "sha512-G+/Pp9c1Ha4FDP+3Sy/Xwg2O4Ahaw3lIZFSX+BL4uWi64CmiETuZPxhKDUD4xBMOUZbBlzvO8HjiK8ePnhBadA==", + "dev": true, + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/bourne": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", + "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==", + "dev": true + }, + "node_modules/@hapi/call": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@hapi/call/-/call-9.0.1.tgz", + "integrity": "sha512-uPojQRqEL1GRZR4xXPqcLMujQGaEpyVPRyBlD8Pp5rqgIwLhtveF9PkixiKru2THXvuN8mUrLeet5fqxKAAMGg==", + "dev": true, + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/catbox": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@hapi/catbox/-/catbox-12.1.1.tgz", + "integrity": "sha512-hDqYB1J+R0HtZg4iPH3LEnldoaBsar6bYp0EonBmNQ9t5CO+1CqgCul2ZtFveW1ReA5SQuze9GPSU7/aecERhw==", + "dev": true, + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/podium": "^5.0.0", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/catbox-memory": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/catbox-memory/-/catbox-memory-6.0.1.tgz", + "integrity": "sha512-sVb+/ZxbZIvaMtJfAbdyY+QJUQg9oKTwamXpEg/5xnfG5WbJLTjvEn4kIGKz9pN3ENNbIL/bIdctmHmqi/AdGA==", + "dev": true, + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/content": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@hapi/content/-/content-6.0.0.tgz", + "integrity": "sha512-CEhs7j+H0iQffKfe5Htdak5LBOz/Qc8TRh51cF+BFv0qnuph3Em4pjGVzJMkI2gfTDdlJKWJISGWS1rK34POGA==", + "dev": true, + "dependencies": { + "@hapi/boom": "^10.0.0" + } + }, + "node_modules/@hapi/cryptiles": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/cryptiles/-/cryptiles-6.0.1.tgz", + "integrity": "sha512-9GM9ECEHfR8lk5ASOKG4+4ZsEzFqLfhiryIJ2ISePVB92OHLp/yne4m+zn7z9dgvM98TLpiFebjDFQ0UHcqxXQ==", + "dev": true, + "dependencies": { + "@hapi/boom": "^10.0.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/file": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/file/-/file-3.0.0.tgz", + "integrity": "sha512-w+lKW+yRrLhJu620jT3y+5g2mHqnKfepreykvdOcl9/6up8GrQQn+l3FRTsjHTKbkbfQFkuksHpdv2EcpKcJ4Q==", + "dev": true + }, + "node_modules/@hapi/hapi": { + "version": "21.3.7", + "resolved": "https://registry.npmjs.org/@hapi/hapi/-/hapi-21.3.7.tgz", + "integrity": "sha512-33J0nreMfqkhY7wwRAZRy+9J+7J4QOH1JtICMjIUmxfaOYSJL/d8JJCtg57SX60944bhlCeu7isb7qyr2jT2oA==", + "dev": true, + "dependencies": { + "@hapi/accept": "^6.0.1", + "@hapi/ammo": "^6.0.1", + "@hapi/boom": "^10.0.1", + "@hapi/bounce": "^3.0.1", + "@hapi/call": "^9.0.1", + "@hapi/catbox": "^12.1.1", + "@hapi/catbox-memory": "^6.0.1", + "@hapi/heavy": "^8.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/mimos": "^7.0.1", + "@hapi/podium": "^5.0.1", + "@hapi/shot": "^6.0.1", + "@hapi/somever": "^4.1.1", + "@hapi/statehood": "^8.1.1", + "@hapi/subtext": "^8.1.0", + "@hapi/teamwork": "^6.0.0", + "@hapi/topo": "^6.0.1", + "@hapi/validate": "^2.0.1" + }, + "engines": { + "node": ">=14.15.0" + } + }, + "node_modules/@hapi/heavy": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@hapi/heavy/-/heavy-8.0.1.tgz", + "integrity": "sha512-gBD/NANosNCOp6RsYTsjo2vhr5eYA3BEuogk6cxY0QdhllkkTaJFYtTXv46xd6qhBVMbMMqcSdtqey+UQU3//w==", + "dev": true, + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/hoek": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.4.tgz", + "integrity": "sha512-PnsP5d4q7289pS2T2EgGz147BFJ2Jpb4yrEdkpz2IhgEUzos1S7HTl7ezWh1yfYzYlj89KzLdCRkqsP6SIryeQ==", + "dev": true + }, + "node_modules/@hapi/iron": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@hapi/iron/-/iron-7.0.1.tgz", + "integrity": "sha512-tEZnrOujKpS6jLKliyWBl3A9PaE+ppuL/+gkbyPPDb/l2KSKQyH4lhMkVb+sBhwN+qaxxlig01JRqB8dk/mPxQ==", + "dev": true, + "dependencies": { + "@hapi/b64": "^6.0.1", + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/cryptiles": "^6.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/mimos": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@hapi/mimos/-/mimos-7.0.1.tgz", + "integrity": "sha512-b79V+BrG0gJ9zcRx1VGcCI6r6GEzzZUgiGEJVoq5gwzuB2Ig9Cax8dUuBauQCFKvl2YWSWyOc8mZ8HDaJOtkew==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^11.0.2", + "mime-db": "^1.52.0" + } + }, + "node_modules/@hapi/nigel": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hapi/nigel/-/nigel-5.0.1.tgz", + "integrity": "sha512-uv3dtYuB4IsNaha+tigWmN8mQw/O9Qzl5U26Gm4ZcJVtDdB1AVJOwX3X5wOX+A07qzpEZnOMBAm8jjSqGsU6Nw==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/vise": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/pez": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@hapi/pez/-/pez-6.1.0.tgz", + "integrity": "sha512-+FE3sFPYuXCpuVeHQ/Qag1b45clR2o54QoonE/gKHv9gukxQ8oJJZPR7o3/ydDTK6racnCJXxOyT1T93FCJMIg==", + "dev": true, + "dependencies": { + "@hapi/b64": "^6.0.1", + "@hapi/boom": "^10.0.1", + "@hapi/content": "^6.0.0", + "@hapi/hoek": "^11.0.2", + "@hapi/nigel": "^5.0.1" + } + }, + "node_modules/@hapi/podium": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hapi/podium/-/podium-5.0.1.tgz", + "integrity": "sha512-eznFTw6rdBhAijXFIlBOMJJd+lXTvqbrBIS4Iu80r2KTVIo4g+7fLy4NKp/8+UnSt5Ox6mJtAlKBU/Sf5080TQ==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/teamwork": "^6.0.0", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/shot": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/shot/-/shot-6.0.1.tgz", + "integrity": "sha512-s5ynMKZXYoDd3dqPw5YTvOR/vjHvMTxc388+0qL0jZZP1+uwXuUD32o9DuuuLsmTlyXCWi02BJl1pBpwRuUrNA==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/somever": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@hapi/somever/-/somever-4.1.1.tgz", + "integrity": "sha512-lt3QQiDDOVRatS0ionFDNrDIv4eXz58IibQaZQDOg4DqqdNme8oa0iPWcE0+hkq/KTeBCPtEOjDOBKBKwDumVg==", + "dev": true, + "dependencies": { + "@hapi/bounce": "^3.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/statehood": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@hapi/statehood/-/statehood-8.1.1.tgz", + "integrity": "sha512-YbK7PSVUA59NArAW5Np0tKRoIZ5VNYUicOk7uJmWZF6XyH5gGL+k62w77SIJb0AoAJ0QdGQMCQ/WOGL1S3Ydow==", + "dev": true, + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/bounce": "^3.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/cryptiles": "^6.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/iron": "^7.0.1", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/subtext": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@hapi/subtext/-/subtext-8.1.0.tgz", + "integrity": "sha512-PyaN4oSMtqPjjVxLny1k0iYg4+fwGusIhaom9B2StinBclHs7v46mIW706Y+Wo21lcgulGyXbQrmT/w4dus6ww==", + "dev": true, + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/content": "^6.0.0", + "@hapi/file": "^3.0.0", + "@hapi/hoek": "^11.0.2", + "@hapi/pez": "^6.1.0", + "@hapi/wreck": "^18.0.1" + } + }, + "node_modules/@hapi/teamwork": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@hapi/teamwork/-/teamwork-6.0.0.tgz", + "integrity": "sha512-05HumSy3LWfXpmJ9cr6HzwhAavrHkJ1ZRCmNE2qJMihdM5YcWreWPfyN0yKT2ZjCM92au3ZkuodjBxOibxM67A==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/validate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", + "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/topo": "^6.0.1" + } + }, + "node_modules/@hapi/vise": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hapi/vise/-/vise-5.0.1.tgz", + "integrity": "sha512-XZYWzzRtINQLedPYlIkSkUr7m5Ddwlu99V9elh8CSygXstfv3UnWIXT0QD+wmR0VAG34d2Vx3olqcEhRRoTu9A==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/wreck": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-18.0.1.tgz", + "integrity": "sha512-OLHER70+rZxvDl75xq3xXOfd3e8XIvz8fWY0dqg92UvhZ29zo24vQgfqgHSYhB5ZiuFpSLeriOisAlxAo/1jWg==", + "dev": true, + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -4049,6 +4367,33 @@ "node": ">= 8" } }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/address/node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -12776,6 +13121,34 @@ "jiti": "bin/jiti.js" } }, + "node_modules/joi": { + "version": "17.12.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz", + "integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/joi/node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true + }, + "node_modules/joi/node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index e0bf179d9..46fdf9ecc 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@babel/preset-env": "^7.16.7", "@commitlint/cli": "^19.0.3", "@commitlint/config-conventional": "^19.0.3", + "@hapi/hapi": "^21.3.7", "@types/connect": "^3.4.35", "@types/express": "^4.17.13", "@types/mime-types": "^2.1.1", @@ -88,6 +89,7 @@ "file-loader": "^6.2.0", "husky": "^9.0.10", "jest": "^29.3.1", + "joi": "^17.12.2", "lint-staged": "^15.2.0", "npm-run-all": "^4.1.5", "prettier": "^3.2.4", diff --git a/src/index.js b/src/index.js index a141a7b4d..ba67ff858 100644 --- a/src/index.js +++ b/src/index.js @@ -103,8 +103,8 @@ const noop = () => {}; */ /** - * @template {IncomingMessage} RequestInternal - * @template {ServerResponse} ResponseInternal + * @template {IncomingMessage} [RequestInternal = IncomingMessage] + * @template {ServerResponse} [ResponseInternal = ServerResponse] * @typedef {Object} Options * @property {{[key: string]: string}} [mimeTypes] * @property {string | undefined} [mimeTypeDefault] @@ -292,4 +292,65 @@ function wdm(compiler, options = {}) { return instance; } +/** + * @template S + * @template O + * @typedef {Object} HapiPluginBase + * @property {(server: S, options: O) => void | Promise} register + */ + +/** + * @template S + * @template O + * @typedef {HapiPluginBase & { pkg: { name: string } }} HapiPlugin + */ + +/** + * @typedef {Options & { compiler: Compiler | MultiCompiler }} HapiOptions + */ + +/** + * @template HapiServer + * @template {HapiOptions} HapiOptionsInternal + * @returns {HapiPlugin} + */ +function hapiPlugin() { + return { + pkg: { + name: "webpack-dev-middleware", + }, + register(server, options) { + const { compiler, ...rest } = options; + + if (!compiler) { + throw new Error("The compiler options is required."); + } + + const devMiddleware = wdm(compiler, rest); + + // @ts-ignore + server.decorate("server", "webpackDevMiddleware", devMiddleware); + // @ts-ignore + server.ext("onRequest", (request, h) => + new Promise((resolve, reject) => { + devMiddleware(request.raw.req, request.raw.res, (error) => { + if (error) { + reject(error); + return; + } + + resolve(request); + }); + }) + .then(() => h.continue) + .catch((error) => { + throw error; + }), + ); + }, + }; +} + +wdm.hapiPlugin = hapiPlugin; + module.exports = wdm; diff --git a/test/api.test.js b/test/api.test.js index b49fded0a..b1b2716a2 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -80,71 +80,66 @@ describe.each([ }); }); - if (webpack.version[0] === 5) { - describe("should accept compiler in watch mode", () => { - beforeEach((done) => { - compiler = webpack( - { ...webpackConfig, ...{ watch: true } }, - (error) => { - if (error) { - throw error; - } - }, - ); + describe("should accept compiler in watch mode", () => { + beforeEach((done) => { + compiler = webpack( + { ...webpackConfig, ...{ watch: true } }, + (error) => { + if (error) { + throw error; + } + }, + ); - instance = middleware(compiler); + instance = middleware(compiler); - app = framework(); - app.use(instance); + app = framework(); + app.use(instance); - listen = app.listen((error) => { - if (error) { - return done(error); - } + listen = app.listen((error) => { + if (error) { + return done(error); + } - return done(); - }); + return done(); }); + }); - afterEach((done) => { - if (instance.context.watching.closed) { - if (listen) { - listen.close(done); - } else { - done(); - } - - return; + afterEach((done) => { + if (instance.context.watching.closed) { + if (listen) { + listen.close(done); + } else { + done(); } - instance.close(() => { - if (listen) { - listen.close(done); - } else { - done(); - } - }); + return; + } + + instance.close(() => { + if (listen) { + listen.close(done); + } else { + done(); + } }); + }); - it("should work", (done) => { - const doneSpy = jest.spyOn( - getCompilerHooks(compiler).done[0], - "fn", - ); + it("should work", (done) => { + const doneSpy = jest.spyOn(getCompilerHooks(compiler).done[0], "fn"); - instance.waitUntilValid(() => { - instance.close(); + instance.waitUntilValid(() => { + instance.close(); - expect(compiler.running).toBe(false); - expect(doneSpy).toHaveBeenCalledTimes(1); + expect(compiler.running).toBe(false); + expect(doneSpy).toHaveBeenCalledTimes(1); - doneSpy.mockRestore(); + doneSpy.mockRestore(); - done(); - }); + done(); }); }); - } + }); }); describe("waitUntilValid method", () => { diff --git a/test/middleware.test.js b/test/middleware.test.js index 663bb3eba..d920bd0cd 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -1,8 +1,9 @@ import fs from "fs"; import path from "path"; -import express from "express"; import connect from "connect"; +import express from "express"; +import Hapi from "@hapi/hapi"; import request from "supertest"; import memfs, { createFsFromVolume, Volume } from "memfs"; import del from "del"; @@ -21,55 +22,191 @@ import webpackClientServerConfig from "./fixtures/webpack.client.server.config"; // Suppress unnecessary stats output global.console.log = jest.fn(); -describe.each([ - ["express", express], - ["connect", connect], -])("%s framework:", (_, framework) => { - describe("middleware", () => { - let instance; - let listen; - let app; - let req; +async function startServer(app) { + return new Promise((resolve, reject) => { + const server = app.listen((error) => { + if (error) { + return reject(error); + } - function listenShorthand(done) { - return app.listen((error) => { - if (error) { - return done(error); - } + return resolve(server); + }); + }); +} + +async function frameworkFactory( + name, + framework, + compiler, + devMiddlewareOptions, + options = {}, +) { + switch (name) { + case "hapi": { + const server = framework.server(); + const hapiPlugin = { + plugin: middleware.hapiPlugin(), + options: { + compiler, + ...devMiddlewareOptions, + }, + }; + + const middlewares = + typeof options.setupMiddlewares === "function" + ? options.setupMiddlewares([hapiPlugin]) + : [hapiPlugin]; + + await Promise.all( + middlewares.map((item) => { + // eslint-disable-next-line no-shadow + const { plugin, options } = item; - return done(); - }); - } + return server.register({ + plugin, + options, + }); + }), + ); - function close(done) { - if (instance.context.watching.closed) { - if (listen) { - listen.close(done); + await server.start(); + + return [server, server.listener, server.webpackDevMiddleware]; + } + default: { + const app = framework(); + const instance = middleware(compiler, devMiddlewareOptions); + const middlewares = + typeof options.setupMiddlewares === "function" + ? options.setupMiddlewares([instance]) + : [instance]; + + for (const item of middlewares) { + if (item.route) { + app.use(item.route, item.fn); } else { - done(); + app.use(item); } + } + + const server = await startServer(app); + + return [server, app, instance]; + } + } +} + +async function closeServer(server) { + // hapi + if (typeof server.stop === "function") { + return server.stop(); + } + + return new Promise((resolve, reject) => { + server.close((err) => { + if (err) { + reject(err); return; } - instance.close(() => { - if (listen) { - listen.close(done); - } else { - done(); + resolve(); + }); + }); +} + +async function close(server, instance) { + return Promise.resolve() + .then(() => { + if (!instance.context.watching.closed) { + return new Promise((resolve, reject) => { + instance.close((err) => { + if (err) { + reject(err); + return; + } + + resolve(); + }); + }); + } + + return Promise.resolve(); + }) + .then(() => { + if (server) { + return closeServer(server); + } + + return Promise.resolve(); + }); +} + +function get404ContentTypeHeader(name) { + switch (name) { + case "hapi": + return "application/json; charset=utf-8"; + default: + return "text/html; charset=utf-8"; + } +} + +function applyTestMiddleware(name, middlewares) { + if (name === "hapi") { + middlewares.push({ + plugin: { + name: "myPlugin", + version: "1.0.0", + register(innerServer) { + innerServer.route({ + method: "GET", + path: "/file.jpg", + handler() { + return "welcome"; + }, + }); + }, + }, + }); + } else { + middlewares.push({ + route: "/file.jpg", + fn: (req, res) => { + // Express API + if (res.send) { + res.send("welcome"); } - }); - } + // Connect API + else { + res.setHeader("Content-Type", "text/html"); + res.end("welcome"); + } + }, + }); + } + + return middlewares; +} + +describe.each([ + ["connect", connect], + ["express", express], + ["hapi", Hapi], +])("%s framework:", (name, framework) => { + describe("middleware", () => { + let instance; + let server; + let app; + let req; describe("basic", () => { describe("should work", () => { let compiler; let codeContent; - let codeLength; const outputPath = path.resolve(__dirname, "./outputs/basic-test"); - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -77,21 +214,16 @@ describe.each([ path: outputPath, }, }); - - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(() => { - compiler.hooks.afterCompile.tap("wdm-test", (params) => { - codeContent = params.assets["bundle.js"].source(); - codeLength = Buffer.byteLength(codeContent); - - done(); - }); + compiler.hooks.afterCompile.tap("wdm-test", (params) => { + codeContent = params.assets["bundle.js"].source(); }); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); + instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, }); @@ -132,7 +264,9 @@ describe.each([ req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it("should not find the bundle file on disk", async () => { const response = await req.get("/bundle.js"); @@ -151,7 +285,7 @@ describe.each([ expect(response.statusCode).toEqual(200); expect(response.headers["content-length"]).toEqual( - String(codeLength), + String(Buffer.byteLength(codeContent)), ); expect(response.headers["content-type"]).toEqual( "application/javascript; charset=utf-8", @@ -164,7 +298,7 @@ describe.each([ expect(response.statusCode).toEqual(200); expect(response.headers["content-length"]).toEqual( - String(codeLength), + String(Buffer.byteLength(codeContent)), ); expect(response.headers["content-type"]).toEqual( "application/javascript; charset=utf-8", @@ -299,7 +433,7 @@ describe.each([ expect(response.statusCode).toEqual(416); expect(response.headers["content-range"]).toEqual( - `bytes */${codeLength}`, + `bytes */${Buffer.byteLength(codeContent)}`, ); expect(response.headers["content-type"]).toEqual( "text/html; charset=utf-8", @@ -325,7 +459,7 @@ describe.each([ expect(response.statusCode).toEqual(206); expect(response.headers["content-range"]).toEqual( - `bytes 3000-3500/${codeLength}`, + `bytes 3000-3500/${Buffer.byteLength(codeContent)}`, ); expect(response.headers["content-length"]).toEqual("501"); expect(response.headers["content-type"]).toEqual( @@ -342,7 +476,7 @@ describe.each([ expect(response.statusCode).toEqual(206); expect(response.headers["content-range"]).toEqual( - `bytes 3000-3500/${codeLength}`, + `bytes 3000-3500/${Buffer.byteLength(codeContent)}`, ); expect(response.headers["content-length"]).toEqual("501"); expect(response.headers["content-type"]).toEqual( @@ -358,7 +492,7 @@ describe.each([ expect(response.statusCode).toEqual(206); expect(response.headers["content-range"]).toEqual( - `bytes 3000-3500/${codeLength}`, + `bytes 3000-3500/${Buffer.byteLength(codeContent)}`, ); expect(response.headers["content-length"]).toEqual("501"); expect(response.headers["content-type"]).toEqual( @@ -375,7 +509,7 @@ describe.each([ expect(response.statusCode).toEqual(206); expect(response.headers["content-range"]).toEqual( - `bytes 3000-3500/${codeLength}`, + `bytes 3000-3500/${Buffer.byteLength(codeContent)}`, ); expect(response.headers["content-length"]).toEqual("501"); expect(response.headers["content-type"]).toEqual( @@ -392,7 +526,7 @@ describe.each([ expect(response.statusCode).toEqual(206); expect(response.headers["content-range"]).toEqual( - `bytes 0-3500/${codeLength}`, + `bytes 0-3500/${Buffer.byteLength(codeContent)}`, ); expect(response.headers["content-length"]).toEqual("3501"); expect(response.headers["content-type"]).toEqual( @@ -409,7 +543,7 @@ describe.each([ expect(response.statusCode).toEqual(206); expect(response.headers["content-range"]).toEqual( - `bytes 0-800/${codeLength}`, + `bytes 0-800/${Buffer.byteLength(codeContent)}`, ); expect(response.headers["content-length"]).toEqual("801"); expect(response.headers["content-type"]).toEqual( @@ -516,7 +650,7 @@ describe.each([ expect(response.statusCode).toEqual(404); expect(response.headers["content-type"]).toEqual( - "text/html; charset=utf-8", + get404ContentTypeHeader(name), ); }); }); @@ -526,7 +660,7 @@ describe.each([ const outputPath = path.resolve(__dirname, "./outputs/basic"); - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -536,17 +670,18 @@ describe.each([ }, }); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "400" code for the "GET" request to the bundle file', async () => { const response = await req.get("/bundle.js"); @@ -556,20 +691,21 @@ describe.each([ }); describe("should work in multi-compiler mode", () => { - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler(webpackMultiConfig); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return "200" code for GET request to the bundle file for the first compiler', async () => { const response = await req.get("/static-one/bundle.js"); @@ -630,7 +766,7 @@ describe.each([ expect(response.statusCode).toEqual(404); expect(response.headers["content-type"]).toEqual( - "text/html; charset=utf-8", + get404ContentTypeHeader(name), ); }); @@ -639,7 +775,7 @@ describe.each([ expect(response.statusCode).toEqual(404); expect(response.headers["content-type"]).toEqual( - "text/html; charset=utf-8", + get404ContentTypeHeader(name), ); }); @@ -648,7 +784,7 @@ describe.each([ expect(response.statusCode).toEqual(404); expect(response.headers["content-type"]).toEqual( - "text/html; charset=utf-8", + get404ContentTypeHeader(name), ); }); }); @@ -675,32 +811,32 @@ describe.each([ }, { value: "invalid.js", - contentType: "text/html; charset=utf-8", + contentType: get404ContentTypeHeader(name), code: 404, }, { value: "complex", - contentType: "text/html; charset=utf-8", + contentType: get404ContentTypeHeader(name), code: 404, }, { value: "complex/invalid.js", - contentType: "text/html; charset=utf-8", + contentType: get404ContentTypeHeader(name), code: 404, }, { value: "complex/complex", - contentType: "text/html; charset=utf-8", + contentType: get404ContentTypeHeader(name), code: 404, }, { value: "complex/complex/invalid.js", - contentType: "text/html; charset=utf-8", + contentType: get404ContentTypeHeader(name), code: 404, }, { value: "%", - contentType: "text/html; charset=utf-8", + contentType: get404ContentTypeHeader(name), code: 404, }, ], @@ -1013,7 +1149,7 @@ describe.each([ let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -1023,12 +1159,11 @@ describe.each([ }, }); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); req = request(app); @@ -1048,7 +1183,9 @@ describe.each([ } }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); for (const { data, urls } of fixtures) { for (const { value, contentType, code } of urls) { @@ -1079,35 +1216,66 @@ describe.each([ }); describe('should respect the value of the "Content-Type" header from other middleware', () => { - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler(webpackConfig); - instance = middleware(compiler); - - app = framework(); - // eslint-disable-next-line no-shadow - app.use((req, res, next) => { - // Express API - if (res.set) { - res.set("Content-Type", "application/vnd.test+octet-stream"); - } - // Connect API - else { - res.setHeader( - "Content-Type", - "application/vnd.test+octet-stream", - ); - } - next(); - }); - app.use(instance); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + // eslint-disable-next-line no-undefined + undefined, + { + setupMiddlewares: (middlewares) => { + if (name === "hapi") { + middlewares.unshift({ + plugin: { + name: "myPlugin", + version: "1.0.0", + register(innerServer) { + innerServer.ext("onRequest", (innerRequest, h) => { + innerRequest.raw.res.setHeader( + "Content-Type", + "application/vnd.test+octet-stream", + ); + + return h.continue; + }); + }, + }, + }); + } else { + middlewares.unshift((req, res, next) => { + // Express API + if (res.set) { + res.set( + "Content-Type", + "application/vnd.test+octet-stream", + ); + } + // Connect API + else { + res.setHeader( + "Content-Type", + "application/vnd.test+octet-stream", + ); + } + + next(); + }); + } - listen = listenShorthand(done); + return middlewares; + }, + }, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should not modify the "Content-Type" header', async () => { const response = await req.get("/bundle.js"); @@ -1119,50 +1287,23 @@ describe.each([ }); }); - describe('should not throw an error on the valid "output.path" value for linux', () => { - it("should be no error", (done) => { - expect(() => { - const compiler = getCompiler(); - - compiler.outputPath = "/my/path"; - - instance = middleware(compiler); - - instance.close(done); - }).not.toThrow(); - }); - }); - - describe('should not throw an error on the valid "output.path" value for windows', () => { - it("should be no error", (done) => { - expect(() => { - const compiler = getCompiler(); - - compiler.outputPath = "C:/my/path"; - - instance = middleware(compiler); - - instance.close(done); - }).not.toThrow(); - }); - }); - describe('should work without "output" options', () => { - beforeAll((done) => { + beforeAll(async () => { // eslint-disable-next-line no-undefined const compiler = getCompiler({ ...webpackConfig, output: undefined }); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return "200" code for GET request to the bundle file', async () => { const response = await req.get("/main.js"); @@ -1196,7 +1337,7 @@ describe.each([ }); describe('should work with trailing slash at the end of the "option.path" option', () => { - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler({ ...webpackConfig, output: { @@ -1205,17 +1346,18 @@ describe.each([ }, }); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return "200" code for GET request to the bundle file', async () => { const response = await req.get("/bundle.js"); @@ -1249,20 +1391,21 @@ describe.each([ }); describe('should respect empty "output.publicPath" and "output.path" options', () => { - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler(webpackConfig); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return "200" code for GET request to the bundle file', async () => { const response = await req.get("/bundle.js"); @@ -1296,7 +1439,7 @@ describe.each([ }); describe('should respect "output.publicPath" and "output.path" options', () => { - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler({ ...webpackConfig, output: { @@ -1306,17 +1449,18 @@ describe.each([ }, }); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return "200" code for GET request to the bundle file', async () => { const response = await req.get("/static/bundle.js"); @@ -1353,7 +1497,7 @@ describe.each([ expect(response.statusCode).toEqual(404); expect(response.headers["content-type"]).toEqual( - "text/html; charset=utf-8", + get404ContentTypeHeader(name), ); }); }); @@ -1361,7 +1505,7 @@ describe.each([ describe('should respect "output.publicPath" and "output.path" options with hash substitutions', () => { let hash; - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler({ ...webpackConfig, output: { @@ -1370,23 +1514,32 @@ describe.each([ path: path.resolve(__dirname, "./outputs/other-basic-[fullhash]"), }, }); + compiler.hooks.afterCompile.tap("wdm-test", ({ hash: h }) => { + hash = h; + }); - instance = middleware(compiler); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); - app = framework(); - app.use(instance); + await new Promise((resolve) => { + const interval = setInterval(() => { + if (hash) { + clearInterval(interval); - listen = listenShorthand(() => { - compiler.hooks.afterCompile.tap("wdm-test", ({ hash: h }) => { - hash = h; - done(); - }); + resolve(); + } + }, 10); }); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return "200" code for GET request to the bundle file', async () => { const response = await req.get(`/static/${hash}/bundle.js`); @@ -1429,7 +1582,7 @@ describe.each([ let hashOne; let hashTwo; - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler([ { ...webpackMultiConfig[0], @@ -1454,27 +1607,35 @@ describe.each([ }, }, ]); + compiler.hooks.done.tap("wdm-test", (stats) => { + const [one, two] = stats.stats; - instance = middleware(compiler); - - app = framework(); - app.use(instance); + hashOne = one.hash; + hashTwo = two.hash; + }); - listen = listenShorthand(() => { - compiler.hooks.done.tap("wdm-test", (params) => { - const [one, two] = params.stats; + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); - hashOne = one.hash; - hashTwo = two.hash; + await new Promise((resolve) => { + const interval = setInterval(() => { + if (hashOne && hashTwo) { + clearInterval(interval); - done(); - }); + resolve(); + } + }, 10); }); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return "200" code for GET request to the bundle file for the first compiler', async () => { const response = await req.get(`/static-one/${hashOne}/bundle.js`); @@ -1520,7 +1681,7 @@ describe.each([ expect(response.statusCode).toEqual(404); expect(response.headers["content-type"]).toEqual( - "text/html; charset=utf-8", + get404ContentTypeHeader(name), ); }); @@ -1544,20 +1705,21 @@ describe.each([ }); describe('should respect "output.publicPath" and "output.path" options in multi-compiler mode with difference "publicPath" and "path"', () => { - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler(webpackMultiConfig); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return "200" code for GET request to the bundle file for the first compiler', async () => { const response = await req.get("/static-one/bundle.js"); @@ -1627,7 +1789,7 @@ describe.each([ }); describe('should respect "output.publicPath" and "output.path" options in multi-compiler mode with same "publicPath"', () => { - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler([ { ...webpackMultiConfig[0], @@ -1647,17 +1809,18 @@ describe.each([ }, ]); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return "200" code for GET request to the bundle file for the first compiler', async () => { const response = await req.get("/my-public/bundle-one.js"); @@ -1709,7 +1872,7 @@ describe.each([ }); describe('should respect "output.publicPath" and "output.path" options in multi-compiler mode with same "path"', () => { - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler([ { ...webpackMultiConfig[0], @@ -1729,17 +1892,18 @@ describe.each([ }, ]); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return "200" code for GET request to the bundle file for the first compiler', async () => { const response = await req.get("/one-public/bundle-one.js"); @@ -1818,20 +1982,21 @@ describe.each([ }); describe('should respect "output.publicPath" and "output.path" options in multi-compiler mode, when the "output.publicPath" option presented in only one configuration (in first)', () => { - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler(webpackClientServerConfig); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return "200" code for GET request to the bundle file', async () => { const response = await req.get("/static/bundle.js"); @@ -1874,23 +2039,24 @@ describe.each([ }); describe('should respect "output.publicPath" and "output.path" options in multi-compiler mode, when the "output.publicPath" option presented in only one configuration (in second)', () => { - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler([ webpackClientServerConfig[1], webpackClientServerConfig[0], ]); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return "200" code for GET request to the bundle file', async () => { const response = await req.get("/static/bundle.js"); @@ -1930,7 +2096,7 @@ describe.each([ }); describe('should respect "output.publicPath" and "output.path" options in multi-compiler mode, when the "output.publicPath" option presented in only one configuration with same "path"', () => { - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler([ { ...webpackClientServerConfig[0], @@ -1949,17 +2115,18 @@ describe.each([ }, ]); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return "200" code for GET request to the bundle file', async () => { const response = await req.get("/static/bundle-one.js"); @@ -1999,17 +2166,12 @@ describe.each([ }); describe("should handle an earlier request if a change happened while compiling", () => { - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler(webpackConfig); - instance = middleware(compiler); - let invalidated = false; - (compiler.hooks.afterDone - ? compiler.hooks.afterDone - : compiler.hooks.done - ).tap("Invalidated", () => { + compiler.hooks.afterDone.tap("Invalidated", () => { if (!invalidated) { instance.invalidate(); @@ -2017,15 +2179,18 @@ describe.each([ } }); - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request to the bundle file', async () => { const response = await req.get("/bundle.js"); @@ -2042,7 +2207,7 @@ describe.each([ "./outputs/basic-test-errors-500", ); - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -2051,16 +2216,11 @@ describe.each([ }, }); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(() => { - compiler.hooks.afterCompile.tap("wdm-test", () => { - done(); - }); - }); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, @@ -2087,7 +2247,9 @@ describe.each([ req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "500" code for the "GET" request to the "image.svg" file', async () => { const response = await req.get("/image.svg").set("Range", "bytes=0-"); @@ -2119,7 +2281,7 @@ describe.each([ "./outputs/basic-test-errors-404", ); - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -2128,16 +2290,11 @@ describe.each([ }, }); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(() => { - compiler.hooks.afterCompile.tap("wdm-test", () => { - done(); - }); - }); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, @@ -2168,7 +2325,9 @@ describe.each([ req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "404" code for the "GET" request to the "image.svg" file', async () => { const response = await req.get("/image.svg").set("Range", "bytes=0-"); @@ -2195,14 +2354,13 @@ describe.each([ describe("should work without `fs.createReadStream`", () => { let compiler; let codeContent; - let codeLength; const outputPath = path.resolve( __dirname, "./outputs/basic-test-no-createReadStream", ); - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -2210,21 +2368,16 @@ describe.each([ path: outputPath, }, }); - - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(() => { - compiler.hooks.afterCompile.tap("wdm-test", (params) => { - codeContent = params.assets["bundle.js"].source(); - codeLength = Buffer.byteLength(codeContent); - - done(); - }); + compiler.hooks.afterCompile.tap("wdm-test", (params) => { + codeContent = params.assets["bundle.js"].source(); }); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); + instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, }); @@ -2238,14 +2391,16 @@ describe.each([ req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request to the bundle file', async () => { const response = await req.get("/bundle.js"); expect(response.statusCode).toEqual(200); expect(response.headers["content-length"]).toEqual( - String(codeLength), + String(Buffer.byteLength(codeContent)), ); expect(response.headers["content-type"]).toEqual( "application/javascript; charset=utf-8", @@ -2286,7 +2441,7 @@ describe.each([ describe("mimeTypes option", () => { describe('should set the correct value for "Content-Type" header to known MIME type', () => { - beforeAll((done) => { + beforeAll(async () => { const outputPath = path.resolve(__dirname, "./outputs/basic"); const compiler = getCompiler({ ...webpackConfig, @@ -2296,12 +2451,11 @@ describe.each([ }, }); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); req = request(app); @@ -2314,7 +2468,9 @@ describe.each([ ); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request to "file.html"', async () => { const response = await req.get("/file.html"); @@ -2328,7 +2484,7 @@ describe.each([ }); describe('should set the correct value for "Content-Type" header to specified MIME type', () => { - beforeAll((done) => { + beforeAll(async () => { const outputPath = path.resolve(__dirname, "./outputs/basic"); const compiler = getCompiler({ ...webpackConfig, @@ -2338,16 +2494,16 @@ describe.each([ }, }); - instance = middleware(compiler, { - mimeTypes: { - myhtml: "text/html", + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + mimeTypes: { + myhtml: "text/html", + }, }, - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + ); req = request(app); @@ -2360,7 +2516,9 @@ describe.each([ ); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request "file.phtml"', async () => { const response = await req.get("/file.myhtml"); @@ -2374,7 +2532,7 @@ describe.each([ }); describe('should override value for "Content-Type" header for known MIME type', () => { - beforeAll((done) => { + beforeAll(async () => { const outputPath = path.resolve(__dirname, "./outputs/basic"); const compiler = getCompiler({ ...webpackConfig, @@ -2384,16 +2542,16 @@ describe.each([ }, }); - instance = middleware(compiler, { - mimeTypes: { - jpg: "image/vnd.test+jpeg", + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + mimeTypes: { + jpg: "image/vnd.test+jpeg", + }, }, - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + ); req = request(app); @@ -2406,7 +2564,9 @@ describe.each([ ); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request "file.jpg"', async () => { const response = await req.get("/file.jpg"); @@ -2419,7 +2579,7 @@ describe.each([ }); describe('should not set "Content-Type" header for route not from outputFileSystem', () => { - beforeAll((done) => { + beforeAll(async () => { const outputPath = path.resolve(__dirname, "./outputs/basic"); const compiler = getCompiler({ ...webpackConfig, @@ -2429,34 +2589,30 @@ describe.each([ }, }); - instance = middleware(compiler, { - mimeTypes: { - jpg: "image/vnd.test+jpeg", + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + mimeTypes: { + jpg: "image/vnd.test+jpeg", + }, }, - }); - - app = framework(); - app.use(instance); - - // eslint-disable-next-line no-shadow - app.use("/file.jpg", (req, res) => { - // Express API - if (res.send) { - res.send("welcome"); - } - // Connect API - else { - res.setHeader("Content-Type", "text/html"); - res.end("welcome"); - } - }); + { + setupMiddlewares: (middlewares) => { + applyTestMiddleware(name, middlewares); - listen = listenShorthand(done); + return middlewares; + }, + }, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request "file.jpg" with default content type', async () => { const response = await req.get("/file.jpg"); @@ -2469,7 +2625,7 @@ describe.each([ describe("mimeTypeDefault option", () => { describe('should set the correct value for "Content-Type" header to unknown MIME type', () => { - beforeAll((done) => { + beforeAll(async () => { const outputPath = path.resolve(__dirname, "./outputs/basic"); const compiler = getCompiler({ ...webpackConfig, @@ -2479,14 +2635,14 @@ describe.each([ }, }); - instance = middleware(compiler, { - mimeTypeDefault: "text/plain", - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + mimeTypeDefault: "text/plain", + }, + ); req = request(app); @@ -2499,7 +2655,9 @@ describe.each([ ); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request to "file.html"', async () => { const response = await req.get("/file.unknown"); @@ -2518,25 +2676,23 @@ describe.each([ let compiler; let spy; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler(webpackConfig); - spy = jest.spyOn(compiler, "watch"); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); req = request(app); }); - afterAll((done) => { + afterAll(async () => { spy.mockRestore(); - close(done); + await close(server, instance); }); it('should pass arguments to the "watch" method', async () => { @@ -2552,25 +2708,24 @@ describe.each([ let compiler; let spy; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler(webpackWatchOptionsConfig); spy = jest.spyOn(compiler, "watch"); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); req = request(app); }); - afterAll((done) => { + afterAll(async () => { spy.mockRestore(); - close(done); + await close(server, instance); }); it('should pass arguments to the "watch" method', async () => { @@ -2589,25 +2744,24 @@ describe.each([ let compiler; let spy; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler(webpackMultiWatchOptionsConfig); spy = jest.spyOn(compiler, "watch"); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); req = request(app); }); - afterAll((done) => { + afterAll(async () => { spy.mockRestore(); - close(done); + await close(server, instance); }); it('should pass arguments to the "watch" method', async () => { @@ -2631,7 +2785,7 @@ describe.each([ describe('should work with "true" value', () => { let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -2641,22 +2795,22 @@ describe.each([ }, }); - instance = middleware(compiler, { writeToDisk: true }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { writeToDisk: true }, + ); req = request(app); }); - afterAll((done) => { + afterAll(async () => { del.sync( path.posix.resolve(__dirname, "./outputs/write-to-disk-true"), ); - close(done); + await close(server, instance); }); it("should find the bundle file on disk", (done) => { @@ -2724,7 +2878,7 @@ describe.each([ let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -2734,25 +2888,25 @@ describe.each([ }, }); - instance = middleware(compiler, { writeToDisk: true }); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { writeToDisk: true }, + ); fs.mkdirSync(outputPath, { recursive: true, }); fs.writeFileSync(path.resolve(outputPath, "test.json"), "{}"); - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - req = request(app); }); - afterAll((done) => { + afterAll(async () => { del.sync(outputPath); - close(done); + await close(server, instance); }); it("should find the bundle file on disk", (done) => { @@ -2797,7 +2951,7 @@ describe.each([ describe('should work with "false" value', () => { let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -2806,15 +2960,17 @@ describe.each([ }, }); - instance = middleware(compiler, { writeToDisk: false }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { writeToDisk: false }, + ); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it("should not find the bundle file on disk", (done) => { request(app) @@ -2857,7 +3013,7 @@ describe.each([ describe('should work with "Function" value when it returns "true"', () => { let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -2869,19 +3025,19 @@ describe.each([ }, }); - instance = middleware(compiler, { - writeToDisk: (filePath) => /bundle\.js$/.test(filePath), - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + writeToDisk: (filePath) => /bundle\.js$/.test(filePath), + }, + ); req = request(app); }); - afterAll((done) => { + afterAll(async () => { del.sync( path.posix.resolve( __dirname, @@ -2889,7 +3045,7 @@ describe.each([ ), ); - close(done); + await close(server, instance); }); it("should find the bundle file on disk", async () => { @@ -2909,7 +3065,7 @@ describe.each([ describe('should work with "Function" value when it returns "false"', () => { let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -2921,17 +3077,19 @@ describe.each([ }, }); - instance = middleware(compiler, { - writeToDisk: (filePath) => !/bundle\.js$/.test(filePath), - }); - - app = framework(); - app.use(instance); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + writeToDisk: (filePath) => !/bundle\.js$/.test(filePath), + }, + ); - listen = listenShorthand(done); + req = request(app); }); - afterAll((done) => { + afterAll(async () => { del.sync( path.posix.resolve( __dirname, @@ -2939,7 +3097,7 @@ describe.each([ ), ); - close(done); + await close(server, instance); }); it("should not find the bundle file on disk", async () => { @@ -2959,7 +3117,7 @@ describe.each([ describe("should work when assets have query string", () => { let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackQueryStringConfig, output: { @@ -2971,17 +3129,17 @@ describe.each([ }, }); - instance = middleware(compiler, { writeToDisk: true }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { writeToDisk: true }, + ); req = request(app); }); - afterAll((done) => { + afterAll(async () => { del.sync( path.posix.resolve( __dirname, @@ -2989,7 +3147,7 @@ describe.each([ ), ); - close(done); + await close(server, instance); }); it("should find the bundle file on disk with no querystring", async () => { @@ -3009,7 +3167,7 @@ describe.each([ describe("should work in multi-compiler mode", () => { let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler([ { ...webpackMultiWatchOptionsConfig[0], @@ -3035,17 +3193,17 @@ describe.each([ }, ]); - instance = middleware(compiler, { writeToDisk: true }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { writeToDisk: true }, + ); req = request(app); }); - afterAll((done) => { + afterAll(async () => { del.sync( path.posix.resolve( __dirname, @@ -3053,7 +3211,7 @@ describe.each([ ), ); - close(done); + await close(server, instance); }); it("should find the bundle files on disk", async () => { @@ -3084,7 +3242,7 @@ describe.each([ let compiler; let hash; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, ...{ @@ -3098,28 +3256,36 @@ describe.each([ }, }, }); + compiler.hooks.afterCompile.tap("wdm-test", ({ hash: h }) => { + hash = h; + }); - instance = middleware(compiler, { writeToDisk: true }); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { writeToDisk: true }, + ); - app = framework(); - app.use(instance); + await new Promise((resolve) => { + const interval = setInterval(() => { + if (hash) { + clearInterval(interval); - listen = listenShorthand(() => { - compiler.hooks.afterCompile.tap("wdm-test", ({ hash: h }) => { - hash = h; - done(); - }); + resolve(); + } + }, 10); }); req = request(app); }); - afterAll((done) => { + afterAll(async () => { del.sync( path.posix.resolve(__dirname, "./outputs/write-to-disk-with-hash/"), ); - close(done); + await close(server, instance); }); it("should find the bundle file on disk", async () => { @@ -3140,23 +3306,25 @@ describe.each([ describe("methods option", () => { let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler(webpackConfig); - instance = middleware(compiler, { - methods: ["POST"], - publicPath: "/public/", - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + methods: ["POST"], + publicPath: "/public/", + }, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "200" code for the "POST" request to the bundle file', async () => { const response = await req.post(`/public/bundle.js`); @@ -3179,22 +3347,31 @@ describe.each([ describe("headers option", () => { describe("works with object", () => { - beforeEach((done) => { + beforeEach(async () => { const compiler = getCompiler(webpackConfig); - instance = middleware(compiler, { - headers: { "X-nonsense-1": "yes", "X-nonsense-2": "no" }, - }); - - app = framework(); - app.use(instance); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + headers: { "X-nonsense-1": "yes", "X-nonsense-2": "no" }, + }, + { + setupMiddlewares: (middlewares) => { + applyTestMiddleware(name, middlewares); - listen = listenShorthand(done); + return middlewares; + }, + }, + ); req = request(app); }); - afterEach(close); + afterEach(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request to the bundle file and return headers', async () => { const response = await req.get(`/bundle.js`); @@ -3205,18 +3382,6 @@ describe.each([ }); it('should return the "200" code for the "GET" request to path not in outputFileSystem but not return headers', async () => { - // eslint-disable-next-line no-shadow - app.use("/file.jpg", (req, res) => { - // Express API - if (res.send) { - res.send("welcome"); - } - // Connect API - else { - res.end("welcome"); - } - }); - const res = await request(app).get("/file.jpg"); expect(res.statusCode).toEqual(200); expect(res.headers["X-nonsense-1"]).toBeUndefined(); @@ -3225,31 +3390,40 @@ describe.each([ }); describe("works with array of objects", () => { - beforeEach((done) => { + beforeEach(async () => { const compiler = getCompiler(webpackConfig); - instance = middleware(compiler, { - headers: [ - { - key: "X-Foo", - value: "value1", - }, - { - key: "X-Bar", - value: "value2", - }, - ], - }); - - app = framework(); - app.use(instance); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + headers: [ + { + key: "X-Foo", + value: "value1", + }, + { + key: "X-Bar", + value: "value2", + }, + ], + }, + { + setupMiddlewares: (middlewares) => { + applyTestMiddleware(name, middlewares); - listen = listenShorthand(done); + return middlewares; + }, + }, + ); req = request(app); }); - afterEach(close); + afterEach(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request to the bundle file and return headers', async () => { const response = await req.get(`/bundle.js`); @@ -3260,18 +3434,6 @@ describe.each([ }); it('should return the "200" code for the "GET" request to path not in outputFileSystem but not return headers', async () => { - // eslint-disable-next-line no-shadow - app.use("/file.jpg", (req, res) => { - // Express API - if (res.send) { - res.send("welcome"); - } - // Connect API - else { - res.end("welcome"); - } - }); - const res = await request(app).get("/file.jpg"); expect(res.statusCode).toEqual(200); expect(res.headers["x-foo"]).toBeUndefined(); @@ -3280,24 +3442,33 @@ describe.each([ }); describe("works with function", () => { - beforeEach((done) => { + beforeEach(async () => { const compiler = getCompiler(webpackConfig); - instance = middleware(compiler, { - headers: () => { - return { "X-nonsense-1": "yes", "X-nonsense-2": "no" }; + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + headers: () => { + return { "X-nonsense-1": "yes", "X-nonsense-2": "no" }; + }, }, - }); - - app = framework(); - app.use(instance); + { + setupMiddlewares: (middlewares) => { + applyTestMiddleware(name, middlewares); - listen = listenShorthand(done); + return middlewares; + }, + }, + ); req = request(app); }); - afterEach(close); + afterEach(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request to the bundle file and return headers', async () => { const response = await req.get(`/bundle.js`); @@ -3308,18 +3479,6 @@ describe.each([ }); it('should return the "200" code for the "GET" request to path not in outputFileSystem but not return headers', async () => { - // eslint-disable-next-line no-shadow - app.use("/file.jpg", (req, res) => { - // Express API - if (res.send) { - res.send("welcome"); - } - // Connect API - else { - res.end("welcome"); - } - }); - const res = await req.get("/file.jpg"); expect(res.statusCode).toEqual(200); expect(res.headers["X-nonsense-1"]).toBeUndefined(); @@ -3328,31 +3487,40 @@ describe.each([ }); describe("works with function returning an array", () => { - beforeEach((done) => { + beforeEach(async () => { const compiler = getCompiler(webpackConfig); - instance = middleware(compiler, { - headers: () => [ - { - key: "X-Foo", - value: "value1", - }, - { - key: "X-Bar", - value: "value2", - }, - ], - }); - - app = framework(); - app.use(instance); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + headers: () => [ + { + key: "X-Foo", + value: "value1", + }, + { + key: "X-Bar", + value: "value2", + }, + ], + }, + { + setupMiddlewares: (middlewares) => { + applyTestMiddleware(name, middlewares); - listen = listenShorthand(done); + return middlewares; + }, + }, + ); req = request(app); }); - afterEach(close); + afterEach(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request to the bundle file and return headers', async () => { const response = await req.get(`/bundle.js`); @@ -3363,18 +3531,6 @@ describe.each([ }); it('should return the "200" code for the "GET" request to path not in outputFileSystem but not return headers', async () => { - // eslint-disable-next-line no-shadow - app.use("/file.jpg", (req, res) => { - // Express API - if (res.send) { - res.send("welcome"); - } - // Connect API - else { - res.end("welcome"); - } - }); - const res = await req.get("/file.jpg"); expect(res.statusCode).toEqual(200); expect(res.headers["x-foo"]).toBeUndefined(); @@ -3383,26 +3539,35 @@ describe.each([ }); describe("works with headers function with params", () => { - beforeEach((done) => { + beforeEach(async () => { const compiler = getCompiler(webpackConfig); - instance = middleware(compiler, { - // eslint-disable-next-line no-unused-vars, no-shadow - headers: (req, res, context) => { - res.setHeader("X-nonsense-1", "yes"); - res.setHeader("X-nonsense-2", "no"); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + // eslint-disable-next-line no-unused-vars, no-shadow + headers: (req, res, context) => { + res.setHeader("X-nonsense-1", "yes"); + res.setHeader("X-nonsense-2", "no"); + }, }, - }); - - app = framework(); - app.use(instance); + { + setupMiddlewares: (middlewares) => { + applyTestMiddleware(name, middlewares); - listen = listenShorthand(done); + return middlewares; + }, + }, + ); req = request(app); }); - afterEach(close); + afterEach(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request to the bundle file and return headers', async () => { const response = await req.get(`/bundle.js`); @@ -3413,18 +3578,6 @@ describe.each([ }); it('should return the "200" code for the "GET" request to path not in outputFileSystem but not return headers', async () => { - // eslint-disable-next-line no-shadow - app.use("/file.jpg", (req, res) => { - // Express API - if (res.send) { - res.send("welcome"); - } - // Connect API - else { - res.end("welcome"); - } - }); - const res = await req.get("/file.jpg"); expect(res.statusCode).toEqual(200); expect(res.headers["X-nonsense-1"]).toBeUndefined(); @@ -3435,20 +3588,22 @@ describe.each([ describe("publicPath option", () => { describe('should work with "string" value', () => { - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler(webpackConfig); - instance = middleware(compiler, { publicPath: "/public/" }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { publicPath: "/public/" }, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request to the bundle file', async () => { const response = await req.get(`/public/bundle.js`); @@ -3458,20 +3613,22 @@ describe.each([ }); describe('should work with "auto" value', () => { - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler(webpackConfig); - instance = middleware(compiler, { publicPath: "auto" }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { publicPath: "auto" }, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request to the bundle file', async () => { const response = await req.get("/bundle.js"); @@ -3484,36 +3641,64 @@ describe.each([ describe("serverSideRender option", () => { let locals; - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler(webpackConfig); - instance = middleware(compiler, { serverSideRender: true }); - - app = framework(); - app.use(instance); - // eslint-disable-next-line no-shadow - app.use((req, res) => { - // eslint-disable-next-line prefer-destructuring - locals = res.locals; - - // Express API - if (res.sendStatus) { - res.sendStatus(200); - } - // Connect API - else { - // eslint-disable-next-line no-param-reassign - res.statusCode = 200; - res.end(); - } - }); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { serverSideRender: true }, + { + setupMiddlewares: (middlewares) => { + if (name === "hapi") { + middlewares.push({ + plugin: { + name: "myPlugin", + version: "1.0.0", + register(innerServer) { + innerServer.route({ + method: "GET", + path: "/foo/bar", + handler(innerReq) { + // eslint-disable-next-line prefer-destructuring + locals = innerReq.raw.res.locals; + + return "welcome"; + }, + }); + }, + }, + }); + } else { + middlewares.push((_req, res) => { + // eslint-disable-next-line prefer-destructuring + locals = res.locals; + + // Express API + if (res.sendStatus) { + res.sendStatus(200); + } + // Connect API + else { + // eslint-disable-next-line no-param-reassign + res.statusCode = 200; + res.end(); + } + }); + } - listen = listenShorthand(done); + return middlewares; + }, + }, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request', async () => { const response = await req.get("/foo/bar"); @@ -3527,18 +3712,19 @@ describe.each([ describe("should work with an unspecified value", () => { let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler(webpackConfig); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + ); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should use the "memfs" package by default', () => { const { Stats } = memfs; @@ -3553,7 +3739,7 @@ describe.each([ describe("should work with the configured value (native fs)", () => { let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler(webpackConfig); const configuredFs = fs; @@ -3561,17 +3747,19 @@ describe.each([ configuredFs.join = path.join.bind(path); configuredFs.mkdirp = () => {}; - instance = middleware(compiler, { - outputFileSystem: configuredFs, - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + outputFileSystem: configuredFs, + }, + ); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it("should use the configurated output filesystem", () => { const { Stats } = fs; @@ -3588,7 +3776,7 @@ describe.each([ describe("should work with the configured value (memfs)", () => { let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler(webpackConfig); const configuredFs = createFsFromVolume(new Volume()); @@ -3596,17 +3784,19 @@ describe.each([ configuredFs.join = path.join.bind(path); configuredFs.mkdirp = () => {}; - instance = middleware(compiler, { - outputFileSystem: configuredFs, - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + outputFileSystem: configuredFs, + }, + ); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it("should use the configured output filesystem", () => { const { Stats } = memfs; @@ -3624,7 +3814,7 @@ describe.each([ describe("should work with the configured value in multi-compiler mode (native fs)", () => { let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler(webpackMultiConfig); const configuredFs = fs; @@ -3632,17 +3822,19 @@ describe.each([ configuredFs.join = path.join.bind(path); configuredFs.mkdirp = () => {}; - instance = middleware(compiler, { - outputFileSystem: configuredFs, - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + outputFileSystem: configuredFs, + }, + ); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it("should use configured output filesystems", () => { const { Stats } = fs; @@ -3666,27 +3858,29 @@ describe.each([ describe("index option", () => { describe('should work with "false" value', () => { - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler(webpackConfig); - instance = middleware(compiler, { index: false, publicPath: "/" }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { index: false, publicPath: "/" }, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "404" code for the "GET" request to the public path', async () => { const response = await req.get("/"); expect(response.statusCode).toEqual(404); expect(response.headers["content-type"]).toEqual( - "text/html; charset=utf-8", + get404ContentTypeHeader(name), ); }); @@ -3701,20 +3895,22 @@ describe.each([ }); describe('should work with "true" value', () => { - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler(webpackConfig); - instance = middleware(compiler, { index: true, publicPath: "/" }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { index: true, publicPath: "/" }, + ); req = request(app); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request to the public path', async () => { const response = await req.get("/"); @@ -3736,7 +3932,7 @@ describe.each([ }); describe('should work with "string" value', () => { - beforeAll((done) => { + beforeAll(async () => { const outputPath = path.resolve(__dirname, "./outputs/basic"); const compiler = getCompiler({ ...webpackConfig, @@ -3746,15 +3942,15 @@ describe.each([ }, }); - instance = middleware(compiler, { - index: "default.html", - publicPath: "/", - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + index: "default.html", + publicPath: "/", + }, + ); req = request(app); @@ -3767,7 +3963,9 @@ describe.each([ ); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request to the public path', async () => { const response = await req.get("/"); @@ -3780,7 +3978,7 @@ describe.each([ }); describe('should work with "string" value with a custom extension', () => { - beforeAll((done) => { + beforeAll(async () => { const outputPath = path.resolve(__dirname, "./outputs/basic"); const compiler = getCompiler({ ...webpackConfig, @@ -3790,15 +3988,15 @@ describe.each([ }, }); - instance = middleware(compiler, { - index: "index.custom", - publicPath: "/", - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + index: "index.custom", + publicPath: "/", + }, + ); req = request(app); @@ -3811,7 +4009,9 @@ describe.each([ ); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request to the public path', async () => { const response = await req.get("/"); @@ -3821,7 +4021,7 @@ describe.each([ }); describe('should work with "string" value with a custom extension and defined a custom MIME type', () => { - beforeAll((done) => { + beforeAll(async () => { const outputPath = path.resolve(__dirname, "./outputs/basic"); const compiler = getCompiler({ ...webpackConfig, @@ -3831,18 +4031,18 @@ describe.each([ }, }); - instance = middleware(compiler, { - index: "index.mycustom", - mimeTypes: { - mycustom: "text/html", + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + index: "index.mycustom", + mimeTypes: { + mycustom: "text/html", + }, + publicPath: "/", }, - publicPath: "/", - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + ); req = request(app); @@ -3855,7 +4055,9 @@ describe.each([ ); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request to the public path', async () => { const response = await req.get("/"); @@ -3868,7 +4070,7 @@ describe.each([ }); describe('should work with "string" value without an extension', () => { - beforeAll((done) => { + beforeAll(async () => { const outputPath = path.resolve(__dirname, "./outputs/basic"); const compiler = getCompiler({ ...webpackConfig, @@ -3878,12 +4080,12 @@ describe.each([ }, }); - instance = middleware(compiler, { index: "noextension" }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { index: "noextension" }, + ); req = request(app); @@ -3896,7 +4098,9 @@ describe.each([ ); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "200" code for the "GET" request to the public path', async () => { const response = await req.get("/"); @@ -3906,7 +4110,7 @@ describe.each([ }); describe('should work with "string" value but the "index" option is a directory', () => { - beforeAll((done) => { + beforeAll(async () => { const outputPath = path.resolve(__dirname, "./outputs/basic"); const compiler = getCompiler({ ...webpackConfig, @@ -3916,15 +4120,15 @@ describe.each([ }, }); - instance = middleware(compiler, { - index: "custom.html", - publicPath: "/", - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + index: "custom.html", + publicPath: "/", + }, + ); req = request(app); @@ -3936,7 +4140,9 @@ describe.each([ ); }); - afterAll(close); + afterAll(async () => { + await close(server, instance); + }); it('should return the "404" code for the "GET" request to the public path', async () => { const response = await req.get("/"); @@ -3949,13 +4155,18 @@ describe.each([ let compiler; let isDirectory; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler(webpackConfig); - instance = middleware(compiler, { - index: "default.html", - publicPath: "/", - }); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + index: "default.html", + publicPath: "/", + }, + ); isDirectory = jest .spyOn(instance.context.outputFileSystem, "statSync") @@ -3966,18 +4177,13 @@ describe.each([ }; }); - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - req = request(app); }); - afterAll((done) => { + afterAll(async () => { isDirectory.mockRestore(); - close(done); + await close(server, instance); }); it('should return the "404" code for the "GET" request to the public path', async () => { @@ -3992,7 +4198,7 @@ describe.each([ describe("should work", () => { let compiler; - beforeAll((done) => { + beforeAll(async () => { const outputPath = path.resolve( __dirname, "./outputs/modify-response-data", @@ -4006,18 +4212,18 @@ describe.each([ }, }); - instance = middleware(compiler, { - modifyResponseData: () => { - const result = Buffer.from("test"); + [server, app, instance] = await frameworkFactory( + name, + framework, + compiler, + { + modifyResponseData: () => { + const result = Buffer.from("test"); - return { data: result, byteLength: result.length }; + return { data: result, byteLength: result.length }; + }, }, - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + ); req = request(app); @@ -4030,8 +4236,8 @@ describe.each([ ); }); - afterAll((done) => { - close(done); + afterAll(async () => { + await close(server, instance); }); it("should modify file", async () => { diff --git a/types/index.d.ts b/types/index.d.ts index 57cde10bc..6cea92174 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -76,8 +76,8 @@ export = wdm; * @typedef {NormalizedHeaders | ((req: RequestInternal, res: ResponseInternal, context: Context) => void | undefined | NormalizedHeaders) | undefined} Headers */ /** - * @template {IncomingMessage} RequestInternal - * @template {ServerResponse} ResponseInternal + * @template {IncomingMessage} [RequestInternal = IncomingMessage] + * @template {ServerResponse} [ResponseInternal = ServerResponse] * @typedef {Object} Options * @property {{[key: string]: string}} [mimeTypes] * @property {string | undefined} [mimeTypeDefault] @@ -160,6 +160,7 @@ declare function wdm< ): API; declare namespace wdm { export { + hapiPlugin, Schema, Compiler, MultiCompiler, @@ -194,6 +195,9 @@ declare namespace wdm { API, WithOptional, WithoutUndefined, + HapiPluginBase, + HapiPlugin, + HapiOptions, }; } type Compiler = import("webpack").Compiler; @@ -203,6 +207,29 @@ type API< ResponseInternal extends ServerResponse, > = Middleware & AdditionalMethods; +/** + * @template S + * @template O + * @typedef {Object} HapiPluginBase + * @property {(server: S, options: O) => void | Promise} register + */ +/** + * @template S + * @template O + * @typedef {HapiPluginBase & { pkg: { name: string } }} HapiPlugin + */ +/** + * @typedef {Options & { compiler: Compiler | MultiCompiler }} HapiOptions + */ +/** + * @template HapiServer + * @template {HapiOptions} HapiOptionsInternal + * @returns {HapiPlugin} + */ +declare function hapiPlugin< + HapiServer, + HapiOptionsInternal extends HapiOptions, +>(): HapiPlugin; type Schema = import("schema-utils/declarations/validate").Schema; type Configuration = import("webpack").Configuration; type Stats = import("webpack").Stats; @@ -285,8 +312,9 @@ type Headers< ) => void | undefined | NormalizedHeaders) | undefined; type Options< - RequestInternal extends import("http").IncomingMessage, - ResponseInternal extends ServerResponse, + RequestInternal extends + import("http").IncomingMessage = import("http").IncomingMessage, + ResponseInternal extends ServerResponse = ServerResponse, > = { mimeTypes?: | { @@ -336,3 +364,14 @@ type WithOptional = Omit & Partial; type WithoutUndefined = T & { [P in K]: NonNullable; }; +type HapiPluginBase = { + register: (server: S, options: O) => void | Promise; +}; +type HapiPlugin = HapiPluginBase & { + pkg: { + name: string; + }; +}; +type HapiOptions = Options & { + compiler: Compiler | MultiCompiler; +}; From 458c17c372a2a1a5a33f8923998dba88d2644135 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Wed, 27 Mar 2024 17:04:30 +0300 Subject: [PATCH 06/16] feat: koa support (#1792) --- README.md | 23 +- package-lock.json | 224 ++++++++++++++++ package.json | 1 + src/index.js | 78 +++++- src/utils/compatibleAPI.js | 49 ++-- test/middleware.test.js | 475 +++++++++++++++------------------ types/index.d.ts | 29 +- types/utils/compatibleAPI.d.ts | 2 + 8 files changed, 585 insertions(+), 296 deletions(-) diff --git a/README.md b/README.md index 5a005d1de..720c6f4b2 100644 --- a/README.md +++ b/README.md @@ -579,6 +579,25 @@ app.use(devMiddleware(compiler, devMiddlewareOptions)); app.listen(3000, () => console.log("Example app listening on port 3000!")); ``` +### Koa + +```js +const Koa = require("koa"); +const webpack = require("webpack"); +const webpackConfig = require("./test/fixtures/webpack.simple.config"); +const middleware = require("./dist"); + +const compiler = webpack(webpackConfig); +const devMiddlewareOptions = { + /** Your webpack-dev-middleware-options */ +}; +const app = new Koa(); + +app.use(middleware.koaWrapper(compiler, devMiddlewareOptions)); + +app.listen(3000); +``` + ### Hapi ```js @@ -613,10 +632,6 @@ process.on("unhandledRejection", (err) => { }); ``` -### Koa - -Soon... - ### Fastify Fastify interop will require the use of `fastify-express` instead of `middie` for providing middleware support. As the authors of `fastify-express` recommend, this should only be used as a stopgap while full Fastify support is worked on. diff --git a/package-lock.json b/package-lock.json index 21597b884..64a6c2479 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "husky": "^9.0.10", "jest": "^29.3.1", "joi": "^17.12.2", + "koa": "^2.15.2", "lint-staged": "^15.2.0", "npm-run-all": "^4.1.5", "prettier": "^3.2.4", @@ -5551,6 +5552,19 @@ "node": ">= 0.8" } }, + "node_modules/cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "dev": true, + "dependencies": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -7277,6 +7291,19 @@ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "dev": true, + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/core-js-compat": { "version": "3.36.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.1.tgz", @@ -7893,6 +7920,12 @@ } } }, + "node_modules/deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", + "dev": true + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -8443,6 +8476,12 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -10802,6 +10841,53 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/http-assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", + "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", + "dev": true, + "dependencies": { + "deep-equal": "~1.0.1", + "http-errors": "~1.8.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-assert/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -11141,6 +11227,21 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -13252,6 +13353,18 @@ "node": "*" } }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dev": true, + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -13279,6 +13392,93 @@ "node": ">=6" } }, + "node_modules/koa": { + "version": "2.15.2", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.2.tgz", + "integrity": "sha512-MXTeZH3M6AJ8ukW2QZ8wqO3Dcdfh2WRRmjCBkEP+NhKNCiqlO5RDqHmSnsyNrbRJrdjyvIGSJho4vQiWgQJSVA==", + "dev": true, + "dependencies": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.9.0", + "debug": "^4.3.2", + "delegates": "^1.0.0", + "depd": "^2.0.0", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^2.0.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + }, + "engines": { + "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" + } + }, + "node_modules/koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "dev": true + }, + "node_modules/koa-convert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", + "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", + "dev": true, + "dependencies": { + "co": "^4.6.0", + "koa-compose": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/koa/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/http-errors/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -14340,6 +14540,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==", + "dev": true + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -16482,6 +16688,15 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "dev": true, + "engines": { + "node": ">=0.6.x" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -17221,6 +17436,15 @@ "node": ">=8" } }, + "node_modules/ylru": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.3.2.tgz", + "integrity": "sha512-RXRJzMiK6U2ye0BlGGZnmpwJDPgakn6aNQ0A7gHRbD4I0uvK4TW6UqkK1V0pp9jskjJBAXd3dRrbzWkqJ+6cxA==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 46fdf9ecc..2c21cff8d 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "husky": "^9.0.10", "jest": "^29.3.1", "joi": "^17.12.2", + "koa": "^2.15.2", "lint-staged": "^15.2.0", "npm-run-all": "^4.1.5", "prettier": "^3.2.4", diff --git a/src/index.js b/src/index.js index ba67ff858..305eaf3a4 100644 --- a/src/index.js +++ b/src/index.js @@ -183,8 +183,8 @@ const noop = () => {}; */ /** - * @template {IncomingMessage} RequestInternal - * @template {ServerResponse} ResponseInternal + * @template {IncomingMessage} [RequestInternal=IncomingMessage] + * @template {ServerResponse} [ResponseInternal=ServerResponse] * @param {Compiler | MultiCompiler} compiler * @param {Options} [options] * @returns {API} @@ -314,7 +314,7 @@ function wdm(compiler, options = {}) { * @template {HapiOptions} HapiOptionsInternal * @returns {HapiPlugin} */ -function hapiPlugin() { +function hapiWrapper() { return { pkg: { name: "webpack-dev-middleware", @@ -351,6 +351,76 @@ function hapiPlugin() { }; } -wdm.hapiPlugin = hapiPlugin; +wdm.hapiWrapper = hapiWrapper; + +/** + * @template {IncomingMessage} [RequestInternal=IncomingMessage] + * @template {ServerResponse} [ResponseInternal=ServerResponse] + * @param {Compiler | MultiCompiler} compiler + * @param {Options} [options] + * @returns {(ctx: any, next: Function) => Promise | void} + */ +function koaWrapper(compiler, options) { + const devMiddleware = wdm(compiler, options); + + /** + * @param {any} ctx + * @param {Function} next + * @returns {Promise} + */ + const wrapper = async function webpackDevMiddleware(ctx, next) { + return new Promise((resolve, reject) => { + const { req } = ctx; + const { res } = ctx; + + res.locals = ctx.state; + /** + * @param {number} status status code + */ + res.status = (status) => { + // eslint-disable-next-line no-param-reassign + ctx.status = status; + }; + /** + * @param {import("fs").ReadStream} stream readable stream + */ + res.pipeInto = (stream) => { + // eslint-disable-next-line no-param-reassign + ctx.body = stream; + resolve(); + }; + /** + * @param {string | Buffer} content content + */ + res.send = (content) => { + // eslint-disable-next-line no-param-reassign + ctx.body = content; + resolve(); + }; + + devMiddleware(req, res, (err) => { + if (err) { + reject(err); + return; + } + + resolve(next()); + }).catch((err) => { + // eslint-disable-next-line no-param-reassign + ctx.status = err.statusCode || err.status || 500; + // eslint-disable-next-line no-param-reassign + ctx.body = { + message: err.message, + }; + }); + }); + }; + + wrapper.devMiddleware = devMiddleware; + + return wrapper; +} + +wdm.koaWrapper = koaWrapper; module.exports = wdm; diff --git a/src/utils/compatibleAPI.js b/src/utils/compatibleAPI.js index 933572ec3..e6ee6c740 100644 --- a/src/utils/compatibleAPI.js +++ b/src/utils/compatibleAPI.js @@ -17,6 +17,7 @@ const escapeHtml = require("./escapeHtml"); * @property {(name: string, value: number | string | string[]) => void} set * @property {(status: number) => void} status * @property {(data: any) => void} send + * @property {(data: any) => void} [pipeInto] */ /** @@ -25,12 +26,14 @@ const escapeHtml = require("./escapeHtml"); * @returns {string[]} */ function getHeaderNames(res) { + // Pseudo API, TODO? if (typeof res.getHeaderNames !== "function") { // @ts-ignore // eslint-disable-next-line no-underscore-dangle return Object.keys(res._headers || {}); } + // Node.js API return res.getHeaderNames(); } @@ -128,6 +131,7 @@ function clearHeadersForResponse(res) { * @param {number} code */ function setStatusCode(res, code) { + // Pseudo API if ( typeof (/** @type {Response & ExpectedResponse} */ (res).status) === "function" @@ -138,6 +142,7 @@ function setStatusCode(res, code) { return; } + // Node.js API // eslint-disable-next-line no-param-reassign res.statusCode = code; } @@ -296,16 +301,6 @@ async function send(req, res, filename, start, end, goNext, options) { typeof (/** @type {import("fs").ReadStream} */ (bufferOrStream).pipe) === "function" ) { - setHeaderForResponse(res, "Content-Length", byteLength); - - if (req.method === "HEAD") { - res.end(); - return; - } - - /** @type {import("fs").ReadStream} */ - (bufferOrStream).pipe(res); - // Cleanup const cleanup = () => { destroyStream( @@ -314,10 +309,7 @@ async function send(req, res, filename, start, end, goNext, options) { ); }; - // Response finished, cleanup - onFinishedStream(res, cleanup); - - // error handling + // Error handling /** @type {import("fs").ReadStream} */ (bufferOrStream).on("error", (error) => { // clean up stream early @@ -336,10 +328,35 @@ async function send(req, res, filename, start, end, goNext, options) { } }); + setHeaderForResponse(res, "Content-Length", byteLength); + + // Pseudo API and Koa API + if ( + typeof (/** @type {Response & ExpectedResponse} */ (res).pipeInto) === + "function" + ) { + // Writable stream into Readable stream + /** @type {Response & ExpectedResponse} */ + (res).pipeInto(bufferOrStream); + } + // Node.js API and Express API and Hapi API + else { + /** @type {import("fs").ReadStream} */ + (bufferOrStream).pipe(res); + } + + if (req.method === "HEAD") { + res.end(); + return; + } + + // Response finished, cleanup + onFinishedStream(res, cleanup); + return; } - // Express API + // Pseudo API and Express API and Koa API if ( typeof (/** @type {Response & ExpectedResponse} */ (res).send) === "function" @@ -349,7 +366,7 @@ async function send(req, res, filename, start, end, goNext, options) { return; } - // Only Node.js API used + // Only Node.js API and Hapi API res.setHeader("Content-Length", byteLength); if (req.method === "HEAD") { diff --git a/test/middleware.test.js b/test/middleware.test.js index d920bd0cd..b96bc3a50 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -3,6 +3,7 @@ import path from "path"; import connect from "connect"; import express from "express"; +import koa from "koa"; import Hapi from "@hapi/hapi"; import request from "supertest"; import memfs, { createFsFromVolume, Volume } from "memfs"; @@ -45,7 +46,7 @@ async function frameworkFactory( case "hapi": { const server = framework.server(); const hapiPlugin = { - plugin: middleware.hapiPlugin(), + plugin: middleware.hapiWrapper(), options: { compiler, ...devMiddlewareOptions, @@ -71,7 +72,34 @@ async function frameworkFactory( await server.start(); - return [server, server.listener, server.webpackDevMiddleware]; + const req = request(server.listener); + + return [server, req, server.webpackDevMiddleware]; + } + case "koa": { + // eslint-disable-next-line new-cap + const app = new framework(); + const koaMiddleware = middleware.koaWrapper( + compiler, + devMiddlewareOptions, + ); + const middlewares = + typeof options.setupMiddlewares === "function" + ? options.setupMiddlewares([koaMiddleware]) + : [koaMiddleware]; + + for (const item of middlewares) { + if (item.route) { + app.use(item.route, item.fn); + } else { + app.use(item); + } + } + + const server = await startServer(app); + const req = request(server); + + return [server, req, koaMiddleware.devMiddleware]; } default: { const app = framework(); @@ -90,8 +118,9 @@ async function frameworkFactory( } const server = await startServer(app); + const req = request(app); - return [server, app, instance]; + return [server, req, instance]; } } } @@ -144,6 +173,8 @@ async function close(server, instance) { function get404ContentTypeHeader(name) { switch (name) { + case "koa": + return "text/plain; charset=utf-8"; case "hapi": return "application/json; charset=utf-8"; default: @@ -168,6 +199,15 @@ function applyTestMiddleware(name, middlewares) { }, }, }); + } else if (name === "koa") { + middlewares.push((ctx, next) => { + if (ctx.request.url === "/file.jpg") { + ctx.set("Content-Type", "text/html"); + ctx.body = "welcome"; + } + + next(); + }); } else { middlewares.push({ route: "/file.jpg", @@ -191,12 +231,12 @@ function applyTestMiddleware(name, middlewares) { describe.each([ ["connect", connect], ["express", express], + ["koa", koa], ["hapi", Hapi], ])("%s framework:", (name, framework) => { describe("middleware", () => { let instance; let server; - let app; let req; describe("basic", () => { @@ -218,7 +258,7 @@ describe.each([ codeContent = params.assets["bundle.js"].source(); }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -260,8 +300,6 @@ describe.each([ path.resolve(outputPath, "empty-file.txt"), "", ); - - req = request(app); }); afterAll(async () => { @@ -469,7 +507,7 @@ describe.each([ expect(response.text.length).toBe(501); }); - it('should return the "206" code for the "GET" request with the valid range header for "HEAD" request', async () => { + it('should return the "206" code for the "HEAD" request with the valid range header', async () => { const response = await req .head("/bundle.js") .set("Range", "bytes=3000-3500"); @@ -670,13 +708,11 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, ); - - req = request(app); }); afterAll(async () => { @@ -694,13 +730,11 @@ describe.each([ beforeAll(async () => { const compiler = getCompiler(webpackMultiConfig); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, ); - - req = request(app); }); afterAll(async () => { @@ -1159,14 +1193,12 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, ); - req = request(app); - const { context: { outputFileSystem: { mkdirSync, writeFileSync }, @@ -1219,7 +1251,7 @@ describe.each([ beforeAll(async () => { const compiler = getCompiler(webpackConfig); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -1227,7 +1259,16 @@ describe.each([ undefined, { setupMiddlewares: (middlewares) => { - if (name === "hapi") { + if (name === "koa") { + middlewares.unshift(async (ctx, next) => { + ctx.set( + "Content-Type", + "application/vnd.test+octet-stream", + ); + + await next(); + }); + } else if (name === "hapi") { middlewares.unshift({ plugin: { name: "myPlugin", @@ -1269,8 +1310,6 @@ describe.each([ }, }, ); - - req = request(app); }); afterAll(async () => { @@ -1292,13 +1331,11 @@ describe.each([ // eslint-disable-next-line no-undefined const compiler = getCompiler({ ...webpackConfig, output: undefined }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, ); - - req = request(app); }); afterAll(async () => { @@ -1346,13 +1383,11 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, ); - - req = request(app); }); afterAll(async () => { @@ -1394,13 +1429,11 @@ describe.each([ beforeAll(async () => { const compiler = getCompiler(webpackConfig); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, ); - - req = request(app); }); afterAll(async () => { @@ -1449,13 +1482,11 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, ); - - req = request(app); }); afterAll(async () => { @@ -1518,7 +1549,7 @@ describe.each([ hash = h; }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -1533,8 +1564,6 @@ describe.each([ } }, 10); }); - - req = request(app); }); afterAll(async () => { @@ -1614,7 +1643,7 @@ describe.each([ hashTwo = two.hash; }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -1629,8 +1658,6 @@ describe.each([ } }, 10); }); - - req = request(app); }); afterAll(async () => { @@ -1708,13 +1735,11 @@ describe.each([ beforeAll(async () => { const compiler = getCompiler(webpackMultiConfig); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, ); - - req = request(app); }); afterAll(async () => { @@ -1809,13 +1834,11 @@ describe.each([ }, ]); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, ); - - req = request(app); }); afterAll(async () => { @@ -1892,13 +1915,11 @@ describe.each([ }, ]); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, ); - - req = request(app); }); afterAll(async () => { @@ -1985,13 +2006,11 @@ describe.each([ beforeAll(async () => { const compiler = getCompiler(webpackClientServerConfig); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, ); - - req = request(app); }); afterAll(async () => { @@ -2045,13 +2064,11 @@ describe.each([ webpackClientServerConfig[0], ]); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, ); - - req = request(app); }); afterAll(async () => { @@ -2115,13 +2132,11 @@ describe.each([ }, ]); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, ); - - req = request(app); }); afterAll(async () => { @@ -2179,13 +2194,11 @@ describe.each([ } }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, ); - - req = request(app); }); afterAll(async () => { @@ -2216,7 +2229,7 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -2243,8 +2256,6 @@ describe.each([ return brokenStream; }; - - req = request(app); }); afterAll(async () => { @@ -2290,7 +2301,7 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -2321,8 +2332,6 @@ describe.each([ return brokenStream; }; - - req = request(app); }); afterAll(async () => { @@ -2372,7 +2381,7 @@ describe.each([ codeContent = params.assets["bundle.js"].source(); }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -2387,8 +2396,6 @@ describe.each([ ); instance.context.outputFileSystem.createReadStream = null; - - req = request(app); }); afterAll(async () => { @@ -2451,14 +2458,12 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, ); - req = request(app); - instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, }); @@ -2494,7 +2499,7 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -2505,8 +2510,6 @@ describe.each([ }, ); - req = request(app); - instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, }); @@ -2542,7 +2545,7 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -2553,8 +2556,6 @@ describe.each([ }, ); - req = request(app); - instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, }); @@ -2589,7 +2590,7 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -2606,8 +2607,6 @@ describe.each([ }, }, ); - - req = request(app); }); afterAll(async () => { @@ -2635,7 +2634,7 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -2644,8 +2643,6 @@ describe.each([ }, ); - req = request(app); - instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, }); @@ -2680,13 +2677,11 @@ describe.each([ compiler = getCompiler(webpackConfig); spy = jest.spyOn(compiler, "watch"); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, ); - - req = request(app); }); afterAll(async () => { @@ -2713,13 +2708,11 @@ describe.each([ spy = jest.spyOn(compiler, "watch"); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, ); - - req = request(app); }); afterAll(async () => { @@ -2749,13 +2742,11 @@ describe.each([ spy = jest.spyOn(compiler, "watch"); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, ); - - req = request(app); }); afterAll(async () => { @@ -2795,14 +2786,12 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, { writeToDisk: true }, ); - - req = request(app); }); afterAll(async () => { @@ -2814,40 +2803,38 @@ describe.each([ }); it("should find the bundle file on disk", (done) => { - request(app) - .get("/public/bundle.js") - .expect(200, (error) => { - if (error) { - return done(error); - } + req.get("/public/bundle.js").expect(200, (error) => { + if (error) { + return done(error); + } - const bundlePath = path.resolve( - __dirname, - "./outputs/write-to-disk-true/bundle.js", - ); + const bundlePath = path.resolve( + __dirname, + "./outputs/write-to-disk-true/bundle.js", + ); - expect( - compiler.hooks.assetEmitted.taps.filter( - (hook) => hook.name === "DevMiddleware", - ).length, - ).toBe(1); - expect(fs.existsSync(bundlePath)).toBe(true); + expect( + compiler.hooks.assetEmitted.taps.filter( + (hook) => hook.name === "DevMiddleware", + ).length, + ).toBe(1); + expect(fs.existsSync(bundlePath)).toBe(true); - instance.invalidate(); + instance.invalidate(); - return compiler.hooks.done.tap( - "DevMiddlewareWriteToDiskTest", - () => { - expect( - compiler.hooks.assetEmitted.taps.filter( - (hook) => hook.name === "DevMiddleware", - ).length, - ).toBe(1); + return compiler.hooks.done.tap( + "DevMiddlewareWriteToDiskTest", + () => { + expect( + compiler.hooks.assetEmitted.taps.filter( + (hook) => hook.name === "DevMiddleware", + ).length, + ).toBe(1); - done(); - }, - ); - }); + done(); + }, + ); + }); }); it("should not allow to get files above root", async () => { @@ -2888,7 +2875,7 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -2899,8 +2886,6 @@ describe.each([ recursive: true, }); fs.writeFileSync(path.resolve(outputPath, "test.json"), "{}"); - - req = request(app); }); afterAll(async () => { @@ -2910,41 +2895,39 @@ describe.each([ }); it("should find the bundle file on disk", (done) => { - request(app) - .get("/bundle.js") - .expect(200, (error) => { - if (error) { - return done(error); - } + req.get("/bundle.js").expect(200, (error) => { + if (error) { + return done(error); + } - const bundlePath = path.resolve(outputPath, "bundle.js"); + const bundlePath = path.resolve(outputPath, "bundle.js"); - expect(fs.existsSync(path.resolve(outputPath, "test.json"))).toBe( - false, - ); + expect(fs.existsSync(path.resolve(outputPath, "test.json"))).toBe( + false, + ); - expect( - compiler.hooks.assetEmitted.taps.filter( - (hook) => hook.name === "DevMiddleware", - ).length, - ).toBe(1); - expect(fs.existsSync(bundlePath)).toBe(true); + expect( + compiler.hooks.assetEmitted.taps.filter( + (hook) => hook.name === "DevMiddleware", + ).length, + ).toBe(1); + expect(fs.existsSync(bundlePath)).toBe(true); - instance.invalidate(); + instance.invalidate(); - return compiler.hooks.done.tap( - "DevMiddlewareWriteToDiskTest", - () => { - expect( - compiler.hooks.assetEmitted.taps.filter( - (hook) => hook.name === "DevMiddleware", - ).length, - ).toBe(1); + return compiler.hooks.done.tap( + "DevMiddlewareWriteToDiskTest", + () => { + expect( + compiler.hooks.assetEmitted.taps.filter( + (hook) => hook.name === "DevMiddleware", + ).length, + ).toBe(1); - done(); - }, - ); - }); + done(); + }, + ); + }); }); }); @@ -2960,7 +2943,7 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -2973,40 +2956,38 @@ describe.each([ }); it("should not find the bundle file on disk", (done) => { - request(app) - .get("/bundle.js") - .expect(200, (error) => { - if (error) { - return done(error); - } - - const bundlePath = path.resolve( - __dirname, - "./outputs/write-to-disk-false/bundle.js", - ); - - expect( - compiler.hooks.assetEmitted.taps.filter( - (hook) => hook.name === "DevMiddleware", - ).length, - ).toBe(0); - expect(fs.existsSync(bundlePath)).toBe(false); - - instance.invalidate(); + req.get("/bundle.js").expect(200, (error) => { + if (error) { + return done(error); + } - return compiler.hooks.done.tap( - "DevMiddlewareWriteToDiskTest", - () => { - expect( - compiler.hooks.assetEmitted.taps.filter( - (hook) => hook.name === "DevMiddleware", - ).length, - ).toBe(0); + const bundlePath = path.resolve( + __dirname, + "./outputs/write-to-disk-false/bundle.js", + ); - done(); - }, - ); - }); + expect( + compiler.hooks.assetEmitted.taps.filter( + (hook) => hook.name === "DevMiddleware", + ).length, + ).toBe(0); + expect(fs.existsSync(bundlePath)).toBe(false); + + instance.invalidate(); + + return compiler.hooks.done.tap( + "DevMiddlewareWriteToDiskTest", + () => { + expect( + compiler.hooks.assetEmitted.taps.filter( + (hook) => hook.name === "DevMiddleware", + ).length, + ).toBe(0); + + done(); + }, + ); + }); }); }); @@ -3025,7 +3006,7 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -3033,8 +3014,6 @@ describe.each([ writeToDisk: (filePath) => /bundle\.js$/.test(filePath), }, ); - - req = request(app); }); afterAll(async () => { @@ -3077,7 +3056,7 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -3085,8 +3064,6 @@ describe.each([ writeToDisk: (filePath) => !/bundle\.js$/.test(filePath), }, ); - - req = request(app); }); afterAll(async () => { @@ -3129,14 +3106,12 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, { writeToDisk: true }, ); - - req = request(app); }); afterAll(async () => { @@ -3193,14 +3168,12 @@ describe.each([ }, ]); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, { writeToDisk: true }, ); - - req = request(app); }); afterAll(async () => { @@ -3260,7 +3233,7 @@ describe.each([ hash = h; }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -3276,8 +3249,6 @@ describe.each([ } }, 10); }); - - req = request(app); }); afterAll(async () => { @@ -3309,7 +3280,7 @@ describe.each([ beforeAll(async () => { compiler = getCompiler(webpackConfig); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -3318,8 +3289,6 @@ describe.each([ publicPath: "/public/", }, ); - - req = request(app); }); afterAll(async () => { @@ -3350,7 +3319,7 @@ describe.each([ beforeEach(async () => { const compiler = getCompiler(webpackConfig); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -3365,8 +3334,6 @@ describe.each([ }, }, ); - - req = request(app); }); afterEach(async () => { @@ -3382,7 +3349,7 @@ describe.each([ }); it('should return the "200" code for the "GET" request to path not in outputFileSystem but not return headers', async () => { - const res = await request(app).get("/file.jpg"); + const res = await req.get("/file.jpg"); expect(res.statusCode).toEqual(200); expect(res.headers["X-nonsense-1"]).toBeUndefined(); expect(res.headers["X-nonsense-2"]).toBeUndefined(); @@ -3393,7 +3360,7 @@ describe.each([ beforeEach(async () => { const compiler = getCompiler(webpackConfig); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -3417,8 +3384,6 @@ describe.each([ }, }, ); - - req = request(app); }); afterEach(async () => { @@ -3434,7 +3399,7 @@ describe.each([ }); it('should return the "200" code for the "GET" request to path not in outputFileSystem but not return headers', async () => { - const res = await request(app).get("/file.jpg"); + const res = await req.get("/file.jpg"); expect(res.statusCode).toEqual(200); expect(res.headers["x-foo"]).toBeUndefined(); expect(res.headers["x-bar"]).toBeUndefined(); @@ -3445,7 +3410,7 @@ describe.each([ beforeEach(async () => { const compiler = getCompiler(webpackConfig); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -3462,8 +3427,6 @@ describe.each([ }, }, ); - - req = request(app); }); afterEach(async () => { @@ -3490,7 +3453,7 @@ describe.each([ beforeEach(async () => { const compiler = getCompiler(webpackConfig); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -3514,8 +3477,6 @@ describe.each([ }, }, ); - - req = request(app); }); afterEach(async () => { @@ -3542,7 +3503,7 @@ describe.each([ beforeEach(async () => { const compiler = getCompiler(webpackConfig); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -3561,8 +3522,6 @@ describe.each([ }, }, ); - - req = request(app); }); afterEach(async () => { @@ -3591,14 +3550,12 @@ describe.each([ beforeAll(async () => { const compiler = getCompiler(webpackConfig); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, { publicPath: "/public/" }, ); - - req = request(app); }); afterAll(async () => { @@ -3616,14 +3573,12 @@ describe.each([ beforeAll(async () => { const compiler = getCompiler(webpackConfig); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, { publicPath: "auto" }, ); - - req = request(app); }); afterAll(async () => { @@ -3644,14 +3599,22 @@ describe.each([ beforeAll(async () => { const compiler = getCompiler(webpackConfig); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, { serverSideRender: true }, { setupMiddlewares: (middlewares) => { - if (name === "hapi") { + if (name === "koa") { + middlewares.push(async (ctx, next) => { + locals = ctx.state; + + ctx.status = 200; + + await next(); + }); + } else if (name === "hapi") { middlewares.push({ plugin: { name: "myPlugin", @@ -3692,8 +3655,6 @@ describe.each([ }, }, ); - - req = request(app); }); afterAll(async () => { @@ -3715,7 +3676,7 @@ describe.each([ beforeAll(async () => { compiler = getCompiler(webpackConfig); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -3747,7 +3708,7 @@ describe.each([ configuredFs.join = path.join.bind(path); configuredFs.mkdirp = () => {}; - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -3784,7 +3745,7 @@ describe.each([ configuredFs.join = path.join.bind(path); configuredFs.mkdirp = () => {}; - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -3822,7 +3783,7 @@ describe.each([ configuredFs.join = path.join.bind(path); configuredFs.mkdirp = () => {}; - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -3861,14 +3822,12 @@ describe.each([ beforeAll(async () => { const compiler = getCompiler(webpackConfig); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, { index: false, publicPath: "/" }, ); - - req = request(app); }); afterAll(async () => { @@ -3898,14 +3857,12 @@ describe.each([ beforeAll(async () => { const compiler = getCompiler(webpackConfig); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, { index: true, publicPath: "/" }, ); - - req = request(app); }); afterAll(async () => { @@ -3942,7 +3899,7 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -3952,8 +3909,6 @@ describe.each([ }, ); - req = request(app); - instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, }); @@ -3988,7 +3943,7 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -3998,8 +3953,6 @@ describe.each([ }, ); - req = request(app); - instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, }); @@ -4031,7 +3984,7 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -4044,8 +3997,6 @@ describe.each([ }, ); - req = request(app); - instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, }); @@ -4080,15 +4031,13 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, { index: "noextension" }, ); - req = request(app); - instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, }); @@ -4120,7 +4069,7 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -4130,8 +4079,6 @@ describe.each([ }, ); - req = request(app); - instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, }); @@ -4158,7 +4105,7 @@ describe.each([ beforeAll(async () => { compiler = getCompiler(webpackConfig); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -4176,8 +4123,6 @@ describe.each([ isDirectory: () => false, }; }); - - req = request(app); }); afterAll(async () => { @@ -4212,7 +4157,7 @@ describe.each([ }, }); - [server, app, instance] = await frameworkFactory( + [server, req, instance] = await frameworkFactory( name, framework, compiler, @@ -4225,8 +4170,6 @@ describe.each([ }, ); - req = request(app); - instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, }); diff --git a/types/index.d.ts b/types/index.d.ts index 6cea92174..ee6f4c9e0 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -145,22 +145,24 @@ export = wdm; * @typedef {T & { [P in K]: NonNullable }} WithoutUndefined */ /** - * @template {IncomingMessage} RequestInternal - * @template {ServerResponse} ResponseInternal + * @template {IncomingMessage} [RequestInternal=IncomingMessage] + * @template {ServerResponse} [ResponseInternal=ServerResponse] * @param {Compiler | MultiCompiler} compiler * @param {Options} [options] * @returns {API} */ declare function wdm< - RequestInternal extends import("http").IncomingMessage, - ResponseInternal extends ServerResponse, + RequestInternal extends + import("http").IncomingMessage = import("http").IncomingMessage, + ResponseInternal extends ServerResponse = ServerResponse, >( compiler: Compiler | MultiCompiler, options?: Options | undefined, ): API; declare namespace wdm { export { - hapiPlugin, + hapiWrapper, + koaWrapper, Schema, Compiler, MultiCompiler, @@ -226,10 +228,25 @@ type API< * @template {HapiOptions} HapiOptionsInternal * @returns {HapiPlugin} */ -declare function hapiPlugin< +declare function hapiWrapper< HapiServer, HapiOptionsInternal extends HapiOptions, >(): HapiPlugin; +/** + * @template {IncomingMessage} [RequestInternal=IncomingMessage] + * @template {ServerResponse} [ResponseInternal=ServerResponse] + * @param {Compiler | MultiCompiler} compiler + * @param {Options} [options] + * @returns {(ctx: any, next: Function) => Promise | void} + */ +declare function koaWrapper< + RequestInternal extends + import("http").IncomingMessage = import("http").IncomingMessage, + ResponseInternal extends ServerResponse = ServerResponse, +>( + compiler: Compiler | MultiCompiler, + options?: Options | undefined, +): (ctx: any, next: Function) => Promise | void; type Schema = import("schema-utils/declarations/validate").Schema; type Configuration = import("webpack").Configuration; type Stats = import("webpack").Stats; diff --git a/types/utils/compatibleAPI.d.ts b/types/utils/compatibleAPI.d.ts index f2af44ce3..88844c4bd 100644 --- a/types/utils/compatibleAPI.d.ts +++ b/types/utils/compatibleAPI.d.ts @@ -10,6 +10,7 @@ export type ExpectedResponse = { set: (name: string, value: number | string | string[]) => void; status: (status: number) => void; send: (data: any) => void; + pipeInto?: ((data: any) => void) | undefined; }; /** * send error options @@ -46,6 +47,7 @@ export type SendOptions< * @property {(name: string, value: number | string | string[]) => void} set * @property {(status: number) => void} status * @property {(data: any) => void} send + * @property {(data: any) => void} [pipeInto] */ /** * @template {ServerResponse} Response From 8203d4c982ec43428b936e7369f92b00c0da60c7 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Wed, 27 Mar 2024 17:53:25 +0300 Subject: [PATCH 07/16] test: fastify test (#1794) --- README.md | 2 +- package-lock.json | 482 ++++++++++++++++++++++++++++++++++++++++ package.json | 2 + test/middleware.test.js | 26 ++- 4 files changed, 506 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 720c6f4b2..52f751300 100644 --- a/README.md +++ b/README.md @@ -648,7 +648,7 @@ const devMiddlewareOptions = { }; (async () => { - await fastify.register(require("fastify-express")); + await fastify.register(require("@fastify/express")); await fastify.use(devMiddleware(compiler, devMiddlewareOptions)); await fastify.listen(3000); })(); diff --git a/package-lock.json b/package-lock.json index 64a6c2479..3d44e2115 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@babel/preset-env": "^7.16.7", "@commitlint/cli": "^19.0.3", "@commitlint/config-conventional": "^19.0.3", + "@fastify/express": "^2.3.0", "@hapi/hapi": "^21.3.7", "@types/connect": "^3.4.35", "@types/express": "^4.17.13", @@ -42,6 +43,7 @@ "eslint-plugin-import": "^2.25.4", "execa": "^5.1.1", "express": "^4.17.1", + "fastify": "^4.26.2", "file-loader": "^6.2.0", "husky": "^9.0.10", "jest": "^29.3.1", @@ -3102,6 +3104,57 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fastify/ajv-compiler": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.5.0.tgz", + "integrity": "sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==", + "dev": true, + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==", + "dev": true + }, + "node_modules/@fastify/express": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@fastify/express/-/express-2.3.0.tgz", + "integrity": "sha512-jvvjlPPCfJsSHfF6tQDyARJ3+c3xXiqcxVZu6bi3xMWCWB3fl07vrjFDeaqnwqKhLZ9+m6cog5dw7gIMKEsTnQ==", + "dev": true, + "dependencies": { + "express": "^4.17.1", + "fastify-plugin": "^4.0.0" + } + }, + "node_modules/@fastify/express/node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==", + "dev": true + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "dev": true, + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, "node_modules/@hapi/accept": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@hapi/accept/-/accept-6.0.3.tgz", @@ -4860,6 +4913,24 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "dev": true + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -5013,6 +5084,12 @@ "node": ">= 8" } }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -5184,6 +5261,15 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -5199,6 +5285,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/avvio": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.3.0.tgz", + "integrity": "sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==", + "dev": true, + "dependencies": { + "@fastify/error": "^3.3.0", + "archy": "^1.0.0", + "debug": "^4.0.0", + "fastq": "^1.17.1" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -5414,6 +5512,26 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -5537,6 +5655,30 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -9405,6 +9547,15 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -9543,6 +9694,18 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/fast-content-type-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==", + "dev": true + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "dev": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -9579,18 +9742,124 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "node_modules/fast-json-stringify": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.13.0.tgz", + "integrity": "sha512-XjTDWKHP3GoMQUOfnjYUbqeHeEt+PvYgvBdG2fRSmYaORILbSr8xTJvZX+w1YSAP5pw2NwKrGRmQleYueZEoxw==", + "dev": true, + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^2.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "dev": true, + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "dev": true }, + "node_modules/fast-uri": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.3.0.tgz", + "integrity": "sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==", + "dev": true + }, + "node_modules/fastify": { + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.26.2.tgz", + "integrity": "sha512-90pjTuPGrfVKtdpLeLzND5nyC4woXZN5VadiNQCicj/iJU4viNHKhsAnb7jmv1vu2IzkLXyBiCzdWuzeXgQ5Ug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.3.0", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^8.0.0", + "light-my-request": "^5.11.0", + "pino": "^8.17.0", + "process-warning": "^3.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "node_modules/fastify/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fastify/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fastify/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -9771,6 +10040,20 @@ "node": ">= 0.6" } }, + "node_modules/find-my-way": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.1.0.tgz", + "integrity": "sha512-41QwjCGcVTODUmLLqTMeoHeiozbMXYMAE1CKFiDyi9zVZ2Vjh0yz3MF0WQZoIb+cmzP/XlbFjlF2NtJmvZHznA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/find-up": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", @@ -10940,6 +11223,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -13299,6 +13602,15 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -13501,6 +13813,17 @@ "node": ">= 0.8.0" } }, + "node_modules/light-my-request": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.12.0.tgz", + "integrity": "sha512-P526OX6E7aeCIfw/9UyJNsAISfcFETghysaWHQAlQYayynShT08MOj4c6fBCvTWBrHXSvqBAKDp3amUPSCQI4w==", + "dev": true, + "dependencies": { + "cookie": "^0.6.0", + "process-warning": "^3.0.0", + "set-cookie-parser": "^2.4.1" + } + }, "node_modules/lilconfig": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", @@ -14505,6 +14828,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -14770,6 +15102,60 @@ "node": ">=6" } }, + "node_modules/pino": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.19.0.tgz", + "integrity": "sha512-oswmokxkav9bADfJ2ifrvfHUwad6MLp73Uat0IkQWY3iAw5xTRoznXbXksZs8oaOUMpmhVWD+PZogNzllWpJaA==", + "dev": true, + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "dev": true, + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==", + "dev": true + }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -14911,12 +15297,27 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "dev": true + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -15012,6 +15413,12 @@ } ] }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "dev": true + }, "node_modules/quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -15200,6 +15607,15 @@ "node": ">=8.10.0" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -15391,6 +15807,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -15500,6 +15925,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-regex2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", + "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", + "dev": true, + "dependencies": { + "ret": "~0.2.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -15524,6 +15967,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "dev": true + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -15602,6 +16051,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", + "dev": true + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -15737,6 +16192,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/sonic-boom": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.0.tgz", + "integrity": "sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==", + "dev": true, + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -16590,6 +17054,15 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/thread-stream": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", + "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "dev": true, + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -16632,6 +17105,15 @@ "node": ">=8.0" } }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", diff --git a/package.json b/package.json index 2c21cff8d..836d95b2f 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@babel/preset-env": "^7.16.7", "@commitlint/cli": "^19.0.3", "@commitlint/config-conventional": "^19.0.3", + "@fastify/express": "^2.3.0", "@hapi/hapi": "^21.3.7", "@types/connect": "^3.4.35", "@types/express": "^4.17.13", @@ -86,6 +87,7 @@ "eslint-plugin-import": "^2.25.4", "execa": "^5.1.1", "express": "^4.17.1", + "fastify": "^4.26.2", "file-loader": "^6.2.0", "husky": "^9.0.10", "jest": "^29.3.1", diff --git a/test/middleware.test.js b/test/middleware.test.js index b96bc3a50..9df969be3 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -3,6 +3,7 @@ import path from "path"; import connect from "connect"; import express from "express"; +import fastify from "fastify"; import koa from "koa"; import Hapi from "@hapi/hapi"; import request from "supertest"; @@ -25,7 +26,7 @@ global.console.log = jest.fn(); async function startServer(app) { return new Promise((resolve, reject) => { - const server = app.listen((error) => { + const server = app.listen({ port: 3000 }, (error) => { if (error) { return reject(error); } @@ -102,7 +103,14 @@ async function frameworkFactory( return [server, req, koaMiddleware.devMiddleware]; } default: { + const isFastify = name === "fastify"; const app = framework(); + + if (isFastify) { + // eslint-disable-next-line global-require + await app.register(require("@fastify/express")); + } + const instance = middleware(compiler, devMiddlewareOptions); const middlewares = typeof options.setupMiddlewares === "function" @@ -117,10 +125,14 @@ async function frameworkFactory( } } + if (isFastify) { + await app.ready(); + } + const server = await startServer(app); - const req = request(app); + const req = isFastify ? request(app.server) : request(app); - return [server, req, instance]; + return [isFastify ? app.server : server, req, instance]; } } } @@ -177,6 +189,8 @@ function get404ContentTypeHeader(name) { return "text/plain; charset=utf-8"; case "hapi": return "application/json; charset=utf-8"; + case "fastify": + return "application/json; charset=utf-8"; default: return "text/html; charset=utf-8"; } @@ -231,6 +245,7 @@ function applyTestMiddleware(name, middlewares) { describe.each([ ["connect", connect], ["express", express], + ["fastify", fastify], ["koa", koa], ["hapi", Hapi], ])("%s framework:", (name, framework) => { @@ -953,12 +968,13 @@ describe.each([ ], }, { - file: "/%foo%/%foo%.js", + // fastify uses the `frameworkErrors` option to handle broken URLs + file: name === "fastify" ? "/foo/foo.js" : "/%foo%/%foo%.js", data: 'console.log("foo");', urls: [ // Filenames can contain characters not allowed in URIs { - value: "%foo%/%foo%.js", + value: name === "fastify" ? "foo/foo.js" : "%foo%/%foo%.js", contentType: "application/javascript; charset=utf-8", code: 200, }, From c64748ea2ef29d609981b00341145c2a11a235f8 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Wed, 27 Mar 2024 18:25:24 +0300 Subject: [PATCH 08/16] refactor: avoid unnecessary inner pseudo API (#1795) --- src/middleware.js | 51 +++++------- src/utils/compatibleAPI.js | 142 +++++---------------------------- types/utils/compatibleAPI.d.ts | 48 +---------- 3 files changed, 44 insertions(+), 197 deletions(-) diff --git a/src/middleware.js b/src/middleware.js index 634ce77e3..a89801298 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -3,14 +3,7 @@ const path = require("path"); const mime = require("mime-types"); const getFilenameFromUrl = require("./utils/getFilenameFromUrl"); -const { - getHeaderFromRequest, - getHeaderFromResponse, - setHeaderForResponse, - setStatusCode, - send, - sendError, -} = require("./utils/compatibleAPI"); +const { setStatusCode, send, sendError } = require("./utils/compatibleAPI"); const ready = require("./utils/ready"); /** @typedef {import("./index.js").NextFunction} NextFunction */ @@ -44,14 +37,6 @@ function wrapper(context) { // eslint-disable-next-line no-param-reassign res.locals = res.locals || {}; - if (req.method && !acceptedMethods.includes(req.method)) { - await goNext(); - - return; - } - - ready(context, processRequest, req); - async function goNext() { if (!context.options.serverSideRender) { return next(); @@ -72,6 +57,12 @@ function wrapper(context) { }); } + if (req.method && !acceptedMethods.includes(req.method)) { + await goNext(); + + return; + } + async function processRequest() { /** @type {import("./utils/getFilenameFromUrl").Extra} */ const extra = {}; @@ -121,34 +112,28 @@ function wrapper(context) { } headers.forEach((header) => { - setHeaderForResponse(res, header.key, header.value); + res.setHeader(header.key, header.value); }); } - if (!getHeaderFromResponse(res, "Content-Type")) { + if (!res.getHeader("Content-Type")) { // content-type name(like application/javascript; charset=utf-8) or false const contentType = mime.contentType(path.extname(filename)); // Only set content-type header if media type is known // https://tools.ietf.org/html/rfc7231#section-3.1.1.5 if (contentType) { - setHeaderForResponse(res, "Content-Type", contentType); + res.setHeader("Content-Type", contentType); } else if (context.options.mimeTypeDefault) { - setHeaderForResponse( - res, - "Content-Type", - context.options.mimeTypeDefault, - ); + res.setHeader("Content-Type", context.options.mimeTypeDefault); } } - if (!getHeaderFromResponse(res, "Accept-Ranges")) { - setHeaderForResponse(res, "Accept-Ranges", "bytes"); + if (!res.getHeader("Accept-Ranges")) { + res.setHeader("Accept-Ranges", "bytes"); } - const rangeHeader = - /** @type {string} */ - (getHeaderFromRequest(req, "range")); + const rangeHeader = /** @type {string} */ (req.headers.range); let len = /** @type {import("fs").Stats} */ (extra.stats).size; let offset = 0; @@ -162,8 +147,7 @@ function wrapper(context) { if (parsedRanges === -1) { context.logger.error("Unsatisfiable range for 'Range' header."); - setHeaderForResponse( - res, + res.setHeader( "Content-Range", getValueContentRangeHeader("bytes", len), ); @@ -189,8 +173,7 @@ function wrapper(context) { if (parsedRanges !== -2 && parsedRanges.length === 1) { // Content-Range setStatusCode(res, 206); - setHeaderForResponse( - res, + res.setHeader( "Content-Range", getValueContentRangeHeader( "bytes", @@ -212,6 +195,8 @@ function wrapper(context) { outputFileSystem: context.outputFileSystem, }); } + + ready(context, processRequest, req); }; } diff --git a/src/utils/compatibleAPI.js b/src/utils/compatibleAPI.js index e6ee6c740..1ddbfe861 100644 --- a/src/utils/compatibleAPI.js +++ b/src/utils/compatibleAPI.js @@ -13,118 +13,11 @@ const escapeHtml = require("./escapeHtml"); /** * @typedef {Object} ExpectedResponse - * @property {(name: string) => string | string[] | undefined} get - * @property {(name: string, value: number | string | string[]) => void} set - * @property {(status: number) => void} status - * @property {(data: any) => void} send + * @property {(status: number) => void} [status] + * @property {(data: any) => void} [send] * @property {(data: any) => void} [pipeInto] */ -/** - * @template {ServerResponse} Response - * @param {Response} res - * @returns {string[]} - */ -function getHeaderNames(res) { - // Pseudo API, TODO? - if (typeof res.getHeaderNames !== "function") { - // @ts-ignore - // eslint-disable-next-line no-underscore-dangle - return Object.keys(res._headers || {}); - } - - // Node.js API - return res.getHeaderNames(); -} - -/** - * @template {IncomingMessage} Request - * @param {Request} req - * @param {string} name - * @returns {string | string[] | undefined} - */ -function getHeaderFromRequest(req, name) { - // Express API - if ( - typeof (/** @type {Request & ExpectedRequest} */ (req).get) === "function" - ) { - return /** @type {Request & ExpectedRequest} */ (req).get(name); - } - - // Node.js API - return req.headers[name]; -} - -/** - * @template {ServerResponse} Response - * @param {Response} res - * @param {string} name - * @returns {number | string | string[] | undefined} - */ -function getHeaderFromResponse(res, name) { - // Express API - if ( - typeof (/** @type {Response & ExpectedResponse} */ (res).get) === "function" - ) { - return /** @type {Response & ExpectedResponse} */ (res).get(name); - } - - // Node.js API - return res.getHeader(name); -} - -/** - * @template {ServerResponse} Response - * @param {Response} res - * @param {string} name - * @param {number | string | string[]} value - * @returns {void} - */ -function setHeaderForResponse(res, name, value) { - // Express API - if ( - typeof (/** @type {Response & ExpectedResponse} */ (res).set) === "function" - ) { - /** @type {Response & ExpectedResponse} */ - (res).set(name, typeof value === "number" ? String(value) : value); - - return; - } - - // Node.js API - res.setHeader(name, value); -} - -/** - * @template {ServerResponse} Response - * @param {Response} res - * @param {Record} headers - */ -function setHeadersForResponse(res, headers) { - const keys = Object.keys(headers); - - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const value = headers[key]; - - if (typeof value !== "undefined") { - setHeaderForResponse(res, key, value); - } - } -} - -/** - * @template {ServerResponse} Response - * @param {Response} res - */ -function clearHeadersForResponse(res) { - const headers = getHeaderNames(res); - - for (let i = 0; i < headers.length; i++) { - res.removeHeader(headers[i]); - } -} - /** * @template {ServerResponse} Response * @param {Response} res @@ -212,17 +105,30 @@ function sendError(req, res, status, options) { `; // Clear existing headers - clearHeadersForResponse(res); + const headers = res.getHeaderNames(); + + for (let i = 0; i < headers.length; i++) { + res.removeHeader(headers[i]); + } if (options && options.headers) { - setHeadersForResponse(res, options.headers); + const keys = Object.keys(options.headers); + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = options.headers[key]; + + if (typeof value !== "undefined") { + res.setHeader(key, value); + } + } } // Send basic response setStatusCode(res, status); - setHeaderForResponse(res, "Content-Type", "text/html; charset=utf-8"); - setHeaderForResponse(res, "Content-Security-Policy", "default-src 'none'"); - setHeaderForResponse(res, "X-Content-Type-Options", "nosniff"); + res.setHeader("Content-Type", "text/html; charset=utf-8"); + res.setHeader("Content-Security-Policy", "default-src 'none'"); + res.setHeader("X-Content-Type-Options", "nosniff"); let byteLength = Buffer.byteLength(document); @@ -232,7 +138,7 @@ function sendError(req, res, status, options) { (options.modifyResponseData(req, res, document, byteLength))); } - setHeaderForResponse(res, "Content-Length", byteLength); + res.setHeader("Content-Length", byteLength); res.end(document); } @@ -328,7 +234,7 @@ async function send(req, res, filename, start, end, goNext, options) { } }); - setHeaderForResponse(res, "Content-Length", byteLength); + res.setHeader("Content-Length", byteLength); // Pseudo API and Koa API if ( @@ -377,10 +283,6 @@ async function send(req, res, filename, start, end, goNext, options) { } module.exports = { - getHeaderNames, - getHeaderFromRequest, - getHeaderFromResponse, - setHeaderForResponse, setStatusCode, send, sendError, diff --git a/types/utils/compatibleAPI.d.ts b/types/utils/compatibleAPI.d.ts index 88844c4bd..7091fbc5d 100644 --- a/types/utils/compatibleAPI.d.ts +++ b/types/utils/compatibleAPI.d.ts @@ -6,10 +6,8 @@ export type ExpectedRequest = { get: (name: string) => string | undefined; }; export type ExpectedResponse = { - get: (name: string) => string | string[] | undefined; - set: (name: string, value: number | string | string[]) => void; - status: (status: number) => void; - send: (data: any) => void; + status?: ((status: number) => void) | undefined; + send?: ((data: any) => void) | undefined; pipeInto?: ((data: any) => void) | undefined; }; /** @@ -43,48 +41,10 @@ export type SendOptions< */ /** * @typedef {Object} ExpectedResponse - * @property {(name: string) => string | string[] | undefined} get - * @property {(name: string, value: number | string | string[]) => void} set - * @property {(status: number) => void} status - * @property {(data: any) => void} send + * @property {(status: number) => void} [status] + * @property {(data: any) => void} [send] * @property {(data: any) => void} [pipeInto] */ -/** - * @template {ServerResponse} Response - * @param {Response} res - * @returns {string[]} - */ -export function getHeaderNames< - Response extends import("../index.js").ServerResponse, ->(res: Response): string[]; -/** - * @template {IncomingMessage} Request - * @param {Request} req - * @param {string} name - * @returns {string | string[] | undefined} - */ -export function getHeaderFromRequest< - Request extends import("http").IncomingMessage, ->(req: Request, name: string): string | string[] | undefined; -/** - * @template {ServerResponse} Response - * @param {Response} res - * @param {string} name - * @returns {number | string | string[] | undefined} - */ -export function getHeaderFromResponse< - Response extends import("../index.js").ServerResponse, ->(res: Response, name: string): number | string | string[] | undefined; -/** - * @template {ServerResponse} Response - * @param {Response} res - * @param {string} name - * @param {number | string | string[]} value - * @returns {void} - */ -export function setHeaderForResponse< - Response extends import("../index.js").ServerResponse, ->(res: Response, name: string, value: number | string | string[]): void; /** * @template {ServerResponse} Response * @param {Response} res From f69b638e0b92278e322524f0104a01905d68bb24 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Wed, 27 Mar 2024 19:46:48 +0300 Subject: [PATCH 09/16] refactor: code --- src/middleware.js | 221 ++++++++++++++++++++++++++- src/utils/compatibleAPI.js | 270 +++------------------------------ test/middleware.test.js | 2 +- types/middleware.d.ts | 35 ++++- types/utils/compatibleAPI.d.ts | 85 ++--------- 5 files changed, 286 insertions(+), 327 deletions(-) diff --git a/src/middleware.js b/src/middleware.js index a89801298..1caa4490f 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -2,14 +2,20 @@ const path = require("path"); const mime = require("mime-types"); +const onFinishedStream = require("on-finished"); + const getFilenameFromUrl = require("./utils/getFilenameFromUrl"); -const { setStatusCode, send, sendError } = require("./utils/compatibleAPI"); +const { setStatusCode, send, pipe } = require("./utils/compatibleAPI"); const ready = require("./utils/ready"); +const escapeHtml = require("./utils/escapeHtml"); /** @typedef {import("./index.js").NextFunction} NextFunction */ /** @typedef {import("./index.js").IncomingMessage} IncomingMessage */ /** @typedef {import("./index.js").ServerResponse} ServerResponse */ /** @typedef {import("./index.js").NormalizedHeaders} NormalizedHeaders */ +/** @typedef {import("fs").ReadStream} ReadStream */ + +const BYTES_RANGE_REGEXP = /^ *bytes/i; /** * @param {string} type @@ -21,7 +27,55 @@ function getValueContentRangeHeader(type, size, range) { return `${type} ${range ? `${range.start}-${range.end}` : "*"}/${size}`; } -const BYTES_RANGE_REGEXP = /^ *bytes/i; +/** + * @param {import("fs").ReadStream} stream stream + * @param {boolean} suppress do need suppress? + * @returns {void} + */ +function destroyStream(stream, suppress) { + if (typeof stream.destroy === "function") { + stream.destroy(); + } + + if (typeof stream.close === "function") { + // Node.js core bug workaround + stream.on( + "open", + /** + * @this {import("fs").ReadStream} + */ + function onOpenClose() { + // @ts-ignore + if (typeof this.fd === "number") { + // actually close down the fd + this.close(); + } + }, + ); + } + + if (typeof stream.addListener === "function" && suppress) { + stream.removeAllListeners("error"); + stream.addListener("error", () => {}); + } +} + +/** @type {Record} */ +const statuses = { + 400: "Bad Request", + 403: "Forbidden", + 404: "Not Found", + 416: "Range Not Satisfiable", + 500: "Internal Server Error", +}; + +/** + * @template {IncomingMessage} Request + * @template {ServerResponse} Response + * @typedef {Object} SendErrorOptions send error options + * @property {Record=} headers headers + * @property {import("./index").ModifyResponseData=} modifyResponseData modify response data callback + */ /** * @template {IncomingMessage} Request @@ -63,7 +117,65 @@ function wrapper(context) { return; } + /** + * @param {number} status status + * @param {Partial>=} options options + * @returns {void} + */ + function sendError(status, options) { + const content = statuses[status] || String(status); + let document = ` + + + +Error + + +
${escapeHtml(content)}
+ +`; + + // Clear existing headers + const headers = res.getHeaderNames(); + + for (let i = 0; i < headers.length; i++) { + res.removeHeader(headers[i]); + } + + if (options && options.headers) { + const keys = Object.keys(options.headers); + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = options.headers[key]; + + if (typeof value !== "undefined") { + res.setHeader(key, value); + } + } + } + + // Send basic response + setStatusCode(res, status); + res.setHeader("Content-Type", "text/html; charset=utf-8"); + res.setHeader("Content-Security-Policy", "default-src 'none'"); + res.setHeader("X-Content-Type-Options", "nosniff"); + + let byteLength = Buffer.byteLength(document); + + if (options && options.modifyResponseData) { + ({ data: document, byteLength } = + /** @type {{data: string, byteLength: number }} */ + (options.modifyResponseData(req, res, document, byteLength))); + } + + res.setHeader("Content-Length", byteLength); + + res.end(document); + } + async function processRequest() { + // Pipe and SendFile /** @type {import("./utils/getFilenameFromUrl").Extra} */ const extra = {}; const filename = getFilenameFromUrl( @@ -77,7 +189,7 @@ function wrapper(context) { context.logger.error(`Malicious path "${filename}".`); } - sendError(req, res, extra.errorCode, { + sendError(extra.errorCode, { modifyResponseData: context.options.modifyResponseData, }); @@ -90,6 +202,7 @@ function wrapper(context) { return; } + // Send logic let { headers } = context.options; if (typeof headers === "function") { @@ -152,7 +265,7 @@ function wrapper(context) { getValueContentRangeHeader("bytes", len), ); - sendError(req, res, 416, { + sendError(416, { headers: { "Content-Range": res.getHeader("Content-Range"), }, @@ -190,10 +303,104 @@ function wrapper(context) { const start = offset; const end = Math.max(offset, offset + len - 1); - send(req, res, filename, start, end, goNext, { - modifyResponseData: context.options.modifyResponseData, - outputFileSystem: context.outputFileSystem, + // Stream logic + const isFsSupportsStream = + typeof context.outputFileSystem.createReadStream === "function"; + + /** @type {string | Buffer | ReadStream} */ + let bufferOrStream; + let byteLength; + + try { + if (isFsSupportsStream) { + bufferOrStream = + /** @type {import("fs").createReadStream} */ + (context.outputFileSystem.createReadStream)(filename, { + start, + end, + }); + + // Handle files with zero bytes + byteLength = end === 0 ? 0 : end - start + 1; + } else { + bufferOrStream = /** @type {import("fs").readFileSync} */ ( + context.outputFileSystem.readFileSync + )(filename); + ({ byteLength } = bufferOrStream); + } + } catch (_ignoreError) { + await goNext(); + + return; + } + + if (context.options.modifyResponseData) { + ({ data: bufferOrStream, byteLength } = + context.options.modifyResponseData( + req, + res, + bufferOrStream, + byteLength, + )); + } + + res.setHeader("Content-Length", byteLength); + + if (req.method === "HEAD") { + // For Koa + if (res.statusCode === 404) { + setStatusCode(res, 200); + } + + res.end(); + return; + } + + const isPipeSupports = + typeof ( + /** @type {import("fs").ReadStream} */ (bufferOrStream).pipe + ) === "function"; + + if (!isPipeSupports) { + send(res, /** @type {Buffer} */ (bufferOrStream)); + return; + } + + // Cleanup + const cleanup = () => { + destroyStream( + /** @type {import("fs").ReadStream} */ (bufferOrStream), + true, + ); + }; + + // Error handling + /** @type {import("fs").ReadStream} */ + (bufferOrStream).on("error", (error) => { + // clean up stream early + cleanup(); + + // Handle Error + switch (/** @type {NodeJS.ErrnoException} */ (error).code) { + case "ENAMETOOLONG": + case "ENOENT": + case "ENOTDIR": + sendError(404, { + modifyResponseData: context.options.modifyResponseData, + }); + break; + default: + sendError(500, { + modifyResponseData: context.options.modifyResponseData, + }); + break; + } }); + + pipe(res, /** @type {ReadStream} */ (bufferOrStream)); + + // Response finished, cleanup + onFinishedStream(res, cleanup); } ready(context, processRequest, req); diff --git a/src/utils/compatibleAPI.js b/src/utils/compatibleAPI.js index 1ddbfe861..2d819e8b4 100644 --- a/src/utils/compatibleAPI.js +++ b/src/utils/compatibleAPI.js @@ -1,15 +1,5 @@ -const onFinishedStream = require("on-finished"); - -const escapeHtml = require("./escapeHtml"); - /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ /** @typedef {import("../index.js").ServerResponse} ServerResponse */ -/** @typedef {import("fs").ReadStream} ReadStream */ - -/** - * @typedef {Object} ExpectedRequest - * @property {(name: string) => string | undefined} get - */ /** * @typedef {Object} ExpectedResponse @@ -19,18 +9,14 @@ const escapeHtml = require("./escapeHtml"); */ /** - * @template {ServerResponse} Response + * @template {ServerResponse & ExpectedResponse} Response * @param {Response} res * @param {number} code */ function setStatusCode(res, code) { // Pseudo API - if ( - typeof (/** @type {Response & ExpectedResponse} */ (res).status) === - "function" - ) { - /** @type {Response & ExpectedResponse} */ - (res).status(code); + if (typeof res.status === "function") { + res.status(code); return; } @@ -41,249 +27,39 @@ function setStatusCode(res, code) { } /** - * @param {import("fs").ReadStream} stream stream - * @param {boolean} suppress do need suppress? - * @returns {void} - */ -function destroyStream(stream, suppress) { - if (typeof stream.destroy === "function") { - stream.destroy(); - } - - if (typeof stream.close === "function") { - // Node.js core bug workaround - stream.on( - "open", - /** - * @this {import("fs").ReadStream} - */ - function onOpenClose() { - // @ts-ignore - if (typeof this.fd === "number") { - // actually close down the fd - this.close(); - } - }, - ); - } - - if (typeof stream.addListener === "function" && suppress) { - stream.removeAllListeners("error"); - stream.addListener("error", () => {}); - } -} - -/** @type {Record} */ -const statuses = { - 400: "Bad Request", - 403: "Forbidden", - 404: "Not Found", - 416: "Range Not Satisfiable", - 500: "Internal Server Error", -}; - -/** - * @template {IncomingMessage} Request * @template {ServerResponse} Response - * @param {Request} req response - * @param {Response} res response - * @param {number} status status - * @param {Partial>=} options options - * @returns {void} + * @param {Response & ExpectedResponse} res + * @param {import("fs").ReadStream} bufferOrStream */ -function sendError(req, res, status, options) { - const content = statuses[status] || String(status); - let document = ` - - - -Error - - -
${escapeHtml(content)}
- -`; - - // Clear existing headers - const headers = res.getHeaderNames(); - - for (let i = 0; i < headers.length; i++) { - res.removeHeader(headers[i]); - } - - if (options && options.headers) { - const keys = Object.keys(options.headers); - - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const value = options.headers[key]; - - if (typeof value !== "undefined") { - res.setHeader(key, value); - } - } - } - - // Send basic response - setStatusCode(res, status); - res.setHeader("Content-Type", "text/html; charset=utf-8"); - res.setHeader("Content-Security-Policy", "default-src 'none'"); - res.setHeader("X-Content-Type-Options", "nosniff"); - - let byteLength = Buffer.byteLength(document); - - if (options && options.modifyResponseData) { - ({ data: document, byteLength } = - /** @type {{data: string, byteLength: number }} */ - (options.modifyResponseData(req, res, document, byteLength))); +function pipe(res, bufferOrStream) { + // Pseudo API and Koa API + if ( + typeof (/** @type {Response & ExpectedResponse} */ (res).pipeInto) === + "function" + ) { + // Writable stream into Readable stream + res.pipeInto(bufferOrStream); + return; } - res.setHeader("Content-Length", byteLength); - - res.end(document); + // Node.js API and Express API and Hapi API + bufferOrStream.pipe(res); } /** * @template {IncomingMessage} Request * @template {ServerResponse} Response - * @typedef {Object} SendOptions send error options - * @property {Record=} headers headers - * @property {import("../index").ModifyResponseData=} modifyResponseData modify response data callback - * @property {import("../index").OutputFileSystem} outputFileSystem modify response data callback + * @param {Response & ExpectedResponse} res + * @param {string | Buffer} bufferOrStream */ - -/** - * @template {IncomingMessage} Request - * @template {ServerResponse} Response - * @param {Request} req - * @param {Response} res - * @param {string} filename - * @param {number} start - * @param {number} end - * @param {() => Promise} goNext - * @param {SendOptions} options - */ -async function send(req, res, filename, start, end, goNext, options) { - const isFsSupportsStream = - typeof options.outputFileSystem.createReadStream === "function"; - - /** @type {string | Buffer | ReadStream} */ - let bufferOrStream; - let byteLength; - - try { - if (isFsSupportsStream) { - bufferOrStream = - /** @type {import("fs").createReadStream} */ - (options.outputFileSystem.createReadStream)(filename, { - start, - end, - }); - - // Handle files with zero bytes - byteLength = end === 0 ? 0 : end - start + 1; - } else { - bufferOrStream = /** @type {import("fs").readFileSync} */ ( - options.outputFileSystem.readFileSync - )(filename); - ({ byteLength } = bufferOrStream); - } - } catch (_ignoreError) { - await goNext(); - - return; - } - - if (options.modifyResponseData) { - ({ data: bufferOrStream, byteLength } = options.modifyResponseData( - req, - res, - bufferOrStream, - byteLength, - )); - } - - if ( - typeof (/** @type {import("fs").ReadStream} */ (bufferOrStream).pipe) === - "function" - ) { - // Cleanup - const cleanup = () => { - destroyStream( - /** @type {import("fs").ReadStream} */ (bufferOrStream), - true, - ); - }; - - // Error handling - /** @type {import("fs").ReadStream} */ - (bufferOrStream).on("error", (error) => { - // clean up stream early - cleanup(); - - // Handle Error - switch (/** @type {NodeJS.ErrnoException} */ (error).code) { - case "ENAMETOOLONG": - case "ENOENT": - case "ENOTDIR": - sendError(req, res, 404, options); - break; - default: - sendError(req, res, 500, options); - break; - } - }); - - res.setHeader("Content-Length", byteLength); - - // Pseudo API and Koa API - if ( - typeof (/** @type {Response & ExpectedResponse} */ (res).pipeInto) === - "function" - ) { - // Writable stream into Readable stream - /** @type {Response & ExpectedResponse} */ - (res).pipeInto(bufferOrStream); - } - // Node.js API and Express API and Hapi API - else { - /** @type {import("fs").ReadStream} */ - (bufferOrStream).pipe(res); - } - - if (req.method === "HEAD") { - res.end(); - return; - } - - // Response finished, cleanup - onFinishedStream(res, cleanup); - - return; - } - +function send(res, bufferOrStream) { // Pseudo API and Express API and Koa API - if ( - typeof (/** @type {Response & ExpectedResponse} */ (res).send) === - "function" - ) { - /** @type {Response & ExpectedResponse} */ - (res).send(bufferOrStream); + if (typeof res.send === "function") { + res.send(bufferOrStream); return; } - // Only Node.js API and Hapi API - res.setHeader("Content-Length", byteLength); - - if (req.method === "HEAD") { - res.end(); - } else { - res.end(bufferOrStream); - } + res.end(bufferOrStream); } -module.exports = { - setStatusCode, - send, - sendError, -}; +module.exports = { setStatusCode, send, pipe }; diff --git a/test/middleware.test.js b/test/middleware.test.js index 9df969be3..8e076d7c8 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -22,7 +22,7 @@ import webpackQueryStringConfig from "./fixtures/webpack.querystring.config"; import webpackClientServerConfig from "./fixtures/webpack.client.server.config"; // Suppress unnecessary stats output -global.console.log = jest.fn(); +// global.console.log = jest.fn(); async function startServer(app) { return new Promise((resolve, reject) => { diff --git a/types/middleware.d.ts b/types/middleware.d.ts index c4cfd6789..f317f2675 100644 --- a/types/middleware.d.ts +++ b/types/middleware.d.ts @@ -1,5 +1,12 @@ /// export = wrapper; +/** + * @template {IncomingMessage} Request + * @template {ServerResponse} Response + * @typedef {Object} SendErrorOptions send error options + * @property {Record=} headers headers + * @property {import("./index").ModifyResponseData=} modifyResponseData modify response data callback + */ /** * @template {IncomingMessage} Request * @template {ServerResponse} Response @@ -13,9 +20,35 @@ declare function wrapper< context: import("./index.js").FilledContext, ): import("./index.js").Middleware; declare namespace wrapper { - export { NextFunction, IncomingMessage, ServerResponse, NormalizedHeaders }; + export { + SendErrorOptions, + NextFunction, + IncomingMessage, + ServerResponse, + NormalizedHeaders, + ReadStream, + }; } +/** + * send error options + */ +type SendErrorOptions< + Request extends import("http").IncomingMessage, + Response extends import("./index.js").ServerResponse, +> = { + /** + * headers + */ + headers?: Record | undefined; + /** + * modify response data callback + */ + modifyResponseData?: + | import("./index").ModifyResponseData + | undefined; +}; type NextFunction = import("./index.js").NextFunction; type IncomingMessage = import("./index.js").IncomingMessage; type ServerResponse = import("./index.js").ServerResponse; type NormalizedHeaders = import("./index.js").NormalizedHeaders; +type ReadStream = import("fs").ReadStream; diff --git a/types/utils/compatibleAPI.d.ts b/types/utils/compatibleAPI.d.ts index 7091fbc5d..369054f7e 100644 --- a/types/utils/compatibleAPI.d.ts +++ b/types/utils/compatibleAPI.d.ts @@ -1,44 +1,13 @@ /// export type IncomingMessage = import("../index.js").IncomingMessage; export type ServerResponse = import("../index.js").ServerResponse; -export type ReadStream = import("fs").ReadStream; -export type ExpectedRequest = { - get: (name: string) => string | undefined; -}; export type ExpectedResponse = { status?: ((status: number) => void) | undefined; send?: ((data: any) => void) | undefined; pipeInto?: ((data: any) => void) | undefined; }; -/** - * send error options - */ -export type SendOptions< - Request extends import("http").IncomingMessage, - Response extends import("../index.js").ServerResponse, -> = { - /** - * headers - */ - headers?: Record | undefined; - /** - * modify response data callback - */ - modifyResponseData?: - | import("../index").ModifyResponseData - | undefined; - /** - * modify response data callback - */ - outputFileSystem: import("../index").OutputFileSystem; -}; /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ /** @typedef {import("../index.js").ServerResponse} ServerResponse */ -/** @typedef {import("fs").ReadStream} ReadStream */ -/** - * @typedef {Object} ExpectedRequest - * @property {(name: string) => string | undefined} get - */ /** * @typedef {Object} ExpectedResponse * @property {(status: number) => void} [status] @@ -46,59 +15,33 @@ export type SendOptions< * @property {(data: any) => void} [pipeInto] */ /** - * @template {ServerResponse} Response + * @template {ServerResponse & ExpectedResponse} Response * @param {Response} res * @param {number} code */ export function setStatusCode< - Response extends import("../index.js").ServerResponse, + Response extends import("http").ServerResponse< + import("http").IncomingMessage + > & + import("../index.js").ExtendedServerResponse & + ExpectedResponse, >(res: Response, code: number): void; /** * @template {IncomingMessage} Request * @template {ServerResponse} Response - * @typedef {Object} SendOptions send error options - * @property {Record=} headers headers - * @property {import("../index").ModifyResponseData=} modifyResponseData modify response data callback - * @property {import("../index").OutputFileSystem} outputFileSystem modify response data callback - */ -/** - * @template {IncomingMessage} Request - * @template {ServerResponse} Response - * @param {Request} req - * @param {Response} res - * @param {string} filename - * @param {number} start - * @param {number} end - * @param {() => Promise} goNext - * @param {SendOptions} options + * @param {Response & ExpectedResponse} res + * @param {string | Buffer} bufferOrStream */ export function send< Request extends import("http").IncomingMessage, Response extends import("../index.js").ServerResponse, ->( - req: Request, - res: Response, - filename: string, - start: number, - end: number, - goNext: () => Promise, - options: SendOptions, -): Promise; +>(res: Response & ExpectedResponse, bufferOrStream: string | Buffer): void; /** - * @template {IncomingMessage} Request * @template {ServerResponse} Response - * @param {Request} req response - * @param {Response} res response - * @param {number} status status - * @param {Partial>=} options options - * @returns {void} + * @param {Response & ExpectedResponse} res + * @param {import("fs").ReadStream} bufferOrStream */ -export function sendError< - Request extends import("http").IncomingMessage, - Response extends import("../index.js").ServerResponse, ->( - req: Request, - res: Response, - status: number, - options?: Partial> | undefined, +export function pipe( + res: Response & ExpectedResponse, + bufferOrStream: import("fs").ReadStream, ): void; From b75918163284495dae5a2f995c2d93805fccfbd7 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Thu, 28 Mar 2024 20:47:08 +0300 Subject: [PATCH 10/16] feat: etag support (#1797) --- .cspell.json | 4 +- README.md | 34 +- package-lock.json | 33 +- src/index.js | 1 + src/middleware.js | 178 ++++++ src/options.json | 5 + src/utils/etag.js | 83 +++ src/utils/parseTokenList.js | 43 ++ .../logging.test.js.snap.webpack4 | 584 ------------------ .../validation-options.test.js.snap.webpack4 | 145 ----- .../validation-options.test.js.snap.webpack5 | 16 + test/middleware.test.js | 164 ++++- test/validation-options.test.js | 4 + types/index.d.ts | 2 + types/utils/etag.d.ts | 16 + types/utils/parseTokenList.d.ts | 8 + 16 files changed, 567 insertions(+), 753 deletions(-) create mode 100644 src/utils/etag.js create mode 100644 src/utils/parseTokenList.js delete mode 100644 test/__snapshots__/logging.test.js.snap.webpack4 delete mode 100644 test/__snapshots__/validation-options.test.js.snap.webpack4 create mode 100644 types/utils/etag.d.ts create mode 100644 types/utils/parseTokenList.d.ts diff --git a/.cspell.json b/.cspell.json index 5fd258574..f393531cc 100644 --- a/.cspell.json +++ b/.cspell.json @@ -19,7 +19,9 @@ "mycustom", "commitlint", "nosniff", - "deoptimize" + "deoptimize", + "etag", + "cachable" ], "ignorePaths": [ "CHANGELOG.md", diff --git a/README.md b/README.md index 52f751300..02de5f84d 100644 --- a/README.md +++ b/README.md @@ -60,19 +60,20 @@ See [below](#other-servers) for an example of use with fastify. ## Options -| Name | Type | Default | Description | -| :---------------------------------------------: | :-----------------------: | :-------------------------------------------: | :------------------------------------------------------------------------------------------------------------------- | -| **[`methods`](#methods)** | `Array` | `[ 'GET', 'HEAD' ]` | Allows to pass the list of HTTP request methods accepted by the middleware | -| **[`headers`](#headers)** | `Array\|Object\|Function` | `undefined` | Allows to pass custom HTTP headers on each request. | -| **[`index`](#index)** | `Boolean\|String` | `index.html` | If `false` (but not `undefined`), the server will not respond to requests to the root URL. | -| **[`mimeTypes`](#mimetypes)** | `Object` | `undefined` | Allows to register custom mime types or extension mappings. | -| **[`mimeTypeDefault`](#mimetypedefault)** | `String` | `undefined` | Allows to register a default mime type when we can't determine the content type. | -| **[`publicPath`](#publicpath)** | `String` | `output.publicPath` (from a configuration) | The public path that the middleware is bound to. | -| **[`stats`](#stats)** | `Boolean\|String\|Object` | `stats` (from a configuration) | Stats options object or preset name. | -| **[`serverSideRender`](#serversiderender)** | `Boolean` | `undefined` | Instructs the module to enable or disable the server-side rendering mode. | -| **[`writeToDisk`](#writetodisk)** | `Boolean\|Function` | `false` | Instructs the module to write files to the configured location on disk as specified in your `webpack` configuration. | -| **[`outputFileSystem`](#outputfilesystem)** | `Object` | [`memfs`](https://github.com/streamich/memfs) | Set the default file system which will be used by webpack as primary destination of generated files. | -| **[`modifyResponseData`](#modifyresponsedata)** | `Function` | `undefined` | Allows to set up a callback to change the response data. | +| Name | Type | Default | Description | +| :---------------------------------------------: | :---------------------------: | :-------------------------------------------: | :--------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | +| **[`methods`](#methods)** | `Array` | `[ 'GET', 'HEAD' ]` | Allows to pass the list of HTTP request methods accepted by the middleware | +| **[`headers`](#headers)** | `Array\ | Object\ | Function` | `undefined` | Allows to pass custom HTTP headers on each request. | +| **[`index`](#index)** | `Boolean\ | String` | `index.html` | If `false` (but not `undefined`), the server will not respond to requests to the root URL. | +| **[`mimeTypes`](#mimetypes)** | `Object` | `undefined` | Allows to register custom mime types or extension mappings. | +| **[`mimeTypeDefault`](#mimetypedefault)** | `String` | `undefined` | Allows to register a default mime type when we can't determine the content type. | +| **[`etag`](#tag)** | `boolean\| "weak"\| "strong"` | `undefined` | Enable or disable etag generation. | +| **[`publicPath`](#publicpath)** | `String` | `output.publicPath` (from a configuration) | The public path that the middleware is bound to. | +| **[`stats`](#stats)** | `Boolean\ | String\ | Object` | `stats` (from a configuration) | Stats options object or preset name. | +| **[`serverSideRender`](#serversiderender)** | `Boolean` | `undefined` | Instructs the module to enable or disable the server-side rendering mode. | +| **[`writeToDisk`](#writetodisk)** | `Boolean\ | Function` | `false` | Instructs the module to write files to the configured location on disk as specified in your `webpack` configuration. | +| **[`outputFileSystem`](#outputfilesystem)** | `Object` | [`memfs`](https://github.com/streamich/memfs) | Set the default file system which will be used by webpack as primary destination of generated files. | +| **[`modifyResponseData`](#modifyresponsedata)** | `Function` | `undefined` | Allows to set up a callback to change the response data. | The middleware accepts an `options` Object. The following is a property reference for the Object. @@ -171,6 +172,13 @@ Default: `undefined` This property allows a user to register a default mime type when we can't determine the content type. +### etag + +Type: `"weak" | "strong"` +Default: `undefined` + +Enable or disable etag generation. Boolean value use + ### publicPath Type: `String` diff --git a/package-lock.json b/package-lock.json index 3d44e2115..9aee263b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "7.1.1", "license": "MIT", "dependencies": { + "cloneable-readable": "^3.0.0", "colorette": "^2.0.10", "memfs": "^4.6.0", "mime-types": "^2.1.31", @@ -4917,7 +4918,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, "dependencies": { "event-target-shim": "^5.0.0" }, @@ -5516,7 +5516,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -5659,7 +5658,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, "funding": [ { "type": "github", @@ -6034,6 +6032,29 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/cloneable-readable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-3.0.0.tgz", + "integrity": "sha512-Lkfd9IRx1nfiBr7UHNxJSl/x7DOeUfYmxzCkxYJC2tyc/9vKgV75msgLGurGQsak/NvJDHMWcshzEXRlxfvhqg==", + "dependencies": { + "readable-stream": "^4.0.0" + } + }, + "node_modules/cloneable-readable/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -9551,7 +9572,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, "engines": { "node": ">=6" } @@ -9566,7 +9586,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, "engines": { "node": ">=0.8.x" } @@ -11227,7 +11246,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -15301,7 +15319,6 @@ "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, "engines": { "node": ">= 0.6.0" } @@ -15892,7 +15909,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -16560,7 +16576,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "dependencies": { "safe-buffer": "~5.2.0" } diff --git a/src/index.js b/src/index.js index 305eaf3a4..e8fc9f736 100644 --- a/src/index.js +++ b/src/index.js @@ -117,6 +117,7 @@ const noop = () => {}; * @property {OutputFileSystem} [outputFileSystem] * @property {boolean | string} [index] * @property {ModifyResponseData} [modifyResponseData] + * @property {"weak" | "strong"} [etag] */ /** diff --git a/src/middleware.js b/src/middleware.js index 1caa4490f..84356beef 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -8,6 +8,8 @@ const getFilenameFromUrl = require("./utils/getFilenameFromUrl"); const { setStatusCode, send, pipe } = require("./utils/compatibleAPI"); const ready = require("./utils/ready"); const escapeHtml = require("./utils/escapeHtml"); +const etag = require("./utils/etag"); +const parseTokenList = require("./utils/parseTokenList"); /** @typedef {import("./index.js").NextFunction} NextFunction */ /** @typedef {import("./index.js").IncomingMessage} IncomingMessage */ @@ -27,6 +29,21 @@ function getValueContentRangeHeader(type, size, range) { return `${type} ${range ? `${range.start}-${range.end}` : "*"}/${size}`; } +/** + * Parse an HTTP Date into a number. + * + * @param {string} date + * @private + */ +function parseHttpDate(date) { + const timestamp = date && Date.parse(date); + + // istanbul ignore next: guard against date.js Date.parse patching + return typeof timestamp === "number" ? timestamp : NaN; +} + +const CACHE_CONTROL_NO_CACHE_REGEXP = /(?:^|,)\s*?no-cache\s*?(?:,|$)/; + /** * @param {import("fs").ReadStream} stream stream * @param {boolean} suppress do need suppress? @@ -174,6 +191,115 @@ function wrapper(context) { res.end(document); } + function isConditionalGET() { + return ( + req.headers["if-match"] || + req.headers["if-unmodified-since"] || + req.headers["if-none-match"] || + req.headers["if-modified-since"] + ); + } + + function isPreconditionFailure() { + const match = req.headers["if-match"]; + + if (match) { + // eslint-disable-next-line no-shadow + const etag = res.getHeader("ETag"); + + return ( + !etag || + (match !== "*" && + parseTokenList(match).every( + // eslint-disable-next-line no-shadow + (match) => + match !== etag && + match !== `W/${etag}` && + `W/${match}` !== etag, + )) + ); + } + + return false; + } + + /** + * @returns {boolean} is cachable + */ + function isCachable() { + return ( + (res.statusCode >= 200 && res.statusCode < 300) || + res.statusCode === 304 + ); + } + + /** + * @param {import("http").OutgoingHttpHeaders} resHeaders + * @returns {boolean} + */ + function isFresh(resHeaders) { + // Always return stale when Cache-Control: no-cache to support end-to-end reload requests + // https://tools.ietf.org/html/rfc2616#section-14.9.4 + const cacheControl = req.headers["cache-control"]; + + if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) { + return false; + } + + // if-none-match + const noneMatch = req.headers["if-none-match"]; + + if (noneMatch && noneMatch !== "*") { + if (!resHeaders.etag) { + return false; + } + + const matches = parseTokenList(noneMatch); + + let etagStale = true; + + for (let i = 0; i < matches.length; i++) { + const match = matches[i]; + + if ( + match === resHeaders.etag || + match === `W/${resHeaders.etag}` || + `W/${match}` === resHeaders.etag + ) { + etagStale = false; + break; + } + } + + if (etagStale) { + return false; + } + } + + // A recipient MUST ignore If-Modified-Since if the request contains an If-None-Match header field; + // the condition in If-None-Match is considered to be a more accurate replacement for the condition in If-Modified-Since, + // and the two are only combined for the sake of interoperating with older intermediaries that might not implement If-None-Match. + if (noneMatch) { + return true; + } + + // if-modified-since + const modifiedSince = req.headers["if-modified-since"]; + + if (modifiedSince) { + const lastModified = resHeaders["last-modified"]; + const modifiedStale = + !lastModified || + !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince)); + + if (modifiedStale) { + return false; + } + } + + return true; + } + async function processRequest() { // Pipe and SendFile /** @type {import("./utils/getFilenameFromUrl").Extra} */ @@ -334,6 +460,56 @@ function wrapper(context) { return; } + if (context.options.etag && !res.getHeader("ETag")) { + const value = + context.options.etag === "weak" + ? /** @type {import("fs").Stats} */ (extra.stats) + : bufferOrStream; + + const val = await etag(value); + + if (val.buffer) { + bufferOrStream = val.buffer; + } + + res.setHeader("ETag", val.hash); + } + + // Conditional GET support + if (isConditionalGET()) { + if (isPreconditionFailure()) { + sendError(412, { + modifyResponseData: context.options.modifyResponseData, + }); + + return; + } + + // For Koa + if (res.statusCode === 404) { + setStatusCode(res, 200); + } + + if ( + isCachable() && + isFresh({ + etag: /** @type {string} */ (res.getHeader("ETag")), + }) + ) { + setStatusCode(res, 304); + + // Remove content header fields + res.removeHeader("Content-Encoding"); + res.removeHeader("Content-Language"); + res.removeHeader("Content-Length"); + res.removeHeader("Content-Range"); + res.removeHeader("Content-Type"); + res.end(); + + return; + } + } + if (context.options.modifyResponseData) { ({ data: bufferOrStream, byteLength } = context.options.modifyResponseData( @@ -361,6 +537,8 @@ function wrapper(context) { /** @type {import("fs").ReadStream} */ (bufferOrStream).pipe ) === "function"; + console.log(isPipeSupports); + if (!isPipeSupports) { send(res, /** @type {Buffer} */ (bufferOrStream)); return; diff --git a/src/options.json b/src/options.json index 91086d193..357db9bf4 100644 --- a/src/options.json +++ b/src/options.json @@ -129,6 +129,11 @@ "description": "Allows to set up a callback to change the response data.", "link": "https://github.com/webpack/webpack-dev-middleware#modifyresponsedata", "instanceof": "Function" + }, + "etag": { + "description": "Enable or disable etag generation.", + "link": "https://github.com/webpack/webpack-dev-middleware#etag", + "enum": ["weak", "strong"] } }, "additionalProperties": false diff --git a/src/utils/etag.js b/src/utils/etag.js new file mode 100644 index 000000000..2aa227d8c --- /dev/null +++ b/src/utils/etag.js @@ -0,0 +1,83 @@ +const crypto = require("crypto"); + +/** @typedef {import("fs").Stats} Stats */ +/** @typedef {import("fs").ReadStream} ReadStream */ + +/** + * Generate a tag for a stat. + * + * @param {Stats} stat + * @return {{ hash: string, buffer?: Buffer }} + */ +function statTag(stat) { + const mtime = stat.mtime.getTime().toString(16); + const size = stat.size.toString(16); + + return { hash: `W/"${size}-${mtime}"` }; +} + +/** + * Generate an entity tag. + * + * @param {Buffer | ReadStream} entity + * @return {Promise<{ hash: string, buffer?: Buffer }>} + */ +async function entityTag(entity) { + const sha1 = crypto.createHash("sha1"); + + if (!Buffer.isBuffer(entity)) { + let byteLength = 0; + + /** @type {Buffer[]} */ + const buffers = []; + + await new Promise((resolve, reject) => { + entity + .on("data", (chunk) => { + sha1.update(chunk); + buffers.push(/** @type {Buffer} */ (chunk)); + byteLength += /** @type {Buffer} */ (chunk).byteLength; + }) + .on("end", () => { + resolve(sha1); + }) + .on("error", reject); + }); + + return { + buffer: Buffer.concat(buffers), + hash: `"${byteLength.toString(16)}-${sha1.digest("base64").substring(0, 27)}"`, + }; + } + + if (entity.byteLength === 0) { + // Fast-path empty + return { hash: '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"' }; + } + + // Compute hash of entity + const hash = sha1.update(entity).digest("base64").substring(0, 27); + + // Compute length of entity + const { byteLength } = entity; + + return { hash: `"${byteLength.toString(16)}-${hash}"` }; +} + +/** + * Create a simple ETag. + * + * @param {Buffer | ReadStream | Stats} entity + * @return {Promise<{ hash: string, buffer?: Buffer }>} + */ +async function etag(entity) { + const isStrong = + Buffer.isBuffer(entity) || + typeof (/** @type {ReadStream} */ (entity).pipe) === "function"; + + return isStrong + ? entityTag(/** @type {Buffer | ReadStream} */ (entity)) + : statTag(/** @type {import("fs").Stats} */ (entity)); +} + +module.exports = etag; diff --git a/src/utils/parseTokenList.js b/src/utils/parseTokenList.js new file mode 100644 index 000000000..fed6c472a --- /dev/null +++ b/src/utils/parseTokenList.js @@ -0,0 +1,43 @@ +/** + * Parse a HTTP token list. + * + * @param {string} str + * @returns {string[]} tokens + */ +function parseTokenList(str) { + let end = 0; + let start = 0; + + const list = []; + + // gather tokens + for (let i = 0, len = str.length; i < len; i++) { + switch (str.charCodeAt(i)) { + case 0x20 /* */: + if (start === end) { + end = i + 1; + start = end; + } + break; + case 0x2c /* , */: + if (start !== end) { + list.push(str.substring(start, end)); + } + end = i + 1; + start = end; + break; + default: + end = i + 1; + break; + } + } + + // final token + if (start !== end) { + list.push(str.substring(start, end)); + } + + return list; +} + +module.exports = parseTokenList; diff --git a/test/__snapshots__/logging.test.js.snap.webpack4 b/test/__snapshots__/logging.test.js.snap.webpack4 deleted file mode 100644 index e534901ff..000000000 --- a/test/__snapshots__/logging.test.js.snap.webpack4 +++ /dev/null @@ -1,584 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`logging should logging an error in "watch" method: stderr 1`] = `"Error: Watch error"`; - -exports[`logging should logging an warning: stderr 1`] = `""`; - -exports[`logging should logging an warning: stdout 1`] = ` -" -WARNING in Warning" -`; - -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #2: stderr 1`] = `""`; - -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #2: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Child broken: -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./broken.js] x bytes {main} [built] [failed] [1 error] - -ERROR in ./broken.js 1:3 -Module parse failed: Unexpected token (1:3) -You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders -> 1()2()3() -| -Child warning: -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./warning.js] x bytes {main} [built] - -WARNING in Warning -Child success: -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #3: stderr 1`] = `""`; - -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #3: stderr 2`] = `""`; - -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #3: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./broken.js] x bytes {main} [built] [failed] [1 error] - -ERROR in ./broken.js 1:3 -Module parse failed: Unexpected token (1:3) -You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders -> 1()2()3() -| -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./warning.js] x bytes {main} [built] - -WARNING in Warning -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration #3: stdout 2`] = ` -"Child -Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./broken.js] x bytes {main} [built] [failed] [1 error] - -ERROR in ./broken.js 1:3 -Module parse failed: Unexpected token (1:3) -You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders -> 1()2()3() -| -Child -Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./warning.js] x bytes {main} [built] - -WARNING in Warning -Child -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted]" -`; - -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration: stderr 1`] = `""`; - -exports[`logging should logging in multi-compiler and respect the "stats" option from configuration: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./broken.js] x bytes {main} [built] [failed] [1 error] - -ERROR in ./broken.js 1:3 -Module parse failed: Unexpected token (1:3) -You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders -> 1()2()3() -| -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./warning.js] x bytes {main} [built] - -WARNING in Warning -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully build and respect colors #2: stderr 1`] = `""`; - -exports[`logging should logging on successfully build and respect colors #2: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully build and respect colors: stderr 1`] = `""`; - -exports[`logging should logging on successfully build and respect colors: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully build and respect the "NO_COLOR" env: stderr 1`] = `""`; - -exports[`logging should logging on successfully build and respect the "NO_COLOR" env: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with custom object value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with custom object value: stdout 1`] = ` -"Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted]" -`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "false" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "false" value: stdout 1`] = `""`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "minimal" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "minimal" value: stdout 1`] = `"x modules"`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "none" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "none" value: stdout 1`] = `""`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "true" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "true" value: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "verbose" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build and respect the "stats" option from configuration with the "verbose" value: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -chunk {main} bundle.js (xxxx) x bytes [entry] [rendered] -> ./foo.js main -[./foo.js] x bytes {main} [depth 0] [built] -single entry ./foo.js main -[./index.html] x bytes {main} [depth 1] [built] -[exports: default] -cjs require ./index.html [./foo.js] 4:0-23 -[./svg.svg] x bytes {main} [depth 1] [built] -[exports: default] -cjs require ./svg.svg [./foo.js] 3:0-20 - -LOG from xxx" -`; - -exports[`logging should logging on successfully build in multi-compiler mode: stderr 1`] = `""`; - -exports[`logging should logging on successfully build in multi-compiler mode: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built] -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./bar.js] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with object value and no colors: stderr 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with object value and no colors: stdout 1`] = ` -"Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted]" -`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with object value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with object value: stdout 1`] = ` -"Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted]" -`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "false" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "false" value: stdout 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "none" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "none" value: stdout 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "normal" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "normal" value: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "true" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "true" value: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "verbose" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the "verbose" value: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -chunk {main} bundle.js (xxxx) x bytes [entry] [rendered] -> ./foo.js main -[./foo.js] x bytes {main} [depth 0] [built] -single entry ./foo.js main -[./index.html] x bytes {main} [depth 1] [built] -[exports: default] -cjs require ./index.html [./foo.js] 4:0-23 -[./svg.svg] x bytes {main} [depth 1] [built] -[exports: default] -cjs require ./svg.svg [./foo.js] 3:0-20 - -LOG from xxx" -`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the object value and colors: stderr 1`] = `""`; - -exports[`logging should logging on successfully build using the "stats" option for middleware with the object value and colors: stdout 1`] = ` -"Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted]" -`; - -exports[`logging should logging on successfully build when the 'stats' doesn't exist: stderr 1`] = `""`; - -exports[`logging should logging on successfully build when the 'stats' doesn't exist: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully build: stderr 1`] = `""`; - -exports[`logging should logging on successfully build: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with object value and colors: stderr 1`] = `""`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with object value and colors: stdout 1`] = ` -"Child -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Child -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main" -`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with object value and no colors: stderr 1`] = `""`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with object value and no colors: stdout 1`] = ` -"Child -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Child -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main" -`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the "false" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the "false" value: stdout 1`] = `""`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the "normal" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the "normal" value: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built] -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./bar.js] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the "true" value: stderr 1`] = `""`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the "true" value: stdout 1`] = ` -"Hash: xxxx -Version: webpack x.x.x -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Entrypoint main = bundle.js -[./foo.js] x bytes {main} [built] -[./index.html] x bytes {main} [built] -[./svg.svg] x bytes {main} [built] -Child -Hash: xxxx -Time: Xms -Built at: x -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -Entrypoint main = bundle.js -[./bar.js] x bytes {main} [built]" -`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the object value: stderr 1`] = `""`; - -exports[`logging should logging on successfully multi-compiler build using the "stats" option for middleware with the object value: stdout 1`] = ` -"Child -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main -index.html x bytes [emitted] -svg.svg x KiB [emitted] -Child -Asset Size Chunks Chunk Names -bundle.js x KiB main [emitted] main" -`; - -exports[`logging should logging on unsuccessful build in multi-compiler: stderr 1`] = `""`; - -exports[`logging should logging on unsuccessful build in multi-compiler: stdout 1`] = ` -"Child - -ERROR in ./broken.js 1:3 -Module parse failed: Unexpected token (1:3) -You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders -> 1()2()3() -| -Child - -ERROR in ./broken.js 1:3 -Module parse failed: Unexpected token (1:3) -You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders -> 1()2()3() -|" -`; - -exports[`logging should logging on unsuccessful build: stderr 1`] = `""`; - -exports[`logging should logging on unsuccessful build: stdout 1`] = ` -" -ERROR in ./broken.js 1:3 -Module parse failed: Unexpected token (1:3) -You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders -> 1()2()3() -|" -`; - -exports[`logging should logging warnings in multi-compiler mode: stderr 1`] = `""`; - -exports[`logging should logging warnings in multi-compiler mode: stdout 1`] = ` -"Child - -WARNING in Warning -Child - -WARNING in Warning" -`; diff --git a/test/__snapshots__/validation-options.test.js.snap.webpack4 b/test/__snapshots__/validation-options.test.js.snap.webpack4 deleted file mode 100644 index 00e92ef14..000000000 --- a/test/__snapshots__/validation-options.test.js.snap.webpack4 +++ /dev/null @@ -1,145 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`validation should throw an error on the "headers" option with "[]" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.headers should be a non-empty array." -`; - -exports[`validation should throw an error on the "headers" option with "[{"foo":"bar"}]" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.headers[0] has an unknown property 'foo'. These properties are valid: - object { key?, value? }" -`; - -exports[`validation should throw an error on the "headers" option with "1" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.headers should be one of these: - [object { key?, value? }, ...] (should not have fewer than 1 item) | object { … } | function - -> Allows to pass custom HTTP headers on each request - -> Read more at https://github.com/webpack/webpack-dev-middleware#headers - Details: - * options.headers should be an array: - [object { key?, value? }, ...] (should not have fewer than 1 item) - * options.headers should be an object: - object { … } - * options.headers should be an instance of function." -`; - -exports[`validation should throw an error on the "headers" option with "true" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.headers should be one of these: - [object { key?, value? }, ...] (should not have fewer than 1 item) | object { … } | function - -> Allows to pass custom HTTP headers on each request - -> Read more at https://github.com/webpack/webpack-dev-middleware#headers - Details: - * options.headers should be an array: - [object { key?, value? }, ...] (should not have fewer than 1 item) - * options.headers should be an object: - object { … } - * options.headers should be an instance of function." -`; - -exports[`validation should throw an error on the "index" option with "{}" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.index should be one of these: - boolean | string - -> Allows to serve an index of the directory. - -> Read more at https://github.com/webpack/webpack-dev-middleware#index - Details: - * options.index should be a boolean. - * options.index should be a string." -`; - -exports[`validation should throw an error on the "index" option with "0" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.index should be one of these: - boolean | string - -> Allows to serve an index of the directory. - -> Read more at https://github.com/webpack/webpack-dev-middleware#index - Details: - * options.index should be a boolean. - * options.index should be a string." -`; - -exports[`validation should throw an error on the "methods" option with "{}" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.methods should be an array: - [string, ...] - -> Allows to pass the list of HTTP request methods accepted by the middleware. - -> Read more at https://github.com/webpack/webpack-dev-middleware#methods" -`; - -exports[`validation should throw an error on the "methods" option with "true" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.methods should be an array: - [string, ...] - -> Allows to pass the list of HTTP request methods accepted by the middleware. - -> Read more at https://github.com/webpack/webpack-dev-middleware#methods" -`; - -exports[`validation should throw an error on the "mimeTypes" option with "foo" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.mimeTypes should be an object: - object { … } - -> Allows a user to register custom mime types or extension mappings. - -> Read more at https://github.com/webpack/webpack-dev-middleware#mimetypes" -`; - -exports[`validation should throw an error on the "outputFileSystem" option with "false" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.outputFileSystem should be an object: - object { … } - -> Set the default file system which will be used by webpack as primary destination of generated files. - -> Read more at https://github.com/webpack/webpack-dev-middleware#outputfilesystem" -`; - -exports[`validation should throw an error on the "publicPath" option with "false" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.publicPath should be one of these: - \\"auto\\" | string | function - -> The \`publicPath\` specifies the public URL address of the output files when referenced in a browser. - -> Read more at https://github.com/webpack/webpack-dev-middleware#publicpath - Details: - * options.publicPath should be \\"auto\\". - * options.publicPath should be a string. - * options.publicPath should be an instance of function." -`; - -exports[`validation should throw an error on the "serverSideRender" option with "0" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.serverSideRender should be a boolean. - -> Instructs the module to enable or disable the server-side rendering mode. - -> Read more at https://github.com/webpack/webpack-dev-middleware#serversiderender" -`; - -exports[`validation should throw an error on the "serverSideRender" option with "foo" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.serverSideRender should be a boolean. - -> Instructs the module to enable or disable the server-side rendering mode. - -> Read more at https://github.com/webpack/webpack-dev-middleware#serversiderender" -`; - -exports[`validation should throw an error on the "stats" option with "0" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.stats should be one of these: - \\"none\\" | \\"summary\\" | \\"errors-only\\" | \\"errors-warnings\\" | \\"minimal\\" | \\"normal\\" | \\"detailed\\" | \\"verbose\\" | boolean | object { … } - -> Stats options object or preset name. - -> Read more at https://github.com/webpack/webpack-dev-middleware#stats - Details: - * options.stats should be one of these: - \\"none\\" | \\"summary\\" | \\"errors-only\\" | \\"errors-warnings\\" | \\"minimal\\" | \\"normal\\" | \\"detailed\\" | \\"verbose\\" - * options.stats should be a boolean. - * options.stats should be an object: - object { … }" -`; - -exports[`validation should throw an error on the "writeToDisk" option with "{}" value 1`] = ` -"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - - options.writeToDisk should be one of these: - boolean | function - -> Allows to write generated files on disk. - -> Read more at https://github.com/webpack/webpack-dev-middleware#writetodisk - Details: - * options.writeToDisk should be a boolean. - * options.writeToDisk should be an instance of function." -`; diff --git a/test/__snapshots__/validation-options.test.js.snap.webpack5 b/test/__snapshots__/validation-options.test.js.snap.webpack5 index 3c2fc74a8..797b295bf 100644 --- a/test/__snapshots__/validation-options.test.js.snap.webpack5 +++ b/test/__snapshots__/validation-options.test.js.snap.webpack5 @@ -1,5 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`validation should throw an error on the "etag" option with "0" value 1`] = ` +"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. + - options.etag should be one of these: + "weak" | "strong" + -> Enable or disable etag generation. + -> Read more at https://github.com/webpack/webpack-dev-middleware#etag" +`; + +exports[`validation should throw an error on the "etag" option with "foo" value 1`] = ` +"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. + - options.etag should be one of these: + "weak" | "strong" + -> Enable or disable etag generation. + -> Read more at https://github.com/webpack/webpack-dev-middleware#etag" +`; + exports[`validation should throw an error on the "headers" option with "[]" value 1`] = ` "Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - options.headers should be a non-empty array." diff --git a/test/middleware.test.js b/test/middleware.test.js index 8e076d7c8..eb068af16 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -22,7 +22,7 @@ import webpackQueryStringConfig from "./fixtures/webpack.querystring.config"; import webpackClientServerConfig from "./fixtures/webpack.client.server.config"; // Suppress unnecessary stats output -// global.console.log = jest.fn(); +global.console.log = jest.fn(); async function startServer(app) { return new Promise((resolve, reject) => { @@ -4210,5 +4210,167 @@ describe.each([ }); }); }); + + describe("etag", () => { + describe("should work and generate weak etag", () => { + beforeEach(async () => { + const compiler = getCompiler(webpackConfig); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + etag: "weak", + }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and set weak etag', async () => { + const response = await req.get(`/bundle.js`); + + expect(response.statusCode).toEqual(200); + expect(response.headers.etag).toBeDefined(); + expect(response.headers.etag.startsWith("W/")).toBe(true); + }); + + it('should return the "304" code for the "GET" request to the bundle file with etag and "if-match" header', async () => { + const response1 = await req.get(`/bundle.js`); + + expect(response1.statusCode).toEqual(200); + expect(response1.headers.etag).toBeDefined(); + expect(response1.headers.etag.startsWith("W/")).toBe(true); + + const response2 = await req + .get(`/bundle.js`) + .set("if-match", response1.headers.etag); + + expect(response2.statusCode).toEqual(304); + expect(response2.headers.etag).toBeDefined(); + expect(response2.headers.etag.startsWith("W/")).toBe(true); + + const response3 = await req.get(`/bundle.js`).set("if-match", "test"); + + expect(response3.statusCode).toEqual(412); + }); + + it('should return the "304" code for the "GET" request to the bundle file with etag "if-none-match" header', async () => { + const response1 = await req.get(`/bundle.js`); + + expect(response1.statusCode).toEqual(200); + expect(response1.headers.etag).toBeDefined(); + expect(response1.headers.etag.startsWith("W/")).toBe(true); + + const response2 = await req + .get(`/bundle.js`) + .set("if-none-match", response1.headers.etag); + + expect(response2.statusCode).toEqual(304); + expect(response2.headers.etag).toBeDefined(); + expect(response2.headers.etag.startsWith("W/")).toBe(true); + + const response3 = await req + .get(`/bundle.js`) + .set("if-none-match", "test"); + + expect(response3.statusCode).toEqual(200); + expect(response3.headers.etag).toBeDefined(); + expect(response3.headers.etag.startsWith("W/")).toBe(true); + }); + + it('should return the "412" code for the "GET" request to the bundle file with etag and wrong "if-match" header', async () => { + const response1 = await req.get(`/bundle.js`); + + expect(response1.statusCode).toEqual(200); + expect(response1.headers.etag).toBeDefined(); + expect(response1.headers.etag.startsWith("W/")).toBe(true); + + const response2 = await req.get(`/bundle.js`).set("if-match", "test"); + + expect(response2.statusCode).toEqual(412); + }); + + it('should return the "200" code for the "GET" request to the bundle file with etag and "if-match" and "cache-control: no-cache" header', async () => { + const response1 = await req.get(`/bundle.js`); + + expect(response1.statusCode).toEqual(200); + expect(response1.headers.etag).toBeDefined(); + expect(response1.headers.etag.startsWith("W/")).toBe(true); + + const response2 = await req + .get(`/bundle.js`) + .set("if-match", response1.headers.etag) + .set("Cache-Control", "no-cache"); + + expect(response2.statusCode).toEqual(200); + expect(response2.headers.etag).toBeDefined(); + expect(response2.headers.etag.startsWith("W/")).toBe(true); + }); + }); + + describe("should work and generate strong etag", () => { + beforeEach(async () => { + const compiler = getCompiler(webpackConfig); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + etag: "strong", + }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and set weak etag', async () => { + const response = await req.get(`/bundle.js`); + + expect(response.statusCode).toEqual(200); + expect(response.headers.etag).toBe( + /* cspell:disable-next-line */ + '"18c7-l/LCspQS5fbbf1kkLGOsK9FTpbg"', + ); + }); + }); + + describe("should work and generate strong etag without createReadStream", () => { + beforeEach(async () => { + const compiler = getCompiler(webpackConfig); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + etag: "strong", + }, + ); + + instance.context.outputFileSystem.createReadStream = null; + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and set weak etag', async () => { + const response = await req.get(`/bundle.js`); + + expect(response.statusCode).toEqual(200); + expect(response.headers.etag).toBe( + /* cspell:disable-next-line */ + '"18c7-l/LCspQS5fbbf1kkLGOsK9FTpbg"', + ); + }); + }); + }); }); }); diff --git a/test/validation-options.test.js b/test/validation-options.test.js index a9b0eebe0..f1d8f8328 100644 --- a/test/validation-options.test.js +++ b/test/validation-options.test.js @@ -67,6 +67,10 @@ describe("validation", () => { ], failure: [true], }, + etag: { + success: ["weak", "strong"], + failure: ["foo", 0], + }, }; function stringifyValue(value) { diff --git a/types/index.d.ts b/types/index.d.ts index ee6f4c9e0..98b3509b7 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -90,6 +90,7 @@ export = wdm; * @property {OutputFileSystem} [outputFileSystem] * @property {boolean | string} [index] * @property {ModifyResponseData} [modifyResponseData] + * @property {"weak" | "strong"} [etag] */ /** * @template {IncomingMessage} RequestInternal @@ -350,6 +351,7 @@ type Options< modifyResponseData?: | ModifyResponseData | undefined; + etag?: "strong" | "weak" | undefined; }; type Middleware< RequestInternal extends import("http").IncomingMessage, diff --git a/types/utils/etag.d.ts b/types/utils/etag.d.ts new file mode 100644 index 000000000..002599793 --- /dev/null +++ b/types/utils/etag.d.ts @@ -0,0 +1,16 @@ +export = etag; +/** + * Create a simple ETag. + * + * @param {Buffer | ReadStream | Stats} entity + * @return {Promise<{ hash: string, buffer?: Buffer }>} + */ +declare function etag(entity: Buffer | ReadStream | Stats): Promise<{ + hash: string; + buffer?: Buffer; +}>; +declare namespace etag { + export { Stats, ReadStream }; +} +type ReadStream = import("fs").ReadStream; +type Stats = import("fs").Stats; diff --git a/types/utils/parseTokenList.d.ts b/types/utils/parseTokenList.d.ts new file mode 100644 index 000000000..67b75c26e --- /dev/null +++ b/types/utils/parseTokenList.d.ts @@ -0,0 +1,8 @@ +export = parseTokenList; +/** + * Parse a HTTP token list. + * + * @param {string} str + * @returns {string[]} tokens + */ +declare function parseTokenList(str: string): string[]; From 18e56833327084c22c1ee6bdad123095a68d144a Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Fri, 29 Mar 2024 14:05:53 +0300 Subject: [PATCH 11/16] feat: support `Last-Modified` header generation (#1798) --- README.md | 7 + src/index.js | 1 + src/middleware.js | 124 +++++++++--- src/options.json | 5 + .../validation-options.test.js.snap.webpack5 | 14 ++ test/middleware.test.js | 185 ++++++++++++++++++ test/validation-options.test.js | 4 + types/index.d.ts | 2 + 8 files changed, 320 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 02de5f84d..683ea7fed 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,13 @@ Default: `undefined` Enable or disable etag generation. Boolean value use +### lastModified + +Type: `Boolean` +Default: `undefined` + +Enable or disable `Last-Modified` header. Uses the file system's last modified value. + ### publicPath Type: `String` diff --git a/src/index.js b/src/index.js index e8fc9f736..d0c634c40 100644 --- a/src/index.js +++ b/src/index.js @@ -118,6 +118,7 @@ const noop = () => {}; * @property {boolean | string} [index] * @property {ModifyResponseData} [modifyResponseData] * @property {"weak" | "strong"} [etag] + * @property {boolean} [lastModified] */ /** diff --git a/src/middleware.js b/src/middleware.js index 84356beef..e1bf81125 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -7,8 +7,6 @@ const onFinishedStream = require("on-finished"); const getFilenameFromUrl = require("./utils/getFilenameFromUrl"); const { setStatusCode, send, pipe } = require("./utils/compatibleAPI"); const ready = require("./utils/ready"); -const escapeHtml = require("./utils/escapeHtml"); -const etag = require("./utils/etag"); const parseTokenList = require("./utils/parseTokenList"); /** @typedef {import("./index.js").NextFunction} NextFunction */ @@ -33,7 +31,7 @@ function getValueContentRangeHeader(type, size, range) { * Parse an HTTP Date into a number. * * @param {string} date - * @private + * @returns {number} */ function parseHttpDate(date) { const timestamp = date && Date.parse(date); @@ -140,6 +138,8 @@ function wrapper(context) { * @returns {void} */ function sendError(status, options) { + // eslint-disable-next-line global-require + const escapeHtml = require("./utils/escapeHtml"); const content = statuses[status] || String(status); let document = ` @@ -201,17 +201,21 @@ function wrapper(context) { } function isPreconditionFailure() { - const match = req.headers["if-match"]; - - if (match) { - // eslint-disable-next-line no-shadow + // if-match + const ifMatch = req.headers["if-match"]; + + // A recipient MUST ignore If-Unmodified-Since if the request contains + // an If-Match header field; the condition in If-Match is considered to + // be a more accurate replacement for the condition in + // If-Unmodified-Since, and the two are only combined for the sake of + // interoperating with older intermediaries that might not implement If-Match. + if (ifMatch) { const etag = res.getHeader("ETag"); return ( !etag || - (match !== "*" && - parseTokenList(match).every( - // eslint-disable-next-line no-shadow + (ifMatch !== "*" && + parseTokenList(ifMatch).every( (match) => match !== etag && match !== `W/${etag}` && @@ -220,6 +224,23 @@ function wrapper(context) { ); } + // if-unmodified-since + const ifUnmodifiedSince = req.headers["if-unmodified-since"]; + + if (ifUnmodifiedSince) { + const unmodifiedSince = parseHttpDate(ifUnmodifiedSince); + + // A recipient MUST ignore the If-Unmodified-Since header field if the + // received field-value is not a valid HTTP-date. + if (!isNaN(unmodifiedSince)) { + const lastModified = parseHttpDate( + /** @type {string} */ (res.getHeader("Last-Modified")), + ); + + return isNaN(lastModified) || lastModified > unmodifiedSince; + } + } + return false; } @@ -288,9 +309,17 @@ function wrapper(context) { if (modifiedSince) { const lastModified = resHeaders["last-modified"]; + const parsedHttpDate = parseHttpDate(modifiedSince); + + // A recipient MUST ignore the If-Modified-Since header field if the + // received field-value is not a valid HTTP-date, or if the request + // method is neither GET nor HEAD. + if (isNaN(parsedHttpDate)) { + return true; + } + const modifiedStale = - !lastModified || - !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince)); + !lastModified || !(parseHttpDate(lastModified) <= parsedHttpDate); if (modifiedStale) { return false; @@ -300,6 +329,38 @@ function wrapper(context) { return true; } + function isRangeFresh() { + const ifRange = + /** @type {string | undefined} */ + (req.headers["if-range"]); + + if (!ifRange) { + return true; + } + + // if-range as etag + if (ifRange.indexOf('"') !== -1) { + const etag = /** @type {string | undefined} */ (res.getHeader("ETag")); + + if (!etag) { + return true; + } + + return Boolean(etag && ifRange.indexOf(etag) !== -1); + } + + // if-range as modified date + const lastModified = + /** @type {string | undefined} */ + (res.getHeader("Last-Modified")); + + if (!lastModified) { + return true; + } + + return parseHttpDate(lastModified) <= parseHttpDate(ifRange); + } + async function processRequest() { // Pipe and SendFile /** @type {import("./utils/getFilenameFromUrl").Extra} */ @@ -372,16 +433,25 @@ function wrapper(context) { res.setHeader("Accept-Ranges", "bytes"); } - const rangeHeader = /** @type {string} */ (req.headers.range); - let len = /** @type {import("fs").Stats} */ (extra.stats).size; let offset = 0; + const rangeHeader = /** @type {string} */ (req.headers.range); + if (rangeHeader && BYTES_RANGE_REGEXP.test(rangeHeader)) { - // eslint-disable-next-line global-require - const parsedRanges = require("range-parser")(len, rangeHeader, { - combine: true, - }); + let parsedRanges = + /** @type {import("range-parser").Ranges | import("range-parser").Result | []} */ + ( + // eslint-disable-next-line global-require + require("range-parser")(len, rangeHeader, { + combine: true, + }) + ); + + // If-Range support + if (!isRangeFresh()) { + parsedRanges = []; + } if (parsedRanges === -1) { context.logger.error("Unsatisfiable range for 'Range' header."); @@ -460,13 +530,22 @@ function wrapper(context) { return; } + if (context.options.lastModified && !res.getHeader("Last-Modified")) { + const modified = + /** @type {import("fs").Stats} */ + (extra.stats).mtime.toUTCString(); + + res.setHeader("Last-Modified", modified); + } + if (context.options.etag && !res.getHeader("ETag")) { const value = context.options.etag === "weak" ? /** @type {import("fs").Stats} */ (extra.stats) : bufferOrStream; - const val = await etag(value); + // eslint-disable-next-line global-require + const val = await require("./utils/etag")(value); if (val.buffer) { bufferOrStream = val.buffer; @@ -493,7 +572,10 @@ function wrapper(context) { if ( isCachable() && isFresh({ - etag: /** @type {string} */ (res.getHeader("ETag")), + etag: /** @type {string | undefined} */ (res.getHeader("ETag")), + "last-modified": + /** @type {string | undefined} */ + (res.getHeader("Last-Modified")), }) ) { setStatusCode(res, 304); @@ -537,8 +619,6 @@ function wrapper(context) { /** @type {import("fs").ReadStream} */ (bufferOrStream).pipe ) === "function"; - console.log(isPipeSupports); - if (!isPipeSupports) { send(res, /** @type {Buffer} */ (bufferOrStream)); return; diff --git a/src/options.json b/src/options.json index 357db9bf4..50443e268 100644 --- a/src/options.json +++ b/src/options.json @@ -134,6 +134,11 @@ "description": "Enable or disable etag generation.", "link": "https://github.com/webpack/webpack-dev-middleware#etag", "enum": ["weak", "strong"] + }, + "lastModified": { + "description": "Enable or disable `Last-Modified` header. Uses the file system's last modified value.", + "link": "https://github.com/webpack/webpack-dev-middleware#lastmodified", + "type": "boolean" } }, "additionalProperties": false diff --git a/test/__snapshots__/validation-options.test.js.snap.webpack5 b/test/__snapshots__/validation-options.test.js.snap.webpack5 index 797b295bf..aa4d0dfef 100644 --- a/test/__snapshots__/validation-options.test.js.snap.webpack5 +++ b/test/__snapshots__/validation-options.test.js.snap.webpack5 @@ -77,6 +77,20 @@ exports[`validation should throw an error on the "index" option with "0" value 1 * options.index should be a non-empty string." `; +exports[`validation should throw an error on the "lastModified" option with "0" value 1`] = ` +"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. + - options.lastModified should be a boolean. + -> Enable or disable \`Last-Modified\` header. Uses the file system's last modified value. + -> Read more at https://github.com/webpack/webpack-dev-middleware#lastmodified" +`; + +exports[`validation should throw an error on the "lastModified" option with "foo" value 1`] = ` +"Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. + - options.lastModified should be a boolean. + -> Enable or disable \`Last-Modified\` header. Uses the file system's last modified value. + -> Read more at https://github.com/webpack/webpack-dev-middleware#lastmodified" +`; + exports[`validation should throw an error on the "methods" option with "{}" value 1`] = ` "Invalid options object. Dev Middleware has been initialized using an options object that does not match the API schema. - options.methods should be an array: diff --git a/test/middleware.test.js b/test/middleware.test.js index eb068af16..c47a18b01 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -4372,5 +4372,190 @@ describe.each([ }); }); }); + + describe("lastModified", () => { + describe("should work and generate Last-Modified header", () => { + beforeEach(async () => { + const compiler = getCompiler(webpackConfig); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + lastModified: true, + }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + function parseHttpDate(date) { + const timestamp = date && Date.parse(date); + + // istanbul ignore next: guard against date.js Date.parse patching + return typeof timestamp === "number" ? timestamp : NaN; + } + + it('should return the "200" code for the "GET" request to the bundle file and set "Last-Modified"', async () => { + const response = await req.get(`/bundle.js`); + + expect(response.statusCode).toEqual(200); + expect(response.headers["last-modified"]).toBeDefined(); + }); + + it('should return the "304" code for the "GET" request to the bundle file with "Last-Modified" and "if-unmodified-since" header', async () => { + const response1 = await req.get(`/bundle.js`); + + expect(response1.statusCode).toEqual(200); + expect(response1.headers["last-modified"]).toBeDefined(); + + const response2 = await req + .get(`/bundle.js`) + .set("if-unmodified-since", response1.headers["last-modified"]); + + expect(response2.statusCode).toEqual(304); + expect(response2.headers["last-modified"]).toBeDefined(); + + const response3 = await req + .get(`/bundle.js`) + .set("if-unmodified-since", "Fri, 29 Mar 2020 10:25:50 GMT"); + + expect(response3.statusCode).toEqual(412); + }); + + it('should return the "304" code for the "GET" request to the bundle file with "Last-Modified" and "if-modified-since" header', async () => { + const response1 = await req.get(`/bundle.js`); + + expect(response1.statusCode).toEqual(200); + expect(response1.headers["last-modified"]).toBeDefined(); + + const response2 = await req + .get(`/bundle.js`) + .set("if-modified-since", response1.headers["last-modified"]); + + expect(response2.statusCode).toEqual(304); + expect(response2.headers["last-modified"]).toBeDefined(); + + const response3 = await req + .get(`/bundle.js`) + .set( + "if-modified-since", + new Date( + parseHttpDate(response1.headers["last-modified"]) - 1000, + ).toUTCString(), + ); + + expect(response3.statusCode).toEqual(200); + expect(response3.headers["last-modified"]).toBeDefined(); + }); + + it('should return the "412" code for the "GET" request to the bundle file with etag and "if-unmodified-since" header', async () => { + const response1 = await req.get(`/bundle.js`); + + expect(response1.statusCode).toEqual(200); + expect(response1.headers["last-modified"]).toBeDefined(); + + const response2 = await req + .get(`/bundle.js`) + .set( + "if-unmodified-since", + new Date( + parseHttpDate(response1.headers["last-modified"]) - 1000, + ).toUTCString(), + ); + + expect(response2.statusCode).toEqual(412); + }); + + it('should return the "200" code for the "GET" request to the bundle file with etag and "if-match" and "cache-control: no-cache" header', async () => { + const response1 = await req.get(`/bundle.js`); + + expect(response1.statusCode).toEqual(200); + expect(response1.headers["last-modified"]).toBeDefined(); + + const response2 = await req + .get(`/bundle.js`) + .set("if-unmodified-since", response1.headers["last-modified"]) + .set("Cache-Control", "no-cache"); + + expect(response2.statusCode).toEqual(200); + expect(response1.headers["last-modified"]).toBeDefined(); + }); + }); + + describe('should work and prefer "if-match" and "if-none-match"', () => { + beforeEach(async () => { + const compiler = getCompiler(webpackConfig); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + etag: "weak", + lastModified: true, + }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + function parseHttpDate(date) { + const timestamp = date && Date.parse(date); + + // istanbul ignore next: guard against date.js Date.parse patching + return typeof timestamp === "number" ? timestamp : NaN; + } + + it('should return the "304" code for the "GET" request to the bundle file and prefer "if-match" over "if-unmodified-since"', async () => { + const response1 = await req.get(`/bundle.js`); + + expect(response1.statusCode).toEqual(200); + expect(response1.headers["last-modified"]).toBeDefined(); + expect(response1.headers.etag).toBeDefined(); + + const response2 = await req + .get(`/bundle.js`) + .set("if-match", response1.headers.etag) + .set( + "if-unmodified-since", + new Date( + parseHttpDate(response1.headers["last-modified"]) - 1000, + ).toUTCString(), + ); + + expect(response2.statusCode).toEqual(304); + expect(response2.headers["last-modified"]).toBeDefined(); + expect(response2.headers.etag).toBeDefined(); + }); + + it('should return the "304" code for the "GET" request to the bundle file and prefer "if-none-match" over "if-modified-since"', async () => { + const response1 = await req.get(`/bundle.js`); + + expect(response1.statusCode).toEqual(200); + expect(response1.headers["last-modified"]).toBeDefined(); + expect(response1.headers.etag).toBeDefined(); + + const response2 = await req + .get(`/bundle.js`) + .set("if-none-match", response1.headers.etag) + .set( + "if-modified-since", + new Date( + parseHttpDate(response1.headers["last-modified"]) - 1000, + ).toUTCString(), + ); + + expect(response2.statusCode).toEqual(304); + expect(response2.headers["last-modified"]).toBeDefined(); + expect(response2.headers.etag).toBeDefined(); + }); + }); + }); }); }); diff --git a/test/validation-options.test.js b/test/validation-options.test.js index f1d8f8328..62632f001 100644 --- a/test/validation-options.test.js +++ b/test/validation-options.test.js @@ -71,6 +71,10 @@ describe("validation", () => { success: ["weak", "strong"], failure: ["foo", 0], }, + lastModified: { + success: [true, false], + failure: ["foo", 0], + }, }; function stringifyValue(value) { diff --git a/types/index.d.ts b/types/index.d.ts index 98b3509b7..1080632b0 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -91,6 +91,7 @@ export = wdm; * @property {boolean | string} [index] * @property {ModifyResponseData} [modifyResponseData] * @property {"weak" | "strong"} [etag] + * @property {boolean} [lastModified] */ /** * @template {IncomingMessage} RequestInternal @@ -352,6 +353,7 @@ type Options< | ModifyResponseData | undefined; etag?: "strong" | "weak" | undefined; + lastModified?: boolean | undefined; }; type Middleware< RequestInternal extends import("http").IncomingMessage, From 1f54424698ff074a9d3b340853fe32758340e448 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Fri, 29 Mar 2024 17:21:31 +0300 Subject: [PATCH 12/16] test: more (#1800) --- src/index.js | 4 +- src/middleware.js | 321 ++++++++++++++++++++------------ src/utils/compatibleAPI.js | 44 ++++- src/utils/getFilenameFromUrl.js | 39 +--- src/utils/memorize.js | 43 +++++ test/middleware.test.js | 193 +++++++++++++++---- types/index.d.ts | 8 +- types/utils/compatibleAPI.d.ts | 16 ++ types/utils/memorize.d.ts | 26 +++ 9 files changed, 495 insertions(+), 199 deletions(-) create mode 100644 src/utils/memorize.js create mode 100644 types/utils/memorize.d.ts diff --git a/src/index.js b/src/index.js index d0c634c40..de2419f10 100644 --- a/src/index.js +++ b/src/index.js @@ -59,7 +59,7 @@ const noop = () => {}; /** * @typedef {Object} ResponseData - * @property {string | Buffer | ReadStream} data + * @property {Buffer | ReadStream} data * @property {number} byteLength */ @@ -69,7 +69,7 @@ const noop = () => {}; * @callback ModifyResponseData * @param {RequestInternal} req * @param {ResponseInternal} res - * @param {string | Buffer | ReadStream} data + * @param {Buffer | ReadStream} data * @param {number} byteLength * @return {ResponseData} */ diff --git a/src/middleware.js b/src/middleware.js index e1bf81125..dcf1217ef 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -5,9 +5,15 @@ const mime = require("mime-types"); const onFinishedStream = require("on-finished"); const getFilenameFromUrl = require("./utils/getFilenameFromUrl"); -const { setStatusCode, send, pipe } = require("./utils/compatibleAPI"); +const { + setStatusCode, + send, + pipe, + createReadStreamOrReadFileSync, +} = require("./utils/compatibleAPI"); const ready = require("./utils/ready"); const parseTokenList = require("./utils/parseTokenList"); +const memorize = require("./utils/memorize"); /** @typedef {import("./index.js").NextFunction} NextFunction */ /** @typedef {import("./index.js").IncomingMessage} IncomingMessage */ @@ -84,6 +90,21 @@ const statuses = { 500: "Internal Server Error", }; +const parseRangeHeaders = memorize( + /** + * @param {string} value + * @returns {import("range-parser").Result | import("range-parser").Ranges} + */ + (value) => { + const [len, rangeHeader] = value.split("|"); + + // eslint-disable-next-line global-require + return require("range-parser")(Number(len), rangeHeader, { + combine: true, + }); + }, +); + /** * @template {IncomingMessage} Request * @template {ServerResponse} Response @@ -141,7 +162,8 @@ function wrapper(context) { // eslint-disable-next-line global-require const escapeHtml = require("./utils/escapeHtml"); const content = statuses[status] || String(status); - let document = ` + let document = Buffer.from( + ` @@ -150,7 +172,9 @@ function wrapper(context) {
${escapeHtml(content)}
-`; +`, + "utf-8", + ); // Clear existing headers const headers = res.getHeaderNames(); @@ -182,7 +206,7 @@ function wrapper(context) { if (options && options.modifyResponseData) { ({ data: document, byteLength } = - /** @type {{data: string, byteLength: number }} */ + /** @type {{ data: Buffer, byteLength: number }} */ (options.modifyResponseData(req, res, document, byteLength))); } @@ -267,9 +291,16 @@ function wrapper(context) { return false; } - // if-none-match + // fields const noneMatch = req.headers["if-none-match"]; + const modifiedSince = req.headers["if-modified-since"]; + // unconditional request + if (!noneMatch && !modifiedSince) { + return false; + } + + // if-none-match if (noneMatch && noneMatch !== "*") { if (!resHeaders.etag) { return false; @@ -305,21 +336,15 @@ function wrapper(context) { } // if-modified-since - const modifiedSince = req.headers["if-modified-since"]; - if (modifiedSince) { const lastModified = resHeaders["last-modified"]; - const parsedHttpDate = parseHttpDate(modifiedSince); // A recipient MUST ignore the If-Modified-Since header field if the // received field-value is not a valid HTTP-date, or if the request // method is neither GET nor HEAD. - if (isNaN(parsedHttpDate)) { - return true; - } - const modifiedStale = - !lastModified || !(parseHttpDate(lastModified) <= parsedHttpDate); + !lastModified || + !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince)); if (modifiedStale) { return false; @@ -361,6 +386,43 @@ function wrapper(context) { return parseHttpDate(lastModified) <= parseHttpDate(ifRange); } + /** + * @returns {string | undefined} + */ + function getRangeHeader() { + const rage = req.headers.range; + + if (rage && BYTES_RANGE_REGEXP.test(rage)) { + return rage; + } + + // eslint-disable-next-line no-undefined + return undefined; + } + + /** + * @param {import("range-parser").Range} range + * @returns {[number, number]} + */ + function getOffsetAndLenFromRange(range) { + const offset = range.start; + const len = range.end - range.start + 1; + + return [offset, len]; + } + + /** + * @param {number} offset + * @param {number} len + * @returns {[number, number]} + */ + function calcStartAndEnd(offset, len) { + const start = offset; + const end = Math.max(offset, offset + len - 1); + + return [start, end]; + } + async function processRequest() { // Pipe and SendFile /** @type {import("./utils/getFilenameFromUrl").Extra} */ @@ -389,6 +451,11 @@ function wrapper(context) { return; } + const { size } = /** @type {import("fs").Stats} */ (extra.stats); + + let len = size; + let offset = 0; + // Send logic let { headers } = context.options; @@ -433,125 +500,75 @@ function wrapper(context) { res.setHeader("Accept-Ranges", "bytes"); } - let len = /** @type {import("fs").Stats} */ (extra.stats).size; - let offset = 0; - - const rangeHeader = /** @type {string} */ (req.headers.range); - - if (rangeHeader && BYTES_RANGE_REGEXP.test(rangeHeader)) { - let parsedRanges = - /** @type {import("range-parser").Ranges | import("range-parser").Result | []} */ - ( - // eslint-disable-next-line global-require - require("range-parser")(len, rangeHeader, { - combine: true, - }) - ); - - // If-Range support - if (!isRangeFresh()) { - parsedRanges = []; - } - - if (parsedRanges === -1) { - context.logger.error("Unsatisfiable range for 'Range' header."); - - res.setHeader( - "Content-Range", - getValueContentRangeHeader("bytes", len), - ); - - sendError(416, { - headers: { - "Content-Range": res.getHeader("Content-Range"), - }, - modifyResponseData: context.options.modifyResponseData, - }); - - return; - } else if (parsedRanges === -2) { - context.logger.error( - "A malformed 'Range' header was provided. A regular response will be sent for this request.", - ); - } else if (parsedRanges.length > 1) { - context.logger.error( - "A 'Range' header with multiple ranges was provided. Multiple ranges are not supported, so a regular response will be sent for this request.", - ); - } - - if (parsedRanges !== -2 && parsedRanges.length === 1) { - // Content-Range - setStatusCode(res, 206); - res.setHeader( - "Content-Range", - getValueContentRangeHeader( - "bytes", - len, - /** @type {import("range-parser").Ranges} */ (parsedRanges)[0], - ), - ); + if (context.options.lastModified && !res.getHeader("Last-Modified")) { + const modified = + /** @type {import("fs").Stats} */ + (extra.stats).mtime.toUTCString(); - offset += parsedRanges[0].start; - len = parsedRanges[0].end - parsedRanges[0].start + 1; - } + res.setHeader("Last-Modified", modified); } - const start = offset; - const end = Math.max(offset, offset + len - 1); + /** @type {number} */ + let start; + /** @type {number} */ + let end; - // Stream logic - const isFsSupportsStream = - typeof context.outputFileSystem.createReadStream === "function"; - - /** @type {string | Buffer | ReadStream} */ + /** @type {undefined | Buffer | ReadStream} */ let bufferOrStream; + /** @type {number} */ let byteLength; - try { - if (isFsSupportsStream) { - bufferOrStream = - /** @type {import("fs").createReadStream} */ - (context.outputFileSystem.createReadStream)(filename, { - start, - end, - }); + const rangeHeader = getRangeHeader(); - // Handle files with zero bytes - byteLength = end === 0 ? 0 : end - start + 1; + if (context.options.etag && !res.getHeader("ETag")) { + /** @type {import("fs").Stats | Buffer | ReadStream | undefined} */ + let value; + + if (context.options.etag === "weak") { + value = /** @type {import("fs").Stats} */ (extra.stats); } else { - bufferOrStream = /** @type {import("fs").readFileSync} */ ( - context.outputFileSystem.readFileSync - )(filename); - ({ byteLength } = bufferOrStream); - } - } catch (_ignoreError) { - await goNext(); + if (rangeHeader) { + const parsedRanges = + /** @type {import("range-parser").Ranges | import("range-parser").Result} */ + (parseRangeHeaders(`${size}|${rangeHeader}`)); + + if ( + parsedRanges !== -2 && + parsedRanges !== -1 && + parsedRanges.length === 1 + ) { + [offset, len] = getOffsetAndLenFromRange(parsedRanges[0]); + } + } - return; - } + [start, end] = calcStartAndEnd(offset, len); - if (context.options.lastModified && !res.getHeader("Last-Modified")) { - const modified = - /** @type {import("fs").Stats} */ - (extra.stats).mtime.toUTCString(); + try { + const result = createReadStreamOrReadFileSync( + filename, + context.outputFileSystem, + start, + end, + ); - res.setHeader("Last-Modified", modified); - } + value = result.bufferOrStream; + ({ bufferOrStream, byteLength } = result); + } catch (_err) { + // Ignore here + } + } - if (context.options.etag && !res.getHeader("ETag")) { - const value = - context.options.etag === "weak" - ? /** @type {import("fs").Stats} */ (extra.stats) - : bufferOrStream; + if (value) { + // eslint-disable-next-line global-require + const result = await require("./utils/etag")(value); - // eslint-disable-next-line global-require - const val = await require("./utils/etag")(value); + // Because we already read stream, we can cache buffer to avoid extra read from fs + if (result.buffer) { + bufferOrStream = result.buffer; + } - if (val.buffer) { - bufferOrStream = val.buffer; + res.setHeader("ETag", result.hash); } - - res.setHeader("ETag", val.hash); } // Conditional GET support @@ -592,16 +609,88 @@ function wrapper(context) { } } + if (rangeHeader) { + let parsedRanges = + /** @type {import("range-parser").Ranges | import("range-parser").Result | []} */ + (parseRangeHeaders(`${size}|${rangeHeader}`)); + + // If-Range support + if (!isRangeFresh()) { + parsedRanges = []; + } + + if (parsedRanges === -1) { + context.logger.error("Unsatisfiable range for 'Range' header."); + + res.setHeader( + "Content-Range", + getValueContentRangeHeader("bytes", size), + ); + + sendError(416, { + headers: { + "Content-Range": res.getHeader("Content-Range"), + }, + modifyResponseData: context.options.modifyResponseData, + }); + + return; + } else if (parsedRanges === -2) { + context.logger.error( + "A malformed 'Range' header was provided. A regular response will be sent for this request.", + ); + } else if (parsedRanges.length > 1) { + context.logger.error( + "A 'Range' header with multiple ranges was provided. Multiple ranges are not supported, so a regular response will be sent for this request.", + ); + } + + if (parsedRanges !== -2 && parsedRanges.length === 1) { + // Content-Range + setStatusCode(res, 206); + res.setHeader( + "Content-Range", + getValueContentRangeHeader( + "bytes", + size, + /** @type {import("range-parser").Ranges} */ (parsedRanges)[0], + ), + ); + + [offset, len] = getOffsetAndLenFromRange(parsedRanges[0]); + } + } + + // When strong Etag generation is enabled we already read file, so we can skip extra fs call + if (!bufferOrStream) { + [start, end] = calcStartAndEnd(offset, len); + + try { + ({ bufferOrStream, byteLength } = createReadStreamOrReadFileSync( + filename, + context.outputFileSystem, + start, + end, + )); + } catch (_ignoreError) { + await goNext(); + + return; + } + } + if (context.options.modifyResponseData) { ({ data: bufferOrStream, byteLength } = context.options.modifyResponseData( req, res, bufferOrStream, + // @ts-ignore byteLength, )); } + // @ts-ignore res.setHeader("Content-Length", byteLength); if (req.method === "HEAD") { diff --git a/src/utils/compatibleAPI.js b/src/utils/compatibleAPI.js index 2d819e8b4..ab315b6f0 100644 --- a/src/utils/compatibleAPI.js +++ b/src/utils/compatibleAPI.js @@ -62,4 +62,46 @@ function send(res, bufferOrStream) { res.end(bufferOrStream); } -module.exports = { setStatusCode, send, pipe }; +/** + * @param {string} filename + * @param {import("../index").OutputFileSystem} outputFileSystem + * @param {number} start + * @param {number} end + * @returns {{ bufferOrStream: (Buffer | import("fs").ReadStream), byteLength: number }} + */ +function createReadStreamOrReadFileSync( + filename, + outputFileSystem, + start, + end, +) { + /** @type {string | Buffer | import("fs").ReadStream} */ + let bufferOrStream; + /** @type {number} */ + let byteLength; + + // Stream logic + const isFsSupportsStream = + typeof outputFileSystem.createReadStream === "function"; + + if (isFsSupportsStream) { + bufferOrStream = + /** @type {import("fs").createReadStream} */ + (outputFileSystem.createReadStream)(filename, { + start, + end, + }); + + // Handle files with zero bytes + byteLength = end === 0 ? 0 : end - start + 1; + } else { + bufferOrStream = + /** @type {import("fs").readFileSync} */ + (outputFileSystem.readFileSync)(filename); + ({ byteLength } = bufferOrStream); + } + + return { bufferOrStream, byteLength }; +} + +module.exports = { setStatusCode, send, pipe, createReadStreamOrReadFileSync }; diff --git a/src/utils/getFilenameFromUrl.js b/src/utils/getFilenameFromUrl.js index 0cb9c05a4..319b149db 100644 --- a/src/utils/getFilenameFromUrl.js +++ b/src/utils/getFilenameFromUrl.js @@ -3,48 +3,13 @@ const { parse } = require("url"); const querystring = require("querystring"); const getPaths = require("./getPaths"); +const memorize = require("./memorize"); /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */ /** @typedef {import("../index.js").ServerResponse} ServerResponse */ -const cacheStore = new WeakMap(); - -/** - * @template T - * @param {Function} fn - * @param {{ cache?: Map } | undefined} cache - * @param {(value: T) => T} callback - * @returns {any} - */ -const mem = (fn, { cache = new Map() } = {}, callback) => { - /** - * @param {any} arguments_ - * @return {any} - */ - const memoized = (...arguments_) => { - const [key] = arguments_; - const cacheItem = cache.get(key); - - if (cacheItem) { - return cacheItem.data; - } - - let result = fn.apply(this, arguments_); - result = callback(result); - - cache.set(key, { - data: result, - }); - - return result; - }; - - cacheStore.set(memoized, cache); - - return memoized; -}; // eslint-disable-next-line no-undefined -const memoizedParse = mem(parse, undefined, (value) => { +const memoizedParse = memorize(parse, undefined, (value) => { if (value.pathname) { // eslint-disable-next-line no-param-reassign value.pathname = decode(value.pathname); diff --git a/src/utils/memorize.js b/src/utils/memorize.js new file mode 100644 index 000000000..a8921157e --- /dev/null +++ b/src/utils/memorize.js @@ -0,0 +1,43 @@ +const cacheStore = new WeakMap(); + +/** + * @template T + * @param {Function} fn + * @param {{ cache?: Map } | undefined} cache + * @param {((value: T) => T)=} callback + * @returns {any} + */ +function memorize(fn, { cache = new Map() } = {}, callback) { + /** + * @param {any} arguments_ + * @return {any} + */ + const memoized = (...arguments_) => { + const [key] = arguments_; + console.log("CACHE", key); + const cacheItem = cache.get(key); + + if (cacheItem) { + return cacheItem.data; + } + + // @ts-ignore + let result = fn.apply(this, arguments_); + + if (callback) { + result = callback(result); + } + + cache.set(key, { + data: result, + }); + + return result; + }; + + cacheStore.set(memoized, cache); + + return memoized; +} + +module.exports = memorize; diff --git a/test/middleware.test.js b/test/middleware.test.js index c47a18b01..cab109ba7 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -242,6 +242,13 @@ function applyTestMiddleware(name, middlewares) { return middlewares; } +function parseHttpDate(date) { + const timestamp = date && Date.parse(date); + + // istanbul ignore next: guard against date.js Date.parse patching + return typeof timestamp === "number" ? timestamp : NaN; +} + describe.each([ ["connect", connect], ["express", express], @@ -4238,7 +4245,7 @@ describe.each([ expect(response.headers.etag.startsWith("W/")).toBe(true); }); - it('should return the "304" code for the "GET" request to the bundle file with etag and "if-match" header', async () => { + it('should return the "304" code for the "GET" request to the bundle file with etag and "if-none-match" header', async () => { const response1 = await req.get(`/bundle.js`); expect(response1.statusCode).toEqual(200); @@ -4247,18 +4254,38 @@ describe.each([ const response2 = await req .get(`/bundle.js`) - .set("if-match", response1.headers.etag); + .set("if-none-match", response1.headers.etag); expect(response2.statusCode).toEqual(304); expect(response2.headers.etag).toBeDefined(); expect(response2.headers.etag.startsWith("W/")).toBe(true); - const response3 = await req.get(`/bundle.js`).set("if-match", "test"); + const response3 = await req + .get(`/bundle.js`) + .set("if-none-match", `${response1.headers.etag}, test`); - expect(response3.statusCode).toEqual(412); + expect(response3.statusCode).toEqual(304); + expect(response3.headers.etag).toBeDefined(); + expect(response3.headers.etag.startsWith("W/")).toBe(true); + + const response4 = await req + .get(`/bundle.js`) + .set("if-none-match", "*"); + + expect(response4.statusCode).toEqual(304); + expect(response4.headers.etag).toBeDefined(); + expect(response4.headers.etag.startsWith("W/")).toBe(true); + + const response5 = await req + .get(`/bundle.js`) + .set("if-none-match", "test"); + + expect(response5.statusCode).toEqual(200); + expect(response5.headers.etag).toBeDefined(); + expect(response5.headers.etag.startsWith("W/")).toBe(true); }); - it('should return the "304" code for the "GET" request to the bundle file with etag "if-none-match" header', async () => { + it('should return the "200" code for the "GET" request to the bundle file with etag and "if-match" header', async () => { const response1 = await req.get(`/bundle.js`); expect(response1.statusCode).toEqual(200); @@ -4267,19 +4294,29 @@ describe.each([ const response2 = await req .get(`/bundle.js`) - .set("if-none-match", response1.headers.etag); + .set("if-match", response1.headers.etag); - expect(response2.statusCode).toEqual(304); + expect(response2.statusCode).toEqual(200); expect(response2.headers.etag).toBeDefined(); expect(response2.headers.etag.startsWith("W/")).toBe(true); const response3 = await req .get(`/bundle.js`) - .set("if-none-match", "test"); + .set("if-match", `${response1.headers.etag}, foo`); expect(response3.statusCode).toEqual(200); expect(response3.headers.etag).toBeDefined(); expect(response3.headers.etag.startsWith("W/")).toBe(true); + + const response4 = await req.get(`/bundle.js`).set("if-match", "*"); + + expect(response4.statusCode).toEqual(200); + expect(response4.headers.etag).toBeDefined(); + expect(response4.headers.etag.startsWith("W/")).toBe(true); + + const response5 = await req.get(`/bundle.js`).set("if-match", "test"); + + expect(response5.statusCode).toEqual(412); }); it('should return the "412" code for the "GET" request to the bundle file with etag and wrong "if-match" header', async () => { @@ -4310,10 +4347,25 @@ describe.each([ expect(response2.headers.etag).toBeDefined(); expect(response2.headers.etag.startsWith("W/")).toBe(true); }); + + it('should return the "206" code for the "GET" request with the valid range header and "if-range" header', async () => { + const response = await req + .get("/bundle.js") + .set("if-range", '"test"') + .set("Range", "bytes=3000-3500"); + + expect(response.statusCode).toEqual(200); + expect(response.headers["content-type"]).toEqual( + "application/javascript; charset=utf-8", + ); + expect(response.headers.etag).toBeDefined(); + expect(response.headers.etag.startsWith("W/")).toBe(true); + }); }); describe("should work and generate strong etag", () => { beforeEach(async () => { + const outputPath = path.resolve(__dirname, "./outputs/basic"); const compiler = getCompiler(webpackConfig); [server, req, instance] = await frameworkFactory( @@ -4324,13 +4376,21 @@ describe.each([ etag: "strong", }, ); + + instance.context.outputFileSystem.mkdirSync(outputPath, { + recursive: true, + }); + instance.context.outputFileSystem.writeFileSync( + path.resolve(outputPath, "file.txt"), + "", + ); }); afterEach(async () => { await close(server, instance); }); - it('should return the "200" code for the "GET" request to the bundle file and set weak etag', async () => { + it('should return the "200" code for the "GET" request to the bundle file and set strong etag', async () => { const response = await req.get(`/bundle.js`); expect(response.statusCode).toEqual(200); @@ -4339,6 +4399,37 @@ describe.each([ '"18c7-l/LCspQS5fbbf1kkLGOsK9FTpbg"', ); }); + + it('should return the "200" code for the "GET" request to the file.txt and set strong etag on empty file', async () => { + const response = await req.get(`/file.txt`); + + expect(response.statusCode).toEqual(200); + expect(response.headers.etag).toBe( + /* cspell:disable-next-line */ + '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"', + ); + }); + + it('should return the "200" code for the "GET" request with the valid range header and wrong "If-Range" header', async () => { + const response = await req + .get("/bundle.js") + .set("Range", "bytes=3000-3500"); + + expect(response.statusCode).toEqual(206); + expect(response.headers["content-length"]).toEqual("501"); + expect(response.headers["content-type"]).toEqual( + "application/javascript; charset=utf-8", + ); + expect(response.text.length).toBe(501); + expect(response.headers.etag).toBeDefined(); + + const response1 = await req + .get("/bundle.js") + .set("If-Range", '"test') + .set("Range", "bytes=3000-3500"); + + expect(response1.statusCode).toEqual(200); + }); }); describe("should work and generate strong etag without createReadStream", () => { @@ -4371,6 +4462,31 @@ describe.each([ ); }); }); + + describe("should work and without etag generation and `if-none-match` header", () => { + beforeEach(async () => { + const compiler = getCompiler(webpackConfig); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it('should return the "200" code for the "GET" request to the bundle file and `if-none-match` header without etag', async () => { + const response = await req + .get(`/bundle.js`) + .set("if-none-match", "etag"); + + expect(response.statusCode).toEqual(200); + expect(response.headers.etag).toBeUndefined(); + }); + }); }); describe("lastModified", () => { @@ -4392,13 +4508,6 @@ describe.each([ await close(server, instance); }); - function parseHttpDate(date) { - const timestamp = date && Date.parse(date); - - // istanbul ignore next: guard against date.js Date.parse patching - return typeof timestamp === "number" ? timestamp : NaN; - } - it('should return the "200" code for the "GET" request to the bundle file and set "Last-Modified"', async () => { const response = await req.get(`/bundle.js`); @@ -4406,7 +4515,7 @@ describe.each([ expect(response.headers["last-modified"]).toBeDefined(); }); - it('should return the "304" code for the "GET" request to the bundle file with "Last-Modified" and "if-unmodified-since" header', async () => { + it('should return the "304" code for the "GET" request to the bundle file with "Last-Modified" and "if-modified-since" header', async () => { const response1 = await req.get(`/bundle.js`); expect(response1.statusCode).toEqual(200); @@ -4414,19 +4523,25 @@ describe.each([ const response2 = await req .get(`/bundle.js`) - .set("if-unmodified-since", response1.headers["last-modified"]); + .set("if-modified-since", response1.headers["last-modified"]); expect(response2.statusCode).toEqual(304); expect(response2.headers["last-modified"]).toBeDefined(); const response3 = await req .get(`/bundle.js`) - .set("if-unmodified-since", "Fri, 29 Mar 2020 10:25:50 GMT"); + .set( + "if-modified-since", + new Date( + parseHttpDate(response1.headers["last-modified"]) - 1000, + ).toUTCString(), + ); - expect(response3.statusCode).toEqual(412); + expect(response3.statusCode).toEqual(200); + expect(response3.headers["last-modified"]).toBeDefined(); }); - it('should return the "304" code for the "GET" request to the bundle file with "Last-Modified" and "if-modified-since" header', async () => { + it('should return the "200" code for the "GET" request to the bundle file with "Last-Modified" and "if-unmodified-since" header', async () => { const response1 = await req.get(`/bundle.js`); expect(response1.statusCode).toEqual(200); @@ -4434,22 +4549,16 @@ describe.each([ const response2 = await req .get(`/bundle.js`) - .set("if-modified-since", response1.headers["last-modified"]); + .set("if-unmodified-since", response1.headers["last-modified"]); - expect(response2.statusCode).toEqual(304); + expect(response2.statusCode).toEqual(200); expect(response2.headers["last-modified"]).toBeDefined(); const response3 = await req .get(`/bundle.js`) - .set( - "if-modified-since", - new Date( - parseHttpDate(response1.headers["last-modified"]) - 1000, - ).toUTCString(), - ); + .set("if-unmodified-since", "Fri, 29 Mar 2020 10:25:50 GMT"); - expect(response3.statusCode).toEqual(200); - expect(response3.headers["last-modified"]).toBeDefined(); + expect(response3.statusCode).toEqual(412); }); it('should return the "412" code for the "GET" request to the bundle file with etag and "if-unmodified-since" header', async () => { @@ -4484,6 +4593,19 @@ describe.each([ expect(response2.statusCode).toEqual(200); expect(response1.headers["last-modified"]).toBeDefined(); }); + + it('should return the "200" code for the "GET" request with the valid range header and old "if-range" header', async () => { + const response = await req + .get("/bundle.js") + .set("if-range", new Date(1000).toUTCString()) + .set("Range", "bytes=3000-3500"); + + expect(response.statusCode).toEqual(200); + expect(response.headers["content-type"]).toEqual( + "application/javascript; charset=utf-8", + ); + expect(response.headers["last-modified"]).toBeDefined(); + }); }); describe('should work and prefer "if-match" and "if-none-match"', () => { @@ -4505,13 +4627,6 @@ describe.each([ await close(server, instance); }); - function parseHttpDate(date) { - const timestamp = date && Date.parse(date); - - // istanbul ignore next: guard against date.js Date.parse patching - return typeof timestamp === "number" ? timestamp : NaN; - } - it('should return the "304" code for the "GET" request to the bundle file and prefer "if-match" over "if-unmodified-since"', async () => { const response1 = await req.get(`/bundle.js`); @@ -4529,7 +4644,7 @@ describe.each([ ).toUTCString(), ); - expect(response2.statusCode).toEqual(304); + expect(response2.statusCode).toEqual(200); expect(response2.headers["last-modified"]).toBeDefined(); expect(response2.headers.etag).toBeDefined(); }); diff --git a/types/index.d.ts b/types/index.d.ts index 1080632b0..ac7d038ca 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -38,7 +38,7 @@ export = wdm; */ /** * @typedef {Object} ResponseData - * @property {string | Buffer | ReadStream} data + * @property {Buffer | ReadStream} data * @property {number} byteLength */ /** @@ -47,7 +47,7 @@ export = wdm; * @callback ModifyResponseData * @param {RequestInternal} req * @param {ResponseInternal} res - * @param {string | Buffer | ReadStream} data + * @param {Buffer | ReadStream} data * @param {number} byteLength * @return {ResponseData} */ @@ -284,7 +284,7 @@ type Callback = ( stats?: import("webpack").Stats | import("webpack").MultiStats | undefined, ) => any; type ResponseData = { - data: string | Buffer | ReadStream; + data: Buffer | ReadStream; byteLength: number; }; type ModifyResponseData< @@ -293,7 +293,7 @@ type ModifyResponseData< > = ( req: RequestInternal, res: ResponseInternal, - data: string | Buffer | ReadStream, + data: Buffer | ReadStream, byteLength: number, ) => ResponseData; type Context< diff --git a/types/utils/compatibleAPI.d.ts b/types/utils/compatibleAPI.d.ts index 369054f7e..0998c346d 100644 --- a/types/utils/compatibleAPI.d.ts +++ b/types/utils/compatibleAPI.d.ts @@ -45,3 +45,19 @@ export function pipe( res: Response & ExpectedResponse, bufferOrStream: import("fs").ReadStream, ): void; +/** + * @param {string} filename + * @param {import("../index").OutputFileSystem} outputFileSystem + * @param {number} start + * @param {number} end + * @returns {{ bufferOrStream: (Buffer | import("fs").ReadStream), byteLength: number }} + */ +export function createReadStreamOrReadFileSync( + filename: string, + outputFileSystem: import("../index").OutputFileSystem, + start: number, + end: number, +): { + bufferOrStream: Buffer | import("fs").ReadStream; + byteLength: number; +}; diff --git a/types/utils/memorize.d.ts b/types/utils/memorize.d.ts new file mode 100644 index 000000000..6313160a0 --- /dev/null +++ b/types/utils/memorize.d.ts @@ -0,0 +1,26 @@ +export = memorize; +/** + * @template T + * @param {Function} fn + * @param {{ cache?: Map } | undefined} cache + * @param {((value: T) => T)=} callback + * @returns {any} + */ +declare function memorize( + fn: Function, + { + cache, + }?: + | { + cache?: + | Map< + string, + { + data: T; + } + > + | undefined; + } + | undefined, + callback?: ((value: T) => T) | undefined, +): any; From 7b666a1f82bed669c1de427931ad471e7db2b1d2 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Fri, 29 Mar 2024 18:18:39 +0300 Subject: [PATCH 13/16] refactor: better types (#1801) --- src/index.js | 28 ++++++++--------- src/middleware.js | 1 + src/utils/getFilenameFromUrl.js | 1 + types/index.d.ts | 54 ++++++++++++++++++--------------- 4 files changed, 46 insertions(+), 38 deletions(-) diff --git a/src/index.js b/src/index.js index de2419f10..1fc9ccdff 100644 --- a/src/index.js +++ b/src/index.js @@ -64,8 +64,8 @@ const noop = () => {}; */ /** - * @template {IncomingMessage} RequestInternal - * @template {ServerResponse} ResponseInternal + * @template {IncomingMessage} [RequestInternal=IncomingMessage] + * @template {ServerResponse} [ResponseInternal=ServerResponse] * @callback ModifyResponseData * @param {RequestInternal} req * @param {ResponseInternal} res @@ -75,8 +75,8 @@ const noop = () => {}; */ /** - * @template {IncomingMessage} RequestInternal - * @template {ServerResponse} ResponseInternal + * @template {IncomingMessage} [RequestInternal=IncomingMessage] + * @template {ServerResponse} [ResponseInternal=ServerResponse] * @typedef {Object} Context * @property {boolean} state * @property {Stats | MultiStats | undefined} stats @@ -89,16 +89,16 @@ const noop = () => {}; */ /** - * @template {IncomingMessage} RequestInternal - * @template {ServerResponse} ResponseInternal + * @template {IncomingMessage} [RequestInternal=IncomingMessage] + * @template {ServerResponse} [ResponseInternal=ServerResponse] * @typedef {WithoutUndefined, "watching">} FilledContext */ /** @typedef {Record | Array<{ key: string, value: number | string }>} NormalizedHeaders */ /** - * @template {IncomingMessage} RequestInternal - * @template {ServerResponse} ResponseInternal + * @template {IncomingMessage} [RequestInternal=IncomingMessage] + * @template {ServerResponse} [ResponseInternal=ServerResponse] * @typedef {NormalizedHeaders | ((req: RequestInternal, res: ResponseInternal, context: Context) => void | undefined | NormalizedHeaders) | undefined} Headers */ @@ -122,8 +122,8 @@ const noop = () => {}; */ /** - * @template {IncomingMessage} RequestInternal - * @template {ServerResponse} ResponseInternal + * @template {IncomingMessage} [RequestInternal=IncomingMessage] + * @template {ServerResponse} [ResponseInternal=ServerResponse] * @callback Middleware * @param {RequestInternal} req * @param {ResponseInternal} res @@ -167,8 +167,8 @@ const noop = () => {}; */ /** - * @template {IncomingMessage} RequestInternal - * @template {ServerResponse} ResponseInternal + * @template {IncomingMessage} [RequestInternal=IncomingMessage] + * @template {ServerResponse} [ResponseInternal=ServerResponse] * @typedef {Middleware & AdditionalMethods} API */ @@ -366,7 +366,7 @@ function koaWrapper(compiler, options) { const devMiddleware = wdm(compiler, options); /** - * @param {any} ctx + * @param {{ req: RequestInternal, res: ResponseInternal & import("./utils/compatibleAPI").ExpectedResponse, status: number, body: Buffer | import("fs").ReadStream | { message: string }, state: Object }} ctx * @param {Function} next * @returns {Promise} */ @@ -392,7 +392,7 @@ function koaWrapper(compiler, options) { resolve(); }; /** - * @param {string | Buffer} content content + * @param {Buffer} content content */ res.send = (content) => { // eslint-disable-next-line no-param-reassign diff --git a/src/middleware.js b/src/middleware.js index dcf1217ef..a8bbe50ed 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -524,6 +524,7 @@ function wrapper(context) { /** @type {import("fs").Stats | Buffer | ReadStream | undefined} */ let value; + // TODO cache etag generation? if (context.options.etag === "weak") { value = /** @type {import("fs").Stats} */ (extra.stats); } else { diff --git a/src/utils/getFilenameFromUrl.js b/src/utils/getFilenameFromUrl.js index 319b149db..67852ee8b 100644 --- a/src/utils/getFilenameFromUrl.js +++ b/src/utils/getFilenameFromUrl.js @@ -40,6 +40,7 @@ function decode(input) { } // TODO refactor me in the next major release, this function should return `{ filename, stats, error }` +// TODO fix redirect logic when `/` at the end, like https://github.com/pillarjs/send/blob/master/index.js#L586 /** * @template {IncomingMessage} Request * @template {ServerResponse} Response diff --git a/types/index.d.ts b/types/index.d.ts index ac7d038ca..54f15e97b 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -42,8 +42,8 @@ export = wdm; * @property {number} byteLength */ /** - * @template {IncomingMessage} RequestInternal - * @template {ServerResponse} ResponseInternal + * @template {IncomingMessage} [RequestInternal=IncomingMessage] + * @template {ServerResponse} [ResponseInternal=ServerResponse] * @callback ModifyResponseData * @param {RequestInternal} req * @param {ResponseInternal} res @@ -52,8 +52,8 @@ export = wdm; * @return {ResponseData} */ /** - * @template {IncomingMessage} RequestInternal - * @template {ServerResponse} ResponseInternal + * @template {IncomingMessage} [RequestInternal=IncomingMessage] + * @template {ServerResponse} [ResponseInternal=ServerResponse] * @typedef {Object} Context * @property {boolean} state * @property {Stats | MultiStats | undefined} stats @@ -65,14 +65,14 @@ export = wdm; * @property {OutputFileSystem} outputFileSystem */ /** - * @template {IncomingMessage} RequestInternal - * @template {ServerResponse} ResponseInternal + * @template {IncomingMessage} [RequestInternal=IncomingMessage] + * @template {ServerResponse} [ResponseInternal=ServerResponse] * @typedef {WithoutUndefined, "watching">} FilledContext */ /** @typedef {Record | Array<{ key: string, value: number | string }>} NormalizedHeaders */ /** - * @template {IncomingMessage} RequestInternal - * @template {ServerResponse} ResponseInternal + * @template {IncomingMessage} [RequestInternal=IncomingMessage] + * @template {ServerResponse} [ResponseInternal=ServerResponse] * @typedef {NormalizedHeaders | ((req: RequestInternal, res: ResponseInternal, context: Context) => void | undefined | NormalizedHeaders) | undefined} Headers */ /** @@ -94,8 +94,8 @@ export = wdm; * @property {boolean} [lastModified] */ /** - * @template {IncomingMessage} RequestInternal - * @template {ServerResponse} ResponseInternal + * @template {IncomingMessage} [RequestInternal=IncomingMessage] + * @template {ServerResponse} [ResponseInternal=ServerResponse] * @callback Middleware * @param {RequestInternal} req * @param {ResponseInternal} res @@ -132,8 +132,8 @@ export = wdm; * @property {Context} context */ /** - * @template {IncomingMessage} RequestInternal - * @template {ServerResponse} ResponseInternal + * @template {IncomingMessage} [RequestInternal=IncomingMessage] + * @template {ServerResponse} [ResponseInternal=ServerResponse] * @typedef {Middleware & AdditionalMethods} API */ /** @@ -207,8 +207,9 @@ declare namespace wdm { type Compiler = import("webpack").Compiler; type MultiCompiler = import("webpack").MultiCompiler; type API< - RequestInternal extends import("http").IncomingMessage, - ResponseInternal extends ServerResponse, + RequestInternal extends + import("http").IncomingMessage = import("http").IncomingMessage, + ResponseInternal extends ServerResponse = ServerResponse, > = Middleware & AdditionalMethods; /** @@ -288,8 +289,9 @@ type ResponseData = { byteLength: number; }; type ModifyResponseData< - RequestInternal extends import("http").IncomingMessage, - ResponseInternal extends ServerResponse, + RequestInternal extends + import("http").IncomingMessage = import("http").IncomingMessage, + ResponseInternal extends ServerResponse = ServerResponse, > = ( req: RequestInternal, res: ResponseInternal, @@ -297,8 +299,9 @@ type ModifyResponseData< byteLength: number, ) => ResponseData; type Context< - RequestInternal extends import("http").IncomingMessage, - ResponseInternal extends ServerResponse, + RequestInternal extends + import("http").IncomingMessage = import("http").IncomingMessage, + ResponseInternal extends ServerResponse = ServerResponse, > = { state: boolean; stats: Stats | MultiStats | undefined; @@ -310,8 +313,9 @@ type Context< outputFileSystem: OutputFileSystem; }; type FilledContext< - RequestInternal extends import("http").IncomingMessage, - ResponseInternal extends ServerResponse, + RequestInternal extends + import("http").IncomingMessage = import("http").IncomingMessage, + ResponseInternal extends ServerResponse = ServerResponse, > = WithoutUndefined, "watching">; type NormalizedHeaders = | Record @@ -320,8 +324,9 @@ type NormalizedHeaders = value: number | string; }>; type Headers< - RequestInternal extends import("http").IncomingMessage, - ResponseInternal extends ServerResponse, + RequestInternal extends + import("http").IncomingMessage = import("http").IncomingMessage, + ResponseInternal extends ServerResponse = ServerResponse, > = | NormalizedHeaders | (( @@ -356,8 +361,9 @@ type Options< lastModified?: boolean | undefined; }; type Middleware< - RequestInternal extends import("http").IncomingMessage, - ResponseInternal extends ServerResponse, + RequestInternal extends + import("http").IncomingMessage = import("http").IncomingMessage, + ResponseInternal extends ServerResponse = ServerResponse, > = ( req: RequestInternal, res: ResponseInternal, From 95c2fb06b7072e4c60f0d781dc8d27e2f990f318 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Fri, 29 Mar 2024 18:48:30 +0300 Subject: [PATCH 14/16] test: refactor (#1802) --- test/middleware.test.js | 507 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 507 insertions(+) diff --git a/test/middleware.test.js b/test/middleware.test.js index cab109ba7..1158467b6 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -10,6 +10,8 @@ import request from "supertest"; import memfs, { createFsFromVolume, Volume } from "memfs"; import del from "del"; +import { Stats } from "webpack"; + import middleware from "../src"; import getCompiler from "./helpers/getCompiler"; @@ -20,6 +22,8 @@ import webpackWatchOptionsConfig from "./fixtures/webpack.watch-options.config"; import webpackMultiWatchOptionsConfig from "./fixtures/webpack.array.watch-options.config"; import webpackQueryStringConfig from "./fixtures/webpack.querystring.config"; import webpackClientServerConfig from "./fixtures/webpack.client.server.config"; +import getCompilerHooks from "./helpers/getCompilerHooks"; +import webpackPublicPathConfig from "./fixtures/webpack.public-path.config"; // Suppress unnecessary stats output global.console.log = jest.fn(); @@ -261,6 +265,509 @@ describe.each([ let server; let req; + describe("API", () => { + let compiler; + + describe("constructor", () => { + describe("should accept compiler", () => { + beforeEach(async () => { + compiler = getCompiler(webpackConfig); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it("should work", (done) => { + const doneSpy = jest.spyOn( + getCompilerHooks(compiler).done[0], + "fn", + ); + + instance.waitUntilValid(() => { + instance.close(); + + expect(compiler.running).toBe(false); + expect(doneSpy).toHaveBeenCalledTimes(1); + + doneSpy.mockRestore(); + + done(); + }); + }); + }); + + describe("should accept compiler in watch mode", () => { + beforeEach(async () => { + compiler = getCompiler({ ...webpackConfig, ...{ watch: true } }); + + instance = middleware(compiler); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it("should work", (done) => { + const doneSpy = jest.spyOn( + getCompilerHooks(compiler).done[0], + "fn", + ); + + instance.waitUntilValid(() => { + instance.close(); + + expect(compiler.running).toBe(false); + expect(doneSpy).toHaveBeenCalledTimes(1); + + doneSpy.mockRestore(); + + done(); + }); + }); + }); + }); + + describe("waitUntilValid method", () => { + beforeEach(async () => { + compiler = getCompiler(webpackConfig); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it("should work without callback", (done) => { + const doneSpy = jest.spyOn(getCompilerHooks(compiler).done[0], "fn"); + + instance.waitUntilValid(); + + const intervalId = setInterval(() => { + if (instance.context.state) { + expect(compiler.running).toBe(true); + expect(instance.context.state).toBe(true); + expect(doneSpy).toHaveBeenCalledTimes(1); + expect(doneSpy.mock.calls[0][0]).toBeInstanceOf(Stats); + + doneSpy.mockRestore(); + + clearInterval(intervalId); + + done(); + } + }); + }); + + it("should work with callback", (done) => { + const doneSpy = jest.spyOn(getCompilerHooks(compiler).done[0], "fn"); + let callbackCounter = 0; + + instance.waitUntilValid(() => { + callbackCounter += 1; + }); + + const intervalId = setInterval(() => { + if (instance.context.state) { + expect(compiler.running).toBe(true); + expect(instance.context.state).toBe(true); + expect(callbackCounter).toBe(1); + expect(doneSpy).toHaveBeenCalledTimes(1); + + doneSpy.mockRestore(); + + clearInterval(intervalId); + + done(); + } + }); + }); + + it("should run callback immediately when state already valid", (done) => { + const doneSpy = jest.spyOn(getCompilerHooks(compiler).done[0], "fn"); + let callbackCounter = 0; + let validToCheck = false; + + instance.waitUntilValid(() => { + callbackCounter += 1; + + instance.waitUntilValid(() => { + validToCheck = true; + callbackCounter += 1; + }); + }); + + const intervalId = setInterval(() => { + if (instance.context.state && validToCheck) { + expect(compiler.running).toBe(true); + expect(instance.context.state).toBe(true); + expect(callbackCounter).toBe(2); + expect(doneSpy).toHaveBeenCalledTimes(1); + + doneSpy.mockRestore(); + + clearInterval(intervalId); + + done(); + } + }); + }); + }); + + describe("invalidate method", () => { + beforeEach(async () => { + compiler = getCompiler(webpackConfig); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it("should work without callback", (done) => { + const doneSpy = jest.spyOn(getCompilerHooks(compiler).done[0], "fn"); + + instance.invalidate(); + + const intervalId = setInterval(() => { + if (instance.context.state) { + expect(compiler.running).toBe(true); + expect(instance.context.state).toBe(true); + expect(doneSpy).toHaveBeenCalledTimes(1); + + doneSpy.mockRestore(); + + clearInterval(intervalId); + + done(); + } + }); + }); + + it("should work with callback", (done) => { + const doneSpy = jest.spyOn(getCompilerHooks(compiler).done[0], "fn"); + let callbackCounter = 0; + + instance.invalidate(() => { + callbackCounter += 1; + }); + + const intervalId = setInterval(() => { + if (instance.context.state) { + expect(compiler.running).toBe(true); + expect(instance.context.state).toBe(true); + expect(callbackCounter).toBe(1); + expect(doneSpy).toHaveBeenCalledTimes(1); + + doneSpy.mockRestore(); + + clearInterval(intervalId); + + done(); + } + }); + }); + }); + + describe("getFilenameFromUrl method", () => { + describe("should work", () => { + beforeEach(async () => { + compiler = getCompiler(webpackConfig); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it("should work", (done) => { + instance.waitUntilValid(() => { + expect(instance.getFilenameFromUrl("/bundle.js")).toBe( + path.join(webpackConfig.output.path, "/bundle.js"), + ); + expect(instance.getFilenameFromUrl("/")).toBe( + path.join(webpackConfig.output.path, "/index.html"), + ); + expect(instance.getFilenameFromUrl("/index.html")).toBe( + path.join(webpackConfig.output.path, "/index.html"), + ); + expect(instance.getFilenameFromUrl("/svg.svg")).toBe( + path.join(webpackConfig.output.path, "/svg.svg"), + ); + expect( + instance.getFilenameFromUrl("/unknown.unknown"), + ).toBeUndefined(); + expect( + instance.getFilenameFromUrl("/unknown/unknown.unknown"), + ).toBeUndefined(); + + done(); + }); + }); + }); + + describe('should work when the "index" option disabled', () => { + beforeEach(async () => { + compiler = getCompiler(webpackConfig); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + index: false, + }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it("should work", (done) => { + instance.waitUntilValid(() => { + expect(instance.getFilenameFromUrl("/bundle.js")).toBe( + path.join(webpackConfig.output.path, "/bundle.js"), + ); + // eslint-disable-next-line no-undefined + expect(instance.getFilenameFromUrl("/")).toBe(undefined); + expect(instance.getFilenameFromUrl("/index.html")).toBe( + path.join(webpackConfig.output.path, "/index.html"), + ); + expect(instance.getFilenameFromUrl("/svg.svg")).toBe( + path.join(webpackConfig.output.path, "/svg.svg"), + ); + expect( + instance.getFilenameFromUrl("/unknown.unknown"), + ).toBeUndefined(); + expect( + instance.getFilenameFromUrl("/unknown/unknown.unknown"), + ).toBeUndefined(); + + done(); + }); + }); + }); + + describe('should work with the "publicPath"', () => { + beforeEach(async () => { + compiler = getCompiler(webpackPublicPathConfig); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it("should work", (done) => { + instance.waitUntilValid(() => { + expect( + instance.getFilenameFromUrl("/public/path/bundle.js"), + ).toBe( + path.join(webpackPublicPathConfig.output.path, "/bundle.js"), + ); + expect(instance.getFilenameFromUrl("/public/path/")).toBe( + path.join(webpackPublicPathConfig.output.path, "/index.html"), + ); + expect( + instance.getFilenameFromUrl("/public/path/index.html"), + ).toBe( + path.join(webpackPublicPathConfig.output.path, "/index.html"), + ); + expect(instance.getFilenameFromUrl("/public/path/svg.svg")).toBe( + path.join(webpackPublicPathConfig.output.path, "/svg.svg"), + ); + + expect(instance.getFilenameFromUrl("/")).toBeUndefined(); + expect( + instance.getFilenameFromUrl("/unknown.unknown"), + ).toBeUndefined(); + expect( + instance.getFilenameFromUrl("/unknown/unknown.unknown"), + ).toBeUndefined(); + + done(); + }); + }); + }); + + describe("should work in multi compiler mode", () => { + beforeEach(async () => { + compiler = getCompiler(webpackMultiConfig); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it("should work", (done) => { + instance.waitUntilValid(() => { + expect(instance.getFilenameFromUrl("/static-one/bundle.js")).toBe( + path.join(webpackMultiConfig[0].output.path, "/bundle.js"), + ); + expect(instance.getFilenameFromUrl("/static-one/")).toBe( + path.join(webpackMultiConfig[0].output.path, "/index.html"), + ); + expect( + instance.getFilenameFromUrl("/static-one/index.html"), + ).toBe( + path.join(webpackMultiConfig[0].output.path, "/index.html"), + ); + expect(instance.getFilenameFromUrl("/static-one/svg.svg")).toBe( + path.join(webpackMultiConfig[0].output.path, "/svg.svg"), + ); + expect( + instance.getFilenameFromUrl("/static-one/unknown.unknown"), + ).toBeUndefined(); + expect( + instance.getFilenameFromUrl( + "/static-one/unknown/unknown.unknown", + ), + ).toBeUndefined(); + + expect(instance.getFilenameFromUrl("/static-two/bundle.js")).toBe( + path.join(webpackMultiConfig[1].output.path, "/bundle.js"), + ); + expect( + instance.getFilenameFromUrl("/static-two/unknown.unknown"), + ).toBeUndefined(); + expect( + instance.getFilenameFromUrl( + "/static-two/unknown/unknown.unknown", + ), + ).toBeUndefined(); + + expect(instance.getFilenameFromUrl("/")).toBeUndefined(); + expect( + instance.getFilenameFromUrl("/static-one/unknown.unknown"), + ).toBeUndefined(); + expect( + instance.getFilenameFromUrl( + "/static-one/unknown/unknown.unknown", + ), + ).toBeUndefined(); + + done(); + }); + }); + }); + }); + + describe("close method", () => { + beforeEach(async () => { + compiler = getCompiler(webpackConfig); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it("should work without callback", (done) => { + const doneSpy = jest.spyOn(getCompilerHooks(compiler).done[0], "fn"); + + instance.waitUntilValid(() => { + instance.close(); + + expect(compiler.running).toBe(false); + expect(doneSpy).toHaveBeenCalledTimes(1); + + doneSpy.mockRestore(); + + done(); + }); + }); + + it("should work with callback", (done) => { + const doneSpy = jest.spyOn(getCompilerHooks(compiler).done[0], "fn"); + + instance.waitUntilValid(() => { + instance.close(() => { + expect(compiler.running).toBe(false); + expect(doneSpy).toHaveBeenCalledTimes(1); + + doneSpy.mockRestore(); + + done(); + }); + }); + }); + }); + + describe("context property", () => { + beforeEach(async () => { + compiler = getCompiler(webpackConfig); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + it("should contain public properties", (done) => { + expect(instance.context.state).toBeDefined(); + expect(instance.context.options).toBeDefined(); + expect(instance.context.compiler).toBeDefined(); + expect(instance.context.watching).toBeDefined(); + expect(instance.context.outputFileSystem).toBeDefined(); + + // the compilation needs to finish, as it will still be running + // after the test is done if not finished, potentially impacting other tests + compiler.hooks.done.tap("wdm-test", () => { + done(); + }); + }); + }); + }); + describe("basic", () => { describe("should work", () => { let compiler; From 7e1fe441f73f3f694e663d296f3a8870179e8976 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Fri, 29 Mar 2024 18:51:30 +0300 Subject: [PATCH 15/16] refactor: tests were rewritten, we don't need it anymore (#1803) --- test/api.test.js | 747 ----------------------------------------------- 1 file changed, 747 deletions(-) delete mode 100644 test/api.test.js diff --git a/test/api.test.js b/test/api.test.js deleted file mode 100644 index b1b2716a2..000000000 --- a/test/api.test.js +++ /dev/null @@ -1,747 +0,0 @@ -import path from "path"; - -import express from "express"; -import connect from "connect"; -import webpack, { Stats } from "webpack"; - -import middleware from "../src"; - -import getCompiler from "./helpers/getCompiler"; -import getCompilerHooks from "./helpers/getCompilerHooks"; -import webpackConfig from "./fixtures/webpack.config"; -import webpackPublicPathConfig from "./fixtures/webpack.public-path.config"; -import webpackMultiConfig from "./fixtures/webpack.array.config"; - -// Suppress unnecessary stats output -global.console.log = jest.fn(); - -describe.each([ - ["express", express], - ["connect", connect], -])("%s framework:", (_, framework) => { - describe("API", () => { - let instance; - let listen; - let app; - let compiler; - - describe("constructor", () => { - describe("should accept compiler", () => { - beforeEach((done) => { - compiler = webpack(webpackConfig); - - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = app.listen((error) => { - if (error) { - return done(error); - } - - return done(); - }); - }); - - afterEach((done) => { - if (instance.context.watching.closed) { - if (listen) { - listen.close(done); - } else { - done(); - } - - return; - } - - instance.close(() => { - if (listen) { - listen.close(done); - } else { - done(); - } - }); - }); - - it("should work", (done) => { - const doneSpy = jest.spyOn(getCompilerHooks(compiler).done[0], "fn"); - - instance.waitUntilValid(() => { - instance.close(); - - expect(compiler.running).toBe(false); - expect(doneSpy).toHaveBeenCalledTimes(1); - - doneSpy.mockRestore(); - - done(); - }); - }); - }); - - describe("should accept compiler in watch mode", () => { - beforeEach((done) => { - compiler = webpack( - { ...webpackConfig, ...{ watch: true } }, - (error) => { - if (error) { - throw error; - } - }, - ); - - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = app.listen((error) => { - if (error) { - return done(error); - } - - return done(); - }); - }); - - afterEach((done) => { - if (instance.context.watching.closed) { - if (listen) { - listen.close(done); - } else { - done(); - } - - return; - } - - instance.close(() => { - if (listen) { - listen.close(done); - } else { - done(); - } - }); - }); - - it("should work", (done) => { - const doneSpy = jest.spyOn(getCompilerHooks(compiler).done[0], "fn"); - - instance.waitUntilValid(() => { - instance.close(); - - expect(compiler.running).toBe(false); - expect(doneSpy).toHaveBeenCalledTimes(1); - - doneSpy.mockRestore(); - - done(); - }); - }); - }); - }); - - describe("waitUntilValid method", () => { - beforeEach((done) => { - compiler = getCompiler(webpackConfig); - - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = app.listen((error) => { - if (error) { - return done(error); - } - - return done(); - }); - }); - - afterEach((done) => { - if (instance.context.watching.closed) { - if (listen) { - listen.close(done); - } else { - done(); - } - - return; - } - - instance.close(() => { - if (listen) { - listen.close(done); - } else { - done(); - } - }); - }); - - it("should work without callback", (done) => { - const doneSpy = jest.spyOn(getCompilerHooks(compiler).done[0], "fn"); - - instance.waitUntilValid(); - - const intervalId = setInterval(() => { - if (instance.context.state) { - expect(compiler.running).toBe(true); - expect(instance.context.state).toBe(true); - expect(doneSpy).toHaveBeenCalledTimes(1); - expect(doneSpy.mock.calls[0][0]).toBeInstanceOf(Stats); - - doneSpy.mockRestore(); - - clearInterval(intervalId); - - done(); - } - }); - }); - - it("should work with callback", (done) => { - const doneSpy = jest.spyOn(getCompilerHooks(compiler).done[0], "fn"); - let callbackCounter = 0; - - instance.waitUntilValid(() => { - callbackCounter += 1; - }); - - const intervalId = setInterval(() => { - if (instance.context.state) { - expect(compiler.running).toBe(true); - expect(instance.context.state).toBe(true); - expect(callbackCounter).toBe(1); - expect(doneSpy).toHaveBeenCalledTimes(1); - - doneSpy.mockRestore(); - - clearInterval(intervalId); - - done(); - } - }); - }); - - it("should run callback immediately when state already valid", (done) => { - const doneSpy = jest.spyOn(getCompilerHooks(compiler).done[0], "fn"); - let callbackCounter = 0; - let validToCheck = false; - - instance.waitUntilValid(() => { - callbackCounter += 1; - - instance.waitUntilValid(() => { - validToCheck = true; - callbackCounter += 1; - }); - }); - - const intervalId = setInterval(() => { - if (instance.context.state && validToCheck) { - expect(compiler.running).toBe(true); - expect(instance.context.state).toBe(true); - expect(callbackCounter).toBe(2); - expect(doneSpy).toHaveBeenCalledTimes(1); - - doneSpy.mockRestore(); - - clearInterval(intervalId); - - done(); - } - }); - }); - }); - - describe("invalidate method", () => { - beforeEach((done) => { - compiler = getCompiler(webpackConfig); - - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = app.listen((error) => { - if (error) { - return done(error); - } - - return done(); - }); - }); - - afterEach((done) => { - if (instance.context.watching.closed) { - if (listen) { - listen.close(done); - } else { - done(); - } - - return; - } - - instance.close(() => { - if (listen) { - listen.close(done); - } else { - done(); - } - }); - }); - - it("should work without callback", (done) => { - const doneSpy = jest.spyOn(getCompilerHooks(compiler).done[0], "fn"); - - instance.invalidate(); - - const intervalId = setInterval(() => { - if (instance.context.state) { - expect(compiler.running).toBe(true); - expect(instance.context.state).toBe(true); - expect(doneSpy).toHaveBeenCalledTimes(1); - - doneSpy.mockRestore(); - - clearInterval(intervalId); - - done(); - } - }); - }); - - it("should work with callback", (done) => { - const doneSpy = jest.spyOn(getCompilerHooks(compiler).done[0], "fn"); - let callbackCounter = 0; - - instance.invalidate(() => { - callbackCounter += 1; - }); - - const intervalId = setInterval(() => { - if (instance.context.state) { - expect(compiler.running).toBe(true); - expect(instance.context.state).toBe(true); - expect(callbackCounter).toBe(1); - expect(doneSpy).toHaveBeenCalledTimes(1); - - doneSpy.mockRestore(); - - clearInterval(intervalId); - - done(); - } - }); - }); - }); - - describe("getFilenameFromUrl method", () => { - describe("should work", () => { - beforeEach((done) => { - compiler = getCompiler(webpackConfig); - - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = app.listen((error) => { - if (error) { - return done(error); - } - - return done(); - }); - }); - - afterEach((done) => { - if (instance.context.watching.closed) { - if (listen) { - listen.close(done); - } else { - done(); - } - - return; - } - - instance.close(() => { - if (listen) { - listen.close(done); - } else { - done(); - } - }); - }); - - it("should work", (done) => { - instance.waitUntilValid(() => { - expect(instance.getFilenameFromUrl("/bundle.js")).toBe( - path.join(webpackConfig.output.path, "/bundle.js"), - ); - expect(instance.getFilenameFromUrl("/")).toBe( - path.join(webpackConfig.output.path, "/index.html"), - ); - expect(instance.getFilenameFromUrl("/index.html")).toBe( - path.join(webpackConfig.output.path, "/index.html"), - ); - expect(instance.getFilenameFromUrl("/svg.svg")).toBe( - path.join(webpackConfig.output.path, "/svg.svg"), - ); - expect( - instance.getFilenameFromUrl("/unknown.unknown"), - ).toBeUndefined(); - expect( - instance.getFilenameFromUrl("/unknown/unknown.unknown"), - ).toBeUndefined(); - - done(); - }); - }); - }); - - describe('should work when the "index" option disabled', () => { - beforeEach((done) => { - compiler = getCompiler(webpackConfig); - - instance = middleware(compiler, { index: false }); - - app = framework(); - app.use(instance); - - listen = app.listen((error) => { - if (error) { - return done(error); - } - - return done(); - }); - }); - - afterEach((done) => { - if (instance.context.watching.closed) { - if (listen) { - listen.close(done); - } else { - done(); - } - - return; - } - - instance.close(() => { - if (listen) { - listen.close(done); - } else { - done(); - } - }); - }); - - it("should work", (done) => { - instance.waitUntilValid(() => { - expect(instance.getFilenameFromUrl("/bundle.js")).toBe( - path.join(webpackConfig.output.path, "/bundle.js"), - ); - // eslint-disable-next-line no-undefined - expect(instance.getFilenameFromUrl("/")).toBe(undefined); - expect(instance.getFilenameFromUrl("/index.html")).toBe( - path.join(webpackConfig.output.path, "/index.html"), - ); - expect(instance.getFilenameFromUrl("/svg.svg")).toBe( - path.join(webpackConfig.output.path, "/svg.svg"), - ); - expect( - instance.getFilenameFromUrl("/unknown.unknown"), - ).toBeUndefined(); - expect( - instance.getFilenameFromUrl("/unknown/unknown.unknown"), - ).toBeUndefined(); - - done(); - }); - }); - }); - - describe('should work with the "publicPath" option', () => { - beforeEach((done) => { - compiler = getCompiler(webpackPublicPathConfig); - - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = app.listen((error) => { - if (error) { - return done(error); - } - - return done(); - }); - }); - - afterEach((done) => { - if (instance.context.watching.closed) { - if (listen) { - listen.close(done); - } else { - done(); - } - - return; - } - - instance.close(() => { - if (listen) { - listen.close(done); - } else { - done(); - } - }); - }); - - it("should work", (done) => { - instance.waitUntilValid(() => { - expect(instance.getFilenameFromUrl("/public/path/bundle.js")).toBe( - path.join(webpackPublicPathConfig.output.path, "/bundle.js"), - ); - expect(instance.getFilenameFromUrl("/public/path/")).toBe( - path.join(webpackPublicPathConfig.output.path, "/index.html"), - ); - expect(instance.getFilenameFromUrl("/public/path/index.html")).toBe( - path.join(webpackPublicPathConfig.output.path, "/index.html"), - ); - expect(instance.getFilenameFromUrl("/public/path/svg.svg")).toBe( - path.join(webpackPublicPathConfig.output.path, "/svg.svg"), - ); - - expect(instance.getFilenameFromUrl("/")).toBeUndefined(); - expect( - instance.getFilenameFromUrl("/unknown.unknown"), - ).toBeUndefined(); - expect( - instance.getFilenameFromUrl("/unknown/unknown.unknown"), - ).toBeUndefined(); - - done(); - }); - }); - }); - - describe("should work in multi compiler mode", () => { - beforeEach((done) => { - compiler = getCompiler(webpackMultiConfig); - - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = app.listen((error) => { - if (error) { - return done(error); - } - - return done(); - }); - }); - - afterEach((done) => { - if (instance.context.watching.closed) { - if (listen) { - listen.close(done); - } else { - done(); - } - - return; - } - - instance.close(() => { - if (listen) { - listen.close(done); - } else { - done(); - } - }); - }); - - it("should work", (done) => { - instance.waitUntilValid(() => { - expect(instance.getFilenameFromUrl("/static-one/bundle.js")).toBe( - path.join(webpackMultiConfig[0].output.path, "/bundle.js"), - ); - expect(instance.getFilenameFromUrl("/static-one/")).toBe( - path.join(webpackMultiConfig[0].output.path, "/index.html"), - ); - expect(instance.getFilenameFromUrl("/static-one/index.html")).toBe( - path.join(webpackMultiConfig[0].output.path, "/index.html"), - ); - expect(instance.getFilenameFromUrl("/static-one/svg.svg")).toBe( - path.join(webpackMultiConfig[0].output.path, "/svg.svg"), - ); - expect( - instance.getFilenameFromUrl("/static-one/unknown.unknown"), - ).toBeUndefined(); - expect( - instance.getFilenameFromUrl( - "/static-one/unknown/unknown.unknown", - ), - ).toBeUndefined(); - - expect(instance.getFilenameFromUrl("/static-two/bundle.js")).toBe( - path.join(webpackMultiConfig[1].output.path, "/bundle.js"), - ); - expect( - instance.getFilenameFromUrl("/static-two/unknown.unknown"), - ).toBeUndefined(); - expect( - instance.getFilenameFromUrl( - "/static-two/unknown/unknown.unknown", - ), - ).toBeUndefined(); - - expect(instance.getFilenameFromUrl("/")).toBeUndefined(); - expect( - instance.getFilenameFromUrl("/static-one/unknown.unknown"), - ).toBeUndefined(); - expect( - instance.getFilenameFromUrl( - "/static-one/unknown/unknown.unknown", - ), - ).toBeUndefined(); - - done(); - }); - }); - }); - }); - - describe("close method", () => { - beforeEach((done) => { - compiler = getCompiler(webpackConfig); - - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = app.listen((error) => { - if (error) { - return done(error); - } - - return done(); - }); - }); - - afterEach((done) => { - if (instance.context.watching.closed) { - if (listen) { - listen.close(done); - } else { - done(); - } - - return; - } - - instance.close(() => { - if (listen) { - listen.close(done); - } else { - done(); - } - }); - }); - - it("should work without callback", (done) => { - const doneSpy = jest.spyOn(getCompilerHooks(compiler).done[0], "fn"); - - instance.waitUntilValid(() => { - instance.close(); - - expect(compiler.running).toBe(false); - expect(doneSpy).toHaveBeenCalledTimes(1); - - doneSpy.mockRestore(); - - done(); - }); - }); - - it("should work with callback", (done) => { - const doneSpy = jest.spyOn(getCompilerHooks(compiler).done[0], "fn"); - - instance.waitUntilValid(() => { - instance.close(() => { - expect(compiler.running).toBe(false); - expect(doneSpy).toHaveBeenCalledTimes(1); - - doneSpy.mockRestore(); - - done(); - }); - }); - }); - }); - - describe("context property", () => { - beforeEach((done) => { - compiler = getCompiler(webpackConfig); - - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = app.listen((error) => { - if (error) { - return done(error); - } - - return done(); - }); - }); - - afterEach((done) => { - if (instance.context.watching.closed) { - if (listen) { - listen.close(done); - } else { - done(); - } - - return; - } - - instance.close(() => { - if (listen) { - listen.close(done); - } else { - done(); - } - }); - }); - - it("should contain public properties", (done) => { - expect(instance.context.state).toBeDefined(); - expect(instance.context.options).toBeDefined(); - expect(instance.context.compiler).toBeDefined(); - expect(instance.context.watching).toBeDefined(); - expect(instance.context.outputFileSystem).toBeDefined(); - - // the compilation needs to finish, as it will still be running - // after the test is done if not finished, potentially impacting other tests - compiler.hooks.done.tap("wdm-test", () => { - done(); - }); - }); - }); - }); -}); From 17ed7eb03ebf7fa47e763d0f51f2b4eaa3f0f3ee Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Fri, 29 Mar 2024 18:52:58 +0300 Subject: [PATCH 16/16] chore(release): 7.2.0 --- CHANGELOG.md | 10 ++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58ef35c19..871381915 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [7.2.0](https://github.com/webpack/webpack-dev-middleware/compare/v7.1.1...v7.2.0) (2024-03-29) + + +### Features + +* hapi support ([b3f9126](https://github.com/webpack/webpack-dev-middleware/commit/b3f9126cfb659c95c0cd77d97eed168c7941c8a8)) +* koa support ([#1792](https://github.com/webpack/webpack-dev-middleware/issues/1792)) ([458c17c](https://github.com/webpack/webpack-dev-middleware/commit/458c17c372a2a1a5a33f8923998dba88d2644135)) +* support `ETag` header generation ([#1797](https://github.com/webpack/webpack-dev-middleware/issues/1797)) ([b759181](https://github.com/webpack/webpack-dev-middleware/commit/b75918163284495dae5a2f995c2d93805fccfbd7)) +* support `Last-Modified` header generation ([#1798](https://github.com/webpack/webpack-dev-middleware/issues/1798)) ([18e5683](https://github.com/webpack/webpack-dev-middleware/commit/18e56833327084c22c1ee6bdad123095a68d144a)) + ### [7.1.1](https://github.com/webpack/webpack-dev-middleware/compare/v7.1.0...v7.1.1) (2024-03-21) diff --git a/package-lock.json b/package-lock.json index 9aee263b1..004acf3b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "webpack-dev-middleware", - "version": "7.1.1", + "version": "7.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "webpack-dev-middleware", - "version": "7.1.1", + "version": "7.2.0", "license": "MIT", "dependencies": { "cloneable-readable": "^3.0.0", diff --git a/package.json b/package.json index 836d95b2f..610863cd9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "webpack-dev-middleware", - "version": "7.1.1", + "version": "7.2.0", "description": "A development middleware for webpack", "license": "MIT", "repository": "webpack/webpack-dev-middleware",