layout | title | permalink | category | github |
---|---|---|---|---|
post |
Developing Addons and Blueprints |
developing-addons-and-blueprints |
extending |
Addons make it possible to easily share common code between applications. However, if an addon only covers a very project specific use-case, an In-Repo-Addon could be considered instead.
This guide will walk through the development cycle of a fictional
addon ember-cli-x-button
.
An addon can be installed with the install
command:
ember install <package name>
To install the (fictional) x-button addon package:
ember install ember-cli-x-button
Ember CLI will detect the presence of an addon by inspecting each of
your applications dependencies and search their package.json
files
for the presence of ember-addon
in the keywords
section (see
below).
{% highlight javascript %} "keywords": [ "ember-addon", ... ] {% endhighlight %}
The Ember CLI addons API currently supports the following scenarios:
- Performing operations on the
EmberApp
created in the consuming application'sember-cli-build.js
- Adding preprocessors to the default registry
- Providing a custom application tree to be merged with the consuming application
- Providing custom express (server) middlewares
- Adding custom/extra blueprints, typically for scaffolding application/project files
- Adding content to consuming applications
- Adding content to the consuming application's tests directory (via
test-support/
)
Ember CLI has an addon command with some options:
ember addon <addon-name> <options...>
Note: An addon can NOT be created inside an existing application.
To create a basic addon: ember addon <addon-name>
Running this command should generate something like the following:
{% highlight bash %} ember addon ember-cli-x-button version x.y.zz installing create .bowerrc create .editorconfig create tests/dummy/.eslintrc.js ... create index.js
Installing packages for tooling via npm Installed browser packages via Bower. {% endhighlight %}
The addon infrastructure is based on "convention over configuration" in accordance with the Ember philosophy. You are encouraged to follow these conventions to make it easier on yourself and for others to better understand your code. The same applies for addon blueprints.
The addon project created follows these structure conventions:
app/
- merged with the application's namespace.addon/
- part of the addon's namespace.blueprints/
- contains any blueprints that come with the addon, each in a separate directorypublic/
- static files which will be available in the application as/your-addon/*
test-support/
- merged with the application'stests/
under the application's namespace.addon-test-support/
- merged with the application'stests/
under the addon's namespace at${addon-name}/test-support/
.tests/
- test infrastructure including a "dummy" app and acceptance test helpers.vendor/
- vendor specific files, such as stylesheets, fonts, external libs etc.ember-cli-build.js
- Compilation configurationpackage.json
- Node meta-data, dependencies etc.index.js
- main Node entry point (as per npm conventions)
The generated addon package.json
file looks something like this:
{% highlight javascript %} { "name": "ember-cli-x-button", // addon name "version": "0.0.1", // version of addon "directories": { "doc": "doc", "test": "test" }, "scripts": { "start": "ember server", "build": "ember build", "test": "ember test" }, "repository": "https://github.com/repo-user/my-addon", "engines": { "node": ">= 0.10.0" }, "keywords": [ "ember-addon" // add more keywords to better categorize the addon ], "ember-addon": { // addon configuration properties "configPath": "tests/dummy/config" }, "author": "", // your name "license": "MIT", // license "devDependencies": { "body-parser": "^1.2.0", ... // add specific dev dependencies here! } } {% endhighlight %}
Let's add some meta data to categorize the addon a little better:
{% highlight javascript %} "keywords": [ "ember-addon", "x-button", "button" ] {% endhighlight %}
If your addon depends on another addon, install it as a dependency in your
package.json
:
{% highlight javascript %} // ... "dependencies": { "ember-ajax": "^0.7.1" } {% endhighlight %}
An application that consumes your addon will automatically install and bundle dependencies specified this way.
An addon will leverage the npm conventions, and look for an index.js
as the
entry point unless another entry point is specified via the "main"
property
in the package.json
file. You are encouraged to use index.js
as the addon
entry point.
The generated index.js
is a simple JavaScript Object (POJO) that you can
customize and expand as you see fit.
{% highlight javascript %} // index.js module.exports = { name: 'ember-cli-x-button' }; {% endhighlight %}
During the build process, the included
hook on your addon will be called,
allowing you to perform setup logic or modify the app or addon:
{% highlight javascript %} // index.js module.exports = { name: 'ember-cli-x-button', included(app, parentAddon) { this._super.included.apply(this, arguments); let target = (parentAddon || app); // Now you can modify the app / parentAddon. For example, if you wanted // to include a custom preprocessor, you could add it to the target's // registry: // // target.registry.add('js', myPreprocessor); } }; {% endhighlight %}
The exported object extends the Addon
class. So any hooks that exist on the
Addon
class may be overridden by addon author.
By default, the "ember-addon"
hash in the package.json
file has the
"configPath"
property defined to point to the config
directory of the test
dummy application.
Optionally, you may specify whether your ember-addon
must run "before"
or
"after"
any other Ember CLI addons. Both of these properties can take either
a string or an array of strings, where the string is the name of the another
Ember CLI addon, as defined in the package.json
of the other addon.
Optionally, you may specify a different name for the "defaultBlueprint"
. It
defaults to the name in the package.json
. This blueprint will be run
automatically when your addon is installed with the ember install
command.
Optionally, you may specify the "demoURL"
property with the fully qualified
URL of a website showing your addon in action. Sites like Ember
Addons and Ember
Observer will display a link to "demoURL"
.
{% highlight javascript %} "ember-addon": { // addon configuration properties "configPath": "tests/dummy/config", "before": "single-addon", "defaultBlueprint": "blueprint-that-isnt-package-name", "demoURL": "http://example.com/ember-addon/demo.html", "after": [ "after-addon-1", "after-addon-2" ] } {% endhighlight %}
The addon's ember-cli-build.js
is only used to configure the dummy application found in
tests/dummy/
. It is never referenced by applications which include the addon.
If you need to use ember-cli-build.js
, you may have to specify paths relative to the
addon root directory. For example to configure
ember-cli-less to use app.less
in the dummy app:
{% highlight javascript %} // ember-cli-build.js const EmberAddon = require('ember-cli/lib/broccoli/ember-addon');
let app = new EmberAddon({ lessOptions: { paths: ['tests/dummy/app/styles/'], outputFile: 'dummy.css' } });
module.exports = app.toTree(); {% endhighlight %}
To create a new component: ember g component x-button
The actual code for the addon goes in addon/components/x-button.js
{% highlight javascript %} import Component from '@ember/component'; import { get } from '@ember/object';
export default Component.extend({ tagName: 'button',
didInsertElement() { this._super(...arguments); this.setupXbutton(); },
willDestroyElement() { this._super(...arguments); this.teardownXbutton(); },
setupXbutton() { // ... },
teardownXbutton() { get(this, 'x-button').destroy(); } }); {% endhighlight %}
In order to allow the consuming application to use the addon component in a template
directly you need to bridge the component via your addon's app/components
directory.
Just re-export your component:
{% highlight javascript %} // app/components/x-button.js export { default } from 'ember-cli-x-button/components/x-button';
{% endhighlight %}
This setup allows others to modify the component by extending it while making
the component available in the consuming applications namespace. This means
anyone who installs your x-button
addon can start using the component in their
templates with {% raw %}{{x-button}}{% endraw %}
without any extra configuration.
A blueprint with the same name as the addon (unless explicitly changed, see above) will be automatically run after install (in development, it must be manually run after linking). This is where you can tie your addon's bower dependencies into the client app so that they actually get installed.
To create the blueprint, add the file blueprints/ember-cli-x-button/index.js
.
This follows the usual Ember blueprints naming conventions.
{% highlight javascript %} // blueprints/ember-cli-x-button/index.js module.exports = { normalizeEntityName() {}, // no-op since we're just adding dependencies
afterInstall() { return this.addBowerPackageToProject('x-button'); // is a promise } }; {% endhighlight %}
You can pull in addons, npm packages, and bower packages, in your blueprint, by using various hooks provided to you. They are as follows:
addAddon(s)ToProject
Adds an addon to the project's package.json and runs its defaultBlueprint if it provides one.addBowerPackage(s)ToProject
Adds a package to the project'sbower.json
.addPackage(s)ToProject
Adds a package to the project'spackage.json
.
Each of these returns a promise, so it is all thenable. The following is an example of each of these: {% highlight javascript %} // blueprints/ember-cli-x-button/index.js module.exports = { normalizeEntityName() {}, // no-op since we're just adding dependencies
afterInstall() { // Add addons to package.json and run defaultBlueprint return this.addAddonsToProject({ // a packages array defines the addons to install packages: [ // name is the addon name, and target (optional) is the version {name: 'ember-cli-code-coverage', target: '0.3.9'}, {name: 'ember-cli-sass'} ] }) .then(() => { // Add npm packages to package.json return this.addPackagesToProject([ {name: 'babel-eslint'}, {name: 'eslint-plugin-ship-shape'} ]); }) .then(() => { return this.addBowerPackagesToProject([ {name: 'bootstrap', target: '3.0.0'} ]); }); } }; {% endhighlight %}
As stated earlier the included
hook on your addon's main entry point is run during
the build process. This is where you want to add import
statements to actually
bring in the dependency files for inclusion. Note that this is a separate step from
adding the actual dependency itself---done in the default blueprint---which merely
makes the dependency available for inclusion.
{% highlight javascript %} // index.js module.exports = { name: 'ember-cli-x-button',
included(app) { this._super.included.apply(this, arguments);
app.import(app.bowerDirectory + '/x-button/dist/js/x-button.js');
app.import(app.bowerDirectory + '/x-button/dist/css/x-button.css');
} }; {% endhighlight %}
In the example file, the included
hook is used. This hook is called
by the EmberApp
constructor and gives access to the consuming
application as app
. When the consuming application's ember-cli-build.js
is processed by Ember CLI to build/serve, the addon's included
function is called passing the EmberApp
instance.
To import static files such as images or fonts in the application
include them in /public
. The consuming application will have access
to them via a directory with your addon's name.
For example, to add an image, save it in /public/images/foo.png
. Then
from the consuming application access it as:
{% highlight css %} .foo {background: url("/your-addon/images/foo.png");} {% endhighlight %}
If you want to add content to a page directly, you can use the content-for
tag. An example of this is {% raw %}{{content-for 'head'}}{% endraw %}
in
app/index.html
, which Ember CLI uses to insert it's own content at build
time. Addons can access the contentFor
hook to insert their own content.
{% highlight javascript %} // index.js module.exports = { name: 'ember-cli-display-environment',
contentFor(type, config) { if (type === 'environment') { return '
'; } } }; {% endhighlight %}This will insert the current environment the app is running under wherever
{% raw %}{{content-for 'environment'}}{% endraw %}
is placed. The contentFor
function will be called for each {% raw %}{{content-for}}{% endraw %}
tag in
index.html
.
Every addon is sent an instance of the parent application's command line output
stream. If you want to write out to the command line in your addon's index.js
file, you should use this.ui.writeLine
rather than console.log
. This will
make the output obey the --silent
flag available with many ember-cli
commands.
{% highlight javascript %} // index.js module.exports = { name: 'ember-cli-command-line-output',
included(app) { this._super.included.apply(this, arguments); this.ui.writeLine('Including external files!'); } } {% endhighlight %}
If you want to go beyond the built in customizations or want/need more
advanced control in general, the following are some of the hooks
(keys) available for your addon Object in the index.js
file. All
hooks expect a function as the value.
{% highlight javascript %} includedCommands: function() {}, blueprintsPath: // return path as String preBuild: postBuild: treeFor: contentFor: included: postprocessTree: serverMiddleware: lintTree: {% endhighlight %}
Documentation for the addon hooks is available here.
An example of advanced customization can be found here and for server middleware here.
The addon project contains a /tests
directory which contains the
necessary infrastructure to run and configure tests for the addon. The
/tests
directory has the following structure:
{% highlight bash %} dummy/ helpers/ unit/ index.html test-helper.js {% endhighlight %}
The /dummy
directory contains the basic layout of a dummy app to be
used for to host your addon for testing. The /helpers
directory
contains various QUnit helpers that are provided and those you
define yourself in order to keep your tests concise. The /unit
directory should contain your unit tests that test your addon in various
usage scenarios. To add integration (acceptance) tests add an
`integration/' directory.
test-helper.js
is the main helper file that you should reference
from any of your unit test files. It imports the resolver
helper
found in /helpers
used to resolve pages in the dummy
app.
index.html
contains the test page that you can load in a browser to
display the results of running your integration tests.
The following is an example of a simple QUnit unit test,
placed in tests/unit/components
.
{% highlight javascript %} // tests/unit/components/button-test.js
import { test, moduleForComponent } from 'ember-qunit'; import startApp from '../../helpers/start-app'; import { run } from '@ember/runloop';
let App;
moduleForComponent('x-button', 'XButtonComponent', { beforeEach() { App = startApp(); }, afterEach() { run(App, 'destroy'); } });
test('is a button tag', function(assert) { assert.equal('BUTTON', this.$().prop('tagName'));
this.subject().teardownXButton(); });
// more tests follow... {% endhighlight %}
For how to run and configure tests, see the Ember CLI Testing section.
You can generate most of ember-cli's built-in blueprints into your
tests/dummy/app
directory to speed up building a dummy app to use for
testing. Any blueprint that
generates into an /app
directory is currently supported:
ember g <blueprint-name> <name> --dummy
For instance:
ember g component taco-button --dummy
Will generate:
{% highlight bash %} tests/ dummy/ app/ components/ taco-button.js templates/ components/ taco-button.hbs {% endhighlight %}
Note that in the above example, the addon-import and component-test were not
generated. The --dummy
option generates the blueprint as if you were in a
non-addon project.
You can also create your own blueprints that can generate into the dummy
directory. The primary requirement is that the blueprint contains a __root__
token in the files directory path.
{% highlight bash %} blueprints/ x-button/ index.js files/ root/ <-- normally "app/" components/ name/ {% endhighlight %}
A blueprint is a bundle of template files with optional installation logic. It is used to scaffold (generate) specific application files based on some arguments and options. For more details see generators-and-blueprints). An addon can have one or more blueprints.
To generate a blueprint for your addon:
ember generate blueprint <blueprint-name>
By convention, the main blueprint of the addon should have the same name as the addon itself:
ember g blueprint <addon-name>
In our example:
ember g blueprint x-button
This will generate a directory blueprints/x-button
for the addon where
you can define your logic and templates for the blueprint. You can
define multiple blueprints for a single addon. The last loaded
blueprint wins with respect to overriding existing (same name)
blueprints that come with Ember or other addons (according to package
load order.)
Blueprints are expected to be located under the blueprints
directory in
the addon root, just like blueprints overrides in your project root.
If you have your blueprints in another directory in your addon, you need
to tell ember-cli where to find them by specifying a blueprintsPath
property for the addon (see advanced customization section below).
If you are familiar with Yeoman (or Rails) generators, blueprints follow very similar conventions and structure.
To dive deeper into blueprints design, please see the Ember CLI blueprints where you get a feeling for the blueprints API.
{% highlight bash %} blueprints/ x-button/ index.js files/ app/ components/ name/ {% endhighlight %}
Note that the special file or directory called __name__
will create a
file/directory at that location in your app with the __name__
replaced
by the first argument (name) you pass to the blueprint being
generated.
ember g x-button my-button
Will thus generate a directory app/components/my-button
in the
application where the blueprint generator is run.
While you are developing and testing, you can run npm link
from the
root of your addon project. This will make your addon locally
available by name.
Then run npm link <addon-name>
in any hosting application project
root to make a link to your addon in your node_modules
directory, and
add the addon to the package.json
. Any change in your addon will now
directly take effect in any project that links to it this way.
Remember that npm link
will not run the default blueprint in the same way that
install
will, so you will have to do that manually via ember g
.
For live reload when developing an addon use the isDevelopingAddon
hook:
{% highlight javascript %} // addon index.js isDevelopingAddon() { return true; } {% endhighlight %}
While testing an addon using npm link, you need an entry in package.json
with
your addon name, with any valid npm version: "<addon-name>":"version"
. Our
fictional example would require "ember-cli-x-button": "*"
. You can now run ember g <addon-name>
in your project.
Use npm and git to publish the addon like a normal npm package.
{% highlight bash %} npm version 0.0.1 git push origin master --follow-tags npm publish {% endhighlight %}
You can upload your addon code to a private git repository and call ember install
with a valid git URL
as the version.
If you are using bitbucket.org the URL formats can be found here.
When using the git+ssh
format, the ember install
command will require there to
be an available ssh key with read access to the repository. This can be tested
by running git clone ssh://git@github.com:user/project.git
.
When using the git+https
format, the ember install
command will ask you for
the account password.
In order to use the addon from your hosting application:
To install your addon from the npmjs.org repository:
ember install <your-addon-name-here>
.
For our x-button sample addon:
ember install ember-cli-x-button my-button
.
This will first install the x-button addon from npm. Then, because we have a blueprint with the same name as our addon, it will run the blueprint automatically with the passed in arguments.
You can update an addon the same way you update an Ember app by
running ember init
in your project root.
For a good walkthrough of the (recent) development of a real world addon, take a look at: Creating a DatePicker Ember CLI addon
Addons specific to your project can be created inside your repo and are
generated in the projects lib
directory in a folder with the name of
the in-repo-addon, e.g. /lib/in-repo-addon-name
and follow the same
file structure as a normal addon.
Some advantages of using an in-repo-addon, instead of an addon outside of your application (repo), are:
- Sandbox for developing a feature that you may want to share as an addon in the future
- Having all the benefits of addon isolation but without the need to
publish or
npm link
Use in-repo-addon
argument with the ember generate
command:
{% highlight bash %} ember generate in-repo-addon in-repo-addon-name {% endhighlight %}
(Replace in-repo-addon-name
with the name of your addon.)
For your in-repo-addon stylesheet, name the file addon.css
and place
it in the styles directory, e.g /lib/in-repo-addon-name/addon/styles/addon.css
This avoids any conflict with the parent application's app.css
file
Likewise if your Ember CLI application uses .less
or .scss
, use the
appropriate file extension for your addon stylesheet file.
In order to complile HTMLBars templates that are part of your in-repo-addon,
your package.json
file will need to include following dependencies:
babel-plugin-htmlbars-inline-precompile
ember-cli-babel
ember-cli-htmlbars
ember-cli-htmlbars-inline-precompile
(Use the same versions found in your Ember CLI Application's package.json
)
To ensure that you can use babel.js and related polyfills with your in-repo-addon
add babel options to the included
hook of the in-repo-addon index.js
:
{% highlight javascript %} module.exports = { name: 'in-repo-addon-name',
isDevelopingAddon() { return true; },
included(app, parentAddon) { let target = (parentAddon || app); target.options = target.options || {}; target.options.babel = target.options.babel || { includePolyfill: true }; return this._super.included.apply(this, arguments); } }; {% endhighlight %}
To generate a blueprint for your in-repo-addon:
{% highlight bash %} ember generate blueprint --in-repo-addon {% endhighlight %}
When generating a blueprint, a shorthand argument -ir
or -in-repo
can be
used in place of --in-repo-addon
.