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;
 |