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
413 lines
10 KiB
3 years ago
|
"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;
|