From ed2d97b11f55e5ff88956ed7cc1454ff55da93d5 Mon Sep 17 00:00:00 2001 From: Andrew Powell Date: Wed, 27 Sep 2017 14:20:35 -0400 Subject: [PATCH] following #106, makes options.publicPath required via the API. alphabetizes optionsSchema (#1055) * following #106, makes options.publicPath required via the API. alphabetizes optionsSchema * fixing options validation tests * reorg options schema * refactoring options validation test to prevent needless duplication --- lib/OptionsValidationError.js | 2 +- lib/Server.js | 2 +- .../options.json} | 427 ++++++++++-------- test/Validation.test.js | 62 ++- test/helper.js | 3 + 5 files changed, 282 insertions(+), 214 deletions(-) rename lib/{optionsSchema.json => schemas/options.json} (88%) diff --git a/lib/OptionsValidationError.js b/lib/OptionsValidationError.js index f23dbf1363..4cc5b381e7 100644 --- a/lib/OptionsValidationError.js +++ b/lib/OptionsValidationError.js @@ -2,7 +2,7 @@ /* eslint no-param-reassign: 'off' */ -const optionsSchema = require('./optionsSchema.json'); +const optionsSchema = require('./schemas/options.json'); const indent = (str, prefix, firstLine) => { if (firstLine) { diff --git a/lib/Server.js b/lib/Server.js index 870bf80b21..fa2368488d 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -21,7 +21,7 @@ const spdy = require('spdy'); const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); const OptionsValidationError = require('./OptionsValidationError'); -const optionsSchema = require('./optionsSchema.json'); +const optionsSchema = require('./schemas/options.json'); const clientStats = { errorDetails: false }; const log = console.log; // eslint-disable-line no-console diff --git a/lib/optionsSchema.json b/lib/schemas/options.json similarity index 88% rename from lib/optionsSchema.json rename to lib/schemas/options.json index 1c8e8765e6..34e6e65b97 100644 --- a/lib/optionsSchema.json +++ b/lib/schemas/options.json @@ -1,26 +1,14 @@ { "additionalProperties": false, + "required": [ "publicPath" ], + "type": "object", + "properties": { - "hot": { - "description": "Enables Hot Module Replacement.", - "type": "boolean" - }, - "hotOnly": { - "description": "Enables Hot Module Replacement without page refresh as fallback.", - "type": "boolean" - }, - "lazy": { - "description": "Disables watch mode and recompiles bundle only on a request.", - "type": "boolean" - }, - "bonjour": { - "description": "Publishes the ZeroConf DNS service", - "type": "boolean" - }, - "host": { - "description": "The host the server listens to.", - "type": "string" + "after": { + "description": "Exposes the Express server to add custom middleware or routes after webpack-dev-middleware got added.", + "instanceof": "Function" }, + "allowedHosts": { "description": "Specifies which hosts are allowed to access the dev server.", "items": { @@ -28,114 +16,163 @@ }, "type": "array" }, - "filename": { - "description": "The filename that needs to be requested in order to trigger a recompile (only in lazy mode).", + + "before": { + "description": "Exposes the Express server to add custom middleware or routes before webpack-dev-middleware will be added.", + "instanceof": "Function" + }, + + "bonjour": { + "description": "Publishes the ZeroConf DNS service", + "type": "boolean" + }, + + "ca": { "anyOf": [ { - "instanceof": "RegExp" + "type": "string" }, { - "type": "string" + "instanceof": "Buffer" } - ] + ], + "description": "The contents of a SSL CA certificate." }, - "publicPath": { - "description": "URL path where the webpack files are served from.", - "type": "string" - }, - "port": { - "description": "The port the server listens to.", + + "cert": { "anyOf": [ { - "type": "number" + "type": "string" }, { - "type": "string" + "instanceof": "Buffer" } - ] - }, - "socket": { - "description": "The Unix socket to listen to (instead of on a host).", - "type": "string" - }, - "watchOptions": { - "description": "Options for changing the watch behavior.", - "type": "object" - }, - "headers": { - "description": "Response headers that are added to each response.", - "additionalProperties": { - "type": "string" - }, - "type": "object" + ], + "description": "The contents of a SSL certificate." }, + "clientLogLevel": { "description": "Controls the log messages shown in the browser.", "enum": [ - "none", + "error", "info", - "warning", - "error" + "none", + "warning" ] }, - "overlay": { - "description": "Shows an error overlay in browser.", + + "compress": { + "description": "Gzip compression for all requests.", + "type": "boolean" + }, + + "contentBase": { "anyOf": [ { - "type": "boolean" + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" }, { - "type": "object", - "properties": { - "errors": { - "type": "boolean" - }, - "warnings": { - "type": "boolean" - } - } + "enum": [ + false + ] + }, + { + "type": "number" + }, + { + "type": "string" } - ] + ], + "description": "A directory to serve files non-webpack files from." }, - "progress": { - "description": "Shows compilation progress in browser console.", + + "disableHostCheck": { + "description": "Disable the Host header check (Security).", "type": "boolean" }, - "key": { - "description": "The contents of a SSL key.", + + "features": { + "description": "The order of which the features will be triggered.", + "items": { + "type": "string" + }, + "type": "array" + }, + + "filename": { "anyOf": [ { - "type": "string" + "instanceof": "RegExp" }, { - "instanceof": "Buffer" + "type": "string" } - ] + ], + "description": "The filename that needs to be requested in order to trigger a recompile (only in lazy mode)." }, - "cert": { - "description": "The contents of a SSL certificate.", + + "headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Response headers that are added to each response.", + "type": "object" + }, + + "historyApiFallback": { "anyOf": [ { - "type": "string" + "type": "boolean" }, { - "instanceof": "Buffer" + "type": "object" } - ] + ], + "description": "404 fallback to a specified file." }, - "ca": { - "description": "The contents of a SSL CA certificate.", + + "host": { + "description": "The host the server listens to.", + "type": "string" + }, + + "hot": { + "description": "Enables Hot Module Replacement.", + "type": "boolean" + }, + + "hotOnly": { + "description": "Enables Hot Module Replacement without page refresh as fallback.", + "type": "boolean" + }, + + "https": { "anyOf": [ { - "type": "string" + "type": "object" }, { - "instanceof": "Buffer" + "type": "boolean" } - ] + ], + "description": "Enable HTTPS for server." }, - "pfx": { - "description": "The contents of a SSL pfx file.", + + "index": { + "description": "The filename that is considered the index file.", + "type": "string" + }, + + "inline": { + "description": "Enable inline mode to include client scripts in bundle (CLI-only).", + "type": "boolean" + }, + + "key": { "anyOf": [ { "type": "string" @@ -143,98 +180,97 @@ { "instanceof": "Buffer" } - ] + ], + "description": "The contents of a SSL key." }, - "pfxPassphrase": { - "description": "The passphrase to a (SSL) PFX file.", - "type": "string" - }, - "requestCert": { - "description": "Enables request for client certificate. This is passed directly to the https server.", + + "lazy": { + "description": "Disables watch mode and recompiles bundle only on a request.", "type": "boolean" }, - "inline": { - "description": "Enable inline mode to include client scripts in bundle (CLI-only).", - "type": "boolean" + + "log": { + "description": "Customize info logs for webpack-dev-middleware.", + "instanceof": "Function" }, - "disableHostCheck": { - "description": "Disable the Host header check (Security).", + + "noInfo": { + "description": "Hide all info messages on console.", "type": "boolean" }, - "public": { - "description": "The public hostname/ip address of the server.", - "type": "string" - }, - "https": { - "description": "Enable HTTPS for server.", + + "open": { "anyOf": [ { - "type": "object" + "type": "string" }, { "type": "boolean" } - ] + ], + "description": "Let the CLI open your browser." }, - "contentBase": { - "description": "A directory to serve files non-webpack files from.", + + "openPage": { + "description": "Let the CLI open your browser to a specific page on the site.", + "type": "string" + }, + + "overlay": { "anyOf": [ { - "items": { - "type": "string" - }, - "minItems": 1, - "type": "array" - }, - { - "enum": [ - false - ] - }, - { - "type": "number" + "type": "boolean" }, { - "type": "string" + "properties": { + "errors": { + "type": "boolean" + }, + "warnings": { + "type": "boolean" + } + }, + "type": "object" } - ] + ], + "description": "Shows an error overlay in browser." }, - "watchContentBase": { - "description": "Watches the contentBase directory for changes.", - "type": "boolean" - }, - "open": { - "description": "Let the CLI open your browser with the URL.", + + "pfx": { "anyOf": [ { "type": "string" }, { - "type": "boolean" + "instanceof": "Buffer" } - ] - }, - "useLocalIp": { - "description": "Let the browser open with your local IP.", - "type": "boolean" + ], + "description": "The contents of a SSL pfx file." }, - "openPage": { - "description": "Let the CLI open your browser to a specific page on the site.", + + "pfxPassphrase": { + "description": "The passphrase to a (SSL) PFX file.", "type": "string" }, - "features": { - "description": "The order of which the features will be triggered.", - "items": { - "type": "string" - }, - "type": "array" + + "port": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "description": "The port the server listens to." }, - "compress": { - "description": "Gzip compression for all requests.", + + "progress": { + "description": "Shows compilation progress in browser console.", "type": "boolean" }, + "proxy": { - "description": "Proxy requests to another server.", "anyOf": [ { "items": { @@ -253,37 +289,56 @@ { "type": "object" } - ] + ], + "description": "Proxy requests to another server." }, - "historyApiFallback": { - "description": "404 fallback to a specified file.", - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "object" - } - ] + + "public": { + "description": "The public hostname/ip address of the server.", + "type": "string" }, - "staticOptions": { - "description": "Options for static files served with contentBase.", - "type": "object" + + "publicPath": { + "description": "URL path where the webpack files are served from.", + "type": "string" }, + + "quiet": { + "description": "Hide all messages on console.", + "type": "boolean" + }, + + "requestCert": { + "description": "Enables request for client certificate. This is passed directly to the https server.", + "type": "boolean" + }, + + "reporter": { + "description": "Customize what the console displays when compiling.", + "instanceof": "Function" + }, + + "serverSideRender": { + "description": "Expose stats for server side rendering (experimental).", + "type": "boolean" + }, + "setup": { "description": "Exposes the Express server to add custom middleware or routes.", "instanceof": "Function" }, - "before": { - "description": "Exposes the Express server to add custom middleware or routes before webpack-dev-middleware will be added.", - "instanceof": "Function" + + "socket": { + "description": "The Unix socket to listen to (instead of on a host).", + "type": "string" }, - "after": { - "description": "Exposes the Express server to add custom middleware or routes after webpack-dev-middleware got added.", - "instanceof": "Function" + + "staticOptions": { + "description": "Options for static files served with contentBase.", + "type": "object" }, + "stats": { - "description": "Decides what bundle information is displayed.", "anyOf": [ { "type": "object" @@ -293,43 +348,35 @@ }, { "enum": [ - "none", "errors-only", "minimal", + "none", "normal", "verbose" ] } - ] - }, - "reporter": { - "description": "Customize what the console displays when compiling.", - "instanceof": "Function" - }, - "noInfo": { - "description": "Hide all info messages on console.", - "type": "boolean" - }, - "quiet": { - "description": "Hide all messages on console.", - "type": "boolean" + ], + "description": "Decides what bundle information is displayed." }, - "serverSideRender": { - "description": "Expose stats for server side rendering (experimental).", + + "useLocalIp": { + "description": "Let the browser open with your local IP.", "type": "boolean" }, - "index": { - "description": "The filename that is considered the index file.", - "type": "string" - }, - "log": { - "description": "Customize info logs for webpack-dev-middleware.", - "instanceof": "Function" - }, + "warn": { "description": "Customize warn logs for webpack-dev-middleware.", "instanceof": "Function" + }, + + "watchContentBase": { + "description": "Watches the contentBase directory for changes.", + "type": "boolean" + }, + + "watchOptions": { + "description": "Options for changing the watch behavior.", + "type": "object" } - }, - "type": "object" + } } diff --git a/test/Validation.test.js b/test/Validation.test.js index c4f6933c52..1ab5e87986 100644 --- a/test/Validation.test.js +++ b/test/Validation.test.js @@ -3,36 +3,51 @@ const webpack = require('webpack'); const OptionsValidationError = require('../lib/OptionsValidationError'); const Server = require('../lib/Server'); +const optionsSchema = require('../lib/schemas/options.json'); const config = require('./fixtures/simple-config/webpack.config'); +const optionNames = Object.keys(optionsSchema.properties).map((name) => { + if (optionsSchema.required.includes(name)) { + return name; + } + + return `${name}?`; +}); +const publicPath = '/'; + describe('Validation', () => { let compiler; + before(() => { compiler = webpack(config); }); + const testCases = [{ name: 'invalid `hot` configuration', - config: { hot: 'asdf' }, + config: { hot: 'asdf', publicPath }, message: [ ' - configuration.hot should be a boolean.' ] - }, { + }, + { name: 'invalid `public` configuration', - config: { public: 1 }, + config: { public: 1, publicPath }, message: [ ' - configuration.public should be a string.' ] - }, { + }, + { name: 'invalid `allowedHosts` configuration', - config: { allowedHosts: 1 }, + config: { allowedHosts: 1, publicPath }, message: [ ' - configuration.allowedHosts should be an array:', ' [string]', ' Specifies which hosts are allowed to access the dev server.' ] - }, { + }, + { name: 'invalid `contentBase` configuration', - config: { contentBase: [0] }, + config: { contentBase: [0], publicPath }, message: [ ' - configuration.contentBase should be one of these:', ' [string] | false | number | string', @@ -43,18 +58,17 @@ describe('Validation', () => { ' * configuration.contentBase should be a number.', ' * configuration.contentBase should be a string.' ] - }, { + }, + { name: 'non-existing key configuration', - config: { asdf: true }, + config: { asdf: true, publicPath }, message: [ - " - configuration has an unknown property 'asdf'. These properties are valid:", - ' object { hot?, hotOnly?, lazy?, bonjour?, host?, allowedHosts?, filename?, publicPath?, port?, socket?, ' + - 'watchOptions?, headers?, clientLogLevel?, overlay?, progress?, key?, cert?, ca?, pfx?, pfxPassphrase?, requestCert?, ' + - 'inline?, disableHostCheck?, public?, https?, contentBase?, watchContentBase?, open?, useLocalIp?, openPage?, features?, ' + - 'compress?, proxy?, historyApiFallback?, staticOptions?, setup?, before?, after?, stats?, reporter?, ' + - 'noInfo?, quiet?, serverSideRender?, index?, log?, warn? }' + // eslint-disable-next-line quotes + ` - configuration has an unknown property 'asdf'. These properties are valid:`, + ` object { ${optionNames.join(', ')} }` ] }]; + testCases.forEach((testCase) => { it(`should fail validation for ${testCase.name}`, () => { try { @@ -74,7 +88,8 @@ describe('Validation', () => { it('should always allow any host if options.disableHostCheck is set', () => { const options = { public: 'test.host:80', - disableHostCheck: true + disableHostCheck: true, + publicPath }; const headers = { host: 'bad.host' @@ -87,7 +102,8 @@ describe('Validation', () => { it('should allow any valid options.public when host is localhost', () => { const options = { - public: 'test.host:80' + public: 'test.host:80', + publicPath }; const headers = { host: 'localhost' @@ -100,7 +116,8 @@ describe('Validation', () => { it('should allow any valid options.public when host is 127.0.0.1', () => { const options = { - public: 'test.host:80' + public: 'test.host:80', + publicPath }; const headers = { host: '127.0.0.1' @@ -112,7 +129,7 @@ describe('Validation', () => { }); it('should allow access for every requests using an IP', () => { - const options = {}; + const options = { publicPath }; const testHosts = [ '192.168.1.123', '192.168.1.2:8080', @@ -133,7 +150,8 @@ describe('Validation', () => { it("should not allow hostnames that don't match options.public", () => { const options = { - public: 'test.host:80' + public: 'test.host:80', + publicPath }; const headers = { host: 'test.hostname:80' @@ -151,7 +169,7 @@ describe('Validation', () => { 'test2.host', 'test3.host' ]; - const options = { allowedHosts: testHosts }; + const options = { allowedHosts: testHosts, publicPath }; const server = new Server(compiler, options); testHosts.forEach((testHost) => { @@ -162,7 +180,7 @@ describe('Validation', () => { }); }); it('should allow hosts that pass a wildcard in allowedHosts', () => { - const options = { allowedHosts: ['.example.com'] }; + const options = { allowedHosts: ['.example.com'], publicPath }; const server = new Server(compiler, options); const testHosts = [ 'www.example.com', diff --git a/test/helper.js b/test/helper.js index 4a4dda7c7d..b980aae1c6 100644 --- a/test/helper.js +++ b/test/helper.js @@ -11,6 +11,9 @@ module.exports = { if (options.quiet === undefined) { options.quiet = true; } + + options.publicPath = options.publicPath || '/'; + const compiler = webpack(config); server = new Server(compiler, options);