UPDATE: ember-auto-import is a convenient addon endorsed by the community that simplifies importing NPM modules into Ember without any setup or extra complexity. It's recommended to use this first. Only in rare instances (such as dealing with legacy code or incompatible implementations) will you need the manual process outlined below.
As I cannot be present to witness your specific circumstances, my answers to your queries would be purely speculative. Instead, I will endeavor to explain how I handle similar situations based on my own experiences.
There are slight distinctions between incorporating vendor code in an app versus adding it to an addon. My knowledge stems from creating an Ember addon. However, since apps allow for in-repo addons, the procedure for addons can easily be replicated within an actual app. In fact, isolating this task either as a separate addon or an in-repo addon proves to be more beneficial than integrating it directly into the app itself.
The initial challenge involves ensuring that the module you intend to utilize is browser compatible, as it will be utilized in a browser environment. If the NPM module is tailored specifically for node.js, it may not function correctly. Additionally, many NPM modules employ various forms of module management such as CommonJS, AMD, UMD, or global namespace. Understanding how these interact with the browser is crucial. Since Ember utilizes AMD in the browser, the NPM module must be wrapped or converted into AMD format (referred to as the shim).
In my scenario, ember-confirmed acts as an Ember wrapper for my NPM module confirmed (disclaimer: I am the author of these modules). I used Babel to compile the ES6 source code into a UMD module for the confirmed NPM module, which is then packaged into the node_modules
directory when referenced through a package.json
.
To accomplish this within the Ember addon, the following steps were necessary:
- Include the module in the addon's
package.json
dependencies section (not devDependencies
). This informs an app which version of the module should be placed in its own node_modules
directory.
- Create a shim for the module so that the built Ember app's AMD module system can resolve it (this allows specifying the
from
part in import statements).
- Instruct any apps utilizing this addon to include both the module code and the shim code in the final build output.
At this stage, an optional fourth step could involve controlling the exports. With the aforementioned steps, users have the ability to
import Something from 'name-of-npm-module';
However, there may be cases where one prefers:
import { stuff, and, things } from 'name-of-my-ember-addon';
This necessitates adding an addon/index.js
file that exports the desired items. Essentially, from 'name-of-ember-addon'
points to the addon's addon/index.js
file, while from 'name-of-npm-module'
uses the previously created shim.
Creating the Shim
I adopted the format from this blog post. The shim is written as if it were post-compiled for browser usage. It remains untranspiled through any means and is responsible for utilizing the AMD define
function and returning a reference to the included NPM module. For instance, the UMD module compiled from my confirmed NPM module adds itself to the global namespace (window.confirmer
) when executed within a built Ember app context. Therefore, my shim defines a confirmer module and assigns it the value of the global reference.
(function() {
function vendorModule() {
'use strict';
var confirmer = self['confirmer'];
return confirmer;
}
define('confirmer', [], vendorModule);
})();
In the case where the source module was not compiled via babel, manual translation of the shim is required. Every ES6 import equates to an object with properties, with one being unique (default
). To achieve this, the shim might appear as follows:
(function() {
function mediaVendorModule(moduleName) {
'use strict';
var MyModule = self['ModuleNamespace']; // Global
return function() {
return {
'default': MyModule[moduleName]
};
};
}
function helperVendorModule() {
'use strict';
var MyModule = self['ModuleNamespace']; // Global
return {
Module6: MyModule.helper.Module6,
Module7: MyModule.helper.Module7
};
}
define('com/XXXX/media/Module4', [], mediaVendorModule('Module4'));
define('com/XXXX/media/Module1', [], mediaVendorModule('Module1'));
define('com/XXXX/media/Module2', [], mediaVendorModule('Module2'));
define('com/XXXX/media/Module3', [], mediaVendorModule('Module3'));
define('com/XXXX/media/Module5', [], mediaVendorModule('Module5'));
define('com/XXXX/Helper', [], helperVendorModule);
})();
Including Files in the App's Build
An addon possesses a root index.js
file guiding the Broccoli pipeline on packaging assets. As NPM modules are considered third-party like Ember.JS, jQuery, moment, etc., they should reside in the vendor.js
file alongside the crowferated shim. To facilitate this, the addon requires two NPM modules specified in the dependencies
section (not devDependencies
):
"dependencies": {
"broccoli-funnel": "^2.0.1",
"broccoli-merge-trees": "^2.0.0",
"ember-cli-babel": "^6.3.0",
"my-npm-module": "*"
}
Subsequently, in our index.js
file, we add these files to the treeForVendor
hook:
/* eslint-env node */
'use strict';
var path = require('path');
var Funnel = require('broccoli-funnel');
var MergeTrees = require('broccoli-merge-trees');
module.exports = {
name: 'ember-confirmed',
included() {
this._super.included.apply(this, arguments);
this.import('vendor/confirmer.js');
this.import('vendor/shims/confirmer.js');
},
treeForVendor(vendorTree) {
var confirmedPath = path.join(path.dirname(require.resolve('confirmed')), 'dist');
var confirmedTree = new Funnel(confirmedPath, {
files: ['confirmer.js']
});
return new MergeTrees([vendorTree, confirmedTree]);
}
};
All these tasks can also be accomplished within an in-repo addon. Remember, you're crafting code to instruct Ember on compiling outputs rather than executing JavaScript. The essence of all this effort lies in producing a well-structured vendor.js
primed for utilization in the browser.