Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/models/addons-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ AddonsFactory.prototype.initializeAddons = function(addonPackages){
var project = this.project;
var graph = new DAG();
var Addon = require('../models/addon');
var MicroAddon = require('../models/micro-addon');
var addonInfo, emberAddonConfig;

debug('initializeAddons for: ', typeof addonParent.name === 'function' ? addonParent.name() : addonParent.name);
Expand All @@ -44,7 +45,8 @@ AddonsFactory.prototype.initializeAddons = function(addonPackages){
graph.topsort(function (vertex) {
var addonInfo = vertex.value;
if (addonInfo) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this should be encapsulated in the lookup method itself.

var AddonConstructor = Addon.lookup(addonInfo);
var isMicroAddon = addonInfo.pkg.keywords.indexOf('ember-micro-addon') > -1;
var AddonConstructor = isMicroAddon ? MicroAddon.lookup(addonInfo) : Addon.lookup(addonInfo);
var addon = new AddonConstructor(addonParent, project);
if (addon.initializeAddons) {
addon.initializeAddons();
Expand Down
215 changes: 215 additions & 0 deletions lib/models/micro-addon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
'use strict';

/**
@module ember-cli
*/

var path = require('path');
var existsSync = require('exists-sync');
var Funnel = require('broccoli-funnel');
var assign = require('lodash/object/assign');
var SilentError = require('silent-error');
var mergeTrees = require('broccoli-merge-trees');


var Addon = require('../models/addon');

/**
Root class for a Micro Addon. If your Addon module exports an Object, this
will be extended with that Object. If the addon module exports a constructor,
it will not be extending this vclass.

MicroAddon extends the base Addon class. The custom behavior of a Micro Addon
is implemented by defining some common hooks the Addon class exposes.

- {{#crosslink "MicroAddon/_buildTree:method"}}_buildTree{{/crosslink}}
- {{#crosslink "MicroAddon/_mapFile:method"}}_mapFile{{/crosslink}}
- {{#crosslink "MicroAddon/treeForApp:method"}}treeForApp{{/crosslink}}
- {{#crosslink "MicroAddon/treeForAddon:method"}}treeForAddon{{/crosslink}}
- {{#crosslink "MicroAddon/treeForTemplate:method"}}treeForTemplate{{/crosslink}}

@class MicroAddon
@extends Addon
@param {(Project|Addon)} parent The project or addon that directly depends on this addon
@param {Project} project The current project (deprecated)
*/
var MicroAddon = Addon.extend({

/**
Builds a tree out of an explicit array of files

@private
@method _buildTree
@param {Array} includedFiles Array of filenames to build a tree from. All files are in addon root
@return {tree} Newly built tree
*/
_buildTree: function(includedFiles) {
var addon = this;

return new Funnel(addon.root, {
include: includedFiles,
getDestinationPath: function(fileName) {
return addon._mapFile(fileName);
}
});
},

/**
Maps a source file (placed in addon root) to a destination file

Component mappings:
- component.js -> components/addon-name.js
- template.hbs -> templates/components/addon-name.hbs
- style.css -> addon/styles/addon-name.css

Helper mappings:
- helper.js -> helpers/addon-name.js

Library mappings:
- library.js -> lib/addon-name.js

@private
@method _mapFile
@param {String} fileName Based file name
@return {String} Mapped file path
*/
_mapFile: function(fileName) {
if (fileName === 'component.js') {
return path.join('components', this.name + '.js');
} else if (fileName === 'template.hbs') {
return path.join('components', this.name + '.hbs');
} else if (fileName === 'style.css') {
return path.join('addon', 'styles', this.name + '.css');
} else if (fileName === 'helper.js') {
return path.join('helpers', this.name + '.js');
} else if (fileName === 'library.js') {
return path.join('lib', this.name + '.js');
}
},

/**
Maps app-related files from their location in a ember-micro-addon structure
to their proper place in an ember-addon structure.

Used by micro-components, micro-helpers and micro-libraries

@public
@method treeForApp
@return {tree} A tree with properly mapped files.
*/
treeForApp: function() {
var includedFiles = ['component.js', 'helper.js'];

return this._buildTree(includedFiles);
},

/**
Maps app-related files from their location in a ember-micro-addon structure
to their proper place in an ember-addon structure.

Used by micro-components

treeForTemplates maps to the templates subfolder automatically, so only the
components subfolder is necessary in the mapped path.

@public
@method treeForTemplates
@return {tree} A tree with properly mapped files.
*/
treeForTemplates: function() {
var includedFiles = ['template.hbs'];

return this._buildTree(includedFiles);
},

/**
Maps style.css to addon/styles/[addon-name].css. At that point, treeForAddon
followed by the regular build process take over and style.css eventually
ends up being merged into the app's vendor.css.

@private
@method treeForAddonStyles
@return {tree} A tree with properly mapped files.
*/
_treeForAddonStyles: function() {
var includedFiles = ['style.css'];

return this._buildTree(includedFiles);
},

/**
Maps library.js to lib/library.js and outputs it to the addon folder. At
that point, treeForAddon and the regular build process take over and
library.js eventually becomes importable from
'[addon-name]/lib/[addon-name]'

@private
@method treeForAddonJs
@return {tree} A tree with properly mapped files.
*/
_treeForAddonJs: function() {
var includedFiles = ['library.js'];

return this._buildTree(includedFiles);
},

/**
Generates an addon tree and an addon style tree using treeForAddonJs and
treeForAddonStyles, then merges them and returns the result.

@public
@method treeForAddon
@return {tree} Merged and compiled output of _treeForAddonJs and
_treeForAddonStyles
*/
treeForAddon: function() {
var addonTree = this._treeForAddonJs();
var compiledAddonTree = this.compileAddon(addonTree);
var addonStylesTree = this._treeForAddonStyles();
var compiledStylesTree = this.compileStyles(addonStylesTree);

return mergeTrees([compiledAddonTree, compiledStylesTree]);
}
});

/**
Returns the micro-addon class for a given addon name.
If the MicroAddon exports a function, that function is used
as constructor. If an Object is exported, a subclass of
`MicroAddon` is returned with the exported hash merged into it.

@private
@static
@method lookup
@param {String} addon MicroAddon name
@return {MicroAddon} MicroAddon class
*/
MicroAddon.lookup = function(addon) {
var Constructor, addonModule, modulePath, moduleDir;

modulePath = Addon.resolvePath(addon);
moduleDir = path.dirname(modulePath);

if (existsSync(modulePath)) {
addonModule = require(modulePath);

if (typeof addonModule === 'function') {
Constructor = addonModule;
Constructor.prototype.root = Constructor.prototype.root || moduleDir;
Constructor.prototype.pkg = Constructor.prototype.pkg || addon.pkg;
} else {
Constructor = MicroAddon.extend(assign({
root: moduleDir,
pkg: addon.pkg
}, addonModule));
}
}

if (!Constructor) {
throw new SilentError('The `' + addon.pkg.name + '` addon could not be found at `' + addon.path + '`.');
}

return Constructor;
};

module.exports = MicroAddon;
107 changes: 107 additions & 0 deletions tests/unit/models/micro-addon-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
'use strict';

var path = require('path');
var Project = require('../../../lib/models/project');
var MicroAddon = require('../../../lib/models/micro-addon');
var expect = require('chai').expect;
var path = require('path');

var fixturePath = path.resolve(__dirname, '../../fixtures/addon');

describe('models/addon.js', function() {
var project, projectPath;

describe('treePaths and treeForMethods', function() {
var ExampleMicroAddon;

beforeEach(function() {
projectPath = path.resolve(fixturePath, 'simple');
var packageContents = require(path.join(projectPath, 'package.json'));

project = new Project(projectPath, packageContents);

ExampleMicroAddon = MicroAddon.extend({
name: 'example',
root: projectPath,
});
});

describe('treeForApp', function() {
it('exists even when not explicitly set', function() {
var first = new ExampleMicroAddon(project);

expect(first.treeForApp).to.be.a('Function');
});
});

describe('treeForAddon', function() {
it('exists even when not explicitly set', function() {
var first = new ExampleMicroAddon(project);

expect(first.treeForAddon).to.be.a('Function');
});
});

describe('treeForTemplates', function() {
it('exists even when not explicitly set', function() {
var first = new ExampleMicroAddon(project);

expect(first.treeForTemplates).to.be.a('Function');
});
});
});

describe('_buildTree', function() {
var ExampleMicroAddon;

beforeEach(function() {
projectPath = path.resolve(fixturePath, 'simple');
var packageContents = require(path.join(projectPath, 'package.json'));

project = new Project(projectPath, packageContents);

ExampleMicroAddon = MicroAddon.extend({
name: 'example',
root: projectPath,
});
});

it('should return a tree', function() {
var addon = new ExampleMicroAddon();

var tree = addon._buildTree(['component.js']);

expect(tree).to.contain.all.keys('inputTree', 'include', 'destDir', 'getDestinationPath');
});
});

describe('_mapFile', function() {
var ExampleMicroAddon, addon;

beforeEach(function() {
projectPath = path.resolve(fixturePath, 'simple');
var packageContents = require(path.join(projectPath, 'package.json'));

project = new Project(projectPath, packageContents);

ExampleMicroAddon = MicroAddon.extend({
name: 'example',
root: projectPath,
});

addon = new ExampleMicroAddon();
});

it('should perform the proper mapping', function() {
expect(addon._mapFile('component.js')).to.equal('components/example.js');
expect(addon._mapFile('helper.js')).to.equal('helpers/example.js');
expect(addon._mapFile('library.js')).to.equal('lib/example.js');
expect(addon._mapFile('template.hbs')).to.equal('components/example.hbs');
expect(addon._mapFile('style.css')).to.equal('addon/styles/example.css');
});

it('should return "undefined" for unsupported file', function() {
expect(addon._mapFile('random.ext')).to.equal(undefined);
});
});
});