From e819cfe3dea79464fae48b9449522679918097b9 Mon Sep 17 00:00:00 2001 From: Loonride Date: Sat, 10 Aug 2019 13:35:24 -0500 Subject: [PATCH] feat(server): add stdin option to API (#2186) * feat(server): add stdin for api * test(stdin): switch to async await tests for stdin * test(cli): use await timer --- lib/Server.js | 3 ++ lib/options.json | 4 ++ lib/utils/createConfig.js | 7 +-- lib/utils/handleStdin.js | 16 +++++++ test/cli/cli.test.js | 42 ++++++++++++++++++ test/helpers/test-bin.js | 19 ++++++-- test/options.test.js | 4 ++ test/ports-map.js | 1 + test/server/stdin-option.test.js | 62 +++++++++++++++++++++++++++ test/server/utils/handleStdin.test.js | 41 ++++++++++++++++++ 10 files changed, 190 insertions(+), 9 deletions(-) create mode 100644 lib/utils/handleStdin.js create mode 100644 test/server/stdin-option.test.js create mode 100644 test/server/utils/handleStdin.test.js diff --git a/lib/Server.js b/lib/Server.js index cc2d9488cf..f260e75919 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -33,6 +33,7 @@ const createDomain = require('./utils/createDomain'); const runBonjour = require('./utils/runBonjour'); const routes = require('./utils/routes'); const getSocketServerImplementation = require('./utils/getSocketServerImplementation'); +const handleStdin = require('./utils/handleStdin'); const schema = require('./options.json'); // Workaround for node ^8.6.0, ^9.0.0 @@ -69,6 +70,8 @@ class Server { normalizeOptions(this.compiler, this.options); + handleStdin(this.options); + updateCompiler(this.compiler, this.options); // this.SocketServerImplementation is a class, so it must be instantiated before use diff --git a/lib/options.json b/lib/options.json index f1b6556f4f..87319af51c 100644 --- a/lib/options.json +++ b/lib/options.json @@ -368,6 +368,9 @@ } ] }, + "stdin": { + "type": "boolean" + }, "useLocalIp": { "type": "boolean" }, @@ -450,6 +453,7 @@ "staticOptions": "should be {Object} (https://webpack.js.org/configuration/dev-server/#devserverstaticoptions)", "stats": "should be {Object|Boolean} (https://webpack.js.org/configuration/dev-server/#devserverstats-)", "transportMode": "should be {String|Object} (https://webpack.js.org/configuration/dev-server/#devservertransportmode)", + "stdin": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverstdin)", "useLocalIp": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserveruselocalip)", "warn": "should be {Function}", "watchContentBase": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverwatchcontentbase)", diff --git a/lib/utils/createConfig.js b/lib/utils/createConfig.js index 0211561235..d5a3e1db99 100644 --- a/lib/utils/createConfig.js +++ b/lib/utils/createConfig.js @@ -81,12 +81,7 @@ function createConfig(config, argv, { port }) { } if (argv.stdin) { - process.stdin.on('end', () => { - // eslint-disable-next-line no-process-exit - process.exit(0); - }); - - process.stdin.resume(); + options.stdin = true; } // TODO https://github.com/webpack/webpack-dev-server/issues/616 (v4) diff --git a/lib/utils/handleStdin.js b/lib/utils/handleStdin.js new file mode 100644 index 0000000000..c26431300c --- /dev/null +++ b/lib/utils/handleStdin.js @@ -0,0 +1,16 @@ +'use strict'; + +function handleStdin(options) { + if (options.stdin) { + // listening for this event only once makes testing easier, + // since it prevents event listeners from hanging open + process.stdin.once('end', () => { + // eslint-disable-next-line no-process-exit + process.exit(0); + }); + + process.stdin.resume(); + } +} + +module.exports = handleStdin; diff --git a/test/cli/cli.test.js b/test/cli/cli.test.js index 4a47075952..c29bad439f 100644 --- a/test/cli/cli.test.js +++ b/test/cli/cli.test.js @@ -8,6 +8,7 @@ const { join, resolve } = require('path'); const execa = require('execa'); const { unlinkAsync } = require('../helpers/fs'); const testBin = require('../helpers/test-bin'); +const timer = require('../helpers/timer'); const httpsCertificateDirectory = resolve( __dirname, @@ -83,6 +84,47 @@ describe('CLI', () => { } }); + it('without --stdin, with stdin "end" event should time out', async (done) => { + const configPath = resolve( + __dirname, + '../fixtures/simple-config/webpack.config.js' + ); + const childProcess = testBin(false, configPath, true); + + childProcess.once('exit', () => { + expect(childProcess.killed).toBeTruthy(); + done(); + }); + + await timer(500); + // this is meant to confirm that it does not have any effect on the running process + // since options.stdin is not enabled + childProcess.stdin.emit('end'); + childProcess.stdin.pause(); + + await timer(500); + + childProcess.kill(); + }); + + it('--stdin, with "end" event should exit without time out', async () => { + const configPath = resolve( + __dirname, + '../fixtures/simple-config/webpack.config.js' + ); + const childProcess = testBin('--stdin', configPath); + + await timer(500); + + childProcess.stdin.emit('end'); + childProcess.stdin.pause(); + + const { exitCode, timedOut, killed } = await childProcess; + expect(exitCode).toEqual(0); + expect(timedOut).toBeFalsy(); + expect(killed).toBeFalsy(); + }); + it('should accept the promise function of webpack.config.js', async () => { try { const { exitCode } = await testBin( diff --git a/test/helpers/test-bin.js b/test/helpers/test-bin.js index 25e3af020a..39f38f0b16 100644 --- a/test/helpers/test-bin.js +++ b/test/helpers/test-bin.js @@ -1,6 +1,7 @@ 'use strict'; const path = require('path'); +const { spawn } = require('child_process'); const execa = require('execa'); const webpackDevServerPath = path.resolve( @@ -12,9 +13,12 @@ const basicConfigPath = path.resolve( '../fixtures/cli/webpack.config.js' ); -function testBin(testArgs, configPath) { +function testBin(testArgs, configPath, useSpawn) { const cwd = process.cwd(); - const env = process.env.NODE_ENV; + const env = { + NODE_ENV: process.env.NODE_ENV, + PATH: process.env.PATH, + }; if (!configPath) { configPath = basicConfigPath; @@ -28,7 +32,16 @@ function testBin(testArgs, configPath) { const args = [webpackDevServerPath, '--config', configPath].concat(testArgs); - return execa('node', args, { cwd, env, timeout: 10000 }); + const opts = { cwd, env, timeout: 10000 }; + let execLib = execa; + // use Node's spawn as a workaround for execa issues + // https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options + if (useSpawn) { + execLib = spawn; + delete opts.timeout; + } + + return execLib('node', args, opts); } module.exports = testBin; diff --git a/test/options.test.js b/test/options.test.js index 8044c49bf7..8f41f4b65f 100644 --- a/test/options.test.js +++ b/test/options.test.js @@ -375,6 +375,10 @@ describe('options', () => { }, ], }, + stdin: { + success: [false], + failure: [''], + }, useLocalIp: { success: [false], failure: [''], diff --git a/test/ports-map.js b/test/ports-map.js index 81133db921..07ebee36a0 100644 --- a/test/ports-map.js +++ b/test/ports-map.js @@ -43,6 +43,7 @@ const portsList = { 'progress-option': 1, 'profile-option': 1, Iframe: 1, + 'stdin-option': 1, }; let startPort = 8089; diff --git a/test/server/stdin-option.test.js b/test/server/stdin-option.test.js new file mode 100644 index 0000000000..da742ffff8 --- /dev/null +++ b/test/server/stdin-option.test.js @@ -0,0 +1,62 @@ +'use strict'; + +const config = require('../fixtures/simple-config/webpack.config'); +const testServer = require('../helpers/test-server'); +const timer = require('../helpers/timer'); +const port = require('../ports-map')['stdin-option']; + +describe('stdin', () => { + // eslint-disable-next-line no-unused-vars + let server; + let exitSpy; + + beforeAll(() => { + exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {}); + }); + + afterEach((done) => { + server = null; + exitSpy.mockReset(); + process.stdin.removeAllListeners('end'); + testServer.close(done); + }); + + describe('enabled', () => { + beforeAll((done) => { + server = testServer.start( + config, + { + port, + stdin: true, + }, + done + ); + }); + + it('should exit process', async () => { + process.stdin.emit('end'); + await timer(1000); + process.stdin.pause(); + expect(exitSpy.mock.calls[0]).toEqual([0]); + }); + }); + + describe('disabled (default)', () => { + beforeAll((done) => { + server = testServer.start( + config, + { + port, + }, + done + ); + }); + + it('should not exit process', async () => { + process.stdin.emit('end'); + await timer(1000); + process.stdin.pause(); + expect(exitSpy.mock.calls.length).toEqual(0); + }); + }); +}); diff --git a/test/server/utils/handleStdin.test.js b/test/server/utils/handleStdin.test.js new file mode 100644 index 0000000000..5d44e0dc06 --- /dev/null +++ b/test/server/utils/handleStdin.test.js @@ -0,0 +1,41 @@ +'use strict'; + +const timer = require('../../helpers/timer'); +const handleStdin = require('../../../lib/utils/handleStdin'); + +describe('handleStdin', () => { + let exitSpy; + + beforeAll(() => { + exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {}); + }); + + afterEach(() => { + process.stdin.removeAllListeners('end'); + exitSpy.mockReset(); + }); + + describe('enabled', () => { + it('should exit process', async () => { + handleStdin({ + stdin: true, + }); + process.stdin.emit('end'); + + await timer(1000); + process.stdin.pause(); + expect(exitSpy.mock.calls[0]).toEqual([0]); + }); + }); + + describe('disabled (default)', () => { + it('should not exit process', async () => { + handleStdin({}); + process.stdin.emit('end'); + + await timer(1000); + process.stdin.pause(); + expect(exitSpy.mock.calls.length).toEqual(0); + }); + }); +});