From 40edb732497fae2d29d09d6b4f3a608e70888301 Mon Sep 17 00:00:00 2001 From: alexander-akait <4567934+alexander-akait@users.noreply.github.com> Date: Sun, 20 Sep 2026 12:44:17 +0000 Subject: [PATCH 1/8] perf: validate options with a precompiled schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `schema-utils`'s `validate()` compiles the options schema with ajv on its first call. That cost 117ms of every `new Server()` — measured over eight interleaved runs, 117.3 +/- 2.8ms, against 0.07ms for each call after it. It is a one-time price every user pays on every start. `lib/options.check.js` is that schema precompiled by `ajv`'s standalone codegen. The constructor asks it first and only falls back to `schema-utils` when it rejects, so the happy path never loads ajv and an invalid config still gets the same message. webpack solves its own schema the same way, and the generated validator agrees with `schema-utils` on all 183 cases of the `validate-options` corpus plus 62 hand-written ones. The constructor drops to 1.0ms, and time-to-listening for a hello-world falls from 565 +/- 14ms to 523 +/- 25ms. --- .changeset/perf-precompiled-options-schema.md | 5 + .prettierignore | 1 + eslint.config.mjs | 7 +- lib/Server.js | 14 +- lib/options.check.js | 5 + package-lock.json | 1 + package.json | 7 +- scripts/generate-schema-check.mjs | 159 ++++++++++++++++++ test/schema-check.test.js | 59 +++++++ types/lib/options.check.d.ts | 25 +++ 10 files changed, 276 insertions(+), 7 deletions(-) create mode 100644 .changeset/perf-precompiled-options-schema.md create mode 100644 lib/options.check.js create mode 100644 scripts/generate-schema-check.mjs create mode 100644 test/schema-check.test.js create mode 100644 types/lib/options.check.d.ts diff --git a/.changeset/perf-precompiled-options-schema.md b/.changeset/perf-precompiled-options-schema.md new file mode 100644 index 0000000000..dec27df3be --- /dev/null +++ b/.changeset/perf-precompiled-options-schema.md @@ -0,0 +1,5 @@ +--- +"webpack-dev-server": patch +--- + +Validate options with a precompiled schema to cut ~115ms from startup. diff --git a/.prettierignore b/.prettierignore index 65cd2f21a9..ef518cb1e7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,3 +4,4 @@ coverage node_modules CHANGELOG.md examples/client/trusted-types-overlay/app.js +/lib/options.check.js diff --git a/eslint.config.mjs b/eslint.config.mjs index c8b7bdb76b..fc6b35de37 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,7 +3,12 @@ import config from "eslint-config-webpack"; import configs from "eslint-config-webpack/configs.js"; export default defineConfig([ - globalIgnores(["client/**/*", "dist/**/*", "examples/**/*"]), + globalIgnores([ + "client/**/*", + "dist/**/*", + "examples/**/*", + "lib/options.check.js", + ]), { extends: [config], ignores: ["client-src/**/*", "!client-src/webpack.config.js"], diff --git a/lib/Server.js b/lib/Server.js index cb4944add5..72976d8851 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -5,6 +5,7 @@ import url from "node:url"; import fs from "graceful-fs"; import ipaddr from "ipaddr.js"; import { validate } from "schema-utils"; +import validateOptions from "./options.check.js"; import schema from "./options.json" with { type: "json" }; // Named `cjsRequire` (not `require`) so it doesn't shadow the implicit CommonJS @@ -343,10 +344,15 @@ class Server { constructor(options, compiler) { options = options === undefined ? {} : options; - validate(/** @type {Schema} */ (schema), options, { - name: "Dev Server", - baseDataPath: "options", - }); + // The precompiled validator answers the common case without ajv, which + // `schema-utils` would otherwise spend ~120ms compiling on every startup. + // Rejected options go through `schema-utils` for the readable message. + if (!validateOptions(options)) { + validate(/** @type {Schema} */ (schema), options, { + name: "Dev Server", + baseDataPath: "options", + }); + } if (compiler) { this.compiler = compiler; diff --git a/lib/options.check.js b/lib/options.check.js new file mode 100644 index 0000000000..fc2a35c50d --- /dev/null +++ b/lib/options.check.js @@ -0,0 +1,5 @@ +// This file was automatically generated. +// DO NOT MODIFY BY HAND. Run `npm run fix:schema-check` to update. +/* eslint-disable */ +// @ts-nocheck +"use strict";export const validate = validate10;export default validate10;const schema11 = {"title":"Dev Server options","type":"object","definitions":{"App":{"instanceof":"Function","description":"Allows to use custom applications, such as 'connect', 'fastify', etc.","link":"https://webpack.js.org/configuration/dev-server/#devserverapp"},"AllowedHosts":{"anyOf":[{"type":"array","minItems":1,"items":{"$ref":"#/definitions/AllowedHostsItem"}},{"enum":["auto","all"]},{"$ref":"#/definitions/AllowedHostsItem"}],"description":"Allows to enumerate the hosts from which access to the dev server are allowed (useful when you are proxying dev server, by default is 'auto').","link":"https://webpack.js.org/configuration/dev-server/#devserverallowedhosts"},"AllowedHostsItem":{"type":"string","minLength":1},"Bonjour":{"anyOf":[{"type":"boolean","cli":{"negatedDescription":"Disallows to broadcasts dev server via ZeroConf networking on start."}},{"type":"object","description":"Options for bonjour.","link":"https://github.com/watson/bonjour#initializing"}],"description":"Allows to broadcasts dev server via ZeroConf networking on start.","link":" https://webpack.js.org/configuration/dev-server/#devserverbonjour"},"Client":{"description":"Allows to specify options for client script in the browser or disable client script.","link":"https://webpack.js.org/configuration/dev-server/#devserverclient","anyOf":[{"enum":[false],"cli":{"negatedDescription":"Disables client script."}},{"type":"object","additionalProperties":false,"properties":{"logging":{"$ref":"#/definitions/ClientLogging"},"overlay":{"$ref":"#/definitions/ClientOverlay"},"progress":{"$ref":"#/definitions/ClientProgress"},"reconnect":{"$ref":"#/definitions/ClientReconnect"},"webSocketTransport":{"$ref":"#/definitions/ClientWebSocketTransport"},"webSocketURL":{"$ref":"#/definitions/ClientWebSocketURL"}}}]},"ClientLogging":{"enum":["none","error","warn","info","log","verbose"],"description":"Allows to set log level in the browser.","link":"https://webpack.js.org/configuration/dev-server/#logging"},"ClientOverlay":{"anyOf":[{"description":"Enables a full-screen overlay in the browser when there are compiler errors or warnings.","link":"https://webpack.js.org/configuration/dev-server/#overlay","type":"boolean","cli":{"negatedDescription":"Disables the full-screen overlay in the browser when there are compiler errors or warnings."}},{"type":"object","additionalProperties":false,"properties":{"errors":{"anyOf":[{"description":"Enables a full-screen overlay in the browser when there are compiler errors.","type":"boolean","cli":{"negatedDescription":"Disables the full-screen overlay in the browser when there are compiler errors."}},{"instanceof":"Function","description":"Filter compiler errors. Return true to include and return false to exclude."}]},"warnings":{"anyOf":[{"description":"Enables a full-screen overlay in the browser when there are compiler warnings.","type":"boolean","cli":{"negatedDescription":"Disables the full-screen overlay in the browser when there are compiler warnings."}},{"instanceof":"Function","description":"Filter compiler warnings. Return true to include and return false to exclude."}]},"runtimeErrors":{"anyOf":[{"description":"Enables a full-screen overlay in the browser when there are uncaught runtime errors.","type":"boolean","cli":{"negatedDescription":"Disables the full-screen overlay in the browser when there are uncaught runtime errors."}},{"instanceof":"Function","description":"Filter uncaught runtime errors. Return true to include and return false to exclude."}]},"trustedTypesPolicyName":{"description":"The name of a Trusted Types policy for the overlay. Defaults to 'webpack-dev-server#overlay'.","type":"string"}}}]},"ClientProgress":{"description":"Displays compilation progress in the browser. Options include 'linear' and 'circular' for visual indicators.","link":"https://webpack.js.org/configuration/dev-server/#progress","type":["boolean","string"],"enum":[true,false,"linear","circular"],"cli":{"negatedDescription":"Does not display compilation progress in the browser."}},"ClientReconnect":{"description":"Tells dev-server the number of times it should try to reconnect the client.","link":"https://webpack.js.org/configuration/dev-server/#reconnect","anyOf":[{"type":"boolean","cli":{"negatedDescription":"Tells dev-server to not to try to reconnect the client."}},{"type":"number","minimum":0}]},"ClientWebSocketTransport":{"anyOf":[{"$ref":"#/definitions/ClientWebSocketTransportEnum"},{"$ref":"#/definitions/ClientWebSocketTransportString"}],"description":"Allows to set custom web socket transport to communicate with dev server.","link":"https://webpack.js.org/configuration/dev-server/#websockettransport"},"ClientWebSocketTransportEnum":{"enum":["ws"]},"ClientWebSocketTransportString":{"type":"string","minLength":1},"ClientWebSocketURL":{"description":"Allows to specify URL to web socket server (useful when you're proxying dev server and client script does not always know where to connect to).","link":"https://webpack.js.org/configuration/dev-server/#websocketurl","anyOf":[{"type":"string","minLength":1},{"type":"object","additionalProperties":false,"properties":{"hostname":{"description":"Tells clients connected to devServer to use the provided hostname.","type":"string","minLength":1},"pathname":{"description":"Tells clients connected to devServer to use the provided path to connect.","type":"string"},"password":{"description":"Tells clients connected to devServer to use the provided password to authenticate.","type":"string"},"port":{"description":"Tells clients connected to devServer to use the provided port.","anyOf":[{"type":"number"},{"type":"string","minLength":1}]},"protocol":{"description":"Tells clients connected to devServer to use the provided protocol.","anyOf":[{"enum":["auto"]},{"type":"string","minLength":1}]},"username":{"description":"Tells clients connected to devServer to use the provided username to authenticate.","type":"string"}}}]},"Compress":{"type":"boolean","description":"Enables gzip compression for everything served.","link":"https://webpack.js.org/configuration/dev-server/#devservercompress","cli":{"negatedDescription":"Disables gzip compression for everything served."}},"DevMiddleware":{"description":"Provide options to 'webpack-dev-middleware' which handles webpack assets.","link":"https://webpack.js.org/configuration/dev-server/#devserverdevmiddleware","type":"object","additionalProperties":true},"HeaderObject":{"type":"object","additionalProperties":false,"properties":{"key":{"description":"key of header.","type":"string"},"value":{"description":"value of header.","type":"string"}},"cli":{"exclude":true}},"Headers":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/HeaderObject"},"minItems":1},{"type":"object"},{"instanceof":"Function"}],"description":"Allows to set custom headers on response.","link":"https://webpack.js.org/configuration/dev-server/#devserverheaders"},"HistoryApiFallback":{"anyOf":[{"type":"boolean","cli":{"negatedDescription":"Disallows to proxy requests through a specified index page."}},{"type":"object","description":"Options for `historyApiFallback`.","link":"https://github.com/bripkens/connect-history-api-fallback#options"}],"description":"Allows to proxy requests through a specified index page (by default 'index.html'), useful for Single Page Applications that utilise the HTML5 History API.","link":"https://webpack.js.org/configuration/dev-server/#devserverhistoryapifallback"},"Host":{"description":"Allows to specify a hostname to use.","link":"https://webpack.js.org/configuration/dev-server/#devserverhost","anyOf":[{"enum":["local-ip","local-ipv4","local-ipv6"]},{"type":"string","minLength":1}]},"Hot":{"anyOf":[{"type":"boolean","cli":{"negatedDescription":"Disables Hot Module Replacement."}},{"enum":["only"]}],"description":"Enables Hot Module Replacement.","link":"https://webpack.js.org/configuration/dev-server/#devserverhot"},"IPC":{"anyOf":[{"type":"string","minLength":1},{"type":"boolean","enum":[true]}],"description":"Listen to a unix socket.","link":"https://webpack.js.org/configuration/dev-server/#devserveripc"},"LiveReload":{"type":"boolean","description":"Enables reload/refresh the page(s) when file changes are detected (enabled by default).","cli":{"negatedDescription":"Disables reload/refresh the page(s) when file changes are detected (enabled by default)."},"link":"https://webpack.js.org/configuration/dev-server/#devserverlivereload"},"OnListening":{"instanceof":"Function","description":"Provides the ability to execute a custom function when dev server starts listening.","link":"https://webpack.js.org/configuration/dev-server/#devserveronlistening"},"Open":{"anyOf":[{"type":"array","items":{"anyOf":[{"$ref":"#/definitions/OpenString"},{"$ref":"#/definitions/OpenObject"}]}},{"$ref":"#/definitions/OpenBoolean"},{"$ref":"#/definitions/OpenString"},{"$ref":"#/definitions/OpenObject"}],"description":"Allows to configure dev server to open the browser(s) and page(s) after server had been started (set it to true to open your default browser).","link":"https://webpack.js.org/configuration/dev-server/#devserveropen"},"OpenBoolean":{"type":"boolean","cli":{"negatedDescription":"Does not open the default browser."}},"OpenObject":{"type":"object","additionalProperties":false,"properties":{"target":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}],"description":"Opens specified page in browser."},"app":{"anyOf":[{"type":"object","additionalProperties":false,"properties":{"name":{"anyOf":[{"type":"array","items":{"type":"string","minLength":1},"minItems":1},{"type":"string","minLength":1}]},"arguments":{"items":{"type":"string","minLength":1}}}},{"type":"string","minLength":1,"description":"Open specified browser.","cli":{"exclude":true}}],"description":"Open specified browser."}}},"OpenString":{"type":"string","minLength":1},"Port":{"anyOf":[{"type":"number","minimum":0,"maximum":65535},{"type":"string","minLength":1},{"enum":["auto"]}],"description":"Allows to specify a port to use.","link":"https://webpack.js.org/configuration/dev-server/#devserverport"},"Proxy":{"type":"array","items":{"anyOf":[{"type":"object"},{"instanceof":"Function"}]},"description":"Allows to proxy requests, can be useful when you have a separate API backend development server and you want to send API requests on the same domain.","link":"https://webpack.js.org/configuration/dev-server/#devserverproxy"},"Server":{"anyOf":[{"$ref":"#/definitions/ServerEnum"},{"$ref":"#/definitions/ServerFn"},{"$ref":"#/definitions/ServerString"},{"$ref":"#/definitions/ServerObject"}],"link":"https://webpack.js.org/configuration/dev-server/#devserverserver","description":"Allows to set server and options (by default 'http')."},"ServerType":{"enum":["http","https","http2"]},"ServerFn":{"instanceof":"Function"},"ServerEnum":{"enum":["http","https","http2"],"cli":{"exclude":true}},"ServerString":{"type":"string","minLength":1,"cli":{"exclude":true}},"ServerObject":{"type":"object","properties":{"type":{"anyOf":[{"$ref":"#/definitions/ServerType"},{"$ref":"#/definitions/ServerString"},{"$ref":"#/definitions/ServerFn"}]},"options":{"$ref":"#/definitions/ServerOptions"}},"additionalProperties":false},"ServerOptions":{"type":"object","additionalProperties":true,"properties":{"passphrase":{"type":"string","description":"Passphrase for a pfx file."},"requestCert":{"type":"boolean","description":"Request for an SSL certificate.","cli":{"negatedDescription":"Does not request for an SSL certificate."}},"ca":{"anyOf":[{"type":"array","items":{"anyOf":[{"type":"string"},{"instanceof":"Buffer"}]}},{"type":"string"},{"instanceof":"Buffer"}],"description":"Path to an SSL CA certificate or content of an SSL CA certificate."},"cert":{"anyOf":[{"type":"array","items":{"anyOf":[{"type":"string"},{"instanceof":"Buffer"}]}},{"type":"string"},{"instanceof":"Buffer"}],"description":"Path to an SSL certificate or content of an SSL certificate."},"crl":{"anyOf":[{"type":"array","items":{"anyOf":[{"type":"string"},{"instanceof":"Buffer"}]}},{"type":"string"},{"instanceof":"Buffer"}],"description":"Path to PEM formatted CRLs (Certificate Revocation Lists) or content of PEM formatted CRLs (Certificate Revocation Lists)."},"key":{"anyOf":[{"type":"array","items":{"anyOf":[{"type":"string"},{"instanceof":"Buffer"},{"type":"object","additionalProperties":true}]}},{"type":"string"},{"instanceof":"Buffer"}],"description":"Path to an SSL key or content of an SSL key."},"pfx":{"anyOf":[{"type":"array","items":{"anyOf":[{"type":"string"},{"instanceof":"Buffer"},{"type":"object","additionalProperties":true}]}},{"type":"string"},{"instanceof":"Buffer"}],"description":"Path to an SSL pfx file or content of an SSL pfx file."}}},"SetupExitSignals":{"type":"boolean","description":"Allows to close dev server and exit the process on SIGINT and SIGTERM signals (enabled by default for CLI).","link":"https://webpack.js.org/configuration/dev-server/#devserversetupexitsignals","cli":{"exclude":true}},"SetupMiddlewares":{"instanceof":"Function","description":"Provides the ability to execute a custom function and apply custom middleware(s).","link":"https://webpack.js.org/configuration/dev-server/#devserversetupmiddlewares"},"Static":{"anyOf":[{"type":"array","items":{"anyOf":[{"$ref":"#/definitions/StaticString"},{"$ref":"#/definitions/StaticObject"}]}},{"type":"boolean","cli":{"negatedDescription":"Disallows to configure options for serving static files from directory."}},{"$ref":"#/definitions/StaticString"},{"$ref":"#/definitions/StaticObject"}],"description":"Allows to configure options for serving static files from directory (by default 'public' directory).","link":"https://webpack.js.org/configuration/dev-server/#devserverstatic"},"StaticObject":{"type":"object","additionalProperties":false,"properties":{"directory":{"type":"string","minLength":1,"description":"Directory for static contents.","link":"https://webpack.js.org/configuration/dev-server/#directory"},"staticOptions":{"type":"object","link":"https://webpack.js.org/configuration/dev-server/#staticoptions","additionalProperties":true},"publicPath":{"anyOf":[{"type":"array","items":{"type":"string"},"minItems":1},{"type":"string"}],"description":"The static files will be available in the browser under this public path.","link":"https://webpack.js.org/configuration/dev-server/#publicpath"},"serveIndex":{"anyOf":[{"type":"boolean","cli":{"negatedDescription":"Does not tell dev server to use serveIndex middleware."}},{"type":"object","additionalProperties":true}],"description":"Tells dev server to use serveIndex middleware when enabled.","link":"https://webpack.js.org/configuration/dev-server/#serveindex"},"watch":{"anyOf":[{"type":"boolean","cli":{"negatedDescription":"Does not watch for files in static content directory."}},{"type":"object","description":"Options for watch.","link":"https://github.com/paulmillr/chokidar#api"}],"description":"Watches for files in static content directory.","link":"https://webpack.js.org/configuration/dev-server/#watch"}}},"StaticString":{"type":"string","minLength":1},"WatchFiles":{"anyOf":[{"type":"array","items":{"anyOf":[{"$ref":"#/definitions/WatchFilesString"},{"$ref":"#/definitions/WatchFilesObject"}]}},{"$ref":"#/definitions/WatchFilesString"},{"$ref":"#/definitions/WatchFilesObject"}],"description":"Allows to configure list of globs/directories/files to watch for file changes.","link":"https://webpack.js.org/configuration/dev-server/#devserverwatchfiles"},"WatchFilesObject":{"cli":{"exclude":true},"type":"object","properties":{"paths":{"anyOf":[{"type":"array","items":{"type":"string","minLength":1}},{"type":"string","minLength":1}],"description":"Path(s) of globs/directories/files to watch for file changes."},"options":{"type":"object","description":"Configure advanced options for watching. See the chokidar documentation for the possible options.","link":"https://github.com/paulmillr/chokidar#api","additionalProperties":true}},"additionalProperties":false},"WatchFilesString":{"type":"string","minLength":1},"WebSocketServer":{"anyOf":[{"$ref":"#/definitions/WebSocketServerEnum"},{"$ref":"#/definitions/WebSocketServerString"},{"$ref":"#/definitions/WebSocketServerFunction"},{"$ref":"#/definitions/WebSocketServerObject"}],"description":"Allows to set web socket server and options (by default 'ws').","link":"https://webpack.js.org/configuration/dev-server/#devserverwebsocketserver"},"WebSocketServerType":{"enum":["ws"]},"WebSocketServerEnum":{"anyOf":[{"enum":[false],"cli":{"negatedDescription":"Disallows to set web socket server and options."}},{"enum":["ws"],"cli":{"exclude":true}}]},"WebSocketServerFunction":{"instanceof":"Function"},"WebSocketServerObject":{"type":"object","properties":{"type":{"anyOf":[{"$ref":"#/definitions/WebSocketServerType"},{"$ref":"#/definitions/WebSocketServerString"},{"$ref":"#/definitions/WebSocketServerFunction"}]},"options":{"type":"object","additionalProperties":true,"cli":{"exclude":true}}},"additionalProperties":false},"WebSocketServerString":{"type":"string","minLength":1,"cli":{"exclude":true}}},"additionalProperties":false,"properties":{"allowedHosts":{"$ref":"#/definitions/AllowedHosts"},"bonjour":{"$ref":"#/definitions/Bonjour"},"client":{"$ref":"#/definitions/Client"},"compress":{"$ref":"#/definitions/Compress"},"devMiddleware":{"$ref":"#/definitions/DevMiddleware"},"headers":{"$ref":"#/definitions/Headers"},"historyApiFallback":{"$ref":"#/definitions/HistoryApiFallback"},"host":{"$ref":"#/definitions/Host"},"hot":{"$ref":"#/definitions/Hot"},"ipc":{"$ref":"#/definitions/IPC"},"liveReload":{"$ref":"#/definitions/LiveReload"},"onListening":{"$ref":"#/definitions/OnListening"},"open":{"$ref":"#/definitions/Open"},"port":{"$ref":"#/definitions/Port"},"proxy":{"$ref":"#/definitions/Proxy"},"server":{"$ref":"#/definitions/Server"},"app":{"$ref":"#/definitions/App"},"setupExitSignals":{"$ref":"#/definitions/SetupExitSignals"},"setupMiddlewares":{"$ref":"#/definitions/SetupMiddlewares"},"static":{"$ref":"#/definitions/Static"},"watchFiles":{"$ref":"#/definitions/WatchFiles"},"webSocketServer":{"$ref":"#/definitions/WebSocketServer"}}};const schema15 = {"anyOf":[{"type":"boolean","cli":{"negatedDescription":"Disallows to broadcasts dev server via ZeroConf networking on start."}},{"type":"object","description":"Options for bonjour.","link":"https://github.com/watson/bonjour#initializing"}],"description":"Allows to broadcasts dev server via ZeroConf networking on start.","link":" https://webpack.js.org/configuration/dev-server/#devserverbonjour"};const schema25 = {"type":"boolean","description":"Enables gzip compression for everything served.","link":"https://webpack.js.org/configuration/dev-server/#devservercompress","cli":{"negatedDescription":"Disables gzip compression for everything served."}};const schema26 = {"description":"Provide options to 'webpack-dev-middleware' which handles webpack assets.","link":"https://webpack.js.org/configuration/dev-server/#devserverdevmiddleware","type":"object","additionalProperties":true};const schema29 = {"anyOf":[{"type":"boolean","cli":{"negatedDescription":"Disallows to proxy requests through a specified index page."}},{"type":"object","description":"Options for `historyApiFallback`.","link":"https://github.com/bripkens/connect-history-api-fallback#options"}],"description":"Allows to proxy requests through a specified index page (by default 'index.html'), useful for Single Page Applications that utilise the HTML5 History API.","link":"https://webpack.js.org/configuration/dev-server/#devserverhistoryapifallback"};const schema30 = {"description":"Allows to specify a hostname to use.","link":"https://webpack.js.org/configuration/dev-server/#devserverhost","anyOf":[{"enum":["local-ip","local-ipv4","local-ipv6"]},{"type":"string","minLength":1}]};const schema31 = {"anyOf":[{"type":"boolean","cli":{"negatedDescription":"Disables Hot Module Replacement."}},{"enum":["only"]}],"description":"Enables Hot Module Replacement.","link":"https://webpack.js.org/configuration/dev-server/#devserverhot"};const schema32 = {"anyOf":[{"type":"string","minLength":1},{"type":"boolean","enum":[true]}],"description":"Listen to a unix socket.","link":"https://webpack.js.org/configuration/dev-server/#devserveripc"};const schema33 = {"type":"boolean","description":"Enables reload/refresh the page(s) when file changes are detected (enabled by default).","cli":{"negatedDescription":"Disables reload/refresh the page(s) when file changes are detected (enabled by default)."},"link":"https://webpack.js.org/configuration/dev-server/#devserverlivereload"};const schema34 = {"instanceof":"Function","description":"Provides the ability to execute a custom function when dev server starts listening.","link":"https://webpack.js.org/configuration/dev-server/#devserveronlistening"};const schema41 = {"anyOf":[{"type":"number","minimum":0,"maximum":65535},{"type":"string","minLength":1},{"enum":["auto"]}],"description":"Allows to specify a port to use.","link":"https://webpack.js.org/configuration/dev-server/#devserverport"};const schema42 = {"type":"array","items":{"anyOf":[{"type":"object"},{"instanceof":"Function"}]},"description":"Allows to proxy requests, can be useful when you have a separate API backend development server and you want to send API requests on the same domain.","link":"https://webpack.js.org/configuration/dev-server/#devserverproxy"};const schema52 = {"instanceof":"Function","description":"Allows to use custom applications, such as 'connect', 'fastify', etc.","link":"https://webpack.js.org/configuration/dev-server/#devserverapp"};const schema53 = {"type":"boolean","description":"Allows to close dev server and exit the process on SIGINT and SIGTERM signals (enabled by default for CLI).","link":"https://webpack.js.org/configuration/dev-server/#devserversetupexitsignals","cli":{"exclude":true}};const schema54 = {"instanceof":"Function","description":"Provides the ability to execute a custom function and apply custom middleware(s).","link":"https://webpack.js.org/configuration/dev-server/#devserversetupmiddlewares"};const func2 = Object.prototype.hasOwnProperty;const schema12 = {"anyOf":[{"type":"array","minItems":1,"items":{"$ref":"#/definitions/AllowedHostsItem"}},{"enum":["auto","all"]},{"$ref":"#/definitions/AllowedHostsItem"}],"description":"Allows to enumerate the hosts from which access to the dev server are allowed (useful when you are proxying dev server, by default is 'auto').","link":"https://webpack.js.org/configuration/dev-server/#devserverallowedhosts"};const schema13 = {"type":"string","minLength":1};function validate11(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){let vErrors = null;let errors = 0;const _errs0 = errors;let valid0 = false;const _errs1 = errors;if(errors === _errs1){if(Array.isArray(data)){if(data.length < 1){const err0 = {instancePath,schemaPath:"#/anyOf/0/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}else {var valid1 = true;const len0 = data.length;for(let i0=0; i0=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err20];}else {vErrors.push(err20);}errors++;}}else {const err21 = {instancePath:instancePath+"/reconnect",schemaPath:"#/definitions/ClientReconnect/anyOf/1/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err21];}else {vErrors.push(err21);}errors++;}}var _valid5 = _errs50 === errors;valid11 = valid11 || _valid5;}if(!valid11){const err22 = {instancePath:instancePath+"/reconnect",schemaPath:"#/definitions/ClientReconnect/anyOf",keyword:"anyOf",params:{},message:"must match a schema in anyOf"};if(vErrors === null){vErrors = [err22];}else {vErrors.push(err22);}errors++;}else {errors = _errs46;if(vErrors !== null){if(_errs46){vErrors.length = _errs46;}else {vErrors = null;}}}var valid1 = _errs44 === errors;}else {var valid1 = true;}if(valid1){if(data.webSocketTransport !== undefined){const _errs53 = errors;if(!(validate14(data.webSocketTransport, {instancePath:instancePath+"/webSocketTransport",parentData:data,parentDataProperty:"webSocketTransport",rootData}))){vErrors = vErrors === null ? validate14.errors : vErrors.concat(validate14.errors);errors = vErrors.length;}var valid1 = _errs53 === errors;}else {var valid1 = true;}if(valid1){if(data.webSocketURL !== undefined){let data9 = data.webSocketURL;const _errs54 = errors;const _errs56 = errors;let valid13 = false;const _errs57 = errors;if(errors === _errs57){if(typeof data9 === "string"){if(data9.length < 1){const err23 = {instancePath:instancePath+"/webSocketURL",schemaPath:"#/definitions/ClientWebSocketURL/anyOf/0/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err23];}else {vErrors.push(err23);}errors++;}}else {const err24 = {instancePath:instancePath+"/webSocketURL",schemaPath:"#/definitions/ClientWebSocketURL/anyOf/0/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err24];}else {vErrors.push(err24);}errors++;}}var _valid6 = _errs57 === errors;valid13 = valid13 || _valid6;if(!valid13){const _errs59 = errors;if(errors === _errs59){if(data9 && typeof data9 == "object" && !Array.isArray(data9)){const _errs61 = errors;for(const key2 in data9){if(!((((((key2 === "hostname") || (key2 === "pathname")) || (key2 === "password")) || (key2 === "port")) || (key2 === "protocol")) || (key2 === "username"))){const err25 = {instancePath:instancePath+"/webSocketURL",schemaPath:"#/definitions/ClientWebSocketURL/anyOf/1/additionalProperties",keyword:"additionalProperties",params:{additionalProperty: key2},message:"must NOT have additional properties"};if(vErrors === null){vErrors = [err25];}else {vErrors.push(err25);}errors++;break;}}if(_errs61 === errors){if(data9.hostname !== undefined){let data10 = data9.hostname;const _errs62 = errors;if(errors === _errs62){if(typeof data10 === "string"){if(data10.length < 1){const err26 = {instancePath:instancePath+"/webSocketURL/hostname",schemaPath:"#/definitions/ClientWebSocketURL/anyOf/1/properties/hostname/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err26];}else {vErrors.push(err26);}errors++;}}else {const err27 = {instancePath:instancePath+"/webSocketURL/hostname",schemaPath:"#/definitions/ClientWebSocketURL/anyOf/1/properties/hostname/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err27];}else {vErrors.push(err27);}errors++;}}var valid14 = _errs62 === errors;}else {var valid14 = true;}if(valid14){if(data9.pathname !== undefined){const _errs64 = errors;if(typeof data9.pathname !== "string"){const err28 = {instancePath:instancePath+"/webSocketURL/pathname",schemaPath:"#/definitions/ClientWebSocketURL/anyOf/1/properties/pathname/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err28];}else {vErrors.push(err28);}errors++;}var valid14 = _errs64 === errors;}else {var valid14 = true;}if(valid14){if(data9.password !== undefined){const _errs66 = errors;if(typeof data9.password !== "string"){const err29 = {instancePath:instancePath+"/webSocketURL/password",schemaPath:"#/definitions/ClientWebSocketURL/anyOf/1/properties/password/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err29];}else {vErrors.push(err29);}errors++;}var valid14 = _errs66 === errors;}else {var valid14 = true;}if(valid14){if(data9.port !== undefined){let data13 = data9.port;const _errs68 = errors;const _errs69 = errors;let valid15 = false;const _errs70 = errors;if(!(typeof data13 == "number")){const err30 = {instancePath:instancePath+"/webSocketURL/port",schemaPath:"#/definitions/ClientWebSocketURL/anyOf/1/properties/port/anyOf/0/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err30];}else {vErrors.push(err30);}errors++;}var _valid7 = _errs70 === errors;valid15 = valid15 || _valid7;if(!valid15){const _errs72 = errors;if(errors === _errs72){if(typeof data13 === "string"){if(data13.length < 1){const err31 = {instancePath:instancePath+"/webSocketURL/port",schemaPath:"#/definitions/ClientWebSocketURL/anyOf/1/properties/port/anyOf/1/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err31];}else {vErrors.push(err31);}errors++;}}else {const err32 = {instancePath:instancePath+"/webSocketURL/port",schemaPath:"#/definitions/ClientWebSocketURL/anyOf/1/properties/port/anyOf/1/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err32];}else {vErrors.push(err32);}errors++;}}var _valid7 = _errs72 === errors;valid15 = valid15 || _valid7;}if(!valid15){const err33 = {instancePath:instancePath+"/webSocketURL/port",schemaPath:"#/definitions/ClientWebSocketURL/anyOf/1/properties/port/anyOf",keyword:"anyOf",params:{},message:"must match a schema in anyOf"};if(vErrors === null){vErrors = [err33];}else {vErrors.push(err33);}errors++;}else {errors = _errs69;if(vErrors !== null){if(_errs69){vErrors.length = _errs69;}else {vErrors = null;}}}var valid14 = _errs68 === errors;}else {var valid14 = true;}if(valid14){if(data9.protocol !== undefined){let data14 = data9.protocol;const _errs74 = errors;const _errs75 = errors;let valid16 = false;const _errs76 = errors;if(!(data14 === "auto")){const err34 = {instancePath:instancePath+"/webSocketURL/protocol",schemaPath:"#/definitions/ClientWebSocketURL/anyOf/1/properties/protocol/anyOf/0/enum",keyword:"enum",params:{allowedValues: schema24.anyOf[1].properties.protocol.anyOf[0].enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err34];}else {vErrors.push(err34);}errors++;}var _valid8 = _errs76 === errors;valid16 = valid16 || _valid8;if(!valid16){const _errs77 = errors;if(errors === _errs77){if(typeof data14 === "string"){if(data14.length < 1){const err35 = {instancePath:instancePath+"/webSocketURL/protocol",schemaPath:"#/definitions/ClientWebSocketURL/anyOf/1/properties/protocol/anyOf/1/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err35];}else {vErrors.push(err35);}errors++;}}else {const err36 = {instancePath:instancePath+"/webSocketURL/protocol",schemaPath:"#/definitions/ClientWebSocketURL/anyOf/1/properties/protocol/anyOf/1/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err36];}else {vErrors.push(err36);}errors++;}}var _valid8 = _errs77 === errors;valid16 = valid16 || _valid8;}if(!valid16){const err37 = {instancePath:instancePath+"/webSocketURL/protocol",schemaPath:"#/definitions/ClientWebSocketURL/anyOf/1/properties/protocol/anyOf",keyword:"anyOf",params:{},message:"must match a schema in anyOf"};if(vErrors === null){vErrors = [err37];}else {vErrors.push(err37);}errors++;}else {errors = _errs75;if(vErrors !== null){if(_errs75){vErrors.length = _errs75;}else {vErrors = null;}}}var valid14 = _errs74 === errors;}else {var valid14 = true;}if(valid14){if(data9.username !== undefined){const _errs79 = errors;if(typeof data9.username !== "string"){const err38 = {instancePath:instancePath+"/webSocketURL/username",schemaPath:"#/definitions/ClientWebSocketURL/anyOf/1/properties/username/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err38];}else {vErrors.push(err38);}errors++;}var valid14 = _errs79 === errors;}else {var valid14 = true;}}}}}}}}else {const err39 = {instancePath:instancePath+"/webSocketURL",schemaPath:"#/definitions/ClientWebSocketURL/anyOf/1/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err39];}else {vErrors.push(err39);}errors++;}}var _valid6 = _errs59 === errors;valid13 = valid13 || _valid6;}if(!valid13){const err40 = {instancePath:instancePath+"/webSocketURL",schemaPath:"#/definitions/ClientWebSocketURL/anyOf",keyword:"anyOf",params:{},message:"must match a schema in anyOf"};if(vErrors === null){vErrors = [err40];}else {vErrors.push(err40);}errors++;}else {errors = _errs56;if(vErrors !== null){if(_errs56){vErrors.length = _errs56;}else {vErrors = null;}}}var valid1 = _errs54 === errors;}else {var valid1 = true;}}}}}}}}else {const err41 = {instancePath,schemaPath:"#/anyOf/1/type",keyword:"type",params:{type: "object"},message:"must be object"};if(vErrors === null){vErrors = [err41];}else {vErrors.push(err41);}errors++;}}var _valid0 = _errs3 === errors;valid0 = valid0 || _valid0;}if(!valid0){const err42 = {instancePath,schemaPath:"#/anyOf",keyword:"anyOf",params:{},message:"must match a schema in anyOf"};if(vErrors === null){vErrors = [err42];}else {vErrors.push(err42);}errors++;validate13.errors = vErrors;return false;}else {errors = _errs0;if(vErrors !== null){if(_errs0){vErrors.length = _errs0;}else {vErrors = null;}}}validate13.errors = vErrors;return errors === 0;}const schema27 = {"anyOf":[{"type":"array","items":{"$ref":"#/definitions/HeaderObject"},"minItems":1},{"type":"object"},{"instanceof":"Function"}],"description":"Allows to set custom headers on response.","link":"https://webpack.js.org/configuration/dev-server/#devserverheaders"};const schema28 = {"type":"object","additionalProperties":false,"properties":{"key":{"description":"key of header.","type":"string"},"value":{"description":"value of header.","type":"string"}},"cli":{"exclude":true}};function validate17(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){let vErrors = null;let errors = 0;const _errs0 = errors;let valid0 = false;const _errs1 = errors;if(errors === _errs1){if(Array.isArray(data)){if(data.length < 1){const err0 = {instancePath,schemaPath:"#/anyOf/0/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items"};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}else {var valid1 = true;const len0 = data.length;for(let i0=0; i0 65535 || isNaN(data13)){const err18 = {instancePath:instancePath+"/port",schemaPath:"#/definitions/Port/anyOf/0/maximum",keyword:"maximum",params:{comparison: "<=", limit: 65535},message:"must be <= 65535"};if(vErrors === null){vErrors = [err18];}else {vErrors.push(err18);}errors++;}else {if(data13 < 0 || isNaN(data13)){const err19 = {instancePath:instancePath+"/port",schemaPath:"#/definitions/Port/anyOf/0/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0"};if(vErrors === null){vErrors = [err19];}else {vErrors.push(err19);}errors++;}}}else {const err20 = {instancePath:instancePath+"/port",schemaPath:"#/definitions/Port/anyOf/0/type",keyword:"type",params:{type: "number"},message:"must be number"};if(vErrors === null){vErrors = [err20];}else {vErrors.push(err20);}errors++;}}var _valid5 = _errs70 === errors;valid16 = valid16 || _valid5;if(!valid16){const _errs72 = errors;if(errors === _errs72){if(typeof data13 === "string"){if(data13.length < 1){const err21 = {instancePath:instancePath+"/port",schemaPath:"#/definitions/Port/anyOf/1/minLength",keyword:"minLength",params:{limit: 1},message:"must NOT have fewer than 1 characters"};if(vErrors === null){vErrors = [err21];}else {vErrors.push(err21);}errors++;}}else {const err22 = {instancePath:instancePath+"/port",schemaPath:"#/definitions/Port/anyOf/1/type",keyword:"type",params:{type: "string"},message:"must be string"};if(vErrors === null){vErrors = [err22];}else {vErrors.push(err22);}errors++;}}var _valid5 = _errs72 === errors;valid16 = valid16 || _valid5;if(!valid16){const _errs74 = errors;if(!(data13 === "auto")){const err23 = {instancePath:instancePath+"/port",schemaPath:"#/definitions/Port/anyOf/2/enum",keyword:"enum",params:{allowedValues: schema41.anyOf[2].enum},message:"must be equal to one of the allowed values"};if(vErrors === null){vErrors = [err23];}else {vErrors.push(err23);}errors++;}var _valid5 = _errs74 === errors;valid16 = valid16 || _valid5;}}if(!valid16){const err24 = {instancePath:instancePath+"/port",schemaPath:"#/definitions/Port/anyOf",keyword:"anyOf",params:{},message:"must match a schema in anyOf"};if(vErrors === null){vErrors = [err24];}else {vErrors.push(err24);}errors++;validate10.errors = vErrors;return false;}else {errors = _errs69;if(vErrors !== null){if(_errs69){vErrors.length = _errs69;}else {vErrors = null;}}}var valid0 = _errs67 === errors;}else {var valid0 = true;}if(valid0){if(data.proxy !== undefined){let data14 = data.proxy;const _errs76 = errors;const _errs77 = errors;if(errors === _errs77){if(Array.isArray(data14)){var valid18 = true;const len0 = data14.length;for(let i0=0; i0} SchemaNode */ + +/** + * Walk every schema node, depth first. + * @param {unknown} node current node + * @param {(node: SchemaNode, pointer: string) => void} visit called for each object node + * @param {string} pointer JSON pointer to `node` + * @returns {void} + */ +function walkSchema(node, visit, pointer = "#") { + if (!node || typeof node !== "object") { + return; + } + + if (!Array.isArray(node)) { + visit(/** @type {SchemaNode} */ (node), pointer); + } + + for (const [key, value] of Object.entries(node)) { + walkSchema(value, visit, `${pointer}/${key}`); + } +} + +/** + * Reject schema constructs the generated validator would silently mistranslate. + * @param {SchemaNode} schema the options schema + * @returns {void} + */ +function assertSupportedSchema(schema) { + walkSchema(schema, (node, pointer) => { + // `unicode: false` below makes ajv measure string length in UTF-16 code + // units rather than code points. The two agree only at a bound of 1, where + // both mean "not empty". + if (node.minLength !== undefined && node.minLength !== 1) { + throw new Error( + `"minLength" must be 1, but is ${node.minLength} at ${pointer}.`, + ); + } + + if (node.maxLength !== undefined) { + throw new Error(`"maxLength" is not supported, found at ${pointer}.`); + } + + if ( + typeof node.instanceof === "string" && + !Object.hasOwn(CONSTRUCTORS, node.instanceof) + ) { + throw new Error( + `"instanceof": ${JSON.stringify(node.instanceof)} at ${pointer} is not supported. Add it to CONSTRUCTORS.`, + ); + } + }); +} + +/** + * @param {SchemaNode} schema the options schema + * @returns {string} source of the standalone validator + */ +function generate(schema) { + assertSupportedSchema(schema); + + const ajv = new Ajv({ + /* eslint-disable no-console -- a generator reports to the terminal */ + logger: { + log: console.log, + // `unicode` is deprecated but still honoured, and `assertSupportedSchema` + // has already established that dropping it changes nothing here. + warn: (...args) => { + if (!String(args[0]).includes("option unicode")) { + console.warn(...args); + } + }, + error: console.error, + }, + /* eslint-enable no-console */ + strict: false, + // The validator only reports whether the options are valid; `schema-utils` + // produces the messages, so collecting every error here would be wasted work. + allErrors: false, + verbose: false, + unicode: false, + code: { source: true, esm: true }, + }); + + ajv.addKeyword({ + keyword: "instanceof", + schemaType: "string", + /** + * @param {import("ajv").KeywordCxt} cxt keyword context + * @returns {void} + */ + code(cxt) { + cxt.fail( + _`!(${cxt.data} instanceof ${CONSTRUCTORS[/** @type {keyof typeof CONSTRUCTORS} */ (cxt.schema)]})`, + ); + }, + }); + + // Documentation-only keywords carried by the schema for the CLI and the docs. + for (const keyword of ["cli", "link"]) { + ajv.addKeyword({ keyword, schemaType: ["string", "object", "boolean"] }); + } + + return BANNER + standaloneCode.default(ajv, ajv.compile(schema)); +} + +const source = generate(JSON.parse(await readFile(SCHEMA_PATH, "utf8"))); + +if (process.argv.includes("--check")) { + const current = await readFile(OUTPUT_PATH, "utf8").catch(() => undefined); + + if (current !== source) { + // eslint-disable-next-line no-console + console.error( + `${path.relative(ROOT, OUTPUT_PATH)} is out of date — run \`npm run fix:schema-check\`.`, + ); + process.exitCode = 1; + } +} else { + await writeFile(OUTPUT_PATH, source); +} diff --git a/test/schema-check.test.js b/test/schema-check.test.js new file mode 100644 index 0000000000..da3135ff7d --- /dev/null +++ b/test/schema-check.test.js @@ -0,0 +1,59 @@ +import { describe, it } from "node:test"; +import { expect } from "expect"; +import validateOptions from "../lib/options.check.js"; + +// `validate-options.test.js` drives the whole option corpus through +// `new Server()`, which catches a validator that wrongly *accepts* invalid +// options. It cannot see one that wrongly *rejects* valid options: `Server` +// falls back to `schema-utils`, which accepts them, so every test still +// passes while the startup cost this validator exists to avoid comes back. +// These assertions are what notices that. +describe("precompiled options validator", () => { + const valid = [ + ["empty options", {}], + ["port as a number", { port: 8080 }], + ["port as a string", { port: "8080" }], + ['port as "auto"', { port: "auto" }], + ["host", { host: "localhost" }], + ["hot", { hot: true }], + ['hot as "only"', { hot: "only" }], + ["static as a string", { static: "/tmp" }], + ["static as an array", { static: [{ directory: "/tmp" }, "/other"] }], + ["overlay as an object", { client: { overlay: { errors: true } } }], + ["a function option", { setupMiddlewares: (middlewares) => middlewares }], + [ + "a Buffer option", + { server: { type: "https", options: { key: Buffer.from("k") } } }, + ], + ["headers as an array", { headers: [{ key: "a", value: "b" }] }], + ["proxy", { proxy: [{ context: ["/api"], target: "http://example.com" }] }], + [ + "watchFiles", + { watchFiles: { paths: "src/**", options: { usePolling: true } } }, + ], + ["allowedHosts as an array", { allowedHosts: ["example.com"] }], + ]; + + for (const [name, options] of valid) { + it(`should accept ${name} without falling back`, () => { + expect(validateOptions(options)).toBe(true); + }); + } + + const invalid = [ + ["an unknown top-level property", { unknownOption: true }], + ["an unknown nested property", { client: { unknownOption: true } }], + ["a wrongly typed option", { port: {} }], + ["a value outside an enum", { client: { logging: "whoops!" } }], + ["an empty string where one is required", { host: "" }], + ["an empty string in an array", { allowedHosts: [""] }], + ["a non-function where a function is required", { setupMiddlewares: "x" }], + ["null", { port: null }], + ]; + + for (const [name, options] of invalid) { + it(`should reject ${name}`, () => { + expect(validateOptions(options)).toBe(false); + }); + } +}); diff --git a/types/lib/options.check.d.ts b/types/lib/options.check.d.ts new file mode 100644 index 0000000000..8e576f971f --- /dev/null +++ b/types/lib/options.check.d.ts @@ -0,0 +1,25 @@ +export function validate( + data: any, + { + instancePath, + parentData, + parentDataProperty, + rootData, + }?: { + instancePath?: string | undefined; + rootData?: any; + }, +): boolean; +export default validate10; +declare function validate10( + data: any, + { + instancePath, + parentData, + parentDataProperty, + rootData, + }?: { + instancePath?: string | undefined; + rootData?: any; + }, +): boolean; From ceea14fab2618f4e571401ef3b163ea248abdd4f Mon Sep 17 00:00:00 2001 From: alexander-akait <4567934+alexander-akait@users.noreply.github.com> Date: Sun, 20 Sep 2026 13:00:06 +0000 Subject: [PATCH 2/8] chore: document the generator's callbacks --- scripts/generate-schema-check.mjs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/scripts/generate-schema-check.mjs b/scripts/generate-schema-check.mjs index 782036ff7e..9c91167726 100644 --- a/scripts/generate-schema-check.mjs +++ b/scripts/generate-schema-check.mjs @@ -65,7 +65,12 @@ function walkSchema(node, visit, pointer = "#") { * @returns {void} */ function assertSupportedSchema(schema) { - walkSchema(schema, (node, pointer) => { + /** + * @param {SchemaNode} node the schema node to check + * @param {string} pointer JSON pointer to `node` + * @returns {void} + */ + const assertNode = (node, pointer) => { // `unicode: false` below makes ajv measure string length in UTF-16 code // units rather than code points. The two agree only at a bound of 1, where // both mean "not empty". @@ -87,7 +92,9 @@ function assertSupportedSchema(schema) { `"instanceof": ${JSON.stringify(node.instanceof)} at ${pointer} is not supported. Add it to CONSTRUCTORS.`, ); } - }); + }; + + walkSchema(schema, assertNode); } /** @@ -101,8 +108,12 @@ function generate(schema) { /* eslint-disable no-console -- a generator reports to the terminal */ logger: { log: console.log, - // `unicode` is deprecated but still honoured, and `assertSupportedSchema` - // has already established that dropping it changes nothing here. + /** + * `unicode` is deprecated but still honoured, and `assertSupportedSchema` + * has already established that dropping it changes nothing here. + * @param {...unknown} args ajv's warning arguments + * @returns {void} + */ warn: (...args) => { if (!String(args[0]).includes("option unicode")) { console.warn(...args); From 547aeda005b5051697df55d87f846372c67c0ade Mon Sep 17 00:00:00 2001 From: alexander-akait <4567934+alexander-akait@users.noreply.github.com> Date: Sun, 20 Sep 2026 13:18:31 +0000 Subject: [PATCH 3/8] refactor: validate options through the compiler where webpack allows it `compiler.hooks.validate` and `compiler.validate`'s lazy-schema and precompiled-check parameters landed together in webpack 5.106, so the hook doubles as the feature probe for them. When it is there, validation goes through the compiler the way webpack's own plugins do, which also lets `validate: false` turn it off. The direct path stays as the fallback, with a TODO to drop it once the minimum supported webpack carries the hook. It is also what runs when no compiler was passed, which is how the server is constructed for `apply()`. --- lib/Server.js | 53 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/lib/Server.js b/lib/Server.js index 72976d8851..736490f268 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -5,7 +5,7 @@ import url from "node:url"; import fs from "graceful-fs"; import ipaddr from "ipaddr.js"; import { validate } from "schema-utils"; -import validateOptions from "./options.check.js"; +import schemaCheck from "./options.check.js"; import schema from "./options.json" with { type: "json" }; // Named `cjsRequire` (not `require`) so it doesn't shadow the implicit CommonJS @@ -332,6 +332,47 @@ const DEFAULT_ALLOWED_PROTOCOLS = /^(file|.+-extension):/i; const pluginName = "webpack-dev-server"; +/** + * Throw unless the options match the schema. + * + * `compiler.hooks.validate` and `compiler.validate`'s lazy-schema and + * precompiled-check parameters landed together in webpack 5.106, so the hook + * doubles as the feature probe for them. Either way the schema is only read, + * and ajv only compiled, once something is actually wrong — the precompiled + * validator answers the common case in ~1ms, against the ~120ms `schema-utils` + * spends compiling the schema on its first call. + * @param {Compiler | MultiCompiler | undefined} compiler compiler, undefined when the server is used as a plugin via `apply()` + * @param {EXPECTED_ANY} options options + * @returns {void} + */ +function validateOptions(compiler, options) { + const firstCompiler = + compiler && "compilers" in compiler ? compiler.compilers[0] : compiler; + + if (firstCompiler && firstCompiler.hooks.validate) { + /** @type {EXPECTED_ANY} */ + (firstCompiler).validate( + () => schema, + options, + { name: "Dev Server", baseDataPath: "options" }, + schemaCheck, + ); + + return; + } + + // TODO remove this fallback in the next major, once the minimum supported + // webpack has `compiler.hooks.validate`, and validate through the compiler + // alone. It is also the path taken when no compiler was passed, which is how + // the server is constructed for `apply()`. + if (!schemaCheck(options)) { + validate(/** @type {Schema} */ (schema), options, { + name: "Dev Server", + baseDataPath: "options", + }); + } +} + /** * @template {BasicApplication} [A=ExpressApplication] * @template {BasicServer} [S=HTTPServer] @@ -344,15 +385,7 @@ class Server { constructor(options, compiler) { options = options === undefined ? {} : options; - // The precompiled validator answers the common case without ajv, which - // `schema-utils` would otherwise spend ~120ms compiling on every startup. - // Rejected options go through `schema-utils` for the readable message. - if (!validateOptions(options)) { - validate(/** @type {Schema} */ (schema), options, { - name: "Dev Server", - baseDataPath: "options", - }); - } + validateOptions(compiler, options); if (compiler) { this.compiler = compiler; From bbd85efd68121eed3763aed8f87f11fdfa3fedf6 Mon Sep 17 00:00:00 2001 From: alexander-akait <4567934+alexander-akait@users.noreply.github.com> Date: Sun, 20 Sep 2026 13:43:26 +0000 Subject: [PATCH 4/8] fix: validate against the compiler that owns the dev server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `compiler.validate` honours that compiler's own `validate` option, so asking the first child of a `MultiCompiler` read a policy that was never about these options: with `validate: false` on child 0 and the dev server configured on child 1, invalid options reached normalization instead of throwing. `getCompilerOptions` already had the selection rule — the child naming `devServer`, else the one targeting the web, else the first. It moves to `findDevServerCompiler`, which both callers now use so the two cannot drift apart again. Reported by CodeRabbit on #5746. --- lib/Server.js | 151 +++++++++++++++++++------------------- test/schema-check.test.js | 53 +++++++++++++ 2 files changed, 128 insertions(+), 76 deletions(-) diff --git a/lib/Server.js b/lib/Server.js index 736490f268..8891654d7c 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -332,47 +332,6 @@ const DEFAULT_ALLOWED_PROTOCOLS = /^(file|.+-extension):/i; const pluginName = "webpack-dev-server"; -/** - * Throw unless the options match the schema. - * - * `compiler.hooks.validate` and `compiler.validate`'s lazy-schema and - * precompiled-check parameters landed together in webpack 5.106, so the hook - * doubles as the feature probe for them. Either way the schema is only read, - * and ajv only compiled, once something is actually wrong — the precompiled - * validator answers the common case in ~1ms, against the ~120ms `schema-utils` - * spends compiling the schema on its first call. - * @param {Compiler | MultiCompiler | undefined} compiler compiler, undefined when the server is used as a plugin via `apply()` - * @param {EXPECTED_ANY} options options - * @returns {void} - */ -function validateOptions(compiler, options) { - const firstCompiler = - compiler && "compilers" in compiler ? compiler.compilers[0] : compiler; - - if (firstCompiler && firstCompiler.hooks.validate) { - /** @type {EXPECTED_ANY} */ - (firstCompiler).validate( - () => schema, - options, - { name: "Dev Server", baseDataPath: "options" }, - schemaCheck, - ); - - return; - } - - // TODO remove this fallback in the next major, once the minimum supported - // webpack has `compiler.hooks.validate`, and validate through the compiler - // alone. It is also the path taken when no compiler was passed, which is how - // the server is constructed for `apply()`. - if (!schemaCheck(options)) { - validate(/** @type {Schema} */ (schema), options, { - name: "Dev Server", - baseDataPath: "options", - }); - } -} - /** * @template {BasicApplication} [A=ExpressApplication] * @template {BasicServer} [S=HTTPServer] @@ -385,7 +344,7 @@ class Server { constructor(options, compiler) { options = options === undefined ? {} : options; - validateOptions(compiler, options); + Server.validateOptions(compiler, options); if (compiler) { this.compiler = compiler; @@ -617,6 +576,77 @@ class Server { return path.resolve(dir, `node_modules/.cache/${pluginName}`); } + /** + * The compiler whose configuration the dev server belongs to. For a + * `MultiCompiler` that is the child naming `devServer`, else the one + * targeting the web, else the first — anything else reads another child's + * settings. + * @private + * @param {Compiler | MultiCompiler} compiler compiler + * @returns {Compiler} the compiler that owns the dev server + */ + static findDevServerCompiler(compiler) { + const { compilers } = /** @type {MultiCompiler} */ (compiler); + + if (typeof compilers === "undefined") { + return /** @type {Compiler} */ (compiler); + } + + if (compilers.length === 1) { + return compilers[0]; + } + + return ( + compilers.find((child) => child.options.devServer) || + compilers.find((child) => Server.isWebTarget(child)) || + compilers[0] + ); + } + + /** + * Throw unless the options match the schema. + * + * `compiler.hooks.validate` and `compiler.validate`'s lazy-schema and + * precompiled-check parameters landed together in webpack 5.106, so the hook + * doubles as the feature probe for them. Validating through the compiler that + * owns the dev server matters: `compiler.validate` honours that compiler's + * `validate` option, so asking any other child would read a policy that was + * never about these options. Either way the schema is only read, and ajv only + * compiled, once something is actually wrong — the precompiled validator + * answers the common case in ~1ms, against the ~120ms `schema-utils` spends + * compiling the schema on its first call. + * @private + * @param {Compiler | MultiCompiler | undefined} compiler compiler, undefined when the server is used as a plugin via `apply()` + * @param {EXPECTED_ANY} options options + * @returns {void} + */ + static validateOptions(compiler, options) { + const owner = compiler ? Server.findDevServerCompiler(compiler) : undefined; + + if (owner && owner.hooks.validate) { + /** @type {EXPECTED_ANY} */ + (owner).validate( + () => schema, + options, + { name: "Dev Server", baseDataPath: "options" }, + schemaCheck, + ); + + return; + } + + // TODO remove this fallback in the next major, once the minimum supported + // webpack has `compiler.hooks.validate`, and validate through the compiler + // alone. It is also the path taken when no compiler was passed, which is + // how the server is constructed for `apply()`. + if (!schemaCheck(options)) { + validate(/** @type {Schema} */ (schema), options, { + name: "Dev Server", + baseDataPath: "options", + }); + } + } + /** * @private * @param {Compiler} compiler compiler @@ -831,40 +861,9 @@ class Server { * @returns {Compiler["options"]} compiler options */ getCompilerOptions() { - if ( - typeof (/** @type {MultiCompiler} */ (this.compiler).compilers) !== - "undefined" - ) { - if (/** @type {MultiCompiler} */ (this.compiler).compilers.length === 1) { - return ( - /** @type {MultiCompiler} */ - (this.compiler).compilers[0].options - ); - } - - // Configuration with the `devServer` options - const compilerWithDevServer = - /** @type {MultiCompiler} */ - (this.compiler).compilers.find((config) => config.options.devServer); - - if (compilerWithDevServer) { - return compilerWithDevServer.options; - } - - // Configuration with `web` preset - const compilerWithWebPreset = - /** @type {MultiCompiler} */ - (this.compiler).compilers.find((config) => Server.isWebTarget(config)); - - if (compilerWithWebPreset) { - return compilerWithWebPreset.options; - } - - // Fallback - return /** @type {MultiCompiler} */ (this.compiler).compilers[0].options; - } - - return /** @type {Compiler} */ (this.compiler).options; + return Server.findDevServerCompiler( + /** @type {Compiler | MultiCompiler} */ (this.compiler), + ).options; } /** diff --git a/test/schema-check.test.js b/test/schema-check.test.js index da3135ff7d..40a9910a62 100644 --- a/test/schema-check.test.js +++ b/test/schema-check.test.js @@ -1,7 +1,13 @@ +import path from "node:path"; import { describe, it } from "node:test"; +import { fileURLToPath } from "node:url"; import { expect } from "expect"; +import webpack from "webpack"; +import Server from "../lib/Server.js"; import validateOptions from "../lib/options.check.js"; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + // `validate-options.test.js` drives the whole option corpus through // `new Server()`, which catches a validator that wrongly *accepts* invalid // options. It cannot see one that wrongly *rejects* valid options: `Server` @@ -57,3 +63,50 @@ describe("precompiled options validator", () => { }); } }); + +// `Server` validates through the compiler that owns the dev server, and +// `compiler.validate` honours that compiler's own `validate` option. Asking any +// other child of a `MultiCompiler` would read a policy that was never about +// these options, and a child opting out would silently take dev server +// validation with it. +describe("options validation routing", () => { + const config = (extra) => ({ + mode: "development", + context: __dirname, + entry: "./fixtures/simple-config/foo.js", + infrastructureLogging: { level: "none" }, + stats: "none", + ...extra, + }); + + it("should validate against the child owning the dev server, not the first", () => { + const compiler = webpack([ + config({ name: "a", validate: false }), + config({ name: "b", devServer: { port: 9001 } }), + ]); + + expect(() => new Server({ unknownOption: true }, compiler)).toThrow( + /Dev Server/, + ); + }); + + it("should fall back to the child targeting the web", () => { + const compiler = webpack([ + config({ name: "a", target: "node", validate: false }), + config({ name: "b", target: "web" }), + ]); + + expect(() => new Server({ unknownOption: true }, compiler)).toThrow( + /Dev Server/, + ); + }); + + it("should pick the same child for the compiler options it reads", () => { + const compiler = webpack([ + config({ name: "a", validate: false }), + config({ name: "b", devServer: { port: 9001 } }), + ]); + + expect(new Server({}, compiler).getCompilerOptions().name).toBe("b"); + }); +}); From 6357fea0b0b26ee5564670e9c34020a4143fef08 Mon Sep 17 00:00:00 2001 From: alexander-akait <4567934+alexander-akait@users.noreply.github.com> Date: Sun, 20 Sep 2026 13:57:25 +0000 Subject: [PATCH 5/8] chore: regenerate types for the validation helpers --- types/lib/Server.d.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/types/lib/Server.d.ts b/types/lib/Server.d.ts index 912e2f0fd5..6f1d6b6632 100644 --- a/types/lib/Server.d.ts +++ b/types/lib/Server.d.ts @@ -1486,6 +1486,34 @@ declare class Server< * @returns {string} path to cache dir */ static findCacheDir(): string; + /** + * The compiler whose configuration the dev server belongs to. For a + * `MultiCompiler` that is the child naming `devServer`, else the one + * targeting the web, else the first — anything else reads another child's + * settings. + * @private + * @param {Compiler | MultiCompiler} compiler compiler + * @returns {Compiler} the compiler that owns the dev server + */ + private static findDevServerCompiler; + /** + * Throw unless the options match the schema. + * + * `compiler.hooks.validate` and `compiler.validate`'s lazy-schema and + * precompiled-check parameters landed together in webpack 5.106, so the hook + * doubles as the feature probe for them. Validating through the compiler that + * owns the dev server matters: `compiler.validate` honours that compiler's + * `validate` option, so asking any other child would read a policy that was + * never about these options. Either way the schema is only read, and ajv only + * compiled, once something is actually wrong — the precompiled validator + * answers the common case in ~1ms, against the ~120ms `schema-utils` spends + * compiling the schema on its first call. + * @private + * @param {Compiler | MultiCompiler | undefined} compiler compiler, undefined when the server is used as a plugin via `apply()` + * @param {EXPECTED_ANY} options options + * @returns {void} + */ + private static validateOptions; /** * @private * @param {Compiler} compiler compiler From bf621777644353076c68f9e459368f85d520c206 Mon Sep 17 00:00:00 2001 From: alexander-akait <4567934+alexander-akait@users.noreply.github.com> Date: Sun, 20 Sep 2026 16:06:04 +0000 Subject: [PATCH 6/8] test: cover the validation fallbacks codecov reported 6 uncovered lines in the diff, all of them fallbacks: `findDevServerCompiler` returning the first child when none names `devServer` or targets the web, and the direct `schema-utils` call taken when there is no compiler to ask, which is how the server is constructed for `apply()`. lib/Server.js's changed lines go from 70/76 to 76/76 covered. --- test/schema-check.test.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/schema-check.test.js b/test/schema-check.test.js index 40a9910a62..140e2c266d 100644 --- a/test/schema-check.test.js +++ b/test/schema-check.test.js @@ -101,6 +101,23 @@ describe("options validation routing", () => { ); }); + it("should fall back to the first child when none matches", () => { + // Neither names `devServer` nor targets the web, so there is nothing to + // prefer and the first child stands in. + const compiler = webpack([ + config({ name: "a", target: "node" }), + config({ name: "b", target: "node" }), + ]); + + expect(new Server({}, compiler).getCompilerOptions().name).toBe("a"); + }); + + it("should validate directly when constructed without a compiler", () => { + // How the server is built for `apply()`: no compiler, so no `validate` + // policy to honour, and `schema-utils` produces the message. + expect(() => new Server({ unknownOption: true })).toThrow(/Dev Server/); + }); + it("should pick the same child for the compiler options it reads", () => { const compiler = webpack([ config({ name: "a", validate: false }), From dcb5ed9a69725ef3d18eb5cca1e69f1f7b3bf6f2 Mon Sep 17 00:00:00 2001 From: alexander-akait <4567934+alexander-akait@users.noreply.github.com> Date: Sun, 20 Sep 2026 16:32:59 +0000 Subject: [PATCH 7/8] test: close the compilers the routing tests create Each `webpack()` call leaves a compiler holding a file-system cache and its purge timer. Node's test runner runs files concurrently, so those stayed alive alongside the browser suites for the rest of the run. Windows Node 22 timed out in `test/e2e/overlay.test.js` on the previous commit, which was test-only; that suite already uses most of its 400s there, and this is the load these tests were adding to it. --- test/schema-check.test.js | 60 ++++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/test/schema-check.test.js b/test/schema-check.test.js index 140e2c266d..6786730586 100644 --- a/test/schema-check.test.js +++ b/test/schema-check.test.js @@ -79,35 +79,54 @@ describe("options validation routing", () => { ...extra, }); - it("should validate against the child owning the dev server, not the first", () => { - const compiler = webpack([ - config({ name: "a", validate: false }), - config({ name: "b", devServer: { port: 9001 } }), - ]); + // Each compiler holds a file-system cache and its purge timer, and this suite + // runs alongside the slow browser ones, so they are closed rather than left + // to the end of the process. + const multiCompiler = (t, ...configs) => { + const compiler = webpack(configs.map((extra) => config(extra))); + + t.after( + () => + new Promise((resolve) => { + compiler.close(resolve); + }), + ); + + return compiler; + }; + + it("should validate against the child owning the dev server, not the first", (t) => { + const compiler = multiCompiler( + t, + { name: "a", validate: false }, + { name: "b", devServer: { port: 9001 } }, + ); expect(() => new Server({ unknownOption: true }, compiler)).toThrow( /Dev Server/, ); }); - it("should fall back to the child targeting the web", () => { - const compiler = webpack([ - config({ name: "a", target: "node", validate: false }), - config({ name: "b", target: "web" }), - ]); + it("should fall back to the child targeting the web", (t) => { + const compiler = multiCompiler( + t, + { name: "a", target: "node", validate: false }, + { name: "b", target: "web" }, + ); expect(() => new Server({ unknownOption: true }, compiler)).toThrow( /Dev Server/, ); }); - it("should fall back to the first child when none matches", () => { + it("should fall back to the first child when none matches", (t) => { // Neither names `devServer` nor targets the web, so there is nothing to // prefer and the first child stands in. - const compiler = webpack([ - config({ name: "a", target: "node" }), - config({ name: "b", target: "node" }), - ]); + const compiler = multiCompiler( + t, + { name: "a", target: "node" }, + { name: "b", target: "node" }, + ); expect(new Server({}, compiler).getCompilerOptions().name).toBe("a"); }); @@ -118,11 +137,12 @@ describe("options validation routing", () => { expect(() => new Server({ unknownOption: true })).toThrow(/Dev Server/); }); - it("should pick the same child for the compiler options it reads", () => { - const compiler = webpack([ - config({ name: "a", validate: false }), - config({ name: "b", devServer: { port: 9001 } }), - ]); + it("should pick the same child for the compiler options it reads", (t) => { + const compiler = multiCompiler( + t, + { name: "a", validate: false }, + { name: "b", devServer: { port: 9001 } }, + ); expect(new Server({}, compiler).getCompilerOptions().name).toBe("b"); }); From 2bc55dd6f2844df3405b173e81c2bf38268b9bf6 Mon Sep 17 00:00:00 2001 From: alexander-akait <4567934+alexander-akait@users.noreply.github.com> Date: Sun, 20 Sep 2026 18:40:45 +0000 Subject: [PATCH 8/8] fix: honour the owning compiler's validate option in plugin mode Used as a plugin the server is constructed without a compiler, so validating in the constructor had no `validate` policy to read and threw before `apply()` could bring the compiler the options are about. Validate in the constructor only when one is passed, and in `apply()` otherwise. --- lib/Server.js | 18 +++++++++++------- test/schema-check.test.js | 18 ++++++++++++++---- types/lib/Server.d.ts | 2 +- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/lib/Server.js b/lib/Server.js index 8891654d7c..382de1914a 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -344,9 +344,9 @@ class Server { constructor(options, compiler) { options = options === undefined ? {} : options; - Server.validateOptions(compiler, options); - if (compiler) { + Server.validateOptions(compiler, options); + this.compiler = compiler; /** @@ -616,14 +616,14 @@ class Server { * answers the common case in ~1ms, against the ~120ms `schema-utils` spends * compiling the schema on its first call. * @private - * @param {Compiler | MultiCompiler | undefined} compiler compiler, undefined when the server is used as a plugin via `apply()` + * @param {Compiler | MultiCompiler} compiler compiler that owns the dev server * @param {EXPECTED_ANY} options options * @returns {void} */ static validateOptions(compiler, options) { - const owner = compiler ? Server.findDevServerCompiler(compiler) : undefined; + const owner = Server.findDevServerCompiler(compiler); - if (owner && owner.hooks.validate) { + if (owner.hooks.validate) { /** @type {EXPECTED_ANY} */ (owner).validate( () => schema, @@ -637,8 +637,7 @@ class Server { // TODO remove this fallback in the next major, once the minimum supported // webpack has `compiler.hooks.validate`, and validate through the compiler - // alone. It is also the path taken when no compiler was passed, which is - // how the server is constructed for `apply()`. + // alone. if (!schemaCheck(options)) { validate(/** @type {Schema} */ (schema), options, { name: "Dev Server", @@ -3799,6 +3798,11 @@ class Server { * @returns {void} */ apply(compiler) { + // As a plugin the server is constructed without a compiler, so validation + // waits for the one that will own it — otherwise that compiler's `validate` + // option would have no say over the options it is about. + Server.validateOptions(compiler, this.options); + this.compiler = compiler; this.isPlugin = true; this.logger = this.compiler.getInfrastructureLogger(pluginName); diff --git a/test/schema-check.test.js b/test/schema-check.test.js index 6786730586..ad583789c6 100644 --- a/test/schema-check.test.js +++ b/test/schema-check.test.js @@ -131,10 +131,20 @@ describe("options validation routing", () => { expect(new Server({}, compiler).getCompilerOptions().name).toBe("a"); }); - it("should validate directly when constructed without a compiler", () => { - // How the server is built for `apply()`: no compiler, so no `validate` - // policy to honour, and `schema-utils` produces the message. - expect(() => new Server({ unknownOption: true })).toThrow(/Dev Server/); + it("should validate on apply() when constructed as a plugin", (t) => { + // Used as a plugin the server is constructed without a compiler, so there + // is nothing to validate against until `apply()` brings one. + const compiler = multiCompiler(t, { name: "a" }); + const server = new Server({ unknownOption: true }); + + expect(() => server.apply(compiler)).toThrow(/Dev Server/); + }); + + it("should honour the owning compiler's validate option on apply()", (t) => { + const compiler = multiCompiler(t, { name: "a", validate: false }); + const server = new Server({ unknownOption: true }); + + expect(() => server.apply(compiler)).not.toThrow(); }); it("should pick the same child for the compiler options it reads", (t) => { diff --git a/types/lib/Server.d.ts b/types/lib/Server.d.ts index 6f1d6b6632..16c4696521 100644 --- a/types/lib/Server.d.ts +++ b/types/lib/Server.d.ts @@ -1509,7 +1509,7 @@ declare class Server< * answers the common case in ~1ms, against the ~120ms `schema-utils` spends * compiling the schema on its first call. * @private - * @param {Compiler | MultiCompiler | undefined} compiler compiler, undefined when the server is used as a plugin via `apply()` + * @param {Compiler | MultiCompiler} compiler compiler that owns the dev server * @param {EXPECTED_ANY} options options * @returns {void} */