Skip to content

Commit c7e1b7e

Browse files
authored
feat(cli): Improve CLI interface (#2753)
1 parent 190d609 commit c7e1b7e

File tree

14 files changed

+270
-90
lines changed

14 files changed

+270
-90
lines changed

packages/cli/bin/feathers

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
#!/usr/bin/env node
2-
'use strict';
2+
'use strict'
33

4-
const { yargs } = require('@feathershq/pinion')
5-
const { command } = require('../lib')
4+
const { program } = require('../lib')
65

7-
const cli = cmd => command(yargs(cmd)).argv
8-
9-
cli(process.argv.slice(2));
6+
program.parse()

packages/cli/package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,13 @@
4747
"mocha": "mocha --timeout 60000 --config ../../.mocharc.json --recursive test/**.test.ts test/**/*.test.ts",
4848
"test": "npm run compile && npm run mocha"
4949
},
50-
"directories": {
51-
"lib": "lib/cli"
52-
},
5350
"publishConfig": {
5451
"access": "public"
5552
},
5653
"dependencies": {
5754
"@feathershq/pinion": "^0.3.5",
5855
"chalk": "^4.0.1",
56+
"commander": "^9.4.0",
5957
"lodash": "^4.17.21",
6058
"prettier": "^2.7.1"
6159
},

packages/cli/src/app/index.ts

Lines changed: 48 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export type AppGeneratorArguments = FeathersBaseContext & Partial<AppGeneratorDa
4747

4848
export const generate = (ctx: AppGeneratorArguments) =>
4949
generator(ctx)
50+
.then(initializeBaseContext())
5051
.then((ctx) => ({
5152
...ctx,
5253
dependencies: [],
@@ -104,7 +105,8 @@ export const generate = (ctx: AppGeneratorArguments) =>
104105
message: 'Which package manager are you using?',
105106
choices: [
106107
{ value: 'npm', name: 'npm' },
107-
{ value: 'yarn', name: 'Yarn' }
108+
{ value: 'yarn', name: 'Yarn' },
109+
{ value: 'pnpm', name: 'pnpm' }
108110
]
109111
},
110112
...connectionPrompts(ctx),
@@ -149,50 +151,58 @@ export const generate = (ctx: AppGeneratorArguments) =>
149151
)
150152
)
151153
.then(
152-
install<AppGeneratorContext>(({ transports, framework, dependencyVersions, dependencies }) => {
153-
const hasSocketio = transports.includes('websockets')
154+
install<AppGeneratorContext>(
155+
({ transports, framework, dependencyVersions, dependencies }) => {
156+
const hasSocketio = transports.includes('websockets')
154157

155-
dependencies.push(
156-
'@feathersjs/feathers',
157-
'@feathersjs/errors',
158-
'@feathersjs/schema',
159-
'@feathersjs/configuration',
160-
'@feathersjs/transport-commons',
161-
'@feathersjs/authentication',
162-
'winston'
163-
)
158+
dependencies.push(
159+
'@feathersjs/feathers',
160+
'@feathersjs/errors',
161+
'@feathersjs/schema',
162+
'@feathersjs/configuration',
163+
'@feathersjs/transport-commons',
164+
'@feathersjs/authentication',
165+
'winston'
166+
)
164167

165-
if (hasSocketio) {
166-
dependencies.push('@feathersjs/socketio')
167-
}
168+
if (hasSocketio) {
169+
dependencies.push('@feathersjs/socketio')
170+
}
168171

169-
if (framework === 'koa') {
170-
dependencies.push('@feathersjs/koa', 'koa-static')
171-
}
172+
if (framework === 'koa') {
173+
dependencies.push('@feathersjs/koa', 'koa-static')
174+
}
172175

173-
if (framework === 'express') {
174-
dependencies.push('@feathersjs/express', 'compression')
175-
}
176+
if (framework === 'express') {
177+
dependencies.push('@feathersjs/express', 'compression')
178+
}
176179

177-
return addVersions(dependencies, dependencyVersions)
178-
})
180+
return addVersions(dependencies, dependencyVersions)
181+
},
182+
false,
183+
ctx.packager
184+
)
179185
)
180186
.then(
181-
install<AppGeneratorContext>(({ language, framework, devDependencies, dependencyVersions }) => {
182-
devDependencies.push('nodemon', 'axios', 'mocha', 'cross-env', 'prettier')
187+
install<AppGeneratorContext>(
188+
({ language, framework, devDependencies, dependencyVersions }) => {
189+
devDependencies.push('nodemon', 'axios', 'mocha', 'cross-env', 'prettier', '@feathersjs/cli')
183190

184-
if (language === 'ts') {
185-
devDependencies.push(
186-
'@types/mocha',
187-
framework === 'koa' ? '@types/koa-static' : '@types/compression',
188-
'@types/node',
189-
'nodemon',
190-
'ts-node',
191-
'typescript',
192-
'shx'
193-
)
194-
}
191+
if (language === 'ts') {
192+
devDependencies.push(
193+
'@types/mocha',
194+
framework === 'koa' ? '@types/koa-static' : '@types/compression',
195+
'@types/node',
196+
'nodemon',
197+
'ts-node',
198+
'typescript',
199+
'shx'
200+
)
201+
}
195202

196-
return addVersions(devDependencies, dependencyVersions)
197-
}, true)
203+
return addVersions(devDependencies, dependencyVersions)
204+
},
205+
true,
206+
ctx.packager
207+
)
198208
)

packages/cli/src/authentication/index.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import chalk from 'chalk'
22
import { generator, runGenerators, prompt, install } from '@feathershq/pinion'
3-
import { addVersions, FeathersBaseContext, getDatabaseAdapter } from '../commons'
3+
import {
4+
addVersions,
5+
checkPreconditions,
6+
FeathersBaseContext,
7+
getDatabaseAdapter,
8+
initializeBaseContext
9+
} from '../commons'
410
import { generate as serviceGenerator, ServiceGeneratorContext } from '../service/index'
511

612
export interface AuthenticationGeneratorContext extends ServiceGeneratorContext {
@@ -67,14 +73,16 @@ export const prompts = (ctx: AuthenticationGeneratorArguments) => [
6773

6874
export const generate = (ctx: AuthenticationGeneratorArguments) =>
6975
generator(ctx)
76+
.then(initializeBaseContext())
77+
.then(checkPreconditions())
7078
.then(prompt<AuthenticationGeneratorArguments, AuthenticationGeneratorContext>(prompts))
7179
.then(async (ctx) => {
7280
const serviceContext = await serviceGenerator({
7381
...ctx,
7482
name: ctx.service,
7583
path: ctx.service,
7684
isEntityService: true,
77-
type: getDatabaseAdapter(ctx.feathers.database)
85+
type: getDatabaseAdapter(ctx.feathers?.database)
7886
})
7987

8088
return {
@@ -99,5 +107,9 @@ export const generate = (ctx: AuthenticationGeneratorArguments) =>
99107
}
100108
}
101109

102-
return install<AuthenticationGeneratorContext>(addVersions(dependencies, ctx.dependencyVersions))(ctx)
110+
return install<AuthenticationGeneratorContext>(
111+
addVersions(dependencies, ctx.dependencyVersions),
112+
false,
113+
ctx.feathers.packager
114+
)(ctx)
103115
})

packages/cli/src/authentication/templates/knex.tpl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export async function down(knex: Knex): Promise<void> {
4242
export const generate = (ctx: AuthenticationGeneratorContext) =>
4343
generator(ctx).then(
4444
when(
45-
(ctx) => getDatabaseAdapter(ctx.feathers.database) === 'knex',
45+
(ctx) => getDatabaseAdapter(ctx.feathers?.database) === 'knex',
4646
renderSource(
4747
migrationTemplate,
4848
toFile(

packages/cli/src/cli.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import chalk from 'chalk'
2+
import { Command } from 'commander'
3+
import { generator, runGenerator, getContext } from '@feathershq/pinion'
4+
import { FeathersBaseContext, version } from './commons'
5+
6+
export * from 'commander'
7+
8+
export const commandRunner = (name: string) => async (options: any) => {
9+
const ctx = getContext<FeathersBaseContext>({
10+
...options
11+
})
12+
13+
await generator(ctx)
14+
.then(runGenerator(__dirname, name, 'index'))
15+
.catch((error) => {
16+
const { logger } = ctx.pinion
17+
18+
logger.error(`Error: ${chalk.white(error.message)}`)
19+
})
20+
}
21+
22+
export const program = new Command()
23+
24+
program
25+
.name('feathers')
26+
.description('The Feathers command line interface 🕊️')
27+
.version(version)
28+
.showHelpAfterError()
29+
30+
const generate = program.command('generate').alias('g')
31+
32+
generate
33+
.command('app')
34+
.description('Generate a new application')
35+
.option('--name <name>', 'The name of the application')
36+
.action(commandRunner('app'))
37+
38+
generate
39+
.command('service')
40+
.description('Generate a new service')
41+
.option('--name <name>', 'The service name')
42+
.option('--path <path>', 'The path to register the service on')
43+
.option('--type <type>', 'The service type (knex, mongodb, custom)')
44+
.action(commandRunner('service'))
45+
46+
generate
47+
.command('hook')
48+
.description('Generate a hook')
49+
.option('--name <name>', 'The name of the hook')
50+
.option('--type <type>', 'The hook type (around or regular)')
51+
.action(commandRunner('hook'))
52+
53+
generate
54+
.command('connection')
55+
.description('Add a new database connection')
56+
.action(commandRunner('connection'))
57+
58+
generate
59+
.command('authentication')
60+
.description('Add authentication to the application')
61+
.action(commandRunner('authentication'))
62+
63+
generate.description(
64+
`Run a generator. Currently available: \n ${generate.commands
65+
.map((cmd) => `${chalk.blue(cmd.name())}: ${cmd.description()} `)
66+
.join('\n ')}`
67+
)

packages/cli/src/commons.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import fs from 'fs'
2+
import { join } from 'path'
13
import { PackageJson } from 'type-fest'
24
import { readFile, writeFile } from 'fs/promises'
35
import {
@@ -14,6 +16,8 @@ import * as ts from 'typescript'
1416
import prettier, { Options as PrettierOptions } from 'prettier'
1517
import path from 'path'
1618

19+
export const { version } = JSON.parse(fs.readFileSync(join(__dirname, '..', 'package.json')).toString())
20+
1721
export type DependencyVersions = { [key: string]: string }
1822

1923
/**
@@ -110,7 +114,8 @@ export const initializeBaseContext =
110114
loadJSON(path.join(__dirname, '..', 'package.json'), (pkg: PackageJson) => ({
111115
dependencyVersions: {
112116
...pkg.devDependencies,
113-
...ctx.dependencyVersions
117+
...ctx.dependencyVersions,
118+
'@feathersjs/cli': version
114119
}
115120
}))
116121
)
@@ -122,6 +127,26 @@ export const initializeBaseContext =
122127
feathers: ctx.pkg?.feathers
123128
}))
124129

130+
/**
131+
* Checks if the current context contains a valid generated application. This is necesary for most
132+
* generators (besides the app generator).
133+
*
134+
* @param ctx The context to check against
135+
* @returns Throws an error or returns the original context
136+
*/
137+
export const checkPreconditions =
138+
() =>
139+
async <T extends FeathersBaseContext>(ctx: T) => {
140+
if (!ctx.feathers) {
141+
console.log(ctx)
142+
throw new Error(`Can not run generator since the current folder does not appear to be a Feathers application.
143+
Either your package.json is missing or it does not have \`feathers\` property.
144+
`)
145+
}
146+
147+
return ctx
148+
}
149+
125150
const importRegex = /from '(\..*)'/g
126151
const escapeNewLines = (code: string) => code.replace(/\n\n/g, '\n/* :newline: */')
127152
const restoreNewLines = (code: string) => code.replace(/\/\* :newline: \*\//g, '\n')

packages/cli/src/connection/index.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { generator, runGenerator, prompt, install, mergeJSON, toFile } from '@feathershq/pinion'
22
import chalk from 'chalk'
3-
import { FeathersBaseContext, DatabaseType, getDatabaseAdapter, addVersions } from '../commons'
3+
import {
4+
FeathersBaseContext,
5+
DatabaseType,
6+
getDatabaseAdapter,
7+
addVersions,
8+
checkPreconditions,
9+
initializeBaseContext
10+
} from '../commons'
411

512
export interface ConnectionGeneratorContext extends FeathersBaseContext {
613
database: DatabaseType
@@ -60,6 +67,8 @@ export const getDatabaseClient = (database: DatabaseType) => DATABASE_CLIENTS[da
6067

6168
export const generate = (ctx: ConnectionGeneratorArguments) =>
6269
generator(ctx)
70+
.then(initializeBaseContext())
71+
.then(checkPreconditions())
6372
.then(prompt<ConnectionGeneratorArguments, ConnectionGeneratorContext>(prompts))
6473
.then(
6574
runGenerator<ConnectionGeneratorContext>(
@@ -105,5 +114,9 @@ export const generate = (ctx: ConnectionGeneratorArguments) =>
105114
}
106115
}
107116

108-
return install<ConnectionGeneratorContext>(addVersions(dependencies, ctx.dependencyVersions))(ctx)
117+
return install<ConnectionGeneratorContext>(
118+
addVersions(dependencies, ctx.dependencyVersions),
119+
false,
120+
ctx.feathers.packager
121+
)(ctx)
109122
})

packages/cli/src/hook/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { generator, prompt, runGenerators } from '@feathershq/pinion'
22
import _ from 'lodash'
3-
import { FeathersBaseContext } from '../commons'
3+
import { checkPreconditions, FeathersBaseContext, initializeBaseContext } from '../commons'
44

55
export interface HookGeneratorContext extends FeathersBaseContext {
66
name: string
@@ -11,6 +11,8 @@ export interface HookGeneratorContext extends FeathersBaseContext {
1111

1212
export const generate = (ctx: HookGeneratorContext) =>
1313
generator(ctx)
14+
.then(initializeBaseContext())
15+
.then(checkPreconditions())
1416
.then(
1517
prompt<HookGeneratorContext>(({ type, name }) => [
1618
{

packages/cli/src/index.ts

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,2 @@
1-
import { Argv, generator, runGenerator, getContext } from '@feathershq/pinion'
2-
import { FeathersBaseContext, initializeBaseContext } from './commons'
3-
4-
export const commandRunner = (yarg: any) => {
5-
const ctx = getContext<FeathersBaseContext>({
6-
...yarg.argv
7-
})
8-
9-
return generate(ctx)
10-
}
11-
12-
export const generate = (ctx: FeathersBaseContext) =>
13-
generator(ctx)
14-
.then(initializeBaseContext())
15-
.then(runGenerator(__dirname, (ctx: FeathersBaseContext) => `${ctx._[1]}`, 'index'))
16-
17-
export const command = (yargs: Argv) =>
18-
yargs
19-
.command('generate', 'Run a generator', (yarg) =>
20-
yarg
21-
.command('app', 'Generate a new app', commandRunner)
22-
.command('service', 'Generate a service', commandRunner)
23-
.command('hook', 'Generate a hook', commandRunner)
24-
.command('connection', 'Connect to a different database', commandRunner)
25-
.command('authentication', 'Set up authentication with a custom entity', commandRunner)
26-
)
27-
.usage('Usage: $0 <command> [options]')
28-
.help()
1+
export * from './cli'
2+
export * from './commons'

0 commit comments

Comments
 (0)