source: imaps-frontend/node_modules/webpack/lib/cli.js@ 79a0317

main
Last change on this file since 79a0317 was 79a0317, checked in by stefan toskovski <stefantoska84@…>, 4 days ago

F4 Finalna Verzija

  • Property mode set to 100644
File size: 17.4 KB
Line 
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5
6"use strict";
7
8const path = require("path");
9const webpackSchema = require("../schemas/WebpackOptions.json");
10
11/** @typedef {TODO & { absolutePath: boolean, instanceof: string, cli: { helper?: boolean, exclude?: boolean } }} Schema */
12
13// TODO add originPath to PathItem for better errors
14/**
15 * @typedef {object} PathItem
16 * @property {any} schema the part of the schema
17 * @property {string} path the path in the config
18 */
19
20/** @typedef {"unknown-argument" | "unexpected-non-array-in-path" | "unexpected-non-object-in-path" | "multiple-values-unexpected" | "invalid-value"} ProblemType */
21
22/**
23 * @typedef {object} Problem
24 * @property {ProblemType} type
25 * @property {string} path
26 * @property {string} argument
27 * @property {any=} value
28 * @property {number=} index
29 * @property {string=} expected
30 */
31
32/**
33 * @typedef {object} LocalProblem
34 * @property {ProblemType} type
35 * @property {string} path
36 * @property {string=} expected
37 */
38
39/**
40 * @typedef {object} ArgumentConfig
41 * @property {string | undefined} description
42 * @property {string} [negatedDescription]
43 * @property {string} path
44 * @property {boolean} multiple
45 * @property {"enum"|"string"|"path"|"number"|"boolean"|"RegExp"|"reset"} type
46 * @property {any[]=} values
47 */
48
49/** @typedef {"string" | "number" | "boolean"} SimpleType */
50
51/**
52 * @typedef {object} Argument
53 * @property {string | undefined} description
54 * @property {SimpleType} simpleType
55 * @property {boolean} multiple
56 * @property {ArgumentConfig[]} configs
57 */
58
59/** @typedef {string | number | boolean | RegExp | (string | number | boolean | RegExp)} Value */
60
61/** @typedef {Record<string, Argument>} Flags */
62
63/**
64 * @param {Schema=} schema a json schema to create arguments for (by default webpack schema is used)
65 * @returns {Flags} object of arguments
66 */
67const getArguments = (schema = webpackSchema) => {
68 /** @type {Flags} */
69 const flags = {};
70
71 /**
72 * @param {string} input input
73 * @returns {string} result
74 */
75 const pathToArgumentName = input =>
76 input
77 .replace(/\./g, "-")
78 .replace(/\[\]/g, "")
79 .replace(
80 /(\p{Uppercase_Letter}+|\p{Lowercase_Letter}|\d)(\p{Uppercase_Letter}+)/gu,
81 "$1-$2"
82 )
83 .replace(/-?[^\p{Uppercase_Letter}\p{Lowercase_Letter}\d]+/gu, "-")
84 .toLowerCase();
85
86 /**
87 * @param {string} path path
88 * @returns {Schema} schema part
89 */
90 const getSchemaPart = path => {
91 const newPath = path.split("/");
92
93 let schemaPart = schema;
94
95 for (let i = 1; i < newPath.length; i++) {
96 const inner = schemaPart[newPath[i]];
97
98 if (!inner) {
99 break;
100 }
101
102 schemaPart = inner;
103 }
104
105 return schemaPart;
106 };
107
108 /**
109 * @param {PathItem[]} path path in the schema
110 * @returns {string | undefined} description
111 */
112 const getDescription = path => {
113 for (const { schema } of path) {
114 if (schema.cli) {
115 if (schema.cli.helper) continue;
116 if (schema.cli.description) return schema.cli.description;
117 }
118 if (schema.description) return schema.description;
119 }
120 };
121
122 /**
123 * @param {PathItem[]} path path in the schema
124 * @returns {string | undefined} negative description
125 */
126 const getNegatedDescription = path => {
127 for (const { schema } of path) {
128 if (schema.cli) {
129 if (schema.cli.helper) continue;
130 if (schema.cli.negatedDescription) return schema.cli.negatedDescription;
131 }
132 }
133 };
134
135 /**
136 * @param {PathItem[]} path path in the schema
137 * @returns {string | undefined} reset description
138 */
139 const getResetDescription = path => {
140 for (const { schema } of path) {
141 if (schema.cli) {
142 if (schema.cli.helper) continue;
143 if (schema.cli.resetDescription) return schema.cli.resetDescription;
144 }
145 }
146 };
147
148 /**
149 * @param {Schema} schemaPart schema
150 * @returns {Pick<ArgumentConfig, "type"|"values"> | undefined} partial argument config
151 */
152 const schemaToArgumentConfig = schemaPart => {
153 if (schemaPart.enum) {
154 return {
155 type: "enum",
156 values: schemaPart.enum
157 };
158 }
159 switch (schemaPart.type) {
160 case "number":
161 return {
162 type: "number"
163 };
164 case "string":
165 return {
166 type: schemaPart.absolutePath ? "path" : "string"
167 };
168 case "boolean":
169 return {
170 type: "boolean"
171 };
172 }
173 if (schemaPart.instanceof === "RegExp") {
174 return {
175 type: "RegExp"
176 };
177 }
178 return undefined;
179 };
180
181 /**
182 * @param {PathItem[]} path path in the schema
183 * @returns {void}
184 */
185 const addResetFlag = path => {
186 const schemaPath = path[0].path;
187 const name = pathToArgumentName(`${schemaPath}.reset`);
188 const description =
189 getResetDescription(path) ||
190 `Clear all items provided in '${schemaPath}' configuration. ${getDescription(
191 path
192 )}`;
193 flags[name] = {
194 configs: [
195 {
196 type: "reset",
197 multiple: false,
198 description,
199 path: schemaPath
200 }
201 ],
202 description: undefined,
203 simpleType:
204 /** @type {SimpleType} */
205 (/** @type {unknown} */ (undefined)),
206 multiple: /** @type {boolean} */ (/** @type {unknown} */ (undefined))
207 };
208 };
209
210 /**
211 * @param {PathItem[]} path full path in schema
212 * @param {boolean} multiple inside of an array
213 * @returns {number} number of arguments added
214 */
215 const addFlag = (path, multiple) => {
216 const argConfigBase = schemaToArgumentConfig(path[0].schema);
217 if (!argConfigBase) return 0;
218
219 const negatedDescription = getNegatedDescription(path);
220 const name = pathToArgumentName(path[0].path);
221 /** @type {ArgumentConfig} */
222 const argConfig = {
223 ...argConfigBase,
224 multiple,
225 description: getDescription(path),
226 path: path[0].path
227 };
228
229 if (negatedDescription) {
230 argConfig.negatedDescription = negatedDescription;
231 }
232
233 if (!flags[name]) {
234 flags[name] = {
235 configs: [],
236 description: undefined,
237 simpleType:
238 /** @type {SimpleType} */
239 (/** @type {unknown} */ (undefined)),
240 multiple: /** @type {boolean} */ (/** @type {unknown} */ (undefined))
241 };
242 }
243
244 if (
245 flags[name].configs.some(
246 item => JSON.stringify(item) === JSON.stringify(argConfig)
247 )
248 ) {
249 return 0;
250 }
251
252 if (
253 flags[name].configs.some(
254 item => item.type === argConfig.type && item.multiple !== multiple
255 )
256 ) {
257 if (multiple) {
258 throw new Error(
259 `Conflicting schema for ${path[0].path} with ${argConfig.type} type (array type must be before single item type)`
260 );
261 }
262 return 0;
263 }
264
265 flags[name].configs.push(argConfig);
266
267 return 1;
268 };
269
270 // TODO support `not` and `if/then/else`
271 // TODO support `const`, but we don't use it on our schema
272 /**
273 * @param {Schema} schemaPart the current schema
274 * @param {string} schemaPath the current path in the schema
275 * @param {{schema: object, path: string}[]} path all previous visited schemaParts
276 * @param {string | null} inArray if inside of an array, the path to the array
277 * @returns {number} added arguments
278 */
279 const traverse = (schemaPart, schemaPath = "", path = [], inArray = null) => {
280 while (schemaPart.$ref) {
281 schemaPart = getSchemaPart(schemaPart.$ref);
282 }
283
284 const repetitions = path.filter(({ schema }) => schema === schemaPart);
285 if (
286 repetitions.length >= 2 ||
287 repetitions.some(({ path }) => path === schemaPath)
288 ) {
289 return 0;
290 }
291
292 if (schemaPart.cli && schemaPart.cli.exclude) return 0;
293
294 const fullPath = [{ schema: schemaPart, path: schemaPath }, ...path];
295
296 let addedArguments = 0;
297
298 addedArguments += addFlag(fullPath, Boolean(inArray));
299
300 if (schemaPart.type === "object") {
301 if (schemaPart.properties) {
302 for (const property of Object.keys(schemaPart.properties)) {
303 addedArguments += traverse(
304 /** @type {Schema} */
305 (schemaPart.properties[property]),
306 schemaPath ? `${schemaPath}.${property}` : property,
307 fullPath,
308 inArray
309 );
310 }
311 }
312
313 return addedArguments;
314 }
315
316 if (schemaPart.type === "array") {
317 if (inArray) {
318 return 0;
319 }
320 if (Array.isArray(schemaPart.items)) {
321 const i = 0;
322 for (const item of schemaPart.items) {
323 addedArguments += traverse(
324 /** @type {Schema} */
325 (item),
326 `${schemaPath}.${i}`,
327 fullPath,
328 schemaPath
329 );
330 }
331
332 return addedArguments;
333 }
334
335 addedArguments += traverse(
336 /** @type {Schema} */
337 (schemaPart.items),
338 `${schemaPath}[]`,
339 fullPath,
340 schemaPath
341 );
342
343 if (addedArguments > 0) {
344 addResetFlag(fullPath);
345 addedArguments++;
346 }
347
348 return addedArguments;
349 }
350
351 const maybeOf = schemaPart.oneOf || schemaPart.anyOf || schemaPart.allOf;
352
353 if (maybeOf) {
354 const items = maybeOf;
355
356 for (let i = 0; i < items.length; i++) {
357 addedArguments += traverse(
358 /** @type {Schema} */
359 (items[i]),
360 schemaPath,
361 fullPath,
362 inArray
363 );
364 }
365
366 return addedArguments;
367 }
368
369 return addedArguments;
370 };
371
372 traverse(schema);
373
374 // Summarize flags
375 for (const name of Object.keys(flags)) {
376 /** @type {Argument} */
377 const argument = flags[name];
378 argument.description = argument.configs.reduce((desc, { description }) => {
379 if (!desc) return description;
380 if (!description) return desc;
381 if (desc.includes(description)) return desc;
382 return `${desc} ${description}`;
383 }, /** @type {string | undefined} */ (undefined));
384 argument.simpleType =
385 /** @type {SimpleType} */
386 (
387 argument.configs.reduce((t, argConfig) => {
388 /** @type {SimpleType} */
389 let type = "string";
390 switch (argConfig.type) {
391 case "number":
392 type = "number";
393 break;
394 case "reset":
395 case "boolean":
396 type = "boolean";
397 break;
398 case "enum": {
399 const values =
400 /** @type {NonNullable<ArgumentConfig["values"]>} */
401 (argConfig.values);
402
403 if (values.every(v => typeof v === "boolean")) type = "boolean";
404 if (values.every(v => typeof v === "number")) type = "number";
405 break;
406 }
407 }
408 if (t === undefined) return type;
409 return t === type ? t : "string";
410 }, /** @type {SimpleType | undefined} */ (undefined))
411 );
412 argument.multiple = argument.configs.some(c => c.multiple);
413 }
414
415 return flags;
416};
417
418const cliAddedItems = new WeakMap();
419
420/** @typedef {string | number} Property */
421
422/**
423 * @param {Configuration} config configuration
424 * @param {string} schemaPath path in the config
425 * @param {number | undefined} index index of value when multiple values are provided, otherwise undefined
426 * @returns {{ problem?: LocalProblem, object?: any, property?: Property, value?: any }} problem or object with property and value
427 */
428const getObjectAndProperty = (config, schemaPath, index = 0) => {
429 if (!schemaPath) return { value: config };
430 const parts = schemaPath.split(".");
431 const property = /** @type {string} */ (parts.pop());
432 let current = config;
433 let i = 0;
434 for (const part of parts) {
435 const isArray = part.endsWith("[]");
436 const name = isArray ? part.slice(0, -2) : part;
437 let value = current[name];
438 if (isArray) {
439 if (value === undefined) {
440 value = {};
441 current[name] = [...Array.from({ length: index }), value];
442 cliAddedItems.set(current[name], index + 1);
443 } else if (!Array.isArray(value)) {
444 return {
445 problem: {
446 type: "unexpected-non-array-in-path",
447 path: parts.slice(0, i).join(".")
448 }
449 };
450 } else {
451 let addedItems = cliAddedItems.get(value) || 0;
452 while (addedItems <= index) {
453 value.push(undefined);
454 addedItems++;
455 }
456 cliAddedItems.set(value, addedItems);
457 const x = value.length - addedItems + index;
458 if (value[x] === undefined) {
459 value[x] = {};
460 } else if (value[x] === null || typeof value[x] !== "object") {
461 return {
462 problem: {
463 type: "unexpected-non-object-in-path",
464 path: parts.slice(0, i).join(".")
465 }
466 };
467 }
468 value = value[x];
469 }
470 } else if (value === undefined) {
471 value = current[name] = {};
472 } else if (value === null || typeof value !== "object") {
473 return {
474 problem: {
475 type: "unexpected-non-object-in-path",
476 path: parts.slice(0, i).join(".")
477 }
478 };
479 }
480 current = value;
481 i++;
482 }
483 const value = current[property];
484 if (property.endsWith("[]")) {
485 const name = property.slice(0, -2);
486 const value = current[name];
487 if (value === undefined) {
488 current[name] = [...Array.from({ length: index }), undefined];
489 cliAddedItems.set(current[name], index + 1);
490 return { object: current[name], property: index, value: undefined };
491 } else if (!Array.isArray(value)) {
492 current[name] = [value, ...Array.from({ length: index }), undefined];
493 cliAddedItems.set(current[name], index + 1);
494 return { object: current[name], property: index + 1, value: undefined };
495 }
496 let addedItems = cliAddedItems.get(value) || 0;
497 while (addedItems <= index) {
498 value.push(undefined);
499 addedItems++;
500 }
501 cliAddedItems.set(value, addedItems);
502 const x = value.length - addedItems + index;
503 if (value[x] === undefined) {
504 value[x] = {};
505 } else if (value[x] === null || typeof value[x] !== "object") {
506 return {
507 problem: {
508 type: "unexpected-non-object-in-path",
509 path: schemaPath
510 }
511 };
512 }
513 return {
514 object: value,
515 property: x,
516 value: value[x]
517 };
518 }
519 return { object: current, property, value };
520};
521
522/**
523 * @param {Configuration} config configuration
524 * @param {string} schemaPath path in the config
525 * @param {any} value parsed value
526 * @param {number | undefined} index index of value when multiple values are provided, otherwise undefined
527 * @returns {LocalProblem | null} problem or null for success
528 */
529const setValue = (config, schemaPath, value, index) => {
530 const { problem, object, property } = getObjectAndProperty(
531 config,
532 schemaPath,
533 index
534 );
535 if (problem) return problem;
536 object[/** @type {Property} */ (property)] = value;
537 return null;
538};
539
540/**
541 * @param {ArgumentConfig} argConfig processing instructions
542 * @param {Configuration} config configuration
543 * @param {Value} value the value
544 * @param {number | undefined} index the index if multiple values provided
545 * @returns {LocalProblem | null} a problem if any
546 */
547const processArgumentConfig = (argConfig, config, value, index) => {
548 if (index !== undefined && !argConfig.multiple) {
549 return {
550 type: "multiple-values-unexpected",
551 path: argConfig.path
552 };
553 }
554 const parsed = parseValueForArgumentConfig(argConfig, value);
555 if (parsed === undefined) {
556 return {
557 type: "invalid-value",
558 path: argConfig.path,
559 expected: getExpectedValue(argConfig)
560 };
561 }
562 const problem = setValue(config, argConfig.path, parsed, index);
563 if (problem) return problem;
564 return null;
565};
566
567/**
568 * @param {ArgumentConfig} argConfig processing instructions
569 * @returns {string | undefined} expected message
570 */
571const getExpectedValue = argConfig => {
572 switch (argConfig.type) {
573 case "boolean":
574 return "true | false";
575 case "RegExp":
576 return "regular expression (example: /ab?c*/)";
577 case "enum":
578 return /** @type {NonNullable<ArgumentConfig["values"]>} */ (
579 argConfig.values
580 )
581 .map(v => `${v}`)
582 .join(" | ");
583 case "reset":
584 return "true (will reset the previous value to an empty array)";
585 default:
586 return argConfig.type;
587 }
588};
589
590/**
591 * @param {ArgumentConfig} argConfig processing instructions
592 * @param {Value} value the value
593 * @returns {any | undefined} parsed value
594 */
595const parseValueForArgumentConfig = (argConfig, value) => {
596 switch (argConfig.type) {
597 case "string":
598 if (typeof value === "string") {
599 return value;
600 }
601 break;
602 case "path":
603 if (typeof value === "string") {
604 return path.resolve(value);
605 }
606 break;
607 case "number":
608 if (typeof value === "number") return value;
609 if (typeof value === "string" && /^[+-]?\d*(\.\d*)[eE]\d+$/) {
610 const n = Number(value);
611 if (!Number.isNaN(n)) return n;
612 }
613 break;
614 case "boolean":
615 if (typeof value === "boolean") return value;
616 if (value === "true") return true;
617 if (value === "false") return false;
618 break;
619 case "RegExp":
620 if (value instanceof RegExp) return value;
621 if (typeof value === "string") {
622 // cspell:word yugi
623 const match = /^\/(.*)\/([yugi]*)$/.exec(value);
624 if (match && !/[^\\]\//.test(match[1]))
625 return new RegExp(match[1], match[2]);
626 }
627 break;
628 case "enum": {
629 const values =
630 /** @type {NonNullable<ArgumentConfig["values"]>} */
631 (argConfig.values);
632 if (values.includes(value)) return value;
633 for (const item of values) {
634 if (`${item}` === value) return item;
635 }
636 break;
637 }
638 case "reset":
639 if (value === true) return [];
640 break;
641 }
642};
643
644/** @typedef {any} Configuration */
645
646/**
647 * @param {Flags} args object of arguments
648 * @param {Configuration} config configuration
649 * @param {Record<string, Value[]>} values object with values
650 * @returns {Problem[] | null} problems or null for success
651 */
652const processArguments = (args, config, values) => {
653 /** @type {Problem[]} */
654 const problems = [];
655 for (const key of Object.keys(values)) {
656 const arg = args[key];
657 if (!arg) {
658 problems.push({
659 type: "unknown-argument",
660 path: "",
661 argument: key
662 });
663 continue;
664 }
665 /**
666 * @param {Value} value value
667 * @param {number | undefined} i index
668 */
669 const processValue = (value, i) => {
670 const currentProblems = [];
671 for (const argConfig of arg.configs) {
672 const problem = processArgumentConfig(argConfig, config, value, i);
673 if (!problem) {
674 return;
675 }
676 currentProblems.push({
677 ...problem,
678 argument: key,
679 value,
680 index: i
681 });
682 }
683 problems.push(...currentProblems);
684 };
685 const value = values[key];
686 if (Array.isArray(value)) {
687 for (let i = 0; i < value.length; i++) {
688 processValue(value[i], i);
689 }
690 } else {
691 processValue(value, undefined);
692 }
693 }
694 if (problems.length === 0) return null;
695 return problems;
696};
697
698module.exports.getArguments = getArguments;
699module.exports.processArguments = processArguments;
Note: See TracBrowser for help on using the repository browser.