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); +});