Skip to content

Commit

Permalink
Merge pull request #23 from trentmwillis/node
Browse files Browse the repository at this point in the history
Enable the asset-manifest to be present in SSR environments
  • Loading branch information
trentmwillis authored Sep 21, 2016
2 parents 2feef20 + 91e88f4 commit 24f8eb3
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 8 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 14 additions & 4 deletions app/asset-manifest.js
Original file line number Diff line number Diff line change
@@ -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;
10 changes: 9 additions & 1 deletion lib/generate-asset-manifest.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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, {
Expand All @@ -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
});
Expand Down
5 changes: 4 additions & 1 deletion lib/manifest-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

/**
Expand Down Expand Up @@ -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
Expand Down
48 changes: 48 additions & 0 deletions lib/node-asset-manifest-generator.js
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 6 additions & 0 deletions lib/utils/node-module-template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* eslint-disable */
define('APP_NAME/node-asset-manifest', function() {
return {
default: ASSET_MANIFEST
};
});
12 changes: 10 additions & 2 deletions node-tests/generate-asset-manifest-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down Expand Up @@ -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'));
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
4 changes: 4 additions & 0 deletions tests/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@

<script src="{{rootURL}}testem.js" integrity=""></script>
<script src="{{rootURL}}assets/vendor.js"></script>

<!-- The following script is included just to verify it will work in Node -->
<script src="{{rootURL}}assets/node-asset-manifest.js"></script>

<script src="{{rootURL}}assets/test-support.js"></script>
<script src="{{rootURL}}assets/dummy.js"></script>
<script src="{{rootURL}}assets/tests.js"></script>
Expand Down
57 changes: 57 additions & 0 deletions tests/unit/asset-manifest-test.js
Original file line number Diff line number Diff line change
@@ -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);
});

0 comments on commit 24f8eb3

Please sign in to comment.