diff --git a/.changeset/sharp-beds-laugh.md b/.changeset/sharp-beds-laugh.md new file mode 100644 index 00000000..b7afde72 --- /dev/null +++ b/.changeset/sharp-beds-laugh.md @@ -0,0 +1,5 @@ +--- +'create-robo': minor +--- + +refactor(cr): ask for typescript support separately from features diff --git a/.changeset/tasty-kangaroos-cheat.md b/.changeset/tasty-kangaroos-cheat.md new file mode 100644 index 00000000..1e7a9227 --- /dev/null +++ b/.changeset/tasty-kangaroos-cheat.md @@ -0,0 +1,5 @@ +--- +'create-robo': minor +--- + +feat(cr): official plugins now included in the feature selection diff --git a/packages/create-robo/src/index.ts b/packages/create-robo/src/index.ts index 37a7815c..94c1d465 100644 --- a/packages/create-robo/src/index.ts +++ b/packages/create-robo/src/index.ts @@ -26,18 +26,14 @@ new Command('create-robo ') const projectName = args[0] const robo = new Robo(projectName) + await robo.askUseTypeScript() + // Get user input to determine which features to include or use the recommended defaults const selectedFeaturesOrDefaults = await robo.getUserInput() - if (selectedFeaturesOrDefaults === 'defaults') { - await robo.createPackage(['TypeScript', 'ESLint', 'Prettier']) - } else { - await robo.createPackage(selectedFeaturesOrDefaults) - } + await robo.createPackage(selectedFeaturesOrDefaults) // Determine if TypeScript is selected and copy the corresponding template files - const useTypeScript = - selectedFeaturesOrDefaults === 'defaults' || (selectedFeaturesOrDefaults as string[]).includes('TypeScript') - await robo.copyTemplateFiles('', useTypeScript) + await robo.copyTemplateFiles('') // Ask the user for their Discord credentials (token and client ID) and store them for later use await robo.askForDiscordCredentials() diff --git a/packages/create-robo/src/robo.ts b/packages/create-robo/src/robo.ts index 9ec257fb..1e2312f5 100644 --- a/packages/create-robo/src/robo.ts +++ b/packages/create-robo/src/robo.ts @@ -3,32 +3,37 @@ import path from 'path' import inquirer from 'inquirer' import chalk from 'chalk' import { fileURLToPath } from 'node:url' -import { exec, getPackageManager, hasProperties } from './utils.js' +import { + ESLINT_IGNORE, + PRETTIER_CONFIG, + exec, + generateRoboConfig, + getPackageManager, + hasProperties, + sortObjectKeys +} from './utils.js' import { logger } from './logger.js' +import type { Plugin } from '@roboplay/robo.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) interface PackageJson { name: string - version: string description: string + version: string engines: { node: string } type: 'module' | 'commonjs' scripts: Record - dependencies: { - 'discord.js': string - '@roboplay/robo.js': string - [key: string]: string - } - devDependencies: { - [key: string]: string - } + dependencies: Record + devDependencies: Record } export default class Robo { + // Custom properties used to build the Robo project private _name: string + private _useTypeScript: boolean private _workingDir: string constructor(name: string) { @@ -36,35 +41,48 @@ export default class Robo { this._workingDir = path.join(process.cwd(), name) } - async getUserInput(): Promise<'defaults' | string[]> { - const { useDefaults } = await inquirer.prompt([ + async askUseTypeScript() { + const { useTypeScript } = await inquirer.prompt([ { type: 'list', - name: 'useDefaults', - message: 'Choose an option:', + name: 'useTypeScript', + message: chalk.blue('Would you like to use TypeScript?'), choices: [ - { - name: 'Use recommended defaults', - value: 'defaults' - }, - { - name: 'Customize features', - value: 'custom' - } + { name: 'Yes', value: true }, + { name: 'No', value: false } ] } ]) - if (useDefaults === 'defaults') { - return 'defaults' - } + this._useTypeScript = useTypeScript + } + async getUserInput(): Promise { const { selectedFeatures } = await inquirer.prompt([ { type: 'checkbox', name: 'selectedFeatures', message: 'Select features:', - choices: [{ name: 'TypeScript' }, { name: 'ESLint' }, { name: 'Prettier' }] + choices: [ + { + name: `${chalk.bold('ESLint')} (recommended) - Keeps your code clean and consistent.`, + short: 'ESLint', + value: 'eslint', + checked: true + }, + { + name: `${chalk.bold('Prettier')} (recommended) - Automatically formats your code for readability.`, + short: 'Prettier', + value: 'prettier', + checked: true + }, + new inquirer.Separator('\nOptional Plugins:'), + { + name: `${chalk.bold('GPT')} - Enable your bot to generate human-like text with the power of GPT.`, + short: 'GPT', + value: 'gpt' + } + ] } ]) @@ -75,6 +93,7 @@ export default class Robo { // Find the package manager that triggered this command const packageManager = getPackageManager() logger.debug(`Using ${chalk.bold(packageManager)} in ${this._workingDir}...`) + await fs.mkdir(this._workingDir, { recursive: true }) // Create a package.json file based on the selected features const packageJson: PackageJson = { @@ -100,17 +119,38 @@ export default class Robo { } const runPrefix = packageManager + packageManager === 'npm' ? 'npm run ' : packageManager + ' ' - if (features.includes('TypeScript')) { + if (this._useTypeScript) { packageJson.devDependencies['@swc/core'] = '^1.3.44' packageJson.devDependencies['@types/node'] = '^18.14.6' packageJson.devDependencies['typescript'] = '^5.0.0' } - if (features.includes('ESLint')) { + if (features.includes('eslint')) { packageJson.devDependencies['eslint'] = '^8.36.0' packageJson.scripts['lint'] = runPrefix + 'lint:eslint' packageJson.scripts['lint:eslint'] = 'eslint . --ext js,jsx,ts,tsx' + + const eslintConfig = { + extends: ['eslint:recommended'], + env: { + node: true + }, + parser: undefined as string | undefined, + plugins: [] as string[], + root: true, + rules: {} + } + if (this._useTypeScript) { + eslintConfig.extends.push('plugin:@typescript-eslint/recommended') + eslintConfig.parser = '@typescript-eslint/parser' + eslintConfig.plugins.push('@typescript-eslint') + + packageJson.devDependencies['@typescript-eslint/eslint-plugin'] = '^5.56.0' + packageJson.devDependencies['@typescript-eslint/parser'] = '^5.56.0' + } + await fs.writeFile(path.join(this._workingDir, '.eslintignore'), ESLINT_IGNORE) + await fs.writeFile(path.join(this._workingDir, '.eslintrc.json'), JSON.stringify(eslintConfig, null, 2)) } - if (features.includes('Prettier')) { + if (features.includes('prettier')) { packageJson.devDependencies['prettier'] = '^2.8.5' packageJson.scripts['lint:style'] = 'prettier --write .' @@ -118,16 +158,41 @@ export default class Robo { if (hasLintScript) { packageJson.scripts['lint'] += ' && ' + runPrefix + 'lint:style' } + + // Create the prettier.config.js file + await fs.writeFile(path.join(this._workingDir, 'prettier.config.js'), PRETTIER_CONFIG) } - await fs.mkdir(this._workingDir, { recursive: true }) + + const plugins: Plugin[] = [] + if (features.includes('gpt')) { + packageJson.dependencies['@roboplay/plugin-gpt'] = '^1.0.0' + plugins.push([ + '@roboplay/plugin-gpt', + { + openaiKey: 'YOUR_OPENAI_KEY_HERE' + } + ]) + } + + // Create the robo.mjs file + const roboConfig = generateRoboConfig(plugins) + await fs.mkdir(path.join(this._workingDir, '.config'), { recursive: true }) + await fs.writeFile(path.join(this._workingDir, '.config', 'robo.mjs'), roboConfig) + + // Sort scripts, dependencies and devDependencies alphabetically because this is important to me + packageJson.scripts = sortObjectKeys(packageJson.scripts) + packageJson.dependencies = sortObjectKeys(packageJson.dependencies) + packageJson.devDependencies = sortObjectKeys(packageJson.devDependencies) + + // Order scripts, dependencies and devDependencies await fs.writeFile(path.join(this._workingDir, 'package.json'), JSON.stringify(packageJson, null, 2)) // Install dependencies using the package manager that triggered the command await exec(`${packageManager} install`, { cwd: this._workingDir }) } - async copyTemplateFiles(sourceDir: string, useTypeScript: boolean): Promise { - const templateDir = useTypeScript ? '../templates/ts' : '../templates/js' + async copyTemplateFiles(sourceDir: string): Promise { + const templateDir = this._useTypeScript ? '../templates/ts' : '../templates/js' const sourcePath = path.join(__dirname, templateDir, sourceDir) const targetPath = path.join(this._workingDir, sourceDir) @@ -140,7 +205,7 @@ export default class Robo { if (stat.isDirectory()) { await fs.mkdir(itemTargetPath, { recursive: true }) - await this.copyTemplateFiles(path.join(sourceDir, item), useTypeScript) + await this.copyTemplateFiles(path.join(sourceDir, item)) } else { await fs.copyFile(itemSourcePath, itemTargetPath) } diff --git a/packages/create-robo/src/utils.ts b/packages/create-robo/src/utils.ts index 555e36e9..d1f277f2 100644 --- a/packages/create-robo/src/utils.ts +++ b/packages/create-robo/src/utils.ts @@ -1,12 +1,41 @@ -import { spawn } from 'child_process' -import { logger } from './logger.js' -import type { SpawnOptions } from 'child_process' +import { spawn } from 'node:child_process' import chalk from 'chalk' +import { logger } from './logger.js' +import type { SpawnOptions } from 'node:child_process' +import type { Plugin } from '@roboplay/robo.js' type PackageManager = 'npm' | 'pnpm' | 'yarn' +export const ESLINT_IGNORE = `node_modules +.config +.robo\n` + export const IS_WINDOWS = /^win/.test(process.platform) +export const PRETTIER_CONFIG = `module.exports = { + printWidth: 120, + semi: false, + singleQuote: true, + trailingComma: 'none', + tabWidth: 2, + useTabs: true +}\n` + +const ROBO_CONFIG = `// @ts-check + +/** + * @type {import('@roboplay/robo.js').Config} + **/ +export default { + clientOptions: { + intents: [ + 'Guilds', + 'GuildMessages' + ] + }, + plugins: {{plugins}} +}\n` + /** * Eh, just Windows things */ @@ -24,7 +53,7 @@ export function exec(command: string, options?: SpawnOptions) { // Run command as child process const args = command.split(' ') const childProcess = spawn(args.shift(), args, { - ...options ?? {}, + ...(options ?? {}), env: { ...process.env, FORCE_COLOR: '1' }, stdio: 'inherit' }) @@ -45,6 +74,11 @@ export function exec(command: string, options?: SpawnOptions) { }) } +export function generateRoboConfig(plugins: Plugin[]) { + const stringifiedPlugins = JSON.stringify(plugins, null, 2).replace(/\n/g, '\n ') + return ROBO_CONFIG.replace('{{plugins}}', stringifiedPlugins) +} + /** * Get the package manager used to run this CLI * This allows developers to use their preferred package manager seamlessly @@ -67,3 +101,12 @@ export function hasProperties>( ): obj is T & Record { return typeof obj === 'object' && obj !== null && props.every((prop) => prop in obj) } + +export function sortObjectKeys(obj: Record) { + return Object.keys(obj) + .sort() + .reduce((acc, key) => { + acc[key] = obj[key] + return acc + }, {} as Record) +} diff --git a/packages/create-robo/templates/js/.config/robo.mjs b/packages/create-robo/templates/js/.config/robo.mjs deleted file mode 100644 index e69994c1..00000000 --- a/packages/create-robo/templates/js/.config/robo.mjs +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-check - -/** - * @type {import('@roboplay/robo.js').Config} - **/ -export default { - clientOptions: { - intents: [ - 'Guilds', - 'GuildMessages' - ] - }, - plugins: [] -} diff --git a/packages/create-robo/templates/ts/.config/robo.mjs b/packages/create-robo/templates/ts/.config/robo.mjs deleted file mode 100644 index e69994c1..00000000 --- a/packages/create-robo/templates/ts/.config/robo.mjs +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-check - -/** - * @type {import('@roboplay/robo.js').Config} - **/ -export default { - clientOptions: { - intents: [ - 'Guilds', - 'GuildMessages' - ] - }, - plugins: [] -}