Nursing Code

Creating Addons for Ember-CLI with TypeScript


It's been possible to write your Ember applications using TypeScript for some time now using Ember CLI TypeScript and the library also helps if you wish to build an addon using TypeScript.

There's a catch, however, if you want to build an addon to be consumed by the CLI in TypeScript, think things like extra parts of the build pipeline vs a component library.

Ember CLI SASS

The Ember community maintains a library called ember-cli-sass which provides for SASS/SCSS compilation with your Ember projects.

There's an unfortunate problem, however, in that it has issues where changing unrelated files causes a full rebuild of your SASS. In larger projects that may become quite painful.

In the largest project I work on regularly, this was causing around 3 seconds to be added to each rebuild. Fairly frustrating as a developer and long enough to cause your mind to wander.

I spent a good amount of time trying to deal with the problem via a fork of ember-cli-sass and exploring another developer's pull request trying to fix it.

The process turned out to be quite frustrating as there's lots of things that that exist in that library to try and solve everyone's problems. This meant trying to deal with dart-sass vs node-sass, many different possible user configurable options and plenty of conditional logic that wasn't applicable to the problem I needed to solve.

In the end, I decided that a faster way to solve my problem was to create our own sass compilation plugin that avoided handling much of the complexity inherent in solving everyone's problems. That's not a criticism of ember-cli-sass per se, but a great feature of open source software. If the stuff that's out there doesn't work for you, you're free to fork it, tweak and rewrite things to meet your own needs.

I Can't Work Without TypeScript

As a previous skeptic of the benefits of TypeScript, I've become a very strong advocate and have found it immensely beneficial in our projects. When I came to build a CLI addon, I wanted to use TypeScript, because not having that compiler yelling at me about my mistakes .. well, it just feels cavalier these days!

Unfortunately, it turns out, there's no agreed way to do this in the community.

I asked around in Discord and looked at as many Ember addons as I could to find other examples where folks have done so.

The feedback was that there's not really of writing or exmaples out there.

Thankfully [ember-cli-typescript]1 provided a great foundations that I could study and then build on.

I'm going to break down what is going on in their approach and how I used it for my addon.

Ember CLI and TypeScript

Ember CLI with recent versions of ember-cli-babel will happily accept TypeScript files for your application code, unfortunately, this only applies to stuff in your actual application, not the tools used to build your application.

What this means, in practice, is if you want to consume TypeScript to build a CLI addon, you need to set things up yourself.

Following along

I've built an addon called fast-sass that follows the pattern I'm going to describe. All the files below can be found here

First some indirection.

This is the content of the index.js file, which Ember CLI expects to define the addon module.

Thankfully, the [ember-cli-typescript]1 folks have documented it!

First it checks if there's a folder called js containing a file, addon.js. If that exists, then export the default from that module and we're done.

If it doesn't exist, then require a difference module register-ts-node. Then return the default from the file called addon defined in the ts folder.

'use strict';

const fs = require('fs');

// If transpiled output is present, always default to loading that first.
// Otherwise, register ts-node if necessary and load from source.
if (fs.existsSync(`${__dirname}/js/addon.js`)) {
  module.exports = require('./js/addon').default;
} else {
  require('./register-ts-node');

  module.exports = require('./ts/addon').default;
}

We'll come back to where the js folder and files come from later.

Next let's look at register-ts-node.

'use strict';

if (!require.extensions['.ts']) {
  let options = { project: `${__dirname}/ts/tsconfig.json` };

  // If we're operating in the context of another project, which might happen
  // if someone has installed ember-cli-typescript from git, only perform
  // transpilation. In this case, we also overwrite the default ignore glob
  // (which ignores everything in `node_modules`) to instead ignore anything
  // that doesn't end with `.ts`.
  if (process.cwd() !== __dirname) {
    options.ignore = [/\.(?!ts$)\w+$/];
    options.transpileOnly = true;
  }

  require('ts-node').register(options);
}

Firstly, the file checks to see if require.extensions has a key named '.ts', if so, there's nothing to do.

If we don't yet have that key, then load up options from ts/tsconfig.json and pass them to ts-node. There's some conditional work to handle some edge cases, which I've not dug into.

After register-ts-node has executed, the final line in index.js kicks in and we're able to require a .ts file as happily as if it were a .js file.

What's ts-node?

ts-node is a library that provides TypeScript execution and REPL for node.js.

Basically it allows you to run TypeScript based code in Node without having to pass it through a compilation process first. This is mainly going to help you while you are developing an addon. End users will consume the compiled js.

Publishing

In order to allow the package you build to be consumed by people via npm, it needs to be published.

When publishing a package to npm the prepublishOnly hook will get called. This will run the TypeScript compiler and build the project based on the specification in tsconfig.json, essentially spitting out the compiled result in /js. Once this has been packaged and uploaded to npm, the postPublish hook is called, which will delete the js folder locally.

    "prepublishOnly": "yarn tsc --noEmit false --project ts",
    "postpublish": "rimraf js"

The intricacies of publishing an npm package are beyond the scope of the post, but you can get started learning here

Your Actual Addon

So now we've managed to wire up a project to build your addon using TypeScript, the rest is up to you! Take a look at the two projects references in this article for more ideas on how to deal with things like missing types for ember-cli and various broccoli plugins.

Extracting What I've Learned

If you're anything like me, you'll have forgotten all about how to do this stuff next time you need it!

In order to make this approach easier to get started I've extracted it into a blueprint.

You can get your own starter addon by running ember addon -b https://github.com/mfeckie/ember-typescript-addon-blueprint.git {your_addon_name} --yarn

Hope y'all find this useful.