africa
Version:
A library to interactively create and read configuration files.
136 lines (117 loc) • 3.96 kB
JavaScript
import ask from 'reloquent'
import bosom from 'bosom'
import Group from './Group'
/**
* Ask questions and write answers to the RC file.
* @param {_reloquent.Questions} questions The set of questions. These need to be updated to include defaults.
* @param {string} path The path for the RC file.
* @param {{
* skipExisting: (boolean|undefined),
* config: (!Object|undefined),
* timeout: (number|undefined)
* }} [options] Additional options.
*/
export async function askQuestionsAndWrite(questions, path, {
skipExisting = false, config = {}, timeout,
} = {}) {
/** @type {!Object<string, !Group>} */
const groups = {}
let current = {}
const q = Object.entries(questions).reduce((acc, [key, question]) => {
if (question instanceof Group) {
groups[key] = question
return acc
}
acc[key] = question
return acc
}, {})
let answers = await ask(q, timeout)
try {
current = await bosom(path)
} catch (err) {
// ok
}
if (skipExisting) {
answers = skipAnswers(answers, config, current) || {}
}
const ga = await Object.entries(groups).reduce(async (acc, [key, group]) => {
acc = await acc
let g = await ask(group.questions, timeout)
if (skipExisting) g = skipAnswers(g, config[key], current[key])
if (g) acc[key] = g
return acc
}, {})
// answers not from questions but in config anyhow
const extra = skipAnswers(current, config, current) || {}
const total = { ...extra, ...answers, ...ga }
await bosom(path, total, { space: 2 })
return total
}
const skipAnswers = (answers, config = {}, current = {}) => {
let allSkipped = true
const skipped = Object.entries(answers).reduce((acc, [key, val]) => {
const CURRENT = current[key]
const DEFAULT = config[key]
if (val == DEFAULT && val != CURRENT) return acc
allSkipped = false
acc[key] = val
return acc
}, {})
if (allSkipped) return null
return skipped
}
/**
* Merge two configurations, with `a` as base one, such that its properties won't be overridden by `b`.
* @param {!Object} a The base configuration.
* @param {!Object} b The extension to configuration.
*/
export const merge = (a, b) => {
return Object.entries(b).reduce((acc, [k, value]) => {
if (typeof value == 'object' && value !== null && typeof a[k] == 'object') {
acc[k] = merge(a[k], value)
}
else acc[k] = value
return acc
}, a)
}
/**
* Adds default value from the config.
* @param {_reloquent.Questions} questions A set of questions to extend with default value from the existing config.
* @param {!Object} current Current configuration object (answers).
* @returns {_reloquent.Questions} Questions with updated defaultValue where answers were present in the passed config object.
*/
const extendQuestions = (questions, current) => {
const q = Object.entries(questions).reduce((acc, [key, question]) => {
const defaultValue = current[key]
if (!defaultValue) {
acc[key] = question
return acc
}
let value = typeof question == 'string' ? { text: question } : question
if (question instanceof Group) {
question.questions = extendQuestions(question.questions, defaultValue)
} else {
value = { ...value, defaultValue }
}
acc[key] = value
return acc
}, {})
return q
}
/**
* Ask questions while adding default values when asking.
* @param {_reloquent.Questions} questions
* @param {string} path The path to the rc file.
* @param {!Object} config Current answers.
* @param {number} [timeout]
* @param {{ skipExisting: (boolean|undefined) }} [opts]
*/
export const forceQuestions = async (questions, path, config, timeout, { skipExisting } = {}) => {
const q = extendQuestions(questions, config)
const conf = await askQuestionsAndWrite(q, path, { timeout, skipExisting, config })
return conf
}
/**
* @suppress {nonStandardJsDocs}
* @typedef {import('../..').Questions} _reloquent.Questions
*/