diff --git a/lib/schemas/utils/socketServerImplementationPrototype.json b/lib/schemas/utils/socketServerImplementationPrototype.json new file mode 100644 index 0000000000..a17dfe538c --- /dev/null +++ b/lib/schemas/utils/socketServerImplementationPrototype.json @@ -0,0 +1,33 @@ +{ + "type": "object", + "properties": { + "constructor": { + "instanceof": "Function" + }, + "send": { + "instanceof": "Function" + }, + "close": { + "instanceof": "Function" + }, + "onConnection": { + "instanceof": "Function" + } + }, + "errorMessage": { + "properties": { + "constructor": "- serverMode must have a constructor that takes a single server argument and calls super(server) on the superclass BaseServer, found via require('webpack-dev-server/lib/servers/BaseServer')", + "send": "- serverMode must have a send(connection, message) method that sends the message string to the provided client connection object", + "close": "- serverMode must have a close(connection) method that closes the provided client connection object", + "onConnection": "- serverMode must have a onConnection(f) method that calls f(connection) whenever a new client connection is made" + }, + "required": { + "constructor": "- serverMode must have a constructor that takes a single server argument and calls super(server) on the superclass BaseServer, found via require('webpack-dev-server/lib/servers/BaseServer')", + "send": "- serverMode must have a send(connection, message) method that sends the message string to the provided client connection object", + "close": "- serverMode must have a close(connection) method that closes the provided client connection object", + "onConnection": "- serverMode must have a onConnection(f) method that calls f(connection) whenever a new client connection is made" + } + }, + "required": ["constructor", "send", "close", "onConnection"], + "additionalProperties": true +} diff --git a/lib/utils/getSocketServerImplementation.js b/lib/utils/getSocketServerImplementation.js index 6df634085f..f53f2af539 100644 --- a/lib/utils/getSocketServerImplementation.js +++ b/lib/utils/getSocketServerImplementation.js @@ -1,5 +1,8 @@ 'use strict'; +const validateOptions = require('schema-utils'); +const schema = require('../schemas/utils/socketServerImplementationPrototype.json'); + function getSocketServerImplementation(options) { let ServerImplementation; let serverImplFound = true; @@ -35,6 +38,8 @@ function getSocketServerImplementation(options) { ); } + validateOptions(schema, ServerImplementation.prototype, 'webpack Dev Server'); + return ServerImplementation; } diff --git a/test/server/utils/__snapshots__/getSocketServerImplementation.test.js.snap b/test/server/utils/__snapshots__/getSocketServerImplementation.test.js.snap new file mode 100644 index 0000000000..d67f1e59f6 --- /dev/null +++ b/test/server/utils/__snapshots__/getSocketServerImplementation.test.js.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getSocketServerImplementation util should throw with serverMode (bad path) 1`] = `[Error: serverMode must be a string denoting a default implementation (e.g. 'sockjs'), a full path to a JS file which exports a class extending BaseServer (webpack-dev-server/lib/servers/BaseServer) via require.resolve(...), or the class itself which extends BaseServer]`; + +exports[`getSocketServerImplementation util should throw with serverMode (no close, onConnection methods) 1`] = ` +[ValidationError: webpack Dev Server Invalid Options + +options - serverMode must have a close(connection) method that closes the provided client connection object +options - serverMode must have a onConnection(f) method that calls f(connection) whenever a new client connection is made +] +`; + +exports[`getSocketServerImplementation util should throw with serverMode (no constructor, send, close, onConnection methods) 1`] = ` +[ValidationError: webpack Dev Server Invalid Options + +options [object Object] +options - serverMode must have a send(connection, message) method that sends the message string to the provided client connection object +options - serverMode must have a close(connection) method that closes the provided client connection object +options - serverMode must have a onConnection(f) method that calls f(connection) whenever a new client connection is made +] +`; + +exports[`getSocketServerImplementation util should throw with serverMode (no onConnection method) 1`] = ` +[ValidationError: webpack Dev Server Invalid Options + +options - serverMode must have a onConnection(f) method that calls f(connection) whenever a new client connection is made +] +`; + +exports[`getSocketServerImplementation util should throw with serverMode (no send, close, onConnection methods) 1`] = ` +[ValidationError: webpack Dev Server Invalid Options + +options - serverMode must have a send(connection, message) method that sends the message string to the provided client connection object +options - serverMode must have a close(connection) method that closes the provided client connection object +options - serverMode must have a onConnection(f) method that calls f(connection) whenever a new client connection is made +] +`; diff --git a/test/server/utils/getSocketServerImplementation.test.js b/test/server/utils/getSocketServerImplementation.test.js index 0a157c39a9..50b7c5bd5c 100644 --- a/test/server/utils/getSocketServerImplementation.test.js +++ b/test/server/utils/getSocketServerImplementation.test.js @@ -1,10 +1,13 @@ 'use strict'; +/* eslint-disable constructor-super, no-empty-function, no-useless-constructor, no-unused-vars, class-methods-use-this */ + const getSocketServerImplementation = require('../../../lib/utils/getSocketServerImplementation'); +const BaseServer = require('../../../lib/servers/BaseServer'); const SockJSServer = require('../../../lib/servers/SockJSServer'); describe('getSocketServerImplementation util', () => { - it("should works with string serverMode ('sockjs')", () => { + it("should work with string serverMode ('sockjs')", () => { let result; expect(() => { @@ -16,7 +19,7 @@ describe('getSocketServerImplementation util', () => { expect(result).toEqual(SockJSServer); }); - it('should works with serverMode (SockJSServer class)', () => { + it('should work with serverMode (SockJSServer class)', () => { let result; expect(() => { @@ -40,11 +43,90 @@ describe('getSocketServerImplementation util', () => { expect(result).toEqual(SockJSServer); }); - it('should throws with serverMode (bad path)', () => { + it('should work with serverMode (additional class methods)', () => { + let result; + + const ExtendedSockJSServer = class ExtendedSockJSServer extends SockJSServer { + myMethod() { + this.test = true; + } + }; + expect(() => { - getSocketServerImplementation({ - serverMode: '/bad/path/to/implementation', + result = getSocketServerImplementation({ + serverMode: ExtendedSockJSServer, }); - }).toThrow(/serverMode must be a string/); + }).not.toThrow(); + + expect(result).toEqual(ExtendedSockJSServer); + }); + + const ClassWithoutConstructor = class ClassWithoutConstructor {}; + // eslint-disable-next-line no-undefined + ClassWithoutConstructor.prototype.constructor = undefined; + + const badSetups = [ + { + title: 'should throw with serverMode (bad path)', + config: { + serverMode: '/bad/path/to/implementation', + }, + }, + { + title: + 'should throw with serverMode (no constructor, send, close, onConnection methods)', + config: { + serverMode: ClassWithoutConstructor, + }, + }, + { + title: + 'should throw with serverMode (no send, close, onConnection methods)', + config: { + serverMode: class ServerImplementation extends BaseServer { + constructor(server) { + super(server); + } + }, + }, + }, + { + title: 'should throw with serverMode (no close, onConnection methods)', + config: { + serverMode: class ServerImplementation extends BaseServer { + constructor(server) { + super(server); + } + + send(connection, message) {} + }, + }, + }, + { + title: 'should throw with serverMode (no onConnection method)', + config: { + serverMode: class ServerImplementation extends BaseServer { + constructor(server) { + super(server); + } + + send(connection, message) {} + + close(connection) {} + }, + }, + }, + ]; + + badSetups.forEach((setup) => { + it(setup.title, () => { + let thrown = false; + try { + getSocketServerImplementation(setup.config); + } catch (e) { + thrown = true; + expect(e).toMatchSnapshot(); + } + }); }); });