import * as fs from 'fs' import * as path from 'path' import camelcase from 'camelcase' import * as chalk from 'chalk' import * as dedent from 'dedent' import { Linter } from 'eslint' import * as inquirer from 'inquirer' import { format } from 'prettier' import { TemplateParams } from './template' import templates from './templates' export default async function cli(args: string[]): Promise { /** * Exit the program with an error message * Program will exit with status code 1 * @param message */ function fail(message: string): never { console.error(chalk.red(`ERROR: `) + chalk.yellow(message)) process.exit(1) } /** * Find a file in the current directory or one of its parents * @param name Name of the file to find * @param dir Directory to start looking from * @returns The file's content, if found */ function findJsonConfig( name: string, dir: string ): { path: string; content: { [key: string]: unknown } } | null { console.debug( chalk.blue('Looking for an ') + chalk.yellow(`"${name}"`) + chalk.blue(' file...') ) if (!path.isAbsolute(dir)) { // Get an absolute path to allow getting its parent using path.dirname() dir = path.join(process.cwd(), dir) } // The previous directory // This is used to check if we reached the top-level directory ; for instance on Linux: // > path.dirname('/') === '/' // So if the previous directory and the current one are equal, this means we reached the top-level directory let prevDir = dir // Search until we find the desired file while (!fs.existsSync(path.join(dir, name))) { // Get path to the parent directory dir = path.dirname(dir) // If the path is empty or equal to the previous path, we reached the top-level directory if (!dir || prevDir === dir) { console.warn(chalk.yellow('File was not found in destination directory or its parents.')) return null } prevDir = dir } // The desired file's path const fpath = path.join(dir, name) console.log(chalk.green('Found at path: ') + chalk.yellow(fpath)) let content: { [key: string]: unknown } // Handle errors properly (e.g. with a parser that does "txt => JSON.parse(txt)") try { content = JSON.parse(fs.readFileSync(fpath, 'utf8')) } catch (err) { fail(`Failed to parse file "${name}" at "${dir}": ${err.message}`) } return { path: fpath, content } } /** Name of the entity template */ const ENTITY_TEMPLATE = 'entity' if (!(ENTITY_TEMPLATE in templates.module)) { fail( 'Template "entity" is missing from modules template.\n' + 'This is likely due to the entity template being renamed.\n' + "Please update the generator's source code and run it again." ) } // The promise resolving with the desired module's name and destination path // If automatic mode is enabled ("--auto") flag, this promise resolves immediatly // Else, it triggers an Inquirer prompt which must be handled asynchronously (thus we're using a promise) let answers: { moduleName: string; moduleDestPath: string } try { const promise = args[0] && args[1] && args[2] === '--auto' ? // Automatic mode Promise.resolve({ moduleName: args[0], moduleDestPath: args[1] }) : // Manual mode inquirer.prompt([ { name: 'moduleName', type: 'input', message: 'Module name: ', default: args[0] ?? '', validate: name => name.trim().length > 0 }, { name: 'moduleDestPath', type: 'input', message: 'Destination path: ', default: args[1] ?? process.cwd(), validate: path => { const valid = fs.existsSync(path) && fs.lstatSync(path).isDirectory() if (!valid) console.error(chalk.red(' Provided path does not exist')) return valid } } ]) answers = await promise } catch (e) { fail('Failed to get answers from Inquirer') } /** Name of the target module (not camel-cased yet) */ const moduleName = answers.moduleName /** Path to the target module */ const modulePath = path.join(answers.moduleDestPath, answers.moduleName) if (fs.existsSync(modulePath)) { fail('Provided module path already exists!') } fs.mkdirSync(modulePath) /** Parameters for template generation */ const params: TemplateParams = { module: moduleName, className: camelcase(moduleName, { pascalCase: true }), camelName: camelcase(moduleName) } /** ESLint options */ const eslintOptions = findJsonConfig('.eslintrc.json', modulePath) const esLinter = new Linter() /** Prettier options */ const prettierOptions = findJsonConfig('.prettierrc', modulePath) // Determine the templates to generate const toGenerate = Object.entries( Object.assign(Object.assign({}, templates.module), templates.dtos) ) // Generate the module using existing templates for (const [key, template] of toGenerate) { // Generate the template const out = template(params) // Get path to the output file const filePath = path.join(modulePath, out.fileName) // Ensure it does not already exist to avoid accidental overriding if (fs.existsSync(filePath)) { fail(`"${key}" file already exists (at path: ${filePath})`) } // Create required parent directories fs.mkdirSync(path.dirname(filePath), { recursive: true }) console.log( chalk.blue( `Generating file ${chalk.yellow('"' + filePath + '"')} from template ${chalk.yellow( key )}...` ) ) // "dedent" is used to remove the indentation from the templates strings // For instance, " a\n b" will be converted to "a\nb" /** The file's future content */ let content = dedent(out.content) // Format the code using ESLint if (eslintOptions) { content = esLinter.verifyAndFix(content, eslintOptions.content as any, { filename: filePath }).output } // Format the code using Prettier if (prettierOptions) { content = format( content, Object.assign({ filepath: filePath, parser: 'typescript' }, prettierOptions.content) ) } fs.writeFileSync(filePath, content) } console.log(chalk.green('Done :D')) }