diff --git a/README.md b/README.md
index 42b6c63..924c9cd 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,19 @@ var app = new EmberApp(defaults, {
});
```
+## Usage with FastBoot / Server-Side Rendering Solutions
+
+Using lazily loaded assets with a server-side rendering solution, such as FastBoot, is often desirable to maximize
+performance for your consumers. However, lazy loading assets on your server is not the same as on the client and
+can actually have negative performance impact. Due to that, the recommendation is to pre-load all your assets in the
+server.
+
+Additionally, at build time we will generate a `node-asset-manifest` file that should be included in your SSR
+environment to ensure that your application can correctly access asset information.
+
+See the ["How to handle running in Node"](https://github.com/trentmwillis/ember-asset-loader/issues/21) issue for more
+information.
+
## Pre-loading Assets During Testing
For test environments it is often useful to load all of the assets in a manifest upfront. You can do this by using the
diff --git a/app/asset-manifest.js b/app/asset-manifest.js
index 91ccf6b..22ea9a0 100644
--- a/app/asset-manifest.js
+++ b/app/asset-manifest.js
@@ -1,13 +1,23 @@
import environment from './config/environment';
-const metaName = environment.modulePrefix + '/asset-manifest';
+const modulePrefix = environment.modulePrefix;
+const metaName = `${modulePrefix}/asset-manifest`;
+const nodeName = `${modulePrefix}/node-asset-manifest`;
+
let config = {};
try {
- const rawConfig = document.querySelector('meta[name="' + metaName + '"]').getAttribute('content');
- config = JSON.parse(unescape(rawConfig));
+ // If we have a Node version of the asset manifest, use that for FastBoot and
+ // similar environments.
+ if (require.has(nodeName)) {
+ config = require(nodeName).default; // eslint-disable-line
+ } else {
+ const rawConfig = document.querySelector('meta[name="' + metaName + '"]').getAttribute('content');
+ config = JSON.parse(unescape(rawConfig));
+ }
} catch(err) {
- throw new Error('Could not read asset manifest from meta tag with name "' + metaName + '".');
+ throw new Error('Failed to load asset manifest. For browser environments, verify the meta tag with name "'+ metaName +
+ '" is present. For non-browser environments, verify that you included the node-asset-manifest module.');
}
export default config;
diff --git a/lib/generate-asset-manifest.js b/lib/generate-asset-manifest.js
index eab1cba..bf23c9b 100644
--- a/lib/generate-asset-manifest.js
+++ b/lib/generate-asset-manifest.js
@@ -1,6 +1,7 @@
var Funnel = require('broccoli-funnel');
var mergeTrees = require('broccoli-merge-trees');
var AssetManifestGenerator = require('./asset-manifest-generator');
+var NodeAssetManifestGenerator = require('./node-asset-manifest-generator');
/**
* Given a tree, this function will generate an asset manifest and merge it back
@@ -22,6 +23,7 @@ module.exports = function generateAssetManifest(tree, options) {
var bundlesLocation = options.bundlesLocation || 'bundles';
var supportedTypes = options.supportedTypes;
+ var appName = options.appName;
// Get all the bundles for this application
var bundles = new Funnel(tree, {
@@ -42,8 +44,14 @@ module.exports = function generateAssetManifest(tree, options) {
annotation: 'Asset Manifest Generator'
});
+ // Generate a module that can be used in Node environments
+ var nodeManifest = new NodeAssetManifestGenerator(manifest, {
+ appName: appName,
+ annotation: 'Node Asset Manifest Generator'
+ });
+
// Merge the manifest back into the build
- return mergeTrees([ tree, manifest ], {
+ return mergeTrees([ tree, manifest, nodeManifest ], {
annotation: 'Merge Asset Manifest',
overwrite: true
});
diff --git a/lib/manifest-generator.js b/lib/manifest-generator.js
index 10fbc14..f4e8b4a 100644
--- a/lib/manifest-generator.js
+++ b/lib/manifest-generator.js
@@ -2,6 +2,7 @@ var path = require('path');
var fs = require('fs-extra');
var Addon = require('ember-cli/lib/models/addon');
var mergeTrees = require('broccoli-merge-trees');
+var objectAssign = require('object-assign');
var findHost = require('./utils/find-host');
/**
@@ -40,8 +41,10 @@ var ManifestGenerator = Addon.extend({
return tree;
}
+ var manifestOptions = objectAssign({ appName: app.name }, this.manifestOptions);
+
var generateAssetManifest = require('./generate-asset-manifest'); // eslint-disable-line global-require
- var treeWithManifest = generateAssetManifest(tree, this.manifestOptions);
+ var treeWithManifest = generateAssetManifest(tree, manifestOptions);
var indexName = options.outputPaths.app.html;
var insertAssetManifest = require('./insert-asset-manifest'); // eslint-disable-line global-require
diff --git a/lib/node-asset-manifest-generator.js b/lib/node-asset-manifest-generator.js
new file mode 100644
index 0000000..74243b0
--- /dev/null
+++ b/lib/node-asset-manifest-generator.js
@@ -0,0 +1,48 @@
+var Plugin = require('broccoli-caching-writer');
+var walk = require('walk-sync');
+var path = require('path');
+var fs = require('fs-extra');
+
+/**
+ * A Broccoli plugin to generate a module to be used in Node for resolving the
+ * asset manifest. Primary use case is for FastBoot like environments.
+ *
+ * @class NodeAssetManifestGenerator
+ * @extends BroccoliCachingWriter
+ */
+function NodeAssetManifestGenerator(inputTrees, options) {
+ options = options || {};
+
+ this.appName = options.appName;
+
+ Plugin.call(this, [ inputTrees ], {
+ annotation: options.annotation
+ });
+}
+
+NodeAssetManifestGenerator.prototype = Object.create(Plugin.prototype);
+NodeAssetManifestGenerator.prototype.constructor = NodeAssetManifestGenerator;
+
+/**
+ * Generates an asset manifest module on build from the passed in
+ * asset-manifest.json file.
+ */
+NodeAssetManifestGenerator.prototype.build = function() {
+ var inputPath = this.inputPaths[0];
+ var assetManifestPath = path.join(inputPath, 'asset-manifest.json');
+ var assetManifest = fs.readJsonSync(assetManifestPath);
+
+ var moduleTemplatePath = path.join(__dirname, './utils/node-module-template.js');
+ var moduleTemplate = fs.readFileSync(moduleTemplatePath, 'utf-8');
+ var module = moduleTemplate
+ .replace('APP_NAME', this.appName)
+ .replace('ASSET_MANIFEST', JSON.stringify(assetManifest));
+
+ var outputAssets = path.join(this.outputPath, 'assets');
+ fs.mkdirSync(outputAssets);
+
+ var nodeModuleFile = path.join(outputAssets, 'node-asset-manifest.js');
+ fs.writeFileSync(nodeModuleFile, module);
+};
+
+module.exports = NodeAssetManifestGenerator;
diff --git a/lib/utils/node-module-template.js b/lib/utils/node-module-template.js
new file mode 100644
index 0000000..a587542
--- /dev/null
+++ b/lib/utils/node-module-template.js
@@ -0,0 +1,6 @@
+/* eslint-disable */
+define('APP_NAME/node-asset-manifest', function() {
+ return {
+ default: ASSET_MANIFEST
+ };
+});
diff --git a/node-tests/generate-asset-manifest-test.js b/node-tests/generate-asset-manifest-test.js
index bbc382f..ed37047 100644
--- a/node-tests/generate-asset-manifest-test.js
+++ b/node-tests/generate-asset-manifest-test.js
@@ -20,10 +20,14 @@ describe('generate-asset-manifest', function() {
var originalFiles = walk(inputTree);
var outputFiles = walk(output);
- assert.equal(outputFiles.length, originalFiles.length + 1, 'output files has one more file than originally');
+ assert.equal(outputFiles.length, originalFiles.length + 2, 'output files has two more files than originally');
+
assert.equal(originalFiles.indexOf('asset-manifest.json'), -1, 'original files does not contain an asset manifest');
assert.notEqual(outputFiles.indexOf('asset-manifest.json'), -1, 'output files does contain an asset manifest');
+ assert.equal(originalFiles.indexOf('assets/node-asset-manifest.js'), -1, 'original files does not contain a Node asset manifest module');
+ assert.notEqual(outputFiles.indexOf('assets/node-asset-manifest.js'), -1, 'output files does contain a Node asset manifest module');
+
var manifestFile = path.join(output, 'asset-manifest.json');
var manifest = fs.readJsonSync(manifestFile);
var expectedManifest = fs.readJsonSync(path.join(manifestsPath, manifestName + '.json'));
@@ -55,10 +59,14 @@ describe('generate-asset-manifest', function() {
var originalFiles = walk(inputTree);
var outputFiles = walk(output);
- assert.equal(outputFiles.length, originalFiles.length, 'output files has same number of files as originally');
+ assert.equal(outputFiles.length, originalFiles.length + 1, 'output files has one more file than originally');
+
assert.notEqual(originalFiles.indexOf('asset-manifest.json'), -1, 'original files does contain an asset manifest');
assert.notEqual(outputFiles.indexOf('asset-manifest.json'), -1, 'output files does contain an asset manifest');
+ assert.equal(originalFiles.indexOf('assets/node-asset-manifest.js'), -1, 'original files does not contain a Node asset manifest module');
+ assert.notEqual(outputFiles.indexOf('assets/node-asset-manifest.js'), -1, 'output files does contain a Node asset manifest module');
+
var manifestFile = path.join(output, 'asset-manifest.json');
var manifest = fs.readJsonSync(manifestFile);
var expectedManifest = fs.readJsonSync(path.join(manifestsPath, 'full-plus-existing.json'));
diff --git a/package.json b/package.json
index 8c8a6b6..9631148 100644
--- a/package.json
+++ b/package.json
@@ -56,6 +56,7 @@
"ember-cli-babel": "^5.1.6",
"exists-sync": "0.0.3",
"fs-extra": "^0.30.0",
+ "object-assign": "^4.1.0",
"walk-sync": "^0.2.7"
},
"ember-addon": {
diff --git a/tests/index.html b/tests/index.html
index f7ff652..d412bf8 100644
--- a/tests/index.html
+++ b/tests/index.html
@@ -23,6 +23,10 @@
+
+
+
+
diff --git a/tests/unit/asset-manifest-test.js b/tests/unit/asset-manifest-test.js
new file mode 100644
index 0000000..bdeb522
--- /dev/null
+++ b/tests/unit/asset-manifest-test.js
@@ -0,0 +1,57 @@
+/* global require */
+
+import { module, test } from 'qunit';
+
+module('Unit | asset-manifest', {
+ beforeEach() {
+ resetModules();
+ this.originalNodeModule = require.entries['dummy/node-asset-manifest'];
+ },
+
+ afterEach() {
+ require.entries['dummy/node-asset-manifest'] = this.originalNodeModule;
+ resetModules();
+ }
+});
+
+function resetModules() {
+ require.unsee('dummy/node-asset-manifest');
+ require.unsee('dummy/asset-manifest');
+}
+
+test('node-asset-manifest is generated properly', function(assert) {
+ const nodeManifest = require('dummy/node-asset-manifest').default;
+ delete require.entries['dummy/node-asset-manifest'];
+
+ const manifest = require('dummy/asset-manifest').default;
+
+ assert.notStrictEqual(nodeManifest, manifest);
+ assert.deepEqual(nodeManifest, manifest);
+});
+
+test('loads the node-asset-manifest if present', function(assert) {
+ const replacementModule = {};
+ define('dummy/node-asset-manifest', () => ({ default: replacementModule}));
+
+ assert.strictEqual(require('dummy/asset-manifest').default, replacementModule);
+});
+
+test('loads the manifest from the meta tag if available', function(assert) {
+ delete require.entries['dummy/node-asset-manifest'];
+
+ const meta = document.querySelector('meta[name="dummy/asset-manifest"]');
+ const metaContent = meta.getAttribute('content');
+ meta.setAttribute('content', '{"derp":"herp"}');
+ assert.deepEqual(require('dummy/asset-manifest').default, { derp: 'herp' });
+ meta.setAttribute('content', metaContent);
+});
+
+test('throws an error if unable to load the manifest', function(assert) {
+ delete require.entries['dummy/node-asset-manifest'];
+
+ const meta = document.querySelector('meta[name="dummy/asset-manifest"]');
+ const metaContent = meta.getAttribute('content');
+ meta.setAttribute('content', 'herp');
+ assert.throws(() => assert.deepEqual(require('dummy/asset-manifest').default, {}));
+ meta.setAttribute('content', metaContent);
+});