Socket
Socket
Sign inDemoInstall

webpack-cli

Package Overview
Dependencies
139
Maintainers
5
Versions
123
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 4.2.0 to 4.3.0

lib/plugins/CLIPlugin.js

12

bin/cli.js

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc