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; +};