webpack-cli
Advanced tools
Comparing version 4.2.0 to 4.3.0
@@ -11,4 +11,4 @@ #!/usr/bin/env node | ||
const { error, success } = require('../lib/utils/logger'); | ||
const { packageExists } = require('../lib/utils/package-exists'); | ||
const { promptInstallation } = require('../lib/utils/prompt-installation'); | ||
const packageExists = require('../lib/utils/package-exists'); | ||
const promptInstallation = require('../lib/utils/prompt-installation'); | ||
@@ -22,6 +22,4 @@ // Prefer the local installation of `webpack-cli` | ||
const [, , ...rawArgs] = process.argv; | ||
if (packageExists('webpack')) { | ||
runCLI(rawArgs); | ||
runCLI(process.argv); | ||
} else { | ||
@@ -32,5 +30,5 @@ promptInstallation('webpack -W', () => { | ||
.then(() => { | ||
success(`${yellow('webpack')} was installed sucessfully.`); | ||
success(`${yellow('webpack')} was installed successfully.`); | ||
runCLI(rawArgs); | ||
runCLI(process.argv); | ||
}) | ||
@@ -37,0 +35,0 @@ .catch(() => { |
@@ -6,2 +6,25 @@ # Change Log | ||
# [4.3.0](https://github.com/webpack/webpack-cli/compare/webpack-cli@4.2.0...webpack-cli@4.3.0) (2020-12-25) | ||
### Bug Fixes | ||
- fix problems with `--mode` and config resolution, there are situations when we resolve an invalid config file, the `--mode` option does not affect on config resolution, if you faced with an error after updating, please use the `--config` option | ||
- correct usage of cli-flags ([#2205](https://github.com/webpack/webpack-cli/issues/2205)) ([c8fc7d1](https://github.com/webpack/webpack-cli/commit/c8fc7d1f195800c4fbe54ed6533e694f40fa7a1b)) | ||
- defer setting default mode to core ([#2095](https://github.com/webpack/webpack-cli/issues/2095)) ([3eb410e](https://github.com/webpack/webpack-cli/commit/3eb410e5d8f8e2149910b65f4a028c85f8af5d28)) | ||
- respect the `--watch-options-stdin` option ([2d1e001](https://github.com/webpack/webpack-cli/commit/2d1e001e7f4f560c2b36607bd1b29dfe2aa32066)) | ||
- respect `--color`/`--no-color` option ([#2042](https://github.com/webpack/webpack-cli/issues/2042)) ([09bd812](https://github.com/webpack/webpack-cli/commit/09bd8126e95c9675b1f6862451f629cd4c439adb)) | ||
- stringify stats using streaming approach ([#2190](https://github.com/webpack/webpack-cli/issues/2190)) ([9bf4e92](https://github.com/webpack/webpack-cli/commit/9bf4e925757b02f7252073501562c95e762dc59b)) | ||
- use logger for error with proper exit code ([#2076](https://github.com/webpack/webpack-cli/issues/2076)) ([2c9069f](https://github.com/webpack/webpack-cli/commit/2c9069fd1f7c0fb70f019900e4b841c5ea33975e)) | ||
- reduce spammy logs ([#2206](https://github.com/webpack/webpack-cli/issues/2206)) ([9b3cc28](https://github.com/webpack/webpack-cli/commit/9b3cc283d7b74aa3bb26fe36c6110436b016e0d9)) | ||
- respect the `infrastructureLogging.level` option (logger uses `stderr`) ([#2144](https://github.com/webpack/webpack-cli/issues/2144)) ([7daccc7](https://github.com/webpack/webpack-cli/commit/7daccc786a0eb4eeae4c5b3632fc28240a696170)) | ||
- respect all options from command line for the `server` command | ||
- `help` and `version` output | ||
- respect `stats` from the config (webpack@4) ([#2098](https://github.com/webpack/webpack-cli/issues/2098)) ([2d6e5c6](https://github.com/webpack/webpack-cli/commit/2d6e5c6f4ed967368a81742bf347e39f24ee16c8)) | ||
- fixed colors work with multi compiler mode (webpack@4) | ||
### Features | ||
- add `bundle` command (alias for `webpack [options]`) | ||
- add `pnpm` support for package installation ([#2040](https://github.com/webpack/webpack-cli/issues/2040)) ([46cba36](https://github.com/webpack/webpack-cli/commit/46cba367f06a6354fe98fcb15e7771e819feeac0)) | ||
# [4.2.0](https://github.com/webpack/webpack-cli/compare/webpack-cli@4.1.0...webpack-cli@4.2.0) (2020-11-04) | ||
@@ -8,0 +31,0 @@ |
const WebpackCLI = require('./webpack-cli'); | ||
const { core } = require('./utils/cli-flags'); | ||
const logger = require('./utils/logger'); | ||
const { isCommandUsed } = require('./utils/arg-utils'); | ||
const argParser = require('./utils/arg-parser'); | ||
const leven = require('leven'); | ||
process.title = 'webpack-cli'; | ||
const runCLI = async (cliArgs) => { | ||
const parsedArgs = argParser(core, cliArgs, true, process.title); | ||
const commandIsUsed = isCommandUsed(cliArgs); | ||
if (commandIsUsed) { | ||
return; | ||
} | ||
const runCLI = async (args) => { | ||
try { | ||
@@ -22,40 +11,3 @@ // Create a new instance of the CLI object | ||
// Handle the default webpack entry CLI argument, where instead of doing 'webpack-cli --entry ./index.js' you can simply do 'webpack-cli ./index.js' | ||
// If the unknown arg starts with a '-', it will be considered an unknown flag rather than an entry | ||
let entry; | ||
if (parsedArgs.unknownArgs.length > 0) { | ||
entry = []; | ||
parsedArgs.unknownArgs = parsedArgs.unknownArgs.filter((item) => { | ||
if (item.startsWith('-')) { | ||
return true; | ||
} | ||
entry.push(item); | ||
return false; | ||
}); | ||
} | ||
if (parsedArgs.unknownArgs.length > 0) { | ||
parsedArgs.unknownArgs.forEach(async (unknown) => { | ||
logger.error(`Unknown argument: ${unknown}`); | ||
const strippedFlag = unknown.substr(2); | ||
const { name: suggestion } = core.find((flag) => leven(strippedFlag, flag.name) < 3); | ||
if (suggestion) { | ||
logger.raw(`Did you mean --${suggestion}?`); | ||
} | ||
}); | ||
process.exit(2); | ||
} | ||
const parsedArgsOpts = parsedArgs.opts; | ||
if (entry) { | ||
parsedArgsOpts.entry = entry; | ||
} | ||
await cli.run(parsedArgsOpts, core); | ||
await cli.run(args); | ||
} catch (error) { | ||
@@ -62,0 +14,0 @@ logger.error(error); |
@@ -14,3 +14,3 @@ const fs = require('fs'); | ||
jest.mock('cross-spawn'); | ||
jest.mock('../get-package-manager', () => jest.fn()); | ||
const globalModulesNpmValue = 'test-npm'; | ||
@@ -26,3 +26,8 @@ jest.setMock('global-modules', globalModulesNpmValue); | ||
const testNpmLockPath = path.resolve(__dirname, 'test-npm-lock'); | ||
const testBothPath = path.resolve(__dirname, 'test-both'); | ||
const testPnpmLockPath = path.resolve(__dirname, 'test-pnpm-lock'); | ||
const testNpmAndPnpmPath = path.resolve(__dirname, 'test-npm-and-pnpm'); | ||
const testNpmAndYarnPath = path.resolve(__dirname, 'test-npm-and-yarn'); | ||
const testYarnAndPnpmPath = path.resolve(__dirname, 'test-yarn-and-pnpm'); | ||
const testAllPath = path.resolve(__dirname, 'test-all-lock'); | ||
const noLockPath = path.resolve(__dirname, 'no-lock-files'); | ||
@@ -38,3 +43,5 @@ const cwdSpy = jest.spyOn(process, 'cwd'); | ||
fs.writeFileSync(path.resolve(testNpmLockPath, 'package-lock.json'), ''); | ||
fs.writeFileSync(path.resolve(testBothPath, 'package-lock.json'), ''); | ||
fs.writeFileSync(path.resolve(testNpmAndPnpmPath, 'package-lock.json'), ''); | ||
fs.writeFileSync(path.resolve(testNpmAndYarnPath, 'package-lock.json'), ''); | ||
fs.writeFileSync(path.resolve(testAllPath, 'package-lock.json'), ''); | ||
}); | ||
@@ -58,4 +65,22 @@ | ||
it('should prioritize yarn with many lock files', () => { | ||
cwdSpy.mockReturnValue(testBothPath); | ||
it('should find pnpm-lock.yaml', () => { | ||
cwdSpy.mockReturnValue(testPnpmLockPath); | ||
expect(getPackageManager()).toEqual('pnpm'); | ||
expect(syncMock.mock.calls.length).toEqual(0); | ||
}); | ||
it('should prioritize npm over pnpm', () => { | ||
cwdSpy.mockReturnValue(testNpmAndPnpmPath); | ||
expect(getPackageManager()).toEqual('npm'); | ||
expect(syncMock.mock.calls.length).toEqual(0); | ||
}); | ||
it('should prioritize npm over yarn', () => { | ||
cwdSpy.mockReturnValue(testNpmAndYarnPath); | ||
expect(getPackageManager()).toEqual('npm'); | ||
expect(syncMock.mock.calls.length).toEqual(0); | ||
}); | ||
it('should prioritize yarn over pnpm', () => { | ||
cwdSpy.mockReturnValue(testYarnAndPnpmPath); | ||
expect(getPackageManager()).toEqual('yarn'); | ||
@@ -65,19 +90,24 @@ expect(syncMock.mock.calls.length).toEqual(0); | ||
it('should use yarn if yarn command works', () => { | ||
// yarn should output a version number to stdout if | ||
// it is installed | ||
cwdSpy.mockReturnValue(path.resolve(__dirname)); | ||
expect(getPackageManager()).toEqual('yarn'); | ||
it('should prioritize npm with many lock files', () => { | ||
cwdSpy.mockReturnValue(testAllPath); | ||
expect(getPackageManager()).toEqual('npm'); | ||
expect(syncMock.mock.calls.length).toEqual(0); | ||
}); | ||
it('should prioritize global npm over other package managers', () => { | ||
cwdSpy.mockReturnValue(noLockPath); | ||
expect(getPackageManager()).toEqual('npm'); | ||
expect(syncMock.mock.calls.length).toEqual(1); | ||
}); | ||
it('should use npm if yarn command fails', () => { | ||
it('should throw error if no package manager is found', () => { | ||
syncMock.mockImplementation(() => { | ||
throw new Error(); | ||
}); | ||
cwdSpy.mockReturnValue(path.resolve(__dirname)); | ||
expect(getPackageManager()).toEqual('npm'); | ||
expect(syncMock.mock.calls.length).toEqual(1); | ||
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); | ||
expect(getPackageManager()).toBeFalsy(); | ||
expect(mockExit).toBeCalledWith(2); | ||
expect(syncMock.mock.calls.length).toEqual(3); // 3 calls for npm, yarn and pnpm | ||
}); | ||
}); | ||
}); |
'use strict'; | ||
jest.mock('execa'); | ||
jest.mock('cross-spawn'); | ||
// eslint-disable-next-line node/no-extraneous-require | ||
const stripAnsi = require('strip-ansi'); | ||
const globalModulesNpmValue = 'test-npm'; | ||
jest.setMock('global-modules', globalModulesNpmValue); | ||
jest.setMock('enquirer', { | ||
prompt: jest.fn(), | ||
}); | ||
jest.setMock('../run-command', { | ||
runCommand: jest.fn(), | ||
}); | ||
jest.setMock('../package-exists', { | ||
packageExists: jest.fn(), | ||
}); | ||
jest.setMock('enquirer', { prompt: jest.fn() }); | ||
jest.setMock('../run-command', jest.fn()); | ||
jest.setMock('../package-exists', jest.fn()); | ||
jest.setMock('../get-package-manager', jest.fn()); | ||
const getPackageManager = require('../get-package-manager'); | ||
const { packageExists } = require('../package-exists'); | ||
const { promptInstallation } = require('../prompt-installation'); | ||
const { runCommand } = require('../run-command'); | ||
const packageExists = require('../package-exists'); | ||
const promptInstallation = require('../prompt-installation'); | ||
const runCommand = require('../run-command'); | ||
const { prompt } = require('enquirer'); | ||
@@ -37,8 +29,9 @@ | ||
it('should prompt to install using npm if npm is package manager', async () => { | ||
prompt.mockReturnValue({ | ||
installConfirm: true, | ||
}); | ||
prompt.mockReturnValue({ installConfirm: true }); | ||
getPackageManager.mockReturnValue('npm'); | ||
const preMessage = jest.fn(); | ||
const promptResult = await promptInstallation('test-package', preMessage); | ||
expect(promptResult).toBeTruthy(); | ||
@@ -48,3 +41,6 @@ expect(preMessage.mock.calls.length).toEqual(1); | ||
expect(runCommand.mock.calls.length).toEqual(1); | ||
expect(prompt.mock.calls[0][0][0].message).toMatch(/Would you like to install test-package\?/); | ||
expect(stripAnsi(prompt.mock.calls[0][0][0].message)).toContain( | ||
"Would you like to install 'test-package' package? (That will run 'npm install -D test-package')", | ||
); | ||
// install the package using npm | ||
@@ -55,12 +51,15 @@ expect(runCommand.mock.calls[0][0]).toEqual('npm install -D test-package'); | ||
it('should prompt to install using yarn if yarn is package manager', async () => { | ||
prompt.mockReturnValue({ | ||
installConfirm: true, | ||
}); | ||
prompt.mockReturnValue({ installConfirm: true }); | ||
getPackageManager.mockReturnValue('yarn'); | ||
const promptResult = await promptInstallation('test-package'); | ||
expect(promptResult).toBeTruthy(); | ||
expect(prompt.mock.calls.length).toEqual(1); | ||
expect(runCommand.mock.calls.length).toEqual(1); | ||
expect(prompt.mock.calls[0][0][0].message).toMatch(/Would you like to install test-package\?/); | ||
expect(stripAnsi(prompt.mock.calls[0][0][0].message)).toContain( | ||
"Would you like to install 'test-package' package? (That will run 'yarn add -D test-package')", | ||
); | ||
// install the package using yarn | ||
@@ -70,8 +69,46 @@ expect(runCommand.mock.calls[0][0]).toEqual('yarn add -D test-package'); | ||
it('should prompt to install using pnpm if pnpm is package manager', async () => { | ||
prompt.mockReturnValue({ installConfirm: true }); | ||
getPackageManager.mockReturnValue('pnpm'); | ||
const promptResult = await promptInstallation('test-package'); | ||
expect(promptResult).toBeTruthy(); | ||
expect(prompt.mock.calls.length).toEqual(1); | ||
expect(runCommand.mock.calls.length).toEqual(1); | ||
expect(stripAnsi(prompt.mock.calls[0][0][0].message)).toContain( | ||
"Would you like to install 'test-package' package? (That will run 'pnpm install -D test-package')", | ||
); | ||
// install the package using npm | ||
expect(runCommand.mock.calls[0][0]).toEqual('pnpm install -D test-package'); | ||
}); | ||
it('should support pre message', async () => { | ||
prompt.mockReturnValue({ installConfirm: true }); | ||
getPackageManager.mockReturnValue('npm'); | ||
const preMessage = jest.fn(); | ||
const promptResult = await promptInstallation('test-package', preMessage); | ||
expect(promptResult).toBeTruthy(); | ||
expect(preMessage.mock.calls.length).toEqual(1); | ||
expect(prompt.mock.calls.length).toEqual(1); | ||
expect(runCommand.mock.calls.length).toEqual(1); | ||
expect(stripAnsi(prompt.mock.calls[0][0][0].message)).toContain( | ||
"Would you like to install 'test-package' package? (That will run 'npm install -D test-package')", | ||
); | ||
// install the package using npm | ||
expect(runCommand.mock.calls[0][0]).toEqual('npm install -D test-package'); | ||
}); | ||
it('should not install if install is not confirmed', async () => { | ||
prompt.mockReturnValue({ | ||
installConfirm: false, | ||
}); | ||
prompt.mockReturnValue({ installConfirm: false }); | ||
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); | ||
const promptResult = await promptInstallation('test-package'); | ||
expect(promptResult).toBeUndefined(); | ||
@@ -81,5 +118,6 @@ expect(prompt.mock.calls.length).toEqual(1); | ||
expect(runCommand.mock.calls.length).toEqual(0); | ||
expect(prompt.mock.calls[0][0][0].message).toMatch(/Would you like to install test-package\?/); | ||
expect(process.exitCode).toEqual(2); | ||
expect(mockExit.mock.calls[0][0]).toEqual(2); | ||
mockExit.mockRestore(); | ||
}); | ||
}); |
@@ -1,282 +0,198 @@ | ||
const { packageExists } = require('./package-exists'); | ||
const packageExists = require('./package-exists'); | ||
const cli = packageExists('webpack') ? require('webpack').cli : undefined; | ||
const HELP_GROUP = 'help'; | ||
const BASIC_GROUP = 'basic'; | ||
const minimumHelpFlags = [ | ||
'config', | ||
'config-name', | ||
'merge', | ||
'env', | ||
'mode', | ||
'watch', | ||
'watch-options-stdin', | ||
'stats', | ||
'devtool', | ||
'entry', | ||
'target', | ||
'progress', | ||
'json', | ||
'name', | ||
'output-path', | ||
]; | ||
const groups = { | ||
HELP_GROUP, | ||
BASIC_GROUP, | ||
}; | ||
const commands = [ | ||
const builtInFlags = [ | ||
// For configs | ||
{ | ||
name: 'init', | ||
name: 'config', | ||
usage: '--config <path-to-config> | --config <path-to-config> --config <path-to-config>', | ||
alias: 'c', | ||
type: String, | ||
usage: 'init [scaffold]', | ||
description: 'Initialize a new webpack configuration', | ||
multiple: true, | ||
description: 'Provide path to a webpack configuration file e.g. ./webpack.config.js.', | ||
}, | ||
{ | ||
name: 'migrate', | ||
alias: 'm', | ||
name: 'config-name', | ||
usage: '--config-name <name-of-config> | --config-name <name-of-config> --config-name <name-of-config>', | ||
type: String, | ||
usage: 'migrate', | ||
description: 'Migrate a configuration to a new version', | ||
multiple: true, | ||
description: 'Name of the configuration to use.', | ||
}, | ||
{ | ||
name: 'loader', | ||
scope: 'external', | ||
alias: 'l', | ||
type: String, | ||
usage: 'loader', | ||
description: 'Scaffold a loader repository', | ||
name: 'merge', | ||
usage: '--config <first-config> --config <second-config> --merge', | ||
alias: 'm', | ||
type: Boolean, | ||
description: "Merge two or more configurations using 'webpack-merge'.", | ||
}, | ||
// Complex configs | ||
{ | ||
name: 'plugin', | ||
alias: 'p', | ||
scope: 'external', | ||
name: 'env', | ||
usage: '--env <variable> | --env <variable> --env <variable=value>', | ||
type: String, | ||
usage: 'plugin', | ||
description: 'Scaffold a plugin repository', | ||
multipleType: true, | ||
description: 'Environment passed to the configuration when it is a function.', | ||
}, | ||
{ | ||
name: 'info', | ||
scope: 'external', | ||
alias: 'i', | ||
type: String, | ||
usage: 'info [options]', | ||
description: 'Outputs information about your system and dependencies', | ||
flags: [ | ||
{ | ||
name: 'output', | ||
type: String, | ||
description: 'To get the output in specified format ( accept json or markdown )', | ||
}, | ||
{ | ||
name: 'version', | ||
type: Boolean, | ||
description: 'Print version information of info package', | ||
}, | ||
], | ||
}, | ||
{ | ||
name: 'serve', | ||
alias: 's', | ||
scope: 'external', | ||
type: String, | ||
usage: 'serve', | ||
description: 'Run the webpack Dev Server', | ||
}, | ||
]; | ||
const core = [ | ||
// Adding more plugins | ||
{ | ||
name: 'entry', | ||
usage: '--entry <path to entry file> | --entry <path> --entry <path>', | ||
type: String, | ||
multiple: true, | ||
group: BASIC_GROUP, | ||
description: 'The entry point(s) of your application e.g. ./src/main.js', | ||
link: 'https://webpack.js.org/concepts/#entry', | ||
}, | ||
{ | ||
name: 'config', | ||
usage: '--config <path to webpack configuration file>', | ||
alias: 'c', | ||
type: String, | ||
multiple: true, | ||
description: 'Provide path to a webpack configuration file e.g. ./webpack.config.js', | ||
link: 'https://webpack.js.org/configuration/', | ||
}, | ||
{ | ||
name: 'color', | ||
usage: '--color', | ||
name: 'hot', | ||
usage: '--hot', | ||
alias: 'h', | ||
type: Boolean, | ||
negative: true, | ||
defaultValue: true, | ||
description: 'Enable/Disable colors on console', | ||
description: 'Enables Hot Module Replacement', | ||
negatedDescription: 'Disables Hot Module Replacement.', | ||
}, | ||
{ | ||
name: 'merge', | ||
usage: '--merge', | ||
alias: 'm', | ||
name: 'analyze', | ||
usage: '--analyze', | ||
type: Boolean, | ||
description: 'Merge two or more configurations using webpack-merge e.g. -c ./webpack.config.js -c ./webpack.test.config.js --merge', | ||
link: 'https://github.com/survivejs/webpack-merge', | ||
multiple: false, | ||
description: 'It invokes webpack-bundle-analyzer plugin to get bundle information.', | ||
}, | ||
{ | ||
name: 'progress', | ||
usage: '--progress', | ||
usage: '--progress | --progress profile', | ||
type: [Boolean, String], | ||
group: BASIC_GROUP, | ||
description: 'Print compilation progress during build', | ||
description: 'Print compilation progress during build.', | ||
}, | ||
{ | ||
name: 'help', | ||
usage: '--help', | ||
type: Boolean, | ||
group: HELP_GROUP, | ||
description: 'Outputs list of supported flags', | ||
name: 'prefetch', | ||
usage: '--prefetch <request>', | ||
type: String, | ||
description: 'Prefetch this request.', | ||
}, | ||
// Output options | ||
{ | ||
name: 'json', | ||
usage: '--json | --json <path-to-stats-file>', | ||
type: [String, Boolean], | ||
alias: 'j', | ||
description: 'Prints result as JSON or store it in a file.', | ||
}, | ||
// For webpack@4 | ||
{ | ||
name: 'entry', | ||
usage: '--entry <path-to-entry-file> | --entry <path> --entry <path>', | ||
type: String, | ||
multiple: true, | ||
description: 'The entry point(s) of your application e.g. ./src/main.js.', | ||
}, | ||
{ | ||
name: 'output-path', | ||
usage: '--output-path <path to output directory>', | ||
usage: '--output-path <path-to-output-directory>', | ||
alias: 'o', | ||
type: String, | ||
description: 'Output location of the file generated by webpack e.g. ./dist/', | ||
link: 'https://webpack.js.org/concepts/#output', | ||
description: 'Output location of the file generated by webpack e.g. ./dist/.', | ||
}, | ||
{ | ||
name: 'target', | ||
usage: '--target <value>', | ||
usage: '--target <value> | --target <value> --target <value>', | ||
alias: 't', | ||
type: String, | ||
multiple: cli !== undefined, | ||
description: 'Sets the build target e.g. node', | ||
link: 'https://webpack.js.org/configuration/target/#target', | ||
description: 'Sets the build target e.g. node.', | ||
}, | ||
{ | ||
name: 'watch', | ||
usage: '--watch', | ||
type: Boolean, | ||
alias: 'w', | ||
group: BASIC_GROUP, | ||
description: 'Watch for files changes', | ||
link: 'https://webpack.js.org/configuration/watch/', | ||
}, | ||
{ | ||
name: 'hot', | ||
usage: '--hot', | ||
alias: 'h', | ||
type: Boolean, | ||
negative: true, | ||
description: 'Enables Hot Module Replacement', | ||
link: 'https://webpack.js.org/concepts/hot-module-replacement/', | ||
}, | ||
{ | ||
name: 'devtool', | ||
usage: '--devtool <value>', | ||
type: String, | ||
negative: true, | ||
alias: 'd', | ||
defaultValue: undefined, | ||
group: BASIC_GROUP, | ||
description: 'Determine source maps to use', | ||
link: 'https://webpack.js.org/configuration/devtool/#devtool', | ||
description: 'Determine source maps to use.', | ||
negatedDescription: 'Do not generate source maps.', | ||
}, | ||
{ | ||
name: 'prefetch', | ||
usage: '--prefetch <request>', | ||
type: String, | ||
description: 'Prefetch this request', | ||
link: 'https://webpack.js.org/plugins/prefetch-plugin/', | ||
}, | ||
{ | ||
name: 'json', | ||
usage: '--json', | ||
type: [String, Boolean], | ||
alias: 'j', | ||
description: 'Prints result as JSON or store it in a file', | ||
}, | ||
{ | ||
name: 'mode', | ||
usage: '--mode <development | production | none>', | ||
type: String, | ||
description: 'Defines the mode to pass to webpack', | ||
link: 'https://webpack.js.org/concepts/#mode', | ||
description: 'Defines the mode to pass to webpack.', | ||
}, | ||
{ | ||
name: 'version', | ||
usage: '--version | --version <external-package>', | ||
alias: 'v', | ||
type: Boolean, | ||
group: HELP_GROUP, | ||
description: 'Get current version', | ||
name: 'name', | ||
usage: '--name', | ||
type: String, | ||
description: 'Name of the configuration. Used when loading multiple configurations.', | ||
}, | ||
{ | ||
name: 'stats', | ||
usage: '--stats <value>', | ||
usage: '--stats | --stats <value>', | ||
type: [String, Boolean], | ||
negative: true, | ||
description: 'It instructs webpack on how to treat the stats e.g. verbose', | ||
link: 'https://webpack.js.org/configuration/stats/#stats', | ||
description: 'It instructs webpack on how to treat the stats e.g. verbose.', | ||
negatedDescription: 'Disable stats output.', | ||
}, | ||
{ | ||
name: 'env', | ||
usage: '--env', | ||
type: String, | ||
multipleType: true, | ||
description: 'Environment passed to the configuration when it is a function', | ||
name: 'watch', | ||
usage: '--watch', | ||
type: Boolean, | ||
negative: true, | ||
alias: 'w', | ||
description: 'Watch for files changes.', | ||
negatedDescription: 'Do not watch for file changes.', | ||
}, | ||
{ | ||
name: 'name', | ||
usage: '--name', | ||
type: String, | ||
group: BASIC_GROUP, | ||
description: 'Name of the configuration. Used when loading multiple configurations.', | ||
}, | ||
{ | ||
name: 'config-name', | ||
usage: '--config-name <name of config>', | ||
type: String, | ||
multiple: true, | ||
description: 'Name of the configuration to use', | ||
}, | ||
{ | ||
name: 'analyze', | ||
usage: '--analyze', | ||
name: 'watch-options-stdin', | ||
usage: '--watch-options-stdin', | ||
type: Boolean, | ||
multiple: false, | ||
description: 'It invokes webpack-bundle-analyzer plugin to get bundle information', | ||
negative: true, | ||
description: 'Stop watching when stdin stream has ended.', | ||
}, | ||
/* { | ||
name: "interactive", | ||
type: Boolean, | ||
alias: "i", | ||
description: "Use webpack interactively", | ||
group: BASIC_GROUP | ||
} */ | ||
]; | ||
// Extract all the flags being exported from core. A list of cli flags generated by core | ||
// can be found here https://github.com/webpack/webpack/blob/master/test/__snapshots__/Cli.test.js.snap | ||
let flagsFromCore = | ||
cli !== undefined | ||
? Object.entries(cli.getArguments()).map(([flag, meta]) => { | ||
if (meta.simpleType === 'string') { | ||
meta.type = String; | ||
meta.usage = `--${flag} <value>`; | ||
} else if (meta.simpleType === 'number') { | ||
meta.type = Number; | ||
meta.usage = `--${flag} <value>`; | ||
} else { | ||
meta.type = Boolean; | ||
meta.negative = !flag.endsWith('-reset'); | ||
meta.usage = `--${flag}`; | ||
} | ||
return { | ||
...meta, | ||
name: flag, | ||
group: 'core', | ||
}; | ||
}) | ||
: []; | ||
// Extract all the flags being exported from core. | ||
// A list of cli flags generated by core can be found here https://github.com/webpack/webpack/blob/master/test/__snapshots__/Cli.test.js.snap | ||
const coreFlags = cli | ||
? Object.entries(cli.getArguments()).map(([flag, meta]) => { | ||
if (meta.simpleType === 'string') { | ||
meta.type = String; | ||
meta.usage = `--${flag} <value>`; | ||
} else if (meta.simpleType === 'number') { | ||
meta.type = Number; | ||
meta.usage = `--${flag} <value>`; | ||
} else { | ||
meta.type = Boolean; | ||
meta.negative = !flag.endsWith('-reset'); | ||
meta.usage = `--${flag}`; | ||
} | ||
// duplicate flags | ||
const duplicateFlags = core.map((flag) => flag.name); | ||
const inBuiltIn = builtInFlags.find((builtInFlag) => builtInFlag.name === flag); | ||
// remove duplicate flags | ||
flagsFromCore = flagsFromCore.filter((flag) => !duplicateFlags.includes(flag.name)); | ||
if (inBuiltIn) { | ||
return { ...meta, name: flag, group: 'core', ...inBuiltIn }; | ||
} | ||
const coreFlagMap = flagsFromCore.reduce((acc, cur) => { | ||
acc.set(cur.name, cur); | ||
return acc; | ||
}, new Map()); | ||
return { ...meta, name: flag, group: 'core' }; | ||
}) | ||
: []; | ||
const flags = [] | ||
.concat(builtInFlags.filter((builtInFlag) => !coreFlags.find((coreFlag) => builtInFlag.name === coreFlag.name))) | ||
.concat(coreFlags) | ||
.map((option) => { | ||
option.help = minimumHelpFlags.includes(option.name) ? 'minimum' : 'verbose'; | ||
module.exports = { | ||
groups, | ||
commands, | ||
core: [...core, ...flagsFromCore], | ||
flagsFromCore, | ||
coreFlagMap, | ||
}; | ||
return option; | ||
}); | ||
module.exports = { cli, flags }; |
const fs = require('fs'); | ||
const path = require('path'); | ||
const logger = require('./logger'); | ||
const { sync } = require('execa'); | ||
@@ -8,3 +9,3 @@ | ||
* Returns the name of package manager to use, | ||
* preferring yarn over npm if available | ||
* preference order - npm > yarn > pnpm | ||
* | ||
@@ -14,12 +15,33 @@ * @returns {String} - The package manager name | ||
function getPackageManager() { | ||
const hasLocalNpm = fs.existsSync(path.resolve(process.cwd(), 'package-lock.json')); | ||
if (hasLocalNpm) { | ||
return 'npm'; | ||
} | ||
const hasLocalYarn = fs.existsSync(path.resolve(process.cwd(), 'yarn.lock')); | ||
const hasLocalNpm = fs.existsSync(path.resolve(process.cwd(), 'package-lock.json')); | ||
if (hasLocalYarn) { | ||
return 'yarn'; | ||
} else if (hasLocalNpm) { | ||
return 'npm'; | ||
} | ||
const hasLocalPnpm = fs.existsSync(path.resolve(process.cwd(), 'pnpm-lock.yaml')); | ||
if (hasLocalPnpm) { | ||
return 'pnpm'; | ||
} | ||
try { | ||
// if the sync function below fails because yarn is not installed, | ||
// the sync function below will fail if npm is not installed, | ||
// an error will be thrown | ||
if (sync('npm', ['--version'])) { | ||
return 'npm'; | ||
} | ||
} catch (e) { | ||
// Nothing | ||
} | ||
try { | ||
// the sync function below will fail if yarn is not installed, | ||
// an error will be thrown | ||
if (sync('yarn', ['--version'])) { | ||
@@ -32,5 +54,14 @@ return 'yarn'; | ||
return 'npm'; | ||
try { | ||
// the sync function below will fail if pnpm is not installed, | ||
// an error will be thrown | ||
if (sync('pnpm', ['--version'])) { | ||
return 'pnpm'; | ||
} | ||
} catch (e) { | ||
logger.error('No package manager found.'); | ||
process.exit(2); | ||
} | ||
} | ||
module.exports = getPackageManager; |
@@ -1,6 +0,5 @@ | ||
function packageExists(packageName) { | ||
function getPkg(packageName) { | ||
try { | ||
require(packageName); | ||
return true; | ||
} catch (err) { | ||
return require.resolve(packageName); | ||
} catch (error) { | ||
return false; | ||
@@ -10,4 +9,2 @@ } | ||
module.exports = { | ||
packageExists, | ||
}; | ||
module.exports = getPkg; |
const { prompt } = require('enquirer'); | ||
const { green } = require('colorette'); | ||
const { runCommand } = require('./run-command'); | ||
const runCommand = require('./run-command'); | ||
const getPackageManager = require('./get-package-manager'); | ||
const { packageExists } = require('./package-exists'); | ||
const packageExists = require('./package-exists'); | ||
const logger = require('./logger'); | ||
@@ -14,27 +15,48 @@ /** | ||
const packageManager = getPackageManager(); | ||
if (!packageManager) { | ||
logger.error("Can't find package manager"); | ||
process.exit(2); | ||
} | ||
// yarn uses 'add' command, rest npm and pnpm both use 'install' | ||
const options = [packageManager === 'yarn' ? 'add' : 'install', '-D', packageName]; | ||
const commandToBeRun = `${packageManager} ${options.join(' ')}`; | ||
if (preMessage) { | ||
preMessage(); | ||
} | ||
const question = `Would you like to install ${packageName}? (That will run ${green(commandToBeRun)})`; | ||
const { installConfirm } = await prompt([ | ||
{ | ||
type: 'confirm', | ||
name: 'installConfirm', | ||
message: question, | ||
initial: 'Y', | ||
}, | ||
]); | ||
let installConfirm; | ||
try { | ||
({ installConfirm } = await prompt([ | ||
{ | ||
type: 'confirm', | ||
name: 'installConfirm', | ||
message: `Would you like to install '${packageName}' package? (That will run '${green(commandToBeRun)}')`, | ||
initial: 'Y', | ||
stdout: process.stderr, | ||
}, | ||
])); | ||
} catch (error) { | ||
logger.error(error); | ||
process.exit(2); | ||
} | ||
if (installConfirm) { | ||
await runCommand(commandToBeRun); | ||
try { | ||
await runCommand(commandToBeRun); | ||
} catch (error) { | ||
logger.error(error); | ||
process.exit(2); | ||
} | ||
return packageExists(packageName); | ||
} | ||
process.exitCode = 2; | ||
process.exit(2); | ||
} | ||
module.exports = { | ||
promptInstallation, | ||
}; | ||
module.exports = promptInstallation; |
const execa = require('execa'); | ||
const logger = require('./logger'); | ||
async function runCommand(command, args = []) { | ||
try { | ||
await execa(command, args, { | ||
stdio: 'inherit', | ||
shell: true, | ||
}); | ||
} catch (e) { | ||
throw new Error(e); | ||
await execa(command, args, { stdio: 'inherit', shell: true }); | ||
} catch (error) { | ||
logger.error(error.message); | ||
process.exit(2); | ||
} | ||
} | ||
module.exports = { | ||
runCommand, | ||
}; | ||
module.exports = runCommand; |
@@ -1,221 +0,1130 @@ | ||
const { packageExists } = require('./utils/package-exists'); | ||
const webpack = packageExists('webpack') ? require('webpack') : undefined; | ||
const logger = require('./utils/logger'); | ||
const path = require('path'); | ||
const { program } = require('commander'); | ||
const getPkg = require('./utils/package-exists'); | ||
const webpack = getPkg('webpack') ? require('webpack') : undefined; | ||
const webpackMerge = require('webpack-merge'); | ||
const { core, coreFlagMap } = require('./utils/cli-flags'); | ||
const argParser = require('./utils/arg-parser'); | ||
const { outputStrategy } = require('./utils/merge-strategies'); | ||
const assignFlagDefaults = require('./utils/flag-defaults'); | ||
const { writeFileSync } = require('fs'); | ||
const { options: coloretteOptions } = require('colorette'); | ||
const WebpackCLIPlugin = require('./plugins/WebpackCLIPlugin'); | ||
const { extensions, jsVariants } = require('interpret'); | ||
const rechoir = require('rechoir'); | ||
const { createWriteStream, existsSync } = require('fs'); | ||
const { distance } = require('fastest-levenshtein'); | ||
const { options: coloretteOptions, yellow, cyan, green, bold } = require('colorette'); | ||
const { stringifyStream: createJsonStringifyStream } = require('@discoveryjs/json-ext'); | ||
// CLI arg resolvers | ||
const handleConfigResolution = require('./groups/resolveConfig'); | ||
const resolveMode = require('./groups/resolveMode'); | ||
const resolveStats = require('./groups/resolveStats'); | ||
const resolveOutput = require('./groups/resolveOutput'); | ||
const basicResolver = require('./groups/basicResolver'); | ||
const resolveAdvanced = require('./groups/resolveAdvanced'); | ||
const { toKebabCase } = require('./utils/helpers'); | ||
const logger = require('./utils/logger'); | ||
const { cli, flags } = require('./utils/cli-flags'); | ||
const CLIPlugin = require('./plugins/CLIPlugin'); | ||
const promptInstallation = require('./utils/prompt-installation'); | ||
const toKebabCase = require('./utils/to-kebab-case'); | ||
const { resolve, extname } = path; | ||
class WebpackCLI { | ||
constructor() { | ||
this.compilerConfiguration = {}; | ||
this.outputConfiguration = {}; | ||
this.logger = logger; | ||
// Initialize program | ||
this.program = program; | ||
this.program.name('webpack'); | ||
this.program.storeOptionsAsProperties(false); | ||
this.utils = { toKebabCase, getPkg, promptInstallation }; | ||
} | ||
/** | ||
* Responsible for handling flags coming from webpack/webpack | ||
* @private\ | ||
* @returns {void} | ||
*/ | ||
_handleCoreFlags(parsedArgs) { | ||
const coreCliHelper = require('webpack').cli; | ||
if (!coreCliHelper) return; | ||
const coreConfig = Object.keys(parsedArgs) | ||
.filter((arg) => { | ||
return coreFlagMap.has(toKebabCase(arg)); | ||
}) | ||
.reduce((acc, cur) => { | ||
acc[toKebabCase(cur)] = parsedArgs[cur]; | ||
return acc; | ||
}, {}); | ||
const coreCliArgs = coreCliHelper.getArguments(); | ||
// Merge the core flag config with the compilerConfiguration | ||
coreCliHelper.processArguments(coreCliArgs, this.compilerConfiguration, coreConfig); | ||
// Assign some defaults to core flags | ||
const configWithDefaults = assignFlagDefaults(this.compilerConfiguration, parsedArgs, this.outputConfiguration); | ||
this._mergeOptionsToConfiguration(configWithDefaults); | ||
} | ||
makeCommand(commandOptions, optionsForCommand = [], action) { | ||
const command = program.command(commandOptions.name, { | ||
noHelp: commandOptions.noHelp, | ||
hidden: commandOptions.hidden, | ||
isDefault: commandOptions.isDefault, | ||
}); | ||
async _baseResolver(cb, parsedArgs, strategy) { | ||
const resolvedConfig = await cb(parsedArgs, this.compilerConfiguration); | ||
this._mergeOptionsToConfiguration(resolvedConfig.options, strategy); | ||
this._mergeOptionsToOutputConfiguration(resolvedConfig.outputOptions); | ||
if (commandOptions.description) { | ||
command.description(commandOptions.description); | ||
} | ||
if (commandOptions.usage) { | ||
command.usage(commandOptions.usage); | ||
} | ||
if (Array.isArray(commandOptions.alias)) { | ||
command.aliases(commandOptions.alias); | ||
} else { | ||
command.alias(commandOptions.alias); | ||
} | ||
if (commandOptions.pkg) { | ||
command.pkg = commandOptions.pkg; | ||
} else { | ||
command.pkg = 'webpack-cli'; | ||
} | ||
if (optionsForCommand.length > 0) { | ||
optionsForCommand.forEach((optionForCommand) => { | ||
this.makeOption(command, optionForCommand); | ||
}); | ||
} | ||
command.action(action); | ||
return command; | ||
} | ||
/** | ||
* Expose commander argParser | ||
* @param {...any} args args for argParser | ||
*/ | ||
argParser(...args) { | ||
return argParser(...args); | ||
// TODO refactor this terrible stuff | ||
makeOption(command, option) { | ||
let optionType = option.type; | ||
let isStringOrBool = false; | ||
if (Array.isArray(optionType)) { | ||
// filter out duplicate types | ||
optionType = optionType.filter((type, index) => { | ||
return optionType.indexOf(type) === index; | ||
}); | ||
// the only multi type currently supported is String and Boolean, | ||
// if there is a case where a different multi type is needed it | ||
// must be added here | ||
if (optionType.length === 0) { | ||
// if no type is provided in the array fall back to Boolean | ||
optionType = Boolean; | ||
} else if (optionType.length === 1 || optionType.length > 2) { | ||
// treat arrays with 1 or > 2 args as a single type | ||
optionType = optionType[0]; | ||
} else { | ||
// only String and Boolean multi type is supported | ||
if (optionType.includes(Boolean) && optionType.includes(String)) { | ||
isStringOrBool = true; | ||
} else { | ||
optionType = optionType[0]; | ||
} | ||
} | ||
} | ||
const flags = option.alias ? `-${option.alias}, --${option.name}` : `--${option.name}`; | ||
let flagsWithType = flags; | ||
if (isStringOrBool) { | ||
// commander recognizes [value] as an optional placeholder, | ||
// making this flag work either as a string or a boolean | ||
flagsWithType = `${flags} [value]`; | ||
} else if (optionType !== Boolean) { | ||
// <value> is a required placeholder for any non-Boolean types | ||
flagsWithType = `${flags} <value>`; | ||
} | ||
if (isStringOrBool || optionType === Boolean || optionType === String) { | ||
if (option.multiple) { | ||
// a multiple argument parsing function | ||
const multiArg = (value, previous = []) => previous.concat([value]); | ||
command.option(flagsWithType, option.description, multiArg, option.defaultValue); | ||
} else if (option.multipleType) { | ||
// for options which accept multiple types like env | ||
// so you can do `--env platform=staging --env production` | ||
// { platform: "staging", production: true } | ||
const multiArg = (value, previous = {}) => { | ||
// this ensures we're only splitting by the first `=` | ||
const [allKeys, val] = value.split(/=(.+)/, 2); | ||
const splitKeys = allKeys.split(/\.(?!$)/); | ||
let prevRef = previous; | ||
splitKeys.forEach((someKey, index) => { | ||
if (!prevRef[someKey]) { | ||
prevRef[someKey] = {}; | ||
} | ||
if (typeof prevRef[someKey] === 'string') { | ||
prevRef[someKey] = {}; | ||
} | ||
if (index === splitKeys.length - 1) { | ||
prevRef[someKey] = val || true; | ||
} | ||
prevRef = prevRef[someKey]; | ||
}); | ||
return previous; | ||
}; | ||
command.option(flagsWithType, option.description, multiArg, option.defaultValue); | ||
} else { | ||
// Prevent default behavior for standalone options | ||
command.option(flagsWithType, option.description, option.defaultValue); | ||
} | ||
} else if (optionType === Number) { | ||
// this will parse the flag as a number | ||
command.option(flagsWithType, option.description, Number, option.defaultValue); | ||
} else { | ||
// in this case the type is a parsing function | ||
command.option(flagsWithType, option.description, optionType, option.defaultValue); | ||
} | ||
if (option.negative) { | ||
// commander requires explicitly adding the negated version of boolean flags | ||
const negatedFlag = `--no-${option.name}`; | ||
command.option(negatedFlag, option.negatedDescription ? option.negatedDescription : `Negative '${option.name}' option.`); | ||
} | ||
} | ||
getCoreFlags() { | ||
return core; | ||
getBuiltInOptions() { | ||
return flags; | ||
} | ||
/** | ||
* Responsible to override webpack options. | ||
* @param {Object} options The options returned by a group helper | ||
* @param {Object} strategy The strategy to pass to webpack-merge. The strategy | ||
* is implemented inside the group helper | ||
* @private | ||
* @returns {void} | ||
*/ | ||
_mergeOptionsToConfiguration(options, strategy) { | ||
/** | ||
* options where they differ per config use this method to apply relevant option to relevant config | ||
* eg mode flag applies per config | ||
*/ | ||
if (Array.isArray(options) && Array.isArray(this.compilerConfiguration)) { | ||
this.compilerConfiguration = options.map((option, index) => { | ||
const compilerConfig = this.compilerConfiguration[index]; | ||
if (strategy) { | ||
return webpackMerge.strategy(strategy)(compilerConfig, option); | ||
async run(args) { | ||
// Built-in internal commands | ||
const bundleCommandOptions = { | ||
name: 'bundle', | ||
alias: 'b', | ||
description: 'Run webpack (default command, can be omitted).', | ||
usage: '[options]', | ||
}; | ||
const versionCommandOptions = { | ||
name: 'version', | ||
alias: 'v', | ||
description: "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.", | ||
usage: '[commands...]', | ||
}; | ||
const helpCommandOptions = { | ||
name: 'help', | ||
alias: 'h', | ||
description: 'Display help for commands and options.', | ||
usage: '[command]', | ||
}; | ||
// Built-in external commands | ||
const externalBuiltInCommandsInfo = [ | ||
{ | ||
name: 'serve', | ||
alias: 's', | ||
pkg: '@webpack-cli/serve', | ||
}, | ||
{ | ||
name: 'info', | ||
alias: 'i', | ||
pkg: '@webpack-cli/info', | ||
}, | ||
{ | ||
name: 'init', | ||
alias: 'c', | ||
pkg: '@webpack-cli/init', | ||
}, | ||
{ | ||
name: 'loader', | ||
alias: 'l', | ||
pkg: '@webpack-cli/generators', | ||
}, | ||
{ | ||
name: 'plugin', | ||
alias: 'p', | ||
pkg: '@webpack-cli/generators', | ||
}, | ||
{ | ||
name: 'migrate', | ||
alias: 'm', | ||
pkg: '@webpack-cli/migrate', | ||
}, | ||
]; | ||
const getCommandNameAndOptions = (args) => { | ||
let commandName; | ||
const options = []; | ||
let allowToSearchCommand = true; | ||
args.forEach((arg) => { | ||
if (!arg.startsWith('-') && allowToSearchCommand) { | ||
commandName = arg; | ||
allowToSearchCommand = false; | ||
return; | ||
} | ||
return webpackMerge(compilerConfig, option); | ||
allowToSearchCommand = false; | ||
options.push(arg); | ||
}); | ||
return; | ||
} | ||
/** | ||
* options is an array (multiple configuration) so we create a new | ||
* configuration where each element is individually merged | ||
*/ | ||
if (Array.isArray(options)) { | ||
this.compilerConfiguration = options.map((configuration) => { | ||
if (strategy) { | ||
return webpackMerge.strategy(strategy)(this.compilerConfiguration, configuration); | ||
const isDefault = typeof commandName === 'undefined'; | ||
return { commandName: isDefault ? bundleCommandOptions.name : commandName, options, isDefault }; | ||
}; | ||
const loadCommandByName = async (commandName, allowToInstall = false) => { | ||
if (commandName === bundleCommandOptions.name || commandName === bundleCommandOptions.alias) { | ||
// Make `bundle|b [options]` command | ||
this.makeCommand(bundleCommandOptions, this.getBuiltInOptions(), async (program) => { | ||
const options = program.opts(); | ||
if (typeof colorFromArguments !== 'undefined') { | ||
options.color = colorFromArguments; | ||
} | ||
if (program.args.length > 0) { | ||
const possibleCommands = [].concat([bundleCommandOptions.name]).concat(program.args); | ||
logger.error('Running multiple commands at the same time is not possible'); | ||
logger.error(`Found commands: ${possibleCommands.map((item) => `'${item}'`).join(', ')}`); | ||
logger.error("Run 'webpack --help' to see available commands and options"); | ||
process.exit(2); | ||
} | ||
await this.bundleCommand(options); | ||
}); | ||
} else if (commandName === helpCommandOptions.name || commandName === helpCommandOptions.alias) { | ||
this.makeCommand( | ||
{ | ||
name: 'help [command]', | ||
alias: 'h', | ||
description: 'Display help for commands and options', | ||
usage: '[command]', | ||
}, | ||
[], | ||
// Stub for the `help` command | ||
() => {}, | ||
); | ||
} else if (commandName === versionCommandOptions.name || commandName === helpCommandOptions.alias) { | ||
this.makeCommand( | ||
{ | ||
name: 'version [commands...]', | ||
alias: 'v', | ||
description: "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands", | ||
usage: '[commands...]', | ||
}, | ||
[], | ||
// Stub for the `help` command | ||
() => {}, | ||
); | ||
} else { | ||
const builtInExternalCommandInfo = externalBuiltInCommandsInfo.find( | ||
(externalBuiltInCommandInfo) => | ||
externalBuiltInCommandInfo.name === commandName || externalBuiltInCommandInfo.alias === commandName, | ||
); | ||
let pkg; | ||
if (builtInExternalCommandInfo) { | ||
({ pkg } = builtInExternalCommandInfo); | ||
} else { | ||
pkg = commandName; | ||
} | ||
return webpackMerge(this.compilerConfiguration, configuration); | ||
if (pkg !== 'webpack-cli' && !getPkg(pkg)) { | ||
if (!allowToInstall) { | ||
const isOptions = commandName.startsWith('-'); | ||
logger.error(`Unknown ${isOptions ? 'option' : 'command'} '${commandName}'`); | ||
logger.error("Run 'webpack --help' to see available commands and options"); | ||
process.exit(2); | ||
} | ||
try { | ||
pkg = await promptInstallation(pkg, () => { | ||
logger.error(`For using this command you need to install: '${green(commandName)}' package`); | ||
}); | ||
} catch (error) { | ||
logger.error(`Action Interrupted, use '${cyan('webpack-cli help')}' to see possible commands`); | ||
process.exit(2); | ||
} | ||
} | ||
let loadedCommand; | ||
try { | ||
loadedCommand = require(pkg); | ||
} catch (error) { | ||
// Ignore, command is not installed | ||
return; | ||
} | ||
if (loadedCommand.default) { | ||
loadedCommand = loadedCommand.default; | ||
} | ||
let command; | ||
try { | ||
command = new loadedCommand(); | ||
await command.apply(this); | ||
} catch (error) { | ||
logger.error(`Unable to load '${pkg}' command`); | ||
logger.error(error); | ||
process.exit(2); | ||
} | ||
} | ||
}; | ||
// Register own exit | ||
this.program.exitOverride(async (error) => { | ||
if (error.exitCode === 0) { | ||
process.exit(0); | ||
} | ||
if (error.code === 'executeSubCommandAsync') { | ||
process.exit(2); | ||
} | ||
if (error.code === 'commander.help') { | ||
process.exit(0); | ||
} | ||
if (error.code === 'commander.unknownOption') { | ||
let name = error.message.match(/'(.+)'/); | ||
if (name) { | ||
name = name[1].substr(2); | ||
if (name.includes('=')) { | ||
name = name.split('=')[0]; | ||
} | ||
const { commandName } = getCommandNameAndOptions(this.program.args); | ||
if (commandName) { | ||
const command = this.program.commands.find( | ||
(command) => command.name() === commandName || command.alias() === commandName, | ||
); | ||
if (!command) { | ||
logger.error(`Can't find and load command '${commandName}'`); | ||
logger.error("Run 'webpack --help' to see available commands and options"); | ||
process.exit(2); | ||
} | ||
const found = command.options.find((option) => distance(name, option.long.slice(2)) < 3); | ||
if (found) { | ||
logger.error(`Did you mean '--${found.name()}'?`); | ||
} | ||
} | ||
} | ||
} | ||
// Codes: | ||
// - commander.unknownCommand | ||
// - commander.missingArgument | ||
// - commander.missingMandatoryOptionValue | ||
// - commander.optionMissingArgument | ||
logger.error("Run 'webpack --help' to see available commands and options"); | ||
process.exit(2); | ||
}); | ||
// Default `--color` and `--no-color` options | ||
// TODO doesn't work with `webpack serve` (never work, need fix), `--stats` doesn't work too, other options are fine | ||
let colorFromArguments; | ||
this.program.option('--color', 'Enable colors on console.'); | ||
this.program.on('option:color', function () { | ||
const { color } = this.opts(); | ||
colorFromArguments = color; | ||
coloretteOptions.enabled = color; | ||
}); | ||
this.program.option('--no-color', 'Disable colors on console.'); | ||
this.program.on('option:no-color', function () { | ||
const { color } = this.opts(); | ||
colorFromArguments = color; | ||
coloretteOptions.enabled = color; | ||
}); | ||
// Make `-v, --version` options | ||
// Make `version|v [commands...]` command | ||
const outputVersion = async (options) => { | ||
// Filter `bundle`, `version` and `help` commands | ||
const possibleCommandNames = options.filter( | ||
(options) => | ||
options !== bundleCommandOptions.name && | ||
options !== bundleCommandOptions.alias && | ||
options !== versionCommandOptions.name && | ||
options !== versionCommandOptions.alias && | ||
options !== helpCommandOptions.name && | ||
options !== helpCommandOptions.alias, | ||
); | ||
possibleCommandNames.forEach((possibleCommandName) => { | ||
const isOption = possibleCommandName.startsWith('-'); | ||
if (!isOption) { | ||
return; | ||
} | ||
logger.error(`Unknown option '${possibleCommandName}'`); | ||
logger.error("Run 'webpack --help' to see available commands and options"); | ||
process.exit(2); | ||
}); | ||
} else { | ||
/** | ||
* The compiler configuration is already an array, so for each element | ||
* we merge the options | ||
*/ | ||
if (Array.isArray(this.compilerConfiguration)) { | ||
this.compilerConfiguration = this.compilerConfiguration.map((thisConfiguration) => { | ||
if (strategy) { | ||
return webpackMerge.strategy(strategy)(thisConfiguration, options); | ||
if (possibleCommandNames.length > 0) { | ||
await Promise.all(possibleCommandNames.map((possibleCommand) => loadCommandByName(possibleCommand))); | ||
for (const possibleCommandName of possibleCommandNames) { | ||
const foundCommand = this.program.commands.find( | ||
(command) => command.name() === possibleCommandName || command.alias() === possibleCommandName, | ||
); | ||
try { | ||
const { name, version } = require(`${foundCommand.pkg}/package.json`); | ||
logger.raw(`${name} ${version}`); | ||
} catch (e) { | ||
logger.error(`Error: External package '${foundCommand.pkg}' not found`); | ||
process.exit(2); | ||
} | ||
return webpackMerge(thisConfiguration, options); | ||
} | ||
} | ||
const pkgJSON = require('../package.json'); | ||
logger.raw(`webpack ${webpack.version}`); | ||
logger.raw(`webpack-cli ${pkgJSON.version}`); | ||
if (getPkg('webpack-dev-server')) { | ||
// eslint-disable-next-line node/no-extraneous-require | ||
const { version } = require('webpack-dev-server/package.json'); | ||
logger.raw(`webpack-dev-server ${version}`); | ||
} | ||
process.exit(0); | ||
}; | ||
this.program.option( | ||
'-v, --version', | ||
"Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.", | ||
); | ||
// Default global `help` command | ||
const outputHelp = async (options, isVerbose, program) => { | ||
const isGlobal = options.length === 0; | ||
const hideVerboseOptions = (command) => { | ||
command.options = command.options.filter((option) => { | ||
const foundOption = flags.find((flag) => { | ||
if (option.negate && flag.negative) { | ||
return `no-${flag.name}` === option.name(); | ||
} | ||
return flag.name === option.name(); | ||
}); | ||
if (foundOption && foundOption.help) { | ||
return foundOption.help === 'minimum'; | ||
} | ||
return true; | ||
}); | ||
}; | ||
if (isGlobal) { | ||
const commandsToLoad = [] | ||
.concat(bundleCommandOptions) | ||
.concat(helpCommandOptions) | ||
.concat(versionCommandOptions) | ||
.concat(externalBuiltInCommandsInfo); | ||
for (const commandToLoad of commandsToLoad) { | ||
await loadCommandByName(commandToLoad.name); | ||
} | ||
const bundleCommand = this.program.commands.find( | ||
(command) => command.name() === bundleCommandOptions.name || command.alias() === bundleCommandOptions.alias, | ||
); | ||
if (!isVerbose) { | ||
hideVerboseOptions(bundleCommand); | ||
} | ||
let helpInformation = bundleCommand | ||
.helpInformation() | ||
.trimRight() | ||
.replace(bundleCommandOptions.description, 'The build tool for modern web applications.') | ||
.replace( | ||
/Usage:.+/, | ||
'Usage: webpack [options]\nAlternative usage: webpack bundle [options]\nAlternative usage: webpack --config <config> [options]\nAlternative usage: webpack bundle --config <config> [options]', | ||
); | ||
logger.raw(helpInformation); | ||
} else { | ||
if (strategy) { | ||
this.compilerConfiguration = webpackMerge.strategy(strategy)(this.compilerConfiguration, options); | ||
const [name, ...optionsWithoutCommandName] = options; | ||
optionsWithoutCommandName.forEach((option) => { | ||
logger.error(`Unknown option '${option}'`); | ||
logger.error("Run 'webpack --help' to see available commands and options"); | ||
process.exit(2); | ||
}); | ||
await loadCommandByName(name); | ||
const command = this.program.commands.find((command) => command.name() === name || command.alias() === name); | ||
if (!command) { | ||
logger.error(`Can't find and load command '${name}'`); | ||
logger.error("Run 'webpack --help' to see available commands and options"); | ||
process.exit(2); | ||
} | ||
if (!isVerbose) { | ||
hideVerboseOptions(command); | ||
} | ||
let helpInformation = command.helpInformation().trimRight(); | ||
if (name === bundleCommandOptions.name || name === bundleCommandOptions.alias) { | ||
helpInformation = helpInformation | ||
.replace(bundleCommandOptions.description, 'The build tool for modern web applications.') | ||
.replace( | ||
/Usage:.+/, | ||
'Usage: webpack [options]\nAlternative usage: webpack bundle [options]\nAlternative usage: webpack --config <config> [options]\nAlternative usage: webpack bundle --config <config> [options]', | ||
); | ||
} | ||
logger.raw(helpInformation); | ||
} | ||
const globalOptions = program.helpInformation().match(/Options:\n(?<globalOptions>.+)\nCommands:\n/s); | ||
if (globalOptions && globalOptions.groups.globalOptions) { | ||
logger.raw('\nGlobal options:'); | ||
logger.raw(globalOptions.groups.globalOptions.trimRight()); | ||
} | ||
if (isGlobal) { | ||
const globalCommands = program.helpInformation().match(/Commands:\n(?<globalCommands>.+)/s); | ||
logger.raw('\nCommands:'); | ||
logger.raw(globalCommands.groups.globalCommands.trimRight()); | ||
} | ||
logger.raw("\nTo see list of all supported commands and options run 'webpack --help=verbose'.\n"); | ||
logger.raw('Webpack documentation: https://webpack.js.org/.'); | ||
logger.raw('CLI documentation: https://webpack.js.org/api/cli/.'); | ||
logger.raw(`${bold('Made with ♥ by the webpack team')}.`); | ||
process.exit(0); | ||
}; | ||
this.program.helpOption(false); | ||
this.program.addHelpCommand(false); | ||
this.program.option('-h, --help [verbose]', 'Display help for commands and options.'); | ||
let isInternalActionCalled = false; | ||
// Default action | ||
this.program.usage('[options]'); | ||
this.program.allowUnknownOption(true); | ||
this.program.action(async (program) => { | ||
if (!isInternalActionCalled) { | ||
isInternalActionCalled = true; | ||
} else { | ||
logger.error('No commands found to run'); | ||
process.exit(2); | ||
} | ||
const { commandName, options, isDefault } = getCommandNameAndOptions(program.args); | ||
const opts = program.opts(); | ||
if (opts.help || commandName === helpCommandOptions.name || commandName === helpCommandOptions.alias) { | ||
let isVerbose = false; | ||
if (opts.help) { | ||
if (typeof opts.help === 'string') { | ||
if (opts.help !== 'verbose') { | ||
logger.error("Unknown value for '--help' option, please use '--help=verbose'"); | ||
process.exit(2); | ||
} | ||
isVerbose = true; | ||
} | ||
} | ||
const optionsForHelp = [].concat(opts.help && !isDefault ? [commandName] : []).concat(options); | ||
await outputHelp(optionsForHelp, isVerbose, program); | ||
} | ||
if (opts.version || commandName === versionCommandOptions.name || commandName === versionCommandOptions.alias) { | ||
const optionsForVersion = [].concat(opts.version ? [commandName] : []).concat(options); | ||
await outputVersion(optionsForVersion, program); | ||
} | ||
await loadCommandByName(commandName, true); | ||
await this.program.parseAsync([commandName, ...options], { from: 'user' }); | ||
}); | ||
await this.program.parseAsync(args); | ||
} | ||
async resolveConfig(options) { | ||
const loadConfig = async (configPath) => { | ||
const ext = extname(configPath); | ||
const interpreted = Object.keys(jsVariants).find((variant) => variant === ext); | ||
if (interpreted) { | ||
rechoir.prepare(extensions, configPath); | ||
} | ||
const { pathToFileURL } = require('url'); | ||
let importESM; | ||
try { | ||
importESM = new Function('id', 'return import(id);'); | ||
} catch (e) { | ||
importESM = null; | ||
} | ||
let options; | ||
try { | ||
try { | ||
options = require(configPath); | ||
} catch (error) { | ||
if (pathToFileURL && importESM && error.code === 'ERR_REQUIRE_ESM') { | ||
const urlForConfig = pathToFileURL(configPath); | ||
options = await importESM(urlForConfig); | ||
options = options.default; | ||
return { options, path: configPath }; | ||
} | ||
throw error; | ||
} | ||
} catch (error) { | ||
logger.error(`Failed to load '${configPath}'`); | ||
logger.error(error); | ||
process.exit(2); | ||
} | ||
if (options.default) { | ||
options = options.default; | ||
} | ||
return { options, path: configPath }; | ||
}; | ||
const evaluateConfig = async (loadedConfig, args) => { | ||
const isMultiCompiler = Array.isArray(loadedConfig.options); | ||
const config = isMultiCompiler ? loadedConfig.options : [loadedConfig.options]; | ||
let evaluatedConfig = await Promise.all( | ||
config.map(async (rawConfig) => { | ||
if (typeof rawConfig.then === 'function') { | ||
rawConfig = await rawConfig; | ||
} | ||
// `Promise` may return `Function` | ||
if (typeof rawConfig === 'function') { | ||
// when config is a function, pass the env from args to the config function | ||
rawConfig = await rawConfig(args.env, args); | ||
} | ||
return rawConfig; | ||
}), | ||
); | ||
loadedConfig.options = isMultiCompiler ? evaluatedConfig : evaluatedConfig[0]; | ||
const isObject = (value) => typeof value === 'object' && value !== null; | ||
if (!isObject(loadedConfig.options) && !Array.isArray(loadedConfig.options)) { | ||
logger.error(`Invalid configuration in '${loadedConfig.path}'`); | ||
process.exit(2); | ||
} | ||
return loadedConfig; | ||
}; | ||
let config = { options: {}, path: new WeakMap() }; | ||
if (options.config && options.config.length > 0) { | ||
const evaluatedConfigs = await Promise.all( | ||
options.config.map(async (value) => { | ||
const configPath = resolve(value); | ||
if (!existsSync(configPath)) { | ||
logger.error(`The specified config file doesn't exist in '${configPath}'`); | ||
process.exit(2); | ||
} | ||
const loadedConfig = await loadConfig(configPath); | ||
return evaluateConfig(loadedConfig, options); | ||
}), | ||
); | ||
config.options = []; | ||
evaluatedConfigs.forEach((evaluatedConfig) => { | ||
if (Array.isArray(evaluatedConfig.options)) { | ||
evaluatedConfig.options.forEach((options) => { | ||
config.options.push(options); | ||
config.path.set(options, evaluatedConfig.path); | ||
}); | ||
} else { | ||
this.compilerConfiguration = webpackMerge(this.compilerConfiguration, options); | ||
config.options.push(evaluatedConfig.options); | ||
config.path.set(evaluatedConfig.options, evaluatedConfig.path); | ||
} | ||
}); | ||
config.options = config.options.length === 1 ? config.options[0] : config.options; | ||
} else { | ||
// Order defines the priority, in increasing order | ||
const defaultConfigFiles = ['webpack.config', '.webpack/webpack.config', '.webpack/webpackfile'] | ||
.map((filename) => | ||
// Since .cjs is not available on interpret side add it manually to default config extension list | ||
[...Object.keys(extensions), '.cjs'].map((ext) => ({ | ||
path: resolve(filename + ext), | ||
ext: ext, | ||
module: extensions[ext], | ||
})), | ||
) | ||
.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []); | ||
let foundDefaultConfigFile; | ||
for (const defaultConfigFile of defaultConfigFiles) { | ||
if (existsSync(defaultConfigFile.path)) { | ||
foundDefaultConfigFile = defaultConfigFile; | ||
break; | ||
} | ||
} | ||
if (foundDefaultConfigFile) { | ||
const loadedConfig = await loadConfig(foundDefaultConfigFile.path); | ||
const evaluatedConfig = await evaluateConfig(loadedConfig, options); | ||
config.options = evaluatedConfig.options; | ||
if (Array.isArray(config.options)) { | ||
config.options.forEach((options) => { | ||
config.path.set(options, evaluatedConfig.path); | ||
}); | ||
} else { | ||
config.path.set(evaluatedConfig.options, evaluatedConfig.path); | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* Responsible for creating and updating the new output configuration | ||
* | ||
* @param {Object} options Output options emitted by the group helper | ||
* @private | ||
* @returns {void} | ||
*/ | ||
_mergeOptionsToOutputConfiguration(options) { | ||
if (options) { | ||
this.outputConfiguration = Object.assign(this.outputConfiguration, options); | ||
if (options.configName) { | ||
const notfoundConfigNames = []; | ||
config.options = options.configName.map((configName) => { | ||
let found; | ||
if (Array.isArray(config.options)) { | ||
found = config.options.find((options) => options.name === configName); | ||
} else { | ||
found = config.options.name === configName ? config.options : undefined; | ||
} | ||
if (!found) { | ||
notfoundConfigNames.push(configName); | ||
} | ||
return found; | ||
}); | ||
if (notfoundConfigNames.length > 0) { | ||
logger.error( | ||
notfoundConfigNames.map((configName) => `Configuration with the name "${configName}" was not found.`).join(' '), | ||
); | ||
process.exit(2); | ||
} | ||
} | ||
} | ||
/** | ||
* It runs in a fancy order all the expected groups. | ||
* Zero config and configuration goes first. | ||
* | ||
* The next groups will override existing parameters | ||
* @returns {Promise<void>} A Promise | ||
*/ | ||
async runOptionGroups(parsedArgs) { | ||
await Promise.resolve() | ||
.then(() => this._baseResolver(handleConfigResolution, parsedArgs)) | ||
.then(() => this._baseResolver(resolveMode, parsedArgs)) | ||
.then(() => this._baseResolver(resolveOutput, parsedArgs, outputStrategy)) | ||
.then(() => this._handleCoreFlags(parsedArgs)) | ||
.then(() => this._baseResolver(basicResolver, parsedArgs)) | ||
.then(() => this._baseResolver(resolveAdvanced, parsedArgs)) | ||
.then(() => this._baseResolver(resolveStats, parsedArgs)); | ||
} | ||
if (options.merge) { | ||
// we can only merge when there are multiple configurations | ||
// either by passing multiple configs by flags or passing a | ||
// single config exporting an array | ||
if (!Array.isArray(config.options) || config.options.length <= 1) { | ||
logger.error('At least two configurations are required for merge.'); | ||
process.exit(2); | ||
} | ||
handleError(error) { | ||
// https://github.com/webpack/webpack/blob/master/lib/index.js#L267 | ||
// https://github.com/webpack/webpack/blob/v4.44.2/lib/webpack.js#L90 | ||
const ValidationError = webpack.ValidationError || webpack.WebpackOptionsValidationError; | ||
const mergedConfigPaths = []; | ||
// In case of schema errors print and exit process | ||
// For webpack@4 and webpack@5 | ||
if (error instanceof ValidationError) { | ||
logger.error(error.message); | ||
} else { | ||
logger.error(error); | ||
config.options = config.options.reduce((accumulator, options) => { | ||
const configPath = config.path.get(options); | ||
const mergedOptions = webpackMerge(accumulator, options); | ||
mergedConfigPaths.push(configPath); | ||
return mergedOptions; | ||
}, {}); | ||
config.path.set(config.options, mergedConfigPaths); | ||
} | ||
return config; | ||
} | ||
createCompiler(options, callback) { | ||
let compiler; | ||
// TODO refactor | ||
async applyOptions(config, options) { | ||
if (options.analyze) { | ||
if (!getPkg('webpack-bundle-analyzer')) { | ||
try { | ||
await promptInstallation('webpack-bundle-analyzer', () => { | ||
logger.error(`It looks like ${yellow('webpack-bundle-analyzer')} is not installed.`); | ||
}); | ||
} catch (error) { | ||
logger.error(`Action Interrupted, Please try once again or install ${yellow('webpack-bundle-analyzer')} manually.`); | ||
process.exit(2); | ||
} | ||
try { | ||
compiler = webpack(options, callback); | ||
} catch (error) { | ||
this.handleError(error); | ||
logger.success(`${yellow('webpack-bundle-analyzer')} was installed successfully.`); | ||
} | ||
} | ||
if (typeof options.progress === 'string' && options.progress !== 'profile') { | ||
logger.error(`'${options.progress}' is an invalid value for the --progress option. Only 'profile' is allowed.`); | ||
process.exit(2); | ||
} | ||
return compiler; | ||
if (Object.keys(options).length === 0 && !process.env.NODE_ENV) { | ||
return config; | ||
} | ||
if (cli) { | ||
const processArguments = (configOptions) => { | ||
const coreFlagMap = flags | ||
.filter((flag) => flag.group === 'core') | ||
.reduce((accumulator, flag) => { | ||
accumulator[flag.name] = flag; | ||
return accumulator; | ||
}, {}); | ||
const CLIoptions = Object.keys(options).reduce((accumulator, name) => { | ||
const kebabName = toKebabCase(name); | ||
if (coreFlagMap[kebabName]) { | ||
accumulator[kebabName] = options[name]; | ||
} | ||
return accumulator; | ||
}, {}); | ||
const problems = cli.processArguments(coreFlagMap, configOptions, CLIoptions); | ||
if (problems) { | ||
const capitalizeFirstLetter = (string) => { | ||
return string.charAt(0).toUpperCase() + string.slice(1); | ||
}; | ||
const groupBy = (xs, key) => { | ||
return xs.reduce((rv, x) => { | ||
(rv[x[key]] = rv[x[key]] || []).push(x); | ||
return rv; | ||
}, {}); | ||
}; | ||
const problemsByPath = groupBy(problems, 'path'); | ||
for (const path in problemsByPath) { | ||
const problems = problemsByPath[path]; | ||
problems.forEach((problem) => { | ||
logger.error( | ||
`${capitalizeFirstLetter(problem.type.replace(/-/g, ' '))}${ | ||
problem.value ? ` '${problem.value}'` : '' | ||
} for the '--${problem.argument}' option${problem.index ? ` by index '${problem.index}'` : ''}`, | ||
); | ||
if (problem.expected) { | ||
logger.error(`Expected: '${problem.expected}'`); | ||
} | ||
}); | ||
} | ||
process.exit(2); | ||
} | ||
return configOptions; | ||
}; | ||
config.options = Array.isArray(config.options) | ||
? config.options.map((options) => processArguments(options)) | ||
: processArguments(config.options); | ||
const setupDefaultOptions = (configOptions) => { | ||
// No need to run for webpack@4 | ||
if (configOptions.cache && configOptions.cache.type === 'filesystem') { | ||
const configPath = config.path.get(configOptions); | ||
if (configPath) { | ||
if (!configOptions.cache.buildDependencies) { | ||
configOptions.cache.buildDependencies = {}; | ||
} | ||
if (!configOptions.cache.buildDependencies.defaultConfig) { | ||
configOptions.cache.buildDependencies.defaultConfig = []; | ||
} | ||
if (Array.isArray(configPath)) { | ||
configPath.forEach((item) => { | ||
configOptions.cache.buildDependencies.defaultConfig.push(item); | ||
}); | ||
} else { | ||
configOptions.cache.buildDependencies.defaultConfig.push(configPath); | ||
} | ||
} | ||
} | ||
return configOptions; | ||
}; | ||
config.options = Array.isArray(config.options) | ||
? config.options.map((options) => setupDefaultOptions(options)) | ||
: setupDefaultOptions(config.options); | ||
} | ||
// Logic for webpack@4 | ||
// TODO remove after drop webpack@4 | ||
const processLegacyArguments = (configOptions) => { | ||
if (options.entry) { | ||
configOptions.entry = options.entry; | ||
} | ||
if (options.outputPath) { | ||
configOptions.output = { | ||
...configOptions.output, | ||
...{ path: path.resolve(options.outputPath) }, | ||
}; | ||
} | ||
if (options.target) { | ||
configOptions.target = options.target; | ||
} | ||
if (typeof options.devtool !== 'undefined') { | ||
configOptions.devtool = options.devtool; | ||
} | ||
if (options.mode) { | ||
configOptions.mode = options.mode; | ||
} else if ( | ||
!configOptions.mode && | ||
process.env && | ||
process.env.NODE_ENV && | ||
(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'node') | ||
) { | ||
configOptions.mode = process.env.NODE_ENV; | ||
} | ||
if (options.name) { | ||
configOptions.name = options.name; | ||
} | ||
if (typeof options.stats !== 'undefined') { | ||
configOptions.stats = options.stats; | ||
} | ||
if (typeof options.watch !== 'undefined') { | ||
configOptions.watch = options.watch; | ||
} | ||
if (typeof options.watchOptionsStdin !== 'undefined') { | ||
configOptions.watchOptions = { | ||
...configOptions.watchOptions, | ||
...{ stdin: options.watchOptionsStdin }, | ||
}; | ||
} | ||
return configOptions; | ||
}; | ||
config.options = Array.isArray(config.options) | ||
? config.options.map((options) => processLegacyArguments(options)) | ||
: processLegacyArguments(config.options); | ||
return config; | ||
} | ||
async getCompiler(args) { | ||
await this.runOptionGroups(args); | ||
return this.createCompiler(this.compilerConfiguration); | ||
async applyCLIPlugin(config, options) { | ||
const addCLIPlugin = (configOptions) => { | ||
if (!configOptions.plugins) { | ||
configOptions.plugins = []; | ||
} | ||
configOptions.plugins.unshift( | ||
new CLIPlugin({ | ||
configPath: config.path, | ||
helpfulOutput: !options.json, | ||
hot: options.hot, | ||
progress: options.progress, | ||
prefetch: options.prefetch, | ||
analyze: options.analyze, | ||
}), | ||
); | ||
return configOptions; | ||
}; | ||
config.options = Array.isArray(config.options) | ||
? config.options.map((options) => addCLIPlugin(options)) | ||
: addCLIPlugin(config.options); | ||
return config; | ||
} | ||
async run(args) { | ||
await this.runOptionGroups(args); | ||
needWatchStdin(compiler) { | ||
if (compiler.compilers) { | ||
return compiler.compilers.some((compiler) => compiler.options.watchOptions && compiler.options.watchOptions.stdin); | ||
} | ||
let compiler; | ||
return compiler.options.watchOptions && compiler.options.watchOptions.stdin; | ||
} | ||
let options = this.compilerConfiguration; | ||
let outputOptions = this.outputConfiguration; | ||
async createCompiler(options, callback) { | ||
const isValidationError = (error) => { | ||
// https://github.com/webpack/webpack/blob/master/lib/index.js#L267 | ||
// https://github.com/webpack/webpack/blob/v4.44.2/lib/webpack.js#L90 | ||
const ValidationError = webpack.ValidationError || webpack.WebpackOptionsValidationError; | ||
const isRawOutput = typeof outputOptions.json === 'undefined'; | ||
return error instanceof ValidationError; | ||
}; | ||
if (isRawOutput) { | ||
const webpackCLIPlugin = new WebpackCLIPlugin({ | ||
progress: outputOptions.progress, | ||
}); | ||
let config = await this.resolveConfig(options); | ||
const addPlugin = (options) => { | ||
if (!options.plugins) { | ||
options.plugins = []; | ||
} | ||
options.plugins.unshift(webpackCLIPlugin); | ||
}; | ||
if (Array.isArray(options)) { | ||
options.forEach(addPlugin); | ||
config = await this.applyOptions(config, options); | ||
config = await this.applyCLIPlugin(config, options); | ||
let compiler; | ||
try { | ||
compiler = webpack( | ||
config.options, | ||
callback | ||
? (error, stats) => { | ||
if (isValidationError(error)) { | ||
logger.error(error.message); | ||
process.exit(2); | ||
} | ||
callback(error, stats); | ||
} | ||
: callback, | ||
); | ||
} catch (error) { | ||
if (isValidationError(error)) { | ||
logger.error(error.message); | ||
} else { | ||
addPlugin(options); | ||
logger.error(error); | ||
} | ||
process.exit(2); | ||
} | ||
// TODO webpack@4 return Watching and MultiWatching instead Compiler and MultiCompiler, remove this after drop webpack@4 | ||
if (compiler && compiler.compiler) { | ||
compiler = compiler.compiler; | ||
} | ||
return compiler; | ||
} | ||
async bundleCommand(options) { | ||
let compiler; | ||
const callback = (error, stats) => { | ||
if (error) { | ||
this.handleError(error); | ||
logger.error(error); | ||
process.exit(2); | ||
@@ -228,5 +1137,7 @@ } | ||
// TODO remove after drop webpack@4 | ||
const statsForWebpack4 = webpack.Stats && webpack.Stats.presetToOptions; | ||
const getStatsOptions = (stats) => { | ||
// TODO remove after drop webpack@4 | ||
if (webpack.Stats && webpack.Stats.presetToOptions) { | ||
if (statsForWebpack4) { | ||
if (!stats) { | ||
@@ -239,41 +1150,85 @@ stats = {}; | ||
stats.colors = typeof stats.colors !== 'undefined' ? stats.colors : coloretteOptions.enabled; | ||
let colors; | ||
// From arguments | ||
if (typeof options.color !== 'undefined') { | ||
colors = options.color; | ||
} | ||
// From stats | ||
else if (typeof stats.colors !== 'undefined') { | ||
colors = stats.colors; | ||
} | ||
// Default | ||
else { | ||
colors = coloretteOptions.enabled; | ||
} | ||
stats.colors = colors; | ||
return stats; | ||
}; | ||
const getStatsOptionsFromCompiler = (compiler) => getStatsOptions(compiler.options ? compiler.options.stats : undefined); | ||
if (!compiler) { | ||
return; | ||
} | ||
const foundStats = compiler.compilers | ||
? { children: compiler.compilers.map(getStatsOptionsFromCompiler) } | ||
: getStatsOptionsFromCompiler(compiler); | ||
? { | ||
children: compiler.compilers.map((compiler) => | ||
getStatsOptions(compiler.options ? compiler.options.stats : undefined), | ||
), | ||
} | ||
: getStatsOptions(compiler.options ? compiler.options.stats : undefined); | ||
if (outputOptions.json === true) { | ||
process.stdout.write(JSON.stringify(stats.toJson(foundStats), null, 2) + '\n'); | ||
} else if (typeof outputOptions.json === 'string') { | ||
const JSONStats = JSON.stringify(stats.toJson(foundStats), null, 2); | ||
// TODO webpack@4 doesn't support `{ children: [{ colors: true }, { colors: true }] }` for stats | ||
if (statsForWebpack4 && compiler.compilers) { | ||
foundStats.colors = foundStats.children.some((child) => child.colors); | ||
} | ||
try { | ||
writeFileSync(outputOptions.json, JSONStats); | ||
logger.success(`stats are successfully stored as json to ${outputOptions.json}`); | ||
} catch (error) { | ||
if (options.json) { | ||
const handleWriteError = (error) => { | ||
logger.error(error); | ||
process.exit(2); | ||
}; | ||
process.exit(2); | ||
if (options.json === true) { | ||
createJsonStringifyStream(stats.toJson(foundStats)) | ||
.on('error', handleWriteError) | ||
.pipe(process.stdout) | ||
.on('error', handleWriteError) | ||
.on('close', () => process.stdout.write('\n')); | ||
} else { | ||
createJsonStringifyStream(stats.toJson(foundStats)) | ||
.on('error', handleWriteError) | ||
.pipe(createWriteStream(options.json)) | ||
.on('error', handleWriteError) | ||
.on('close', () => logger.success(`stats are successfully stored as json to ${options.json}`)); | ||
} | ||
} else { | ||
logger.raw(`${stats.toString(foundStats)}`); | ||
const printedStats = stats.toString(foundStats); | ||
// Avoid extra empty line when `stats: 'none'` | ||
if (printedStats) { | ||
logger.raw(`${stats.toString(foundStats)}`); | ||
} | ||
} | ||
}; | ||
compiler = this.createCompiler(options, callback); | ||
options.env = { WEBPACK_BUNDLE: true, ...options.env }; | ||
if (compiler && outputOptions.interactive) { | ||
const interactive = require('./utils/interactive'); | ||
compiler = await this.createCompiler(options, callback); | ||
interactive(compiler, options, outputOptions); | ||
if (!compiler) { | ||
return; | ||
} | ||
return Promise.resolve(); | ||
const isWatch = (compiler) => | ||
compiler.compilers ? compiler.compilers.some((compiler) => compiler.options.watch) : compiler.options.watch; | ||
if (isWatch(compiler) && this.needWatchStdin(compiler)) { | ||
process.stdin.on('end', () => { | ||
process.exit(0); | ||
}); | ||
process.stdin.resume(); | ||
} | ||
} | ||
@@ -280,0 +1235,0 @@ } |
{ | ||
"name": "webpack-cli", | ||
"version": "4.2.0", | ||
"version": "4.3.0", | ||
"description": "CLI for webpack & friends", | ||
@@ -30,12 +30,12 @@ "license": "MIT", | ||
"dependencies": { | ||
"@webpack-cli/info": "^1.1.0", | ||
"@webpack-cli/serve": "^1.1.0", | ||
"@discoveryjs/json-ext": "^0.5.0", | ||
"@webpack-cli/info": "^1.2.0", | ||
"@webpack-cli/serve": "^1.2.0", | ||
"colorette": "^1.2.1", | ||
"command-line-usage": "^6.1.0", | ||
"commander": "^6.2.0", | ||
"enquirer": "^2.3.6", | ||
"execa": "^4.1.0", | ||
"fastest-levenshtein": "^1.0.12", | ||
"import-local": "^3.0.2", | ||
"interpret": "^2.2.0", | ||
"leven": "^3.1.0", | ||
"rechoir": "^0.7.0", | ||
@@ -68,3 +68,3 @@ "v8-compile-cache": "^2.2.0", | ||
}, | ||
"gitHead": "0caa9184e22ed857e175c8dc0dd1e0a26f216374" | ||
"gitHead": "7c2311a541d93e67d9c328f26b96d36418eac823" | ||
} |
Sorry, the diff of this file is too big to display
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses eval() which is a dangerous function. This prevents the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
213496
794
30
1635
15
2
+ Added@discoveryjs/json-ext@^0.5.0
+ Addedfastest-levenshtein@^1.0.12
+ Added@discoveryjs/json-ext@0.5.7(transitive)
+ Addedfastest-levenshtein@1.0.16(transitive)
- Removedcommand-line-usage@^6.1.0
- Removedleven@^3.1.0
- Removedansi-styles@3.2.1(transitive)
- Removedarray-back@4.0.2(transitive)
- Removedchalk@2.4.2(transitive)
- Removedcolor-convert@1.9.3(transitive)
- Removedcolor-name@1.1.3(transitive)
- Removedcommand-line-usage@6.1.3(transitive)
- Removeddeep-extend@0.6.0(transitive)
- Removedescape-string-regexp@1.0.5(transitive)
- Removedhas-flag@3.0.0(transitive)
- Removedleven@3.1.0(transitive)
- Removedreduce-flatten@2.0.0(transitive)
- Removedsupports-color@5.5.0(transitive)
- Removedtable-layout@1.0.2(transitive)
- Removedtypical@5.2.0(transitive)
- Removedwordwrapjs@4.0.1(transitive)
Updated@webpack-cli/info@^1.2.0
Updated@webpack-cli/serve@^1.2.0