create-pro-ts-lib
Version:
A command-line-interface for building Typescript libraries
270 lines (240 loc) • 7.17 kB
JavaScript
const yargs = require('yargs');
const { exit, env } = require('process');
const { hideBin } = require('yargs/helpers');
const chalk = require('chalk');
const path = require('path');
const {
promptsWrapper: prompts,
CANCELLED_REQUEST,
optionsToPromptsChoices,
} = require('./utils/prompts');
const config = require('./config');
const loadBaseLogic = require('./logics/base');
const FileManager = require('./utils/FilesManager');
const { resolveDirectory, ArgumentExtractor } = require('./utils/arguments');
const {
postProcessFiles,
ERRORS: TEMPLATE_ERROR,
createFiles,
} = require('./utils/template');
const { toYargsOptionsParam, OptionsCollection } = require('./utils/options');
const { detectPackageManager, createScriptString } = require('./utils/package-manager');
const { options } = config;
const mainCommand = yargs(hideBin(process.argv))
.usage('usage: \n\r create-pro-ts-lib <directory> <options>')
.help()
.version(config.version)
.options(
toYargsOptionsParam([
...options,
...config.flags,
...config.options,
...config.buildOptions,
])
)
.positional('directory', {
describe: 'A directory where you want initialize your ts project',
type: 'string',
demandOption: false,
});
async function main(argv) {
const cliDir = resolveDirectory(argv);
const argumentExtractor = new ArgumentExtractor(config);
const allOptions = new OptionsCollection().addAll(options);
const allBuildOptions = new OptionsCollection().addAll(config.buildOptions);
const allFlags = new OptionsCollection().addAll(config.flags);
const flags = argumentExtractor.getFlags(argv);
const cliOptions = argumentExtractor.getOptions(argv);
const cliBuildOptions = argumentExtractor.getBuildOptions(argv);
const prettier = allOptions.findByName('prettier');
const eslint = allOptions.findByName('eslint');
const prettierEslint = allOptions.findByName('prettier-eslint');
const webpack = allBuildOptions.findByName('webpack');
const vite = allBuildOptions.findByName('vite');
const allFlag = flags['all']?.flag;
const nameFlag = flags['name'];
const dryFlag = flags['dry'];
let shouldSetDifferentName = false;
//build questions
const questions = [];
if (flags['no-colors']) {
chalk.level = 0;
}
if (dryFlag) {
console.log(
chalk.hex(
'#FF7F11'
)`**Warning: --dry flag appeared and no actual files will be created`
);
}
if (!cliDir && nameFlag) {
console.log(
chalk.hex(
'#FF7F11'
)`**Warning: can't use --name without specifying dir`
);
}
if (!cliDir || (/[\/.]/.test(cliDir) && !nameFlag)) {
questions.push({
type: 'text',
name: 'name',
message: 'Project Name',
initial: (cliDir ?? '').replaceAll('/', '-'),
validate: value => {
if (value.length === 0) return 'You must fill this field';
else if (!/[a-zA-z0-9_\-@\/]/.test(value))
return 'This name contains illegal characters';
return true;
},
});
shouldSetDifferentName = true;
}
if (!allFlag && cliOptions.length === 0) {
const choices = optionsToPromptsChoices(options);
questions.push({
type: 'multiselect',
name: 'options',
message: 'Select your features',
hint: '- Space to select. Return to submit',
instructions: false,
min: 1,
choices,
});
}
if (cliBuildOptions.length !== 1) {
if (cliBuildOptions.length > 1) {
console.log(
chalk.hex(
'#FF7F11'
)`Warning: expected 1 build option, received ${cliBuildOptions.length}`
);
}
questions.push({
type: 'select',
name: 'buildOption',
hint: 'Return/Enter to submit',
message: 'Select your build tool',
choices: optionsToPromptsChoices(config.buildOptions),
});
}
try {
const formResults = await prompts(questions);
const name = formResults?.name ?? cliDir ?? nameFlag.value;
const dir = cliDir ?? name;
const filesManager = new FileManager(dir);
const selectedOptions = new OptionsCollection()
.addAll(cliOptions)
.addAll(formResults?.options);
const selectedBuildOptions = new OptionsCollection()
.addAll(cliOptions)
.add(formResults?.buildOption);
if (allFlag) {
selectedOptions.addAll(options);
if (selectedBuildOptions.list.length === 0) {
const defaultOption = config.buildOptions.find(
op => op.initialSelected
);
console.log(
`Info: no build option specified, using ${defaultOption.name} as default`
);
selectedBuildOptions.add(defaultOption);
}
}
if (selectedOptions.includes(webpack, vite)) {
console.warn(
'WARNING: Redundant Webpack and Vite configuration - It is recommended to choose only one build tool'
);
}
if (
selectedOptions.includes(prettier, eslint) ||
selectedOptions.includes(prettierEslint)
) {
// letting prettier-eslint eslint.config.js override eslint and prettier
selectedOptions
.remove(eslint)
.remove(prettierEslint)
.add(eslint) // will add them in order
.add(prettierEslint);
}
if (nameFlag || shouldSetDifferentName) {
const nameFlagInstance =
nameFlag?.flag ?? allFlags.findByName('name');
selectedOptions.add(nameFlagInstance);
}
const logicPayload = { dir, options: selectedOptions, name, flags, packageManager: detectPackageManager() };
await loadBaseLogic(filesManager, config, logicPayload);
await Promise.all(
selectedOptions.list
.concat(selectedBuildOptions.list)
.map(async option =>
option?.logic(filesManager, config, {
...logicPayload,
optionValue: argv[option.name],
})
)
);
postProcessFiles(filesManager);
if (!dryFlag) {
await createFiles(filesManager);
} else {
Object.keys(filesManager.relativeFiles).forEach(file => {
console.log(chalk.green`created`, file);
});
}
//printing the final output
const scripts = Object.keys(
filesManager.get('package.json').scripts ?? {}
).filter(x => !(/^pre/.test(x) || /^post/.test(x)));
const purple = chalk.hex('#c58af9');
const { blue } = chalk;
console.log(
'\r\n',
chalk.green.bold`Success!`,
'\r\n',
`path: ${purple(path.join(process.cwd(), dir))}`,
'\r\n\r\n',
'Now run:',
dir === '.' ? '' : `\r\n\t ${purple`cd`} ${blue(dir)}`,
selectedOptions.includes('husky')
? `\r\n\t ${chalk.greenBright`// git init or clone a repo`}`
: '',
`\r\n\t ${purple(detectPackageManager())} install`,
'\r\n\r\n',
'Available Commands:',
scripts
.map(
script =>
`\r\n\t${createScriptString(script, { cli: purple })}`
)
.sort((a, b) =>
(a.split('run').at(1) ?? a).localeCompare(
b.split('run').at(1) ?? b
)
)
.join(''),
'\r\n'
);
} catch (e) {
const { red } = chalk;
switch (e?.code) {
case CANCELLED_REQUEST:
console.error(red`Ok nevermind...`);
break;
case TEMPLATE_ERROR.DIR_EXISTS_ERROR:
console.error(red`ERROR! Directory already exists`);
break;
case TEMPLATE_ERROR.DIR_NOT_EMPTY_ERROR:
console.error(red`ERROR! Directory contains files`);
break;
default:
console.error(chalk.red(e.message));
}
if (env['VERBOSE']) {
console.error(e);
}
exit(1);
}
}
// Running the code
void main(mainCommand.argv);