Frontend Development Environment as a Package
Or When to Build Your Own CRA
The growth of JavaScript tooling ecosystem in recent years has been astonishing. The number of available tools exploded and the complexity of a setup required to bootstrap a project which would be considered modern has skyrocketed. This, as well as the quick rate of churn of these tools contributed largely to the famous JavaScript fatigue. The frustration fuelled a number of projects that aim at abstracting away that complexity, usually in a form of zero-config development setups. Most notable examples are nwb and create-react-app but the list is growing.
These projects usually help developers in two ways:
- Taking care of boilerplate app code This can take a form of simply providing a copy-pasted app skeleton but can get pretty elaborate with CLI scaffolding tools like yeoman.
- Providing the development environment This means bundling a group of tools that compose a frontend dev environment. These include devevelopment server with live reloading, build pipeline with code bundling and compiling, testing setup, linting, typechecking etc.
I recently got interested in exploring the latter, letβs call it Development
Environment as a Package (itβs a mouthful, so letβs use π»ππ¦ for short). The
idea is pretty simple. Take all the infrastructure the sits around the code of
your application and extract it into a single installable package that your
project depends on. Just yarn add my-dev-env -D
and youβre done. While this
seems plausible, letβs first explore why it might not be a good idea.
Why you shouldnβt use π»ππ¦
Any time you extract a piece of your project or itβs dependencies and build an abstraction around it, you introduce an indirection, place the extracted code in a black box. This indirection will inevitably need to be unboxed once the limitations or bugs in the extracted code start to leak. Breaking the abstraction to peak inside can get messy and frustrating. It is especially so, when the process involves complex tools with elaborate configurations, like in the case of modern frontend tooling (ESlint, Webpack, etc.).
Practical example Youβre getting cryptic test runner errors for your unit tests suite. In a traditional setup you can debug by playing with the test runner configuration in the root of the project, but with π»ππ¦ you will have to
yarn link
the dev env package first or make the changes somewhere in thenode_modules directory.
Building wrappers around lower level tools leads to a loss of access to some of their features. This gets especially limiting when the tools introduce new features and your wrapper prevents you from easily accessing them.
Practical example A new version of webpack introduces optimisations to bundle size, provided you add a plugin to your configuration. With a traditional setup, you can simply upgrade webpack and paste the plugin configuration to your webpack.config.js but with π»ππ¦ you will need to go through the process of updating the external dependency (which may be 3rd party).
Lastly, most tools are optimised for usage directly in your project, and extracting them to a package can require a bit of hoops jumping and cringeworthy hacks. This adds new layers of complexity to your setup you wouldnβt otherwise need.
While these are undeniable drawbacks of the π»ππ¦ approach, they didnβt prevent the projects like create-react-app from becoming very popular.
Why you should use π»ππ¦
Some obvious wins include significant reduction in the time and effort required to start a new project as well as convenient upgrades of the toolchain, especially across multiple projects. Itβs also much easier to focus on authoring code that adds real value to your enduser, when you donβt have to spend hours trying to get different tools work together. Outsourcing the development environment setup can be a godsend for all developers that find the frontend ops tasks challenging, tedious or dull.
Practical example A frontend development environment consisting of development server, linting, testing and build pipeline can easily have 50 development dependencies and 10 configuration files. By using e.g. CRA you will end up with one development dependency and no configuration files.
But the positive impact of using π»ππ¦ goes beyond time saving and cleaner directory listing. In my experience, it can encourage a more decoupled architecture for large scale projects. By allowing effortless bootstrapping and streamlined updates, it helps making the decision to split codebases. In consequence, it also encourages continuous improvement through experimenting with new tools and approaches, which can be easily back-ported to older projects or discarded.
Practical example When starting to work on a new projects a team opts out of using the π»ππ¦ used in all other projects in a the company. They do so in order to try Jest as the unit test runner instead of Mocha which is used in the current setup. After a while they make a decision to migrate to Jest fullyβββthey install Jest in their π»ππ¦, release a new version, upgrade it in all other projects and use codemods to migrate the tests.
From an even more holistic perspective, choosing the π»ππ¦ approach instead of separate setups for each project can have positive effects beyond codebases. In companies with product teams working on multiple frontend projects, the introduction of π»ππ¦ makes it easier for developers to contribute cross-team or switch teams and for projects to change ownership. It also reduces the overall time wasted on solving the same problems in a slightly different way by different teams.
Practical example At the company I work for, Issuu, we have 5 product teams, each maintaining several frontend projects. A while ago, each of these projects had a similar, yet slightly different development environment. We started the migration to a π»ππ¦ by first trying the approach in one of the teams when starting a new product. Today we have around 10 projects across 3 teams that use it, and plan to migrate more soon. We love how improvements implemented by one team can be used across all these project with no additional effort.
As mentioned, there is a wide selection of open source π»ππ¦ solutions out there. However, Iβve decided to experiment with writing my own, which gave me some idea about pros and cons of this approach. Letβs start with the cons.
Why you shouldnβt write your own π»ππ¦
If the reason youβre considering building a CRA clone is the NIH syndrome then it would probably be more beneficial for you and the community to spend your time contributing to one of the existing projects instead.
By sticking to the established and battle-tested solutions you and your team will avoid a range of problems that other developers has encountered and spent hours solving. You will also be able to easily keep up with the evolving community best standards without the crippling FOMO.
Practical example Some time ago it turned out that the way some of the π»ππ¦ like preach-cli handle development SSL certificates in an unsafe way. The vulnerability was quickly fixed by the communities behind the projects.
When making a decision to opt out of using one of the existing dev environments supported by community you should also take into consideration the size of the team and the number of projects involved. In my experience this approach makes most sense in an environment where a significant number of frontend projects are initiated and need to be maintained.
It should also be mentioned that writing a robust π»ππ¦ is not an easy task, especially if you fancy advanced features like ejecting (i.e. an option to copy all the boilerplate into the project).
Regardless of these points, I found that an in-house solution worked well for my use case. Here is why.
Why you should write your own π»ππ¦
Composing your own development environment gives you the ability to fine tune it to the specific needs that you and your team might have. You donβt have to make trade-offs that can sometimes be difficult or simply not acceptable. You can bring only the tools you actually need and configure them the way you prefer. Development setup is a very opinionated domain, so conforming to all decisions made by an off-the-shelf solution can be challenging.
Practical example A lot of developers refrained from choosing CRA because of the lack of support for CSS preprocessors like Sass as well as server rendering of the markup. Furthermore, since you are in charge, you can make quick iterations and fix issues and introduce features, without the need of community approval.
The experience and understanding of the tooling ecosystem gained while composing your own π»ππ¦ is invaluable. Choosing the right tools and making them all play together as an installable package requires a deep understanding of their features, tradeoffs and configuration.
Practical example When choosing a JavaScript bundler for my π»ππ¦, I learned that rollup works great for libraries and widgets because of the clean output and easy module type control, while for products like web-apps webpack does an amazing job, thanks to e.g. robust code splitting.
Finally, when building an in-house π»ππ¦ you can usually afford to focus on DX (Developer Experience) much more than for a one-off development setup, making the life of your colleagues or your own easier. You can optimise the most common tasks and workflows, and polish the CLIs with e.g. readable output and autocompletion.
Practical example This kind of care for DX is exemplified when running CRA dev server on the port that is already used by another processβββit will automatically choose an alternative one. Sweet!
Example of a simple π»ππ¦
While the codebase for projects like CRA can be a bit overwhelming, building a
simple π»ππ¦ does not have to be difficult. Some time ago I gave
a lightning talk on this subject
at a CopenhagenJS meetup and to aid the talk Iβve published a simple example of
a π»ππ¦ for authoring JavaScript libraries, with the
code on GitHub. You can try it
by running yarn add cphjs-inst-dev-env -D
and then using one of the three
available commands: yarn cphjs-tdd
yarn cphjs-test
and yarn cphjs-build
.
With this one development dependency you get bundling with Rollup with Babel
compilation, support for Sass with SCSS syntax and importing from JS, linting
with ESLint with Prettier auto-fixing, and a TDD setup with Jest for running
tests on the compiled bundle.
Iβve used the development environment as a package approach for my side-projects (with CRA) as well as at work (with an in-house solution) for a while now. Iβm quite happy with it, but Iβm eager to hear about your opinion. What do you think?
Big thanks to Kenneth Skovhus and Mads Hartmann for reviewing this post.