1"use strict"; 2var __importDefault = (this && this.__importDefault) || function (mod) { 3 return (mod && mod.__esModule) ? mod : { "default": mod }; 4}; 5Object.defineProperty(exports, "__esModule", { value: true }); 6exports.jack = exports.Jack = exports.isConfigOption = exports.isConfigType = void 0; 7const node_util_1 = require("node:util"); 8const parse_args_js_1 = require("./parse-args.js"); 9// it's a tiny API, just cast it inline, it's fine 10//@ts-ignore 11const cliui_1 = __importDefault(require("@isaacs/cliui")); 12const node_path_1 = require("node:path"); 13const width = Math.min((process && process.stdout && process.stdout.columns) || 80, 80); 14// indentation spaces from heading level 15const indent = (n) => (n - 1) * 2; 16const toEnvKey = (pref, key) => { 17 return [pref, key.replace(/[^a-zA-Z0-9]+/g, ' ')] 18 .join(' ') 19 .trim() 20 .toUpperCase() 21 .replace(/ /g, '_'); 22}; 23const toEnvVal = (value, delim = '\n') => { 24 const str = typeof value === 'string' 25 ? value 26 : typeof value === 'boolean' 27 ? value 28 ? '1' 29 : '0' 30 : typeof value === 'number' 31 ? String(value) 32 : Array.isArray(value) 33 ? value 34 .map((v) => toEnvVal(v)) 35 .join(delim) 36 : /* c8 ignore start */ 37 undefined; 38 if (typeof str !== 'string') { 39 throw new Error(`could not serialize value to environment: ${JSON.stringify(value)}`); 40 } 41 /* c8 ignore stop */ 42 return str; 43}; 44const fromEnvVal = (env, type, multiple, delim = '\n') => (multiple 45 ? env 46 ? env.split(delim).map(v => fromEnvVal(v, type, false)) 47 : [] 48 : type === 'string' 49 ? env 50 : type === 'boolean' 51 ? env === '1' 52 : +env.trim()); 53const isConfigType = (t) => typeof t === 'string' && 54 (t === 'string' || t === 'number' || t === 'boolean'); 55exports.isConfigType = isConfigType; 56const undefOrType = (v, t) => v === undefined || typeof v === t; 57// print the value type, for error message reporting 58const valueType = (v) => typeof v === 'string' 59 ? 'string' 60 : typeof v === 'boolean' 61 ? 'boolean' 62 : typeof v === 'number' 63 ? 'number' 64 : Array.isArray(v) 65 ? joinTypes([...new Set(v.map(v => valueType(v)))]) + '[]' 66 : `${v.type}${v.multiple ? '[]' : ''}`; 67const joinTypes = (types) => types.length === 1 && typeof types[0] === 'string' 68 ? types[0] 69 : `(${types.join('|')})`; 70const isValidValue = (v, type, multi) => { 71 if (multi) { 72 if (!Array.isArray(v)) 73 return false; 74 return !v.some((v) => !isValidValue(v, type, false)); 75 } 76 if (Array.isArray(v)) 77 return false; 78 return typeof v === type; 79}; 80const isConfigOption = (o, type, multi) => !!o && 81 typeof o === 'object' && 82 (0, exports.isConfigType)(o.type) && 83 o.type === type && 84 undefOrType(o.short, 'string') && 85 undefOrType(o.description, 'string') && 86 undefOrType(o.hint, 'string') && 87 undefOrType(o.validate, 'function') && 88 (o.default === undefined || isValidValue(o.default, type, multi)) && 89 !!o.multiple === multi; 90exports.isConfigOption = isConfigOption; 91function num(o = {}) { 92 const { default: def, validate: val, ...rest } = o; 93 if (def !== undefined && !isValidValue(def, 'number', false)) { 94 throw new TypeError('invalid default value'); 95 } 96 const validate = val 97 ? val 98 : undefined; 99 return { 100 ...rest, 101 default: def, 102 validate, 103 type: 'number', 104 multiple: false, 105 }; 106} 107function numList(o = {}) { 108 const { default: def, validate: val, ...rest } = o; 109 if (def !== undefined && !isValidValue(def, 'number', true)) { 110 throw new TypeError('invalid default value'); 111 } 112 const validate = val 113 ? val 114 : undefined; 115 return { 116 ...rest, 117 default: def, 118 validate, 119 type: 'number', 120 multiple: true, 121 }; 122} 123function opt(o = {}) { 124 const { default: def, validate: val, ...rest } = o; 125 if (def !== undefined && !isValidValue(def, 'string', false)) { 126 throw new TypeError('invalid default value'); 127 } 128 const validate = val 129 ? val 130 : undefined; 131 return { 132 ...rest, 133 default: def, 134 validate, 135 type: 'string', 136 multiple: false, 137 }; 138} 139function optList(o = {}) { 140 const { default: def, validate: val, ...rest } = o; 141 if (def !== undefined && !isValidValue(def, 'string', true)) { 142 throw new TypeError('invalid default value'); 143 } 144 const validate = val 145 ? val 146 : undefined; 147 return { 148 ...rest, 149 default: def, 150 validate, 151 type: 'string', 152 multiple: true, 153 }; 154} 155function flag(o = {}) { 156 const { hint, default: def, validate: val, ...rest } = o; 157 if (def !== undefined && !isValidValue(def, 'boolean', false)) { 158 throw new TypeError('invalid default value'); 159 } 160 const validate = val 161 ? val 162 : undefined; 163 if (hint !== undefined) { 164 throw new TypeError('cannot provide hint for flag'); 165 } 166 return { 167 ...rest, 168 default: def, 169 validate, 170 type: 'boolean', 171 multiple: false, 172 }; 173} 174function flagList(o = {}) { 175 const { hint, default: def, validate: val, ...rest } = o; 176 if (def !== undefined && !isValidValue(def, 'boolean', true)) { 177 throw new TypeError('invalid default value'); 178 } 179 const validate = val 180 ? val 181 : undefined; 182 if (hint !== undefined) { 183 throw new TypeError('cannot provide hint for flag list'); 184 } 185 return { 186 ...rest, 187 default: def, 188 validate, 189 type: 'boolean', 190 multiple: true, 191 }; 192} 193const toParseArgsOptionsConfig = (options) => { 194 const c = {}; 195 for (const longOption in options) { 196 const config = options[longOption]; 197 /* c8 ignore start */ 198 if (!config) { 199 throw new Error('config must be an object: ' + longOption); 200 } 201 /* c8 ignore start */ 202 if ((0, exports.isConfigOption)(config, 'number', true)) { 203 c[longOption] = { 204 type: 'string', 205 multiple: true, 206 default: config.default?.map(c => String(c)), 207 }; 208 } 209 else if ((0, exports.isConfigOption)(config, 'number', false)) { 210 c[longOption] = { 211 type: 'string', 212 multiple: false, 213 default: config.default === undefined 214 ? undefined 215 : String(config.default), 216 }; 217 } 218 else { 219 const conf = config; 220 c[longOption] = { 221 type: conf.type, 222 multiple: conf.multiple, 223 default: conf.default, 224 }; 225 } 226 const clo = c[longOption]; 227 if (typeof config.short === 'string') { 228 clo.short = config.short; 229 } 230 if (config.type === 'boolean' && 231 !longOption.startsWith('no-') && 232 !options[`no-${longOption}`]) { 233 c[`no-${longOption}`] = { 234 type: 'boolean', 235 multiple: config.multiple, 236 }; 237 } 238 } 239 return c; 240}; 241const isHeading = (r) => r.type === 'heading'; 242const isDescription = (r) => r.type === 'description'; 243/** 244 * Class returned by the {@link jack} function and all configuration 245 * definition methods. This is what gets chained together. 246 */ 247class Jack { 248 #configSet; 249 #shorts; 250 #options; 251 #fields = []; 252 #env; 253 #envPrefix; 254 #allowPositionals; 255 #usage; 256 #usageMarkdown; 257 constructor(options = {}) { 258 this.#options = options; 259 this.#allowPositionals = options.allowPositionals !== false; 260 this.#env = 261 this.#options.env === undefined ? process.env : this.#options.env; 262 this.#envPrefix = options.envPrefix; 263 // We need to fib a little, because it's always the same object, but it 264 // starts out as having an empty config set. Then each method that adds 265 // fields returns `this as Jack<C & { ...newConfigs }>` 266 this.#configSet = Object.create(null); 267 this.#shorts = Object.create(null); 268 } 269 /** 270 * Set the default value (which will still be overridden by env or cli) 271 * as if from a parsed config file. The optional `source` param, if 272 * provided, will be included in error messages if a value is invalid or 273 * unknown. 274 */ 275 setConfigValues(values, source = '') { 276 try { 277 this.validate(values); 278 } 279 catch (er) { 280 throw Object.assign(er, source ? { source } : {}); 281 } 282 for (const [field, value] of Object.entries(values)) { 283 const my = this.#configSet[field]; 284 // already validated, just for TS's benefit 285 /* c8 ignore start */ 286 if (!my) { 287 throw new Error('unexpected field in config set: ' + field); 288 } 289 /* c8 ignore stop */ 290 my.default = value; 291 } 292 return this; 293 } 294 /** 295 * Parse a string of arguments, and return the resulting 296 * `{ values, positionals }` object. 297 * 298 * If an {@link JackOptions#envPrefix} is set, then it will read default 299 * values from the environment, and write the resulting values back 300 * to the environment as well. 301 * 302 * Environment values always take precedence over any other value, except 303 * an explicit CLI setting. 304 */ 305 parse(args = process.argv) { 306 if (args === process.argv) { 307 args = args.slice(process._eval !== undefined ? 1 : 2); 308 } 309 if (this.#envPrefix) { 310 for (const [field, my] of Object.entries(this.#configSet)) { 311 const ek = toEnvKey(this.#envPrefix, field); 312 const env = this.#env[ek]; 313 if (env !== undefined) { 314 my.default = fromEnvVal(env, my.type, !!my.multiple, my.delim); 315 } 316 } 317 } 318 const options = toParseArgsOptionsConfig(this.#configSet); 319 const result = (0, parse_args_js_1.parseArgs)({ 320 args, 321 options, 322 // always strict, but using our own logic 323 strict: false, 324 allowPositionals: this.#allowPositionals, 325 tokens: true, 326 }); 327 const p = { 328 values: {}, 329 positionals: [], 330 }; 331 for (const token of result.tokens) { 332 if (token.kind === 'positional') { 333 p.positionals.push(token.value); 334 if (this.#options.stopAtPositional) { 335 p.positionals.push(...args.slice(token.index + 1)); 336 return p; 337 } 338 } 339 else if (token.kind === 'option') { 340 let value = undefined; 341 if (token.name.startsWith('no-')) { 342 const my = this.#configSet[token.name]; 343 const pname = token.name.substring('no-'.length); 344 const pos = this.#configSet[pname]; 345 if (pos && 346 pos.type === 'boolean' && 347 (!my || 348 (my.type === 'boolean' && !!my.multiple === !!pos.multiple))) { 349 value = false; 350 token.name = pname; 351 } 352 } 353 const my = this.#configSet[token.name]; 354 if (!my) { 355 throw new Error(`Unknown option '${token.rawName}'. ` + 356 `To specify a positional argument starting with a '-', ` + 357 `place it at the end of the command after '--', as in ` + 358 `'-- ${token.rawName}'`); 359 } 360 if (value === undefined) { 361 if (token.value === undefined) { 362 if (my.type !== 'boolean') { 363 throw new Error(`No value provided for ${token.rawName}, expected ${my.type}`); 364 } 365 value = true; 366 } 367 else { 368 if (my.type === 'boolean') { 369 throw new Error(`Flag ${token.rawName} does not take a value, received '${token.value}'`); 370 } 371 if (my.type === 'string') { 372 value = token.value; 373 } 374 else { 375 value = +token.value; 376 if (value !== value) { 377 throw new Error(`Invalid value '${token.value}' provided for ` + 378 `'${token.rawName}' option, expected number`); 379 } 380 } 381 } 382 } 383 if (my.multiple) { 384 const pv = p.values; 385 const tn = pv[token.name] ?? []; 386 pv[token.name] = tn; 387 tn.push(value); 388 } 389 else { 390 const pv = p.values; 391 pv[token.name] = value; 392 } 393 } 394 } 395 for (const [field, c] of Object.entries(this.#configSet)) { 396 if (c.default !== undefined && !(field in p.values)) { 397 //@ts-ignore 398 p.values[field] = c.default; 399 } 400 } 401 for (const [field, value] of Object.entries(p.values)) { 402 const valid = this.#configSet[field]?.validate; 403 if (valid && !valid(value)) { 404 throw new Error(`Invalid value provided for --${field}: ${JSON.stringify(value)}`); 405 } 406 } 407 this.#writeEnv(p); 408 return p; 409 } 410 /** 411 * do not set fields as 'no-foo' if 'foo' exists and both are bools 412 * just set foo. 413 */ 414 #noNoFields(f, val, s = f) { 415 if (!f.startsWith('no-') || typeof val !== 'boolean') 416 return; 417 const yes = f.substring('no-'.length); 418 // recurse so we get the core config key we care about. 419 this.#noNoFields(yes, val, s); 420 if (this.#configSet[yes]?.type === 'boolean') { 421 throw new Error(`do not set '${s}', instead set '${yes}' as desired.`); 422 } 423 } 424 /** 425 * Validate that any arbitrary object is a valid configuration `values` 426 * object. Useful when loading config files or other sources. 427 */ 428 validate(o) { 429 if (!o || typeof o !== 'object') { 430 throw new Error('Invalid config: not an object'); 431 } 432 for (const field in o) { 433 this.#noNoFields(field, o[field]); 434 const config = this.#configSet[field]; 435 if (!config) { 436 throw new Error(`Unknown config option: ${field}`); 437 } 438 if (!isValidValue(o[field], config.type, !!config.multiple)) { 439 throw Object.assign(new Error(`Invalid value ${valueType(o[field])} for ${field}, expected ${valueType(config)}`), { 440 field, 441 value: o[field], 442 }); 443 } 444 if (config.validate && !config.validate(o[field])) { 445 throw new Error(`Invalid config value for ${field}: ${o[field]}`); 446 } 447 } 448 } 449 #writeEnv(p) { 450 if (!this.#env || !this.#envPrefix) 451 return; 452 for (const [field, value] of Object.entries(p.values)) { 453 const my = this.#configSet[field]; 454 this.#env[toEnvKey(this.#envPrefix, field)] = toEnvVal(value, my?.delim); 455 } 456 } 457 /** 458 * Add a heading to the usage output banner 459 */ 460 heading(text, level, { pre = false } = {}) { 461 if (level === undefined) { 462 level = this.#fields.some(r => isHeading(r)) ? 2 : 1; 463 } 464 this.#fields.push({ type: 'heading', text, level, pre }); 465 return this; 466 } 467 /** 468 * Add a long-form description to the usage output at this position. 469 */ 470 description(text, { pre } = {}) { 471 this.#fields.push({ type: 'description', text, pre }); 472 return this; 473 } 474 /** 475 * Add one or more number fields. 476 */ 477 num(fields) { 478 return this.#addFields(fields, num); 479 } 480 /** 481 * Add one or more multiple number fields. 482 */ 483 numList(fields) { 484 return this.#addFields(fields, numList); 485 } 486 /** 487 * Add one or more string option fields. 488 */ 489 opt(fields) { 490 return this.#addFields(fields, opt); 491 } 492 /** 493 * Add one or more multiple string option fields. 494 */ 495 optList(fields) { 496 return this.#addFields(fields, optList); 497 } 498 /** 499 * Add one or more flag fields. 500 */ 501 flag(fields) { 502 return this.#addFields(fields, flag); 503 } 504 /** 505 * Add one or more multiple flag fields. 506 */ 507 flagList(fields) { 508 return this.#addFields(fields, flagList); 509 } 510 /** 511 * Generic field definition method. Similar to flag/flagList/number/etc, 512 * but you must specify the `type` (and optionally `multiple` and `delim`) 513 * fields on each one, or Jack won't know how to define them. 514 */ 515 addFields(fields) { 516 const next = this; 517 for (const [name, field] of Object.entries(fields)) { 518 this.#validateName(name, field); 519 next.#fields.push({ 520 type: 'config', 521 name, 522 value: field, 523 }); 524 } 525 Object.assign(next.#configSet, fields); 526 return next; 527 } 528 #addFields(fields, fn) { 529 const next = this; 530 Object.assign(next.#configSet, Object.fromEntries(Object.entries(fields).map(([name, field]) => { 531 this.#validateName(name, field); 532 const option = fn(field); 533 next.#fields.push({ 534 type: 'config', 535 name, 536 value: option, 537 }); 538 return [name, option]; 539 }))); 540 return next; 541 } 542 #validateName(name, field) { 543 if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(name)) { 544 throw new TypeError(`Invalid option name: ${name}, ` + 545 `must be '-' delimited ASCII alphanumeric`); 546 } 547 if (this.#configSet[name]) { 548 throw new TypeError(`Cannot redefine option ${field}`); 549 } 550 if (this.#shorts[name]) { 551 throw new TypeError(`Cannot redefine option ${name}, already ` + 552 `in use for ${this.#shorts[name]}`); 553 } 554 if (field.short) { 555 if (!/^[a-zA-Z0-9]$/.test(field.short)) { 556 throw new TypeError(`Invalid ${name} short option: ${field.short}, ` + 557 'must be 1 ASCII alphanumeric character'); 558 } 559 if (this.#shorts[field.short]) { 560 throw new TypeError(`Invalid ${name} short option: ${field.short}, ` + 561 `already in use for ${this.#shorts[field.short]}`); 562 } 563 this.#shorts[field.short] = name; 564 this.#shorts[name] = name; 565 } 566 } 567 /** 568 * Return the usage banner for the given configuration 569 */ 570 usage() { 571 if (this.#usage) 572 return this.#usage; 573 let headingLevel = 1; 574 const ui = (0, cliui_1.default)({ width }); 575 const first = this.#fields[0]; 576 let start = first?.type === 'heading' ? 1 : 0; 577 if (first?.type === 'heading') { 578 ui.div({ 579 padding: [0, 0, 0, 0], 580 text: normalize(first.text), 581 }); 582 } 583 ui.div({ padding: [0, 0, 0, 0], text: 'Usage:' }); 584 if (this.#options.usage) { 585 ui.div({ 586 text: this.#options.usage, 587 padding: [0, 0, 0, 2], 588 }); 589 } 590 else { 591 const cmd = (0, node_path_1.basename)(String(process.argv[1])); 592 const shortFlags = []; 593 const shorts = []; 594 const flags = []; 595 const opts = []; 596 for (const [field, config] of Object.entries(this.#configSet)) { 597 if (config.short) { 598 if (config.type === 'boolean') 599 shortFlags.push(config.short); 600 else 601 shorts.push([config.short, config.hint || field]); 602 } 603 else { 604 if (config.type === 'boolean') 605 flags.push(field); 606 else 607 opts.push([field, config.hint || field]); 608 } 609 } 610 const sf = shortFlags.length ? ' -' + shortFlags.join('') : ''; 611 const so = shorts.map(([k, v]) => ` --${k}=<${v}>`).join(''); 612 const lf = flags.map(k => ` --${k}`).join(''); 613 const lo = opts.map(([k, v]) => ` --${k}=<${v}>`).join(''); 614 const usage = `${cmd}${sf}${so}${lf}${lo}`.trim(); 615 ui.div({ 616 text: usage, 617 padding: [0, 0, 0, 2], 618 }); 619 } 620 ui.div({ padding: [0, 0, 0, 0], text: '' }); 621 const maybeDesc = this.#fields[start]; 622 if (maybeDesc && isDescription(maybeDesc)) { 623 const print = normalize(maybeDesc.text, maybeDesc.pre); 624 start++; 625 ui.div({ padding: [0, 0, 0, 0], text: print }); 626 ui.div({ padding: [0, 0, 0, 0], text: '' }); 627 } 628 const { rows, maxWidth } = this.#usageRows(start); 629 // every heading/description after the first gets indented by 2 630 // extra spaces. 631 for (const row of rows) { 632 if (row.left) { 633 // If the row is too long, don't wrap it 634 // Bump the right-hand side down a line to make room 635 const configIndent = indent(Math.max(headingLevel, 2)); 636 if (row.left.length > maxWidth - 3) { 637 ui.div({ text: row.left, padding: [0, 0, 0, configIndent] }); 638 ui.div({ text: row.text, padding: [0, 0, 0, maxWidth] }); 639 } 640 else { 641 ui.div({ 642 text: row.left, 643 padding: [0, 1, 0, configIndent], 644 width: maxWidth, 645 }, { padding: [0, 0, 0, 0], text: row.text }); 646 } 647 if (row.skipLine) { 648 ui.div({ padding: [0, 0, 0, 0], text: '' }); 649 } 650 } 651 else { 652 if (isHeading(row)) { 653 const { level } = row; 654 headingLevel = level; 655 // only h1 and h2 have bottom padding 656 // h3-h6 do not 657 const b = level <= 2 ? 1 : 0; 658 ui.div({ ...row, padding: [0, 0, b, indent(level)] }); 659 } 660 else { 661 ui.div({ ...row, padding: [0, 0, 1, indent(headingLevel + 1)] }); 662 } 663 } 664 } 665 return (this.#usage = ui.toString()); 666 } 667 /** 668 * Return the usage banner markdown for the given configuration 669 */ 670 usageMarkdown() { 671 if (this.#usageMarkdown) 672 return this.#usageMarkdown; 673 const out = []; 674 let headingLevel = 1; 675 const first = this.#fields[0]; 676 let start = first?.type === 'heading' ? 1 : 0; 677 if (first?.type === 'heading') { 678 out.push(`# ${normalizeOneLine(first.text)}`); 679 } 680 out.push('Usage:'); 681 if (this.#options.usage) { 682 out.push(normalizeMarkdown(this.#options.usage, true)); 683 } 684 else { 685 const cmd = (0, node_path_1.basename)(String(process.argv[1])); 686 const shortFlags = []; 687 const shorts = []; 688 const flags = []; 689 const opts = []; 690 for (const [field, config] of Object.entries(this.#configSet)) { 691 if (config.short) { 692 if (config.type === 'boolean') 693 shortFlags.push(config.short); 694 else 695 shorts.push([config.short, config.hint || field]); 696 } 697 else { 698 if (config.type === 'boolean') 699 flags.push(field); 700 else 701 opts.push([field, config.hint || field]); 702 } 703 } 704 const sf = shortFlags.length ? ' -' + shortFlags.join('') : ''; 705 const so = shorts.map(([k, v]) => ` --${k}=<${v}>`).join(''); 706 const lf = flags.map(k => ` --${k}`).join(''); 707 const lo = opts.map(([k, v]) => ` --${k}=<${v}>`).join(''); 708 const usage = `${cmd}${sf}${so}${lf}${lo}`.trim(); 709 out.push(normalizeMarkdown(usage, true)); 710 } 711 const maybeDesc = this.#fields[start]; 712 if (maybeDesc && isDescription(maybeDesc)) { 713 out.push(normalizeMarkdown(maybeDesc.text, maybeDesc.pre)); 714 start++; 715 } 716 const { rows } = this.#usageRows(start); 717 // heading level in markdown is number of # ahead of text 718 for (const row of rows) { 719 if (row.left) { 720 out.push('#'.repeat(headingLevel + 1) + 721 ' ' + 722 normalizeOneLine(row.left, true)); 723 if (row.text) 724 out.push(normalizeMarkdown(row.text)); 725 } 726 else if (isHeading(row)) { 727 const { level } = row; 728 headingLevel = level; 729 out.push(`${'#'.repeat(headingLevel)} ${normalizeOneLine(row.text, row.pre)}`); 730 } 731 else { 732 out.push(normalizeMarkdown(row.text, !!row.pre)); 733 } 734 } 735 return (this.#usageMarkdown = out.join('\n\n') + '\n'); 736 } 737 #usageRows(start) { 738 // turn each config type into a row, and figure out the width of the 739 // left hand indentation for the option descriptions. 740 let maxMax = Math.max(12, Math.min(26, Math.floor(width / 3))); 741 let maxWidth = 8; 742 let prev = undefined; 743 const rows = []; 744 for (const field of this.#fields.slice(start)) { 745 if (field.type !== 'config') { 746 if (prev?.type === 'config') 747 prev.skipLine = true; 748 prev = undefined; 749 field.text = normalize(field.text, !!field.pre); 750 rows.push(field); 751 continue; 752 } 753 const { value } = field; 754 const desc = value.description || ''; 755 const mult = value.multiple ? 'Can be set multiple times' : ''; 756 const dmDelim = mult && (desc.includes('\n') ? '\n\n' : '\n'); 757 const text = normalize(desc + dmDelim + mult); 758 const hint = value.hint || 759 (value.type === 'number' 760 ? 'n' 761 : value.type === 'string' 762 ? field.name 763 : undefined); 764 const short = !value.short 765 ? '' 766 : value.type === 'boolean' 767 ? `-${value.short} ` 768 : `-${value.short}<${hint}> `; 769 const left = value.type === 'boolean' 770 ? `${short}--${field.name}` 771 : `${short}--${field.name}=<${hint}>`; 772 const row = { text, left, type: 'config' }; 773 if (text.length > width - maxMax) { 774 row.skipLine = true; 775 } 776 if (prev && left.length > maxMax) 777 prev.skipLine = true; 778 prev = row; 779 const len = left.length + 4; 780 if (len > maxWidth && len < maxMax) { 781 maxWidth = len; 782 } 783 rows.push(row); 784 } 785 return { rows, maxWidth }; 786 } 787 /** 788 * Return the configuration options as a plain object 789 */ 790 toJSON() { 791 return Object.fromEntries(Object.entries(this.#configSet).map(([field, def]) => [ 792 field, 793 { 794 type: def.type, 795 ...(def.multiple ? { multiple: true } : {}), 796 ...(def.delim ? { delim: def.delim } : {}), 797 ...(def.short ? { short: def.short } : {}), 798 ...(def.description 799 ? { description: normalize(def.description) } 800 : {}), 801 ...(def.validate ? { validate: def.validate } : {}), 802 ...(def.default !== undefined ? { default: def.default } : {}), 803 }, 804 ])); 805 } 806 /** 807 * Custom printer for `util.inspect` 808 */ 809 [node_util_1.inspect.custom](_, options) { 810 return `Jack ${(0, node_util_1.inspect)(this.toJSON(), options)}`; 811 } 812} 813exports.Jack = Jack; 814// Unwrap and un-indent, so we can wrap description 815// strings however makes them look nice in the code. 816const normalize = (s, pre = false) => pre 817 ? // prepend a ZWSP to each line so cliui doesn't strip it. 818 s 819 .split('\n') 820 .map(l => `\u200b${l}`) 821 .join('\n') 822 : s 823 // remove single line breaks, except for lists 824 .replace(/([^\n])\n[ \t]*([^\n])/g, (_, $1, $2) => !/^[-*]/.test($2) ? `${$1} ${$2}` : `${$1}\n${$2}`) 825 // normalize mid-line whitespace 826 .replace(/([^\n])[ \t]+([^\n])/g, '$1 $2') 827 // two line breaks are enough 828 .replace(/\n{3,}/g, '\n\n') 829 // remove any spaces at the start of a line 830 .replace(/\n[ \t]+/g, '\n') 831 .trim(); 832// normalize for markdown printing, remove leading spaces on lines 833const normalizeMarkdown = (s, pre = false) => { 834 const n = normalize(s, pre).replace(/\\/g, '\\\\'); 835 return pre 836 ? `\`\`\`\n${n.replace(/\u200b/g, '')}\n\`\`\`` 837 : n.replace(/\n +/g, '\n').trim(); 838}; 839const normalizeOneLine = (s, pre = false) => { 840 const n = normalize(s, pre) 841 .replace(/[\s\u200b]+/g, ' ') 842 .trim(); 843 return pre ? `\`${n}\`` : n; 844}; 845/** 846 * Main entry point. Create and return a {@link Jack} object. 847 */ 848const jack = (options = {}) => new Jack(options); 849exports.jack = jack; 850//# sourceMappingURL=index.js.map