413 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			413 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| "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;
 |