By TEN BITCOMB

Leveraging In-Repo Addons In Your Ember App

There's a little-known feature in Ember CLI that can help solve a lot of common problems.

One of the great things about Ember CLI is its addon feature, which allows for the simple encapsulation of functionality that can be shared between Ember apps.

What makes an Ember addon different from any other Node module is that it can hook into your application's build process, as well as contain a directory structure that your app can resolve for components, helpers, or really any kind of Ember object.

For instance, here is the index.js for an addon that I wrote to make the SCSS from Material Web Components available to ember-cli-sass without losing the @material namespace, as would happen if I just added node_modules/@material to the includePaths option under sassOptions. It also adds icon fonts to the build tree.

const { existsSync,
        mkdirSync,
        symlinkSync }  = require('fs'),
        Funnel         = require('broccoli-funnel'),
      { UnwatchedDir } = require('broccoli-source'),
        MergeTrees     = require('broccoli-merge-trees');

module.exports = {
  name: require('./package').name,

  isDevelopingAddon() {
    return true;
  },
  
  included(app) {
    const tmpDir = `${app.project.root}/tmp/mdc`;
    const nodeModules = `${app.project.root}/node_modules`;
    if(!existsSync(tmpDir)) {
      mkdirSync(tmpDir);
    }
    const symlinkDir = `${tmpDir}/@material`;
    if(!existsSync(symlinkDir)) {
      symlinkSync(`${nodeModules}/@material`, symlinkDir);
    }
    const sassOptions = app.options.sassOptions || {};
    sassOptions.includePaths = sassOptions.includePaths || [];
    sassOptions.includePaths.push(tmpDir);
    app.options.sassOptions = sassOptions;
  },

  treeForPublic(){
    const materialIcons = new Funnel(new UnwatchedDir(`${this.project.root}/node_modules/material-design-icons/iconfont`), {
      destDir: 'assets/fonts/material-design-iconfont',
      include: ['*.eot', '*.ttf', '*.woff', '*.woff2']
    });
    return MergeTrees([materialIcons]);
  }
};

As you can see, it's really just an object with some functions that get called whenever an Ember app is built. An Ember addon, at the bare minimum, can be just an index.js file and a package.json file.

By creating a directory and symbolic link inside tmp/ and adding the directory to sassOptions, my addon makes it possible to @import '@material/button'; within my SCSS files.

By using the treeForPublic hook, I make the iconfont available under assets/fonts/material-design-iconfont/ by adding them to the "public" tree.

Your ember-cli-build.js file can already do much of what an Ember addon can do, but if you find yourself augmenting your build process in multiple ways, it can be a good idea to move that code into separate addons so that related code is grouped, and that you can potentially reuse your build code in other Ember projects.

Good Uses for Addons

Addons can do more than just act as packages for components. Here are some other use cases where you might want to write an addon:

  • Grouping related code. (An idea I will keep emphasizing!)
  • Creating interfaces with packages and assets that don't already support Ember.
  • Pre/post processing files and code through build hooks.
  • Adding custom commands to Ember CLI. (via includedCommands hook)
  • Generating files dynamically and adding them to your build. (sitemaps and RSS feeds, for instance)
  • Serving up an HTTP API when running the Ember server.

Normal Addons

Just like when you create a new Ember app, you can use Ember CLI to create a new Ember addon from blueprints:

ember new addon my-addon

This will generate a very similar folder structure to an Ember app, but with the added addon/ directory for providing modules that are namespaced to the addon. Anything in the app/ directory inside an addon will be resolved in the host app's namespace, so if it has an app.css file, it will override the CSS file of the same name in the host app.

You can use ember g component <component-name>, just like you would with an app, and it will generate the appropriate files under addon/ and app/.

Addons are a powerful way to package up features and hook into your build process outside of ember-cli-build.js.

In-repo Addons

There's a little-known and poorly documented feature of Ember CLI called in-repo addons.

As the name suggests, an in-repo addon lives inside of an Ember app under the lib/ directory(or packages/ if you have Module Unification enabled).

By running ember generate in-repo-addon <addon name>, you can group related code into packages to simply de-clutter your app and hook into your build process.

The blueprint for an in-repo addon is more sparse than that of a normal addon in that it doesn't come with addon/, app/, test/, or ember-cli-build.js out of the box, but said files can be added either through generators or by hand.

By default, an in-repo addon will come with index.js and package.json.

Using Generators with In-repo Addons

Generating components in an in-repo addon, for instance, is slightly different than in a normal addon or app, but it's just as simple:

ember g component --in-repo <in-repo addon name> <component name>

The component will be made immediately available to your app, but it will be contained within your addon. This is an excellent way of grouping code together, especially if you are writing lots of small UI components. If there's any potential for reuse, your code should go into an in-repo addon.

Managing Dependencies

When writing an addon, you may want to include dependencies that are separate from your app's dependencies. This can be done by listing dependencies in your addon's package.json file. In order to do that, however, you must add a version: key to the package.json file. I don't know why Ember CLI doesn't just give in-repo addons a version.

While your Ember app will automatically resolve files in your in-repo addon, it will not resolve its dependencies by default because your addon isn't actually installed as an NPM package.

Once you've added a version number to your addon's package.json, just run the following:

ember install ./lib/<package name>

Now, when you add dependencies to your addon, they will be installed when someone runs npm install or yarn install on your app.

Write More [In-repo] Addons!

Addons are a great way to enforce decoupling, which is good for long-term project health. They promote sharing of code and, above all, they can allow you to do basically anything to your app builds.

In-repo addons are particularly convenient, even for doing simple things to your builds, and make it easy to namespace components. I'd love to see more Ember projects bundle code into in-repo addons.