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:
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.
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 the_node_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.
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.
_create-react-app_
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.
If the reason you’re considering building a create-react-app
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.
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
_create-react-app_
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
_create-react-app_
dev server on the port that is already used by another process — it will automatically choose an alternative one. Sweet!
While the codebase for projects like create-react-app
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 create-react-app
) 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.