UNPKG

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