I wrote a static site builder from scratch. Here is what I learned.
Dev Guide Daily
Posted on October 23, 2020
Hey everyone! 👋
I humbly submit my Hacktober contribution to open-source: no‑fuss: a static site builder. The reason I created it was:
- Wanted to recreate the experience of editing a codepen locally. No fuss, no complicated frameworks, just
pug
,less
and somejs
. - Wanted to create a project that's well documented, simple and approachable by anyone to contribute.
The result of this was No Fuss. According to the test coverage report, it only has 187 relevant lines of code, that implement the whole thing. Here's the feature set:
✅ Support for pug
and less
files (easily extensible)
✅ Fingerprinting out of the box for any type of file
✅ Live reload development server
✅ Incremental builds
✅ Pluggable architecture
✅ Great developer documentation
Yes, I put developer documentation as a feature, because I notice that a lot of popular projects have great user documentation, but if you want to contribute, often you wouldn't know where to start.
How it works
In short, here's a diagram showing how it works:
If you want to learn more, check out the docs here. That all being said, here is what I learned while working on this project.
Don't optimize too early
In my first iteration of the project, I made the classic mistake of optimizing prematurely. This lead to complicated code that I found hard to work with, and I was making less and less progress every day.
The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming. - Donald Knuth
One of the most important things to keep in mind as a developer is that you might be wrong. Don't get attached to any particular idea or approach. This way of thinking allowed me to throw out the old complicated implementation, and start from scratch with a simpler one.
The main difference between the first and the second approach is in the data structure used to keep track of dependencies between files.
In the first approach I created a tree structure, because it would be the most optimal. However, it was difficult to understand and I had to write a lot of comments in the code even for myself.
The second approach is maybe not as optimal as the first one, but it uses a simple list structure, which is much easier to work with.
Always ask: do I really need this?
One of the most important parts of the whole project is parsing of files. This process allows us to determine the dependencies between the files, which in turn enables:
- fingerprinting the assets
- incremental builds
Initially, I thought that for each type of file I want to support, like less
or pug
, I should import the official parser, to get the AST - abstract syntax tree, and then traverse it to find the links to other files.
However, after thinking about it, I realized that the only thing I need is to pull out strings that look like file paths.
So, do I really need the full abstract syntax tree of every file type? - Nope 😊
I made a quick and dirty implementation in less than 35 lines of relevant code using regex
, that basically works for any language or file. The regex is not perfect, but it got the job done for the initial pass. Check out the docs here.
Always write tests
Soon after starting the project I had a version that seemingly worked. When I ran it locally, it did generate the output files correctly. However, only when I started writing tests, I discovered how terrible my implementation was.
It was reading files unnecessarily more than once and it suffered from weird race conditions. All of this was only apparent in the tests, and I couldn't tell this was happening in practice, because there wasn't any noticeable slowdown when running the CLI.
Code coverage is your friend
I never really pushed for 100% code coverage in my other projects, but since this one was so small in scope, I decided to try and achieve this metric for the sake of exercise.
However, once I did a deep dive and really went through every single line that wasn't covered, I found that the lack of code coverage in some places was the result of weird code.
Seeking to improve code coverage ultimately lead to figuring out how to remove unnecessary code altogether. I would estimate that because of this effort alone, I was able to both reduce the number of lines by 10 to 15%, while improving the readability of the code at the same time.
The biggest improvements were made in removing unnecessary conditionals. For example, my TypeScript types allowed for certain values to be undefined
at certain times, and I would have if
statements or ?.
operators in order to guard against that. But due to the way the algorithm works, those cases could not actually occur in practice, so they were impossible to test.
Once I saw this, I was able to improve my types, which allowed me to remove the if
statements, which in turn made the code a lot more concise and readable.
Conclusion
Thanks for reading this post. 😊 I wanted to share the things I learned, and I hope that it inspires you to hack and create something better yourself. 🚀
There are many things that could be improved, it's by no means perfect. If you see any of them, feel free to send a PR. 😊
If you like my content, consider following me on twitter @DevGuideDaily.
Posted on October 23, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024