@tapjs/run
Version:
Command-line interface for the node-tap runner
241 lines • 8.54 kB
JavaScript
import chalk from 'chalk';
import { spawnSync } from 'child_process';
import { readFileSync, writeFileSync } from 'fs';
import { stat } from 'fs/promises';
import { mkdirpSync } from 'mkdirp';
import { dirname, relative, resolve } from 'path';
import { rimrafSync } from 'rimraf';
import yaml from 'tap-yaml';
import { strict } from 'tcompare';
const fileExists = async (p) => stat(p).then(s => s.isFile(), () => false);
const vimTag = '# v' + 'im: set filetype=yaml :';
const logVim = () => console.log(vimTag);
const logYaml = (v) => console.log(yaml.stringify(v).trimEnd());
const set = async (args, config) => {
const usage = 'Usage: tap config set <key=[val]> [<key=[val]> ...]';
if (!args.length) {
console.error(chalk.red('must provide key=value arguments'));
console.error(usage);
process.exit(1);
}
const { valuesFromConfigFile } = config;
const data = Object.fromEntries(Object.entries(valuesFromConfigFile).map(([k, v]) => [
k,
Array.isArray(v) ? [...v] : v,
]));
const allDefs = config.jack.toJSON();
for (const kv of args) {
const [key, ...r] = kv.split('=');
if (!r.length) {
console.error(chalk.red(`no value provided for ${key}`));
console.error(usage);
process.exit(1);
}
const val = r.join('=');
const def = allDefs[key];
if (!def) {
console.error(chalk.red('Unknown config key: ' + key));
continue;
}
if (!val) {
delete data[key];
continue;
}
const { type, multiple } = def;
let v = type === 'number' ? parseInt(val, 10) : val;
if (type === 'boolean') {
if (v !== 'true' && v !== 'false') {
console.error(chalk.red(`${key} must be 'true' or 'false'`));
process.exit(1);
}
else {
v = v === 'true';
}
}
if (multiple) {
if (data[key])
data[key].push(v);
else
data[key] = [v];
}
else {
data[key] = v;
}
}
if (strict(data, valuesFromConfigFile).match) {
console.error('no changes made to config data');
process.exit(0);
}
try {
config.jack.validate(data);
}
catch (er) {
console.error(chalk.red('invalid configuration'));
console.error(er.message);
process.exit(1);
}
await config.editConfigFile(data, undefined, true);
const rel = relative(process.cwd(), config.configFile);
console.log(`wrote changes to ${rel}`);
};
const get = async (args, config) => {
if (!args.length) {
console.error(chalk.red('no keys provided'));
console.error('Usage: tap config get <key> [<key> ...]');
process.exit(1);
}
const defs = config.jack.toJSON();
let values = undefined;
for (const k of args) {
if (!defs[k]) {
console.error(chalk.red(`unknown config field: ${k}`));
continue;
}
const value = config.get(k);
if (value === undefined)
continue;
values ??= {};
values[k] = value;
}
if (values) {
console.log(yaml.stringify(values).trimEnd());
}
};
const list = async (args, config) => {
const { valuesFromConfigFile, values } = config;
const filter = ([k, v]) => (!Array.isArray(v) || v.length) && (!args.length || args.includes(k));
const fileEntries = Object.entries(valuesFromConfigFile)
.filter(filter)
.sort(([a], [b]) => a.localeCompare(b, 'en'));
const fromFile = Object.fromEntries(fileEntries);
const envCliEntries = Object.entries(values)
.filter(([k, v]) => filter([k, v]) && fromFile[k] !== v)
.sort(([a], [b]) => a.localeCompare(b, 'en'));
const fromEnvCli = Object.fromEntries(envCliEntries);
let output = `${vimTag}\n`;
if (fileEntries.length) {
output +=
`\n# from ${relative(process.cwd(), config.configFile)}\n` +
yaml.stringify(fromFile);
}
if (envCliEntries.length) {
if (fileEntries.length) {
output += `\n# env, cli, and defaults\n`;
}
output += yaml.stringify(fromEnvCli);
}
output = output
.replace(/^(# .*)$/gm, (_, _1) => chalk.dim(_1))
.replace(/^([^:\n ]+):/gm, (_, _1) => chalk.bold(_1) + ':');
console.log(output.trimEnd());
};
const dump = async (config) => {
const { values } = config;
const v = Object.fromEntries(Object.entries(values).filter(([_, v]) => !Array.isArray(v) || v.length));
logVim();
logYaml(v);
};
// exported for testing
export const firstOf = (a, def) => {
for (const i of a)
if (i)
return i;
return def;
};
const isEmpty = (a) => a === undefined || (Array.isArray(a) && a.length === 0);
const edit = async (args, config) => {
const { configFile, valuesFromConfigFile } = config;
const fileDefs = new Set(Object.keys(valuesFromConfigFile));
const rel = relative(process.cwd(), configFile);
const editor = firstOf([process.env.VISUAL, process.env.EDITOR],
/* c8 ignore start */
process.platform === 'win32' ? 'notepad.exe' : 'vim');
const tmp = resolve(config.projectRoot, '.tap', 'config-edit-tmp.yaml');
if (args[0] !== 'resume') {
rimrafSync(tmp);
mkdirpSync(dirname(tmp));
const allDefs = config.jack.toJSON();
const allConfigs = Object.entries(allDefs)
.filter(([k]) => !fileDefs.has(k))
.sort(([a], [b]) => a.localeCompare(b, 'en'))
.map(([k, def]) => {
return !isEmpty(def.default) ?
yaml.stringify({ [k]: def.default }).trimEnd()
: `${k}: ${def.type}${def.multiple ? '[]' : ''}`;
})
.join('\n')
.replace(/^/gm, '# ');
const tmpData = `${vimTag}
# this is a yaml file containing tap configuration
#
# when you're done, the data will be written to ${rel}${/\.json$/.test(rel) ?
'\n# (in the "tap" section of the json file)'
: ''}
# if the config isn't valid, or if you quit without making changes,
# then no changes will be made.
#
# Comments and empty lines will be removed.
${Object.keys(valuesFromConfigFile).length ?
'#\n# Existing configs:\n\n' + yaml.stringify(valuesFromConfigFile)
: ''}
# All available configuration options. See \`tap help\` for descriptions.
#
${allConfigs}
`;
writeFileSync(tmp, tmpData, 'utf8');
}
else if (!(await fileExists(tmp))) {
console.error(chalk.red('no prior editing session to resume'));
process.exit(1);
}
const result = spawnSync(editor, [tmp], { stdio: 'inherit' });
const { signal, status } = result;
const editResume = 'run `tap config edit resume` to continue making changes';
if (signal || status) {
console.error(chalk.red('edit command failed, no changes made'));
console.error(editResume);
console.error({ signal, status });
process.exit(1);
}
// read the file and see if we have changes to make.
const newData = readFileSync(tmp, 'utf8');
const newValues = yaml.parse(newData);
if (strict(valuesFromConfigFile, newValues).match) {
console.error('no changes made to config data');
console.error(editResume);
process.exit(0);
}
try {
config.jack.validate(newValues);
}
catch (er) {
console.error(chalk.red('invalid configuration'));
console.error(er.message);
console.error(editResume);
process.exit(1);
}
await config.editConfigFile(newValues, configFile, true);
rimrafSync(tmp);
console.log(`wrote changes to ${rel}`);
};
export const config = async (args, config) => {
if (!args.length)
args = ['list'];
switch (args[0]) {
case 'dump':
return dump(config);
case 'get':
return get(args.slice(1), config);
case 'list':
return list(args.slice(1), config);
case 'edit':
return edit(args.slice(1), config);
case 'set':
return set(args.slice(1), config);
default:
console.error(chalk.red('invalid tap config command'));
console.error('must be one of: get list dump set edit');
process.exit(1);
}
};
//# sourceMappingURL=config.js.map