Adding Deno support to the Eta template engine

Background

A few months ago I wrote about my creation of Eta, an embedded JavaScript template engine.

Since then, Eta has met with a fair bit of success (it's now used by Facebook's popular Docusaurus library to generate SSR pages) and has seen quite a few updates.

I was particularly excited to add Deno support. Since Deno is relatively new, not many template engines are compatible with it, and none of them are as full-featured as Eta. Additionally, Eta is written in TypeScript, which is ideal for Deno modules because Deno has built-in support for TypeScript.

Libraries like EJS and lodash.template are still far more popular than Eta for Node.js users, despite being less lightweight, less reliable, and slower. I hoped that adding Deno support to Eta would increase its popularity where older libraries weren't an option.

Challenges

I knew actually porting the module to use Deno's syntax would be quite easy. All I needed to do was add the .ts ending to imports and use Deno's standard library instead of Node built-ins like fs.

I could see a few challenges, though. First of all, Eta needed to continue working with Node.js. Using Deno, you import modules using URLs:

import  *  as  fs  from  "https://deno.land/std@0.66.0/fs/mod.ts"

This definitely does not work in Node.js and will cause errors.

Second, Deno resolves file paths differently than Node.js with TypeScript. Using the file extension .ts in imports – like import someMod from './some-mod.ts' – causes Node to throw an error, but not specifying the extension causes Deno to throw an error.

Finally, I planned to host Eta on deno.land/x, Deno's 3rd-party module registry. I wanted users to be able to import the module using a short URL, like this:

import * as eta from "https://deno.land/x/eta/mod.ts";

Rather than having to specify a long nested directory path, like this:

import * as eta from "https://deno.land/x/eta/dist/deno/mod.ts";

The Solution

After a bit of internet research, I found a library called Denoify. Denoify is a build tool that takes TypeScript source files and outputs files built for Deno.

Denoify automatically converts import paths to work with Deno, converting statements like this:

import { myFunc } from './my-func'

To this:

import { myFunc } from './my-func.ts'

The main advantage of Denoify, though, is that it lets you provide a Deno-specific implementation of your files.

Say you have a file, file-handlers.ts, that requires the Node fs module. With Denoify, you can create a file-handlers.deno.ts file that uses Deno's standard library fs instead.

Denoify will automatically swap out the override file when you build (this will sound familiar to users of React Native, which has a feature this was based on). It turns out this is a super helpful feature.

In my case, I was able to extract all my file handling logic into one file called file-methods.ts, and created a Deno-specific implementation at file-methods.deno.ts. Other scripts could import ... from './file-methods' just like normal, but file-methods.ts itself was a different file inside the Deno build.

Testing

The last thing I had to do before release was add testing for the Deno build. Luckily, Deno has a built-in assertions module. Its syntax is fairly similar to other assertion libraries I'd used – as an example, here's a simple test I wrote.

import { assertEquals } from 'https://deno.land/std@0.67.0/testing/asserts.ts'
import { render } from '../../deno_dist/mod.ts'

Deno.test('simple render', () => {
  const template = `Hi <%=it.name%>`
  assertEquals(eta.render(template, { name: 'Ben' }), 'Hi Ben')
})

I ended up creating a small subdirectory named deno/ inside my main tests folder. There I put several tests that focused mainly on general functionality (just in case somehow the build went wrong and everything broke) and file handling (Eta has, as described above, unique file handling code for Deno).

Final Steps

It turns out that linters, test files, and documentation generators try to operate on every single file they see within their input directory, even if it isn't directly in their test path.

I spent a lot of time figuring out how to:

  • Make ESLint ignore *.deno.ts files
  • Make Prettier not try to format Deno files
  • Make Jest ignore the test/deno subdirectory
  • Make Coveralls ignore the test/deno subdirectory
  • Make TypeDoc ignore Deno files
  • Etc.

Finally, though, I got all of my Deno files correctly ignored. I added the denoify command to my build script, and ... voila! Eta supported Deno!

Publishing

One really nice feature of deno.land/x is that it supports adding a module that lives in the subdirectory of a Git repository. In my case, I configured denoify to create an output folder called deno_dist. This folder contains all of Eta's source files, as well as README.md and LICENSE.

I added Eta to deno.land/x, so users can view and import it from deno.land/x/eta. The registry automatically updates the module, thanks to webhooks, every time I create a new tagged release.

Conclusion

So there we have it, an account of how I gave my npm package Deno support! I hope this helps any who are working to bring Deno support to their packages and libraries. Don't hesitate to ask in the comments if you have any questions!

⚡ Obligatory shameless plug ⚡

If you're looking for a template engine for your next Deno project, try Eta! It's lightweight, has great performance, and is super configurable.

Check out Eta's documentation, or see examples of Eta being used with Opine and Alosaur!