Giter Site home page Giter Site logo

cli's Introduction

⌘ ReCli generator

Powerful but simple cli boilerplate generator

  • Clear: Human readable config file do what you mean
  • Useful: Can insert code in already existing modules
  • Flexible: Can transform, reuse and extend answers on the fly

Table of Contents

Motivation

Reduce file creation routine from dev process for one side. From other is to increase the code quality by automating new code injection. It allows you to have strict code agreements cross over their project, and decrease the onboarding process.

Install

$ npm install --save @recli/cli

or if you use yarn

$ yarn add @recli/cli

Running from command line

You can run ReCli directly from the CLI (if it's globally available in your PATH, e.g. by yarn global add @recli/cli or npm install @recli/cli --global) with a variety of useful options.

Here's how to run ReCli and have debug output:

recli --test

Create generator

After install you can start using it

// using ES6 modules
import { cliOf } from '@recli/cli'

// using CommonJS modules
const { cliOf } = require("@recli/cli");

Usage

On init ou must pass module to second argument.

For work with user inputs used Inquirer.js. You can freely use it api his API as is.

cliOf('Create something', module) // global node.js module
  .ask({
    name: 'variableName',
    message: 'How we name it?',
    type: 'input'
  })

API

Full API for generators are here:

const { cliOf, useImport, usePath, useCustom, file, childProcess } = require("@recli/cli");

cliOf('Create reducer', module)
  .ask({
    name: 'reducerName',
    message: 'Reducer name',
    type: 'input'
  })
  .setKey('key')
  .ask({
    name: 'model',
    message: 'Reducer data model',
    type: 'input'
  })
  .setAnswers((answers) => {
    // extend answers object with new data
    return {
      reducerName: answers.reducerName,
      model: answers.model,
      upperCaseReducerName: answers.reducerName.toUpperCase(),
      otherVariable: 'My name is John Cena',
    }
  });
  .move(['./reducer.template.js'], '../../fake/destination')
  .rename('../../fake/destination/reducer.template.js', (answers) => {
    return `${answers.reducerName}.js`;
  })
  .useHooks('../../fake/destination/store.js', (answers) => [
    useImport(`./${answers.file2}`, answers.file2),
    usePath(`./${answers.file2}`),
    useCustom({regex, content}),
  ])
  .ask({
    message: 'Witch styles do you use?',
    type: 'list',
    name: 'style',
    choices: [
      {name: 'style.template.scss', value: 'Scss'},
      {name: 'style.template.less', value: 'Less'},
    ],
  })
  .call((answers) => {
    // do any wiered stuff you need,
    // use setAnswers other vice to store result
    return axios.get(`./ping-hook/?${answers.style}`)
  })
  .check((answers, goTo) => {
    if (answers.continue) {
      goTo('begining')
    }
  })
  .move((answers) => [
    {from: './' + answers.style, to: 'style/' + answers.style}
  ], '../../fake/destination')
  //---
  .ask({
    name: 'nestedGenerator',
    message: 'Pick one of nesteds...',
    type: 'list',
    choices: () => file.getAvailableGenerators('examples/generators/**/index.js'),
  })
  .useGenerator((answers) => require(answers.nestedGenerator))
  //---
  .ask({
    name: 'pickTask',
    message: 'Which task does we run?',
    type: 'list',
    choices: () => {
      return [
        'npm run build',
        'tsc --emitDeclarationOnly'
      ];
    }
  })
  .call(async (answers) => {
    await childProcess.spawn(answers.pickTask);
  })

Notes all callback functions can be async or return promise, to apply pause on the task.

Formatters & validators

Case conversion:

var { to } = require('@recli/cli')

to.camel('what_the_heck')      // "whatTheHeck"
to.capital('what the heck')    // "What The Heck"
to.constant('whatTheHeck')     // "WHAT_THE_HECK"
to.dot('whatTheHeck')          // "what.the.heck"
to.inverse('whaT tHe HeCK')    // "WHAt ThE HeCK"
to.lower('whatTheHeck')        // "what the heck"
to.pascal('what.the.heck')     // "WhatTheHeck"
to.sentence('WHAT THE HECK.')  // "What the heck."
to.slug('whatTheHeck')         // "what-the-heck"
to.snake('whatTheHeck')        // "what_the_heck"
to.space('what.the.heck')      // "what the heck"
to.title('what the heck')      // "What the Heck"
to.upper('whatTheHeck')        // "WHAT THE HECK"

Validators:

var { validation } = require('@recli/cli')

cliOf('validation', module)
  // adding question
  .ask({
    name: 'libraryName',
    message: 'Please enter the name of module you want to import.',
    type: 'input',
    validate: validation.validate([
      validation.isNoWhitespace,
      validation.isDashFormat,
      validation.isFirstLetterUpper,
      validation.isFirstLetterLower,
      validation.isFileIsNew('directory/**/components'),
      validation.isCamelFormat
    ]),
    default: 'lodash'
  })

⚛️ Hooks

The core feature all around is code injectors to existed files. We call it hooks, to be on hype.

It works like so. Let's imagine you have a file called router.js. After new route generation you want to append new route here.

So, let's add some hook to the router.js,

import React from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";

import { HomeRoute } from './home-route';
/* recli:use-import */

function AppRouter() {
  return (
    <Router>
        <Route path="/about/" component={HomeRoute} />
        {/* recli:use-route */}
      </div>
    </Router>
  );
}

export default AppRouter;

So step after generation we expect to have like this

import React from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";

import { HomeRoute } from './home-route';
import { NewRoute } from './new-route';
/* recli:use-import */

function AppRouter() {
  return (
    <Router>
        <Route path="/about/" component={HomeRoute} />
        <Route path="/new-route/" component={NewRoute} />
        {/* recli:use-route */}
      </div>
    </Router>
  );
}

export default AppRouter;

To make it done we have hooks:

  • useImport -> useImport({ NewRoute }, ./new-route) -> import { NewRoute } from './new-route';
  • usePath -> usePath(./new-route) -> './new-route',
  • useModuleName -> useModuleName(NodeModule) -> NodeModule,
  • useCustom -> useCustom({ regex, content }) -> content

they are applied to file by

  cliOf('My awesome task')
    ...
    .useHooks('path', (answers) => [
      useImport(`{${answers.camelCaseName}}`, `./${answers.name}.js`),
      usePath(`./${answers.name}.js`),
      useCustom({
        regex: /(\s*)(\/\*.*recli:use-module-name.*\*\/)/,
        content: `$1${moduleName}$1$2,`,
      }),
    ])

🚀 Setup

npm install @recli/cli
// or
yarn add @recli/cli

it's possible to use it by global setup

npm install @recli/cli -g
// or
yarn add @recli/cli -g

Placing generators right way

The default agreements you should have generators folder at project root with your own generators are inside folders.

Other words, your generators should match the path: generators/**/index*

To override default behavior is simple (following same markup: https://storybook.js.org/docs/guides/guide-react/)

To do that, create a file at .recli/config.js with the following content:

const { configure } = require("@recli/cli");

function loadStories() {
  require("../generators/**/index*");
  // You can require as many stories as you need.
}

configure(loadStories, module);

But the full file path will resolved to node.js module and will execute it.

Let's say you wan't to have generators like stand alone module, to share it cross over the projects you have. Let's say it have name: @recli/xxx-generators

The code markup can looks like:

  • create a file at index.js with the following content:

    const { configure } = require("recli");
    
    function loadStories() {
      require("./generators/**/*.gen.js");
    }
    
    configure(loadStories, module);
  • make sure you have package.json main section like that:

    "main": "index.js",
  • the @recli/cli will try to find the generators in that node module by using the path you prodive at index.js

Tech notes

You can't use modern syntax different to Node.js you have installed. Cause we doesn't use babel, webpack inside.

@recli/cli is written by using TS. So you will receive the extra IDE help by using TS for generators. But, you have to compile them. It should be simple for stand alone set of generators.


MIT

cli's People

Contributors

akiyamka avatar alchangyan avatar wegorich avatar

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

Forkers

akiyamka

cli's Issues

-h --help and -v --version commands missed

To debug and use the console utility, it will be extremely useful to acquire basic commands that display the current version and minimal help.
Highly recommend using yargs package for this (can auto generate this things)

it should be configure less

So core idea, that module should install and manage all deps by its own.

the only configuration is possible by using the defined folder: re-cli in the root of the project. Allow us extends that tool in future.

Other should be done similar to create react app, or storybook, or nextjs

How to use templates?

I mainly use once generated modules like this:
template.js

import React from 'react';
import PT from 'proptypes';
import cn from 'classnames';
import style from './style.css';

export default function {componentName}({ className }) {
  return <div className={cn(className, style.root)}></div>
}

{componentName}.propTypes = {
  className: PT.string
}

If I need replace {componentName} i can use make something like this.

  .useHooks('suddenly/cant/use/answers/here', (answers) => [
    useCustom({
      regex: /{moduleName}/,
      content: answers.moduleName,
    })
  ])

But it more complicated cases (replace more the one variable, print something conditionally, use iteration) it can be difficult do only with regexp .

I suggest adding the ability to use the (template literals)[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals].
All variables must be available in template scope.
For example:
template file must contain special extention tmpl.js end export function:

module.tmpl.js

module.exports = answers => (
`
import React from 'react';
import PT from 'proptypes';
import cn from 'classnames';
import style from './style.css';
${answers.libs.map(lib => `import ${lib.name} from ${lib.path}` )}

export default function ${answers.moduleName}({ className }) {
  return <div className={cn(className, style.root)}>
    ${ answers.language === 'ru' ? 'Здарова мир' : 'Hello world' }
  </div>
}

 ${answers.moduleName}.propTypes = {
  className: PT.string
 }

`)

And run it when module .move()

cliOf('Create module', module)
  .ask({
    name: 'moduleName',
    message: 'How we name it?',
    type: 'input'
  })
  .move(['./module.tmpl.js], './src/modules/)

or we can add method write that can just write string in file, it more transparent and simpler but in that case we need manually require or import every template

const { cliOf } = require('@recli/cli');
const moduleTemplate = require('./templates/module.tmpl.js');

cliOf('Create module', module)
  .ask({
    name: 'moduleName',
    message: 'How we name it?',
    type: 'input'
  })
  .write([ answers => moduleTemplate(answers) ], './src/modules/module/index.js')

.move method work like copy

Api is misleading in some places.

Move it's copy

.move([ './some', './files' ], '../in/this/dir')
Files are expected to be moved to a ../in/this/dir directory, but instead they will be copied.

I suggest calling it "copy":

.copy([ './some', './files' ], '../in/this/dir');

Move method missed

Some time we need realy just move file to another dir.
Of course a method that really moves files mut be with name ".move".
In addition, this method can replace .rename method, just like a mv utility does in Unix systems:

Moving:

.move(['./someFile.js', './someAnotherFile.js'],  './someFolder/')`;

Renaming:

.move(['./folder/someOldName.js'],  './folder/someNewName.js');

Ambiguous api

Another problem is { from: ..., to: ... } syntax:

  .move((answers) => [
    {from: './' + answers.style, to: 'style/' + answers.style}
  ], '../../fake/destination')

There is no unambiguous interpretation of what a this command may mean.
This can mean - rename file from first name to second name, but we already have .rename function, hmm...
Or maybe this mean I can move the file from one to another directory!
Oh, but why then need a second argument in this case?
It frustrates me very much.

Semi-dynamics

I can use answers and arrays in first argument but can't in second - this is pretty weird.
I will write more about this in a separate issue

.move method accept answers only in first arg

I mentioned in #9 that the absence of dynamics in the second argument causes some inconvenience.

Current situation

Currently i can't create new folder with variable name.

cliOf('Create module', module)
 .ask({
   name: 'moduleName',
   message: 'How we name it?',
   type: 'input'
 })
 .move(answers => ([
   './template'
 ]), `../src/modules/???`)

And I can't understand how i can rename the newly created folder after creating it with some temporary unique name. For example:

const tempHash = Date.now();

cliOf('Create module', module)
  .ask({
    name: 'moduleName',
    message: 'How we name it?',
    type: 'input'
  })
  .move(answers => ([
    './template'
  ]), `../src/modules/${tempHash}`)
 .rename(`../src/modules/${tempHash}`, (answers) => {
      // how I can rename folder, instead file name?
  })

Workaround (work only for unix systems ⚠️ ):

  .call(async ({ moduleName }) => {
    await childProcess.spawn(
      `cp -r ./generators/module ./k2-packages/${moduleName}/`
    );
  })

Proposed api:

I think a simpler api that will cover most of the use cases will look something like this

Just copy with renaming:

cliOf('Create module', module)
  .copy('./templates/module/index.ts', './src/NewModule/main.ts')

/* result:

ls ./src/NewModule/  ⇐ (NewModule folder is being created if not exist before that)
main.ts

*/
 

Copy by mask:

cliOf('Create module', module)
  .copy('./templates/module/*.ts', './src/NewModule/')

/* result:

ls ./src/NewModule/
index.ts
utils.ts
some.ts

*/

Copy files using answers (in both arguments):

cliOf('Create module', module)
  .ask({
    name: 'type',
    message: 'What type of it?',
    type: 'input'
  })
  .ask({
    name: 'name',
    message: 'How we name it?',
    type: 'input'
  })
  .copy(answers => ([
     `./templates/${answers.type}/*.ts`,
     `./src/${answers.name}/`
   ))

/* result:

ls ./src/myAwesomeModule/
index.ts
utils.ts
some.ts

*/

Copy in place (Optional - just for consistent)

cliOf('Clone module', module)

  .copy(`./src/module/`)
/*
  ls ./src/module_copy/
  index.ts
  utils.ts
  some.ts
*/

  .copy(`./src/module/*`)
/*
  ls ./src/module/
  index.ts
  index_copy.ts
  utils.ts
  utils_copy.ts
  some.ts
  some_copy.ts
*/

  .copy(`./src/module/some.ts*`)
/*
  ls ./src/module/
  index.ts
  index_copy.ts
  utils.ts
  utils_copy.ts
  some.ts
  some_copy.ts
  some_copy_copy.ts
*/

Run recli from code

How i can run recli from script?
Just call cliOf('Create module', module)... from any js file don't work.

Inquirer "when" got empty "answers"

  .ask({
    type: 'confirm',
    name: 'bacon',
    message: 'Do you like bacon?'
  })
  .ask({
    type: 'input',
    name: 'favorite',
    message: 'Bacon lover, what is your favorite type of bacon?',
    when: function(answers) {
      console.log(answers) // BUG: get {}, expected { bacon: true | false }
      return answers.bacon;
    }
  })

Inquirer "when" got empty "answers" because recli run every question as separate inquirer sequence

You can push questions to separate array config.questions and run all questions in one inquirer sequence before tasks

Full example

custom generators path ignored

/appRoot/.recli/config.js

const { configure } = require('@recli/cli');

function loadCli() {
  require('../cli.js');
}

configure(loadCli, module);

/appRoot/.cli.js

const { cliOf, childProcess } = require('@recli/cli');

cliOf('Create module', module)
  .ask({
    name: 'moduleName',
    message: 'How we name it?',
    type: 'input'
  })
  .move(answers => ([
    { from: './template', to: ''}
  ]), '../../modules/')
recli --test
? Please select generator: (Use arrow keys)
(nothing)

Added workflow similar to Storybook or by your own

The task to create new generator should be simple.

Like that or simplier

genOf('Add Router', module)
  .ask('What name of the file', {
       type: 'input',
       name: 'xxx',
       skip?: (answers, context) => true / false;
       validation?: [{ validator: (answers, context)=> null | string }]
  })
  .gen('./template/file.js', (answers, context) => {
     return path:string;
   })
  .update( 'src/hello/../file.js',  [{
       hook: 're-cli:add-route',
       regexp?:  Regexp | (answers, context) => regexp;
       callback: (answers, context) => string
 }])

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.