Reading List
The most recent articles from a list of feeds I subscribe to.
Hands
ESLint as a learning resource
I've been doing some AWS/serverless stuff at work recently. If you've been following me long enough, you know I switched paths about 6 months ago. A couple of hiccups here and there, but it's been going well.
Today, and not for the first time, I picked up some education from an ESLint rule page.
no-await-in-loop tells us how to optimise our code and when not to use parallelisation. It took just two minutes of reading, too.
I love little educational moments like these – especially when they are sandwiched between the start and completion of a task.
I encourage you to get in touch over email by using the following convenience link for any discussion: comment via email.
TinyPilot: Month 31
New here?
Hi, I’m Michael. I’m a software developer and the founder of TinyPilot, an independent computer hardware company. I started the company in 2020, and it now earns $60-80k/month in revenue and employs six other people.
Every month, I publish a retrospective like this one to share how things are going with my business and my professional life overall.
Highlights
- TinyPilot began shipping a new product: the Voyager 2a.
- I canceled our contract with a new 3PL vendor a few weeks into the relationship.
Goal grades
At the start of each month, I declare what I’d like to accomplish. Here’s how I did against those goals:
Writing Javascript without a build system
Hello! I’ve been writing some Javascript this week, and as always when I start a new frontend project, I was faced with the question: should I use a build system?
I want to talk about what’s appealing to me about build systems, why I (usually) still don’t use them, and why I find it frustrating that some frontend Javascript libraries require that you use a build system.
I’m writing this because most of the writing I see about JS assumes that you’re using a build system, and it can be hard to navigate for folks like me who write very simple small Javascript projects that don’t require a build system.
what’s a build system?
The idea is that you have a bunch of Javascript or Typescript code, and you want to translate it into different Javascript code before you put it on your website.
Build systems can do lots of useful things, like:
- combining 100s of JS files into one big bundle (for efficiency reasons)
- translating Typescript into Javascript
- typechecking Typescript
- minification
- adding polyfills to support older browsers
- compiling JSX
- treeshaking (remove unused JS code to reduce file sizes)
- building CSS (like tailwind does)
- and probably lots of other important things
Because of this, if you’re building a complex frontend project today, probably you’re using a build system like webpack, rollup, esbuild, parcel, or vite.
Lots of those features are appealing to me, and I’ve used build systems in the past for some of these reasons: Mess With DNS uses esbuild to translate Typescript and combine lots of files into one big file, for example.
the goal: easily make changes to old tiny websites
I make a lot of small simple websites, I have approximately 0 maintenance energy for any of them, and I change them very infrequently.
My goal is that if I have a site that I made 3 or 5 years ago, I’d like to be able to, in 20 minutes:
- get the source from github on a new computer
- make some changes
- put it on the internet
But my experience with build systems (not just Javascript build systems!), is that if you have a 5-year-old site, often it’s a huge pain to get the site built again.
And because most of my websites are pretty small, the advantage of using a
build system is pretty small – I don’t really need Typescript or JSX. I can
just have one 400-line script.js file and call it a day.
example: trying to build the SQL playground
One of my sites (the sql playground) uses a build system (it’s using Vue). I last edited that project 2 years ago, on a different machine.
Let’s see if I can still easily build it today on my machine. To start out, we have to run npm install. Here’s the output I get.
$ npm install
[lots of output redacted]
npm ERR! code 1
npm ERR! path /Users/bork/work/sql-playground.wizardzines.com/node_modules/grpc
npm ERR! command failed
npm ERR! command sh /var/folders/3z/g3qrs9s96mg6r4dmzryjn3mm0000gn/T/install-b52c96ad.sh
npm ERR! CXX(target) Release/obj.target/grpc/deps/grpc/src/core/lib/surface/init.o
npm ERR! CXX(target) Release/obj.target/grpc/deps/grpc/src/core/lib/avl/avl.o
npm ERR! CXX(target) Release/obj.target/grpc/deps/grpc/src/core/lib/backoff/backoff.o
npm ERR! CXX(target) Release/obj.target/grpc/deps/grpc/src/core/lib/channel/channel_args.o
npm ERR! CXX(target) Release/obj.target/grpc/deps/grpc/src/core/lib/channel/channel_stack.o
npm ERR! CXX(target) Release/obj.target/grpc/deps/grpc/src/core/lib/channel/channel_stack_builder.o
npm ERR! CXX(target) Release/obj.target/grpc/deps/grpc/src/core/lib/channel/channel_trace.o
npm ERR! CXX(target) Release/obj.target/grpc/deps/grpc/src/core/lib/channel/channelz.o
There’s some kind of error building grpc. No problem. I don’t
really need that dependency anyway, so I can just take 5 minutes to tear it out
and rebuild. Now I can npm install and everything works.
Now let’s try to build the project:
$ npm run build
? Building for production...Error: error:0308010C:digital envelope routines::unsupported
at new Hash (node:internal/crypto/hash:71:19)
at Object.createHash (node:crypto:130:10)
at module.exports (/Users/bork/work/sql-playground.wizardzines.com/node_modules/webpack/lib/util/createHash.js:135:53)
at NormalModule._initBuildHash (/Users/bork/work/sql-playground.wizardzines.com/node_modules/webpack/lib/NormalModule.js:414:16)
at handleParseError (/Users/bork/work/sql-playground.wizardzines.com/node_modules/webpack/lib/NormalModule.js:467:10)
at /Users/bork/work/sql-playground.wizardzines.com/node_modules/webpack/lib/NormalModule.js:499:5
at /Users/bork/work/sql-playground.wizardzines.com/node_modules/webpack/lib/NormalModule.js:356:12
at /Users/bork/work/sql-playground.wizardzines.com/node_modules/loader-runner/lib/LoaderRunner.js:373:3
at iterateNormalLoaders (/Users/bork/work/sql-playground.wizardzines.com/node_modules/loader-runner/lib/LoaderRunner.js:214:10)
at iterateNormalLoaders (/Users/bork/work/sql-playground.wizardzines.com/node_modules/loader-runner/lib/LoaderRunner.js:221:10)
at /Users/bork/work/sql-playground.wizardzines.com/node_modules/loader-runner/lib/LoaderRunner.js:236:3
at runSyncOrAsync (/Users/bork/work/sql-playground.wizardzines.com/node_modules/loader-runner/lib/LoaderRunner.js:130:11)
at iterateNormalLoaders (/Users/bork/work/sql-playground.wizardzines.com/node_modules/loader-runner/lib/LoaderRunner.js:232:2)
at Array.<anonymous> (/Users/bork/work/sql-playground.wizardzines.com/node_modules/loader-runner/lib/LoaderRunner.js:205:4)
at Storage.finished (/Users/bork/work/sql-playground.wizardzines.com/node_modules/enhanced-resolve/lib/CachedInputFileSystem.js:43:16)
at /Users/bork/work/sql-playground.wizardzines.com/node_modules/enhanced-resolve/lib/CachedInputFileSystem.js:79:9
This stack overflow answer suggests running export NODE_OPTIONS=--openssl-legacy-provider to fix this error.
That works, and finally I can npm run build to build the project.
This isn’t really that bad (I only had to remove a dependency and pass a slightly mysterious node option!), but I would rather not be derailed by those build errors.
for me, a build system isn’t worth it for small projects
For me, a complicated Javascript build system just doesn’t seem worth it for small 500-line projects – it means giving up being able to easily update the project in the future in exchange for some pretty marginal benefits.
esbuild seems a little more stable
I want to give a quick shoutout to esbuild: I learned about esbuild in 2021 and used for a project, and so far it does seem a more reliable way to build JS projects.
I just tried to build an esbuild project that I last touched 8 months ago on
a new computer, and it worked. But I can’t say for sure if I’ll be able to
easily build that project in 2 years. Maybe it will, I hope so!
not using a build system is usually pretty easy
Here’s what the part of nginx playground code that imports all the libraries looks like:
<script src="js/vue.global.prod.js"></script>
<script src="codemirror-5.63.0/lib/codemirror.js"></script>
<script src="codemirror-5.63.0/mode/nginx/nginx.js"></script>
<script src="codemirror-5.63.0/mode/shell/shell.js"></script>
<script src="codemirror-5.63.0/mode/javascript/javascript.js"></script>
<link rel="stylesheet" href="codemirror-5.63.0/lib/codemirror.css">
<script src="script.js "></script>
This project is also using Vue, but it just uses a <script src to load Vue –
there’s no build process for the frontend.
a no-build-system template for using Vue
A couple of people asked how to get started writing Javascript without a build system. Of course you can write vanilla JS if you want, but my usual framework is Vue 3.
Here’s a tiny template I built for starting a Vue 3 project with no build system. It’s just 2 files and ~30 lines of HTML/JS.
some libraries require you to use a build system
This build system stuff is on my mind recently because I’m using CodeMirror 5 for a new project this week, and I saw there was a new version, CodeMirror 6.
So I thought – cool, maybe I should use CodeMirror 6 instead of CodeMirror 5. But – it seems like you can’t use CodeMirror 6 without a build system (according to the migration guide). So I’m going to stick with CodeMirror 5.
Similarly, you used to be able to just download Tailwind as a giant CSS file, but Tailwind 3 doesn’t seem to be available as a big CSS file at all anymore, you need to run Javascript to build it. So I’m going to keep using Tailwind 2 for now. (I know, I know, you’re not supposed to use the big CSS file, but it’s only 300KB gzipped and I really don’t want a build step)
(edit: it looks like Tailwind released a standalone CLI in 2021 which seems like a nice option)
I’m not totally sure why some libraries don’t provide a no-build-system version – maybe distributing a no-build-system version would add a lot of additional complexity to the library, and the maintainer doesn’t think it’s worth it. Or maybe the library’s design means that it’s not possible to distribute a no-build-system version for some reason.
I’d love more tips for no-build-system javascript
My main strategies so far are:
- search for “CDN” on a library’s website to find a standalone javascript file
- use https://unpkg.com to see if the library has a built version I can use
- host my own version of libraries instead of relying on a CDN that might go down
- write my own simple integrations instead of pulling in another dependency (for example I wrote my own CodeMirror component for Vue the other day)
- if I want a build system, use esbuild
A couple of other things that look interesting but that I haven’t looked into:
- this typescript proposal for type syntax in Javascript comments
- ES modules generally
PHP wishlist: The pipe operator
Is it weird to have a favorite operator? Well, the pipe operator |> is mine. Not only does it look cool, it opens a world of possibilities for better code.
Unfortunately, it’s not available in any of the languages I use on a daily basis. There are proposals to add it to PHP and JavaScript, but we’re not there yet. I’d like to expand on why I think the pipe operator would be a valuable addition to the language from a PHP developer’s perspective.
The pipe operator takes a value from the left side, and passes it as the input for a function on the right side.
// This is the same as `sum(1, 2)`
1 |> sum(2);
Adding the pipe operator to PHP isn’t a new idea. Sister language Hack has always had the pipe operator.
Hack’s pipe operator is more verbose as it requires a $$ token to indicate which parameter the output should be passed to.
'Hello, world!'
|> strtolower($$)
|> substr($$, 12)
|> str_replace(',', '', $$)
// 'hello world'
PHP’s standard library wasn’t built with the pipe operator in mind. Functions like str_replace($find, $replace, $subject) or array_map($callback, $subject) aren’t a good match because the subject isn’t the first argument. Hack uses a token to have them play well with the pipe operator.
I prefer a more succinct approach without the token. With short closures in PHP 8, I don’t think the token isn’t as needed anymore as we can wrap functions with an incompatible signature.
'Hello, world!'
|> strtolower(...)
|> substr(12)
|> fn ($greeting) => str_replace(',', '', $greeting)
// 'hello world'
No more wrappers
I love Laravel collections, but I prefer the pipe operator. Take this chain of operations using a collection:
$users = Users::all()
->map(fn (User $user) => …)
->filter(fn (User $user) => …)
->take(5)
->toArray();
With the pipe operator, User:all() could return an array and work with a set of array functions.
// (This doesn't exist, just an example)
use Illuminate\Support\Collection\{map, filter, take};
$users = Users::all()
|> map(fn (User $user) => …)
|> filter(fn (User $user) => …)
|> take(5);
The chain starts with an array and ends with an array. No need to start with a collection object, and cast it back to an array after. We can keep using the primitive, more portable datatype.
Another benefit is we don’t need macros anymore.
Collection::macro('foo', fn (Collection $collection) => …);
We can mix and match third party methods our with our own.
use App\Support\Collection\{foo}
use Illuminate\Support\Collection\{map, take};
$users = Users::all()
|> map(…)
|> foo(…)
|> take(…);
This is true for any class that wraps or extends a primitive object. Laravel recently added a Str class similar to Collection. With the pipe operator, we don’t need an additional class to do fluent operations on strings. Many projects also use a custom DateTime implementation like Carbon or Chronos. All would be moot if we can define our own functions that we can pipe a DateTime object into.
Carbon::now()->isFuture();
// vs.
DateTime::now()
|> isFuture();
With the pipe operator, we get the ergonomics of boxed objects, without the overhead of wrapping & unwrapping them.
The deeper effect on your codebase
While the above examples illustrate the ergonomics of the pipe operator, it also has a deeper effect on your codebase. It promotes decoupling data from processes.
For example, we’re building a webshop that sells paper. We’ve got a Paper model, and a custom collection class to filter down a collection of Paper.
We want a unique array of colors available for letter-sized paper that costs between €5 and €10.
$paperCollection
->withPriceBetween(5_00, 10_00)
->withSize(PaperSize::Letter))
->colors()
->toArray();
The collection class:
class PaperCollection extends Collection
{
public function withSize(PaperSize $size): self
{
return $this
->filter(fn (Paper $paper) => $paper->size === $size);
}
public function withPriceBetween(int $min, int $max): self
{
return $this
->filter(fn (Paper $paper) => $paper->price >= $min && $paper->price < $max);
}
public function colors(): Collection
{
return $this
->map(fn (Paper $paper) => $paper->color)
->uniq();
}
}
But the webshop doesn’t only sell paper, it also sells pens. Now we want a unique array of colors available for ballpoint pens cost between €5 and €10. We’ll create a Pen and PenCollection.
$penCollection
->withPriceBetween(5_00, 10_00)
->withType(PenType::Ballpoint))
->colors()
->toArray();
class PenCollection extends Collection
{
public function withType(PenType $size): self
{
return $this
->filter(fn (Pen $pen) => $pen->type === $type);
}
public function withPriceBetween(int $min, int $max): self
{
return $this
->filter(fn (Pen $pen) => $pen->price >= $min && $pen->price < $max);
}
public function colors(): Collection
{
return $this
->map(fn (Pen $pen) => $pen->color)
->uniq();
}
}
We’re entering duplication territory. Twice is fine, but once we start selling scissors we’ll run out of patience. How can we refactor?
We could move withPriceBetween and colors to a trait, but we still need custom PenCollection and PaperCollection classes.
We could have our custom collections extend a common ProductCollection, but in my experience we’re digging ourselves a deeper hole that way. At some point, we’ll come across another shared method that doesn’t fit “product” either.
Enter the pipe operator. No more need to worry about having methods on a single collection class. If we convert them to static methods we can use different functions throughout without them needing a fixed home.
class PaperCollection
{
/** @param Paper[] $paper */
public static function withSize(array $paper, PaperSize $size): self
{
return array_filter(
$paper,
fn (Paper $paper) => $paper->size === $size,
);
}
}
class PenCollection extends Collection
{
/** @param Pen[] $pens */
public static function withType(array $pens, PenType $size): self
{
return array_filter(
$pens,
fn (Pen $pen) => $pen->type === $type,
);
}
}
class ProductCollection
{
/** @param Product[] $products */
public static function withPriceBetween(array $products, int $min, int $max): self
{
return array_filter(
$products,
fn (Product $product) => $product->price >= $min && $product->price < $max,
);
}
/** @param Product[] $products */
public static function colors(array $products): Collection
{
return $products
|> fn (array $products) => array_map(fn (Product $product) => $product->color, $products)
|> array_unique();
}
}
$paperCollection
|> ProductCollection::withPriceBetween(5_00, 10_00);
|> PaperCollection::withPaperSize(PaperSize::Letter)
|> ProductCollection::colors()
$penCollection
|> ProductCollection::withPriceBetween(0_00, 10_00);
|> PaperCollection::withType(PenType::Ballpoint)
|> ProductCollection::colors()
Namespaced functions are also an option. They’ll need to be stored in a separate file and autoloaded accordingly.
namespace App\Paper;
/** @param Paper[] $paper */
function withSize(array $paper, PaperSize $size): self
{
return array_filter(
$paper,
fn (Paper $paper) => $paper->size === $size,
);
}
But they look so much better!
use App\Paper\withPaperSize;
use App\Product\{withPriceBetween, colors};
$paperCollection
|> withPriceBetween(5_00, 10_00);
|> withPaperSize(PaperSize::Letter)
|> colors()
Pipe operator RFC
In 2020, Larry Garfield created an RFC to add the pipe operator in PHP. I like how Larry described the pipe operator in a comment:
Scalar methods work if and only if the method you want to use is one that was pre-blessed as a method. If not, you’re SOL. Pipes allow any type-compatible function at all to be used, anywhere. There’s simply no comparison in terms of the flexibility it allows.
Unfortunately the RFC was declined for PHP 8.1. There were two recurring arguments why it was declined.
First, there was discussion wether it should use a token. Back to one of my previous examples:
'Hello, world!'
|> strtolower($$)
|> substr($$, 12)
|> str_replace(',', '', $$)
// 'hello world'
I don’t really mind wrapping code in an arrow function when the argument order is an issue. Or I would use a third party library that wraps standard PHP functions with pipe-friendly signatures.
'Hello, world!'
|> strtolower(...)
|> substr(12)
|> fn ($greeting) => str_replace(',', '', $greeting)
// 'hello world'
Second, many noted a pipe function can exist in userland. While true, a custom pipe will be difficult to statically analyze for type-safety between function calls. More importantly, it doesn’t promote writing pipe-friendly code in general. Adding the pipe operator to the language would push developers to consider separating data from processes.
I hope the pipe operator can be reconsidered in a future PHP version.