Skip to content

Commit

Permalink
Merge pull request #21 from AppGeo/better-addon-support
Browse files Browse the repository at this point in the history
adds robust addon support
  • Loading branch information
CNDW committed Mar 19, 2016
2 parents 1a2a971 + cee0130 commit 0ceb0ea
Show file tree
Hide file tree
Showing 17 changed files with 651 additions and 28 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ npm-debug.log
*~
coverage/
/vendor/
/emberate-addons/
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
language: node_js
node_js:
- "5.1"
- "4.0"
- "0.11"
- "0.10"
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ Emberate exports a function with the following signature: `emberate(path, option
- loaderPath - require path to the module loader used to connect commonjs modules with Ember's module system, defaults to a modified version of [ember-cli's loader](https://github.com/ember-cli/loader.js) - *advanced option, only override if needed*
- resolverPath - require path for a custom Resolver. Defaults to the most current version of [ember-cli's resolver](https://github.com/ember-cli/ember-resolver) - *advanced option, only override if needed*
- debugAdapterPath - require path for a custom debug Adapter, defaults to the current version included with [ember-cli's resolver](https://github.com/ember-cli/ember-resolver).
- addonPath - `emberate-addons` by default, the path that ember addons will be installed into.
- addonSupport - `false` by default, set to true to enable addon support
* __callback__ - optional, returns once done writing, if used _outPath_ option above.

The callback is only fired if you specify `outPath` in the options hash, e.g.
Expand Down Expand Up @@ -137,6 +139,16 @@ For ease of use with npm scripts and for quick testing.
-p, --pod-module-prefix [pod-module-prefix] Pod Module Prefix, the directroy that the ember-resolver uses for pods
```
### Ember Addons
There is basic support for ember-addons. You should be able to npm install the addon, and assuming the published addon conforms to ember's addon standard, emberate will include the addon in your app bundle entry file.
You can optionally exclude the addon support from your build by setting `addonSupport` to false in the emberate options. Since Ember Addons are normally written in es6, you will need to include a transpiler in your browserify bundle. The most simple way to do so is with the [babelify transform](https://github.com/babel/babelify).
Addon support is stricty tied to the addon itself, and if the addon is installing dependencies via blueprints, you will have to manually add the dependency to your project. This includes any css assets, preprocessors, or vendor libs that would normally be included in the ember-cli build pipeline.
Addon support is in very early stages, and will be further fleshed out as edge case issues are discovered. Please create an issue if you find an addon that does not work so that we can continue to find edge case issues and solve for them.
### Via Grunt
```js
Expand Down
43 changes: 43 additions & 0 deletions lib/addonResolver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict';

var fs = require('fs');
var path = require('path');
var readJson = require('read-json-sync');
var CombinedStream = require('combined-stream');
var spider = require('spider-stream');

module.exports = function(options) {
var stream = CombinedStream.create();
var moduleList = fs.readdirSync('node_modules');
moduleList.forEach(function (moduleName) {
var modulePath = path.resolve(path.join('node_modules', moduleName));
try {
var data = readJson(path.join(modulePath, 'package.json'));
} catch (err) {
return;
}
if (isAddon(data)) {
options.addonList.push(moduleName);
var addonPath = path.resolve(path.join(modulePath, 'addon'));
crawlPath(stream, addonPath);
var addonAppPath = path.resolve(path.join(modulePath, 'app'));
crawlPath(stream, addonAppPath);
}
});
return stream;
}

function isAddon(data) {
try {
return data.keywords.indexOf('ember-addon') !== -1;
} catch (err) {
return false;
}
}

function crawlPath(returnStream, path) {
try {
fs.accessSync(path, fs.F_OK);
returnStream.append(spider(path));
} catch (e) { /* noop */ }
}
2 changes: 1 addition & 1 deletion lib/defaultTemplate.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ define("{{modulePrefix}}/app", ['exports', 'ember/resolver', 'ember/load-initia
exports['default'] = App;
});
{{#each modules}}
define("{{../modulePrefix}}/{{name}}", ["exports"], function(exports) {
define("{{name}}", ["exports"], function(exports) {
exports["default"] = es6RequireShim(require('{{path}}'));
});
{{/each}}
Expand Down
30 changes: 26 additions & 4 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ var spider = require('spider-stream');
var fileStream = require('./util/fileStream');
var nameStream = require('./util/nameStream');
var templateStream = require('./util/templateStream');
var addonResolver = require('./addonResolver');

module.exports = function (rootPath, userOptions, templatePath) {
var defaultPath = path.join(__dirname, 'defaultTemplate.hbs');
var defaultOptions = {
appName: 'App',
templatePath: defaultPath,
addonPath: 'emberate-addons',
addonSupport: false,
modulePrefix: 'app',
debug: true,
debugAdapterPath: 'emberate/vendor/container-debug-adapter.js',
Expand All @@ -28,12 +31,31 @@ module.exports = function (rootPath, userOptions, templatePath) {
callback = arguments[arguments.length - 1];
}

rootPath = rootPath || process.cwd();
options.rootPath = rootPath || process.cwd();
options.addonList = [];

var out = spider(rootPath)
.pipe(fileStream(rootPath))
.pipe(nameStream())
/* istanbul ignore next */
function logError(error) {
console.log(error.message);
console.log(error.stack);
console.trace(error);
this.emit('end');
}

var out;
if (options.addonSupport) {
out = addonResolver(options)
.on('error', logError)
.append(spider(options.rootPath))
.pipe(fileStream(options))
.pipe(nameStream(options))
.pipe(templateStream(options));
} else {
out = spider(options.rootPath)
.pipe(fileStream(options))
.pipe(nameStream(options))
.pipe(templateStream(options));
}

if (callback) {
out.pipe(fs.createWriteStream(options.outPath))
Expand Down
96 changes: 96 additions & 0 deletions lib/util/copyAddonFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use strict';
/**
* Ember addons are often written in a way that is incompatible
* with node's require specification. In ember-cli, the modules
* are transpiled on a per file basis and wrapped in an AMD module
* loader. The contents of the 'addon' directory are registered in
* the module loader as `<%addon name%>/<%file type%>/<%file name%>`
* and the contents of the 'app' directory are registered in the module
* loader as `<%app name%>/<%file type%>/<%file name%>`.
*
* The addon's `app` files usually require the addon's `addon` files by
* the name that they will be registered under, and not the 1 to 1 file
* to module mapping that node uses for module requires.
*
* The require statements are expecting to be using the require defined
* by Ember's module loader, so at application runtime, these paths are
* correct. But for browserify and node, these paths are broken because
* the files are actually located in
* `<%addon name%>/<%(app|addon)%>/<%file type%>/<%file name%>`
*
* This, coupled with the way that browserify handles applying transforms
* to files located in `node_modules`, makes the addons incompatable with
* browserify and node. This function acts as a translator by copying the
* addon files that are going to be included into an application directory
* and transpiling the broken require statements to work with the new file
* location and the 1 to 1 mapping that node expects.
*/
var path = require('path');
var fs = require('fs');
var mkdirp = require('mkdirp');

var espree = require('espree');
var astring = require('astring');

var nodeFolder = path.resolve('node_modules');
module.exports = function copyAddonFile(file, options, cb) {
function isAddonImport(importString) {
return options.addonList.indexOf(nameSpaceFrom(importString)) !== -1;
}
var newFile = file.replace(nodeFolder, path.resolve(options.addonPath));
fs.readFile(file, function(err, data) {
var text = data.toString();
// make sure the file is not a template
if (/\.js$/.test(file)) {
// parse the file for static analysis
var AST = parseData(text);
for (var node in AST.body) {
// For now, assume all ember addons are written in es6, this will only look for 'import' statements,
// and match on import statements that are trying to import addon modules
if (AST.body[node].type === "ImportDeclaration" && isAddonImport(AST.body[node].source.value)) {
// transpile the import statement to a node compatable format
var namespace = nameSpaceFrom(AST.body[node].source.value);
// For now only expect the addons to reference <%namespace%>/addon modules
var namespacedImportDir = path.join(path.resolve(options.addonPath), namespace, 'addon');
// resolve the new location of the file
var relativeNamespacedDir = path.relative(path.dirname(newFile), namespacedImportDir);
var newImportValue = AST.body[node].source.value.replace(namespace, relativeNamespacedDir);
AST.body[node].source.value = newImportValue;
AST.body[node].source.raw = "'"+newImportValue+"'";
}
}
// recompile the file
text = astring(AST, {
indent: ' '
});
}
mkdirp(path.dirname(newFile), function (err) {
/* istanbul ignore next */
if (err) {
console.log('error creating directory for file: '+newFile);
console.log(err.stack);
return cb(newFile);
}
fs.writeFile(newFile, text, function(err) {
/* istanbul ignore next */
if (err) {
console.log('error writing file: '+newFile);
console.log(err.stack);
}
cb(newFile);
});
});
});
}

function nameSpaceFrom(file) {
file = file.replace(nodeFolder+'/', '');
return file.split('/')[0];
}

function parseData(text) {
return espree.parse(text, {
ecmaVersion: 6,
sourceType: "module"
});
}
25 changes: 19 additions & 6 deletions lib/util/fileStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,30 @@
var through = require('through2');
var path = require('path');
var isModulable = require('./isModulable');
var copyAddonFile = require('./copyAddonFile');

module.exports = function(base) {
var fullBase = path.resolve(base);
module.exports = function(options) {
var nodeFolder = path.resolve('node_modules');
var fullBase = path.resolve(options.rootPath);

return through.obj(function(chunk, _, next) {
var relativePath = path.relative(fullBase, chunk);

if (isModulable(relativePath)) {
if (chunk.indexOf(nodeFolder) !== -1) {
// we don't want blueprints
if (!/\/blueprints\//i.test(chunk) && !/\/styles\//i.test(chunk)) {
var self = this;
copyAddonFile(chunk, options, function(newPath) {
self.push('ember-addon:'+newPath);
next();
});
} else {
next();
}
} else if (isModulable(relativePath)) {
this.push(relativePath);
next();
} else {
next();
}

next();
});
};
3 changes: 3 additions & 0 deletions lib/util/isAddon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = function isAddon(path) {
return /^ember-addon\:/i.test(path);
}
12 changes: 6 additions & 6 deletions lib/util/nameStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ var through = require('through2');
var normalizeName = require('./normalizeName');
var normalizePath = require('./normalizePath');

module.exports = function() {
module.exports = function(options) {
return through.obj(function(chunk, _, next) {
this.push({
path: normalizePath(chunk),
name: normalizeName(chunk)
});

var data = {
path: normalizePath(chunk, options),
name: normalizeName(chunk, options)
};
this.push(data);
next();
});
};
23 changes: 21 additions & 2 deletions lib/util/normalizeName.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
var path = require('path');
var rmExt = require('./rmExt');
var isAddon = require('./isAddon');

module.exports = function(name) {
module.exports = function(name, options) {
name = rmExt(name);
var namespace = options.modulePrefix;
if (isAddon(name)) {
// Preserve the addon's namespace in the name if the file
// is located in the 'addon' directory
if (/^ember\-addon\:.+\/addon\//i.test(name)) {

var basePath = 'ember-addon:'+path.resolve(options.addonPath)+'/';
name = name.replace(basePath, '');
var parts = name.split('/');
namespace = parts.shift();
// remove the 'addon' segment of the path
parts.shift();
name = parts.join('/');
} else {
name = name.replace(/^ember\-addon\:.+\/app\//i, '');
}
}
if (/^components\/.+\/template$/i.test(name)) {
name = 'templates/'+name.replace(/\/template$/i, '');
} else if (/^components\/.+\/component$/i.test(name)) {
name = name.replace(/\/component$/i, '');
}
return name;
return namespace+'/'+name;
}
11 changes: 9 additions & 2 deletions lib/util/normalizePath.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
var rmExt = require('./rmExt');
var fixPathSep = require('./fixPathSep');
var path = require('path');
var isAddon = require('./isAddon');

module.exports = function(pathName) {
module.exports = function(pathName, options) {
pathName = /\.hbs$/i.test(pathName) ? fixPathSep(pathName) : rmExt(pathName);
return './' + pathName;
if (isAddon(pathName)) {
pathName = pathName.replace('ember-addon:', '');
return path.relative(path.resolve(options.rootPath), pathName);
} else {
return './' + pathName;
}
}
12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "emberate",
"description": "helper for using emberjs with commonjs modules",
"version": "3.1.1",
"version": "3.2.0",
"license": "MIT",
"main": "./lib",
"homepage": "https://github.com/AppGeo/emberate",
Expand Down Expand Up @@ -35,10 +35,14 @@
"prepublish": "node ./transpile.js"
},
"dependencies": {
"astring": "^0.5.0",
"chalk": "^0.5.1",
"combined-stream": "^1.0.5",
"espree": "^3.1.1",
"extend": "^1.3.0",
"fleck": "^0.5.1",
"ltcdr": "^2.2.1",
"mkdirp": "^0.5.1",
"read-json-sync": "^1.1.1",
"readable-stream": "^1.0.31",
"spider-stream": "^0.2.2",
"through2": "^0.6.1"
Expand All @@ -47,7 +51,11 @@
"babel": "^6.5.2",
"babel-core": "^6.5.2",
"babel-preset-es2015": "^6.5.0",
"ember-charts": "^1.0.0",
"ember-named-yields": "^1.2.0",
"ember-portal": "0.0.5",
"ember-resolver": "^2.0.3",
"ember-wormhole": "^0.3.5",
"handlebars": "^2.0.0",
"istanbul": "^0.3.2",
"tap-spec": "^0.2.1",
Expand Down
Loading

0 comments on commit 0ceb0ea

Please sign in to comment.