|
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 | + let packageManager; |
100 | 96 |
|
101 | | - throw err; |
| 97 | + if (fs.existsSync(path.resolve(process.cwd(), 'yarn.lock'))) { |
| 98 | + packageManager = 'yarn'; |
| 99 | + } else if (fs.existsSync(path.resolve(process.cwd(), 'pnpm-lock.yaml'))) { |
| 100 | + packageManager = 'pnpm'; |
| 101 | + } else { |
| 102 | + packageManager = 'npm'; |
102 | 103 | } |
103 | 104 |
|
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 | | - } |
| 105 | + const installOptions = [packageManager === 'yarn' ? 'add' : 'install', '-D']; |
113 | 106 |
|
114 | | - throw err; |
115 | | - } |
| 107 | + console.error( |
| 108 | + `We will use "${packageManager}" to install the CLI via "${packageManager} ${installOptions.join( |
| 109 | + ' ' |
| 110 | + )}".` |
| 111 | + ); |
116 | 112 |
|
117 | | - server.listen(options.port, options.host, (err) => { |
118 | | - if (err) { |
119 | | - throw err; |
| 113 | + const question = `Do you want to install 'webpack-cli' (yes/no): `; |
| 114 | + |
| 115 | + const questionInterface = readLine.createInterface({ |
| 116 | + input: process.stdin, |
| 117 | + output: process.stderr, |
| 118 | + }); |
| 119 | + |
| 120 | + // In certain scenarios (e.g. when STDIN is not in terminal mode), the callback function will not be |
| 121 | + // executed. Setting the exit code here to ensure the script exits correctly in those cases. The callback |
| 122 | + // function is responsible for clearing the exit code if the user wishes to install webpack-cli. |
| 123 | + process.exitCode = 1; |
| 124 | + questionInterface.question(question, (answer) => { |
| 125 | + questionInterface.close(); |
| 126 | + |
| 127 | + const normalizedAnswer = answer.toLowerCase().startsWith('y'); |
| 128 | + |
| 129 | + if (!normalizedAnswer) { |
| 130 | + console.error( |
| 131 | + "You need to install 'webpack-cli' to use webpack via CLI.\n" + |
| 132 | + 'You can also install the CLI manually.' |
| 133 | + ); |
| 134 | + |
| 135 | + return; |
120 | 136 | } |
| 137 | + process.exitCode = 0; |
| 138 | + |
| 139 | + console.log( |
| 140 | + `Installing '${ |
| 141 | + cli.package |
| 142 | + }' (running '${packageManager} ${installOptions.join(' ')} ${ |
| 143 | + cli.package |
| 144 | + }')...` |
| 145 | + ); |
| 146 | + |
| 147 | + runCommand(packageManager, installOptions.concat(cli.package)) |
| 148 | + .then(() => { |
| 149 | + runCli(cli); |
| 150 | + }) |
| 151 | + .catch((error) => { |
| 152 | + console.error(error); |
| 153 | + process.exitCode = 1; |
| 154 | + }); |
121 | 155 | }); |
| 156 | +} else { |
| 157 | + runCli(cli); |
122 | 158 | } |
123 | | - |
124 | | -processOptions(config, argv, (config, options) => { |
125 | | - startDevServer(config, options); |
126 | | -}); |
|
0 commit comments