Skip to content

Commit

Permalink
feat(cr): revamped feature selection
Browse files Browse the repository at this point in the history
- Now asks for TypeScript support before anything else
- Official plugins are now available as preset options in feature selection
- Added descriptions to feature selections
  • Loading branch information
Pkmmte committed May 2, 2023
1 parent ed8987b commit a5c1e6f
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 73 deletions.
5 changes: 5 additions & 0 deletions .changeset/sharp-beds-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'create-robo': minor
---

refactor(cr): ask for typescript support separately from features
5 changes: 5 additions & 0 deletions .changeset/tasty-kangaroos-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'create-robo': minor
---

feat(cr): official plugins now included in the feature selection
12 changes: 4 additions & 8 deletions packages/create-robo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,14 @@ new Command('create-robo <projectName>')
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()
Expand Down
131 changes: 98 additions & 33 deletions packages/create-robo/src/robo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,68 +3,86 @@ 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<string, string>
dependencies: {
'discord.js': string
'@roboplay/robo.js': string
[key: string]: string
}
devDependencies: {
[key: string]: string
}
dependencies: Record<string, string>
devDependencies: Record<string, string>
}

export default class Robo {
// Custom properties used to build the Robo project
private _name: string
private _useTypeScript: boolean
private _workingDir: string

constructor(name: string) {
this._name = name
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<string[]> {
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'
}
]
}
])

Expand All @@ -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 = {
Expand All @@ -100,34 +119,80 @@ 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 .'

const hasLintScript = packageJson.scripts['lint']
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<void> {
const templateDir = useTypeScript ? '../templates/ts' : '../templates/js'
async copyTemplateFiles(sourceDir: string): Promise<void> {
const templateDir = this._useTypeScript ? '../templates/ts' : '../templates/js'
const sourcePath = path.join(__dirname, templateDir, sourceDir)
const targetPath = path.join(this._workingDir, sourceDir)

Expand All @@ -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)
}
Expand Down
51 changes: 47 additions & 4 deletions packages/create-robo/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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
*/
Expand All @@ -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'
})
Expand All @@ -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
Expand All @@ -67,3 +101,12 @@ export function hasProperties<T extends Record<string, unknown>>(
): obj is T & Record<keyof T, unknown> {
return typeof obj === 'object' && obj !== null && props.every((prop) => prop in obj)
}

export function sortObjectKeys(obj: Record<string, string>) {
return Object.keys(obj)
.sort()
.reduce((acc, key) => {
acc[key] = obj[key]
return acc
}, {} as Record<string, string>)
}
14 changes: 0 additions & 14 deletions packages/create-robo/templates/js/.config/robo.mjs

This file was deleted.

14 changes: 0 additions & 14 deletions packages/create-robo/templates/ts/.config/robo.mjs

This file was deleted.

0 comments on commit a5c1e6f

Please sign in to comment.