DEV Community

Cover image for Create a CLI tool to help bootstraping Flutter project using Node.JS - Part 1
AILI Fida Aliotti Christino
AILI Fida Aliotti Christino

Posted on • Updated on

Create a CLI tool to help bootstraping Flutter project using Node.JS - Part 1

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 srcfolder
  • create a bunch of utilities folder arranged like this:

folder structure

The transitions.dart file for example hold some code for page transitions using animations. Here is what it looks like:

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,
          );
        });
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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 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:

mkdir fluttertool && cd fluttertool && yarn init -y && code .
Enter fullscreen mode Exit fullscreen mode

Nothing special here, it will create our folder, create a package.json file inside it and open VSCode 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:

{
  "name": "fluttertool",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}
Enter fullscreen mode Exit fullscreen mode

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

{
  "name": "flutternodetool",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "type": "module"
  "bin": {
    "fluttertools": "./index.js"
  },
}
Enter fullscreen mode Exit fullscreen mode

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:

#! /usr/bin/env node --es-module-specifier-resolution=node
console.log("Hello from my fluttertool");
Enter fullscreen mode Exit fullscreen mode

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!

sudo npm i -g .
Enter fullscreen mode Exit fullscreen mode

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:

fluttertool
Enter fullscreen mode Exit fullscreen mode

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) and text coloring (chalk).

yarn add inquirer chalk
Enter fullscreen mode Exit fullscreen mode

ESLint

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

yarn eslint --init
Enter fullscreen mode Exit fullscreen mode

Answer to these questions when prompted:

? 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
Enter fullscreen mode Exit fullscreen mode

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:

fluttertool
Enter fullscreen mode Exit fullscreen mode

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

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

#! /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();
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

// 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 };
Enter fullscreen mode Exit fullscreen mode

Pretty cool hun? You can generate your own text here

Now let's return to our actions.jsfile and write this:

const createProject = () => {}
const createModule = () => {}

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

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

export default actions;
Enter fullscreen mode Exit fullscreen mode

And change our index.js file to:

#! /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();
Enter fullscreen mode Exit fullscreen mode

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.

// 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);
  });
};
Enter fullscreen mode Exit fullscreen mode

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.

flutter create <projectname> --org <organizationname>
Enter fullscreen mode Exit fullscreen mode

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

...
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 }
    );
  });
Enter fullscreen mode Exit fullscreen mode

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

For the sake of readability, let's create another file called: utils.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 };

Enter fullscreen mode Exit fullscreen mode

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

// 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);
    });
  });
Enter fullscreen mode Exit fullscreen mode

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 where we will:

  • add template files
  • implement the module creation action

Thanks! 😄 See you next time! 👋

Buy Me A Coffee

Top comments (2)

Collapse
 
nichiren96 profile image
Roel Tombozafy

Very useful since I use the same folder structure like yours!!! Can't wait to see the part 2 and use this tool for my next flutter project creation

Collapse
 
xremanu profile image
xreManu

Very interesting