Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[legacy-framework] refactor(cli): CLI refactoring #25

Merged
merged 9 commits into from
Apr 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,14 @@ Run `yarn` from the monorepo root
`yarn build`

<!-- toc -->

- [blitz-cli](#blitz-cli)
- [Usage](#usage)
- [Commands](#commands)
<!-- tocstop -->
* [blitz-cli](#blitz-cli)
* [Usage](#usage)
* [Commands](#commands)
<!-- tocstop -->

# Usage

<!-- usage -->

```sh-session
$ npm install -g @blitzjs/cli
$ blitz COMMAND
Expand All @@ -42,15 +40,13 @@ USAGE
$ blitz COMMAND
...
```

<!-- usagestop -->

# Commands

<!-- commands -->

- [`blitz help [COMMAND]`](#blitz-help-command)
- [`blitz new [PATH]`](#blitz-new-path)
* [`blitz help [COMMAND]`](#blitz-help-command)
* [`blitz new [PATH]`](#blitz-new-path)

## `blitz help [COMMAND]`

Expand Down Expand Up @@ -83,9 +79,9 @@ ARGUMENTS
OPTIONS
-h, --help show CLI help
-t, --[no-]ts generate a TypeScript project
--dry-run show what files will be created without writing them to disk
--yarn use Yarn as the package manager
```

_See code: [lib/commands/new.js](https://github.com/blitz-js/blitz/blob/v0.0.1/lib/commands/new.js)_

<!-- commandsstop -->
35 changes: 28 additions & 7 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
"scripts": {
"b": "./bin/run",
"version": "oclif-dev readme && git add README.md",
"build": "oclif-dev pack",
"prepack": "rm -rf lib && tsc -b && oclif-dev manifest && oclif-dev readme",
"postpack": "rm -f oclif.manifest.json",
"build": "pkg . --out-path dist",
"prebuild": "rimraf lib && tsc -b && oclif-dev readme",
"test": "jest --coverage",
"test:watch": "jest --watch"
},
Expand All @@ -30,21 +29,30 @@
"@oclif/config": "^1.14.0",
"@oclif/plugin-help": "^2.2.3",
"@oclif/plugin-not-found": "^1.2.3",
"chalk": "^3.0.0",
"chokidar": "^3.3.1",
"diff": "^4.0.2",
"enquirer": "^2.3.4",
"execa": "^4.0.0",
"fs-extra": "^8.1.0",
"has-yarn": "^2.1.0",
"yeoman-environment": "^2.8.0",
"yeoman-generator": "^4.5.0"
"mem-fs": "^1.1.3",
"mem-fs-editor": "^6.0.0",
"vinyl": "^2.2.0"
},
"devDependencies": {
"@oclif/dev-cli": "^1.22.2",
"@oclif/test": "^1.2.5",
"@types/diff": "^4.0.2",
"@types/fs-extra": "^8.1.0",
"@types/yeoman-environment": "^2.3.2",
"@types/yeoman-generator": "^3.1.4",
"@types/mem-fs": "^1.1.2",
"@types/mem-fs-editor": "^5.1.1",
"@types/vinyl": "^2.0.4",
"chai": "^4.2.0",
"chokidar": "^3.3.1",
"globby": "^11.0.0",
"pkg": "^4.4.3",
"rimraf": "^3.0.2",
"ts-node": "^8.6.2"
},
"oclif": {
Expand All @@ -55,6 +63,19 @@
"@oclif/plugin-not-found"
]
},
"pkg": {
"scripts": [
"./lib/**/*.js"
],
"assets": [
"./templates/**/*"
],
"targets": [
"node12-linux-x64",
"node12-macos-x64",
"node12-win-x64"
]
},
"engines": {
"yarn": "^1.19.1",
"node": ">=12.16.1"
Expand Down
8 changes: 8 additions & 0 deletions packages/cli/src/command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {Command as OclifCommand} from '@oclif/command'
import Enquirer = require('enquirer')

abstract class Command extends OclifCommand {
protected enquirer = new Enquirer()
}

export default Command
35 changes: 27 additions & 8 deletions packages/cli/src/commands/new.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import {Command, flags} from '@oclif/command'
import yeoman = require('yeoman-environment')
import * as path from 'path'
import {flags} from '@oclif/command'
import Command from '../command'
import AppGenerator from '../generators/app'
const debug = require('debug')('blitz:new')

import PromptAbortedError from '../errors/prompt-aborted'

export interface Flags {
ts: boolean
yarn: boolean
Expand All @@ -26,19 +30,34 @@ export default class New extends Command {
default: true,
allowNo: true,
}),
yarn: flags.boolean({description: 'use Yarn as the package manager', default: true}),
yarn: flags.boolean({description: 'use Yarn as the package manager', default: true, allowNo: true}),
'dry-run': flags.boolean({description: 'show what files will be created without writing them to disk'}),
}

async run() {
const {args, flags} = this.parse(New)
debug('args: ', args)
debug('flags: ', flags)
const env = yeoman.createEnv()

env.register(require.resolve('../generators/app'), 'generate:app')
env.run(['generate:app', args.path], flags as Flags, (err: Error | null) => {
if (err) this.error(err) // Maybe tell a bit more...
this.log('App created!') // This needs some sparkles ✨
const destinationRoot = args?.path ? path.resolve(args?.path) : process.cwd()
const appName = path.basename(destinationRoot)

const generator = new AppGenerator({
sourceRoot: path.join(__dirname, '../../templates/app'),
destinationRoot,
appName,
dryRun: flags['dry-run'],
install: true,
yarn: flags.yarn,
})

try {
await generator.run()
this.log('App Created!')
} catch (err) {
if (err instanceof PromptAbortedError) this.exit(0)

this.error(err)
}
}
}
5 changes: 5 additions & 0 deletions packages/cli/src/errors/prompt-aborted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default class PromptAbortedError extends Error {
constructor() {
super('Prompt aborted')
}
}
87 changes: 87 additions & 0 deletions packages/cli/src/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as fs from 'fs-extra'
import * as path from 'path'
import {EventEmitter} from 'events'
import {create as createStore, Store} from 'mem-fs'
import {create as createEditor, Editor} from 'mem-fs-editor'
import Enquirer = require('enquirer')
import execa = require('execa')

import ConflictChecker from './transforms/conflict-checker'

export interface GeneratorOptions {
sourceRoot: string
destinationRoot?: string
yarn?: boolean
install?: boolean
dryRun?: boolean
}

/**
* The base generator class.
* Every generator must extend this class.
*/
abstract class Generator<T extends GeneratorOptions = GeneratorOptions> extends EventEmitter {
private readonly store: Store

protected readonly fs: Editor
protected readonly enquirer: Enquirer

private performedActions: string[] = []

constructor(protected readonly options: T) {
super()

this.store = createStore()
this.fs = createEditor(this.store)
this.enquirer = new Enquirer()
if (!this.options.destinationRoot) this.options.destinationRoot = process.cwd()
}

abstract async write(): Promise<void>

sourcePath(...paths: string[]): string {
return path.join(this.options.sourceRoot, ...paths)
}

destinationPath(...paths: string[]): string {
return path.join(this.options.destinationRoot!, ...paths)
}

async install() {
await execa(this.options.yarn ? 'yarn' : 'npm', ['install'])
}

async run() {
if (!this.options.dryRun) {
await fs.ensureDir(this.options.destinationRoot!)
process.chdir(this.options.destinationRoot!)
}

await this.write()

await new Promise((resolve, reject) => {
const conflictChecker = new ConflictChecker({
dryRun: this.options.dryRun,
})
conflictChecker.on('error', err => {
reject(err)
})
conflictChecker.on('fileStatus', (data: string) => {
this.performedActions.push(data)
})

this.fs.commit([conflictChecker], err => {
if (err) reject(err)
resolve()
})
})

this.performedActions.forEach(action => {
console.log(action)
})

if (this.options.install && !this.options.dryRun) await this.install()
}
}

export default Generator
108 changes: 19 additions & 89 deletions packages/cli/src/generators/app.ts
Original file line number Diff line number Diff line change
@@ -1,94 +1,11 @@
import {execSync} from 'child_process'
import * as path from 'path'
import Generator = require('yeoman-generator')
const debug = require('debug')('blitz:generate-app')
import Generator, {GeneratorOptions} from '../generator'

import {Flags} from '../commands/new'

let hasYarn = false
try {
execSync('yarn -v', {stdio: 'ignore'})
hasYarn = true
} catch {}

class AppGenerator extends Generator {
constructor(args: string | string[], opts: Flags) {
super(args, opts)

this.options.path = args[0] ? path.resolve(args[0]) : this.destinationRoot()
this.options.appName = path.basename(this.options.path)
}

async prompting() {
const answers = await this.prompt([
{
type: 'input',
name: 'appName',
message: 'How do you want your project to be called?',
default: this.options.appName,
},
{
type: 'confirm',
name: 'ts',
message: 'Do you want to use TypeScript?',
default: this.options.ts,
when: !this.options.ts,
},
{
type: 'confirm',
name: 'yarn',
message: 'Do you want to use Yarn instead of NPM?',
default: this.options.yarn || hasYarn,
when: !this.options.yarn,
},
])

debug('Answers: ', answers)

this.options = {
...this.options,
ts: answers.ts ?? this.options.ts,
yarn: answers.yarn ?? this.options.yarn,
}

debug('Options: ', answers)

const fullPath = path.join(
this.options.path,
answers.appName !== this.options.appName ? answers.appName : '',
)
this.destinationRoot(path.resolve(fullPath))
process.chdir(this.destinationRoot())

if (answers.appName) this.options.appName = answers.appName
}

writing() {
this.sourceRoot(path.join(__dirname, '../../templates/app'))

this.fs.copyTpl(this.templatePath('README.md.ejs'), this.destinationPath('README.md'), {
name: this.options.appName,
})
this.fs.copyTpl(this.templatePath('pages/index.js.ejs'), this.destinationPath('pages/index.js'), {
name: this.options.appName,
})

this.fs.writeJSON(this.destinationPath('package.json'), this._packageJson())

this.fs.copy(this.templatePath('gitignore'), this.destinationPath('.gitignore'))
}

install() {
const dependencies = ['next', 'react', 'react-dom']
const install = (depts: string[], opts: object) =>
this.options.yarn ? this.yarnInstall(depts, opts) : this.npmInstall(depts, opts)
// const dev = this.options.yarn ? {dev: true} : {'save-dev': true}
const save = this.options.yarn ? {} : {save: true}

install(dependencies, save)
}
export interface AppGeneratorOptions extends GeneratorOptions {
appName: string
}

_packageJson() {
class AppGenerator extends Generator<AppGeneratorOptions> {
packageJson() {
return {
name: this.options.appName,
version: '0.0.1',
Expand All @@ -100,6 +17,19 @@ class AppGenerator extends Generator {
},
}
}

async write() {
this.fs.copyTpl(this.sourcePath('README.md.ejs'), this.destinationPath('README.md'), {
name: 'Hello',
})
this.fs.copyTpl(this.sourcePath('pages/index.js.ejs'), this.destinationPath('pages/index.js'), {
name: 'Hello',
})

this.fs.writeJSON(this.destinationPath('package.json'), this.packageJson())

this.fs.copy(this.sourcePath('gitignore'), this.destinationPath('.gitignore'))
}
}

export default AppGenerator
Loading