diff --git a/bin/cli-flags.js b/bin/cli-flags.js index b2a71e92b8..b3766b3ac7 100644 --- a/bin/cli-flags.js +++ b/bin/cli-flags.js @@ -480,6 +480,29 @@ module.exports = { simpleType: 'string', multiple: false, }, + 'ipc-socket': { + configs: [ + { + type: 'string', + multiple: false, + description: + 'Listen to a unix socket. https://webpack.js.org/configuration/dev-server/#devserverunixsocket', + path: 'ipcSocket', + }, + { + type: 'enum', + values: [true], + multiple: false, + description: + 'Listen to a unix socket. https://webpack.js.org/configuration/dev-server/#devserverunixsocket', + path: 'ipcSocket', + }, + ], + description: + 'Listen to a unix socket. https://webpack.js.org/configuration/dev-server/#devserverunixsocket', + simpleType: 'string', + multiple: false, + }, 'live-reload': { configs: [ { diff --git a/lib/Server.js b/lib/Server.js index e45278d4e2..174393d6a8 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -711,6 +711,45 @@ class Server { this.options.host = internalIp.v6.sync() || '::'; } + if (this.options.ipcSocket) { + const net = require('net'); + const fs = require('fs'); + + this.server.on('error', (e1) => { + if (e1.code === 'EADDRINUSE') { + const clientSocket = new net.Socket(); + clientSocket.on('error', (e2) => { + if (e2.code === 'ECONNREFUSED') { + // No other server listening on this socket so it can be safely removed + fs.unlinkSync(this.options.ipcSocket); + this.server.listen( + this.options.ipcSocket, + this.options.host, + (err) => { + if (err) throw err; + } + ); + } + }); + clientSocket.connect({ path: this.options.ipcSocket }, () => { + throw new Error('This socket is already used'); + }); + } + }); + return this.server.listen( + this.options.socket, + this.options.host, + (err1) => { + if (err1) throw err1; + // chmod 666 (rw rw rw) + const READ_WRITE = 438; + fs.chmod(this.options.socket, READ_WRITE, (err2) => { + if (err2) throw err2; + this.logStatus(); + }); + } + ); + } return Server.getFreePort(this.options.port) .then((foundPort) => { this.options.port = foundPort; @@ -1031,11 +1070,7 @@ class Server { // disabling refreshing on changing the content if (this.options.liveReload) { watcher.on('change', (item) => { - this.sendMessage( - this.webSocketConnections, - 'static-changed', - item - ); + this.sendMessage(this.webSocketConnections, 'static-changed', item); }); } diff --git a/lib/options.json b/lib/options.json index 52cd0e9eba..5ceda4319c 100644 --- a/lib/options.json +++ b/lib/options.json @@ -300,7 +300,19 @@ ], "description": "Enables Hot Module Replacement. https://webpack.js.org/configuration/dev-server/#devserverhot" }, - + "IPCSocket": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "boolean", + "enum": [true] + } + ], + "description": "Listen to a unix socket. https://webpack.js.org/configuration/dev-server/#devserverunixsocket" + }, "LiveReload": { "type": "boolean", "description": "Enables reload/refresh the page(s) when file changes are detected (enabled by default). https://webpack.js.org/configuration/dev-server/#devserverlivereload" @@ -684,6 +696,9 @@ "https": { "$ref": "#/definitions/HTTPS" }, + "ipcSocket": { + "$ref": "#/definitions/IPCSocket" + }, "liveReload": { "$ref": "#/definitions/LiveReload" }, diff --git a/lib/utils/normalizeOptions.js b/lib/utils/normalizeOptions.js index 538a90feac..7a19baedb4 100644 --- a/lib/utils/normalizeOptions.js +++ b/lib/utils/normalizeOptions.js @@ -1,6 +1,7 @@ 'use strict'; const path = require('path'); +const os = require('os'); const fs = require('graceful-fs'); const isAbsoluteUrl = require('is-absolute-url'); const getCompilerConfigArray = require('./getCompilerConfigArray'); @@ -163,6 +164,10 @@ function normalizeOptions(compiler, options, logger) { options.compress = true; } + if (options.ipcSocket === true) { + options.ipcSocket = path.resolve(os.tmpdir(), 'webpack-dev-server.socket'); + } + // if the user enables http2, we can safely enable https if ((options.http2 && !options.https) || options.https === true) { options.https = { diff --git a/test/__snapshots__/validate-options.test.js.snap.webpack4 b/test/__snapshots__/validate-options.test.js.snap.webpack4 index ef404505a2..0cc22be94d 100644 --- a/test/__snapshots__/validate-options.test.js.snap.webpack4 +++ b/test/__snapshots__/validate-options.test.js.snap.webpack4 @@ -366,6 +366,21 @@ exports[`options validate should throw an error on the "https" option with '{"re -> Request for an SSL certificate." `; +exports[`options validate should throw an error on the "ipcSocket" option with '' value 1`] = ` +"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. + - options.ipcSocket should be an non-empty string." +`; + +exports[`options validate should throw an error on the "ipcSocket" option with 'false' value 1`] = ` +"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. + - options.ipcSocket should be one of these: + non-empty string | true + -> Listen to a unix socket. https://webpack.js.org/configuration/dev-server/#devserverunixsocket + Details: + * options.ipcSocket should be a non-empty string. + * options.ipcSocket should be true." +`; + exports[`options validate should throw an error on the "onAfterSetupMiddleware" option with 'false' value 1`] = ` "ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. - options.onAfterSetupMiddleware should be an instance of function. diff --git a/test/__snapshots__/validate-options.test.js.snap.webpack5 b/test/__snapshots__/validate-options.test.js.snap.webpack5 index ef404505a2..0cc22be94d 100644 --- a/test/__snapshots__/validate-options.test.js.snap.webpack5 +++ b/test/__snapshots__/validate-options.test.js.snap.webpack5 @@ -366,6 +366,21 @@ exports[`options validate should throw an error on the "https" option with '{"re -> Request for an SSL certificate." `; +exports[`options validate should throw an error on the "ipcSocket" option with '' value 1`] = ` +"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. + - options.ipcSocket should be an non-empty string." +`; + +exports[`options validate should throw an error on the "ipcSocket" option with 'false' value 1`] = ` +"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. + - options.ipcSocket should be one of these: + non-empty string | true + -> Listen to a unix socket. https://webpack.js.org/configuration/dev-server/#devserverunixsocket + Details: + * options.ipcSocket should be a non-empty string. + * options.ipcSocket should be true." +`; + exports[`options validate should throw an error on the "onAfterSetupMiddleware" option with 'false' value 1`] = ` "ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema. - options.onAfterSetupMiddleware should be an instance of function. diff --git a/test/validate-options.test.js b/test/validate-options.test.js index 9533c29976..6e4af2c8e4 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -234,6 +234,10 @@ const tests = { }, ], }, + ipcSocket: { + success: [true, '/tmp/webpack-dev-server.socket'], + failure: ['', false], + }, onListening: { success: [() => {}], failure: [''],