From f6b09ca0533a153c18e009a4fa1d08bc906b1fbf Mon Sep 17 00:00:00 2001 From: begedin Date: Tue, 14 Jul 2015 11:32:31 +0200 Subject: [PATCH 1/7] Add micro-addon model --- lib/models/micro-addon.js | 40 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 lib/models/micro-addon.js diff --git a/lib/models/micro-addon.js b/lib/models/micro-addon.js new file mode 100644 index 0000000000..a70711ffc6 --- /dev/null +++ b/lib/models/micro-addon.js @@ -0,0 +1,40 @@ +'use strict'; + +var path = require('path'); +var Funnel = require('broccoli-funnel'); +var Addon = require('../models/addon'); + +Addon.prototype.buildTree = function(sourceTree, includedFiles) { + var addon = this; + + return new Funnel(sourceTree, { + include: includedFiles, + getDestination: function(fileName) { + return addon.mapFile(fileName); + } + }); +}; + +Addon.prototype.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'); + } +}; + +Addon.prototype.treeForApp = function() { + return this.buildTree(this.root, ['component.js', 'helper.js']); +}; + +Addon.prototype.treeForTemplates = function() { + return this.buildTreew(this.root, ['style.css']); +}; + +Addon.prototype.treeForAddon = function() { + return this.buildTree(this.root, ['template.hbs']); +}; From e9fc665c774e770a54af322a1146c7b7e8635b03 Mon Sep 17 00:00:00 2001 From: begedin Date: Tue, 14 Jul 2015 11:43:40 +0200 Subject: [PATCH 2/7] Add file existance checks --- lib/models/micro-addon.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/models/micro-addon.js b/lib/models/micro-addon.js index a70711ffc6..fee7e56570 100644 --- a/lib/models/micro-addon.js +++ b/lib/models/micro-addon.js @@ -1,9 +1,16 @@ 'use strict'; var path = require('path'); +var existsSync = require('exists-sync'); var Funnel = require('broccoli-funnel'); var Addon = require('../models/addon'); +function getExistingFiles(fileList) { + return fileList.filter(function(file) { + return existsSync(file); + }); +} + Addon.prototype.buildTree = function(sourceTree, includedFiles) { var addon = this; @@ -24,17 +31,28 @@ Addon.prototype.mapFile = function(fileName) { 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'); } }; Addon.prototype.treeForApp = function() { - return this.buildTree(this.root, ['component.js', 'helper.js']); + var supportedFiles = ['component.js', 'helper.js', 'library.js']; + var includedFiles = getExistingFiles(supportedFiles); + + return this.buildTree(this.root, includedFiles); }; Addon.prototype.treeForTemplates = function() { - return this.buildTreew(this.root, ['style.css']); + var supportedFiles = ['style.css']; + var includedFiles = getExistingFiles(supportedFiles); + + return this.buildTree(this.root, includedFiles); }; Addon.prototype.treeForAddon = function() { - return this.buildTree(this.root, ['template.hbs']); + var supportedFiles = ['template.hbs']; + var includedFiles = getExistingFiles(supportedFiles); + + return this.buildTree(this.root, includedFiles); }; From b359044520db8513c218ed3b1586ec2c3ccb875f Mon Sep 17 00:00:00 2001 From: begedin Date: Tue, 14 Jul 2015 14:51:13 +0200 Subject: [PATCH 3/7] Got micro-addons to work properly --- lib/models/addons-factory.js | 4 +- lib/models/micro-addon.js | 116 ++++++++++++++++++++++++----------- 2 files changed, 82 insertions(+), 38 deletions(-) diff --git a/lib/models/addons-factory.js b/lib/models/addons-factory.js index 3cdc60e41b..8008823edc 100644 --- a/lib/models/addons-factory.js +++ b/lib/models/addons-factory.js @@ -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); @@ -44,7 +45,8 @@ AddonsFactory.prototype.initializeAddons = function(addonPackages){ graph.topsort(function (vertex) { var addonInfo = vertex.value; if (addonInfo) { - 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(); diff --git a/lib/models/micro-addon.js b/lib/models/micro-addon.js index fee7e56570..51b8c62f6e 100644 --- a/lib/models/micro-addon.js +++ b/lib/models/micro-addon.js @@ -1,58 +1,100 @@ '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 Addon = require('../models/addon'); -function getExistingFiles(fileList) { - return fileList.filter(function(file) { - return existsSync(file); +function getExistingFiles(root, fileList) { + var filteredList = fileList.filter(function(file) { + return existsSync(path.join(root,file)); }); + + return filteredList; } -Addon.prototype.buildTree = function(sourceTree, includedFiles) { - var addon = this; +var MicroAddon = Addon.extend({ + + buildTree: function(sourceTree, includedFiles) { + var addon = this; + + return new Funnel(sourceTree, { + include: includedFiles, + getDestinationPath: function(fileName) { + return addon.mapFile(fileName); + } + }); + }, - return new Funnel(sourceTree, { - include: includedFiles, - getDestination: function(fileName) { - return addon.mapFile(fileName); + 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'); } - }); -}; + }, + + treeForApp: function() { + var supportedFiles = ['component.js', 'helper.js', 'library.js']; + var includedFiles = getExistingFiles(this.root, supportedFiles); + + return this.buildTree(this.root, includedFiles); + }, + + treeForTemplates: function() { + var supportedFiles = ['template.hbs']; + var includedFiles = getExistingFiles(this.root, supportedFiles); -Addon.prototype.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'); + return this.buildTree(this.root, includedFiles); + }, + + treeForAddon: function() { + var supportedFiles = ['style.css']; + var includedFiles = getExistingFiles(this.root, supportedFiles); + + return this.buildTree(this.root, includedFiles); } -}; +}); -Addon.prototype.treeForApp = function() { - var supportedFiles = ['component.js', 'helper.js', 'library.js']; - var includedFiles = getExistingFiles(supportedFiles); +MicroAddon.lookup = function(addon) { + var Constructor, addonModule, modulePath, moduleDir; - return this.buildTree(this.root, includedFiles); -}; + modulePath = Addon.resolvePath(addon); + moduleDir = path.dirname(modulePath); -Addon.prototype.treeForTemplates = function() { - var supportedFiles = ['style.css']; - var includedFiles = getExistingFiles(supportedFiles); + if (existsSync(modulePath)) { + addonModule = require(modulePath); - return this.buildTree(this.root, includedFiles); -}; + 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)); + } + } -Addon.prototype.treeForAddon = function() { - var supportedFiles = ['template.hbs']; - var includedFiles = getExistingFiles(supportedFiles); + if (!Constructor) { + throw new SilentError('The `' + addon.pkg.name + '` addon could not be found at `' + addon.path + '`.'); + } - return this.buildTree(this.root, includedFiles); + return Constructor; }; + +module.exports = MicroAddon; From 3df2b91750c695d4d05173d9e723d9cc97fb2a8e Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Wed, 15 Jul 2015 09:09:39 +0200 Subject: [PATCH 4/7] Added proper code commenting --- lib/models/micro-addon.js | 120 +++++++++++++++++++++++++++++++++----- 1 file changed, 107 insertions(+), 13 deletions(-) diff --git a/lib/models/micro-addon.js b/lib/models/micro-addon.js index 51b8c62f6e..2dc4728498 100644 --- a/lib/models/micro-addon.js +++ b/lib/models/micro-addon.js @@ -4,13 +4,13 @@ @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 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 Addon = require('../models/addon'); +var Addon = require('../models/addon'); function getExistingFiles(root, fileList) { var filteredList = fileList.filter(function(file) { @@ -20,20 +20,66 @@ function getExistingFiles(root, fileList) { return filteredList; } +/** + 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({ - buildTree: function(sourceTree, includedFiles) { + /** + 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(sourceTree, { + return new Funnel(addon.root, { include: includedFiles, getDestinationPath: function(fileName) { - return addon.mapFile(fileName); + return addon._mapFile(fileName); } }); }, - mapFile: function(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') { @@ -47,28 +93,76 @@ var MicroAddon = Addon.extend({ } }, + /** + 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 supportedFiles = ['component.js', 'helper.js', 'library.js']; var includedFiles = getExistingFiles(this.root, supportedFiles); - return this.buildTree(this.root, includedFiles); + 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 supportedFiles = ['template.hbs']; var includedFiles = getExistingFiles(this.root, supportedFiles); - return this.buildTree(this.root, includedFiles); + 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 + + We use treeForAddon to make use of automatic style merging for any .css + files placed in the addon/styles folder + + @public + @method treeForAddon + @return {tree} A tree with properly mapped files. + */ treeForAddon: function() { var supportedFiles = ['style.css']; var includedFiles = getExistingFiles(this.root, supportedFiles); - return this.buildTree(this.root, includedFiles); + return this._buildTree(includedFiles); } }); +/** + 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; From 0b417bc225bf62a5332b7eefbe384400f36844fe Mon Sep 17 00:00:00 2001 From: begedin Date: Wed, 15 Jul 2015 12:01:35 +0200 Subject: [PATCH 5/7] Cleaned up uneccessary file existance checks. broccolli-funnel handles that internally --- lib/models/micro-addon.js | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/lib/models/micro-addon.js b/lib/models/micro-addon.js index 2dc4728498..8871d80d16 100644 --- a/lib/models/micro-addon.js +++ b/lib/models/micro-addon.js @@ -12,14 +12,6 @@ var SilentError = require('silent-error'); var Addon = require('../models/addon'); -function getExistingFiles(root, fileList) { - var filteredList = fileList.filter(function(file) { - return existsSync(path.join(root,file)); - }); - - return filteredList; -} - /** 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, @@ -104,8 +96,7 @@ var MicroAddon = Addon.extend({ @return {tree} A tree with properly mapped files. */ treeForApp: function() { - var supportedFiles = ['component.js', 'helper.js', 'library.js']; - var includedFiles = getExistingFiles(this.root, supportedFiles); + var includedFiles = ['component.js', 'helper.js', 'library.js']; return this._buildTree(includedFiles); }, @@ -124,8 +115,7 @@ var MicroAddon = Addon.extend({ @return {tree} A tree with properly mapped files. */ treeForTemplates: function() { - var supportedFiles = ['template.hbs']; - var includedFiles = getExistingFiles(this.root, supportedFiles); + var includedFiles = ['template.hbs']; return this._buildTree(includedFiles); }, @@ -144,8 +134,7 @@ var MicroAddon = Addon.extend({ @return {tree} A tree with properly mapped files. */ treeForAddon: function() { - var supportedFiles = ['style.css']; - var includedFiles = getExistingFiles(this.root, supportedFiles); + var includedFiles = ['style.css']; return this._buildTree(includedFiles); } From e578fe37710b362ef2d94a10c606c2501563b019 Mon Sep 17 00:00:00 2001 From: begedin Date: Wed, 15 Jul 2015 12:01:49 +0200 Subject: [PATCH 6/7] Added unit tests for micro-addon --- tests/unit/models/micro-addon-test.js | 107 ++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 tests/unit/models/micro-addon-test.js diff --git a/tests/unit/models/micro-addon-test.js b/tests/unit/models/micro-addon-test.js new file mode 100644 index 0000000000..d9ea1ff958 --- /dev/null +++ b/tests/unit/models/micro-addon-test.js @@ -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); + }); + }); +}); \ No newline at end of file From c9bb8c37d81ed3eb6c52f7390fd9a8c55cc0ca2d Mon Sep 17 00:00:00 2001 From: begedin Date: Thu, 16 Jul 2015 08:27:40 +0200 Subject: [PATCH 7/7] Reworked treeForAddon so libraries are importable from the addon namespace --- lib/models/micro-addon.js | 52 +++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/lib/models/micro-addon.js b/lib/models/micro-addon.js index 8871d80d16..27ce8015b8 100644 --- a/lib/models/micro-addon.js +++ b/lib/models/micro-addon.js @@ -9,6 +9,8 @@ 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'); @@ -77,7 +79,7 @@ var MicroAddon = Addon.extend({ } 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'); + 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') { @@ -96,7 +98,7 @@ var MicroAddon = Addon.extend({ @return {tree} A tree with properly mapped files. */ treeForApp: function() { - var includedFiles = ['component.js', 'helper.js', 'library.js']; + var includedFiles = ['component.js', 'helper.js']; return this._buildTree(includedFiles); }, @@ -121,22 +123,52 @@ var MicroAddon = Addon.extend({ }, /** - Maps app-related files from their location in a ember-micro-addon structure - to their proper place in an ember-addon structure. + 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. - Used by micro-components + @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); + }, - We use treeForAddon to make use of automatic style merging for any .css - files placed in the addon/styles folder + /** + 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} A tree with properly mapped files. + @return {tree} Merged and compiled output of _treeForAddonJs and + _treeForAddonStyles */ treeForAddon: function() { - var includedFiles = ['style.css']; + var addonTree = this._treeForAddonJs(); + var compiledAddonTree = this.compileAddon(addonTree); + var addonStylesTree = this._treeForAddonStyles(); + var compiledStylesTree = this.compileStyles(addonStylesTree); - return this._buildTree(includedFiles); + return mergeTrees([compiledAddonTree, compiledStylesTree]); } });