Developers commonly acquire CLI skills across diverse technologies. CLI's are integral to tech stacks, automate tasks like code generation, building, and dependency management. While often tied to specific technologies, CLIs can serve broader purposes within a team or project, streamlining Continuous Integration processes. This article explores the advantages of this approach.
Why custom CLI?
As mentioned above, CLIs automate work with specific technologies by executing tasks from the console. During development, tasks extend beyond technology-specific ones, encompassing organization-wide, team-centric, and project-specific aspects. The main benefits of creating a custom CLI within an organization or project include:
⛽ Efficiency Boost: The CLI empowers streamlined task execution through single commands or scripts, expediting work. Its prowess lies in the swift and effortless handling of repetitive tasks, translating to considerable time savings.
🎡 Reproducibility: CLI ensures precise process replication, eliminating manual step execution. A consolidated script captures all steps, enabling effortless, consistent reruns of the same sequence of actions.
🔀 Sharing: CLI fosters collaboration by allowing easy sharing with team members. Ensuring uniform access to tools and procedures enhances teamwork consistency, mitigating errors arising from tool disparities.
🏗️ Automation: CLI facilitates the automation of diverse tasks, accelerating the software development process. It proves instrumental in activities such as building, testing, and deploying applications.
🚏 Scalability: CLI simplifies the scaling of software development processes, particularly vital for large projects. It effortlessly facilitates tasks like running multiple servers simultaneously.
Custom, in-house CLIs establish an abstraction layer above external technologies, company processes, and the team. This reduces dependencies, easing new developer onboarding, standardizing team processes, and enabling automation.
Understanding executors in NX
Let's start from installing dedicated @nrwl/nx-plugin@latestpet plugin. Then, we need to generate our own plugin code with the following command: nx g @nrwl/nx-plugin:plugin [my-plugin-name].
Now, let's explore Task Executors. To execute commands in the terminal, use nx run [project] [command] or nx [command] [project]. Here, project refers to the project in the monorepo, and command signifies the NodeJS task to be run.
Furthermore, for production or testing environments, customize configurations: nx [command] [project] --configuration=[configuration].
Moreover, additional parameters are adaptable: nx [command] [project] --[myCustomParam]=[myCustomParamValue].
Creating own executor in NX
To implement the specified command, use: nx generate @nrwl/nx-plugin:executor [check] --project=[my-plugin]. Here, [my-plugin] represents the previously created plugin, and [check] signifies the task name.
Upon execution, the repository should exhibit the following folder/file structure:
The folders/files structure
The central configuration file, schema.json, acts as the primary executor scheme file, detailing crucial metadata for your executor. This includes parameter descriptions for the dedicated task, which may be optional, with options for selecting multiple or specific values.
The foundational logic resides in the executor.ts file.
In the implementation, export a function returning an object with a success flag indicating the completion status. The function takes options passed via the CLI as the first argument and the context of execution (indicating the triggered project) as the second parameter.
The final step involves assigning the task to a specific project, achievable within the project.json file under the target section.
Execute the task by using nx run [my-project]:[check] command.
Understanding code generators
Generators offer a streamlined approach to automating numerous tasks integral to your programming workflow. Whether it involves crafting templates for components and functionalities, ensuring libraries are generated and organized with precision, or updating configuration files, generators play a pivotal role in standardizing these processes with a reliable and consistent methodology.
There are three main types of generators:
🛫 Generators that expand the project code during the installation of plugin
Such generators are invoked when executing the command: npm i [plugin].
🛠️ Local generators
Generators that create templates for components, services, functionalities, etc... They've following invocation schema: nx generate [plugin]:[generator-name] [options].
🚀 Update generators
Invoked by Nx plugins during their updates to keep configuration files in sync with the latest tool versions. This generator is called when executing the command: nx migrate [plugin].
Creating own NX generator
You need to execute following command: nx generate @nrwl/nx-plugin:generator [my-generator] --project=[my-plugin], where the [my-plugin] is the name of previously created plugin and the [my-generator] is the name of generator.
The generator will create a project file structure like this:
The folders/files structure
The generator.ts file contains the entire logic of the generator. This file includes a function that is invoked to perform manipulations on the file system tree. The schema.json file contains a description of the generator, available options, validation information, and default values (similarly to an executor).
The tree parameter represents the file structure and enables the removal, addition, and editing of files. Devkit provides various tools to facilitate working with files. For instance, in the above example, the installPackageTask function installs Node packages. Additionally, within our custom generator, we can leverage other generators. The libraryGenerator function, for instance, creates a library based on another generator.
The $default object is used to read command-line arguments passed to the generator. To run the generator created in this way, use the command line as follows: nx generate @myorg/[my-plugin]:[my-generator] [mylib].
Adding packages on npm
Within the NRWL Extension, we can also create an NPM package that serves as a CLI with functionalities independent of a specific project. Such a CLI can provide standardization across teams or even the entire organization, including:
- ⛴️ setting up the work environment, installing the appropriate versions of tools, consistent with those used by other developers,
- 🛸 creating projects according to the established schema used in the project.
How to create own NPM package?
To facilitate passing arguments to the console, we can use the additional NPM package yargs. Install this tool with the command: npm install yargs.
In the library folder, create a bin directory, and within it, create a file named my-cli.js with the following content:
This allows referencing the CLI source, which will be located at the path src/lib/cli.js. The content of such a source could look like this:
This configuration provides us with the init command, with the name parameter (alias n), calling the workspaceInit function, which can contain the logic for creating a project. For example, initializing an NX workspace.
Before publishing the library in the NPM registry, we just need to configure it appropriately.
This configuration allows, after publishing and globally installing the NPM package, to use your custom CLI by executing the following command in the command line: my-cli init -—name=[new-project].
Architect's extra insight!
If you enjoyed this article, feel free to explore related topics:
Custom CLIs present extensive prospects for standardization and automation in project workflows. The NRWL Extension monorepo tool extends these capabilities, allowing the creation and expansion of CLIs from a project to an entire organization. This article delves into the fundamentals of CLI creation and utilization.