process-arguments.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. "use strict";
  2. const path = require("path");
  3. // Based on https://github.com/webpack/webpack/blob/master/lib/cli.js
  4. // Please do not modify it
  5. /** @typedef {"unknown-argument" | "unexpected-non-array-in-path" | "unexpected-non-object-in-path" | "multiple-values-unexpected" | "invalid-value"} ProblemType */
  6. /**
  7. * @typedef {Object} Problem
  8. * @property {ProblemType} type
  9. * @property {string} path
  10. * @property {string} argument
  11. * @property {any=} value
  12. * @property {number=} index
  13. * @property {string=} expected
  14. */
  15. /**
  16. * @typedef {Object} LocalProblem
  17. * @property {ProblemType} type
  18. * @property {string} path
  19. * @property {string=} expected
  20. */
  21. /**
  22. * @typedef {Object} ArgumentConfig
  23. * @property {string} description
  24. * @property {string} path
  25. * @property {boolean} multiple
  26. * @property {"enum"|"string"|"path"|"number"|"boolean"|"RegExp"|"reset"} type
  27. * @property {any[]=} values
  28. */
  29. /**
  30. * @typedef {Object} Argument
  31. * @property {string} description
  32. * @property {"string"|"number"|"boolean"} simpleType
  33. * @property {boolean} multiple
  34. * @property {ArgumentConfig[]} configs
  35. */
  36. const cliAddedItems = new WeakMap();
  37. /**
  38. * @param {any} config configuration
  39. * @param {string} schemaPath path in the config
  40. * @param {number | undefined} index index of value when multiple values are provided, otherwise undefined
  41. * @returns {{ problem?: LocalProblem, object?: any, property?: string | number, value?: any }} problem or object with property and value
  42. */
  43. const getObjectAndProperty = (config, schemaPath, index = 0) => {
  44. if (!schemaPath) {
  45. return { value: config };
  46. }
  47. const parts = schemaPath.split(".");
  48. const property = parts.pop();
  49. let current = config;
  50. let i = 0;
  51. for (const part of parts) {
  52. const isArray = part.endsWith("[]");
  53. const name = isArray ? part.slice(0, -2) : part;
  54. let value = current[name];
  55. if (isArray) {
  56. // eslint-disable-next-line no-undefined
  57. if (value === undefined) {
  58. value = {};
  59. current[name] = [...Array.from({ length: index }), value];
  60. cliAddedItems.set(current[name], index + 1);
  61. } else if (!Array.isArray(value)) {
  62. return {
  63. problem: {
  64. type: "unexpected-non-array-in-path",
  65. path: parts.slice(0, i).join("."),
  66. },
  67. };
  68. } else {
  69. let addedItems = cliAddedItems.get(value) || 0;
  70. while (addedItems <= index) {
  71. // eslint-disable-next-line no-undefined
  72. value.push(undefined);
  73. // eslint-disable-next-line no-plusplus
  74. addedItems++;
  75. }
  76. cliAddedItems.set(value, addedItems);
  77. const x = value.length - addedItems + index;
  78. // eslint-disable-next-line no-undefined
  79. if (value[x] === undefined) {
  80. value[x] = {};
  81. } else if (value[x] === null || typeof value[x] !== "object") {
  82. return {
  83. problem: {
  84. type: "unexpected-non-object-in-path",
  85. path: parts.slice(0, i).join("."),
  86. },
  87. };
  88. }
  89. value = value[x];
  90. }
  91. // eslint-disable-next-line no-undefined
  92. } else if (value === undefined) {
  93. // eslint-disable-next-line no-multi-assign
  94. value = current[name] = {};
  95. } else if (value === null || typeof value !== "object") {
  96. return {
  97. problem: {
  98. type: "unexpected-non-object-in-path",
  99. path: parts.slice(0, i).join("."),
  100. },
  101. };
  102. }
  103. current = value;
  104. // eslint-disable-next-line no-plusplus
  105. i++;
  106. }
  107. const value = current[/** @type {string} */ (property)];
  108. if (/** @type {string} */ (property).endsWith("[]")) {
  109. const name = /** @type {string} */ (property).slice(0, -2);
  110. // eslint-disable-next-line no-shadow
  111. const value = current[name];
  112. // eslint-disable-next-line no-undefined
  113. if (value === undefined) {
  114. // eslint-disable-next-line no-undefined
  115. current[name] = [...Array.from({ length: index }), undefined];
  116. cliAddedItems.set(current[name], index + 1);
  117. // eslint-disable-next-line no-undefined
  118. return { object: current[name], property: index, value: undefined };
  119. } else if (!Array.isArray(value)) {
  120. // eslint-disable-next-line no-undefined
  121. current[name] = [value, ...Array.from({ length: index }), undefined];
  122. cliAddedItems.set(current[name], index + 1);
  123. // eslint-disable-next-line no-undefined
  124. return { object: current[name], property: index + 1, value: undefined };
  125. }
  126. let addedItems = cliAddedItems.get(value) || 0;
  127. while (addedItems <= index) {
  128. // eslint-disable-next-line no-undefined
  129. value.push(undefined);
  130. // eslint-disable-next-line no-plusplus
  131. addedItems++;
  132. }
  133. cliAddedItems.set(value, addedItems);
  134. const x = value.length - addedItems + index;
  135. // eslint-disable-next-line no-undefined
  136. if (value[x] === undefined) {
  137. value[x] = {};
  138. } else if (value[x] === null || typeof value[x] !== "object") {
  139. return {
  140. problem: {
  141. type: "unexpected-non-object-in-path",
  142. path: schemaPath,
  143. },
  144. };
  145. }
  146. return {
  147. object: value,
  148. property: x,
  149. value: value[x],
  150. };
  151. }
  152. return { object: current, property, value };
  153. };
  154. /**
  155. * @param {ArgumentConfig} argConfig processing instructions
  156. * @param {any} value the value
  157. * @returns {any | undefined} parsed value
  158. */
  159. const parseValueForArgumentConfig = (argConfig, value) => {
  160. // eslint-disable-next-line default-case
  161. switch (argConfig.type) {
  162. case "string":
  163. if (typeof value === "string") {
  164. return value;
  165. }
  166. break;
  167. case "path":
  168. if (typeof value === "string") {
  169. return path.resolve(value);
  170. }
  171. break;
  172. case "number":
  173. if (typeof value === "number") {
  174. return value;
  175. }
  176. if (typeof value === "string" && /^[+-]?\d*(\.\d*)[eE]\d+$/) {
  177. const n = +value;
  178. if (!isNaN(n)) return n;
  179. }
  180. break;
  181. case "boolean":
  182. if (typeof value === "boolean") {
  183. return value;
  184. }
  185. if (value === "true") {
  186. return true;
  187. }
  188. if (value === "false") {
  189. return false;
  190. }
  191. break;
  192. case "RegExp":
  193. if (value instanceof RegExp) {
  194. return value;
  195. }
  196. if (typeof value === "string") {
  197. // cspell:word yugi
  198. const match = /^\/(.*)\/([yugi]*)$/.exec(value);
  199. if (match && !/[^\\]\//.test(match[1])) {
  200. return new RegExp(match[1], match[2]);
  201. }
  202. }
  203. break;
  204. case "enum":
  205. if (/** @type {any[]} */ (argConfig.values).includes(value)) {
  206. return value;
  207. }
  208. for (const item of /** @type {any[]} */ (argConfig.values)) {
  209. if (`${item}` === value) return item;
  210. }
  211. break;
  212. case "reset":
  213. if (value === true) {
  214. return [];
  215. }
  216. break;
  217. }
  218. };
  219. /**
  220. * @param {ArgumentConfig} argConfig processing instructions
  221. * @returns {string | undefined} expected message
  222. */
  223. const getExpectedValue = (argConfig) => {
  224. switch (argConfig.type) {
  225. default:
  226. return argConfig.type;
  227. case "boolean":
  228. return "true | false";
  229. case "RegExp":
  230. return "regular expression (example: /ab?c*/)";
  231. case "enum":
  232. return /** @type {any[]} */ (argConfig.values)
  233. .map((v) => `${v}`)
  234. .join(" | ");
  235. case "reset":
  236. return "true (will reset the previous value to an empty array)";
  237. }
  238. };
  239. /**
  240. * @param {any} config configuration
  241. * @param {string} schemaPath path in the config
  242. * @param {any} value parsed value
  243. * @param {number | undefined} index index of value when multiple values are provided, otherwise undefined
  244. * @returns {LocalProblem | null} problem or null for success
  245. */
  246. const setValue = (config, schemaPath, value, index) => {
  247. const { problem, object, property } = getObjectAndProperty(
  248. config,
  249. schemaPath,
  250. index
  251. );
  252. if (problem) {
  253. return problem;
  254. }
  255. object[/** @type {string} */ (property)] = value;
  256. return null;
  257. };
  258. /**
  259. * @param {ArgumentConfig} argConfig processing instructions
  260. * @param {any} config configuration
  261. * @param {any} value the value
  262. * @param {number | undefined} index the index if multiple values provided
  263. * @returns {LocalProblem | null} a problem if any
  264. */
  265. const processArgumentConfig = (argConfig, config, value, index) => {
  266. // eslint-disable-next-line no-undefined
  267. if (index !== undefined && !argConfig.multiple) {
  268. return {
  269. type: "multiple-values-unexpected",
  270. path: argConfig.path,
  271. };
  272. }
  273. const parsed = parseValueForArgumentConfig(argConfig, value);
  274. // eslint-disable-next-line no-undefined
  275. if (parsed === undefined) {
  276. return {
  277. type: "invalid-value",
  278. path: argConfig.path,
  279. expected: getExpectedValue(argConfig),
  280. };
  281. }
  282. const problem = setValue(config, argConfig.path, parsed, index);
  283. if (problem) {
  284. return problem;
  285. }
  286. return null;
  287. };
  288. /**
  289. * @param {Record<string, Argument>} args object of arguments
  290. * @param {any} config configuration
  291. * @param {Record<string, string | number | boolean | RegExp | (string | number | boolean | RegExp)[]>} values object with values
  292. * @returns {Problem[] | null} problems or null for success
  293. */
  294. const processArguments = (args, config, values) => {
  295. /**
  296. * @type {Problem[]}
  297. */
  298. const problems = [];
  299. for (const key of Object.keys(values)) {
  300. const arg = args[key];
  301. if (!arg) {
  302. problems.push({
  303. type: "unknown-argument",
  304. path: "",
  305. argument: key,
  306. });
  307. // eslint-disable-next-line no-continue
  308. continue;
  309. }
  310. /**
  311. * @param {any} value
  312. * @param {number | undefined} i
  313. */
  314. const processValue = (value, i) => {
  315. const currentProblems = [];
  316. for (const argConfig of arg.configs) {
  317. const problem = processArgumentConfig(argConfig, config, value, i);
  318. if (!problem) {
  319. return;
  320. }
  321. currentProblems.push({
  322. ...problem,
  323. argument: key,
  324. value,
  325. index: i,
  326. });
  327. }
  328. problems.push(...currentProblems);
  329. };
  330. const value = values[key];
  331. if (Array.isArray(value)) {
  332. for (let i = 0; i < value.length; i++) {
  333. processValue(value[i], i);
  334. }
  335. } else {
  336. // eslint-disable-next-line no-undefined
  337. processValue(value, undefined);
  338. }
  339. }
  340. if (problems.length === 0) {
  341. return null;
  342. }
  343. return problems;
  344. };
  345. module.exports = processArguments;