diff --git a/packages/cli/bin/feathers b/packages/cli/bin/feathers index c27794d2fc..7266b01c6f 100755 --- a/packages/cli/bin/feathers +++ b/packages/cli/bin/feathers @@ -1,9 +1,6 @@ #!/usr/bin/env node -'use strict'; +'use strict' -const { yargs } = require('@feathershq/pinion') -const { command } = require('../lib') +const { program } = require('../lib') -const cli = cmd => command(yargs(cmd)).argv - -cli(process.argv.slice(2)); +program.parse() diff --git a/packages/cli/package.json b/packages/cli/package.json index fb526ce4f2..f4e382c3cc 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -47,15 +47,13 @@ "mocha": "mocha --timeout 60000 --config ../../.mocharc.json --recursive test/**.test.ts test/**/*.test.ts", "test": "npm run compile && npm run mocha" }, - "directories": { - "lib": "lib/cli" - }, "publishConfig": { "access": "public" }, "dependencies": { "@feathershq/pinion": "^0.3.5", "chalk": "^4.0.1", + "commander": "^9.4.0", "lodash": "^4.17.21", "prettier": "^2.7.1" }, diff --git a/packages/cli/src/app/index.ts b/packages/cli/src/app/index.ts index e5216f0045..d3d017fec4 100644 --- a/packages/cli/src/app/index.ts +++ b/packages/cli/src/app/index.ts @@ -47,6 +47,7 @@ export type AppGeneratorArguments = FeathersBaseContext & Partial generator(ctx) + .then(initializeBaseContext()) .then((ctx) => ({ ...ctx, dependencies: [], @@ -104,7 +105,8 @@ export const generate = (ctx: AppGeneratorArguments) => message: 'Which package manager are you using?', choices: [ { value: 'npm', name: 'npm' }, - { value: 'yarn', name: 'Yarn' } + { value: 'yarn', name: 'Yarn' }, + { value: 'pnpm', name: 'pnpm' } ] }, ...connectionPrompts(ctx), @@ -149,50 +151,58 @@ export const generate = (ctx: AppGeneratorArguments) => ) ) .then( - install(({ transports, framework, dependencyVersions, dependencies }) => { - const hasSocketio = transports.includes('websockets') + install( + ({ transports, framework, dependencyVersions, dependencies }) => { + const hasSocketio = transports.includes('websockets') - dependencies.push( - '@feathersjs/feathers', - '@feathersjs/errors', - '@feathersjs/schema', - '@feathersjs/configuration', - '@feathersjs/transport-commons', - '@feathersjs/authentication', - 'winston' - ) + dependencies.push( + '@feathersjs/feathers', + '@feathersjs/errors', + '@feathersjs/schema', + '@feathersjs/configuration', + '@feathersjs/transport-commons', + '@feathersjs/authentication', + 'winston' + ) - if (hasSocketio) { - dependencies.push('@feathersjs/socketio') - } + if (hasSocketio) { + dependencies.push('@feathersjs/socketio') + } - if (framework === 'koa') { - dependencies.push('@feathersjs/koa', 'koa-static') - } + if (framework === 'koa') { + dependencies.push('@feathersjs/koa', 'koa-static') + } - if (framework === 'express') { - dependencies.push('@feathersjs/express', 'compression') - } + if (framework === 'express') { + dependencies.push('@feathersjs/express', 'compression') + } - return addVersions(dependencies, dependencyVersions) - }) + return addVersions(dependencies, dependencyVersions) + }, + false, + ctx.packager + ) ) .then( - install(({ language, framework, devDependencies, dependencyVersions }) => { - devDependencies.push('nodemon', 'axios', 'mocha', 'cross-env', 'prettier') + install( + ({ language, framework, devDependencies, dependencyVersions }) => { + devDependencies.push('nodemon', 'axios', 'mocha', 'cross-env', 'prettier', '@feathersjs/cli') - if (language === 'ts') { - devDependencies.push( - '@types/mocha', - framework === 'koa' ? '@types/koa-static' : '@types/compression', - '@types/node', - 'nodemon', - 'ts-node', - 'typescript', - 'shx' - ) - } + if (language === 'ts') { + devDependencies.push( + '@types/mocha', + framework === 'koa' ? '@types/koa-static' : '@types/compression', + '@types/node', + 'nodemon', + 'ts-node', + 'typescript', + 'shx' + ) + } - return addVersions(devDependencies, dependencyVersions) - }, true) + return addVersions(devDependencies, dependencyVersions) + }, + true, + ctx.packager + ) ) diff --git a/packages/cli/src/authentication/index.ts b/packages/cli/src/authentication/index.ts index 8577312216..4f4fce804f 100644 --- a/packages/cli/src/authentication/index.ts +++ b/packages/cli/src/authentication/index.ts @@ -1,6 +1,12 @@ import chalk from 'chalk' import { generator, runGenerators, prompt, install } from '@feathershq/pinion' -import { addVersions, FeathersBaseContext, getDatabaseAdapter } from '../commons' +import { + addVersions, + checkPreconditions, + FeathersBaseContext, + getDatabaseAdapter, + initializeBaseContext +} from '../commons' import { generate as serviceGenerator, ServiceGeneratorContext } from '../service/index' export interface AuthenticationGeneratorContext extends ServiceGeneratorContext { @@ -67,6 +73,8 @@ export const prompts = (ctx: AuthenticationGeneratorArguments) => [ export const generate = (ctx: AuthenticationGeneratorArguments) => generator(ctx) + .then(initializeBaseContext()) + .then(checkPreconditions()) .then(prompt(prompts)) .then(async (ctx) => { const serviceContext = await serviceGenerator({ @@ -74,7 +82,7 @@ export const generate = (ctx: AuthenticationGeneratorArguments) => name: ctx.service, path: ctx.service, isEntityService: true, - type: getDatabaseAdapter(ctx.feathers.database) + type: getDatabaseAdapter(ctx.feathers?.database) }) return { @@ -99,5 +107,9 @@ export const generate = (ctx: AuthenticationGeneratorArguments) => } } - return install(addVersions(dependencies, ctx.dependencyVersions))(ctx) + return install( + addVersions(dependencies, ctx.dependencyVersions), + false, + ctx.feathers.packager + )(ctx) }) diff --git a/packages/cli/src/authentication/templates/knex.tpl.ts b/packages/cli/src/authentication/templates/knex.tpl.ts index dcacd96937..311ea8a2e0 100644 --- a/packages/cli/src/authentication/templates/knex.tpl.ts +++ b/packages/cli/src/authentication/templates/knex.tpl.ts @@ -42,7 +42,7 @@ export async function down(knex: Knex): Promise { export const generate = (ctx: AuthenticationGeneratorContext) => generator(ctx).then( when( - (ctx) => getDatabaseAdapter(ctx.feathers.database) === 'knex', + (ctx) => getDatabaseAdapter(ctx.feathers?.database) === 'knex', renderSource( migrationTemplate, toFile( diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts new file mode 100644 index 0000000000..6495686698 --- /dev/null +++ b/packages/cli/src/cli.ts @@ -0,0 +1,67 @@ +import chalk from 'chalk' +import { Command } from 'commander' +import { generator, runGenerator, getContext } from '@feathershq/pinion' +import { FeathersBaseContext, version } from './commons' + +export * from 'commander' + +export const commandRunner = (name: string) => async (options: any) => { + const ctx = getContext({ + ...options + }) + + await generator(ctx) + .then(runGenerator(__dirname, name, 'index')) + .catch((error) => { + const { logger } = ctx.pinion + + logger.error(`Error: ${chalk.white(error.message)}`) + }) +} + +export const program = new Command() + +program + .name('feathers') + .description('The Feathers command line interface 🕊️') + .version(version) + .showHelpAfterError() + +const generate = program.command('generate').alias('g') + +generate + .command('app') + .description('Generate a new application') + .option('--name ', 'The name of the application') + .action(commandRunner('app')) + +generate + .command('service') + .description('Generate a new service') + .option('--name ', 'The service name') + .option('--path ', 'The path to register the service on') + .option('--type ', 'The service type (knex, mongodb, custom)') + .action(commandRunner('service')) + +generate + .command('hook') + .description('Generate a hook') + .option('--name ', 'The name of the hook') + .option('--type ', 'The hook type (around or regular)') + .action(commandRunner('hook')) + +generate + .command('connection') + .description('Add a new database connection') + .action(commandRunner('connection')) + +generate + .command('authentication') + .description('Add authentication to the application') + .action(commandRunner('authentication')) + +generate.description( + `Run a generator. Currently available: \n ${generate.commands + .map((cmd) => `${chalk.blue(cmd.name())}: ${cmd.description()} `) + .join('\n ')}` +) diff --git a/packages/cli/src/commons.ts b/packages/cli/src/commons.ts index 21ec41ebe6..72f3e88412 100644 --- a/packages/cli/src/commons.ts +++ b/packages/cli/src/commons.ts @@ -1,3 +1,5 @@ +import fs from 'fs' +import { join } from 'path' import { PackageJson } from 'type-fest' import { readFile, writeFile } from 'fs/promises' import { @@ -14,6 +16,8 @@ import * as ts from 'typescript' import prettier, { Options as PrettierOptions } from 'prettier' import path from 'path' +export const { version } = JSON.parse(fs.readFileSync(join(__dirname, '..', 'package.json')).toString()) + export type DependencyVersions = { [key: string]: string } /** @@ -110,7 +114,8 @@ export const initializeBaseContext = loadJSON(path.join(__dirname, '..', 'package.json'), (pkg: PackageJson) => ({ dependencyVersions: { ...pkg.devDependencies, - ...ctx.dependencyVersions + ...ctx.dependencyVersions, + '@feathersjs/cli': version } })) ) @@ -122,6 +127,26 @@ export const initializeBaseContext = feathers: ctx.pkg?.feathers })) +/** + * Checks if the current context contains a valid generated application. This is necesary for most + * generators (besides the app generator). + * + * @param ctx The context to check against + * @returns Throws an error or returns the original context + */ +export const checkPreconditions = + () => + async (ctx: T) => { + if (!ctx.feathers) { + console.log(ctx) + throw new Error(`Can not run generator since the current folder does not appear to be a Feathers application. +Either your package.json is missing or it does not have \`feathers\` property. +`) + } + + return ctx + } + const importRegex = /from '(\..*)'/g const escapeNewLines = (code: string) => code.replace(/\n\n/g, '\n/* :newline: */') const restoreNewLines = (code: string) => code.replace(/\/\* :newline: \*\//g, '\n') diff --git a/packages/cli/src/connection/index.ts b/packages/cli/src/connection/index.ts index f94cbb4fcb..3d57c4a6cf 100644 --- a/packages/cli/src/connection/index.ts +++ b/packages/cli/src/connection/index.ts @@ -1,6 +1,13 @@ import { generator, runGenerator, prompt, install, mergeJSON, toFile } from '@feathershq/pinion' import chalk from 'chalk' -import { FeathersBaseContext, DatabaseType, getDatabaseAdapter, addVersions } from '../commons' +import { + FeathersBaseContext, + DatabaseType, + getDatabaseAdapter, + addVersions, + checkPreconditions, + initializeBaseContext +} from '../commons' export interface ConnectionGeneratorContext extends FeathersBaseContext { database: DatabaseType @@ -60,6 +67,8 @@ export const getDatabaseClient = (database: DatabaseType) => DATABASE_CLIENTS[da export const generate = (ctx: ConnectionGeneratorArguments) => generator(ctx) + .then(initializeBaseContext()) + .then(checkPreconditions()) .then(prompt(prompts)) .then( runGenerator( @@ -105,5 +114,9 @@ export const generate = (ctx: ConnectionGeneratorArguments) => } } - return install(addVersions(dependencies, ctx.dependencyVersions))(ctx) + return install( + addVersions(dependencies, ctx.dependencyVersions), + false, + ctx.feathers.packager + )(ctx) }) diff --git a/packages/cli/src/hook/index.ts b/packages/cli/src/hook/index.ts index 5427b9c040..3f0b430d98 100644 --- a/packages/cli/src/hook/index.ts +++ b/packages/cli/src/hook/index.ts @@ -1,6 +1,6 @@ import { generator, prompt, runGenerators } from '@feathershq/pinion' import _ from 'lodash' -import { FeathersBaseContext } from '../commons' +import { checkPreconditions, FeathersBaseContext, initializeBaseContext } from '../commons' export interface HookGeneratorContext extends FeathersBaseContext { name: string @@ -11,6 +11,8 @@ export interface HookGeneratorContext extends FeathersBaseContext { export const generate = (ctx: HookGeneratorContext) => generator(ctx) + .then(initializeBaseContext()) + .then(checkPreconditions()) .then( prompt(({ type, name }) => [ { diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 7d2900e68a..f717fbb249 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,28 +1,2 @@ -import { Argv, generator, runGenerator, getContext } from '@feathershq/pinion' -import { FeathersBaseContext, initializeBaseContext } from './commons' - -export const commandRunner = (yarg: any) => { - const ctx = getContext({ - ...yarg.argv - }) - - return generate(ctx) -} - -export const generate = (ctx: FeathersBaseContext) => - generator(ctx) - .then(initializeBaseContext()) - .then(runGenerator(__dirname, (ctx: FeathersBaseContext) => `${ctx._[1]}`, 'index')) - -export const command = (yargs: Argv) => - yargs - .command('generate', 'Run a generator', (yarg) => - yarg - .command('app', 'Generate a new app', commandRunner) - .command('service', 'Generate a service', commandRunner) - .command('hook', 'Generate a hook', commandRunner) - .command('connection', 'Connect to a different database', commandRunner) - .command('authentication', 'Set up authentication with a custom entity', commandRunner) - ) - .usage('Usage: $0 [options]') - .help() +export * from './cli' +export * from './commons' diff --git a/packages/cli/src/service/index.ts b/packages/cli/src/service/index.ts index 578bee305d..16108085af 100644 --- a/packages/cli/src/service/index.ts +++ b/packages/cli/src/service/index.ts @@ -1,7 +1,12 @@ import _ from 'lodash' import { generator, runGenerator, runGenerators, prompt } from '@feathershq/pinion' -import { FeathersBaseContext, getDatabaseAdapter } from '../commons' +import { + checkPreconditions, + FeathersBaseContext, + getDatabaseAdapter, + initializeBaseContext +} from '../commons' export interface ServiceGeneratorContext extends FeathersBaseContext { /** @@ -62,6 +67,8 @@ export type ServiceGeneratorArguments = FeathersBaseContext & export const generate = (ctx: ServiceGeneratorArguments) => generator(ctx) + .then(initializeBaseContext()) + .then(checkPreconditions()) .then( prompt( ({ name, path, type, authentication, isEntityService }) => [ @@ -89,7 +96,7 @@ export const generate = (ctx: ServiceGeneratorArguments) => type: 'list', when: !type, message: 'What kind of service is it?', - default: getDatabaseAdapter(ctx.feathers.database), + default: getDatabaseAdapter(ctx.feathers?.database), choices: [ { value: 'knex', diff --git a/packages/cli/test/generators.test.ts b/packages/cli/test/generators.test.ts index 688a5f602f..07808a2db9 100644 --- a/packages/cli/test/generators.test.ts +++ b/packages/cli/test/generators.test.ts @@ -6,12 +6,15 @@ import assert from 'assert' import { getContext } from '@feathershq/pinion' import { AppGeneratorContext } from '../src/app' -import { generate } from '../lib' import { FeathersBaseContext } from '../src/commons' import { ConnectionGeneratorArguments } from '../src/connection' import { ServiceGeneratorArguments } from '../src/service' import { combinate, dependencyVersions } from './utils' +import { generate as generateApp } from '../lib/app' +import { generate as generateConnection } from '../lib/connection' +import { generate as generateService } from '../lib/service' + const matrix = { language: ['js', 'ts'] as const, framework: ['koa', 'express'] as const @@ -35,7 +38,7 @@ describe('@feathersjs/cli', () => { before(async () => { cwd = await mkdtemp(path.join(os.tmpdir(), name + '-')) - context = await generate( + context = await generateApp( getContext( { name, @@ -64,7 +67,7 @@ describe('@feathersjs/cli', () => { }) it('generates a MongoDB connection and service and passes tests', async () => { - const connectionContext = await generate( + const connectionContext = await generateConnection( getContext( { dependencyVersions, @@ -75,7 +78,7 @@ describe('@feathersjs/cli', () => { { cwd } ) ) - const mongoServiceContext = await generate( + const mongoServiceContext = await generateService( getContext( { dependencyVersions, @@ -96,7 +99,7 @@ describe('@feathersjs/cli', () => { }) it('generates a custom service and passes tests', async () => { - const customServiceContext = await generate( + const customServiceContext = await generateService( getContext( { dependencyVersions, diff --git a/packages/create-feathers-app/bin/create-feathers-app b/packages/create-feathers-app/bin/create-feathers-app new file mode 100755 index 0000000000..da5069547b --- /dev/null +++ b/packages/create-feathers-app/bin/create-feathers-app @@ -0,0 +1,16 @@ +#!/usr/bin/env node +'use strict'; + +const { comandRunner } = require('@feathersjs/cli') + +export const program = new Command() + +program + .name('create-feathers-app') + .description('Create a new Feathers application 🕊️') + .argument('', 'The name of your new application') + // .version(version) + .showHelpAfterError() + .action((name, options) => { + console.log(name, options) + }) \ No newline at end of file diff --git a/packages/create-feathers-app/package.json b/packages/create-feathers-app/package.json new file mode 100644 index 0000000000..74bd187b72 --- /dev/null +++ b/packages/create-feathers-app/package.json @@ -0,0 +1,56 @@ +{ + "name": "create-feathers-app", + "description": "Create a new Feathers application", + "version": "5.0.0-pre.29", + "homepage": "https://feathersjs.com", + "bin": { + "feathers": "./bin/create-feathers-app" + }, + "keywords": [ + "feathers", + "feathers-plugin" + ], + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/daffl" + }, + "repository": { + "type": "git", + "url": "git://github.com/feathersjs/feathers.git" + }, + "author": { + "name": "Feathers contributors", + "email": "hello@feathersjs.com", + "url": "https://feathersjs.com" + }, + "contributors": [], + "bugs": { + "url": "https://github.com/feathersjs/feathers/issues" + }, + "engines": { + "node": ">= 14" + }, + "files": [ + "CHANGELOG.md", + "LICENSE", + "README.md", + "lib/**", + "bin/**", + "*.d.ts", + "*.js" + ], + "scripts": { + "test": "echo \"No tests necessary\"" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@feathersjs/cli": "^5.0.0-pre.29" + }, + "devDependencies": { + }, + "gitHead": "4314dc89a41a8bbaabf00b47697bf7887861d17d" + } + \ No newline at end of file