feat: manual options/configs selection support

This commit is contained in:
Guz013
2023-08-04 10:07:59 -03:00
parent e775d83ccf
commit d2adda8aeb
6 changed files with 90 additions and 41 deletions

View File

@@ -37,6 +37,7 @@
"yaml": "^2.3.1"
},
"devDependencies": {
"@types/node": "^20.4.2"
"@types/node": "^20.4.2",
"@types/prompts": "^2.4.4"
}
}

View File

@@ -13,17 +13,20 @@ export default class Cli {
dir: process.cwd(),
};
setArgs() {
/**
* @param {import('./types').CliArgs} [args] Cli arguments object
*/
constructor(args) {
this.#program
.option('--packages <string...>')
.option('--merge-to-root')
.option('--dir <path>', undefined);
this.#program.parse();
.option('--dir <path>', undefined)
.parse();
this.args = {
...this.args,
...this.#program.opts(),
...args,
};
this.args.dir = !this.args.dir.startsWith('/')
@@ -33,17 +36,15 @@ export default class Cli {
}
async run() {
this.setArgs();
console.log(this.args.dir);
const processor = new ConfigsProcessor({ configs });
const packages = (await new Workspace(this.args.dir, this.args?.packages )
.getPackages())
const workspace = new Workspace(this.args.dir, this.args?.packages);
let packages = (await workspace.getPackages())
.map(pkg => {
pkg.config = processor.detectConfig(pkg);
return pkg;
});
packages = await processor.questionConfigs(packages);
const configsMaps = processor.generateConfigMap(packages);

View File

@@ -4,6 +4,8 @@ export default [
{
name: 'framework',
type: 'multiple',
description: 'The UI frameworks being used in the project',
manual: true,
options: [
{
name: 'svelte',

View File

@@ -1,8 +1,9 @@
#!node
import path from 'node:path';
import { createSpinner } from 'nanospinner';
import glob from 'picomatch';
import prompts from 'prompts';
import c from 'picocolors';
import str from './lib/str.js';
export default class ConfigsProcessor {
/** @type {string} */
@@ -31,25 +32,17 @@ export default class ConfigsProcessor {
* @param {import('./types.js').Package} pkg - Package to detect from
* @param {import('./types.js').Config['options']} options - Options to be passed
* @param {boolean} single - Whether to only detect one option
* @param {import('nanospinner').Spinner} spinner - Spinner to update
* @returns {string[]} - The detected options
*/
detectOptions(pkg, options, single, spinner) {
detectOptions(pkg, options, single) {
/** @type {string[]} */
const detectedOptions = [];
for (const option of options) {
spinner.update({
text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)}`)}`,
});
if (option.detect === true) {
detectedOptions.push(option.name);
spinner.update({
text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)} ${c.green('✓')}`)}`,
});
continue;
}
else if (!option.detect) continue;
@@ -61,54 +54,96 @@ export default class ConfigsProcessor {
if (files.length > 0 || directories.length > 0) {
detectedOptions.push(option.name);
spinner.update({
text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)} ${c.green('✔')}`)}`,
});
if (single) break;
}
else {
spinner.update({
text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: option ${c.bold(option.name)} ${c.red('✖')}`)}`,
});
}
}
return detectedOptions;
}
/**
* @param {import('./types.js').Package} pkg - The package to detect configs
* @returns {import('./types.js').Package['config']} - Detected configs record
* @param {import('./types.js').Package[]} packages - The package to detect configs
* @returns {Promise<import('./types.js').Package[]>} - The selected options by the user
*/
async questionConfigs(packages) {
const instructions = c.dim(`\n${c.bold('A: Toggle all')} - ↑/↓: Highlight option - ←/→/[space]: Toggle selection - enter/return: Complete answer`);
for (const config of this.configs.filter(c => c.manual)) {
/** @type {import('prompts').Choice[]} */
const configChoices = config.options.map(option => {return { title: `${str.capitalize(option.name)}`, value: option.name };});
/** @type {Record<string, string[]>} */
const selectedOptions = await prompts({
name: config.name,
type: config.type === 'single' ? 'select' : 'multiselect',
message: str.capitalize(config.name),
choices: configChoices,
hint: config.description,
instructions: instructions + c.dim(c.italic('\nSelect none if you don\'t want to use this configuration\n')),
});
if (selectedOptions[config.name].length === 0) continue;
/** @type {{title: string, value: import('./types').Package}[]} */
const packagesOptions = packages
.map(pkg => {
return !pkg.root
? {
title: `${pkg.name} ${c.dim(pkg.path.replace(this.dir, '.'))}`,
value: pkg,
}
: { title: 'root', value: pkg };
})
.filter(p => p.title !== 'root');
/** @type {Record<'packages', import('./types').Package[]>} */
const selected = await prompts({
name: 'packages',
type: 'multiselect',
message: `What packages would you like to apply ${config.type === 'single' ? 'this choice' : 'these choices'}?`,
choices: packagesOptions,
min: 1,
instructions: instructions + c.dim(c.italic('\nToggle all to use in the root configuration\n')),
});
selected.packages = selected.packages ?? [];
selected.packages.map(pkg => { pkg.config = { ...pkg.config, ...selectedOptions }; return pkg; });
packages.map(pkg => selected.packages.find(s => s.name === pkg.name) ?? pkg);
}
return packages;
}
/**
* @param {import('./types').Package} pkg - The package to detect configs
* @returns {import('./types').Package['config']} - Detected configs record
*/
detectConfig(pkg) {
const spinner = createSpinner(`Configuring ${c.bold(c.blue(pkg.name))}`);
spinner.start();
/** @type {import('./types.js').Package['config']} */
const pkgConfig = {};
for (const config of this.configs) {
for (const config of this.configs.filter(c => !c.manual)) {
pkgConfig[config.name] = this.detectOptions(
pkg,
config.options,
config.type === 'single',
spinner,
);
spinner.update({ text: `Configuring ${c.bold(c.blue(pkg.name))}${c.dim(`: config ${config.name}`)}` });
}
spinner.success({ text: `Configuring ${c.bold(c.blue(pkg.name))}\n${c.dim(JSON.stringify(pkgConfig))}\n` });
return pkgConfig;
}
/**
* @param {import('./types.js').Package[]} packages Packages to generate the map from
* @returns {import('./types.js').PackagesConfigsMap} A map of what packages has some configuration
* @param {import('./types').Package[]} packages Packages to generate the map from
* @returns {import('./types').PackagesConfigsMap} A map of what packages has some configuration
*/
generateConfigMap(packages) {
/** @type {import('./types.js').PackagesConfigsMap} */
/** @type {import('./types').PackagesConfigsMap} */
const configMap = new Map();
for (const pkg of packages) {
@@ -122,7 +157,6 @@ export default class ConfigsProcessor {
optionsMap.set(option, [pkg.path, ...paths]);
if (paths.length >= packages.length - 2 || paths.includes(this.dir)) {
console.log('a', packages.length, paths.length);
optionsMap.set(option, [this.dir]);
}
});

View File

@@ -0,0 +1,10 @@
/**
* @param {string} str - The string to capitalize
* @returns {string} The capitalized string
*/
function capitalize(str) {
return str[0].toUpperCase() + str.slice(1);
}
export default { capitalize };

View File

@@ -10,6 +10,7 @@ export interface Config {
name: string
type: 'single' | 'multiple'
manual?: boolean
description?: string
options: {
name: string
packages: Record<string, string | string[] | [string, string][]>