From 965f812fe322147b2cdd8cfbe918802fa201789b Mon Sep 17 00:00:00 2001 From: Nathan Hammond Date: Fri, 26 Aug 2016 12:40:25 -0700 Subject: [PATCH] Make insertion happen as a broccoli plugin. --- lib/asset-manifest-inserter.js | 60 ++++++++++++++++++ lib/insert-asset-manifest.js | 17 ++++++ lib/manifest-generator.js | 47 ++++---------- lib/meta-handler.js | 22 +++++++ node-tests/asset-manifest-inserter-tests.js | 61 +++++++++++++++++++ .../insertion-test/asset-manifest.json | 3 + node-tests/fixtures/insertion-test/extra.html | 26 ++++++++ node-tests/fixtures/insertion-test/index.html | 26 ++++++++ .../fixtures/insertion-test/tests/index.html | 40 ++++++++++++ node-tests/meta-handler-tests.js | 61 +++++++++++++++++++ package.json | 3 +- 11 files changed, 330 insertions(+), 36 deletions(-) create mode 100644 lib/asset-manifest-inserter.js create mode 100644 lib/insert-asset-manifest.js create mode 100644 lib/meta-handler.js create mode 100644 node-tests/asset-manifest-inserter-tests.js create mode 100644 node-tests/fixtures/insertion-test/asset-manifest.json create mode 100644 node-tests/fixtures/insertion-test/extra.html create mode 100644 node-tests/fixtures/insertion-test/index.html create mode 100644 node-tests/fixtures/insertion-test/tests/index.html create mode 100644 node-tests/meta-handler-tests.js diff --git a/lib/asset-manifest-inserter.js b/lib/asset-manifest-inserter.js new file mode 100644 index 0000000..fdab6df --- /dev/null +++ b/lib/asset-manifest-inserter.js @@ -0,0 +1,60 @@ +var BroccoliCachingWriter = require('broccoli-caching-writer'); +var path = require('path'); +var fs = require('fs-extra'); +var existsSync = require('exists-sync'); +var metaReplacer = require('./meta-handler').replacer; + +/** + * A Broccoli Plugin to modify index.html to include the asset manifest. + * + * @class AssetManifestInserter + * @extends BroccoliCachingWriter + * + * @private + * @param {Tree} tree + * @param {object} options + */ + +AssetManifestInserter.prototype = Object.create(BroccoliCachingWriter.prototype); +AssetManifestInserter.prototype.constructor = AssetManifestInserter; +function AssetManifestInserter(inputNodes, options) { + options = options || {}; + options.indexName = options.indexName || 'index.html'; + options.cacheInclude = [options.indexName, 'tests/index.html', 'asset-manifest.json']; + + BroccoliCachingWriter.call(this, inputNodes, { + annotation: options.annotation + }); + this.options = options; +} + +AssetManifestInserter.prototype.build = function() { + var sourceDir = this.inputPaths[0]; + var manifestFilePath = path.join(sourceDir, 'asset-manifest.json'); + var indexFilePath = path.join(sourceDir, this.options.indexName); + var testIndexFilePath = path.join(sourceDir, 'tests', 'index.html'); + + var manifest; + try { + manifest = fs.readJsonSync(manifestFilePath); + } catch (error) { + console.warn('\n\nWarning: Unable to read asset-manifest.json from build with error: ' + error) + console.warn('Warning: Proceeding without generated manifest; you will need to manually provide a manifest to the Asset Loader Service to load bundles at runtime. If this was intentional you can turn this message off via the `noManifest` flag.\n\n'); + manifest = { bundles: {} }; + } + + if (existsSync(indexFilePath)) { + var indexFile = fs.readFileSync(indexFilePath, { encoding: 'utf8' }); + var index = metaReplacer(indexFile, manifest); + fs.writeFileSync(path.join(this.outputPath, this.options.indexName), index); + } + + if (existsSync(testIndexFilePath)) { + var testIndexFile = fs.readFileSync(testIndexFilePath, { encoding: 'utf8' }); + var testIndex = metaReplacer(testIndexFile, manifest); + fs.mkdirSync(path.join(this.outputPath, 'tests')); + fs.writeFileSync(path.join(this.outputPath, 'tests', 'index.html'), testIndex); + } +}; + +module.exports = AssetManifestInserter; diff --git a/lib/insert-asset-manifest.js b/lib/insert-asset-manifest.js new file mode 100644 index 0000000..5680ea9 --- /dev/null +++ b/lib/insert-asset-manifest.js @@ -0,0 +1,17 @@ +var mergeTrees = require('broccoli-merge-trees'); +var AssetManifestInserter = require('./asset-manifest-inserter'); + +/** + * Given a tree, this function will insert the asset manifest into index.html. + * + * The `options` object is passed directly into the broccoli plugin. + * + * @public + * @param {Tree} tree + * @param {object} options + * @return {Tree} + */ +module.exports = function insertAssetManifest(tree, options) { + var indicesWithManifests = new AssetManifestInserter([tree], options); + return mergeTrees([tree, indicesWithManifests], { overwrite: true }); +}; diff --git a/lib/manifest-generator.js b/lib/manifest-generator.js index b3e75fa..2cfe7ad 100644 --- a/lib/manifest-generator.js +++ b/lib/manifest-generator.js @@ -1,6 +1,7 @@ var path = require('path'); var fs = require('fs-extra'); var Addon = require('ember-cli/lib/models/addon'); +var mergeTrees = require('broccoli-merge-trees'); /** * A simple base class for other addons to extend when they want to generate an @@ -27,47 +28,23 @@ var ManifestGenerator = Addon.extend({ * specified manifestOptions. */ postprocessTree: function(type, tree) { - if (type === 'all') { - var generateAssetManifest = require('./generate-asset-manifest'); // eslint-disable-line global-require - return generateAssetManifest(tree, this.manifestOptions); - } - - return tree; - }, + if (type !== 'all') { return tree; } - /** - * Replace the manifest placeholder with an escaped version of the manifest. - * We do this in both the app's index and test's index. - */ - postBuild: function(result) { - var options = this.app && this.app.options && this.app.options.assetLoader; - if (options && options.noManifest) { - return; - } + var options = this.app && this.app.options && this.app.options; + var assetLoaderOptions = options.assetLoader; - var manifestFile = path.join(result.directory, 'asset-manifest.json'); - var manifest; + var generateAssetManifest = require('./generate-asset-manifest'); // eslint-disable-line global-require + var treeWithManifest = generateAssetManifest(tree, this.manifestOptions); - try { - manifest = fs.readJsonSync(manifestFile); - } catch (error) { - console.warn('\n\nWarning: Unable to read asset-manifest.json from build with error: ' + error) - console.warn('Warning: Proceeding without generated manifest; you will need to manually provide a manifest to the Asset Loader Service to load bundles at runtime. If this was intentional you can turn this message off via the `noManifest` flag.\n\n'); - manifest = { bundles: {} }; + if (assetLoaderOptions && assetLoaderOptions.noManifest) { + return treeWithManifest; } - var escapedManifest = escape(JSON.stringify(manifest)); - - var indexFilePath = path.join(result.directory, 'index.html'); - this.replaceAssetManifestPlaceholder(indexFilePath, escapedManifest); - - var testsIndexFilePath = path.join(result.directory, 'tests/index.html') - this.replaceAssetManifestPlaceholder(testsIndexFilePath, escapedManifest); - }, + var indexName = options.outputPaths.app.html; + var insertAssetManifest = require('./insert-asset-manifest'); // eslint-disable-line global-require + var treeWithInsertedManifest = insertAssetManifest(treeWithManifest, { indexName: indexName }); - replaceAssetManifestPlaceholder: function(filePath, manifest) { - var resolvedFile = fs.readFileSync(filePath, { encoding: 'utf8' }); - fs.outputFileSync(filePath, resolvedFile.replace(/%GENERATED_ASSET_MANIFEST%/, manifest)); + return treeWithInsertedManifest; } }); diff --git a/lib/meta-handler.js b/lib/meta-handler.js new file mode 100644 index 0000000..9460ba0 --- /dev/null +++ b/lib/meta-handler.js @@ -0,0 +1,22 @@ +/** + * Replace the manifest meta tag with an updated version of the manifest. + * We do this in both the app's and test's index.html file. + */ +var regex = /]*>/; + +function escaper(sourceJSON) { + return escape(JSON.stringify(sourceJSON)); +} + +function replacer(fileContents, manifest) { + if (!regex.test(fileContents)) { + return fileContents; + } + + var metaName = regex.exec(fileContents)[1]; + var metaString = ''; + + return fileContents.replace(regex, metaString); +} + +module.exports = { escaper, replacer }; diff --git a/node-tests/asset-manifest-inserter-tests.js b/node-tests/asset-manifest-inserter-tests.js new file mode 100644 index 0000000..58dcbad --- /dev/null +++ b/node-tests/asset-manifest-inserter-tests.js @@ -0,0 +1,61 @@ +var path = require('path'); +var assert = require('assert'); +var walk = require('walk-sync'); +var fs = require('fs-extra'); +var broccoli = require('broccoli'); + +var AssetManifestInserter = require('../lib/asset-manifest-inserter'); +var metaHandler = require('../lib/meta-handler'); + +var fixturePath = path.join(__dirname, 'fixtures'); +var inputTrees = [ path.join(fixturePath, 'insertion-test') ]; + +function build(assertion, options) { + return function() { + options = options || {}; + + var inserter = new AssetManifestInserter(inputTrees, options); + var builder = new broccoli.Builder(inserter); + + return builder.build() + .then(assertion) + .then(function() { + builder.cleanup(); + }); + } +} + +describe('asset-manifest-inserter', function() { + describe('build', function() { + + it('only modifies index.html', build(function(results) { + var output = results.directory; + assert.deepEqual(walk(output), [ 'index.html', 'tests/', 'tests/index.html' ]); + }) + ); + + it('uses the correct file', build(function(results) { + var output = results.directory; + assert.deepEqual(walk(output), [ 'extra.html', 'tests/', 'tests/index.html' ]); + }, { indexName: 'extra.html' }) + ); + + it('successfully modifies the manifest', build(function(results) { + var output = results.directory; + var indexFilePath = path.join(output, 'index.html'); + var testIndexFilePath = path.join(output, 'tests', 'index.html'); + var manifestFilePath = path.join(inputTrees[0], 'asset-manifest.json') + + var index = fs.readFileSync(indexFilePath, { encoding: 'utf8' }); + var testIndex = fs.readFileSync(testIndexFilePath, { encoding: 'utf8' }); + var assetManifest = fs.readJsonSync(manifestFilePath); + + var needle = metaHandler.escaper(assetManifest); + + assert.notEqual(index.indexOf(needle), -1); + assert.notEqual(testIndex.indexOf(needle), -1); + }) + ); + + }); +}); diff --git a/node-tests/fixtures/insertion-test/asset-manifest.json b/node-tests/fixtures/insertion-test/asset-manifest.json new file mode 100644 index 0000000..7a9e864 --- /dev/null +++ b/node-tests/fixtures/insertion-test/asset-manifest.json @@ -0,0 +1,3 @@ +{ + "key": "value" +} diff --git a/node-tests/fixtures/insertion-test/extra.html b/node-tests/fixtures/insertion-test/extra.html new file mode 100644 index 0000000..698600c --- /dev/null +++ b/node-tests/fixtures/insertion-test/extra.html @@ -0,0 +1,26 @@ + + + + + + Dummy + + + + + + + + + + + + + + + + + + + + diff --git a/node-tests/fixtures/insertion-test/index.html b/node-tests/fixtures/insertion-test/index.html new file mode 100644 index 0000000..698600c --- /dev/null +++ b/node-tests/fixtures/insertion-test/index.html @@ -0,0 +1,26 @@ + + + + + + Dummy + + + + + + + + + + + + + + + + + + + + diff --git a/node-tests/fixtures/insertion-test/tests/index.html b/node-tests/fixtures/insertion-test/tests/index.html new file mode 100644 index 0000000..ef1fabb --- /dev/null +++ b/node-tests/fixtures/insertion-test/tests/index.html @@ -0,0 +1,40 @@ + + + + + + Dummy Tests + + + + + + + + + + + + + + + + +
+
+ +
+
+
+ + + + + + + + + + + + diff --git a/node-tests/meta-handler-tests.js b/node-tests/meta-handler-tests.js new file mode 100644 index 0000000..8e89027 --- /dev/null +++ b/node-tests/meta-handler-tests.js @@ -0,0 +1,61 @@ +var assert = require('assert'); +var metaHandler = require('../lib/meta-handler'); + +function testStringGenerator(sourceJSON) { + return ''; +} + +describe('meta-handler', function() { + describe('replacer', function() { + it('does not throw on empty input', function() { + var result = metaHandler.replacer('', ''); + assert.equal(result, ''); + }); + + it('supports ">" in bundle', function() { + var replacement = 'result'; + + var before = testStringGenerator({ foo: '>' }); + var after = testStringGenerator(replacement); + + var result = metaHandler.replacer(before, replacement); + assert.equal(result, after); + }); + + it('only replaces the first occurrence', function() { + var replacement = 'result'; + + var before = [ + testStringGenerator({ one: 1 }), + testStringGenerator({ two: 2 }) + ]; + + var after = [ + testStringGenerator(replacement), + before[1] + ]; + + var result = metaHandler.replacer(before.join(''), replacement); + assert.equal(result, after.join('')); + }); + + it('supports newlines', function() { + var replacement = 'result'; + + var before = [ + "First line.", + testStringGenerator({ one: 1 }), + testStringGenerator({ two: 2 }) + ]; + + var after = [ + before[0], + testStringGenerator(replacement), + before[2] + ]; + + var result = metaHandler.replacer(before.join('\r\n'), replacement); + assert.equal(result, after.join('\r\n')); + }); + }); +}); diff --git a/package.json b/package.json index d81b831..5d723df 100644 --- a/package.json +++ b/package.json @@ -50,10 +50,11 @@ "ember-addon" ], "dependencies": { - "broccoli-caching-writer": "^3.0.0", + "broccoli-caching-writer": "^3.0.3", "broccoli-funnel": "^1.0.3", "broccoli-merge-trees": "^1.1.2", "ember-cli-babel": "^5.1.6", + "exists-sync": "0.0.3", "fs-extra": "^0.30.0", "walk-sync": "^0.2.7" },