| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716 | /*	MIT License http://www.opensource.org/licenses/mit-license.php	Author Tobias Koppers @sokra*/"use strict";const path = require("path");const webpackSchema = require("../schemas/WebpackOptions.json");/** @typedef {import("json-schema").JSONSchema4} JSONSchema4 *//** @typedef {import("json-schema").JSONSchema6} JSONSchema6 *//** @typedef {import("json-schema").JSONSchema7} JSONSchema7 *//** @typedef {JSONSchema4 | JSONSchema6 | JSONSchema7} JSONSchema *//** @typedef {JSONSchema & { absolutePath: boolean, instanceof: string, cli: { helper?: boolean, exclude?: boolean, description?: string, negatedDescription?: string, resetDescription?: string } }} Schema */// TODO add originPath to PathItem for better errors/** * @typedef {object} PathItem * @property {Schema} schema the part of the schema * @property {string} path the path in the config *//** @typedef {"unknown-argument" | "unexpected-non-array-in-path" | "unexpected-non-object-in-path" | "multiple-values-unexpected" | "invalid-value"} ProblemType *//** @typedef {string | number | boolean | RegExp} Value *//** * @typedef {object} Problem * @property {ProblemType} type * @property {string} path * @property {string} argument * @property {Value=} value * @property {number=} index * @property {string=} expected *//** * @typedef {object} LocalProblem * @property {ProblemType} type * @property {string} path * @property {string=} expected *//** @typedef {{ [key: string]: EnumValue }} EnumValueObject *//** @typedef {EnumValue[]} EnumValueArray *//** @typedef {string | number | boolean | EnumValueObject | EnumValueArray | null} EnumValue *//** * @typedef {object} ArgumentConfig * @property {string=} description * @property {string=} negatedDescription * @property {string} path * @property {boolean} multiple * @property {"enum" | "string" | "path" | "number" | "boolean" | "RegExp" | "reset"} type * @property {EnumValue[]=} values *//** @typedef {"string" | "number" | "boolean"} SimpleType *//** * @typedef {object} Argument * @property {string | undefined} description * @property {SimpleType} simpleType * @property {boolean} multiple * @property {ArgumentConfig[]} configs *//** @typedef {Record<string, Argument>} Flags *//** @typedef {Record<string, EXPECTED_ANY>} ObjectConfiguration *//** * @param {Schema=} schema a json schema to create arguments for (by default webpack schema is used) * @returns {Flags} object of arguments */const getArguments = (schema = webpackSchema) => {	/** @type {Flags} */	const flags = {};	/**	 * @param {string} input input	 * @returns {string} result	 */	const pathToArgumentName = input =>		input			.replace(/\./g, "-")			.replace(/\[\]/g, "")			.replace(				/(\p{Uppercase_Letter}+|\p{Lowercase_Letter}|\d)(\p{Uppercase_Letter}+)/gu,				"$1-$2"			)			.replace(/-?[^\p{Uppercase_Letter}\p{Lowercase_Letter}\d]+/gu, "-")			.toLowerCase();	/**	 * @param {string} path path	 * @returns {Schema} schema part	 */	const getSchemaPart = path => {		const newPath = path.split("/");		let schemaPart = schema;		for (let i = 1; i < newPath.length; i++) {			const inner = schemaPart[/** @type {keyof Schema} */ (newPath[i])];			if (!inner) {				break;			}			schemaPart = inner;		}		return schemaPart;	};	/**	 * @param {PathItem[]} path path in the schema	 * @returns {string | undefined} description	 */	const getDescription = path => {		for (const { schema } of path) {			if (schema.cli) {				if (schema.cli.helper) continue;				if (schema.cli.description) return schema.cli.description;			}			if (schema.description) return schema.description;		}	};	/**	 * @param {PathItem[]} path path in the schema	 * @returns {string | undefined} negative description	 */	const getNegatedDescription = path => {		for (const { schema } of path) {			if (schema.cli) {				if (schema.cli.helper) continue;				if (schema.cli.negatedDescription) return schema.cli.negatedDescription;			}		}	};	/**	 * @param {PathItem[]} path path in the schema	 * @returns {string | undefined} reset description	 */	const getResetDescription = path => {		for (const { schema } of path) {			if (schema.cli) {				if (schema.cli.helper) continue;				if (schema.cli.resetDescription) return schema.cli.resetDescription;			}		}	};	/**	 * @param {Schema} schemaPart schema	 * @returns {Pick<ArgumentConfig, "type" | "values"> | undefined} partial argument config	 */	const schemaToArgumentConfig = schemaPart => {		if (schemaPart.enum) {			return {				type: "enum",				values: schemaPart.enum			};		}		switch (schemaPart.type) {			case "number":				return {					type: "number"				};			case "string":				return {					type: schemaPart.absolutePath ? "path" : "string"				};			case "boolean":				return {					type: "boolean"				};		}		if (schemaPart.instanceof === "RegExp") {			return {				type: "RegExp"			};		}		return undefined;	};	/**	 * @param {PathItem[]} path path in the schema	 * @returns {void}	 */	const addResetFlag = path => {		const schemaPath = path[0].path;		const name = pathToArgumentName(`${schemaPath}.reset`);		const description =			getResetDescription(path) ||			`Clear all items provided in '${schemaPath}' configuration. ${getDescription(				path			)}`;		flags[name] = {			configs: [				{					type: "reset",					multiple: false,					description,					path: schemaPath				}			],			description: undefined,			simpleType:				/** @type {SimpleType} */				(/** @type {unknown} */ (undefined)),			multiple: /** @type {boolean} */ (/** @type {unknown} */ (undefined))		};	};	/**	 * @param {PathItem[]} path full path in schema	 * @param {boolean} multiple inside of an array	 * @returns {number} number of arguments added	 */	const addFlag = (path, multiple) => {		const argConfigBase = schemaToArgumentConfig(path[0].schema);		if (!argConfigBase) return 0;		const negatedDescription = getNegatedDescription(path);		const name = pathToArgumentName(path[0].path);		/** @type {ArgumentConfig} */		const argConfig = {			...argConfigBase,			multiple,			description: getDescription(path),			path: path[0].path		};		if (negatedDescription) {			argConfig.negatedDescription = negatedDescription;		}		if (!flags[name]) {			flags[name] = {				configs: [],				description: undefined,				simpleType:					/** @type {SimpleType} */					(/** @type {unknown} */ (undefined)),				multiple: /** @type {boolean} */ (/** @type {unknown} */ (undefined))			};		}		if (			flags[name].configs.some(				item => JSON.stringify(item) === JSON.stringify(argConfig)			)		) {			return 0;		}		if (			flags[name].configs.some(				item => item.type === argConfig.type && item.multiple !== multiple			)		) {			if (multiple) {				throw new Error(					`Conflicting schema for ${path[0].path} with ${argConfig.type} type (array type must be before single item type)`				);			}			return 0;		}		flags[name].configs.push(argConfig);		return 1;	};	// TODO support `not` and `if/then/else`	// TODO support `const`, but we don't use it on our schema	/**	 * @param {Schema} schemaPart the current schema	 * @param {string} schemaPath the current path in the schema	 * @param {PathItem[]} path all previous visited schemaParts	 * @param {string | null} inArray if inside of an array, the path to the array	 * @returns {number} added arguments	 */	const traverse = (schemaPart, schemaPath = "", path = [], inArray = null) => {		while (schemaPart.$ref) {			schemaPart = getSchemaPart(schemaPart.$ref);		}		const repetitions = path.filter(({ schema }) => schema === schemaPart);		if (			repetitions.length >= 2 ||			repetitions.some(({ path }) => path === schemaPath)		) {			return 0;		}		if (schemaPart.cli && schemaPart.cli.exclude) return 0;		/** @type {PathItem[]} */		const fullPath = [{ schema: schemaPart, path: schemaPath }, ...path];		let addedArguments = 0;		addedArguments += addFlag(fullPath, Boolean(inArray));		if (schemaPart.type === "object") {			if (schemaPart.properties) {				for (const property of Object.keys(schemaPart.properties)) {					addedArguments += traverse(						/** @type {Schema} */						(schemaPart.properties[property]),						schemaPath ? `${schemaPath}.${property}` : property,						fullPath,						inArray					);				}			}			return addedArguments;		}		if (schemaPart.type === "array") {			if (inArray) {				return 0;			}			if (Array.isArray(schemaPart.items)) {				const i = 0;				for (const item of schemaPart.items) {					addedArguments += traverse(						/** @type {Schema} */						(item),						`${schemaPath}.${i}`,						fullPath,						schemaPath					);				}				return addedArguments;			}			addedArguments += traverse(				/** @type {Schema} */				(schemaPart.items),				`${schemaPath}[]`,				fullPath,				schemaPath			);			if (addedArguments > 0) {				addResetFlag(fullPath);				addedArguments++;			}			return addedArguments;		}		const maybeOf = schemaPart.oneOf || schemaPart.anyOf || schemaPart.allOf;		if (maybeOf) {			const items = maybeOf;			for (let i = 0; i < items.length; i++) {				addedArguments += traverse(					/** @type {Schema} */					(items[i]),					schemaPath,					fullPath,					inArray				);			}			return addedArguments;		}		return addedArguments;	};	traverse(schema);	// Summarize flags	for (const name of Object.keys(flags)) {		/** @type {Argument} */		const argument = flags[name];		argument.description = argument.configs.reduce((desc, { description }) => {			if (!desc) return description;			if (!description) return desc;			if (desc.includes(description)) return desc;			return `${desc} ${description}`;		}, /** @type {string | undefined} */ (undefined));		argument.simpleType =			/** @type {SimpleType} */			(				argument.configs.reduce((t, argConfig) => {					/** @type {SimpleType} */					let type = "string";					switch (argConfig.type) {						case "number":							type = "number";							break;						case "reset":						case "boolean":							type = "boolean";							break;						case "enum": {							const values =								/** @type {NonNullable<ArgumentConfig["values"]>} */								(argConfig.values);							if (values.every(v => typeof v === "boolean")) type = "boolean";							if (values.every(v => typeof v === "number")) type = "number";							break;						}					}					if (t === undefined) return type;					return t === type ? t : "string";				}, /** @type {SimpleType | undefined} */ (undefined))			);		argument.multiple = argument.configs.some(c => c.multiple);	}	return flags;};const cliAddedItems = new WeakMap();/** @typedef {string | number} Property *//** * @param {ObjectConfiguration} config configuration * @param {string} schemaPath path in the config * @param {number | undefined} index index of value when multiple values are provided, otherwise undefined * @returns {{ problem?: LocalProblem, object?: ObjectConfiguration, property?: Property, value?: EXPECTED_OBJECT | EXPECTED_ANY[] }} problem or object with property and value */const getObjectAndProperty = (config, schemaPath, index = 0) => {	if (!schemaPath) return { value: config };	const parts = schemaPath.split(".");	const property = /** @type {string} */ (parts.pop());	let current = config;	let i = 0;	for (const part of parts) {		const isArray = part.endsWith("[]");		const name = isArray ? part.slice(0, -2) : part;		let value = current[name];		if (isArray) {			if (value === undefined) {				value = {};				current[name] = [...Array.from({ length: index }), value];				cliAddedItems.set(current[name], index + 1);			} else if (!Array.isArray(value)) {				return {					problem: {						type: "unexpected-non-array-in-path",						path: parts.slice(0, i).join(".")					}				};			} else {				let addedItems = cliAddedItems.get(value) || 0;				while (addedItems <= index) {					value.push(undefined);					addedItems++;				}				cliAddedItems.set(value, addedItems);				const x = value.length - addedItems + index;				if (value[x] === undefined) {					value[x] = {};				} else if (value[x] === null || typeof value[x] !== "object") {					return {						problem: {							type: "unexpected-non-object-in-path",							path: parts.slice(0, i).join(".")						}					};				}				value = value[x];			}		} else if (value === undefined) {			value = current[name] = {};		} else if (value === null || typeof value !== "object") {			return {				problem: {					type: "unexpected-non-object-in-path",					path: parts.slice(0, i).join(".")				}			};		}		current = value;		i++;	}	const value = current[property];	if (property.endsWith("[]")) {		const name = property.slice(0, -2);		const value = current[name];		if (value === undefined) {			current[name] = [...Array.from({ length: index }), undefined];			cliAddedItems.set(current[name], index + 1);			return { object: current[name], property: index, value: undefined };		} else if (!Array.isArray(value)) {			current[name] = [value, ...Array.from({ length: index }), undefined];			cliAddedItems.set(current[name], index + 1);			return { object: current[name], property: index + 1, value: undefined };		}		let addedItems = cliAddedItems.get(value) || 0;		while (addedItems <= index) {			value.push(undefined);			addedItems++;		}		cliAddedItems.set(value, addedItems);		const x = value.length - addedItems + index;		if (value[x] === undefined) {			value[x] = {};		} else if (value[x] === null || typeof value[x] !== "object") {			return {				problem: {					type: "unexpected-non-object-in-path",					path: schemaPath				}			};		}		return {			object: value,			property: x,			value: value[x]		};	}	return { object: current, property, value };};/** * @param {ObjectConfiguration} config configuration * @param {string} schemaPath path in the config * @param {ParsedValue} value parsed value * @param {number | undefined} index index of value when multiple values are provided, otherwise undefined * @returns {LocalProblem | null} problem or null for success */const setValue = (config, schemaPath, value, index) => {	const { problem, object, property } = getObjectAndProperty(		config,		schemaPath,		index	);	if (problem) return problem;	/** @type {ObjectConfiguration} */	(object)[/** @type {Property} */ (property)] = value;	return null;};/** * @param {ArgumentConfig} argConfig processing instructions * @param {ObjectConfiguration} config configuration * @param {Value} value the value * @param {number | undefined} index the index if multiple values provided * @returns {LocalProblem | null} a problem if any */const processArgumentConfig = (argConfig, config, value, index) => {	if (index !== undefined && !argConfig.multiple) {		return {			type: "multiple-values-unexpected",			path: argConfig.path		};	}	const parsed = parseValueForArgumentConfig(argConfig, value);	if (parsed === undefined) {		return {			type: "invalid-value",			path: argConfig.path,			expected: getExpectedValue(argConfig)		};	}	const problem = setValue(config, argConfig.path, parsed, index);	if (problem) return problem;	return null;};/** * @param {ArgumentConfig} argConfig processing instructions * @returns {string | undefined} expected message */const getExpectedValue = argConfig => {	switch (argConfig.type) {		case "boolean":			return "true | false";		case "RegExp":			return "regular expression (example: /ab?c*/)";		case "enum":			return /** @type {NonNullable<ArgumentConfig["values"]>} */ (				argConfig.values			)				.map(v => `${v}`)				.join(" | ");		case "reset":			return "true (will reset the previous value to an empty array)";		default:			return argConfig.type;	}};/** @typedef {null | string | number | boolean | RegExp | EnumValue | []} ParsedValue *//** * @param {ArgumentConfig} argConfig processing instructions * @param {Value} value the value * @returns {ParsedValue | undefined} parsed value */const parseValueForArgumentConfig = (argConfig, value) => {	switch (argConfig.type) {		case "string":			if (typeof value === "string") {				return value;			}			break;		case "path":			if (typeof value === "string") {				return path.resolve(value);			}			break;		case "number":			if (typeof value === "number") return value;			if (typeof value === "string" && /^[+-]?\d*(\.\d*)[eE]\d+$/) {				const n = Number(value);				if (!Number.isNaN(n)) return n;			}			break;		case "boolean":			if (typeof value === "boolean") return value;			if (value === "true") return true;			if (value === "false") return false;			break;		case "RegExp":			if (value instanceof RegExp) return value;			if (typeof value === "string") {				// cspell:word yugi				const match = /^\/(.*)\/([yugi]*)$/.exec(value);				if (match && !/[^\\]\//.test(match[1])) {					return new RegExp(match[1], match[2]);				}			}			break;		case "enum": {			const values =				/** @type {EnumValue[]} */				(argConfig.values);			if (values.includes(/** @type {Exclude<Value, RegExp>} */ (value))) {				return value;			}			for (const item of values) {				if (`${item}` === value) return item;			}			break;		}		case "reset":			if (value === true) return [];			break;	}};/** @typedef {Record<string, Value[]>} Values *//** * @param {Flags} args object of arguments * @param {ObjectConfiguration} config configuration * @param {Values} values object with values * @returns {Problem[] | null} problems or null for success */const processArguments = (args, config, values) => {	/** @type {Problem[]} */	const problems = [];	for (const key of Object.keys(values)) {		const arg = args[key];		if (!arg) {			problems.push({				type: "unknown-argument",				path: "",				argument: key			});			continue;		}		/**		 * @param {Value} value value		 * @param {number | undefined} i index		 */		const processValue = (value, i) => {			const currentProblems = [];			for (const argConfig of arg.configs) {				const problem = processArgumentConfig(argConfig, config, value, i);				if (!problem) {					return;				}				currentProblems.push({					...problem,					argument: key,					value,					index: i				});			}			problems.push(...currentProblems);		};		const value = values[key];		if (Array.isArray(value)) {			for (let i = 0; i < value.length; i++) {				processValue(value[i], i);			}		} else {			processValue(value, undefined);		}	}	if (problems.length === 0) return null;	return problems;};module.exports.getArguments = getArguments;module.exports.processArguments = processArguments;
 |