Repository: ember-cli/ember-cli
Pull request #7045 (2017-05-16 08:37:31)

Make app.import() work with files inside `node_modules`
Instead of funneling all of `node_modules` we build a list of npm packages that we want to funnel, and then only use those to reduce the necessary IO work.

stefanpenner: Does this need to take into account the node algorithm to not be self-trollish?
Turbo87: > Does this need to take into account the node algorithm to not be self-trollish?

I guess that would make sense, yeah. I have no clue how to test this though... 😞 

Patch:
diff --git a/lib/broccoli/ember-app.js b/lib/broccoli/ember-app.js
index a63d3bfef..633beac18 100644
--- a/lib/broccoli/ember-app.js
+++ b/lib/broccoli/ember-app.js
@@ -9,6 +9,7 @@ const existsSync = require('exists-sync');
 const path = require('path');
 const p = require('ember-cli-preprocess-registry/preprocessors');
 const chalk = require('chalk');
+const resolve = require('resolve');
 
 const Project = require('../models/project');
 const cleanBaseURL = require('clean-base-url');
@@ -134,6 +135,7 @@ class EmberApp {
     this.otherAssetPaths = [];
     this.legacyTestFilesToAppend = [];
     this.vendorTestStaticStyles = [];
+    this._nodeModules = [];
 
     this.trees = this.options.trees;
 
@@ -941,6 +943,18 @@ class EmberApp {
     return this._cachedBowerTree;
   }
 
+  _nodeModuleTrees() {
+    if (!this._cachedNodeModuleTrees) {
+      this._cachedNodeModuleTrees = this._nodeModules.map(module => new Funnel(module.path, {
+        srcDir: '/',
+        destDir: `node_modules/${module.name}/`,
+        annotation: `Funnel (node_modules/${module.name})`,
+      }));
+    }
+
+    return this._cachedNodeModuleTrees;
+  }
+
   _addonTree() {
     if (!this._cachedAddonTree) {
       let addonTrees = this.addonTreesFor('addon');
@@ -1003,6 +1017,8 @@ class EmberApp {
         trees.unshift(bower);
       }
 
+      this._nodeModuleTrees().forEach(tree => trees.unshift(tree));
+
       let externalTree = this._mergeTrees(trees, {
         annotation: 'TreeMerger (ExternalTree)',
         overwrite: true,
@@ -1522,8 +1538,16 @@ class EmberApp {
       prepend: false,
     });
 
+    let match = assetPath.match(/^node_modules\/([^/]+)\//);
+    if (match) {
+      let basedir = options.resolveFrom || this.project.root;
+      let name = match[1];
+      let _path = path.dirname(resolve.sync(`${name}/package.json`, { basedir }));
+      this._nodeModules.push({ name, path: _path });
+    }
+
     let directory = path.dirname(assetPath);
-    let subdirectory = directory.replace(new RegExp(`^vendor/|${this.bowerDirectory}`), '');
+    let subdirectory = directory.replace(new RegExp(`^vendor/|${this.bowerDirectory}|node_modules/`), '');
     let extension = path.extname(assetPath);
 
     if (!extension) {
diff --git a/lib/models/addon.js b/lib/models/addon.js
index 635474646..b8454da2f 100644
--- a/lib/models/addon.js
+++ b/lib/models/addon.js
@@ -665,6 +665,9 @@ let addonProto = {
      @since 2.7.0
    */
   import(asset, options) {
+    options = options || {};
+    options.resolveFrom = options.resolveFrom || this.root;
+
     let app = this._findHost();
     app.import(asset, options);
   },


Test patch:
diff --git a/tests/fixtures/app-import/app/app.js b/tests/fixtures/app-import/app/app.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/fixtures/app-import/bower_components/ember/ember.js b/tests/fixtures/app-import/bower_components/ember/ember.js
new file mode 100644
index 000000000..650c50855
--- /dev/null
+++ b/tests/fixtures/app-import/bower_components/ember/ember.js
@@ -0,0 +1,3 @@
+window.Ember = {
+  foo: 'bar';
+};
diff --git a/tests/fixtures/app-import/bower_components/jquery/dist/jquery.js b/tests/fixtures/app-import/bower_components/jquery/dist/jquery.js
new file mode 100644
index 000000000..d59c33630
--- /dev/null
+++ b/tests/fixtures/app-import/bower_components/jquery/dist/jquery.js
@@ -0,0 +1,3 @@
+window.$ = function() {
+  console.log('this is jQuery!');
+};
diff --git a/tests/fixtures/app-import/config/environment.js b/tests/fixtures/app-import/config/environment.js
new file mode 100644
index 000000000..33eb55001
--- /dev/null
+++ b/tests/fixtures/app-import/config/environment.js
@@ -0,0 +1,8 @@
+'use strict';
+
+module.exports = function(environment) {
+  return {
+    modulePrefix: 'app-import',
+    environment,
+  };
+};
diff --git a/tests/fixtures/app-import/package.json b/tests/fixtures/app-import/package.json
new file mode 100644
index 000000000..08706ccd6
--- /dev/null
+++ b/tests/fixtures/app-import/package.json
@@ -0,0 +1,44 @@
+{
+  "name": "app-import",
+  "version": "0.0.0",
+  "description": "Small description for app-import goes here",
+  "license": "MIT",
+  "author": "",
+  "directories": {
+    "doc": "doc",
+    "test": "tests"
+  },
+  "repository": "",
+  "scripts": {
+    "build": "ember build",
+    "start": "ember server",
+    "test": "ember test"
+  },
+  "devDependencies": {
+    "broccoli-asset-rev": "^2.4.5",
+    "ember-ajax": "^3.0.0",
+    "ember-cli": "~2.14.0-beta.1",
+    "ember-cli-app-version": "^3.0.0",
+    "ember-cli-babel": "^6.0.0",
+    "ember-cli-dependency-checker": "^1.3.0",
+    "ember-cli-eslint": "^3.0.0",
+    "ember-cli-htmlbars": "^1.1.1",
+    "ember-cli-htmlbars-inline-precompile": "^0.4.0",
+    "ember-cli-inject-live-reload": "^1.4.1",
+    "ember-cli-qunit": "^4.0.0",
+    "ember-cli-shims": "^1.1.0",
+    "ember-cli-sri": "^2.1.0",
+    "ember-cli-uglify": "^1.2.0",
+    "ember-data": "~2.13.0",
+    "ember-export-application-global": "^2.0.0",
+    "ember-load-initializers": "^1.0.0",
+    "ember-resolver": "^4.0.0",
+    "ember-source": "~2.14.0-beta.1",
+    "ember-welcome-page": "^3.0.0",
+    "loader.js": "^4.2.3"
+  },
+  "engines": {
+    "node": ">= 4"
+  },
+  "private": true
+}
diff --git a/tests/unit/broccoli/ember-app/import-test.js b/tests/unit/broccoli/ember-app/import-test.js
new file mode 100644
index 000000000..881bbad4d
--- /dev/null
+++ b/tests/unit/broccoli/ember-app/import-test.js
@@ -0,0 +1,224 @@
+'use strict';
+
+const co = require('co');
+const fs = require('fs-extra');
+const broccoliTestHelper = require('broccoli-test-helper');
+const expect = require('chai').expect;
+
+const EmberApp = require('../../../../lib/broccoli/ember-app');
+const MockCLI = require('../../../helpers/mock-cli');
+const Project = require('../../../../lib/models/project');
+
+const buildOutput = broccoliTestHelper.buildOutput;
+const createTempDir = broccoliTestHelper.createTempDir;
+
+describe('EmberApp.import()', function() {
+  let input, cwd;
+
+  beforeEach(function() {
+    cwd = process.cwd();
+    return createTempDir().then(tempDir => (input = tempDir));
+  });
+
+  afterEach(function() {
+    process.chdir(cwd);
+    delete process.env.EMBER_ENV;
+    return input.dispose();
+  });
+
+  function createApp(options) {
+    options = options || {};
+
+    let path = input.path();
+    process.chdir(path);
+
+    let pkg = fs.readJsonSync(`${path}/package.json`);
+
+    let cli = new MockCLI();
+    let project = new Project(path, pkg, cli.ui, cli);
+
+    let app = new EmberApp({
+      project,
+      _ignoreMissingLoader: true,
+      sourcemaps: { enabled: false },
+    }, options);
+
+    app._processedTemplatesTree = () => '';
+
+    return app;
+  }
+
+  it('does not import bower dependencies if they are not explicitly imported', co.wrap(function *() {
+    input.copy(`${__dirname}/../../../fixtures/app-import`);
+    input.write({
+      'bower_components': {
+        'moment': {
+          'moment.js': 'window.moment = "what does time even mean?";',
+        },
+      },
+    });
+
+    let app = createApp();
+
+    let output = yield buildOutput(app.javascript());
+    let outputTree = output.read();
+    expect(Object.keys(outputTree)).to.deep.equal(['assets']);
+    expect(Object.keys(outputTree['assets']).sort()).to.deep.equal(['app-import.js', 'vendor.js']);
+    expect(outputTree['assets']['vendor.js']).to.contain('window.Ember = {');
+    expect(outputTree['assets']['vendor.js']).to.contain('window.$ = function() {');
+    expect(outputTree['assets']['vendor.js']).to.not.contain('window.moment');
+  }));
+
+  it('can import bower dependencies into vendor.js', co.wrap(function *() {
+    input.copy(`${__dirname}/../../../fixtures/app-import`);
+    input.write({
+      'bower_components': {
+        'moment': {
+          'moment.js': 'window.moment = "what does time even mean?";',
+        },
+      },
+    });
+
+    let app = createApp();
+
+    app.import('bower_components/moment/moment.js');
+
+    let output = yield buildOutput(app.javascript());
+    let outputTree = output.read();
+    expect(Object.keys(outputTree)).to.deep.equal(['assets']);
+    expect(Object.keys(outputTree['assets']).sort()).to.deep.equal(['app-import.js', 'vendor.js']);
+    expect(outputTree['assets']['vendor.js']).to.contain('window.Ember = {');
+    expect(outputTree['assets']['vendor.js']).to.contain('window.$ = function() {');
+    expect(outputTree['assets']['vendor.js']).to.contain('window.moment');
+  }));
+
+  it('handles imports with different environments (development)', co.wrap(function *() {
+    input.copy(`${__dirname}/../../../fixtures/app-import`);
+    input.write({
+      'bower_components': {
+        'moment': {
+          'moment.js': 'window.moment = "what does time even mean?";',
+          'moment.min.js': 'window.moment="what does time even mean?"',
+        },
+      },
+    });
+
+    let app = createApp();
+
+    app.import({
+      development: 'bower_components/moment/moment.js',
+      production: 'bower_components/moment/moment.min.js',
+    });
+
+    let output = yield buildOutput(app.javascript());
+    let outputTree = output.read();
+    expect(Object.keys(outputTree)).to.deep.equal(['assets']);
+    expect(Object.keys(outputTree['assets']).sort()).to.deep.equal(['app-import.js', 'vendor.js']);
+    expect(outputTree['assets']['vendor.js']).to.contain('window.moment = "');
+  }));
+
+  it('handles imports with different environments (production)', co.wrap(function *() {
+    input.copy(`${__dirname}/../../../fixtures/app-import`);
+    input.write({
+      'bower_components': {
+        'moment': {
+          'moment.js': 'window.moment = "what does time even mean?";',
+          'moment.min.js': 'window.moment = "verysmallmoment"',
+        },
+      },
+    });
+
+    process.env.EMBER_ENV = 'production';
+
+    let app = createApp();
+
+    app.import({
+      development: 'bower_components/moment/moment.js',
+      production: 'bower_components/moment/moment.min.js',
+    });
+
+    let output = yield buildOutput(app.javascript());
+    let outputTree = output.read();
+    expect(Object.keys(outputTree)).to.deep.equal(['assets']);
+    expect(Object.keys(outputTree['assets']).sort()).to.deep.equal(['app-import.js', 'vendor.js']);
+    expect(outputTree['assets']['vendor.js']).to.contain('verysmallmoment');
+  }));
+
+  it('can import node dependencies into vendor.js', co.wrap(function *() {
+    input.copy(`${__dirname}/../../../fixtures/app-import`);
+    input.write({
+      'node_modules': {
+        'moment': {
+          'package.json': '{}',
+          'moment.js': 'window.moment = "what does time even mean?";',
+        },
+      },
+    });
+
+    let app = createApp();
+
+    app.import('node_modules/moment/moment.js');
+
+    let output = yield buildOutput(app.javascript());
+    let outputTree = output.read();
+    expect(Object.keys(outputTree)).to.deep.equal(['assets']);
+    expect(Object.keys(outputTree['assets']).sort()).to.deep.equal(['app-import.js', 'vendor.js']);
+    expect(outputTree['assets']['vendor.js']).to.contain('window.Ember = {');
+    expect(outputTree['assets']['vendor.js']).to.contain('window.$ = function() {');
+    expect(outputTree['assets']['vendor.js']).to.contain('window.moment');
+  }));
+
+  it('handles imports from node with different environments (development)', co.wrap(function *() {
+    input.copy(`${__dirname}/../../../fixtures/app-import`);
+    input.write({
+      'node_modules': {
+        'moment': {
+          'package.json': '{}',
+          'moment.js': 'window.moment = "what does time even mean?";',
+          'moment.min.js': 'window.moment="what does time even mean?"',
+        },
+      },
+    });
+
+    let app = createApp();
+
+    app.import({
+      development: 'node_modules/moment/moment.js',
+      production: 'node_modules/moment/moment.min.js',
+    });
+
+    let output = yield buildOutput(app.javascript());
+    let outputTree = output.read();
+    expect(Object.keys(outputTree)).to.deep.equal(['assets']);
+    expect(Object.keys(outputTree['assets']).sort()).to.deep.equal(['app-import.js', 'vendor.js']);
+    expect(outputTree['assets']['vendor.js']).to.contain('window.moment = "');
+  }));
+
+  it('handles imports from node with different environments (production)', co.wrap(function *() {
+    input.copy(`${__dirname}/../../../fixtures/app-import`);
+    input.write({
+      'node_modules': {
+        'moment': {
+          'package.json': '{}',
+          'moment.js': 'window.moment = "what does time even mean?";',
+          'moment.min.js': 'window.moment = "verysmallmoment"',
+        },
+      },
+    });
+
+    process.env.EMBER_ENV = 'production';
+
+    let app = createApp();
+
+    app.import({
+      development: 'node_modules/moment/moment.js',
+      production: 'node_modules/moment/moment.min.js',
+    });
+
+    let output = yield buildOutput(app.javascript());
+    let outputTree = output.read();
+    expect(Object.keys(outputTree)).to.deep.equal(['assets']);
+    expect(Object.keys(outputTree['assets']).sort()).to.deep.equal(['app-import.js', 'vendor.js']);
+    expect(outputTree['assets']['vendor.js']).to.contain('verysmallmoment');
+  }));
+});