A Laboratory Powered by Deno, Fresh, and Tailwind
A Fresh approach
I built a route to generate the CSS files I needed. Neither Mocha nor Vitest works correctly with Deno, so I looked up the documentation on testing and wrote some unit tests. I haven’t checked whether UI testing with Playwright is possible.
Fresh generates a template that includes Twind (Tailwind as a standalone
tool). I’ve been skeptical of the utility classes pattern for a long time; now
I could try it myself. I can honestly say I find it much slower, more limiting, and more tedious
than the now-ancient BEM pattern. In addition, I initially
couldn’t seem to customize the library at all, or even tell whether the defaults were active: the
text-emerald-700 class shown in the Tailwind
documentation had no effect. After seeing something
relevant on Discord I restored the apparently ineffectual code I’d removed and found that
twind.config.ts did work. Ironically, however—considering the origin of the project—I can’t see
how to turn off CSS minification.
I thought Fresh does something special with forms when hydrated, but no. To allow regular form
submission, I put each form in an
island and only hide
the submit button if
true (in which case
my event handlers are responsible for submitting the form). The second experiment worked
Impressions from baby’s first Deno project
Deno’s npm support confused me.
import x from "npm:y" failed in the general case, but
@preact/core worked. I eventually noticed Fresh had created an import map to enable
the latter. I’m now using Skypack or esm.sh URLs for npm modules. It’s unclear what the right
I told GitLab CI to cache the .deno directory, told Deno to use that directory, and ended up with
deno lint checking the entire dependency tree. It transpires that directories can’t be excluded
globally in deno.json. Putting identical
exclude entries under
test fixed it.
deno fmt disagrees with Prettier, which I suppose is because it uses the dprint formatter. I’m sticking to Prettier for consistency with other projects. Apart from that, I love the fast, tool-free Deno experience. Running my tests in CI the first time, with zero caching, took 16 seconds. npm would still have been resolving the dependency tree.
I made Emacs use the extremely fast deno run -A npm:prettier instead of npx prettier too (with a bit of help from b-fuze when it came to the correct invocations). I also set up colour conversion in Emacs using Deno, but that’s a story for another time.
Refining the build
The first version of the Dockerfile was quite straightforward. Some time after setting it up and deploying it in my Kubernetes cluster, I was unexpectedly greeted by a mystifying build error on an unrelated commit:
Output#8 5.192 error: The source code is invalid, as it does not match the expected hash in the lock file. #8 5.192 Specifier: https://email@example.com #8 5.192 Lock file: /app/deno.lock
I hadn’t touched the Preact dependency. Experimentation revealed the build worked locally if I removed deno.lock and if I ran deno test after removing it, but not if I only ran deno task start. On the Deno Discord, all I found at first was a recommendation to delete deno.lock, which I had already done. Searching for the error itself turned up one message, linking to an outwardly unrelated issue with a promising suggestion:
- Remove the deno.lock if you have one
- Clean the cache with
deno cache --reload dev.tsand for the
- Run, then remove the
These instructions worked like a charm. I committed the updated files and the issue went away.
I couldn’t tell how to use the barely-documented distroless variant of the official Deno
image. It has a different user (
nonroot instead of
deno) and no shell, obviously, so I can’t run the normal build steps and don’t know what to copy
in a multi-stage build. On top of that, Fresh needs to build and cache things at runtime. I can’t
give it a read-only filesystem. It even requires a privileged user.
b-fuze directed me to the
but it was already set to
/deno-dir in the base image. The culprit was in fact
made the image use the
deno user and mounted an ephemeral volume at runtime over
That seemed to solve the problem. I still couldn’t use a read-only filesystem, though, since Fresh
DENO_DIR at runtime.
Later improvements involved copying
/deno-dir from the build stage to make use of the cached
dependencies and copying a
/home/deno created in the build stage to
/home/nonroot. With this
I can continue to mount an ephemeral volume over the home directory at runtime as well as drop all
privileges. The dive tool now reports 99% efficiency and
everything is tolerably well isolated.
I wanted to use deno compile to create a standalone binary, just for fun. Sadly, it gave me a type error. Upgrading from Fresh v1.1.5 to v1.3.1 and Deno v1.33 to v1.36 made it succeed, but no pages would load:
OutputAn error occurred during route handling or page rendering. ReferenceError: React is not defined at Object.App (file:///D:/Media/My/www/lab/routes/_app.tsx:5:5) at https://firstname.lastname@example.org/X-ZS8q/denonext/preact-render-to-string.mjs:12:1830 at T (https://email@example.com/X-ZS8q/denonext/preact-render-to-string.mjs:12:1857) at T (https://firstname.lastname@example.org/X-ZS8q/denonext/preact-render-to-string.mjs:12:1933) at T (https://email@example.com/X-ZS8q/denonext/preact-render-to-string.mjs:12:1933) at z (https://firstname.lastname@example.org/X-ZS8q/denonext/preact-render-to-string.mjs:12:316) at renderInner (https://email@example.com/src/server/render.ts:125:20) at renderSync (https://firstname.lastname@example.org/src/server/render.ts:147:13) at https://email@example.com/src/server/render.ts:197:46 at Object.DEFAULT_RENDER_FN [as renderFn] (https://firstname.lastname@example.org/src/server/render.ts:10:5) ReferenceError: React is not defined at Object.App (file:///D:/Media/My/www/lab/routes/_app.tsx:5:5) at https://email@example.com/X-ZS8q/denonext/preact-render-to-string.mjs:12:1830 at T (https://firstname.lastname@example.org/X-ZS8q/denonext/preact-render-to-string.mjs:12:1857) at T (https://email@example.com/X-ZS8q/denonext/preact-render-to-string.mjs:12:1933) at T (https://firstname.lastname@example.org/X-ZS8q/denonext/preact-render-to-string.mjs:12:1933) at z (https://email@example.com/X-ZS8q/denonext/preact-render-to-string.mjs:12:316) at renderInner (https://firstname.lastname@example.org/src/server/render.ts:125:20) at renderSync (https://email@example.com/src/server/render.ts:147:13) at https://firstname.lastname@example.org/src/server/render.ts:197:46 at Object.DEFAULT_RENDER_FN [as renderFn] (https://email@example.com/src/server/render.ts:10:5)
A good time so far
Setting aside the architecture and the runtime compilation, the only major limitations of Fresh I’ve encountered so far are related to the lack of a layout mechanism, which was rectified in v1.4.0, and the absence of useful context in some areas, which was rectified in v1.2.0 (superseding my own simplistic attempt). I intend to update my project soon.
Other than that, the experience has been overwhelmingly positive. I can’t repeat this enough: Deno is fast. I’m fairly used to the slow React feedback loop. SvelteKit is much quicker, but never instantaneous. Using Fresh, going from merge request to new Docker image takes under a minute with tests. This enables shockingly quick and easy iteration on what are ultimately lightweight, progressively enhanced tools with absolutely no fat on them.