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/.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 }} 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/README.md b/README.md index bc102ccc2..683ea7fed 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,20 @@ 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 + +### lastModified + +Type: `Boolean` +Default: `undefined` + +Enable or disable `Last-Modified` header. Uses the file system's last modified value. + ### publicPath Type: `String` @@ -540,6 +555,98 @@ 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!")); +``` + +### 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 +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); +}); +``` + ### 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 +658,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.register(require("@fastify/express")); + await fastify.use(devMiddleware(compiler, devMiddlewareOptions)); await fastify.listen(3000); })(); ``` 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. diff --git a/package-lock.json b/package-lock.json index 532b58f1c..004acf3b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "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", "colorette": "^2.0.10", "memfs": "^4.6.0", "mime-types": "^2.1.31", @@ -22,6 +23,8 @@ "@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", "@types/mime-types": "^2.1.1", @@ -41,9 +44,12 @@ "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", + "joi": "^17.12.2", + "koa": "^2.15.2", "lint-staged": "^15.2.0", "npm-run-all": "^4.1.5", "prettier": "^3.2.4", @@ -2535,9 +2541,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 +2573,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 +2593,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 +2602,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 +2635,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 +2820,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 +2943,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 +2962,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 +2974,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" @@ -3085,6 +3105,373 @@ "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", + "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", @@ -4035,6 +4422,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", @@ -4500,6 +4914,23 @@ "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==", + "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", @@ -4653,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", @@ -4824,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", @@ -4839,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", @@ -5054,6 +5512,25 @@ "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==", + "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", @@ -5177,6 +5654,29 @@ "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==", + "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", @@ -5192,6 +5692,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", @@ -5519,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", @@ -6918,6 +7454,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", @@ -7149,22 +7698,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 +7734,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 +7764,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 +7780,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 +7792,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 +7808,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 +7852,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": { @@ -7534,6 +8083,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", @@ -8084,6 +8639,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", @@ -9007,6 +9568,14 @@ "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==", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -9017,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" } @@ -9071,9 +9639,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", @@ -9145,6 +9713,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", @@ -9172,25 +9752,131 @@ "micromatch": "^4.0.4" }, "engines": { - "node": ">=8.6.0" + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "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/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "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-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==", + "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": { @@ -9373,6 +10059,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", @@ -10443,6 +11143,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", @@ -10495,6 +11242,25 @@ "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==", + "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", @@ -10782,6 +11548,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", @@ -12762,6 +13543,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", @@ -12811,6 +13620,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", @@ -12865,6 +13683,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", @@ -12892,6 +13722,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", @@ -12914,6 +13831,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", @@ -13918,6 +14846,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", @@ -13953,6 +14890,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", @@ -14177,6 +15120,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", @@ -14318,12 +15315,26 @@ "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==", + "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", @@ -14419,6 +15430,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", @@ -14607,6 +15624,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", @@ -14798,6 +15824,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", @@ -14874,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", @@ -14907,6 +15941,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", @@ -14931,6 +15983,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", @@ -15009,6 +16067,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", @@ -15144,6 +16208,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", @@ -15503,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" } @@ -15997,6 +17069,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", @@ -16039,6 +17120,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", @@ -16095,6 +17185,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", @@ -16834,6 +17933,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 e0bf179d9..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", @@ -66,6 +66,8 @@ "@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", "@types/mime-types": "^2.1.1", @@ -85,9 +87,12 @@ "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", + "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 a141a7b4d..1fc9ccdff 100644 --- a/src/index.js +++ b/src/index.js @@ -59,24 +59,24 @@ const noop = () => {}; /** * @typedef {Object} ResponseData - * @property {string | Buffer | ReadStream} data + * @property {Buffer | ReadStream} data * @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 - * @param {string | Buffer | ReadStream} data + * @param {Buffer | ReadStream} data * @param {number} byteLength * @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 @@ -89,22 +89,22 @@ 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 */ /** - * @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] @@ -117,11 +117,13 @@ const noop = () => {}; * @property {OutputFileSystem} [outputFileSystem] * @property {boolean | string} [index] * @property {ModifyResponseData} [modifyResponseData] + * @property {"weak" | "strong"} [etag] + * @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 @@ -165,8 +167,8 @@ const noop = () => {}; */ /** - * @template {IncomingMessage} RequestInternal - * @template {ServerResponse} ResponseInternal + * @template {IncomingMessage} [RequestInternal=IncomingMessage] + * @template {ServerResponse} [ResponseInternal=ServerResponse] * @typedef {Middleware & AdditionalMethods} API */ @@ -183,8 +185,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} @@ -292,4 +294,135 @@ 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 hapiWrapper() { + 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.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 {{ 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} + */ + 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 {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/middleware.js b/src/middleware.js index 634ce77e3..a8bbe50ed 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -2,21 +2,26 @@ const path = require("path"); const mime = require("mime-types"); +const onFinishedStream = require("on-finished"); + const getFilenameFromUrl = require("./utils/getFilenameFromUrl"); const { - getHeaderFromRequest, - getHeaderFromResponse, - setHeaderForResponse, setStatusCode, send, - sendError, + 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 */ /** @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 @@ -28,7 +33,85 @@ function getValueContentRangeHeader(type, size, range) { return `${type} ${range ? `${range.start}-${range.end}` : "*"}/${size}`; } -const BYTES_RANGE_REGEXP = /^ *bytes/i; +/** + * Parse an HTTP Date into a number. + * + * @param {string} date + * @returns {number} + */ +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? + * @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", +}; + +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 + * @typedef {Object} SendErrorOptions send error options + * @property {Record=} headers headers + * @property {import("./index").ModifyResponseData=} modifyResponseData modify response data callback + */ /** * @template {IncomingMessage} Request @@ -44,14 +127,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,7 +147,284 @@ function wrapper(context) { }); } + if (req.method && !acceptedMethods.includes(req.method)) { + await goNext(); + + return; + } + + /** + * @param {number} status status + * @param {Partial>=} options options + * @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 = Buffer.from( + ` + + + +Error + + +
${escapeHtml(content)}
+ +`, + "utf-8", + ); + + // 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: Buffer, byteLength: number }} */ + (options.modifyResponseData(req, res, document, byteLength))); + } + + res.setHeader("Content-Length", byteLength); + + 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() { + // 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 || + (ifMatch !== "*" && + parseTokenList(ifMatch).every( + (match) => + match !== etag && + match !== `W/${etag}` && + `W/${match}` !== etag, + )) + ); + } + + // 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; + } + + /** + * @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; + } + + // 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; + } + + 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 + if (modifiedSince) { + const lastModified = resHeaders["last-modified"]; + + // 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. + const modifiedStale = + !lastModified || + !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince)); + + if (modifiedStale) { + return false; + } + } + + 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); + } + + /** + * @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} */ const extra = {}; const filename = getFilenameFromUrl( @@ -86,7 +438,7 @@ function wrapper(context) { context.logger.error(`Malicious path "${filename}".`); } - sendError(req, res, extra.errorCode, { + sendError(extra.errorCode, { modifyResponseData: context.options.modifyResponseData, }); @@ -99,6 +451,12 @@ function wrapper(context) { return; } + const { size } = /** @type {import("fs").Stats} */ (extra.stats); + + let len = size; + let offset = 0; + + // Send logic let { headers } = context.options; if (typeof headers === "function") { @@ -121,54 +479,156 @@ 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")); + if (context.options.lastModified && !res.getHeader("Last-Modified")) { + const modified = + /** @type {import("fs").Stats} */ + (extra.stats).mtime.toUTCString(); - let len = /** @type {import("fs").Stats} */ (extra.stats).size; - let offset = 0; + res.setHeader("Last-Modified", modified); + } - if (rangeHeader && BYTES_RANGE_REGEXP.test(rangeHeader)) { - // eslint-disable-next-line global-require - const parsedRanges = require("range-parser")(len, rangeHeader, { - combine: true, - }); + /** @type {number} */ + let start; + /** @type {number} */ + let end; + + /** @type {undefined | Buffer | ReadStream} */ + let bufferOrStream; + /** @type {number} */ + let byteLength; + + const rangeHeader = getRangeHeader(); + + if (context.options.etag && !res.getHeader("ETag")) { + /** @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 { + 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]); + } + } + + [start, end] = calcStartAndEnd(offset, len); + + try { + const result = createReadStreamOrReadFileSync( + filename, + context.outputFileSystem, + start, + end, + ); + + value = result.bufferOrStream; + ({ bufferOrStream, byteLength } = result); + } catch (_err) { + // Ignore here + } + } + + if (value) { + // eslint-disable-next-line global-require + const result = 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; + } + + res.setHeader("ETag", result.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 | undefined} */ (res.getHeader("ETag")), + "last-modified": + /** @type {string | undefined} */ + (res.getHeader("Last-Modified")), + }) + ) { + 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 (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."); - setHeaderForResponse( - res, + res.setHeader( "Content-Range", - getValueContentRangeHeader("bytes", len), + getValueContentRangeHeader("bytes", size), ); - sendError(req, res, 416, { + sendError(416, { headers: { "Content-Range": res.getHeader("Content-Range"), }, @@ -189,29 +649,109 @@ function wrapper(context) { if (parsedRanges !== -2 && parsedRanges.length === 1) { // Content-Range setStatusCode(res, 206); - setHeaderForResponse( - res, + res.setHeader( "Content-Range", getValueContentRangeHeader( "bytes", - len, + size, /** @type {import("range-parser").Ranges} */ (parsedRanges)[0], ), ); - offset += parsedRanges[0].start; - len = parsedRanges[0].end - parsedRanges[0].start + 1; + [offset, len] = getOffsetAndLenFromRange(parsedRanges[0]); } } - const start = offset; - const end = Math.max(offset, offset + len - 1); + // 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(); - send(req, res, filename, start, end, goNext, { - modifyResponseData: context.options.modifyResponseData, - outputFileSystem: context.outputFileSystem, + 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") { + // 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/options.json b/src/options.json index 91086d193..50443e268 100644 --- a/src/options.json +++ b/src/options.json @@ -129,6 +129,16 @@ "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"] + }, + "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/src/utils/compatibleAPI.js b/src/utils/compatibleAPI.js index 933572ec3..ab315b6f0 100644 --- a/src/utils/compatibleAPI.js +++ b/src/utils/compatibleAPI.js @@ -1,370 +1,107 @@ -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 - * @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 + * @template {ServerResponse & ExpectedResponse} Response * @param {Response} res - * @returns {string[]} - */ -function getHeaderNames(res) { - if (typeof res.getHeaderNames !== "function") { - // @ts-ignore - // eslint-disable-next-line no-underscore-dangle - return Object.keys(res._headers || {}); - } - - 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} + * @param {number} code */ -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); +function setStatusCode(res, code) { + // Pseudo API + if (typeof res.status === "function") { + res.status(code); 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]); - } + // eslint-disable-next-line no-param-reassign + res.statusCode = code; } /** * @template {ServerResponse} Response - * @param {Response} res - * @param {number} code + * @param {Response & ExpectedResponse} res + * @param {import("fs").ReadStream} bufferOrStream */ -function setStatusCode(res, code) { +function pipe(res, bufferOrStream) { + // Pseudo API and Koa API if ( - typeof (/** @type {Response & ExpectedResponse} */ (res).status) === + typeof (/** @type {Response & ExpectedResponse} */ (res).pipeInto) === "function" ) { - /** @type {Response & ExpectedResponse} */ - (res).status(code); - + // Writable stream into Readable stream + res.pipeInto(bufferOrStream); return; } - // eslint-disable-next-line no-param-reassign - res.statusCode = code; + // Node.js API and Express API and Hapi API + bufferOrStream.pipe(res); } -/** - * @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 {string | Buffer} bufferOrStream */ -function sendError(req, res, status, options) { - const content = statuses[status] || String(status); - let document = ` - - - -Error - - -
${escapeHtml(content)}
- -`; - - // Clear existing headers - clearHeadersForResponse(res); - - if (options && options.headers) { - setHeadersForResponse(res, options.headers); - } - - // 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"); - - let byteLength = Buffer.byteLength(document); - - if (options && options.modifyResponseData) { - ({ data: document, byteLength } = - /** @type {{data: string, byteLength: number }} */ - (options.modifyResponseData(req, res, document, byteLength))); +function send(res, bufferOrStream) { + // Pseudo API and Express API and Koa API + if (typeof res.send === "function") { + res.send(bufferOrStream); + return; } - setHeaderForResponse(res, "Content-Length", byteLength); - - res.end(document); + res.end(bufferOrStream); } /** - * @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 {import("../index").OutputFileSystem} outputFileSystem * @param {number} start * @param {number} end - * @param {() => Promise} goNext - * @param {SendOptions} options + * @returns {{ bufferOrStream: (Buffer | import("fs").ReadStream), byteLength: number }} */ -async function send(req, res, filename, start, end, goNext, options) { - const isFsSupportsStream = - typeof options.outputFileSystem.createReadStream === "function"; - - /** @type {string | Buffer | ReadStream} */ +function createReadStreamOrReadFileSync( + filename, + outputFileSystem, + start, + end, +) { + /** @type {string | Buffer | import("fs").ReadStream} */ let bufferOrStream; + /** @type {number} */ 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" - ) { - setHeaderForResponse(res, "Content-Length", byteLength); - - if (req.method === "HEAD") { - res.end(); - return; - } - - /** @type {import("fs").ReadStream} */ - (bufferOrStream).pipe(res); - - // Cleanup - const cleanup = () => { - destroyStream( - /** @type {import("fs").ReadStream} */ (bufferOrStream), - true, - ); - }; - - // Response finished, cleanup - onFinishedStream(res, cleanup); - - // 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; - } - }); - - return; - } - - // Express API - if ( - typeof (/** @type {Response & ExpectedResponse} */ (res).send) === - "function" - ) { - /** @type {Response & ExpectedResponse} */ - (res).send(bufferOrStream); - return; - } - - // Only Node.js API used - res.setHeader("Content-Length", byteLength); - - if (req.method === "HEAD") { - res.end(); + // 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 { - res.end(bufferOrStream); + bufferOrStream = + /** @type {import("fs").readFileSync} */ + (outputFileSystem.readFileSync)(filename); + ({ byteLength } = bufferOrStream); } + + return { bufferOrStream, byteLength }; } -module.exports = { - getHeaderNames, - getHeaderFromRequest, - getHeaderFromResponse, - setHeaderForResponse, - setStatusCode, - send, - sendError, -}; +module.exports = { setStatusCode, send, pipe, createReadStreamOrReadFileSync }; 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/getFilenameFromUrl.js b/src/utils/getFilenameFromUrl.js index 0cb9c05a4..67852ee8b 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); @@ -75,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/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/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..aa4d0dfef 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." @@ -61,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/api.test.js b/test/api.test.js deleted file mode 100644 index b49fded0a..000000000 --- a/test/api.test.js +++ /dev/null @@ -1,752 +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(); - }); - }); - }); - - if (webpack.version[0] === 5) { - 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(); - }); - }); - }); - }); -}); diff --git a/test/middleware.test.js b/test/middleware.test.js index 663bb3eba..1158467b6 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -1,12 +1,17 @@ import fs from "fs"; import path from "path"; -import express from "express"; 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"; import memfs, { createFsFromVolume, Volume } from "memfs"; import del from "del"; +import { Stats } from "webpack"; + import middleware from "../src"; import getCompiler from "./helpers/getCompiler"; @@ -17,59 +22,760 @@ 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(); + +async function startServer(app) { + return new Promise((resolve, reject) => { + const server = app.listen({ port: 3000 }, (error) => { + if (error) { + return reject(error); + } + + return resolve(server); + }); + }); +} + +async function frameworkFactory( + name, + framework, + compiler, + devMiddlewareOptions, + options = {}, +) { + switch (name) { + case "hapi": { + const server = framework.server(); + const hapiPlugin = { + plugin: middleware.hapiWrapper(), + 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 server.register({ + plugin, + options, + }); + }), + ); + + await server.start(); + + 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 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" + ? options.setupMiddlewares([instance]) + : [instance]; + + for (const item of middlewares) { + if (item.route) { + app.use(item.route, item.fn); + } else { + app.use(item); + } + } + + if (isFastify) { + await app.ready(); + } + + const server = await startServer(app); + const req = isFastify ? request(app.server) : request(app); + + return [isFastify ? app.server : server, req, 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; + } + + 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 "koa": + 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"; + } +} + +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 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", + 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; +} + +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], + ["fastify", fastify], + ["koa", koa], + ["hapi", Hapi], +])("%s framework:", (name, framework) => { + describe("middleware", () => { + let instance; + 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(); + }); + }); -// Suppress unnecessary stats output -global.console.log = jest.fn(); + it("should work with callback", (done) => { + const doneSpy = jest.spyOn(getCompilerHooks(compiler).done[0], "fn"); -describe.each([ - ["express", express], - ["connect", connect], -])("%s framework:", (_, framework) => { - describe("middleware", () => { - let instance; - let listen; - let app; - let req; + instance.waitUntilValid(() => { + instance.close(() => { + expect(compiler.running).toBe(false); + expect(doneSpy).toHaveBeenCalledTimes(1); - function listenShorthand(done) { - return app.listen((error) => { - if (error) { - return done(error); - } + doneSpy.mockRestore(); - return done(); + done(); + }); + }); + }); }); - } - function close(done) { - if (instance.context.watching.closed) { - if (listen) { - listen.close(done); - } else { - done(); - } + describe("context property", () => { + beforeEach(async () => { + compiler = getCompiler(webpackConfig); - return; - } + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); + }); - instance.close(() => { - if (listen) { - listen.close(done); - } else { - done(); - } + 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; let codeContent; - let codeLength; const outputPath = path.resolve(__dirname, "./outputs/basic-test"); - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -77,21 +783,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, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); + instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, }); @@ -128,11 +829,11 @@ describe.each([ path.resolve(outputPath, "empty-file.txt"), "", ); - - 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 +852,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 +865,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 +1000,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 +1026,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( @@ -335,14 +1036,14 @@ 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"); 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 +1059,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 +1076,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 +1093,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 +1110,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 +1217,7 @@ describe.each([ expect(response.statusCode).toEqual(404); expect(response.headers["content-type"]).toEqual( - "text/html; charset=utf-8", + get404ContentTypeHeader(name), ); }); }); @@ -526,7 +1227,7 @@ describe.each([ const outputPath = path.resolve(__dirname, "./outputs/basic"); - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -536,17 +1237,16 @@ describe.each([ }, }); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); }); - 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 +1256,19 @@ 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); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); }); - 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 +1329,7 @@ describe.each([ expect(response.statusCode).toEqual(404); expect(response.headers["content-type"]).toEqual( - "text/html; charset=utf-8", + get404ContentTypeHeader(name), ); }); @@ -639,7 +1338,7 @@ describe.each([ expect(response.statusCode).toEqual(404); expect(response.headers["content-type"]).toEqual( - "text/html; charset=utf-8", + get404ContentTypeHeader(name), ); }); @@ -648,7 +1347,7 @@ describe.each([ expect(response.statusCode).toEqual(404); expect(response.headers["content-type"]).toEqual( - "text/html; charset=utf-8", + get404ContentTypeHeader(name), ); }); }); @@ -675,32 +1374,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, }, ], @@ -783,12 +1482,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, }, @@ -1013,7 +1713,7 @@ describe.each([ let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -1023,14 +1723,11 @@ describe.each([ }, }); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); const { context: { @@ -1048,7 +1745,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 +1778,73 @@ 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, req, instance] = await frameworkFactory( + name, + framework, + compiler, + // eslint-disable-next-line no-undefined + undefined, + { + setupMiddlewares: (middlewares) => { + if (name === "koa") { + middlewares.unshift(async (ctx, next) => { + ctx.set( + "Content-Type", + "application/vnd.test+octet-stream", + ); - listen = listenShorthand(done); + await next(); + }); + } else 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(); + }); + } - req = request(app); + return middlewares; + }, + }, + ); }); - 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 +1856,21 @@ 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); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); }); - 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 +1904,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 +1913,16 @@ describe.each([ }, }); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); }); - 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 +1956,19 @@ 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); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); }); - 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 +2002,7 @@ describe.each([ }); describe('should respect "output.publicPath" and "output.path" options', () => { - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler({ ...webpackConfig, output: { @@ -1306,17 +2012,16 @@ describe.each([ }, }); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); }); - 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 +2058,7 @@ describe.each([ expect(response.statusCode).toEqual(404); expect(response.headers["content-type"]).toEqual( - "text/html; charset=utf-8", + get404ContentTypeHeader(name), ); }); }); @@ -1361,7 +2066,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 +2075,30 @@ describe.each([ path: path.resolve(__dirname, "./outputs/other-basic-[fullhash]"), }, }); + compiler.hooks.afterCompile.tap("wdm-test", ({ hash: h }) => { + hash = h; + }); - instance = middleware(compiler); + [server, req, 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 +2141,7 @@ describe.each([ let hashOne; let hashTwo; - beforeAll((done) => { + beforeAll(async () => { const compiler = getCompiler([ { ...webpackMultiConfig[0], @@ -1454,27 +2166,33 @@ 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, req, 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 +2238,7 @@ describe.each([ expect(response.statusCode).toEqual(404); expect(response.headers["content-type"]).toEqual( - "text/html; charset=utf-8", + get404ContentTypeHeader(name), ); }); @@ -1544,20 +2262,19 @@ 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); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); }); - 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 +2344,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 +2364,16 @@ describe.each([ }, ]); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); }); - 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 +2425,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 +2445,16 @@ describe.each([ }, ]); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); }); - 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 +2533,19 @@ 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); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); }); - 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 +2588,22 @@ 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); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); }); - 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 +2643,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 +2662,16 @@ describe.each([ }, ]); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); }); - 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 +2711,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 +2724,16 @@ describe.each([ } }); - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); }); - 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 +2750,7 @@ describe.each([ "./outputs/basic-test-errors-500", ); - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -2051,16 +2759,11 @@ describe.each([ }, }); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(() => { - compiler.hooks.afterCompile.tap("wdm-test", () => { - done(); - }); - }); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, @@ -2083,11 +2786,11 @@ describe.each([ return brokenStream; }; - - 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 +2822,7 @@ describe.each([ "./outputs/basic-test-errors-404", ); - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -2128,16 +2831,11 @@ describe.each([ }, }); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(() => { - compiler.hooks.afterCompile.tap("wdm-test", () => { - done(); - }); - }); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, @@ -2164,11 +2862,11 @@ describe.each([ return brokenStream; }; - - 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 +2893,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 +2907,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, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); + instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, }); @@ -2234,18 +2926,18 @@ describe.each([ ); instance.context.outputFileSystem.createReadStream = null; - - 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 +2978,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,14 +2988,11 @@ describe.each([ }, }); - instance = middleware(compiler); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, @@ -2314,7 +3003,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 +3019,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,18 +3029,16 @@ describe.each([ }, }); - instance = middleware(compiler, { - mimeTypes: { - myhtml: "text/html", + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + mimeTypes: { + myhtml: "text/html", + }, }, - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + ); instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, @@ -2360,7 +3049,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 +3065,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,18 +3075,16 @@ describe.each([ }, }); - instance = middleware(compiler, { - mimeTypes: { - jpg: "image/vnd.test+jpeg", + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + mimeTypes: { + jpg: "image/vnd.test+jpeg", + }, }, - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + ); instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, @@ -2406,7 +3095,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 +3110,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 +3120,28 @@ describe.each([ }, }); - instance = middleware(compiler, { - mimeTypes: { - jpg: "image/vnd.test+jpeg", + [server, req, 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"); - } - }); - - listen = listenShorthand(done); + { + setupMiddlewares: (middlewares) => { + applyTestMiddleware(name, middlewares); - req = request(app); + return middlewares; + }, + }, + ); }); - 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 +3154,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,16 +3164,14 @@ describe.each([ }, }); - instance = middleware(compiler, { - mimeTypeDefault: "text/plain", - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + mimeTypeDefault: "text/plain", + }, + ); instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, @@ -2499,7 +3182,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 +3203,21 @@ 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); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); }); - afterAll((done) => { + afterAll(async () => { spy.mockRestore(); - close(done); + await close(server, instance); }); it('should pass arguments to the "watch" method', async () => { @@ -2552,25 +3233,22 @@ 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); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); }); - afterAll((done) => { + afterAll(async () => { spy.mockRestore(); - close(done); + await close(server, instance); }); it('should pass arguments to the "watch" method', async () => { @@ -2589,25 +3267,22 @@ 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); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + ); }); - afterAll((done) => { + afterAll(async () => { spy.mockRestore(); - close(done); + await close(server, instance); }); it('should pass arguments to the "watch" method', async () => { @@ -2631,7 +3306,7 @@ describe.each([ describe('should work with "true" value', () => { let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -2641,59 +3316,55 @@ describe.each([ }, }); - instance = middleware(compiler, { writeToDisk: true }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { writeToDisk: true }, + ); }); - 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) => { - 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 () => { @@ -2724,7 +3395,7 @@ describe.each([ let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -2734,70 +3405,66 @@ describe.each([ }, }); - instance = middleware(compiler, { writeToDisk: true }); + [server, req, 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) => { - 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(); + }, + ); + }); }); }); describe('should work with "false" value', () => { let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -2806,58 +3473,58 @@ describe.each([ }, }); - instance = middleware(compiler, { writeToDisk: false }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, req, 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) - .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(); + }, + ); + }); }); }); describe('should work with "Function" value when it returns "true"', () => { let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, output: { @@ -2869,19 +3536,17 @@ describe.each([ }, }); - instance = middleware(compiler, { - writeToDisk: (filePath) => /bundle\.js$/.test(filePath), - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + writeToDisk: (filePath) => /bundle\.js$/.test(filePath), + }, + ); }); - afterAll((done) => { + afterAll(async () => { del.sync( path.posix.resolve( __dirname, @@ -2889,7 +3554,7 @@ describe.each([ ), ); - close(done); + await close(server, instance); }); it("should find the bundle file on disk", async () => { @@ -2909,7 +3574,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 +3586,17 @@ describe.each([ }, }); - instance = middleware(compiler, { - writeToDisk: (filePath) => !/bundle\.js$/.test(filePath), - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + writeToDisk: (filePath) => !/bundle\.js$/.test(filePath), + }, + ); }); - afterAll((done) => { + afterAll(async () => { del.sync( path.posix.resolve( __dirname, @@ -2939,7 +3604,7 @@ describe.each([ ), ); - close(done); + await close(server, instance); }); it("should not find the bundle file on disk", async () => { @@ -2959,7 +3624,7 @@ describe.each([ describe("should work when assets have query string", () => { let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackQueryStringConfig, output: { @@ -2971,17 +3636,15 @@ describe.each([ }, }); - instance = middleware(compiler, { writeToDisk: true }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { writeToDisk: true }, + ); }); - afterAll((done) => { + afterAll(async () => { del.sync( path.posix.resolve( __dirname, @@ -2989,7 +3652,7 @@ describe.each([ ), ); - close(done); + await close(server, instance); }); it("should find the bundle file on disk with no querystring", async () => { @@ -3009,7 +3672,7 @@ describe.each([ describe("should work in multi-compiler mode", () => { let compiler; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler([ { ...webpackMultiWatchOptionsConfig[0], @@ -3035,17 +3698,15 @@ describe.each([ }, ]); - instance = middleware(compiler, { writeToDisk: true }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { writeToDisk: true }, + ); }); - afterAll((done) => { + afterAll(async () => { del.sync( path.posix.resolve( __dirname, @@ -3053,7 +3714,7 @@ describe.each([ ), ); - close(done); + await close(server, instance); }); it("should find the bundle files on disk", async () => { @@ -3084,7 +3745,7 @@ describe.each([ let compiler; let hash; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler({ ...webpackConfig, ...{ @@ -3098,28 +3759,34 @@ describe.each([ }, }, }); + compiler.hooks.afterCompile.tap("wdm-test", ({ hash: h }) => { + hash = h; + }); - instance = middleware(compiler, { writeToDisk: true }); + [server, req, 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 +3807,23 @@ 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); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + methods: ["POST"], + publicPath: "/public/", + }, + ); }); - 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 +3846,29 @@ 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); - - listen = listenShorthand(done); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + headers: { "X-nonsense-1": "yes", "X-nonsense-2": "no" }, + }, + { + setupMiddlewares: (middlewares) => { + applyTestMiddleware(name, middlewares); - req = request(app); + return middlewares; + }, + }, + ); }); - 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,19 +3879,7 @@ 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"); + 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(); @@ -3225,31 +3887,38 @@ 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); - - listen = listenShorthand(done); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + headers: [ + { + key: "X-Foo", + value: "value1", + }, + { + key: "X-Bar", + value: "value2", + }, + ], + }, + { + setupMiddlewares: (middlewares) => { + applyTestMiddleware(name, middlewares); - req = request(app); + return middlewares; + }, + }, + ); }); - 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,19 +3929,7 @@ 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"); + const res = await req.get("/file.jpg"); expect(res.statusCode).toEqual(200); expect(res.headers["x-foo"]).toBeUndefined(); expect(res.headers["x-bar"]).toBeUndefined(); @@ -3280,24 +3937,31 @@ 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, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + headers: () => { + return { "X-nonsense-1": "yes", "X-nonsense-2": "no" }; + }, }, - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); + { + setupMiddlewares: (middlewares) => { + applyTestMiddleware(name, middlewares); - req = request(app); + return middlewares; + }, + }, + ); }); - 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 +3972,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 +3980,38 @@ 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); - - listen = listenShorthand(done); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + headers: () => [ + { + key: "X-Foo", + value: "value1", + }, + { + key: "X-Bar", + value: "value2", + }, + ], + }, + { + setupMiddlewares: (middlewares) => { + applyTestMiddleware(name, middlewares); - req = request(app); + return middlewares; + }, + }, + ); }); - 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 +4022,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 +4030,33 @@ 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, req, 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); - - listen = listenShorthand(done); + { + setupMiddlewares: (middlewares) => { + applyTestMiddleware(name, middlewares); - req = request(app); + return middlewares; + }, + }, + ); }); - 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 +4067,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 +4077,20 @@ 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); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { publicPath: "/public/" }, + ); }); - 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 +4100,20 @@ 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); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { publicPath: "auto" }, + ); }); - 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 +4126,70 @@ 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; + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { serverSideRender: true }, + { + setupMiddlewares: (middlewares) => { + if (name === "koa") { + middlewares.push(async (ctx, next) => { + locals = ctx.state; - // Express API - if (res.sendStatus) { - res.sendStatus(200); - } - // Connect API - else { - // eslint-disable-next-line no-param-reassign - res.statusCode = 200; - res.end(); - } - }); + ctx.status = 200; - listen = listenShorthand(done); + await next(); + }); + } else 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(); + } + }); + } - req = request(app); + return middlewares; + }, + }, + ); }); - 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 +4203,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, req, 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 +4230,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 +4238,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, req, 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 +4267,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 +4275,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, req, 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 +4305,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 +4313,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, req, 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 +4349,27 @@ 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); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { index: false, publicPath: "/" }, + ); }); - 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 +4384,20 @@ 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); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { index: true, publicPath: "/" }, + ); }); - 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 +4419,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,17 +4429,15 @@ describe.each([ }, }); - instance = middleware(compiler, { - index: "default.html", - publicPath: "/", - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + index: "default.html", + publicPath: "/", + }, + ); instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, @@ -3767,7 +4448,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 +4463,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,17 +4473,15 @@ describe.each([ }, }); - instance = middleware(compiler, { - index: "index.custom", - publicPath: "/", - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + index: "index.custom", + publicPath: "/", + }, + ); instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, @@ -3811,7 +4492,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 +4504,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,20 +4514,18 @@ describe.each([ }, }); - instance = middleware(compiler, { - index: "index.mycustom", - mimeTypes: { - mycustom: "text/html", + [server, req, 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); + ); instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, @@ -3855,7 +4536,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 +4551,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,14 +4561,12 @@ describe.each([ }, }); - instance = middleware(compiler, { index: "noextension" }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { index: "noextension" }, + ); instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, @@ -3896,7 +4577,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 +4589,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,17 +4599,15 @@ describe.each([ }, }); - instance = middleware(compiler, { - index: "custom.html", - publicPath: "/", - }); - - app = framework(); - app.use(instance); - - listen = listenShorthand(done); - - req = request(app); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + index: "custom.html", + publicPath: "/", + }, + ); instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, @@ -3936,7 +4617,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 +4632,18 @@ describe.each([ let compiler; let isDirectory; - beforeAll((done) => { + beforeAll(async () => { compiler = getCompiler(webpackConfig); - instance = middleware(compiler, { - index: "default.html", - publicPath: "/", - }); + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + index: "default.html", + publicPath: "/", + }, + ); isDirectory = jest .spyOn(instance.context.outputFileSystem, "statSync") @@ -3965,19 +4653,12 @@ describe.each([ isDirectory: () => false, }; }); - - 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 +4673,7 @@ describe.each([ describe("should work", () => { let compiler; - beforeAll((done) => { + beforeAll(async () => { const outputPath = path.resolve( __dirname, "./outputs/modify-response-data", @@ -4006,20 +4687,18 @@ describe.each([ }, }); - instance = middleware(compiler, { - modifyResponseData: () => { - const result = Buffer.from("test"); + [server, req, 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); + ); instance.context.outputFileSystem.mkdirSync(outputPath, { recursive: true, @@ -4030,8 +4709,8 @@ describe.each([ ); }); - afterAll((done) => { - close(done); + afterAll(async () => { + await close(server, instance); }); it("should modify file", async () => { @@ -4045,5 +4724,460 @@ 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-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", `${response1.headers.etag}, test`); + + 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 "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); + 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(200); + expect(response2.headers.etag).toBeDefined(); + expect(response2.headers.etag.startsWith("W/")).toBe(true); + + const response3 = await req + .get(`/bundle.js`) + .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 () => { + 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); + }); + + 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( + name, + framework, + compiler, + { + 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 strong 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"', + ); + }); + + 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", () => { + 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"', + ); + }); + }); + + 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", () => { + 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); + }); + + 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-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 "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); + 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(200); + 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 "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(); + }); + + 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"', () => { + beforeEach(async () => { + const compiler = getCompiler(webpackConfig); + + [server, req, instance] = await frameworkFactory( + name, + framework, + compiler, + { + etag: "weak", + lastModified: true, + }, + ); + }); + + afterEach(async () => { + await close(server, instance); + }); + + 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(200); + 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 a9b0eebe0..62632f001 100644 --- a/test/validation-options.test.js +++ b/test/validation-options.test.js @@ -67,6 +67,14 @@ describe("validation", () => { ], failure: [true], }, + etag: { + 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 57cde10bc..54f15e97b 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -38,22 +38,22 @@ export = wdm; */ /** * @typedef {Object} ResponseData - * @property {string | Buffer | ReadStream} data + * @property {Buffer | ReadStream} data * @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 - * @param {string | Buffer | ReadStream} data + * @param {Buffer | ReadStream} data * @param {number} byteLength * @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,19 +65,19 @@ 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 */ /** - * @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] @@ -90,10 +90,12 @@ export = wdm; * @property {OutputFileSystem} [outputFileSystem] * @property {boolean | string} [index] * @property {ModifyResponseData} [modifyResponseData] + * @property {"weak" | "strong"} [etag] + * @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 @@ -130,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 */ /** @@ -145,21 +147,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 { + hapiWrapper, + koaWrapper, Schema, Compiler, MultiCompiler, @@ -194,15 +199,57 @@ declare namespace wdm { API, WithOptional, WithoutUndefined, + HapiPluginBase, + HapiPlugin, + HapiOptions, }; } 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; +/** + * @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 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; @@ -238,21 +285,23 @@ type Callback = ( stats?: import("webpack").Stats | import("webpack").MultiStats | undefined, ) => any; type ResponseData = { - data: string | Buffer | ReadStream; + data: Buffer | ReadStream; 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, - data: string | Buffer | ReadStream, + data: Buffer | ReadStream, 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; @@ -264,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 @@ -274,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 | (( @@ -285,8 +336,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?: | { @@ -305,10 +357,13 @@ type Options< modifyResponseData?: | ModifyResponseData | undefined; + etag?: "strong" | "weak" | undefined; + 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, @@ -336,3 +391,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; +}; 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 f2af44ce3..0998c346d 100644 --- a/types/utils/compatibleAPI.d.ts +++ b/types/utils/compatibleAPI.d.ts @@ -1,142 +1,63 @@ /// 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 = { - get: (name: string) => string | string[] | undefined; - set: (name: string, value: number | string | string[]) => void; - status: (status: number) => void; - send: (data: any) => void; -}; -/** - * 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; + status?: ((status: number) => void) | undefined; + send?: ((data: any) => void) | undefined; + pipeInto?: ((data: any) => void) | undefined; }; /** @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 {(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 - */ -/** - * @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} + * @property {(status: number) => void} [status] + * @property {(data: any) => void} [send] + * @property {(data: any) => void} [pipeInto] */ -export function setHeaderForResponse< - Response extends import("../index.js").ServerResponse, ->(res: Response, name: string, value: number | string | string[]): void; /** - * @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 + * @param {Response & ExpectedResponse} res + * @param {string | Buffer} bufferOrStream */ +export function send< + Request extends import("http").IncomingMessage, + Response extends import("../index.js").ServerResponse, +>(res: Response & ExpectedResponse, bufferOrStream: string | Buffer): void; /** - * @template {IncomingMessage} Request * @template {ServerResponse} Response - * @param {Request} req - * @param {Response} res + * @param {Response & ExpectedResponse} res + * @param {import("fs").ReadStream} bufferOrStream + */ +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 - * @param {() => Promise} goNext - * @param {SendOptions} options + * @returns {{ bufferOrStream: (Buffer | import("fs").ReadStream), byteLength: number }} */ -export function send< - Request extends import("http").IncomingMessage, - Response extends import("../index.js").ServerResponse, ->( - req: Request, - res: Response, +export function createReadStreamOrReadFileSync( filename: string, + outputFileSystem: import("../index").OutputFileSystem, start: number, end: number, - goNext: () => Promise, - options: SendOptions, -): Promise; -/** - * @template {IncomingMessage} Request - * @template {ServerResponse} Response - * @param {Request} req response - * @param {Response} res response - * @param {number} status status - * @param {Partial>=} options options - * @returns {void} - */ -export function sendError< - Request extends import("http").IncomingMessage, - Response extends import("../index.js").ServerResponse, ->( - req: Request, - res: Response, - status: number, - options?: Partial> | undefined, -): void; +): { + bufferOrStream: Buffer | import("fs").ReadStream; + byteLength: number; +}; 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/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; 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[];