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] feat(cli): blitz generate command #46

Closed
wants to merge 15 commits into from
Closed
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"lerna": "^3.20.2",
"lint-staged": "^10.0.8",
"npm-run-all": "^4.1.5",
"nyc": "^15.0.0",
"prettier": "^1.19.1",
"pretty-quick": "2.0.1",
"rimraf": "^3.0.2",
Expand Down
37 changes: 29 additions & 8 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
"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",
"test": "jest --coverage"
"build": "pkg . --out-path dist",
"prebuild": "rimraf lib && tsc -b && oclif-dev readme",
"test": "nyc --extension .ts jest"
},
"author": "Mina Abadir @mabadir",
"main": "lib/index.js",
Expand All @@ -28,16 +27,25 @@
"@oclif/config": "^1.14.0",
"@oclif/plugin-help": "^2.2.3",
"@oclif/plugin-not-found": "^1.2.3",
"yeoman-environment": "^2.8.0",
"yeoman-generator": "^4.5.0"
"chalk": "^3.0.0",
"diff": "^4.0.2",
"enquirer": "^2.3.4",
"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/yeoman-environment": "^2.3.2",
"@types/yeoman-generator": "^3.1.4",
"@types/diff": "^4.0.2",
"@types/fs-extra": "^8.1.0",
"@types/mem-fs": "^1.1.2",
"@types/mem-fs-editor": "^5.1.1",
"@types/vinyl": "^2.0.4",
"chai": "^4.2.0",
"globby": "^11.0.0",
"pkg": "^4.4.3",
"rimraf": "^3.0.2",
"ts-node": "^8.6.2"
},
"oclif": {
Expand All @@ -48,6 +56,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
95 changes: 95 additions & 0 deletions packages/cli/src/commands/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import * as path from 'path'
import {Command, flags} from '@oclif/command'
import ContextGenerator from '../generators/context'
const {MultiSelect} = require('enquirer')
const debug = require('debug')('blitz:new')

export interface Flags {
ts: boolean
yarn: boolean
}

interface InitialMethodOption {
name: string
}

export default class NewEntity extends Command {
static description = 'Generate a new Blitz entity'
static aliases = ['g']

static args = [
{
name: 'contextArg',
required: true,
description: 'context/entity name and path',
},
]

static flags = {
help: flags.help({char: 'h'}),
}

static initialControllerMethodOptions: InitialMethodOption[] = [
{name: 'ALL'},
{name: 'index'},
{name: 'show'},
{name: 'create'},
{name: 'update'},
{name: 'delete'},
]

async requestInitialControllerMethods(): Promise<string[]> {
try {
const prompt = new MultiSelect({
message: 'Select initial methods',
choices: NewEntity.initialControllerMethodOptions,
})

const result = await prompt.run()
return result
} catch (err) {
this.error(err)
}
}

transformArguments(contextArg: string): {contextPath: string; contextName: string; entityName: string} {
const splitPath = contextArg.split('/')
const [contextName] = [...splitPath].slice(-2)
const contextPath = splitPath.join('/')

return {
contextPath: contextPath,
contextName,
entityName: splitPath[splitPath.length - 1],
}
}

async run() {
const {
args,
args: {contextArg},
flags,
} = this.parse(NewEntity)
debug('args: ', args)
debug('flags: ', flags)

const {contextPath, contextName, entityName} = this.transformArguments(contextArg)

const destinationRoot = process.cwd() + `/${contextPath}`

const generator = new ContextGenerator({
sourceRoot: path.join(__dirname, '../../templates/entity'),
destinationRoot,
contextPath,
contextName,
entityName,
})

try {
await generator.run()
this.log(`${contextName} Context Generated!`)
} catch (err) {
this.error(err)
}
}
}
30 changes: 23 additions & 7 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 Down Expand Up @@ -33,12 +37,24 @@ export default class New extends Command {
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,
})

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')
}
}
77 changes: 77 additions & 0 deletions packages/cli/src/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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 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
makeDir: (path: fs.PathLike, options?: string | number | fs.MakeDirectoryOptions | null | undefined) => void

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

this.store = createStore()
this.fs = createEditor(this.store)
this.enquirer = new Enquirer()
this.makeDir = fs.mkdirSync
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)
}

// TODO: Install all the packages with npm or yarn
async install() {}

// TODO: Check for conflicts with stream transforms
// TODO: Handle dry run
async run() {
await fs.ensureDir(this.options.destinationRoot!)

process.chdir(this.options.destinationRoot!)

await this.write()

await new Promise((resolve, reject) => {
const conflictChecker = new ConflictChecker()
conflictChecker.on('error', err => {
reject(err)
})

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

if (this.options.install) await this.install()
}
}

export default Generator
Loading