Skip to content

Commit

Permalink
Merge b844284 into 9363be9
Browse files Browse the repository at this point in the history
  • Loading branch information
jakehow committed Apr 15, 2015
2 parents 9363be9 + b844284 commit de69084
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 31 deletions.
74 changes: 74 additions & 0 deletions lib/cli/node-modules-path.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use strict';

var path = require('path');

function isSubdirectoryOf(parentPath, possibleChildPath) {
return possibleChildPath.length > parentPath.length &&
possibleChildPath.indexOf(parentPath) === 0;
}

/**
A utility function for determining what path an addon may be found at. Addons
will only be resolved in the project's own `node_modules/` directory, they
do not follow the standard node `require` logic that a standard
`require('mode-module')` lookup would use, which finds the module according
to the `NODE_PATH`.
A description of node's lookup logic can be found here:
https://nodejs.org/api/modules.html#modules_all_together
Using this method to discover the correct location of project's `node_modules`
directory allows addons to be looked up properly even when that `node_modules`
directory is outside of the project's root.
This method checks the env variable `EMBER_NODE_PATH`. If present, its value
is used to determine the `node_modules` path.
Possible use cases for this include caching
the `node_modules/` directory outside of a source code checkout, and
ensuring the same source code (shared over a network) can be used with
different environments (Linux, OSX) where binary compatibility may not
exist.
For example, if you have a project in /projects/my-app and its `node_modules`
directory is at /resource/node_modules, you would:
```
# Node uses this as its search path for standard `require('module')` calls
export NODE_PATH=/resource/node_modules
# So that ember addon discovery looks here
export EMBER_NODE_PATH=/resource/node_modules
cd /projects/my-app && ember build
```
@private
@method nodeModulesPath
@param {String} context The starting directory to use to find the
node_modules path. This will usually be the
project's root
@return {String} absolute path to the node_modules directory
*/
module.exports = function nodeModulesPath(context) {

var nodePath = process.env.EMBER_NODE_PATH;
var contextPath = path.resolve(context);

if (nodePath) {
var configuredPath = path.resolve(nodePath);

// The contextPath is likely the project root, or possibly a subdirectory in
// node_modules/ nested dependencies. If it is more specific than the
// the configuredPath (i.e. it is a subdirectory of the configuredPath)
// prefer the more specific contextPath.
if (isSubdirectoryOf(configuredPath, contextPath)) {
return path.resolve(contextPath, 'node_modules');
} else {
return path.resolve(nodePath);
}
} else {
return path.resolve(contextPath, 'node_modules');
}
};
8 changes: 4 additions & 4 deletions lib/models/addon-discovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ AddonDiscovery.prototype.constructor = AddonDiscovery;
AddonDiscovery.prototype.discoverProjectAddons = function(project) {
var projectAsAddon = this.discoverFromProjectItself(project);
var internalAddons = this.discoverFromInternalProjectAddons(project);
var dependencyAddons = this.discoverFromDependencies(project.root, project.pkg, false);
var dependencyAddons = this.discoverFromDependencies(project.root, project.nodeModulesPath, project.pkg, false);
var inRepoAddons = this.discoverInRepoAddons(project.root, project.pkg);
var addons = projectAsAddon.concat(internalAddons, dependencyAddons, inRepoAddons);

Expand All @@ -60,7 +60,7 @@ AddonDiscovery.prototype.discoverProjectAddons = function(project) {
*/
AddonDiscovery.prototype.discoverChildAddons = function(addon) {
debug('discoverChildAddons: %s(%s)', addon.name, addon.root);
var dependencyAddons = this.discoverFromDependencies(addon.root, addon.pkg, true);
var dependencyAddons = this.discoverFromDependencies(addon.root, addon.nodeModulesPath, addon.pkg, true);
var inRepoAddons = this.discoverInRepoAddons(addon.root, addon.pkg);
var addons = dependencyAddons.concat(inRepoAddons);
return addons;
Expand Down Expand Up @@ -91,7 +91,7 @@ AddonDiscovery.prototype.discoverFromProjectItself = function(project) {
@private
@method discoverFromDependencies
*/
AddonDiscovery.prototype.discoverFromDependencies = function(root, pkg, excludeDevDeps) {
AddonDiscovery.prototype.discoverFromDependencies = function(root, nodeModulesPath, pkg, excludeDevDeps) {
var discovery = this;
var addons = Object.keys(this.dependencies(pkg, excludeDevDeps)).map(function(name) {
if (name !== 'ember-cli') {
Expand All @@ -103,7 +103,7 @@ AddonDiscovery.prototype.discoverFromDependencies = function(root, pkg, excludeD

// this supports packages that do not have a valid entry point
// script (aka `main` entry in `package.json` or `index.js`)
addonPath = path.join(root, 'node_modules', name);
addonPath = path.join(nodeModulesPath, name);
var addon = discovery.discoverAtPath(addonPath);
if (addon) {
var chalk = require('chalk');
Expand Down
6 changes: 6 additions & 0 deletions lib/models/addon.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var glob = require('glob');
var SilentError = require('../errors/silent');
var reexport = require('../utilities/reexport');
var debug = require('debug')('ember-cli:addon');
var nodeModulesPath = require('../cli/node-modules-path');

var p = require('../preprocessors');
var preprocessJs = p.preprocessJs;
Expand Down Expand Up @@ -62,6 +63,11 @@ function Addon(parent, project) {
this.registry = p.defaultRegistry(this);
this._didRequiredBuildPackages = false;

if (!this.root) {
throw new Error('Addon classes must be instantiated with the `root` property');
}
this.nodeModulesPath = nodeModulesPath(this.root);

this.treePaths = {
app: 'app',
styles: 'app/styles',
Expand Down
50 changes: 32 additions & 18 deletions lib/models/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,24 @@
/**
@module ember-cli
*/
var Promise = require('../ext/promise');
var path = require('path');
var findup = Promise.denodeify(require('findup'));
var resolve = Promise.denodeify(require('resolve'));
var fs = require('fs');
var find = require('lodash/collection/find');
var assign = require('lodash/object/assign');
var forOwn = require('lodash/object/forOwn');
var merge = require('lodash/object/merge');
var debug = require('debug')('ember-cli:project');
var AddonDiscovery = require('../models/addon-discovery');
var AddonsFactory = require('../models/addons-factory');
var Command = require('../models/command');
var UI = require('../ui');

var versionUtils = require('../utilities/version-utils');
var emberCLIVersion = versionUtils.emberCLIVersion;
var Promise = require('../ext/promise');
var path = require('path');
var findup = Promise.denodeify(require('findup'));
var resolve = Promise.denodeify(require('resolve'));
var fs = require('fs');
var find = require('lodash/collection/find');
var assign = require('lodash/object/assign');
var forOwn = require('lodash/object/forOwn');
var merge = require('lodash/object/merge');
var debug = require('debug')('ember-cli:project');
var AddonDiscovery = require('../models/addon-discovery');
var AddonsFactory = require('../models/addons-factory');
var Command = require('../models/command');
var UI = require('../ui');
var nodeModulesPath = require('../cli/node-modules-path');

var versionUtils = require('../utilities/version-utils');
var emberCLIVersion = versionUtils.emberCLIVersion;

/**
The Project model is tied to your package.json. It is instiantiated
Expand All @@ -39,6 +40,7 @@ function Project(root, pkg, ui) {
this.addons = [];
this.liveReloadFilterPatterns = [];
this.setupBowerDirectory();
this.setupNodeModulesPath();
this.addonDiscovery = new AddonDiscovery(this.ui);
this.addonsFactory = new AddonsFactory(this, this);
}
Expand Down Expand Up @@ -68,6 +70,18 @@ Project.prototype.setupBowerDirectory = function() {
debug('bowerDirectory: %s', this.bowerDirectory);
};

/**
Sets the path to the node_modules directory for this
project.
@private
@method setupNodeModulesPath
*/
Project.prototype.setupNodeModulesPath = function(){
this.nodeModulesPath = nodeModulesPath(this.root);
debug('nodeModulesPath: %s', this.nodeModulesPath);
};

var NULL_PROJECT = new Project(process.cwd(), {});

NULL_PROJECT.isEmberCLIProject = function() {
Expand Down Expand Up @@ -216,7 +230,7 @@ Project.prototype.require = function(file) {
if (/^\.\//.test(file)) { // Starts with ./
return require(path.join(this.root, file));
} else {
return require(path.join(this.root, 'node_modules', file));
return require(path.join(this.nodeModulesPath, file));
}
};

Expand Down
5 changes: 3 additions & 2 deletions lib/utilities/require-local.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use strict';

var path = require('path');
var path = require('path');
var nodeModulesPath = require('../cli/node-modules-path');

module.exports = function requireLocal(lib) {
return require(path.join(process.cwd(), 'node_modules', lib));
return require(path.join(nodeModulesPath(process.cwd()), lib));
};
1 change: 1 addition & 0 deletions tests/helpers/mock-project.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ MockProject.prototype.discoverAddons = Project.prototype.discoverAddons;
MockProject.prototype.addIfAddon = Project.prototype.addIfAddon;
MockProject.prototype.supportedInternalAddonPaths = Project.prototype.supportedInternalAddonPaths;
MockProject.prototype.setupBowerDirectory = Project.prototype.setupBowerDirectory;
MockProject.prototype.setupNodeModulesPath = Project.prototype.setupNodeModulesPath;
MockProject.prototype.dependencies = function() {
return [];
};
Expand Down
43 changes: 43 additions & 0 deletions tests/unit/cli/node-modules-path-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict';
/*jshint expr: true*/

var expect = require('chai').expect;
var path = require('path');
var nodeModulesPath = require('../../../lib/cli/node-modules-path');

describe('cli/node-module-path.js', function() {

afterEach(function() {
delete process.env.EMBER_NODE_PATH;
});


it('nodeModulesPath() should return the local node_modules path by default.', function() {
// Valid commands
var expectedPath = path.join(process.cwd(),'node_modules');

expect(
nodeModulesPath(process.cwd())
).to.equal(expectedPath);
});

it('nodeModulesPath() should return subdirectories of EMBER_NODE_PATH when set to an absolute path.', function() {
if (process.platform === 'win32') {
process.env.EMBER_NODE_PATH = 'C:\\tmp\node_modules';
} else {
process.env.EMBER_NODE_PATH = '/tmp/node_modules';
}

expect(nodeModulesPath(process.cwd())).to.equal(path.resolve(process.env.EMBER_NODE_PATH));

var addOnPath = path.resolve(process.env.EMBER_NODE_PATH, 'node_modules', 'my-add-on');
var addOnModulesPath = path.resolve(process.env.EMBER_NODE_PATH, 'node_modules', 'my-add-on', 'node_modules');
expect(nodeModulesPath(addOnPath)).to.equal(addOnModulesPath);
});

it('nodeModulesPath() should return subdirectories of EMBER_NODE_PATH when set to a relative path.', function() {
process.env.EMBER_NODE_PATH = '../../tmp/node_modules';
expect(nodeModulesPath(process.cwd())).to.equal(path.resolve('../../tmp','node_modules'));
expect(nodeModulesPath('../../tmp/node_modules/my-add-on')).to.equal(path.resolve('../../tmp','node_modules','my-add-on','node_modules'));
});
});
12 changes: 8 additions & 4 deletions tests/unit/models/addon-discovery-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ describe('models/addon-discovery.js', function() {

it('can find a package without a main entry point [DEPRECATED]', function() {
var root = path.join(fixturePath, 'shared-package', 'base');
var addonNodeModulesPath = path.join(root, 'node_modules');
var actualPaths = [];
var discovery = new AddonDiscovery(ui);

Expand All @@ -194,7 +195,7 @@ describe('models/addon-discovery.js', function() {
return providedPath;
};

discovery.discoverFromDependencies(root, mockPkg, true);
discovery.discoverFromDependencies(root, addonNodeModulesPath, mockPkg, true);

var expectedPaths = [
path.join(root, 'node_modules', 'foo-bar'),
Expand All @@ -211,6 +212,7 @@ describe('models/addon-discovery.js', function() {

it('does not error when dependencies are not found', function() {
var root = path.join(fixturePath, 'shared-package', 'base');
var addonNodeModulesPath = path.join(root, 'node_modules');
var actualPaths = [];
var discovery = new AddonDiscovery(ui);

Expand All @@ -221,7 +223,7 @@ describe('models/addon-discovery.js', function() {
return providedPath;
};

discovery.discoverFromDependencies(root, mockPkg, true);
discovery.discoverFromDependencies(root, addonNodeModulesPath, mockPkg, true);

var expectedPaths = [
path.join(root, 'node_modules', 'foo-bar'),
Expand All @@ -234,6 +236,7 @@ describe('models/addon-discovery.js', function() {

it('calls discoverAtPath for each entry in dependencies', function() {
var root = path.join(fixturePath, 'shared-package', 'base');
var addonNodeModulesPath = path.join(root, 'node_modules');
var actualPaths = [];
var discovery = new AddonDiscovery(ui);

Expand All @@ -243,7 +246,7 @@ describe('models/addon-discovery.js', function() {
return providedPath;
};

discovery.discoverFromDependencies(root, mockPkg);
discovery.discoverFromDependencies(root, addonNodeModulesPath, mockPkg);

var expectedPaths = [
path.join(root, '..', 'node_modules', 'dev-foo-bar'),
Expand All @@ -256,6 +259,7 @@ describe('models/addon-discovery.js', function() {

it('excludes devDeps if `excludeDevDeps` is true', function() {
var root = path.join(fixturePath, 'shared-package', 'base');
var addonNodeModulesPath = path.join(root, 'node_modules');
var actualPaths = [];
var discovery = new AddonDiscovery(ui);

Expand All @@ -265,7 +269,7 @@ describe('models/addon-discovery.js', function() {
return providedPath;
};

discovery.discoverFromDependencies(root, mockPkg, true);
discovery.discoverFromDependencies(root, addonNodeModulesPath, mockPkg, true);

var expectedPaths = [
path.join(root, 'node_modules', 'foo-bar'),
Expand Down
17 changes: 15 additions & 2 deletions tests/unit/models/addon-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ var fixturePath = path.resolve(__dirname, '../../fixtures/addon');
describe('models/addon.js', function() {
var addon, project, projectPath;

describe('root property', function() {
it('is required', function() {
expect(function() {
var TheAddon = Addon.extend({root:undefined});
new TheAddon();
}).to.throw(/root/);
});
});

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

Expand All @@ -31,6 +40,7 @@ describe('models/addon.js', function() {

FirstAddon = Addon.extend({
name: 'first',
root: projectPath,

init: function() {
this.treePaths.vendor = 'blazorz';
Expand All @@ -40,6 +50,7 @@ describe('models/addon.js', function() {

SecondAddon = Addon.extend({
name: 'first',
root: projectPath,

init: function() {
this.treePaths.vendor = 'blammo';
Expand Down Expand Up @@ -345,7 +356,8 @@ describe('models/addon.js', function() {

beforeEach(function() {
var MyAddon = Addon.extend({
name: 'test-project'
name: 'test-project',
root: 'foo'
});

var projectPath = path.resolve(fixturePath, 'simple');
Expand Down Expand Up @@ -507,7 +519,8 @@ describe('models/addon.js', function() {
project = new Project(projectPath, packageContents, ui);

var AddonTemp = Addon.extend({
name: 'temp'
name: 'temp',
root: 'foo'
});

addon = new AddonTemp(project, project);
Expand Down

0 comments on commit de69084

Please sign in to comment.