You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2343 lines
68 KiB
2343 lines
68 KiB
const fs = require("fs"); |
|
const path = require("path"); |
|
const { pathToFileURL } = require("url"); |
|
const util = require("util"); |
|
|
|
const { program, Option } = require("commander"); |
|
|
|
const WEBPACK_PACKAGE = process.env.WEBPACK_PACKAGE || "webpack"; |
|
const WEBPACK_DEV_SERVER_PACKAGE = process.env.WEBPACK_DEV_SERVER_PACKAGE || "webpack-dev-server"; |
|
|
|
class WebpackCLI { |
|
constructor() { |
|
this.colors = this.createColors(); |
|
this.logger = this.getLogger(); |
|
|
|
// Initialize program |
|
this.program = program; |
|
this.program.name("webpack"); |
|
this.program.configureOutput({ |
|
writeErr: this.logger.error, |
|
outputError: (str, write) => |
|
write(`Error: ${this.capitalizeFirstLetter(str.replace(/^error:/, "").trim())}`), |
|
}); |
|
} |
|
|
|
capitalizeFirstLetter(str) { |
|
if (typeof str !== "string") { |
|
return ""; |
|
} |
|
|
|
return str.charAt(0).toUpperCase() + str.slice(1); |
|
} |
|
|
|
toKebabCase(str) { |
|
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); |
|
} |
|
|
|
createColors(useColor) { |
|
const { createColors, isColorSupported } = require("colorette"); |
|
|
|
let shouldUseColor; |
|
|
|
if (useColor) { |
|
shouldUseColor = useColor; |
|
} else { |
|
shouldUseColor = isColorSupported; |
|
} |
|
|
|
return { ...createColors({ useColor: shouldUseColor }), isColorSupported: shouldUseColor }; |
|
} |
|
|
|
getLogger() { |
|
return { |
|
error: (val) => console.error(`[webpack-cli] ${this.colors.red(util.format(val))}`), |
|
warn: (val) => console.warn(`[webpack-cli] ${this.colors.yellow(val)}`), |
|
info: (val) => console.info(`[webpack-cli] ${this.colors.cyan(val)}`), |
|
success: (val) => console.log(`[webpack-cli] ${this.colors.green(val)}`), |
|
log: (val) => console.log(`[webpack-cli] ${val}`), |
|
raw: (val) => console.log(val), |
|
}; |
|
} |
|
|
|
checkPackageExists(packageName) { |
|
if (process.versions.pnp) { |
|
return true; |
|
} |
|
|
|
let dir = __dirname; |
|
|
|
do { |
|
try { |
|
if (fs.statSync(path.join(dir, "node_modules", packageName)).isDirectory()) { |
|
return true; |
|
} |
|
} catch (_error) { |
|
// Nothing |
|
} |
|
} while (dir !== (dir = path.dirname(dir))); |
|
|
|
return false; |
|
} |
|
|
|
getAvailablePackageManagers() { |
|
const { sync } = require("execa"); |
|
const installers = ["npm", "yarn", "pnpm"]; |
|
const hasPackageManagerInstalled = (packageManager) => { |
|
try { |
|
sync(packageManager, ["--version"]); |
|
|
|
return packageManager; |
|
} catch (err) { |
|
return false; |
|
} |
|
}; |
|
const availableInstallers = installers.filter((installer) => |
|
hasPackageManagerInstalled(installer), |
|
); |
|
|
|
if (!availableInstallers.length) { |
|
this.logger.error("No package manager found."); |
|
|
|
process.exit(2); |
|
} |
|
|
|
return availableInstallers; |
|
} |
|
|
|
getDefaultPackageManager() { |
|
const { sync } = require("execa"); |
|
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")); |
|
|
|
if (hasLocalYarn) { |
|
return "yarn"; |
|
} |
|
|
|
const hasLocalPnpm = fs.existsSync(path.resolve(process.cwd(), "pnpm-lock.yaml")); |
|
|
|
if (hasLocalPnpm) { |
|
return "pnpm"; |
|
} |
|
|
|
try { |
|
// 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"])) { |
|
return "yarn"; |
|
} |
|
} catch (e) { |
|
// Nothing |
|
} |
|
|
|
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) { |
|
this.logger.error("No package manager found."); |
|
|
|
process.exit(2); |
|
} |
|
} |
|
|
|
async doInstall(packageName, options = {}) { |
|
const packageManager = this.getDefaultPackageManager(); |
|
|
|
if (!packageManager) { |
|
this.logger.error("Can't find package manager"); |
|
|
|
process.exit(2); |
|
} |
|
|
|
if (options.preMessage) { |
|
options.preMessage(); |
|
} |
|
|
|
// yarn uses 'add' command, rest npm and pnpm both use 'install' |
|
const commandToBeRun = `${packageManager} ${[ |
|
packageManager === "yarn" ? "add" : "install", |
|
"-D", |
|
packageName, |
|
].join(" ")}`; |
|
|
|
const prompt = ({ message, defaultResponse, stream }) => { |
|
const readline = require("readline"); |
|
const rl = readline.createInterface({ |
|
input: process.stdin, |
|
output: stream, |
|
}); |
|
|
|
return new Promise((resolve) => { |
|
rl.question(`${message} `, (answer) => { |
|
// Close the stream |
|
rl.close(); |
|
|
|
const response = (answer || defaultResponse).toLowerCase(); |
|
|
|
// Resolve with the input response |
|
if (response === "y" || response === "yes") { |
|
resolve(true); |
|
} else { |
|
resolve(false); |
|
} |
|
}); |
|
}); |
|
}; |
|
|
|
let needInstall; |
|
|
|
try { |
|
needInstall = await prompt({ |
|
message: `[webpack-cli] Would you like to install '${this.colors.green( |
|
packageName, |
|
)}' package? (That will run '${this.colors.green(commandToBeRun)}') (${this.colors.yellow( |
|
"Y/n", |
|
)})`, |
|
defaultResponse: "Y", |
|
stream: process.stderr, |
|
}); |
|
} catch (error) { |
|
this.logger.error(error); |
|
|
|
process.exit(error); |
|
} |
|
|
|
if (needInstall) { |
|
const execa = require("execa"); |
|
|
|
try { |
|
await execa(commandToBeRun, [], { stdio: "inherit", shell: true }); |
|
} catch (error) { |
|
this.logger.error(error); |
|
|
|
process.exit(2); |
|
} |
|
|
|
return packageName; |
|
} |
|
|
|
process.exit(2); |
|
} |
|
|
|
async tryRequireThenImport(module, handleError = true) { |
|
let result; |
|
|
|
try { |
|
result = require(module); |
|
} catch (error) { |
|
const dynamicImportLoader = require("./utils/dynamic-import-loader")(); |
|
if ( |
|
(error.code === "ERR_REQUIRE_ESM" || process.env.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) && |
|
pathToFileURL && |
|
dynamicImportLoader |
|
) { |
|
const urlForConfig = pathToFileURL(module); |
|
|
|
result = await dynamicImportLoader(urlForConfig); |
|
result = result.default; |
|
|
|
return result; |
|
} |
|
|
|
if (handleError) { |
|
this.logger.error(error); |
|
process.exit(2); |
|
} else { |
|
throw error; |
|
} |
|
} |
|
|
|
// For babel/typescript |
|
if (result && typeof result === "object" && "default" in result) { |
|
result = result.default || {}; |
|
} |
|
|
|
return result || {}; |
|
} |
|
|
|
loadJSONFile(pathToFile, handleError = true) { |
|
let result; |
|
|
|
try { |
|
result = require(pathToFile); |
|
} catch (error) { |
|
if (handleError) { |
|
this.logger.error(error); |
|
process.exit(2); |
|
} else { |
|
throw error; |
|
} |
|
} |
|
|
|
return result; |
|
} |
|
|
|
async makeCommand(commandOptions, options, action) { |
|
const alreadyLoaded = this.program.commands.find( |
|
(command) => |
|
command.name() === commandOptions.name.split(" ")[0] || |
|
command.aliases().includes(commandOptions.alias), |
|
); |
|
|
|
if (alreadyLoaded) { |
|
return; |
|
} |
|
|
|
const command = this.program.command(commandOptions.name, { |
|
noHelp: commandOptions.noHelp, |
|
hidden: commandOptions.hidden, |
|
isDefault: commandOptions.isDefault, |
|
}); |
|
|
|
if (commandOptions.description) { |
|
command.description(commandOptions.description, commandOptions.argsDescription); |
|
} |
|
|
|
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"; |
|
} |
|
|
|
const { forHelp } = this.program; |
|
|
|
let allDependenciesInstalled = true; |
|
|
|
if (commandOptions.dependencies && commandOptions.dependencies.length > 0) { |
|
for (const dependency of commandOptions.dependencies) { |
|
const isPkgExist = this.checkPackageExists(dependency); |
|
|
|
if (isPkgExist) { |
|
continue; |
|
} else if (!isPkgExist && forHelp) { |
|
allDependenciesInstalled = false; |
|
continue; |
|
} |
|
|
|
let skipInstallation = false; |
|
|
|
// Allow to use `./path/to/webpack.js` outside `node_modules` |
|
if (dependency === WEBPACK_PACKAGE && fs.existsSync(WEBPACK_PACKAGE)) { |
|
skipInstallation = true; |
|
} |
|
|
|
// Allow to use `./path/to/webpack-dev-server.js` outside `node_modules` |
|
if (dependency === WEBPACK_DEV_SERVER_PACKAGE && fs.existsSync(WEBPACK_PACKAGE)) { |
|
skipInstallation = true; |
|
} |
|
|
|
if (skipInstallation) { |
|
continue; |
|
} |
|
|
|
await this.doInstall(dependency, { |
|
preMessage: () => { |
|
this.logger.error( |
|
`For using '${this.colors.green( |
|
commandOptions.name.split(" ")[0], |
|
)}' command you need to install: '${this.colors.green(dependency)}' package.`, |
|
); |
|
}, |
|
}); |
|
} |
|
} |
|
|
|
if (options) { |
|
if (typeof options === "function") { |
|
if (forHelp && !allDependenciesInstalled) { |
|
command.description( |
|
`${ |
|
commandOptions.description |
|
} To see all available options you need to install ${commandOptions.dependencies |
|
.map((dependency) => `'${dependency}'`) |
|
.join(", ")}.`, |
|
); |
|
options = []; |
|
} else { |
|
options = await options(); |
|
} |
|
} |
|
|
|
options.forEach((optionForCommand) => { |
|
this.makeOption(command, optionForCommand); |
|
}); |
|
} |
|
|
|
command.action(action); |
|
|
|
return command; |
|
} |
|
|
|
makeOption(command, option) { |
|
let mainOption; |
|
let negativeOption; |
|
|
|
if (option.configs) { |
|
let needNegativeOption = false; |
|
let negatedDescription; |
|
const mainOptionType = new Set(); |
|
|
|
option.configs.forEach((config) => { |
|
// Possible value: "enum" | "string" | "path" | "number" | "boolean" | "RegExp" | "reset" |
|
switch (config.type) { |
|
case "reset": |
|
mainOptionType.add(Boolean); |
|
break; |
|
case "boolean": |
|
if (!needNegativeOption) { |
|
needNegativeOption = true; |
|
negatedDescription = config.negatedDescription; |
|
} |
|
|
|
mainOptionType.add(Boolean); |
|
break; |
|
case "number": |
|
mainOptionType.add(Number); |
|
break; |
|
case "string": |
|
case "path": |
|
case "RegExp": |
|
mainOptionType.add(String); |
|
break; |
|
case "enum": { |
|
let hasFalseEnum = false; |
|
|
|
const enumTypes = config.values.map((value) => { |
|
switch (typeof value) { |
|
case "string": |
|
mainOptionType.add(String); |
|
break; |
|
case "number": |
|
mainOptionType.add(Number); |
|
break; |
|
case "boolean": |
|
if (!hasFalseEnum && value === false) { |
|
hasFalseEnum = true; |
|
break; |
|
} |
|
|
|
mainOptionType.add(Boolean); |
|
break; |
|
} |
|
}); |
|
|
|
if (!needNegativeOption) { |
|
needNegativeOption = hasFalseEnum; |
|
negatedDescription = config.negatedDescription; |
|
} |
|
|
|
return enumTypes; |
|
} |
|
} |
|
}); |
|
|
|
mainOption = { |
|
flags: option.alias ? `-${option.alias}, --${option.name}` : `--${option.name}`, |
|
description: option.description || "", |
|
type: mainOptionType, |
|
multiple: option.multiple, |
|
defaultValue: option.defaultValue, |
|
}; |
|
|
|
if (needNegativeOption) { |
|
negativeOption = { |
|
flags: `--no-${option.name}`, |
|
description: |
|
negatedDescription || option.negatedDescription || `Negative '${option.name}' option.`, |
|
}; |
|
} |
|
} else { |
|
mainOption = { |
|
flags: option.alias ? `-${option.alias}, --${option.name}` : `--${option.name}`, |
|
// TODO `describe` used by `webpack-dev-server@3` |
|
description: option.description || option.describe || "", |
|
type: option.type |
|
? new Set(Array.isArray(option.type) ? option.type : [option.type]) |
|
: new Set([Boolean]), |
|
multiple: option.multiple, |
|
defaultValue: option.defaultValue, |
|
}; |
|
|
|
if (option.negative) { |
|
negativeOption = { |
|
flags: `--no-${option.name}`, |
|
description: option.negatedDescription |
|
? option.negatedDescription |
|
: `Negative '${option.name}' option.`, |
|
}; |
|
} |
|
} |
|
|
|
if (mainOption.type.size > 1 && mainOption.type.has(Boolean)) { |
|
mainOption.flags = `${mainOption.flags} [value${mainOption.multiple ? "..." : ""}]`; |
|
} else if (mainOption.type.size > 0 && !mainOption.type.has(Boolean)) { |
|
mainOption.flags = `${mainOption.flags} <value${mainOption.multiple ? "..." : ""}>`; |
|
} |
|
|
|
if (mainOption.type.size === 1) { |
|
if (mainOption.type.has(Number)) { |
|
let skipDefault = true; |
|
|
|
const optionForCommand = new Option(mainOption.flags, mainOption.description) |
|
.argParser((value, prev = []) => { |
|
if (mainOption.defaultValue && mainOption.multiple && skipDefault) { |
|
prev = []; |
|
skipDefault = false; |
|
} |
|
|
|
return mainOption.multiple ? [].concat(prev).concat(Number(value)) : Number(value); |
|
}) |
|
.default(mainOption.defaultValue); |
|
|
|
optionForCommand.helpLevel = option.helpLevel; |
|
|
|
command.addOption(optionForCommand); |
|
} else if (mainOption.type.has(String)) { |
|
let skipDefault = true; |
|
|
|
const optionForCommand = new Option(mainOption.flags, mainOption.description) |
|
.argParser((value, prev = []) => { |
|
if (mainOption.defaultValue && mainOption.multiple && skipDefault) { |
|
prev = []; |
|
skipDefault = false; |
|
} |
|
|
|
return mainOption.multiple ? [].concat(prev).concat(value) : value; |
|
}) |
|
.default(mainOption.defaultValue); |
|
|
|
optionForCommand.helpLevel = option.helpLevel; |
|
|
|
command.addOption(optionForCommand); |
|
} else if (mainOption.type.has(Boolean)) { |
|
const optionForCommand = new Option(mainOption.flags, mainOption.description).default( |
|
mainOption.defaultValue, |
|
); |
|
|
|
optionForCommand.helpLevel = option.helpLevel; |
|
|
|
command.addOption(optionForCommand); |
|
} else { |
|
const optionForCommand = new Option(mainOption.flags, mainOption.description) |
|
.argParser(Array.from(mainOption.type)[0]) |
|
.default(mainOption.defaultValue); |
|
|
|
optionForCommand.helpLevel = option.helpLevel; |
|
|
|
command.addOption(optionForCommand); |
|
} |
|
} else if (mainOption.type.size > 1) { |
|
let skipDefault = true; |
|
|
|
const optionForCommand = new Option( |
|
mainOption.flags, |
|
mainOption.description, |
|
mainOption.defaultValue, |
|
) |
|
.argParser((value, prev = []) => { |
|
if (mainOption.defaultValue && mainOption.multiple && skipDefault) { |
|
prev = []; |
|
skipDefault = false; |
|
} |
|
|
|
if (mainOption.type.has(Number)) { |
|
const numberValue = Number(value); |
|
|
|
if (!isNaN(numberValue)) { |
|
return mainOption.multiple ? [].concat(prev).concat(numberValue) : numberValue; |
|
} |
|
} |
|
|
|
if (mainOption.type.has(String)) { |
|
return mainOption.multiple ? [].concat(prev).concat(value) : value; |
|
} |
|
|
|
return value; |
|
}) |
|
.default(mainOption.defaultValue); |
|
|
|
optionForCommand.helpLevel = option.helpLevel; |
|
|
|
command.addOption(optionForCommand); |
|
} else if (mainOption.type.size === 0 && negativeOption) { |
|
const optionForCommand = new Option(mainOption.flags, mainOption.description); |
|
|
|
// Hide stub option |
|
optionForCommand.hideHelp(); |
|
optionForCommand.helpLevel = option.helpLevel; |
|
|
|
command.addOption(optionForCommand); |
|
} |
|
|
|
if (negativeOption) { |
|
const optionForCommand = new Option(negativeOption.flags, negativeOption.description); |
|
|
|
optionForCommand.helpLevel = option.helpLevel; |
|
|
|
command.addOption(optionForCommand); |
|
} |
|
} |
|
|
|
getBuiltInOptions() { |
|
if (this.builtInOptionsCache) { |
|
return this.builtInOptionsCache; |
|
} |
|
|
|
const minimumHelpFlags = [ |
|
"config", |
|
"config-name", |
|
"merge", |
|
"env", |
|
"mode", |
|
"watch", |
|
"watch-options-stdin", |
|
"stats", |
|
"devtool", |
|
"entry", |
|
"target", |
|
"progress", |
|
"json", |
|
"name", |
|
"output-path", |
|
"node-env", |
|
]; |
|
|
|
const builtInFlags = [ |
|
// For configs |
|
{ |
|
name: "config", |
|
alias: "c", |
|
configs: [ |
|
{ |
|
type: "string", |
|
}, |
|
], |
|
multiple: true, |
|
description: "Provide path to a webpack configuration file e.g. ./webpack.config.js.", |
|
}, |
|
{ |
|
name: "config-name", |
|
configs: [ |
|
{ |
|
type: "string", |
|
}, |
|
], |
|
multiple: true, |
|
description: "Name of the configuration to use.", |
|
}, |
|
{ |
|
name: "merge", |
|
alias: "m", |
|
configs: [ |
|
{ |
|
type: "enum", |
|
values: [true], |
|
}, |
|
], |
|
description: "Merge two or more configurations using 'webpack-merge'.", |
|
}, |
|
// Complex configs |
|
{ |
|
name: "env", |
|
type: (value, previous = {}) => { |
|
// for https://github.com/webpack/webpack-cli/issues/2642 |
|
if (value.endsWith("=")) { |
|
value.concat('""'); |
|
} |
|
|
|
// 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) { |
|
if (typeof val === "string") { |
|
prevRef[someKey] = val; |
|
} else { |
|
prevRef[someKey] = true; |
|
} |
|
} |
|
|
|
prevRef = prevRef[someKey]; |
|
}); |
|
|
|
return previous; |
|
}, |
|
multiple: true, |
|
description: "Environment passed to the configuration when it is a function.", |
|
}, |
|
{ |
|
name: "node-env", |
|
configs: [ |
|
{ |
|
type: "string", |
|
}, |
|
], |
|
multiple: false, |
|
description: "Sets process.env.NODE_ENV to the specified value.", |
|
}, |
|
|
|
// Adding more plugins |
|
{ |
|
name: "hot", |
|
alias: "h", |
|
configs: [ |
|
{ |
|
type: "string", |
|
}, |
|
{ |
|
type: "boolean", |
|
}, |
|
], |
|
negative: true, |
|
description: "Enables Hot Module Replacement", |
|
negatedDescription: "Disables Hot Module Replacement.", |
|
}, |
|
{ |
|
name: "analyze", |
|
configs: [ |
|
{ |
|
type: "enum", |
|
values: [true], |
|
}, |
|
], |
|
multiple: false, |
|
description: "It invokes webpack-bundle-analyzer plugin to get bundle information.", |
|
}, |
|
{ |
|
name: "progress", |
|
configs: [ |
|
{ |
|
type: "string", |
|
}, |
|
{ |
|
type: "enum", |
|
values: [true], |
|
}, |
|
], |
|
description: "Print compilation progress during build.", |
|
}, |
|
{ |
|
name: "prefetch", |
|
configs: [ |
|
{ |
|
type: "string", |
|
}, |
|
], |
|
description: "Prefetch this request.", |
|
}, |
|
|
|
// Output options |
|
{ |
|
name: "json", |
|
configs: [ |
|
{ |
|
type: "string", |
|
}, |
|
{ |
|
type: "enum", |
|
values: [true], |
|
}, |
|
], |
|
alias: "j", |
|
description: "Prints result as JSON or store it in a file.", |
|
}, |
|
|
|
// For webpack@4 |
|
{ |
|
name: "entry", |
|
configs: [ |
|
{ |
|
type: "string", |
|
}, |
|
], |
|
multiple: true, |
|
description: "The entry point(s) of your application e.g. ./src/main.js.", |
|
}, |
|
{ |
|
name: "output-path", |
|
alias: "o", |
|
configs: [ |
|
{ |
|
type: "string", |
|
}, |
|
], |
|
description: "Output location of the file generated by webpack e.g. ./dist/.", |
|
}, |
|
{ |
|
name: "target", |
|
alias: "t", |
|
configs: [ |
|
{ |
|
type: "string", |
|
}, |
|
], |
|
multiple: this.webpack.cli !== undefined, |
|
description: "Sets the build target e.g. node.", |
|
}, |
|
{ |
|
name: "devtool", |
|
configs: [ |
|
{ |
|
type: "string", |
|
}, |
|
{ |
|
type: "enum", |
|
values: [false], |
|
}, |
|
], |
|
negative: true, |
|
alias: "d", |
|
description: "Determine source maps to use.", |
|
negatedDescription: "Do not generate source maps.", |
|
}, |
|
{ |
|
name: "mode", |
|
configs: [ |
|
{ |
|
type: "string", |
|
}, |
|
], |
|
description: "Defines the mode to pass to webpack.", |
|
}, |
|
{ |
|
name: "name", |
|
configs: [ |
|
{ |
|
type: "string", |
|
}, |
|
], |
|
description: "Name of the configuration. Used when loading multiple configurations.", |
|
}, |
|
{ |
|
name: "stats", |
|
configs: [ |
|
{ |
|
type: "string", |
|
}, |
|
{ |
|
type: "boolean", |
|
}, |
|
], |
|
negative: true, |
|
description: "It instructs webpack on how to treat the stats e.g. verbose.", |
|
negatedDescription: "Disable stats output.", |
|
}, |
|
{ |
|
name: "watch", |
|
configs: [ |
|
{ |
|
type: "boolean", |
|
}, |
|
], |
|
negative: true, |
|
alias: "w", |
|
description: "Watch for files changes.", |
|
negatedDescription: "Do not watch for file changes.", |
|
}, |
|
{ |
|
name: "watch-options-stdin", |
|
configs: [ |
|
{ |
|
type: "boolean", |
|
}, |
|
], |
|
negative: true, |
|
description: "Stop watching when stdin stream has ended.", |
|
negatedDescription: "Do not stop watching when stdin stream has ended.", |
|
}, |
|
]; |
|
|
|
// 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 = this.webpack.cli |
|
? Object.entries(this.webpack.cli.getArguments()).map(([flag, meta]) => { |
|
const inBuiltIn = builtInFlags.find((builtInFlag) => builtInFlag.name === flag); |
|
|
|
if (inBuiltIn) { |
|
return { |
|
...meta, |
|
name: flag, |
|
group: "core", |
|
...inBuiltIn, |
|
configs: meta.configs || [], |
|
}; |
|
} |
|
|
|
return { ...meta, name: flag, group: "core" }; |
|
}) |
|
: []; |
|
|
|
const options = [] |
|
.concat( |
|
builtInFlags.filter( |
|
(builtInFlag) => !coreFlags.find((coreFlag) => builtInFlag.name === coreFlag.name), |
|
), |
|
) |
|
.concat(coreFlags) |
|
.map((option) => { |
|
option.helpLevel = minimumHelpFlags.includes(option.name) ? "minimum" : "verbose"; |
|
|
|
return option; |
|
}); |
|
|
|
this.builtInOptionsCache = options; |
|
|
|
return options; |
|
} |
|
|
|
async loadWebpack(handleError = true) { |
|
return this.tryRequireThenImport(WEBPACK_PACKAGE, handleError); |
|
} |
|
|
|
async run(args, parseOptions) { |
|
// Built-in internal commands |
|
const buildCommandOptions = { |
|
name: "build [entries...]", |
|
alias: ["bundle", "b"], |
|
description: "Run webpack (default command, can be omitted).", |
|
usage: "[entries...] [options]", |
|
dependencies: [WEBPACK_PACKAGE], |
|
}; |
|
const watchCommandOptions = { |
|
name: "watch [entries...]", |
|
alias: "w", |
|
description: "Run webpack and watch for files changes.", |
|
usage: "[entries...] [options]", |
|
dependencies: [WEBPACK_PACKAGE], |
|
}; |
|
const versionCommandOptions = { |
|
name: "version [commands...]", |
|
alias: "v", |
|
description: |
|
"Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.", |
|
}; |
|
const helpCommandOptions = { |
|
name: "help [command] [option]", |
|
alias: "h", |
|
description: "Display help for commands and options.", |
|
}; |
|
// Built-in external commands |
|
const externalBuiltInCommandsInfo = [ |
|
{ |
|
name: "serve [entries...]", |
|
alias: ["server", "s"], |
|
pkg: "@webpack-cli/serve", |
|
}, |
|
{ |
|
name: "info", |
|
alias: "i", |
|
pkg: "@webpack-cli/info", |
|
}, |
|
{ |
|
name: "init", |
|
alias: ["create", "new", "c", "n"], |
|
pkg: "@webpack-cli/generators", |
|
}, |
|
{ |
|
name: "loader", |
|
alias: "l", |
|
pkg: "@webpack-cli/generators", |
|
}, |
|
{ |
|
name: "plugin", |
|
alias: "p", |
|
pkg: "@webpack-cli/generators", |
|
}, |
|
{ |
|
name: "migrate", |
|
alias: "m", |
|
pkg: "@webpack-cli/migrate", |
|
}, |
|
{ |
|
name: "configtest [config-path]", |
|
alias: "t", |
|
pkg: "@webpack-cli/configtest", |
|
}, |
|
]; |
|
|
|
const knownCommands = [ |
|
buildCommandOptions, |
|
watchCommandOptions, |
|
versionCommandOptions, |
|
helpCommandOptions, |
|
...externalBuiltInCommandsInfo, |
|
]; |
|
const getCommandName = (name) => name.split(" ")[0]; |
|
const isKnownCommand = (name) => |
|
knownCommands.find( |
|
(command) => |
|
getCommandName(command.name) === name || |
|
(Array.isArray(command.alias) ? command.alias.includes(name) : command.alias === name), |
|
); |
|
const isCommand = (input, commandOptions) => { |
|
const longName = getCommandName(commandOptions.name); |
|
|
|
if (input === longName) { |
|
return true; |
|
} |
|
|
|
if (commandOptions.alias) { |
|
if (Array.isArray(commandOptions.alias)) { |
|
return commandOptions.alias.includes(input); |
|
} else { |
|
return commandOptions.alias === input; |
|
} |
|
} |
|
|
|
return false; |
|
}; |
|
const findCommandByName = (name) => |
|
this.program.commands.find( |
|
(command) => name === command.name() || command.aliases().includes(name), |
|
); |
|
const isOption = (value) => value.startsWith("-"); |
|
const isGlobalOption = (value) => |
|
value === "--color" || |
|
value === "--no-color" || |
|
value === "-v" || |
|
value === "--version" || |
|
value === "-h" || |
|
value === "--help"; |
|
|
|
const loadCommandByName = async (commandName, allowToInstall = false) => { |
|
const isBuildCommandUsed = isCommand(commandName, buildCommandOptions); |
|
const isWatchCommandUsed = isCommand(commandName, watchCommandOptions); |
|
|
|
if (isBuildCommandUsed || isWatchCommandUsed) { |
|
await this.makeCommand( |
|
isBuildCommandUsed ? buildCommandOptions : watchCommandOptions, |
|
async () => { |
|
this.webpack = await this.loadWebpack(); |
|
|
|
return isWatchCommandUsed |
|
? this.getBuiltInOptions().filter((option) => option.name !== "watch") |
|
: this.getBuiltInOptions(); |
|
}, |
|
async (entries, options) => { |
|
if (entries.length > 0) { |
|
options.entry = [...entries, ...(options.entry || [])]; |
|
} |
|
|
|
await this.runWebpack(options, isWatchCommandUsed); |
|
}, |
|
); |
|
} else if (isCommand(commandName, helpCommandOptions)) { |
|
// Stub for the `help` command |
|
this.makeCommand(helpCommandOptions, [], () => {}); |
|
} else if (isCommand(commandName, versionCommandOptions)) { |
|
// Stub for the `version` command |
|
this.makeCommand(versionCommandOptions, [], () => {}); |
|
} else { |
|
const builtInExternalCommandInfo = externalBuiltInCommandsInfo.find( |
|
(externalBuiltInCommandInfo) => |
|
getCommandName(externalBuiltInCommandInfo.name) === commandName || |
|
(Array.isArray(externalBuiltInCommandInfo.alias) |
|
? externalBuiltInCommandInfo.alias.includes(commandName) |
|
: externalBuiltInCommandInfo.alias === commandName), |
|
); |
|
|
|
let pkg; |
|
|
|
if (builtInExternalCommandInfo) { |
|
({ pkg } = builtInExternalCommandInfo); |
|
} else { |
|
pkg = commandName; |
|
} |
|
|
|
if (pkg !== "webpack-cli" && !this.checkPackageExists(pkg)) { |
|
if (!allowToInstall) { |
|
return; |
|
} |
|
|
|
pkg = await this.doInstall(pkg, { |
|
preMessage: () => { |
|
this.logger.error( |
|
`For using this command you need to install: '${this.colors.green(pkg)}' package.`, |
|
); |
|
}, |
|
}); |
|
} |
|
|
|
let loadedCommand; |
|
|
|
try { |
|
loadedCommand = await this.tryRequireThenImport(pkg, false); |
|
} catch (error) { |
|
// Ignore, command is not installed |
|
|
|
return; |
|
} |
|
|
|
let command; |
|
|
|
try { |
|
command = new loadedCommand(); |
|
|
|
await command.apply(this); |
|
} catch (error) { |
|
this.logger.error(`Unable to load '${pkg}' command`); |
|
this.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 { operands } = this.program.parseOptions(this.program.args); |
|
const operand = |
|
typeof operands[0] !== "undefined" |
|
? operands[0] |
|
: getCommandName(buildCommandOptions.name); |
|
|
|
if (operand) { |
|
const command = findCommandByName(operand); |
|
|
|
if (!command) { |
|
this.logger.error(`Can't find and load command '${operand}'`); |
|
this.logger.error("Run 'webpack --help' to see available commands and options"); |
|
process.exit(2); |
|
} |
|
|
|
const levenshtein = require("fastest-levenshtein"); |
|
|
|
command.options.forEach((option) => { |
|
if (!option.hidden && levenshtein.distance(name, option.long.slice(2)) < 3) { |
|
this.logger.error(`Did you mean '--${option.name()}'?`); |
|
} |
|
}); |
|
} |
|
} |
|
} |
|
|
|
// Codes: |
|
// - commander.unknownCommand |
|
// - commander.missingArgument |
|
// - commander.missingMandatoryOptionValue |
|
// - commander.optionMissingArgument |
|
|
|
this.logger.error("Run 'webpack --help' to see available commands and options"); |
|
process.exit(2); |
|
}); |
|
|
|
// Default `--color` and `--no-color` options |
|
const cli = this; |
|
this.program.option("--color", "Enable colors on console."); |
|
this.program.on("option:color", function () { |
|
const { color } = this.opts(); |
|
|
|
cli.isColorSupportChanged = color; |
|
cli.colors = cli.createColors(color); |
|
}); |
|
this.program.option("--no-color", "Disable colors on console."); |
|
this.program.on("option:no-color", function () { |
|
const { color } = this.opts(); |
|
|
|
cli.isColorSupportChanged = color; |
|
cli.colors = cli.createColors(color); |
|
}); |
|
|
|
// Make `-v, --version` options |
|
// Make `version|v [commands...]` command |
|
const outputVersion = async (options) => { |
|
// Filter `bundle`, `watch`, `version` and `help` commands |
|
const possibleCommandNames = options.filter( |
|
(option) => |
|
!isCommand(option, buildCommandOptions) && |
|
!isCommand(option, watchCommandOptions) && |
|
!isCommand(option, versionCommandOptions) && |
|
!isCommand(option, helpCommandOptions), |
|
); |
|
|
|
possibleCommandNames.forEach((possibleCommandName) => { |
|
if (!isOption(possibleCommandName)) { |
|
return; |
|
} |
|
|
|
this.logger.error(`Unknown option '${possibleCommandName}'`); |
|
this.logger.error("Run 'webpack --help' to see available commands and options"); |
|
process.exit(2); |
|
}); |
|
|
|
if (possibleCommandNames.length > 0) { |
|
await Promise.all( |
|
possibleCommandNames.map((possibleCommand) => loadCommandByName(possibleCommand)), |
|
); |
|
|
|
for (const possibleCommandName of possibleCommandNames) { |
|
const foundCommand = findCommandByName(possibleCommandName); |
|
|
|
if (!foundCommand) { |
|
this.logger.error(`Unknown command '${possibleCommandName}'`); |
|
this.logger.error("Run 'webpack --help' to see available commands and options"); |
|
process.exit(2); |
|
} |
|
|
|
try { |
|
const { name, version } = this.loadJSONFile(`${foundCommand.pkg}/package.json`); |
|
|
|
this.logger.raw(`${name} ${version}`); |
|
} catch (e) { |
|
this.logger.error(`Error: External package '${foundCommand.pkg}' not found`); |
|
process.exit(2); |
|
} |
|
} |
|
} |
|
|
|
let webpack; |
|
|
|
try { |
|
webpack = await this.loadWebpack(false); |
|
} catch (_error) { |
|
// Nothing |
|
} |
|
|
|
this.logger.raw(`webpack: ${webpack ? webpack.version : "not installed"}`); |
|
|
|
const pkgJSON = this.loadJSONFile("../package.json"); |
|
|
|
this.logger.raw(`webpack-cli: ${pkgJSON.version}`); |
|
|
|
let devServer; |
|
|
|
try { |
|
devServer = await this.loadJSONFile("webpack-dev-server/package.json", false); |
|
} catch (_error) { |
|
// Nothing |
|
} |
|
|
|
this.logger.raw(`webpack-dev-server ${devServer ? devServer.version : "not installed"}`); |
|
|
|
process.exit(0); |
|
}; |
|
this.program.option( |
|
"-v, --version", |
|
"Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.", |
|
); |
|
|
|
const outputHelp = async (options, isVerbose, isHelpCommandSyntax, program) => { |
|
const { bold } = this.colors; |
|
const outputIncorrectUsageOfHelp = () => { |
|
this.logger.error("Incorrect use of help"); |
|
this.logger.error( |
|
"Please use: 'webpack help [command] [option]' | 'webpack [command] --help'", |
|
); |
|
this.logger.error("Run 'webpack --help' to see available commands and options"); |
|
process.exit(2); |
|
}; |
|
|
|
const isGlobalHelp = options.length === 0; |
|
const isCommandHelp = options.length === 1 && !isOption(options[0]); |
|
|
|
if (isGlobalHelp || isCommandHelp) { |
|
program.configureHelp({ |
|
sortSubcommands: true, |
|
// Support multiple aliases |
|
commandUsage: (command) => { |
|
let parentCmdNames = ""; |
|
|
|
for (let parentCmd = command.parent; parentCmd; parentCmd = parentCmd.parent) { |
|
parentCmdNames = `${parentCmd.name()} ${parentCmdNames}`; |
|
} |
|
|
|
if (isGlobalHelp) { |
|
return `${parentCmdNames}${command.usage()}\n${bold( |
|
"Alternative usage to run commands:", |
|
)} ${parentCmdNames}[command] [options]`; |
|
} |
|
|
|
return `${parentCmdNames}${command.name()}|${command |
|
.aliases() |
|
.join("|")} ${command.usage()}`; |
|
}, |
|
// Support multiple aliases |
|
subcommandTerm: (command) => { |
|
const humanReadableArgumentName = (argument) => { |
|
const nameOutput = argument.name + (argument.variadic === true ? "..." : ""); |
|
|
|
return argument.required ? "<" + nameOutput + ">" : "[" + nameOutput + "]"; |
|
}; |
|
const args = command._args.map((arg) => humanReadableArgumentName(arg)).join(" "); |
|
|
|
return `${command.name()}|${command.aliases().join("|")}${args ? ` ${args}` : ""}${ |
|
command.options.length > 0 ? " [options]" : "" |
|
}`; |
|
}, |
|
visibleOptions: function visibleOptions(command) { |
|
return command.options.filter((option) => { |
|
if (option.hidden) { |
|
return false; |
|
} |
|
|
|
switch (option.helpLevel) { |
|
case "verbose": |
|
return isVerbose; |
|
case "minimum": |
|
default: |
|
return true; |
|
} |
|
}); |
|
}, |
|
padWidth(command, helper) { |
|
return Math.max( |
|
helper.longestArgumentTermLength(command, helper), |
|
helper.longestOptionTermLength(command, helper), |
|
// For global options |
|
helper.longestOptionTermLength(program, helper), |
|
helper.longestSubcommandTermLength(isGlobalHelp ? program : command, helper), |
|
); |
|
}, |
|
formatHelp: (command, helper) => { |
|
const termWidth = helper.padWidth(command, helper); |
|
const helpWidth = helper.helpWidth || process.env.WEBPACK_CLI_HELP_WIDTH || 80; |
|
const itemIndentWidth = 2; |
|
const itemSeparatorWidth = 2; // between term and description |
|
|
|
const formatItem = (term, description) => { |
|
if (description) { |
|
const fullText = `${term.padEnd(termWidth + itemSeparatorWidth)}${description}`; |
|
|
|
return helper.wrap( |
|
fullText, |
|
helpWidth - itemIndentWidth, |
|
termWidth + itemSeparatorWidth, |
|
); |
|
} |
|
|
|
return term; |
|
}; |
|
|
|
const formatList = (textArray) => |
|
textArray.join("\n").replace(/^/gm, " ".repeat(itemIndentWidth)); |
|
|
|
// Usage |
|
let output = [`${bold("Usage:")} ${helper.commandUsage(command)}`, ""]; |
|
|
|
// Description |
|
const commandDescription = isGlobalHelp |
|
? "The build tool for modern web applications." |
|
: helper.commandDescription(command); |
|
|
|
if (commandDescription.length > 0) { |
|
output = output.concat([commandDescription, ""]); |
|
} |
|
|
|
// Arguments |
|
const argumentList = helper |
|
.visibleArguments(command) |
|
.map((argument) => formatItem(argument.term, argument.description)); |
|
|
|
if (argumentList.length > 0) { |
|
output = output.concat([bold("Arguments:"), formatList(argumentList), ""]); |
|
} |
|
|
|
// Options |
|
const optionList = helper |
|
.visibleOptions(command) |
|
.map((option) => |
|
formatItem(helper.optionTerm(option), helper.optionDescription(option)), |
|
); |
|
|
|
if (optionList.length > 0) { |
|
output = output.concat([bold("Options:"), formatList(optionList), ""]); |
|
} |
|
|
|
// Global options |
|
const globalOptionList = program.options.map((option) => |
|
formatItem(helper.optionTerm(option), helper.optionDescription(option)), |
|
); |
|
|
|
if (globalOptionList.length > 0) { |
|
output = output.concat([bold("Global options:"), formatList(globalOptionList), ""]); |
|
} |
|
|
|
// Commands |
|
const commandList = helper |
|
.visibleCommands(isGlobalHelp ? program : command) |
|
.map((command) => |
|
formatItem(helper.subcommandTerm(command), helper.subcommandDescription(command)), |
|
); |
|
|
|
if (commandList.length > 0) { |
|
output = output.concat([bold("Commands:"), formatList(commandList), ""]); |
|
} |
|
|
|
return output.join("\n"); |
|
}, |
|
}); |
|
|
|
if (isGlobalHelp) { |
|
await Promise.all( |
|
knownCommands.map((knownCommand) => { |
|
return loadCommandByName(getCommandName(knownCommand.name)); |
|
}), |
|
); |
|
|
|
const buildCommand = findCommandByName(getCommandName(buildCommandOptions.name)); |
|
|
|
this.logger.raw(buildCommand.helpInformation()); |
|
} else { |
|
const name = options[0]; |
|
|
|
await loadCommandByName(name); |
|
|
|
const command = findCommandByName(name); |
|
|
|
if (!command) { |
|
const builtInCommandUsed = externalBuiltInCommandsInfo.find( |
|
(command) => command.name.includes(name) || name === command.alias, |
|
); |
|
if (typeof builtInCommandUsed !== "undefined") { |
|
this.logger.error( |
|
`For using '${name}' command you need to install '${builtInCommandUsed.pkg}' package.`, |
|
); |
|
} else { |
|
this.logger.error(`Can't find and load command '${name}'`); |
|
this.logger.error("Run 'webpack --help' to see available commands and options."); |
|
} |
|
process.exit(2); |
|
} |
|
|
|
this.logger.raw(command.helpInformation()); |
|
} |
|
} else if (isHelpCommandSyntax) { |
|
let isCommandSpecified = false; |
|
let commandName = getCommandName(buildCommandOptions.name); |
|
let optionName; |
|
|
|
if (options.length === 1) { |
|
optionName = options[0]; |
|
} else if (options.length === 2) { |
|
isCommandSpecified = true; |
|
commandName = options[0]; |
|
optionName = options[1]; |
|
|
|
if (isOption(commandName)) { |
|
outputIncorrectUsageOfHelp(); |
|
} |
|
} else { |
|
outputIncorrectUsageOfHelp(); |
|
} |
|
|
|
await loadCommandByName(commandName); |
|
|
|
const command = isGlobalOption(optionName) ? program : findCommandByName(commandName); |
|
|
|
if (!command) { |
|
this.logger.error(`Can't find and load command '${commandName}'`); |
|
this.logger.error("Run 'webpack --help' to see available commands and options"); |
|
process.exit(2); |
|
} |
|
|
|
const option = command.options.find( |
|
(option) => option.short === optionName || option.long === optionName, |
|
); |
|
|
|
if (!option) { |
|
this.logger.error(`Unknown option '${optionName}'`); |
|
this.logger.error("Run 'webpack --help' to see available commands and options"); |
|
process.exit(2); |
|
} |
|
|
|
const nameOutput = |
|
option.flags.replace(/^.+[[<]/, "").replace(/(\.\.\.)?[\]>].*$/, "") + |
|
(option.variadic === true ? "..." : ""); |
|
const value = option.required |
|
? "<" + nameOutput + ">" |
|
: option.optional |
|
? "[" + nameOutput + "]" |
|
: ""; |
|
|
|
this.logger.raw( |
|
`${bold("Usage")}: webpack${isCommandSpecified ? ` ${commandName}` : ""} ${option.long}${ |
|
value ? ` ${value}` : "" |
|
}`, |
|
); |
|
|
|
if (option.short) { |
|
this.logger.raw( |
|
`${bold("Short:")} webpack${isCommandSpecified ? ` ${commandName}` : ""} ${ |
|
option.short |
|
}${value ? ` ${value}` : ""}`, |
|
); |
|
} |
|
|
|
if (option.description) { |
|
this.logger.raw(`${bold("Description:")} ${option.description}`); |
|
} |
|
|
|
if (!option.negate && option.defaultValue) { |
|
this.logger.raw(`${bold("Default value:")} ${JSON.stringify(option.defaultValue)}`); |
|
} |
|
|
|
const flag = this.getBuiltInOptions().find((flag) => option.long === `--${flag.name}`); |
|
|
|
if (flag && flag.configs) { |
|
const possibleValues = flag.configs.reduce((accumulator, currentValue) => { |
|
if (currentValue.values) { |
|
return accumulator.concat(currentValue.values); |
|
} else { |
|
return accumulator; |
|
} |
|
}, []); |
|
|
|
if (possibleValues.length > 0) { |
|
this.logger.raw( |
|
`${bold("Possible values:")} ${JSON.stringify(possibleValues.join(" | "))}`, |
|
); |
|
} |
|
} |
|
|
|
this.logger.raw(""); |
|
|
|
// TODO implement this after refactor cli arguments |
|
// logger.raw('Documentation: https://webpack.js.org/option/name/'); |
|
} else { |
|
outputIncorrectUsageOfHelp(); |
|
} |
|
|
|
this.logger.raw( |
|
"To see list of all supported commands and options run 'webpack --help=verbose'.\n", |
|
); |
|
this.logger.raw(`${bold("Webpack documentation:")} https://webpack.js.org/.`); |
|
this.logger.raw(`${bold("CLI documentation:")} https://webpack.js.org/api/cli/.`); |
|
this.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 (options, program) => { |
|
if (!isInternalActionCalled) { |
|
isInternalActionCalled = true; |
|
} else { |
|
this.logger.error("No commands found to run"); |
|
process.exit(2); |
|
} |
|
|
|
// Command and options |
|
const { operands, unknown } = this.program.parseOptions(program.args); |
|
const defaultCommandToRun = getCommandName(buildCommandOptions.name); |
|
const hasOperand = typeof operands[0] !== "undefined"; |
|
const operand = hasOperand ? operands[0] : defaultCommandToRun; |
|
const isHelpOption = typeof options.help !== "undefined"; |
|
const isHelpCommandSyntax = isCommand(operand, helpCommandOptions); |
|
|
|
if (isHelpOption || isHelpCommandSyntax) { |
|
let isVerbose = false; |
|
|
|
if (isHelpOption) { |
|
if (typeof options.help === "string") { |
|
if (options.help !== "verbose") { |
|
this.logger.error("Unknown value for '--help' option, please use '--help=verbose'"); |
|
process.exit(2); |
|
} |
|
|
|
isVerbose = true; |
|
} |
|
} |
|
|
|
this.program.forHelp = true; |
|
|
|
const optionsForHelp = [] |
|
.concat(isHelpOption && hasOperand ? [operand] : []) |
|
// Syntax `webpack help [command]` |
|
.concat(operands.slice(1)) |
|
// Syntax `webpack help [option]` |
|
.concat(unknown) |
|
.concat( |
|
isHelpCommandSyntax && typeof options.color !== "undefined" |
|
? [options.color ? "--color" : "--no-color"] |
|
: [], |
|
) |
|
.concat( |
|
isHelpCommandSyntax && typeof options.version !== "undefined" ? ["--version"] : [], |
|
); |
|
|
|
await outputHelp(optionsForHelp, isVerbose, isHelpCommandSyntax, program); |
|
} |
|
|
|
const isVersionOption = typeof options.version !== "undefined"; |
|
const isVersionCommandSyntax = isCommand(operand, versionCommandOptions); |
|
|
|
if (isVersionOption || isVersionCommandSyntax) { |
|
const optionsForVersion = [] |
|
.concat(isVersionOption ? [operand] : []) |
|
.concat(operands.slice(1)) |
|
.concat(unknown); |
|
|
|
await outputVersion(optionsForVersion, program); |
|
} |
|
|
|
let commandToRun = operand; |
|
let commandOperands = operands.slice(1); |
|
|
|
if (isKnownCommand(commandToRun)) { |
|
await loadCommandByName(commandToRun, true); |
|
} else { |
|
const isEntrySyntax = fs.existsSync(operand); |
|
|
|
if (isEntrySyntax) { |
|
commandToRun = defaultCommandToRun; |
|
commandOperands = operands; |
|
|
|
await loadCommandByName(commandToRun); |
|
} else { |
|
this.logger.error(`Unknown command or entry '${operand}'`); |
|
|
|
const levenshtein = require("fastest-levenshtein"); |
|
const found = knownCommands.find( |
|
(commandOptions) => |
|
levenshtein.distance(operand, getCommandName(commandOptions.name)) < 3, |
|
); |
|
|
|
if (found) { |
|
this.logger.error( |
|
`Did you mean '${getCommandName(found.name)}' (alias '${ |
|
Array.isArray(found.alias) ? found.alias.join(", ") : found.alias |
|
}')?`, |
|
); |
|
} |
|
|
|
this.logger.error("Run 'webpack --help' to see available commands and options"); |
|
process.exit(2); |
|
} |
|
} |
|
|
|
await this.program.parseAsync([commandToRun, ...commandOperands, ...unknown], { |
|
from: "user", |
|
}); |
|
}); |
|
|
|
await this.program.parseAsync(args, parseOptions); |
|
} |
|
|
|
async loadConfig(options) { |
|
const interpret = require("interpret"); |
|
const loadConfigByPath = async (configPath, argv = {}) => { |
|
const ext = path.extname(configPath); |
|
const interpreted = Object.keys(interpret.jsVariants).find((variant) => variant === ext); |
|
|
|
if (interpreted) { |
|
const rechoir = require("rechoir"); |
|
|
|
try { |
|
rechoir.prepare(interpret.extensions, configPath); |
|
} catch (error) { |
|
if (error.failures) { |
|
this.logger.error(`Unable load '${configPath}'`); |
|
this.logger.error(error.message); |
|
error.failures.forEach((failure) => { |
|
this.logger.error(failure.error.message); |
|
}); |
|
this.logger.error("Please install one of them"); |
|
process.exit(2); |
|
} |
|
|
|
this.logger.error(error); |
|
process.exit(2); |
|
} |
|
} |
|
|
|
let options; |
|
|
|
try { |
|
options = await this.tryRequireThenImport(configPath, false); |
|
} catch (error) { |
|
this.logger.error(`Failed to load '${configPath}' config`); |
|
|
|
if (this.isValidationError(error)) { |
|
this.logger.error(error.message); |
|
} else { |
|
this.logger.error(error); |
|
} |
|
|
|
process.exit(2); |
|
} |
|
|
|
if (Array.isArray(options)) { |
|
await Promise.all( |
|
options.map(async (_, i) => { |
|
if (typeof options[i].then === "function") { |
|
options[i] = await options[i]; |
|
} |
|
|
|
// `Promise` may return `Function` |
|
if (typeof options[i] === "function") { |
|
// when config is a function, pass the env from args to the config function |
|
options[i] = await options[i](argv.env, argv); |
|
} |
|
}), |
|
); |
|
} else { |
|
if (typeof options.then === "function") { |
|
options = await options; |
|
} |
|
|
|
// `Promise` may return `Function` |
|
if (typeof options === "function") { |
|
// when config is a function, pass the env from args to the config function |
|
options = await options(argv.env, argv); |
|
} |
|
} |
|
|
|
const isObject = (value) => typeof value === "object" && value !== null; |
|
|
|
if (!isObject(options) && !Array.isArray(options)) { |
|
this.logger.error(`Invalid configuration in '${configPath}'`); |
|
|
|
process.exit(2); |
|
} |
|
|
|
return { options, path: configPath }; |
|
}; |
|
|
|
const config = { options: {}, path: new WeakMap() }; |
|
|
|
if (options.config && options.config.length > 0) { |
|
const loadedConfigs = await Promise.all( |
|
options.config.map((configPath) => |
|
loadConfigByPath(path.resolve(configPath), options.argv), |
|
), |
|
); |
|
|
|
config.options = []; |
|
|
|
loadedConfigs.forEach((loadedConfig) => { |
|
const isArray = Array.isArray(loadedConfig.options); |
|
|
|
// TODO we should run webpack multiple times when the `--config` options have multiple values with `--merge`, need to solve for the next major release |
|
if (config.options.length === 0) { |
|
config.options = loadedConfig.options; |
|
} else { |
|
if (!Array.isArray(config.options)) { |
|
config.options = [config.options]; |
|
} |
|
|
|
if (isArray) { |
|
loadedConfig.options.forEach((item) => { |
|
config.options.push(item); |
|
}); |
|
} else { |
|
config.options.push(loadedConfig.options); |
|
} |
|
} |
|
|
|
if (isArray) { |
|
loadedConfig.options.forEach((options) => { |
|
config.path.set(options, loadedConfig.path); |
|
}); |
|
} else { |
|
config.path.set(loadedConfig.options, loadedConfig.path); |
|
} |
|
}); |
|
|
|
config.options = config.options.length === 1 ? config.options[0] : config.options; |
|
} else { |
|
// Order defines the priority, in decreasing 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(interpret.extensions), ".cjs"].map((ext) => ({ |
|
path: path.resolve(filename + ext), |
|
ext: ext, |
|
module: interpret.extensions[ext], |
|
})), |
|
) |
|
.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []); |
|
|
|
let foundDefaultConfigFile; |
|
|
|
for (const defaultConfigFile of defaultConfigFiles) { |
|
if (!fs.existsSync(defaultConfigFile.path)) { |
|
continue; |
|
} |
|
|
|
foundDefaultConfigFile = defaultConfigFile; |
|
break; |
|
} |
|
|
|
if (foundDefaultConfigFile) { |
|
const loadedConfig = await loadConfigByPath(foundDefaultConfigFile.path, options.argv); |
|
|
|
config.options = loadedConfig.options; |
|
|
|
if (Array.isArray(config.options)) { |
|
config.options.forEach((item) => { |
|
config.path.set(item, loadedConfig.path); |
|
}); |
|
} else { |
|
config.path.set(loadedConfig.options, loadedConfig.path); |
|
} |
|
} |
|
} |
|
|
|
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) { |
|
this.logger.error( |
|
notFoundConfigNames |
|
.map((configName) => `Configuration with the name "${configName}" was not found.`) |
|
.join(" "), |
|
); |
|
process.exit(2); |
|
} |
|
} |
|
|
|
if (options.merge) { |
|
const merge = await this.tryRequireThenImport("webpack-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) { |
|
this.logger.error("At least two configurations are required for merge."); |
|
process.exit(2); |
|
} |
|
|
|
const mergedConfigPaths = []; |
|
|
|
config.options = config.options.reduce((accumulator, options) => { |
|
const configPath = config.path.get(options); |
|
const mergedOptions = merge(accumulator, options); |
|
|
|
mergedConfigPaths.push(configPath); |
|
|
|
return mergedOptions; |
|
}, {}); |
|
config.path.set(config.options, mergedConfigPaths); |
|
} |
|
|
|
return config; |
|
} |
|
|
|
async buildConfig(config, options) { |
|
const runFunctionOnEachConfig = (options, fn) => { |
|
if (Array.isArray(options)) { |
|
for (let item of options) { |
|
item = fn(item); |
|
} |
|
} else { |
|
options = fn(options); |
|
} |
|
|
|
return options; |
|
}; |
|
|
|
if (options.analyze) { |
|
if (!this.checkPackageExists("webpack-bundle-analyzer")) { |
|
await this.doInstall("webpack-bundle-analyzer", { |
|
preMessage: () => { |
|
this.logger.error( |
|
`It looks like ${this.colors.yellow("webpack-bundle-analyzer")} is not installed.`, |
|
); |
|
}, |
|
}); |
|
|
|
this.logger.success( |
|
`${this.colors.yellow("webpack-bundle-analyzer")} was installed successfully.`, |
|
); |
|
} |
|
} |
|
|
|
if (typeof options.progress === "string" && options.progress !== "profile") { |
|
this.logger.error( |
|
`'${options.progress}' is an invalid value for the --progress option. Only 'profile' is allowed.`, |
|
); |
|
process.exit(2); |
|
} |
|
|
|
if (typeof options.hot === "string" && options.hot !== "only") { |
|
this.logger.error( |
|
`'${options.hot}' is an invalid value for the --hot option. Use 'only' instead.`, |
|
); |
|
process.exit(2); |
|
} |
|
|
|
const CLIPlugin = await this.tryRequireThenImport("./plugins/CLIPlugin"); |
|
|
|
const internalBuildConfig = (item) => { |
|
// Output warnings |
|
if ( |
|
item.watch && |
|
options.argv && |
|
options.argv.env && |
|
(options.argv.env["WEBPACK_WATCH"] || options.argv.env["WEBPACK_SERVE"]) |
|
) { |
|
this.logger.warn( |
|
`No need to use the '${ |
|
options.argv.env["WEBPACK_WATCH"] ? "watch" : "serve" |
|
}' command together with '{ watch: true }' configuration, it does not make sense.`, |
|
); |
|
|
|
if (options.argv.env["WEBPACK_SERVE"]) { |
|
item.watch = false; |
|
} |
|
} |
|
|
|
// Apply options |
|
if (this.webpack.cli) { |
|
const args = this.getBuiltInOptions() |
|
.filter((flag) => flag.group === "core") |
|
.reduce((accumulator, flag) => { |
|
accumulator[flag.name] = flag; |
|
|
|
return accumulator; |
|
}, {}); |
|
|
|
const values = Object.keys(options).reduce((accumulator, name) => { |
|
if (name === "argv") { |
|
return accumulator; |
|
} |
|
|
|
const kebabName = this.toKebabCase(name); |
|
|
|
if (args[kebabName]) { |
|
accumulator[kebabName] = options[name]; |
|
} |
|
|
|
return accumulator; |
|
}, {}); |
|
|
|
const problems = this.webpack.cli.processArguments(args, item, values); |
|
|
|
if (problems) { |
|
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) => { |
|
this.logger.error( |
|
`${this.capitalizeFirstLetter(problem.type.replace(/-/g, " "))}${ |
|
problem.value ? ` '${problem.value}'` : "" |
|
} for the '--${problem.argument}' option${ |
|
problem.index ? ` by index '${problem.index}'` : "" |
|
}`, |
|
); |
|
|
|
if (problem.expected) { |
|
this.logger.error(`Expected: '${problem.expected}'`); |
|
} |
|
}); |
|
} |
|
|
|
process.exit(2); |
|
} |
|
|
|
// Setup default cache options |
|
if (item.cache && item.cache.type === "filesystem") { |
|
const configPath = config.path.get(item); |
|
|
|
if (configPath) { |
|
if (!item.cache.buildDependencies) { |
|
item.cache.buildDependencies = {}; |
|
} |
|
|
|
if (!item.cache.buildDependencies.defaultConfig) { |
|
item.cache.buildDependencies.defaultConfig = []; |
|
} |
|
|
|
if (Array.isArray(configPath)) { |
|
configPath.forEach((oneOfConfigPath) => { |
|
item.cache.buildDependencies.defaultConfig.push(oneOfConfigPath); |
|
}); |
|
} else { |
|
item.cache.buildDependencies.defaultConfig.push(configPath); |
|
} |
|
} |
|
} |
|
} |
|
|
|
// Setup legacy logic for webpack@4 |
|
// TODO respect `--entry-reset` in th next major release |
|
// TODO drop in the next major release |
|
if (options.entry) { |
|
item.entry = options.entry; |
|
} |
|
|
|
if (options.outputPath) { |
|
item.output = { ...item.output, ...{ path: path.resolve(options.outputPath) } }; |
|
} |
|
|
|
if (options.target) { |
|
item.target = options.target; |
|
} |
|
|
|
if (typeof options.devtool !== "undefined") { |
|
item.devtool = options.devtool; |
|
} |
|
|
|
if (options.name) { |
|
item.name = options.name; |
|
} |
|
|
|
if (typeof options.stats !== "undefined") { |
|
item.stats = options.stats; |
|
} |
|
|
|
if (typeof options.watch !== "undefined") { |
|
item.watch = options.watch; |
|
} |
|
|
|
if (typeof options.watchOptionsStdin !== "undefined") { |
|
item.watchOptions = { ...item.watchOptions, ...{ stdin: options.watchOptionsStdin } }; |
|
} |
|
|
|
if (options.mode) { |
|
item.mode = options.mode; |
|
} |
|
|
|
// Respect `process.env.NODE_ENV` |
|
if ( |
|
!item.mode && |
|
process.env && |
|
process.env.NODE_ENV && |
|
(process.env.NODE_ENV === "development" || |
|
process.env.NODE_ENV === "production" || |
|
process.env.NODE_ENV === "none") |
|
) { |
|
item.mode = process.env.NODE_ENV; |
|
} |
|
|
|
// Setup stats |
|
// TODO remove after drop webpack@4 |
|
const statsForWebpack4 = this.webpack.Stats && this.webpack.Stats.presetToOptions; |
|
|
|
if (statsForWebpack4) { |
|
if (typeof item.stats === "undefined") { |
|
item.stats = {}; |
|
} else if (typeof item.stats === "boolean") { |
|
item.stats = this.webpack.Stats.presetToOptions(item.stats); |
|
} else if ( |
|
typeof item.stats === "string" && |
|
(item.stats === "none" || |
|
item.stats === "verbose" || |
|
item.stats === "detailed" || |
|
item.stats === "normal" || |
|
item.stats === "minimal" || |
|
item.stats === "errors-only" || |
|
item.stats === "errors-warnings") |
|
) { |
|
item.stats = this.webpack.Stats.presetToOptions(item.stats); |
|
} |
|
} else { |
|
if (typeof item.stats === "undefined") { |
|
item.stats = { preset: "normal" }; |
|
} else if (typeof item.stats === "boolean") { |
|
item.stats = item.stats ? { preset: "normal" } : { preset: "none" }; |
|
} else if (typeof item.stats === "string") { |
|
item.stats = { preset: item.stats }; |
|
} |
|
} |
|
|
|
let colors; |
|
|
|
// From arguments |
|
if (typeof this.isColorSupportChanged !== "undefined") { |
|
colors = Boolean(this.isColorSupportChanged); |
|
} |
|
// From stats |
|
else if (typeof item.stats.colors !== "undefined") { |
|
colors = item.stats.colors; |
|
} |
|
// Default |
|
else { |
|
colors = Boolean(this.colors.isColorSupported); |
|
} |
|
|
|
// TODO remove after drop webpack v4 |
|
if (typeof item.stats === "object" && item.stats !== null) { |
|
item.stats.colors = colors; |
|
} |
|
|
|
// Apply CLI plugin |
|
if (!item.plugins) { |
|
item.plugins = []; |
|
} |
|
|
|
item.plugins.unshift( |
|
new CLIPlugin({ |
|
configPath: config.path.get(item), |
|
helpfulOutput: !options.json, |
|
hot: options.hot, |
|
progress: options.progress, |
|
prefetch: options.prefetch, |
|
analyze: options.analyze, |
|
}), |
|
); |
|
|
|
return options; |
|
}; |
|
|
|
runFunctionOnEachConfig(config.options, internalBuildConfig); |
|
|
|
return config; |
|
} |
|
|
|
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 = |
|
this.webpack.ValidationError || this.webpack.WebpackOptionsValidationError; |
|
|
|
return error instanceof ValidationError || error.name === "ValidationError"; |
|
} |
|
|
|
async createCompiler(options, callback) { |
|
if (typeof options.nodeEnv === "string") { |
|
process.env.NODE_ENV = options.nodeEnv; |
|
} |
|
|
|
let config = await this.loadConfig(options); |
|
config = await this.buildConfig(config, options); |
|
|
|
let compiler; |
|
|
|
try { |
|
compiler = this.webpack( |
|
config.options, |
|
callback |
|
? (error, stats) => { |
|
if (error && this.isValidationError(error)) { |
|
this.logger.error(error.message); |
|
process.exit(2); |
|
} |
|
|
|
callback(error, stats); |
|
} |
|
: callback, |
|
); |
|
} catch (error) { |
|
if (this.isValidationError(error)) { |
|
this.logger.error(error.message); |
|
} else { |
|
this.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; |
|
} |
|
|
|
needWatchStdin(compiler) { |
|
if (compiler.compilers) { |
|
return compiler.compilers.some( |
|
(compiler) => compiler.options.watchOptions && compiler.options.watchOptions.stdin, |
|
); |
|
} |
|
|
|
return compiler.options.watchOptions && compiler.options.watchOptions.stdin; |
|
} |
|
|
|
async runWebpack(options, isWatchCommand) { |
|
// eslint-disable-next-line prefer-const |
|
let compiler; |
|
let createJsonStringifyStream; |
|
|
|
if (options.json) { |
|
const jsonExt = await this.tryRequireThenImport("@discoveryjs/json-ext"); |
|
|
|
createJsonStringifyStream = jsonExt.stringifyStream; |
|
} |
|
|
|
const callback = (error, stats) => { |
|
if (error) { |
|
this.logger.error(error); |
|
process.exit(2); |
|
} |
|
|
|
if (stats.hasErrors()) { |
|
process.exitCode = 1; |
|
} |
|
|
|
if (!compiler) { |
|
return; |
|
} |
|
|
|
const statsOptions = compiler.compilers |
|
? { |
|
children: compiler.compilers.map((compiler) => |
|
compiler.options ? compiler.options.stats : undefined, |
|
), |
|
} |
|
: compiler.options |
|
? compiler.options.stats |
|
: undefined; |
|
|
|
// TODO webpack@4 doesn't support `{ children: [{ colors: true }, { colors: true }] }` for stats |
|
const statsForWebpack4 = this.webpack.Stats && this.webpack.Stats.presetToOptions; |
|
|
|
if (compiler.compilers && statsForWebpack4) { |
|
statsOptions.colors = statsOptions.children.some((child) => child.colors); |
|
} |
|
|
|
if (options.json && createJsonStringifyStream) { |
|
const handleWriteError = (error) => { |
|
this.logger.error(error); |
|
process.exit(2); |
|
}; |
|
|
|
if (options.json === true) { |
|
createJsonStringifyStream(stats.toJson(statsOptions)) |
|
.on("error", handleWriteError) |
|
.pipe(process.stdout) |
|
.on("error", handleWriteError) |
|
.on("close", () => process.stdout.write("\n")); |
|
} else { |
|
createJsonStringifyStream(stats.toJson(statsOptions)) |
|
.on("error", handleWriteError) |
|
.pipe(fs.createWriteStream(options.json)) |
|
.on("error", handleWriteError) |
|
// Use stderr to logging |
|
.on("close", () => { |
|
process.stderr.write( |
|
`[webpack-cli] ${this.colors.green( |
|
`stats are successfully stored as json to ${options.json}`, |
|
)}\n`, |
|
); |
|
}); |
|
} |
|
} else { |
|
const printedStats = stats.toString(statsOptions); |
|
|
|
// Avoid extra empty line when `stats: 'none'` |
|
if (printedStats) { |
|
this.logger.raw(printedStats); |
|
} |
|
} |
|
}; |
|
|
|
const env = |
|
isWatchCommand || options.watch |
|
? { WEBPACK_WATCH: true, ...options.env } |
|
: { WEBPACK_BUNDLE: true, WEBPACK_BUILD: true, ...options.env }; |
|
|
|
options.argv = { ...options, env }; |
|
|
|
if (isWatchCommand) { |
|
options.watch = true; |
|
} |
|
|
|
compiler = await this.createCompiler(options, callback); |
|
|
|
if (!compiler) { |
|
return; |
|
} |
|
|
|
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(); |
|
} |
|
} |
|
} |
|
|
|
module.exports = WebpackCLI;
|
|
|