Create a CLI tool to help bootstraping Flutter project using Node.JS - Part 1
Tags: flutter, tooling, node, cli
Published: 2023-02-15 06:53:11

Hey folks! I know i'm not the only one who struggles and wastes time when i create every new flutter project. So i've decided to make a tool for helping me bootstraping my projects. It saves me around 1 hour of my time everytime i start a new personal project.

## The problem
Let's start by describing what i do when i start every project:
- create a new flutter project with the name in it
- create a `src`folder
- create a bunch of utilities folder arranged like this:

![folder structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/89df7bqg3z8weig9ll10.png)

The `transitions.dart` file for example hold some code for page transitions using [animations](url). Here is what it looks like:

```dart
import 'package:animations/animations.dart';
import 'package:flutter/material.dart';

class Transitions {
  static Route sharedAxisPageTransition(Widget screen,
      {bool isHorizontal = true}) {
    final SharedAxisTransitionType _transitionType = isHorizontal
        ? SharedAxisTransitionType.horizontal
        : SharedAxisTransitionType.vertical;

    return PageRouteBuilder<SharedAxisTransition>(
        pageBuilder: (context, animation, secondaryAnimation) => screen,
        transitionsBuilder: (context, animation, secondaryAnimation, child) {
          return SharedAxisTransition(
            animation: animation,
            secondaryAnimation: secondaryAnimation,
            transitionType: _transitionType,
            child: child,
          );
        });
  }
}
```

- and finally create a folder with respective view (`entity_view.dart`) and logic (`entity_viewmodel.dart`) for each entity (screen) i need. I'm pointing out that i'm using the [stacked package](https://pub.dev/packages/stacked) as a state management.  

## The solution

Ok, now that we saw what were the steps took, let's try to automate them. I decided to use NodeJS as i was comfortable with it but you can use any tool that you like to build CLI. 

### Create our project

First, let's create our project. Again, you can use any package manager that you like, it doesn't matter.

Create a folder to hold your pretty CLI and let's start:

```bash
mkdir fluttertool && cd fluttertool && yarn init -y && code .
```

Nothing special here, it will create our folder, create a `package.json` file inside it and open [VSCode](https://code.visualstudio.com/) for us.

Now that we have our starting point, let's add some informations inside our `package.json` file.

Initially, we have something like this:
```json
{
  "name": "fluttertool",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}
``` 

Let's add some lines to tell that we will use this project as a cli tool.

```json
{
  "name": "flutternodetool",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "type": "module"
  "bin": {
    "fluttertools": "./index.js"
  },
}
``` 

Ok. That last line specifies a command-line interface (CLI) for the module. When the module is installed, a command-line tool called fluttertools will be created that can be used to run the module's functionality. 

The `bin` property maps the name of the command-line tool to the file that should be executed when the tool is run, in this case index.js.

Let's create this `index.js` file:
```js
#! /usr/bin/env node --es-module-specifier-resolution=node
console.log("Hello from my fluttertool");
```

The first line of code, `#! /usr/bin/env node`, is known as a "shebang" or "hashbang" and is used to specify the interpreter for the script. 

In our case, it specifies that the script should be run using the node interpreter, which is typically used for executing JavaScript code on the command line.

Ok, let's test our code!

```bash
sudo npm i -g .
```

With this line, we will install our current package globally, so we will be able to run our tool using our alias previously set: "fluttertool".

Now, try to run:
```bash
fluttertool
```
inside your terminal and you will see the message we set before inside our `index.js` file.

Now we have our starting point. Let's add some fancy tools to it.

### Dependencies

We will add some dependencies to help us deal with CLI behaviours ([inquirer](https://www.npmjs.com/package/inquirer)) and text coloring ([chalk](https://www.npmjs.com/package/chalk)). 

```bash
yarn add inquirer chalk
```

### ESLint

Let's also add `eslint` to help us with checking syntax and find problems in our code. Run this command:
```bash
yarn eslint --init
```

Answer to these questions when prompted:
```bash
? How would you like to use ESLint?
❯ To check syntax and find problems

? What type of modules does your project use?
❯ JavaScript modules (import/export)

? Which framework does your project use?
❯ None of these

? Does your project use TypeScript? › No

? Where does your code run? …  (Press <space> to select, <a> to toggle all, <i> to invert selection)
✔ Node

? What format do you want your config file to be in?
❯ JavaScript

Successfully created .eslintrc.cjs
```

Now that've got our tools set, we can jump into our commands.

### Expected Commands
We will have a bunch of command to implement. But for now, let's implement the necessary ones:

#### Project creation

What my tool will mainly do is create a flutter project for me, add my helper files and init project with an entry file (defaults to "Home").

What i'm expecting is when i run this:
```bash
fluttertool
```

My tool will ask me the name of my app and the organization name, create a flutter project with these informations, create an *src* directory and organize folders and files like we described previously:

![folder structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/89df7bqg3z8weig9ll10.png)

And finally add my helper files. Ok, let's jump back into our `index.js` file.

```js
#! /usr/bin/env node --es-module-specifier-resolution=node

import inquirer from "inquirer";

async function run() {
  const { choice } = await inquirer.prompt({
    type: "list",
    name: "choice",
    message: "What do you want to do?",
    choices: [
      "1 - Create a new project",
      "2 - Add a new stacked module",
    ],
  });
}

run();
```

What's going on here. Well we are calling `inquirer` module which will help us to interact with the user. We use the `prompt` method to display informations to the user and, in that case, we are waiting for actions by showing him the options that our tool offers. 

Let's re-run our command. As our tool has been installed globally, every changes we make in our code are automatically applied.

```bash
> fluttertool
? What do you want to do? (Use arrow keys)
❯ 1 - Create a new project 
  2 - Add a new stacked module
```

As you can see, the user can select an option by using the arrow keys and the result will be stored inside the `choice` variable.

Let's now implement the actions and the log helper. Create a file named `log.js` and `actions.js`.

```js
// log.js
import chalk from "chalk";
const log = console.log;

const text = (text) => log(chalk.white(`${text}`));
const success = (text) => log(chalk.green(`✔ ${text}`));
const info = (text) => log(chalk.blue(`${text}`));
const error = (text) => log(chalk.red(`✖ ${text}`));

const renderTitle = () =>
  log(
    chalk.bold.magenta(`
  ░█▀▀░█░░░█░█░▀█▀░▀█▀░█▀▀░█▀▄░░░▀█▀░█▀█░█▀█░█░░░█▀▀
  ░█▀▀░█░░░█░█░░█░░░█░░█▀▀░█▀▄░░░░█░░█░█░█░█░█░░░▀▀█
  ░▀░░░▀▀▀░▀▀▀░░▀░░░▀░░▀▀▀░▀░▀░░░░▀░░▀▀▀░▀▀▀░▀▀▀░▀▀▀
  `)
  );

export { success, info, text, error, renderTitle };
```

Pretty cool hun? You can generate your own text [here](https://textkool.com/en/test-ascii-art-generator)

Now let's return to our `actions.js`file and write this:
```js
const createProject = () => {}
const createModule = () => {}

const actions = (option) => {
  if (option.startsWith(1)) {
    createProject();
  }

  if (option.startsWith(2)) {
    createModule();
  }
};

export default actions;
```

And change our `index.js` file to:
```js
#! /usr/bin/env node --es-module-specifier-resolution=node

import inquirer from "inquirer";
import actions from "./actions";

async function run() {
  const { choice } = await inquirer.prompt({
    type: "list",
    name: "choice",
    message: "What do you want to do?",
    choices: ["1 - Create a new project", "2 - Add a new stacked module"],
  });

  actions(choice);
}

run();
```

I think that it's pretty obvious here that actions will dispatch our action according to the option the user gave.

Let's implement our `createProject` function. 

```js
// actions.js
const createProject = async () => {
  info("\n========================================");
  info("OK! Let's create your wonderful project!");
  info("========================================\n");

  const { project, organization } = await inquirer.prompt([
    {
      type: "input",
      name: "project",
      message: "What is the name of your project?",
      default: "myproject",
    },
    {
      type: "input",
      name: "organization",
      message: "What is the name of your organization?",
      default: "test",
    },
  ]);

  text("Creating project skeleton...");
  const command = spawn("flutter", [
    "create",
    project,
    "--org",
    `com.${organization}.${project}`,
  ]);

  command.stderr.on("data", (data) => {
    error(data);
    process.exit(0);
  });
};
``` 

What our code is doing here is when the user selects the `createProject` option, we will ask him for the project name (defaults to "myproject") and the organization name (defaults to "test") and then the tool will launch a command like you generally do when creating a flutter project.

```bash
flutter create <projectname> --org <organizationname>
```

Now will wait for the command to complete and then we will make the next operations: **adding dependencies (animations and stacked)**

```js
...
command.stderr.on("data", (data) => {
    error(data);
    process.exit(0);
  });

  command.on("close", () => {
    text("Adding dependencies...");
    const dependencies = spawn(
      "flutter",
      ["pub", "add", "animations", "stacked"],
      { cwd: project }
    );
  });
```

The `spawn` command here needs to be executed inside our newly created project, that's why we are changing our working directory to the name of the project here.

Again it will add dependencies like you would do in your regular flutter application.

Next we will create our main project structure:

![project structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8ipueab4vsgwobgr7ffo.png)

For the sake of readability, let's create another file called: `utils.js`.

```js
// utils.js

import { resolve } from "path";
import { error, success } from "./log";
import { existsSync, mkdirSync } from "fs";

const createFolders = (project) => {
  const folders = [
    "src",
    "src/views",
    "src/views/screens",
    "src/views/widgets",
    "src/services",
    "src/helpers",
    "src/models",
  ];

  try {
    for (const folder of folders) {
      const _path = resolve(project, "lib", folder);
      if (!existsSync(_path)) {
        mkdirSync(_path);
      }
    }

    success("Folders and files are correctly created");
  } catch (err) {
    error(err);
    process.exit(0);
  }
};

export { createFolders };

```
Now let's update our `actions.js` file to use this:
```js
// actions.js

...
command.on("close", () => {
    text("Adding dependencies...");
    const dependencies = spawn(
      "flutter",
      ["pub", "add", "animations", "stacked"],
      { cwd: project }
    );

    dependencies.stderr.on("data", (data) => {
      error(`Cannot find folder ${project} - ${data}`);
      process.exit(0);
    });

    dependencies.on("close", () => {
      success(`Dependencies are correctly added...`);
      info(`Creating folders...`);
      createFolders(project);
    });
  });
```

That closes our first part of the article. What we've done so far: 
- created a cli node project
- interact with user about the application informations
- create a project using these informations
- create folder structure along with it

Please like the article or leave a comment if you'd like to see the [part 2](https://dev.to/lynxgsm/create-a-cli-tool-to-help-bootstraping-flutter-project-using-nodejs-part-2-19dk) where we will:
- add template files
- implement the module creation action

Thanks! 😄 See you next time! 👋

<a href="https://www.buymeacoffee.com/ailifidaach" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>