You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

413 lines
10 KiB

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