Modify a host app's files during addon installation


by Ryan Toronto

Last week I was working on a testing addon that needed to perform some cleanup logic at the end of every acceptance test. Ember apps ship with a test helper called destroy-app that runs a block of code each time a test's application is torn down, which is exactly what I needed.

Let me show you how I updated the addon to modify this helper with some extra teardown code.


Let's start by looking at the default helper:

// tests/helpers/destroy-app.js
import Ember from 'ember';

export default function destroyApp(application) {
  Ember.run(application, 'destroy');
}

The addon I was working on exposed a cleanup() function that needed to be called from destroyApp. Initially I thought about adding some instructions to the documentation:

// To use this addon, copy and paste the following into
// test/helpers/destroy-app.js

import Ember from 'ember';
import { cleanup } from 'ember-wait-for-test-helper/wait-for';

export default function destroyApp(application) {
  cleanup();
  Ember.run(application, 'destroy');
}

This would have worked, but it's not very user friendly. If a user missed this crucial step, they could have quite a bad experience with the addon.

Then I came across an automated way to modify files using Ember CLI - just what I needed. Let's see how it works.

You're probably familiar with blueprints like ember generate template, but you might not know that every addon has an optional blueprint that runs whenever a host app installs the addon. This is called an install blueprint, and it's what we'll use to modify the destroyApp helper.

Let's create the install blueprint for the addon I was working on, ember-wait-for-test-helper. To create an install blueprint, generate a new blueprint with the same name as your addon:

$> ember generate blueprint ember-wait-for-test-helper

This creates a file called blueprints/ember-wait-for-test-helper/index.js which exports an empty object:

/*jshint node:true*/

module.exports = {
};

There's a few hooks available here that we can use to affect the host application. But first, we need to add an empty normalizeEntityName function:

module.exports = {
  normalizeEntityName: function() {
  }
};

Why do we need an empty function?

All other blueprints take an argument — for example, ember generate component date-picker or ember generate route application. The normalizeEntityName function lets you manipulate that argument for your particular use case - but by default it also exits if no argument is found. Since install blueprints run with no argument, we need to override this function so our blueprint won't exit and our hooks will actually run.

We can now work on the actual code that will add our cleanup function to the host app. To do this we'll be using the afterInstall hook.

module.exports = {
  normalizeEntityName: function() {
  },

  afterInstall: function() {
  }
};

Now, our goal is to add two lines to the host app's destroy-app.js file.

  // tests/helpers/destroy-app.js
  import Ember from 'ember';
+ import { cleanup } from 'ember-wait-for-test-helper/wait-for';

  export default function destroyApp(application) {
+   cleanup();
    Ember.run(application, 'destroy');
  }

We can do this using a built-in blueprint function called insertIntoFile. This function takes three arguments: a path for the file to change, the text to insert, and where to insert it.

Let's see what it looks like to insert the first line:

afterInstall: function() {
  var addon = this;

  addon.insertIntoFile(
    "tests/helpers/destroy-app.js",
    "import { cleanup } from 'ember-wait-for-test-helper/wait-for';",
    {
      after: "import Ember from 'ember';\n"
    }
  );
}

This will insert cleanup's import statement into destroy-app right after Ember's import statement.

insertIntoFile() returns a promise, so we'll use its callback to insert our second line. We want the second line to appear right before Ember.run(application, 'destroy'), so we'll use the before key this time:

  afterInstall: function() {
    var addon = this;

    addon.insertIntoFile(
      "tests/helpers/destroy-app.js",
      "import { cleanup } from 'ember-wait-for-test-helper/wait-for';",
      {
        after: "import Ember from 'ember';\n"
      }
+   ).then(function() {
+     return addon.insertIntoFile(
+       "tests/helpers/destroy-app.js",
+       "  cleanup();",
+       {
+         before: "  Ember.run(application, 'destroy');"
+       });
+   });
  }

Now both of our lines will be added.

As one final step, we want our hook to first check that the file its trying to modify actually exists. To do this, we'll use the existsSync node module.

Our final blueprint looks like this:

/*jshint node:true*/
var existsSync = require('exists-sync');

module.exports = {
  normalizeEntityName: function() {
  },

  afterInstall: function() {
    var addon = this;

    if (existsSync('tests/helpers/destroy-app.js')) {
      return addon.insertIntoFile(
        "tests/helpers/destroy-app.js",
        "import { cleanup } from 'ember-wait-for-test-helper/wait-for';",
        {
          after: "import Ember from 'ember';\n"
        }
      ).then(function() {
        return addon.insertIntoFile(
          "tests/helpers/destroy-app.js",
          "  cleanup();",
          {
            before: "  Ember.run(application, 'destroy');"
          });
      });
    }

  }
};

Now whenever a user installs this addon, it will automatically add its cleanup function to the destroy-app helper file.


More reading:

Questions?

Send us a tweet:

Or ask us in Inside EmberMap, our private Slack workspace for subscribers.