Adding Deno support to the Eta template engine
Tags: deno, typescript, javascript, denoify
Published: 2020-09-14 14:43:19

_How I added Deno support to my module, Eta, using Denoify_

## Background

A few months ago I wrote about my creation of [Eta](https://eta.js.org), an embedded JavaScript template engine.

{% link https://dev.to/nebrelbug/i-built-a-js-template-engine-3x-faster-than-ejs-lj8 %}

Since then, Eta has met with a fair bit of success (it's now used by Facebook's popular [Docusaurus](https://v2.docusaurus.io) 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](https://deno.land/std@0.68.0) 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:
```js
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 https://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:
```ts
import * as eta from "https://deno.land/x/eta/mod.ts";
```
Rather than having to specify a long nested directory path, like this:
```ts
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](https://github.com/garronej/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:

```ts
import { myFunc } from './my-func'
```
To this:
```ts
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](https://reactnative.dev/docs/platform-specific-code#platform-specific-extensions)). 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](https://deno.land/manual/testing/assertions). Its syntax is fairly similar to other assertion libraries I'd used – as an example, here's a simple test I wrote.

```ts
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 https://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 https://deno.land/x, so users can view and import it from https://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!


_:zap: Obligatory shameless plug :zap:_

If you're looking for a template engine for your next Deno project, try [Eta](https://eta.js.org)! It's lightweight, has great performance, and is super configurable.

Check out [Eta's documentation](https://eta.js.org/docs/learn/install), or see examples of Eta being used with [Opine](https://github.com/asos-craigmorten/opine/tree/main/examples/eta) and [Alosaur](https://github.com/alosaur/alosaur/tree/master/examples/eta)!