|
1 | 1 | #!/usr/bin/env node
|
| 2 | +/* Based on webpack/bin/webpack.js */ |
| 3 | +/* eslint-disable no-console */ |
2 | 4 |
|
3 | 5 | 'use strict';
|
4 | 6 |
|
5 |
| -/* eslint-disable no-shadow, no-console */ |
6 |
| - |
7 |
| -const debug = require('debug')('webpack-dev-server'); |
8 |
| -const importLocal = require('import-local'); |
9 |
| -const yargs = require('yargs'); |
10 |
| -const webpack = require('webpack'); |
11 |
| -const Server = require('../lib/Server'); |
12 |
| -const setupExitSignals = require('../lib/utils/setupExitSignals'); |
13 |
| -const colors = require('../lib/utils/colors'); |
14 |
| -const processOptions = require('../lib/utils/processOptions'); |
15 |
| -const getVersions = require('../lib/utils/getVersions'); |
16 |
| -const getColorsOption = require('../lib/utils/getColorsOption'); |
17 |
| -const options = require('./options'); |
18 |
| - |
19 |
| -let server; |
20 |
| -const serverData = { |
21 |
| - server: null, |
| 7 | +/** |
| 8 | + * @param {string} command process to run |
| 9 | + * @param {string[]} args command line arguments |
| 10 | + * @returns {Promise<void>} promise |
| 11 | + */ |
| 12 | +const runCommand = (command, args) => { |
| 13 | + const cp = require('child_process'); |
| 14 | + return new Promise((resolve, reject) => { |
| 15 | + const executedCommand = cp.spawn(command, args, { |
| 16 | + stdio: 'inherit', |
| 17 | + shell: true, |
| 18 | + }); |
| 19 | + |
| 20 | + executedCommand.on('error', (error) => { |
| 21 | + reject(error); |
| 22 | + }); |
| 23 | + |
| 24 | + executedCommand.on('exit', (code) => { |
| 25 | + if (code === 0) { |
| 26 | + resolve(); |
| 27 | + } else { |
| 28 | + reject(); |
| 29 | + } |
| 30 | + }); |
| 31 | + }); |
22 | 32 | };
|
23 |
| -// we must pass an object that contains the server object as a property so that |
24 |
| -// we can update this server property later, and setupExitSignals will be able to |
25 |
| -// recognize that the server has been instantiated, because we will set |
26 |
| -// serverData.server to the new server object. |
27 |
| -setupExitSignals(serverData); |
28 | 33 |
|
29 |
| -// Prefer the local installation of webpack-dev-server |
30 |
| -if (importLocal(__filename)) { |
31 |
| - debug('Using local install of webpack-dev-server'); |
| 34 | +/** |
| 35 | + * @param {string} packageName name of the package |
| 36 | + * @returns {boolean} is the package installed? |
| 37 | + */ |
| 38 | +const isInstalled = (packageName) => { |
| 39 | + try { |
| 40 | + require.resolve(packageName); |
32 | 41 |
|
33 |
| - return; |
34 |
| -} |
| 42 | + return true; |
| 43 | + } catch (err) { |
| 44 | + return false; |
| 45 | + } |
| 46 | +}; |
35 | 47 |
|
36 |
| -try { |
37 |
| - require.resolve('webpack-cli'); |
38 |
| -} catch (err) { |
39 |
| - console.error('The CLI moved into a separate package: webpack-cli'); |
40 |
| - console.error( |
41 |
| - "Please install 'webpack-cli' in addition to webpack itself to use the CLI" |
42 |
| - ); |
43 |
| - console.error('-> When using npm: npm i -D webpack-cli'); |
44 |
| - console.error('-> When using yarn: yarn add -D webpack-cli'); |
| 48 | +/** |
| 49 | + * @param {CliOption} cli options |
| 50 | + * @returns {void} |
| 51 | + */ |
| 52 | +const runCli = (cli) => { |
| 53 | + if (cli.preprocess) { |
| 54 | + cli.preprocess(); |
| 55 | + } |
| 56 | + const path = require('path'); |
| 57 | + const pkgPath = require.resolve(`${cli.package}/package.json`); |
| 58 | + // eslint-disable-next-line import/no-dynamic-require |
| 59 | + const pkg = require(pkgPath); |
| 60 | + // eslint-disable-next-line import/no-dynamic-require |
| 61 | + require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName])); |
| 62 | +}; |
45 | 63 |
|
46 |
| - process.exitCode = 1; |
47 |
| -} |
| 64 | +/** |
| 65 | + * @typedef {Object} CliOption |
| 66 | + * @property {string} name display name |
| 67 | + * @property {string} package npm package name |
| 68 | + * @property {string} binName name of the executable file |
| 69 | + * @property {boolean} installed currently installed? |
| 70 | + * @property {string} url homepage |
| 71 | + * @property {function} preprocess preprocessor |
| 72 | + */ |
| 73 | + |
| 74 | +/** @type {CliOption} */ |
| 75 | +const cli = { |
| 76 | + name: 'webpack-cli', |
| 77 | + package: 'webpack-cli', |
| 78 | + binName: 'webpack-cli', |
| 79 | + installed: isInstalled('webpack-cli'), |
| 80 | + url: 'https://github.com/webpack/webpack-cli', |
| 81 | + preprocess() { |
| 82 | + process.argv.splice(2, 0, 'serve'); |
| 83 | + }, |
| 84 | +}; |
48 | 85 |
|
49 |
| -yargs.usage( |
50 |
| - `${getVersions()}\nUsage: https://webpack.js.org/configuration/dev-server/` |
51 |
| -); |
52 |
| - |
53 |
| -// [email protected] path : 'webpack-cli/bin/config/config-yargs' |
54 |
| -let configYargsPath; |
55 |
| -try { |
56 |
| - require.resolve('webpack-cli/bin/config/config-yargs'); |
57 |
| - configYargsPath = 'webpack-cli/bin/config/config-yargs'; |
58 |
| -} catch (e) { |
59 |
| - configYargsPath = 'webpack-cli/bin/config-yargs'; |
60 |
| -} |
61 |
| -// eslint-disable-next-line import/no-extraneous-dependencies |
62 |
| -// eslint-disable-next-line import/no-dynamic-require |
63 |
| -require(configYargsPath)(yargs); |
64 |
| - |
65 |
| -// It is important that this is done after the webpack yargs config, |
66 |
| -// so it overrides webpack's version info. |
67 |
| -yargs.version(getVersions()); |
68 |
| -yargs.options(options); |
69 |
| - |
70 |
| -const argv = yargs.argv; |
71 |
| - |
72 |
| -// [email protected] path : 'webpack-cli/bin/utils/convert-argv' |
73 |
| -let convertArgvPath; |
74 |
| -try { |
75 |
| - require.resolve('webpack-cli/bin/utils/convert-argv'); |
76 |
| - convertArgvPath = 'webpack-cli/bin/utils/convert-argv'; |
77 |
| -} catch (e) { |
78 |
| - convertArgvPath = 'webpack-cli/bin/convert-argv'; |
79 |
| -} |
80 |
| -// eslint-disable-next-line import/no-extraneous-dependencies |
81 |
| -// eslint-disable-next-line import/no-dynamic-require |
82 |
| -const config = require(convertArgvPath)(yargs, argv, { |
83 |
| - outputFilename: '/bundle.js', |
84 |
| -}); |
| 86 | +if (!cli.installed) { |
| 87 | + const path = require('path'); |
| 88 | + const fs = require('graceful-fs'); |
| 89 | + const readLine = require('readline'); |
85 | 90 |
|
86 |
| -function startDevServer(config, options) { |
87 |
| - let compiler; |
| 91 | + const notify = `CLI for webpack must be installed.\n ${cli.name} (${cli.url})\n`; |
88 | 92 |
|
89 |
| - const configArr = config instanceof Array ? config : [config]; |
90 |
| - const statsColors = getColorsOption(configArr); |
| 93 | + console.error(notify); |
91 | 94 |
|
92 |
| - try { |
93 |
| - compiler = webpack(config); |
94 |
| - } catch (err) { |
95 |
| - if (err instanceof webpack.WebpackOptionsValidationError) { |
96 |
| - console.error(colors.error(statsColors, err.message)); |
97 |
| - // eslint-disable-next-line no-process-exit |
98 |
| - process.exit(1); |
99 |
| - } |
| 95 | + const isYarn = fs.existsSync(path.resolve(process.cwd(), 'yarn.lock')); |
100 | 96 |
|
101 |
| - throw err; |
102 |
| - } |
| 97 | + const packageManager = isYarn ? 'yarn' : 'npm'; |
| 98 | + const installOptions = [isYarn ? 'add' : 'install', '-D']; |
103 | 99 |
|
104 |
| - try { |
105 |
| - server = new Server(compiler, options); |
106 |
| - serverData.server = server; |
107 |
| - } catch (err) { |
108 |
| - if (err.name === 'ValidationError') { |
109 |
| - console.error(colors.error(statsColors, err.message)); |
110 |
| - // eslint-disable-next-line no-process-exit |
111 |
| - process.exit(1); |
112 |
| - } |
| 100 | + console.error( |
| 101 | + `We will use "${packageManager}" to install the CLI via "${packageManager} ${installOptions.join( |
| 102 | + ' ' |
| 103 | + )}".` |
| 104 | + ); |
113 | 105 |
|
114 |
| - throw err; |
115 |
| - } |
| 106 | + const question = `Do you want to install 'webpack-cli' (yes/no): `; |
| 107 | + |
| 108 | + const questionInterface = readLine.createInterface({ |
| 109 | + input: process.stdin, |
| 110 | + output: process.stderr, |
| 111 | + }); |
| 112 | + |
| 113 | + // In certain scenarios (e.g. when STDIN is not in terminal mode), the callback function will not be |
| 114 | + // executed. Setting the exit code here to ensure the script exits correctly in those cases. The callback |
| 115 | + // function is responsible for clearing the exit code if the user wishes to install webpack-cli. |
| 116 | + process.exitCode = 1; |
| 117 | + questionInterface.question(question, (answer) => { |
| 118 | + questionInterface.close(); |
| 119 | + |
| 120 | + const normalizedAnswer = answer.toLowerCase().startsWith('y'); |
116 | 121 |
|
117 |
| - server.listen(options.port, options.host, (err) => { |
118 |
| - if (err) { |
119 |
| - throw err; |
| 122 | + if (!normalizedAnswer) { |
| 123 | + console.error( |
| 124 | + "You need to install 'webpack-cli' to use webpack via CLI.\n" + |
| 125 | + 'You can also install the CLI manually.' |
| 126 | + ); |
| 127 | + |
| 128 | + return; |
120 | 129 | }
|
| 130 | + process.exitCode = 0; |
| 131 | + |
| 132 | + console.log( |
| 133 | + `Installing '${ |
| 134 | + cli.package |
| 135 | + }' (running '${packageManager} ${installOptions.join(' ')} ${ |
| 136 | + cli.package |
| 137 | + }')...` |
| 138 | + ); |
| 139 | + |
| 140 | + runCommand(packageManager, installOptions.concat(cli.package)) |
| 141 | + .then(() => { |
| 142 | + runCli(cli); |
| 143 | + }) |
| 144 | + .catch((error) => { |
| 145 | + console.error(error); |
| 146 | + process.exitCode = 1; |
| 147 | + }); |
121 | 148 | });
|
| 149 | +} else { |
| 150 | + runCli(cli); |
122 | 151 | }
|
123 |
| - |
124 |
| -processOptions(config, argv, (config, options) => { |
125 |
| - startDevServer(config, options); |
126 |
| -}); |
|
0 commit comments