"use strict"; const path = require("path"); // Based on https://github.com/webpack/webpack/blob/master/lib/cli.js // Please do not modify it /** @typedef {"unknown-argument" | "unexpected-non-array-in-path" | "unexpected-non-object-in-path" | "multiple-values-unexpected" | "invalid-value"} ProblemType */ /** * @typedef {Object} Problem * @property {ProblemType} type * @property {string} path * @property {string} argument * @property {any=} value * @property {number=} index * @property {string=} expected */ /** * @typedef {Object} LocalProblem * @property {ProblemType} type * @property {string} path * @property {string=} expected */ /** * @typedef {Object} ArgumentConfig * @property {string} description * @property {string} path * @property {boolean} multiple * @property {"enum"|"string"|"path"|"number"|"boolean"|"RegExp"|"reset"} type * @property {any[]=} values */ /** * @typedef {Object} Argument * @property {string} description * @property {"string"|"number"|"boolean"} simpleType * @property {boolean} multiple * @property {ArgumentConfig[]} configs */ const cliAddedItems = new WeakMap(); /** * @param {any} 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?: any, property?: string | number, value?: 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 = 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) { // eslint-disable-next-line no-undefined 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) { // eslint-disable-next-line no-undefined value.push(undefined); // eslint-disable-next-line no-plusplus addedItems++; } cliAddedItems.set(value, addedItems); const x = value.length - addedItems + index; // eslint-disable-next-line no-undefined 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]; } // eslint-disable-next-line no-undefined } else if (value === undefined) { // eslint-disable-next-line no-multi-assign 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; // eslint-disable-next-line no-plusplus i++; } const value = current[/** @type {string} */ (property)]; if (/** @type {string} */ (property).endsWith("[]")) { const name = /** @type {string} */ (property).slice(0, -2); // eslint-disable-next-line no-shadow const value = current[name]; // eslint-disable-next-line no-undefined if (value === undefined) { // eslint-disable-next-line no-undefined current[name] = [...Array.from({ length: index }), undefined]; cliAddedItems.set(current[name], index + 1); // eslint-disable-next-line no-undefined return { object: current[name], property: index, value: undefined }; } else if (!Array.isArray(value)) { // eslint-disable-next-line no-undefined current[name] = [value, ...Array.from({ length: index }), undefined]; cliAddedItems.set(current[name], index + 1); // eslint-disable-next-line no-undefined return { object: current[name], property: index + 1, value: undefined }; } let addedItems = cliAddedItems.get(value) || 0; while (addedItems <= index) { // eslint-disable-next-line no-undefined value.push(undefined); // eslint-disable-next-line no-plusplus addedItems++; } cliAddedItems.set(value, addedItems); const x = value.length - addedItems + index; // eslint-disable-next-line no-undefined 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 {ArgumentConfig} argConfig processing instructions * @param {any} value the value * @returns {any | undefined} parsed value */ const parseValueForArgumentConfig = (argConfig, value) => { // eslint-disable-next-line default-case 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 = +value; if (!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": if (/** @type {any[]} */ (argConfig.values).includes(value)) { return value; } for (const item of /** @type {any[]} */ (argConfig.values)) { if (`${item}` === value) return item; } break; case "reset": if (value === true) { return []; } break; } }; /** * @param {ArgumentConfig} argConfig processing instructions * @returns {string | undefined} expected message */ const getExpectedValue = (argConfig) => { switch (argConfig.type) { default: return argConfig.type; case "boolean": return "true | false"; case "RegExp": return "regular expression (example: /ab?c*/)"; case "enum": return /** @type {any[]} */ (argConfig.values) .map((v) => `${v}`) .join(" | "); case "reset": return "true (will reset the previous value to an empty array)"; } }; /** * @param {any} config configuration * @param {string} schemaPath path in the config * @param {any} 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; } object[/** @type {string} */ (property)] = value; return null; }; /** * @param {ArgumentConfig} argConfig processing instructions * @param {any} config configuration * @param {any} 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) => { // eslint-disable-next-line no-undefined if (index !== undefined && !argConfig.multiple) { return { type: "multiple-values-unexpected", path: argConfig.path, }; } const parsed = parseValueForArgumentConfig(argConfig, value); // eslint-disable-next-line no-undefined 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 {Record} args object of arguments * @param {any} config configuration * @param {Record} 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, }); // eslint-disable-next-line no-continue continue; } /** * @param {any} value * @param {number | undefined} i */ 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 { // eslint-disable-next-line no-undefined processValue(value, undefined); } } if (problems.length === 0) { return null; } return problems; }; module.exports = processArguments;