| "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<string, Argument>} args object of arguments |
| * @param {any} config configuration |
| * @param {Record<string, string | number | boolean | RegExp | (string | number | boolean | RegExp)[]>} 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; |