Skip to content

Commit 1ab4177

Browse files
committed
feat(cli): add stdin option
1 parent d87a369 commit 1ab4177

File tree

11 files changed

+160
-17
lines changed

11 files changed

+160
-17
lines changed

examples/cli/stdin/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# CLI: Stdin Option
2+
3+
Specifying this option instructs the server to close when `stdin` ends.
4+
5+
```console
6+
npm run webpack-dev-server -- --stdin
7+
```
8+
9+
## What Should Happen
10+
11+
1. The server should begin running.
12+
2. Press `CTL+D` on your keyboard.
13+
3. The server should close.
14+
15+
_Note: the keyboard shortcut for terminating `stdin` can vary depending on the
16+
operating systems._

examples/cli/stdin/app.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use strict';
2+
3+
const target = document.querySelector('#target');
4+
5+
target.innerHTML =
6+
'Press <code>CTL+D</code> on your keyboard to close the server.';

examples/cli/stdin/webpack.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use strict';
2+
3+
module.exports = {
4+
context: __dirname,
5+
entry: './app.js',
6+
};

examples/util.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ const fs = require('fs');
66
const path = require('path');
77
const HtmlWebpackPlugin = require('html-webpack-plugin');
88
const marked = require('marked');
9-
const webpack = require('webpack');
109

1110
module.exports = {
1211
setup(config) {
@@ -65,7 +64,6 @@ module.exports = {
6564

6665
marked(readme, { renderer });
6766

68-
result.plugins.push(new webpack.NamedModulesPlugin());
6967
result.plugins.push(
7068
new HtmlWebpackPlugin({
7169
filename: 'index.html',

lib/Server.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,7 @@ class Server {
7272
routes(this);
7373
killable(this.listeningApp);
7474

75-
if (this.options.setupExitSignals) {
76-
setupExitSignals(this);
77-
}
75+
setupExitSignals(this);
7876

7977
// Proxy WebSocket without the initial http request
8078
// https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade

lib/options.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,9 @@
352352
}
353353
]
354354
},
355+
"stdin": {
356+
"type": "boolean"
357+
},
355358
"transportMode": {
356359
"anyOf": [
357360
{
@@ -411,6 +414,7 @@
411414
"public": "should be {String} (https://webpack.js.org/configuration/dev-server/#devserverpublic)",
412415
"setupExitSignals": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserversetupexitsignals)",
413416
"static": "should be {Boolean|String|Object|Array} (https://webpack.js.org/configuration/dev-server/#devserverstatic)",
417+
"stdin": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverstdin)",
414418
"transportMode": "should be {String|Object} (https://webpack.js.org/configuration/dev-server/#devservertransportmode)",
415419
"useLocalIp": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserveruselocalip)"
416420
}

lib/utils/setupExitSignals.js

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,28 @@
33
const signals = ['SIGINT', 'SIGTERM'];
44

55
function setupExitSignals(server) {
6-
signals.forEach((signal) => {
7-
process.on(signal, () => {
8-
if (server) {
9-
server.close(() => {
10-
// eslint-disable-next-line no-process-exit
11-
process.exit();
12-
});
13-
} else {
6+
const closeAndExit = () => {
7+
if (server) {
8+
server.close(() => {
149
// eslint-disable-next-line no-process-exit
1510
process.exit();
16-
}
11+
});
12+
} else {
13+
// eslint-disable-next-line no-process-exit
14+
process.exit();
15+
}
16+
};
17+
18+
if (server.options.setupExitSignals) {
19+
signals.forEach((signal) => {
20+
process.on(signal, closeAndExit);
1721
});
18-
});
22+
}
23+
24+
if (server.options.stdin) {
25+
process.stdin.on('end', closeAndExit);
26+
process.stdin.resume();
27+
}
1928
}
2029

2130
module.exports = setupExitSignals;

test/__snapshots__/Validation.test.js.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,5 @@ exports[`Validation validation should fail validation for invalid \`static\` con
3939
exports[`Validation validation should fail validation for no additional properties 1`] = `
4040
"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema.
4141
- configuration has an unknown property 'additional'. These properties are valid:
42-
object { bonjour?, client?, compress?, dev?, firewall?, headers?, historyApiFallback?, host?, hot?, http2?, https?, injectClient?, injectHot?, liveReload?, onAfterSetupMiddleware?, onBeforeSetupMiddleware?, onListening?, open?, openPage?, overlay?, port?, profile?, progress?, proxy?, public?, setupExitSignals?, static?, transportMode?, useLocalIp? }"
42+
object { bonjour?, client?, compress?, dev?, firewall?, headers?, historyApiFallback?, host?, hot?, http2?, https?, injectClient?, injectHot?, liveReload?, onAfterSetupMiddleware?, onBeforeSetupMiddleware?, onListening?, open?, openPage?, overlay?, port?, profile?, progress?, proxy?, public?, setupExitSignals?, static?, stdin?, transportMode?, useLocalIp? }"
4343
`;

test/cli/cli.test.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,50 @@ runCLITest('CLI', () => {
210210
});
211211
});
212212

213+
it('should exit the process when stdin ends if --stdin', (done) => {
214+
const cliPath = resolve(__dirname, '../../bin/webpack-dev-server.js');
215+
const examplePath = resolve(__dirname, '../../examples/cli/public');
216+
const cp = execa('node', [cliPath, '--stdin'], { cwd: examplePath });
217+
218+
cp.stderr.on('data', (data) => {
219+
const bits = data.toString();
220+
221+
if (/Compiled successfully/.test(bits)) {
222+
expect(cp.pid !== 0).toBe(true);
223+
224+
cp.stdin.write('hello');
225+
cp.stdin.end('world');
226+
}
227+
});
228+
229+
cp.on('exit', () => {
230+
done();
231+
});
232+
});
233+
234+
it('should exit the process when stdin ends if --stdin, even before the compilation is done', (done) => {
235+
const cliPath = resolve(__dirname, '../../bin/webpack-dev-server.js');
236+
const cwd = resolve(__dirname, '../fixtures/cli');
237+
const cp = execa('node', [cliPath, '--stdin'], { cwd });
238+
239+
let killed = false;
240+
241+
cp.stderr.on('data', () => {
242+
if (!killed) {
243+
expect(cp.pid !== 0).toBe(true);
244+
245+
cp.stdin.write('hello');
246+
cp.stdin.end('world');
247+
}
248+
249+
killed = true;
250+
});
251+
252+
cp.on('exit', () => {
253+
done();
254+
});
255+
});
256+
213257
// TODO: do not skip after @webpack-cli/serve passes null port by default
214258
// https://github.com/webpack/webpack-cli/pull/2126
215259
it.skip('should use different random port when multiple instances are started on different processes', (done) => {

test/server/setupExitSignals-option.test.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,33 @@ describe('setupExitSignals option', () => {
88
let server;
99
let exitSpy;
1010
let killSpy;
11+
let stdinResumeSpy;
1112
const signals = ['SIGINT', 'SIGTERM'];
1213

1314
beforeEach((done) => {
1415
server = testServer.start(
1516
config,
1617
{
1718
setupExitSignals: true,
19+
stdin: true,
1820
port,
1921
},
2022
done
2123
);
2224
exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {});
25+
stdinResumeSpy = jest
26+
.spyOn(process.stdin, 'resume')
27+
.mockImplementation(() => {});
2328
killSpy = jest.spyOn(server.listeningApp, 'kill');
2429
});
2530

2631
afterEach((done) => {
2732
exitSpy.mockReset();
33+
stdinResumeSpy.mockReset();
2834
signals.forEach((signal) => {
2935
process.removeAllListeners(signal);
3036
});
37+
process.stdin.removeAllListeners('end');
3138
testServer.close(done);
3239
});
3340

@@ -40,4 +47,14 @@ describe('setupExitSignals option', () => {
4047
done();
4148
}, 1000);
4249
});
50+
51+
it('should close and exit on stdin end', (done) => {
52+
process.stdin.emit('end');
53+
54+
setTimeout(() => {
55+
expect(killSpy.mock.calls.length).toEqual(1);
56+
expect(exitSpy.mock.calls.length).toEqual(1);
57+
done();
58+
}, 1000);
59+
});
4360
});

test/server/utils/setupExitSignals.test.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ const setupExitSignals = require('../../../lib/utils/setupExitSignals');
55
describe('setupExitSignals', () => {
66
let server;
77
let exitSpy;
8+
let stdinResumeSpy;
89
const signals = ['SIGINT', 'SIGTERM'];
910

1011
beforeAll(() => {
1112
exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {});
13+
stdinResumeSpy = jest
14+
.spyOn(process.stdin, 'resume')
15+
.mockImplementation(() => {});
1216
server = {
1317
close: jest.fn((callback) => {
1418
callback();
@@ -22,11 +26,20 @@ describe('setupExitSignals', () => {
2226
signals.forEach((signal) => {
2327
process.removeAllListeners(signal);
2428
});
29+
process.stdin.removeAllListeners('end');
2530
});
2631

2732
signals.forEach((signal) => {
2833
it(`should close server, then exit process (${signal})`, (done) => {
29-
setupExitSignals(server);
34+
const serverSetupExitSignals = Object.assign(
35+
{
36+
options: {
37+
setupExitSignals: true,
38+
},
39+
},
40+
server
41+
);
42+
setupExitSignals(serverSetupExitSignals);
3043
process.emit(signal);
3144

3245
setTimeout(() => {
@@ -36,4 +49,36 @@ describe('setupExitSignals', () => {
3649
}, 1000);
3750
});
3851
});
52+
53+
it(`should resume stdin if stdin: true`, () => {
54+
const serverStdin = Object.assign(
55+
{
56+
options: {
57+
stdin: true,
58+
},
59+
},
60+
server
61+
);
62+
setupExitSignals(serverStdin);
63+
expect(stdinResumeSpy.mock.calls.length).toEqual(1);
64+
});
65+
66+
it(`should close server, then exit process (stdin end)`, (done) => {
67+
const serverStdin = Object.assign(
68+
{
69+
options: {
70+
stdin: true,
71+
},
72+
},
73+
server
74+
);
75+
setupExitSignals(serverStdin);
76+
process.stdin.emit('end');
77+
78+
setTimeout(() => {
79+
expect(server.close.mock.calls.length).toEqual(1);
80+
expect(exitSpy.mock.calls.length).toEqual(1);
81+
done();
82+
}, 1000);
83+
});
3984
});

0 commit comments

Comments
 (0)