Skip to content

Commit e3c2683

Browse files
feat: improve the open option, you can specify target and app options
BREAKING CHANGE: the `openPage` option and `--open-page` CLI option was removed in favor `open: boolean | string | (string | { target?: string | string[], app: string | string[] })[]` and `--open-target [URL]` and `--open-app <browser>` for CLI
1 parent b3374c3 commit e3c2683

File tree

10 files changed

+959
-676
lines changed

10 files changed

+959
-676
lines changed

bin/cli-flags.js

+28-4
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,11 @@ module.exports = {
111111
'Do not close and exit the process on SIGNIT and SIGTERM.',
112112
negative: true,
113113
},
114+
// TODO remove in the next major release in favor `--open-target`
114115
{
115116
name: 'open',
116117
type: [Boolean, String],
118+
multiple: true,
117119
configs: [
118120
{
119121
type: 'boolean',
@@ -122,18 +124,40 @@ module.exports = {
122124
type: 'string',
123125
},
124126
],
125-
description:
126-
'Open the default browser, or optionally specify a browser name.',
127+
description: 'Open the default browser.',
127128
},
128129
{
129-
name: 'open-page',
130+
name: 'open-app',
130131
type: String,
131132
configs: [
132133
{
133134
type: 'string',
134135
},
135136
],
136-
description: 'Open default browser with the specified page.',
137+
description: 'Open specified browser.',
138+
processor(opts) {
139+
opts.open = opts.open || {};
140+
opts.open.app = opts.openApp.split(' ');
141+
delete opts.openApp;
142+
},
143+
},
144+
{
145+
name: 'open-target',
146+
type: String,
147+
configs: [
148+
{
149+
type: 'boolean',
150+
},
151+
{
152+
type: 'string',
153+
},
154+
],
155+
description: 'Open specified browser.',
156+
processor(opts) {
157+
opts.open = opts.open || {};
158+
opts.open.target = opts.openTarget;
159+
delete opts.openTarget;
160+
},
137161
multiple: true,
138162
},
139163
{

lib/Server.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -719,10 +719,10 @@ class Server {
719719
);
720720
}
721721

722-
if (this.options.open || this.options.openPage) {
722+
if (this.options.open) {
723723
const openTarget = prettyPrintUrl(this.hostname || 'localhost');
724724

725-
runOpen(openTarget, this.options, this.logger);
725+
runOpen(openTarget, this.options.open, this.logger);
726726
}
727727
}
728728

lib/options.json

+64-15
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"minLength": 1
1111
},
1212
"staticOptions": {
13-
"type": "object"
13+
"type": "object",
14+
"additionalProperties": true
1415
},
1516
"publicPath": {
1617
"anyOf": [
@@ -34,7 +35,8 @@
3435
"type": "boolean"
3536
},
3637
{
37-
"type": "object"
38+
"type": "object",
39+
"additionalProperties": true
3840
}
3941
]
4042
},
@@ -53,6 +55,54 @@
5355
"StaticString": {
5456
"type": "string",
5557
"minLength": 1
58+
},
59+
"OpenBoolean": {
60+
"type": "boolean"
61+
},
62+
"OpenString": {
63+
"type": "string",
64+
"minLength": 1
65+
},
66+
"OpenObject": {
67+
"type": "object",
68+
"additionalProperties": false,
69+
"properties": {
70+
"target": {
71+
"anyOf": [
72+
{
73+
"type": "boolean"
74+
},
75+
{
76+
"type": "string",
77+
"minLength": 1
78+
},
79+
{
80+
"type": "array",
81+
"items": {
82+
"type": "string",
83+
"minLength": 1
84+
},
85+
"minItems": 1
86+
}
87+
]
88+
},
89+
"app": {
90+
"anyOf": [
91+
{
92+
"type": "string",
93+
"minLength": 1
94+
},
95+
{
96+
"type": "array",
97+
"items": {
98+
"type": "string",
99+
"minLength": 1
100+
},
101+
"minItems": 1
102+
}
103+
]
104+
}
105+
}
56106
}
57107
},
58108
"properties": {
@@ -258,25 +308,25 @@
258308
"open": {
259309
"anyOf": [
260310
{
261-
"type": "string"
311+
"$ref": "#/definitions/OpenBoolean"
262312
},
263313
{
264-
"type": "boolean"
314+
"$ref": "#/definitions/OpenString"
265315
},
266316
{
267-
"type": "object"
268-
}
269-
]
270-
},
271-
"openPage": {
272-
"anyOf": [
273-
{
274-
"type": "string"
317+
"$ref": "#/definitions/OpenObject"
275318
},
276319
{
277320
"type": "array",
278321
"items": {
279-
"type": "string"
322+
"anyOf": [
323+
{
324+
"$ref": "#/definitions/OpenString"
325+
},
326+
{
327+
"$ref": "#/definitions/OpenObject"
328+
}
329+
]
280330
},
281331
"minItems": 1
282332
}
@@ -393,8 +443,7 @@
393443
"onAfterSetupMiddleware": "should be {Function} (https://webpack.js.org/configuration/dev-server/#devserverafter)",
394444
"onBeforeSetupMiddleware": "should be {Function} (https://webpack.js.org/configuration/dev-server/#devserverbefore)",
395445
"onListening": "should be {Function} (https://webpack.js.org/configuration/dev-server/#onlistening)",
396-
"open": "should be {String|Boolean|Object} (https://webpack.js.org/configuration/dev-server/#devserveropen)",
397-
"openPage": "should be {String|Array} (https://webpack.js.org/configuration/dev-server/#devserveropenpage)",
446+
"open": "should be {Boolean|String|(STRING | Object)[]} (https://webpack.js.org/configuration/dev-server/#devserveropen)",
398447
"port": "should be {Number|String|Null} (https://webpack.js.org/configuration/dev-server/#devserverport)",
399448
"proxy": "should be {Object|Array} (https://webpack.js.org/configuration/dev-server/#devserverproxy)",
400449
"public": "should be {String} (https://webpack.js.org/configuration/dev-server/#devserverpublic)",

lib/utils/runOpen.js

+64-18
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,75 @@ const isAbsoluteUrl = require('is-absolute-url');
55

66
function runOpen(uri, options, logger) {
77
// https://github.com/webpack/webpack-dev-server/issues/1990
8-
let openOptions = { wait: false };
9-
let openOptionValue = '';
10-
11-
if (typeof options.open === 'string') {
12-
openOptions = Object.assign({}, openOptions, { app: options.open });
13-
openOptionValue = `: "${options.open}"`;
14-
} else if (typeof options.open === 'object') {
15-
openOptions = options.open;
16-
openOptionValue = `: "${JSON.stringify(options.open)}"`;
17-
}
8+
const defaultOpenOptions = { wait: false };
9+
const openTasks = [];
10+
11+
const getOpenTask = (item) => {
12+
if (typeof item === 'boolean') {
13+
return [{ target: uri, options: defaultOpenOptions }];
14+
}
15+
16+
if (typeof item === 'string') {
17+
return [{ target: item, options: defaultOpenOptions }];
18+
}
19+
20+
let targets;
21+
22+
if (item.target) {
23+
targets = Array.isArray(item.target) ? item.target : [item.target];
24+
} else {
25+
targets = [uri];
26+
}
1827

19-
const pages =
20-
typeof options.openPage === 'string'
21-
? [options.openPage]
22-
: options.openPage || [''];
28+
return targets.map((target) => {
29+
const openOptions = defaultOpenOptions;
30+
31+
if (item.app) {
32+
openOptions.app = item.app;
33+
}
34+
35+
return { target, options: openOptions };
36+
});
37+
};
38+
39+
if (Array.isArray(options)) {
40+
options.forEach((item) => {
41+
openTasks.push(...getOpenTask(item));
42+
});
43+
} else {
44+
openTasks.push(...getOpenTask(options));
45+
}
2346

2447
return Promise.all(
25-
pages.map((page) => {
26-
const pageUrl = page && isAbsoluteUrl(page) ? page : `${uri}${page}`;
48+
openTasks.map((openTask) => {
49+
let openTarget;
50+
51+
if (openTask.target) {
52+
if (typeof openTask.target === 'boolean') {
53+
openTarget = uri;
54+
} else {
55+
openTarget = isAbsoluteUrl(openTask.target)
56+
? openTask.target
57+
: new URL(openTask.target, uri).toString();
58+
}
59+
} else {
60+
openTarget = uri;
61+
}
2762

28-
return open(pageUrl, openOptions).catch(() => {
63+
return open(openTarget, openTask.options).catch(() => {
2964
logger.warn(
30-
`Unable to open "${pageUrl}" in browser${openOptionValue}. If you are running in a headless environment, please do not use the --open flag`
65+
`Unable to open "${openTarget}" page${
66+
// eslint-disable-next-line no-nested-ternary
67+
openTask.options.app
68+
? Array.isArray(openTask.options.app)
69+
? ` in "${
70+
openTask.options.app[0]
71+
}" app with "${openTask.options.app
72+
.slice(1)
73+
.join(' ')}" arguments`
74+
: ` in "${openTask.options.app}" app`
75+
: ''
76+
}. If you are running in a headless environment, please do not use the "--open" flag or the "open" option.`
3177
);
3278
});
3379
})

test/__snapshots__/Validation.test.js.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,5 @@ exports[`Validation validation should fail validation for invalid \`static\` con
4343
exports[`Validation validation should fail validation for no additional properties 1`] = `
4444
"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema.
4545
- configuration has an unknown property 'additional'. These properties are valid:
46-
object { bonjour?, client?, compress?, dev?, firewall?, headers?, historyApiFallback?, host?, hot?, http2?, https?, liveReload?, onAfterSetupMiddleware?, onBeforeSetupMiddleware?, onListening?, open?, openPage?, port?, proxy?, public?, setupExitSignals?, static?, transportMode? }"
46+
object { bonjour?, client?, compress?, dev?, firewall?, headers?, historyApiFallback?, host?, hot?, http2?, https?, liveReload?, onAfterSetupMiddleware?, onBeforeSetupMiddleware?, onListening?, open?, port?, proxy?, public?, setupExitSignals?, static?, transportMode? }"
4747
`;

test/cli/cli.test.js

+72
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,78 @@ describe('CLI', () => {
219219
.catch(done);
220220
});
221221

222+
it('--open', (done) => {
223+
testBin('--open')
224+
.then((output) => {
225+
expect(output.exitCode).toEqual(0);
226+
done();
227+
})
228+
.catch(done);
229+
});
230+
231+
it('--open /index.html', (done) => {
232+
testBin('--open /index.html')
233+
.then((output) => {
234+
expect(output.exitCode).toEqual(0);
235+
done();
236+
})
237+
.catch(done);
238+
});
239+
240+
it('--open /first.html second.html', (done) => {
241+
testBin('--open /first.html second.html')
242+
.then((output) => {
243+
expect(output.exitCode).toEqual(0);
244+
done();
245+
})
246+
.catch(done);
247+
});
248+
249+
it('--open-app google-chrome', (done) => {
250+
testBin('--open-app google-chrome')
251+
.then((output) => {
252+
expect(output.exitCode).toEqual(0);
253+
done();
254+
})
255+
.catch(done);
256+
});
257+
258+
it('--open-target', (done) => {
259+
testBin('--open-target')
260+
.then((output) => {
261+
expect(output.exitCode).toEqual(0);
262+
done();
263+
})
264+
.catch(done);
265+
});
266+
267+
it('--open-target index.html', (done) => {
268+
testBin('--open-target index.html')
269+
.then((output) => {
270+
expect(output.exitCode).toEqual(0);
271+
done();
272+
})
273+
.catch(done);
274+
});
275+
276+
it('--open-target /first.html second.html', (done) => {
277+
testBin('--open-target /first.html second.html')
278+
.then((output) => {
279+
expect(output.exitCode).toEqual(0);
280+
done();
281+
})
282+
.catch(done);
283+
});
284+
285+
it('--open-target /index.html --open-app google-chrome', (done) => {
286+
testBin('--open-target /index.html --open-app google-chrome')
287+
.then((output) => {
288+
expect(output.exitCode).toEqual(0);
289+
done();
290+
})
291+
.catch(done);
292+
});
293+
222294
it('should log public path', (done) => {
223295
testBin(
224296
'--no-color',

0 commit comments

Comments
 (0)