htmx.js file, the same file that gets sent to browsers in production, give or take minification and compression.
I do not speak for the htmx project, but I have made a few nontrivial contributions to it, and have been a vocal advocate for retaining this no-build setup every time the issue has arisen. From my perspective, here’s why htmx does not have a build step.
Maintenance is a cost paid for with labor, and open-source codebases are the projects that can least afford to pay it. Opting not to use a build step drastically minimizes the labor required to keep htmx up-to-date. This experience has been borne out by intercooler.js, the predecessor to htmx which is maintained indefinitely with (as I understand) very little effort. When htmx 1.0 was released, TypeScript was at version 4.1; when intercooler.js was released, TypeScript was pre-1.0. Would code written in those TypeScript versions compile unmodified in today’s TypeScript compiler (version 5.1 at the time of writing)? Maybe, maybe not.
The htmx DX is very simple—your browser loads a single file, which in every environment is the exact same file you wrote. The tradeoffs required to maintain that experience are real, but they’re tradeoffs that make sense for this project.
Modularization is one of the honking great ideas of software. Modules make it possible to solve incredibly complex problems by breaking down code into well-contained substructures that solve smaller problems. Modules are really useful.
Sometimes, however, you want to solve simple problems, or at least relatively simple problems. In those cases, it can be helpful not to use the building blocks of more complex software, lest you emulate their complexity without creating commensurate value. At its core, htmx solves a relatively simple problem: it adds a handful of attributes to HTML that make it easier to replace DOM elements using the declarative character of hypertext. Requiring that htmx remain in a single file (again, around 3,500 LOC) enforces a degree of intention on the library; there is a real pressure when working on the htmx source to justify the addition of new code, a pressure which maintains an equilibrium of relative simplicity.
While the DX costs are obvious, there are also surprising DX benefits. If you search a function name in the source file, you’ll instantly find every invocation of that function (this also mitigates the need for more-advanced code introspection). The lack of places for functionality to hide makes working on htmx a lot more approachable. Far, far more complex projects use aspects of this approach as well: SQLite3 compiles from a single-file source amalgamation (though they use separate files for development, they’re not crazy) which makes hacking on it significantly easier. You could never build the linux kernel this way—but htmx is not the linux kernel.
Future versions of htmx might use JSDoc to get some of the same guarantees without the build step. Other libraries, like Svelte, have been trending in this direction as well, in part due to the debugging friction that TypeScript files introduce.
async/await, anonymous functions, and functional array methods (i.e.
.forEach)—none of which can be used in the htmx source.
While this is incredibly annoying, in practice it is not a huge impediment. The lack of some nice language features doesn’t prevent you from writing code with functional paradigms. Would it be nice not to write a custom forEach method? Of course. But until all the browsers targeted by htmx support ES6, it’s not hard to supplement ES5 with a few helper functions. If you are used to ES6, you will automatically write better ES5.
IE11 support is going to be dropped in htmx 2.0, at which point ES6 will be allowed in the source code.
This point is obvious, but it’s worth re-stating: the htmx source would be a lot tidier if it could be split it into modules. There are other factors that affect code quality besides tidiness, but to the extent that the htmx source is high-quality, it is not because it is tidy.
This makes doing certain things with htmx very difficult. The idiomorph algorithm might be included in the htmx 2.0 core, but it’s also maintained as a separate package so that people can use the DOM-morphing algorithm without using htmx. If the core could include multiple files, one could easily accomplish this with any number of mirroring schemes, such as git submodules. But the core is a single file, so the idiomorph code will have to live there as well.
This essay might be better titled “Why htmx Doesn’t Have a Build Step Right Now.” As previously mentioned, circumstances change and these tradeoffs can be revisited at any time! One issue we’re exploring at the moment has to do with releases. When htmx cuts releases, it uses a few different shell commands to populate the
dist directory with minified and compressed versions of
htmx.js (pedants are welcome to point out that this is obviously, in some sense, a build step). In the future, we might expand that script to auto-generate the Universal Module Definition. Or we might have new distribution needs that require an even more involved setup. Who knows!