New Component Project

A Python 🐍 Command Line Interface (CLI) that helps a front end developer's workflow when creating new React Components.


Introduction
High-level summary of what the project is

This project is a Python 🐍 Command Line Interface (CLI) that helps a front end developer's workflow when creating new React Components.
The project is released on PyPi, the Python package index, and can be installed on the command line, using pipx install new-component.

List of core functionalities / interesting features.

The project uses Jinja templates to create new component directories within a ReactJS project.

My role in the project.

I am the sole author of the project and its documentation.

Technologies used

Python is the programming language the CLI is written in.

I used several python packages to help me:

  • Typer (https://typer.tiangolo.com) is a Python CLI toolkit.
  • Jinja2 is a Python package for rendering the component templates
  • Links to live demo + source code (if applicable)
    Purpose and Goal
    Why did you build this project? Why is it important to you?

    I took inspiration from Josh W Comeau’s Node Package Manager (NPM) package of the same name, new-component.

    It is a fantastic tool, but didn’t have the support for the Component Templates that I wanted. It’s worth checking out!

    I have been working with styled-components, which are fantastic.
    Styled-components lets you write actual CSS in your JavaScript.

    “Visual primitives for the component age. Use the best bits of ES6 and CSS to style your apps without stress 💅🏾” "This means you can use all the features of CSS you use and love, including (but by far not limited to) media queries, all pseudo-selectors, nesting, etc.”

    CSS is an often overlooked but crucial part in every website!

    I’ve been on a journey to learn CSS to develop this vital web development skill.

    I built this tool to help my workflow when developing the building blocks of a React project!

    What was the expected outcome of the project?

    This project was completed on schedule.

    I published the CLI application to PyPi using GitHub Workflows.

    What were the initial designs?

    The initial designs were simple notions of having a folder of Jinja2 templates (.j2 files) for each of the Component files. The CLI would then render the template with the name of the Component, which is only CLI argument. For example, new-component Button should create a Button folder within the components directly that contains javascript files rendered from each of the Jinja templates.

    Other preliminary planning

    The planning of the technology stack came from my exposure to Tiangolo’s (Sebastian Ramirez?) FastAPI project. Typer (typer.tiangolo.com) is a sibling of FastAPI that is designed to help craft CLI applications. I am a fan of the use of the Pydantic library in Python which enforces type checking of inputs, uses Python doc 📝 strings and type hints 🔍 within the application. It’s a methodology that promotes best practices by utilizing their capabilities fully in the design of the tool.

    This was a project that allowed me to explore Typer and to create a CLI with colorful terminal output, and to publish it on PyPI. Two things that make me smile 😄

    Spotlight
    What is the “killer feature” of the project?

    Emoji and style!

    ./src/components/ doesn't exist. Do you want to create it? [y/N]: y
    Warning: created the src/components directory.
    
    ✨ Creating a new BlockQuote Component ✨!
    
    Directory: src/components/BlockQuote
    Type:      styled 💅
    
    ✅ Directory created.
    ✅ BlockQuote component created and saved to disk.
    ✅ Index file created and saved to disk.
    
    BlockQuote component created! 🚀
    

    The terminal output is colored and has a great user experience. The required directories are auto-detected and you are prompted before the CLI takes any unexpected action on your behalf.

    What feature does it have that took the most work, or was the most technically impressive?

    The feature I’m most proud of in this application is use of python modules to compartmentalize different function, constants, and utilities in separate files. The initial version of the application was a hard to read. It was a few hundred lines of python code that were hard to reason about what the main function of the CLI was doing.

    I refactored the code into several files: _constants.py, _echos.py, _jinja.py, _utils.py, _version.py, and cli.py.
    Each file has a name corresponding to the function it performs within the application. This allows the code to be modular, testable, and to be understood.

    Consider the first version of the main function: 0.1.0#L20-L88

    @app.command()
    def main(
        name: str = typer.Argument(
            ...,  # Required to give name, as in `new-component ContactForm`
            help="Name of component to create.",
        ),
        directory: str = typer.Option(
            __COMPONENTS_DIR__,
            "--directory",
            "-d",
            help="The directory in which to create the component.",
        ),
        version: Optional[bool] = typer.Option(
            None,
            "--version",
            "-v",
            help="Show the application's version and exit.",
            callback=_version_callback,
            is_eager=True,
        ),
    ) -> None:
        """
        Creates an new component directory in a React project,
        with opinionated defaults for styled-components.
        See https://styled-components.com/ for more information.
        """
    
        components_directory = Path(directory)
        new_directory = components_directory / name
    
        if new_directory.exists() is False:
            new_directory.mkdir(
                parents=True
            )  # Create directory, creating missing parent folders
    
        # index_file = new_directory / "index.js"
    
        new_directory_full_path = Path.cwd() / new_directory
        message_start = "Created a new "
        component = typer.style(name, fg=typer.colors.GREEN, bold=True)
        message_end = " Component 💅 🚀!"
        new_directory_path = typer.style(
            new_directory_full_path, fg=typer.colors.GREEN, bold=True
        )
        message = message_start + component + message_end
        typer.echo(message)
        typer.echo(new_directory_path)
    
        installed_location = new_component.__file__
        templates_dir = installed_location.replace("__init__.py", "")
        template_path = Path(templates_dir) / "templates"
        # typer.echo(template_path)
    
        from jinja2 import Environment, FileSystemLoader
    
        loader = FileSystemLoader(template_path)
        jinja_environment = Environment(loader=loader)
        index_template = jinja_environment.get_template("index.js.j2")
        index_output = index_template.render({"ComponentName": name})
    
        with open(f"{new_directory}/index.js", "w") as f:
            f.write(index_output)
    
        component_template = jinja_environment.get_template("component.js.j2")
        component_output = component_template.render({"ComponentName": name})
    
        with open(f"{new_directory}/{name}.js", "w") as f:
            f.write(component_output)
    

    Every single action of the application is enumerated declaratively.
    There are distinct tasks being performed within the main function, but they are hard to separate from each other at first glance.

    Now, let’s look at a later version 0.3.0 of the same function:

    0.3.0#L46-L137

    @app.command()
    def main(
        name: str = typer.Argument(
            ...,  # Required to give name, as in `new-component ContactForm`
            help="Name of component to create.",
        ),
        directory: str = typer.Option(
            None,
            "--directory",
            "-d",
            help="The directory in which to create the component.",
        ),
        extension: str = typer.Option(
            None,
            "--extension",
            "-e",
            help="The file extension for the created component files.",
        ),
        version: Optional[bool] = typer.Option(
            None,
            "--version",
            "-v",
            help="Show the application's version and exit.",
            callback=_version_callback,
            is_eager=True,
        ),
    ) -> None:
        """
        Creates an new component directory in a React project,
        with opinionated defaults for styled-components.
        For information on styled-components, see https://styled-components.com/.
        For online documentation, see https://new-component.iancleary.me/.
        """
    
        # load and merge config
        file_config = _load_config()
        config = _merge_config(
            file_config=file_config, directory=directory, extension=extension
        )
    
        # update variables form config
        directory = config["directory"]
        extension = config["extension"]
    
        # path to components directory
        components_directory = Path(directory)
    
        # Prompt user to create components directory, if it doesn't exist
        if components_directory.exists() is False:
            _create_components_dir_confirm(components_directory=components_directory)
            _create_directory(directory=components_directory)
            _create_components_dir_echo(components_directory=components_directory)
    
        # Create Paths to the new Component directory
        new_component_directory = components_directory / name
        full_path_to_component_directory = Path.cwd() / new_component_directory
    
        # Allow user to abort if component already exists, else create component directory
        if full_path_to_component_directory.exists() is True:
            _overwrite_component_echo(
                components_directory=new_component_directory, component_name=name
            )
            _overwrite_component_confirm(component_name=name)
    
        else:
            _create_directory(directory=full_path_to_component_directory)
    
        # Setup Jinja Variables used in template render
        variables = {"ComponentName": name}
    
        # Render component files in your components directory
        _create_output(
            new_directory=full_path_to_component_directory,
            template_name="index",
            variables=variables,
            extension=extension,
        )
        _create_output(
            new_directory=full_path_to_component_directory,
            template_name="component",
            variables=variables,
            extension=extension,
            filename=f"{name}",
        )
    
        # Echo final status to user
        _create_new_component_echo(
            component_name=name,
            path_to_component_directory=new_component_directory,
        )
    

    The purpose of the code is much clearer.

  • Input variables from the CLI arguments,
  • Load the configuration of the CLI
  • Creating output files and echoing status to the user.
  • How often have you inherited code someone wrote that is a bunch of spaghetti?
    Have you been on a project where parts of the code are only touched by the people that originally wrote them?

    There shouldn’t be fear around maintaining some code.

    The refactor from 0.1.0 to 0.3.0 grouped and organized lower level procedural code into clean code that is easily consumable by others.

    Clean code that is sustainable by others is the type of code I am proud to have written.

    What were the technical hurdles that got in your way? Any major problems you hit during development?

    One problem I encountered during development was how relative python modules were imported.
    Consider the difference between the following:

    from .module1 import function_to_do_a

    vs.

    from package.module import function_to_do_a

    Do both work when run from the command line?

    When executed as the new-component command, the scope of the app is the main function. This function needs the package name in the import to resolved, so from package.module works, while the relative from .module does not.

    How did you solve those problems? What was the solution?

    This section is optional. If the project is actively being used by real people, talk a little bit about the current status, who uses it, why they use it, what they say to you about it, stuff like that. If the project was contrived specifically for the portfolio, omit this section.

    Lessons Learned
    What did I learn doing this project?

    I learned several things during this project.

    The value of scrpting repeated tasks. When the repition of a tasks is common, it can often pay to automate the tasks. In this case, I've started to learm more about the design pattern of styled compoonents, and am very happy to develop those skills.

    I exercised my project management experience in how I break down tasks from the initial design and how I refactor and publish new features.

    If you used a framework or other libraries/tools, was it a good choice? How did it help? In which ways was it insufficient?

    I used the Typer project and am very happy with my choice. It enabled the stlyed output of the terminal, the main application internals were handled for me. It is a case where open source shines!

    How has this affected the work you’ve done since then? Real examples of how this project built your knowledge for future projects is fantastic.

    This project has built knowledge of how React components and styled components work together. It has been useful in creating the portfolio site you are currently reading!

    Lets Go Further Together

    Feel free to reach out if you are looking for a systems engineer, have a question, or just want to connect.