Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revamp asset manifest generation #11

Merged
merged 4 commits into from
Jul 29, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,59 @@

Provides experimental support for the [Asset Manifest RFC](https://github.com/emberjs/rfcs/pull/153) and [Asset Loader Service RFC](https://github.com/emberjs/rfcs/pull/158).

## Usage

Ember Asset Loader does three primary things:

1. Provides a base class to easily generate an Asset Manifest,
2. Provides an Ember service to use the generated manifest at runtime, and
3. Initializes the above service with the above generated manifest.

### Generating an Asset Manifest

You can generate an Asset Manifest by creating either a standalone or in-repo addon which extends from the
`ManifestGenerator` base class:

```js
var ManifestGenerator = require('ember-asset-loader/lib/manifest-generator');
module.exports = ManifestGenerator.extend({
name: 'asset-generator-addon',
manifestOptions: {
bundlesLocation: 'engines-dist',
supportedTypes: [ 'js', 'css' ]
}
});
```

The `ManifestGenerator` will generate an asset manifest and merge it into your build tree during post-processing. It
generates the manifest according to the options specified in `manifestOptions`:

* The `bundlesLocation` option is a string that specifies which directory in the build tree contains the bundles to be
placed into the asset manifest. This defaults `bundles`.

* The `supportedTypes` option is an array that specifies which types of files should be included into the bundles for
the asset manifest. This defaults to `[ 'js', 'css' ]`.

_Note: This class provides default `contentFor`, `postprocessTree`, and `postBuild` hooks so be sure that you call
`_super` if you override one of those methods._

### Why isn't a manifest generated by default?

No manifest is generated by default since there is no convention for bundling assets within Ember currently. Thus, to
prevent introducing unintuitive or conflicting behavior, we provide no default generation.

If no manifest is generated, you'll get a warning at build time to ensure that you understand no manifest has been
generated and thus you'll have to provide a manifest manually in order to use the Asset Loader Service. This warning can
be disabled via the `noManifest` option from the consuming application:

```js
var app = new EmberApp(defaults, {
assetLoader: {
noManifest: true
}
});
```

## Installation

* `git clone https://github.com/trentmwillis/ember-asset-loader`
Expand Down
4 changes: 4 additions & 0 deletions bin/install-test-addons.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash

rm -rf node_modules/test-generator-plugin
ln -s ../tests/dummy/lib/test-generator-plugin node_modules/test-generator-plugin
1 change: 0 additions & 1 deletion ember-cli-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ var EmberAddon = require('ember-cli/lib/broccoli/ember-addon');

module.exports = function(defaults) {
var app = new EmberAddon(defaults, {
// Add options here
});

/*
Expand Down
49 changes: 1 addition & 48 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,6 @@
/* jshint node: true */
'use strict';

var path = require('path');
var fs = require('fs-extra');

module.exports = {
name: 'ember-asset-loader',

/**
* Generate an asset manifest from the "all" tree.
*/
postprocessTree: function(type, tree) {
if (type === 'all') {
tree = require('./lib/generate-asset-manifest')(tree); // eslint-disable-line global-require
}

return tree;
},

/**
* Insert a meta tag to hold the manifest in the DOM. We won't insert the
* manifest until after postprocessing so the content is a placeholder value.
*/
contentFor: function(type, config) {
if (type === 'head-footer') {
var metaName = config.modulePrefix + '/asset-manifest';
return '<meta name="' + metaName + '" content="%GENERATED_ASSET_MANIFEST%" />';
}
},

/**
* 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 manifestFile = path.join(result.directory, 'asset-manifest.json');
var manifest = fs.readJsonSync(manifestFile);

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

replaceAssetManifestPlaceholder: function(filePath, manifest) {
var resolvedFile = fs.readFileSync(filePath, { encoding: 'utf8' });
fs.outputFileSync(filePath, resolvedFile.replace(/%GENERATED_ASSET_MANIFEST%/, manifest));
}
name: 'ember-asset-loader'
};
83 changes: 57 additions & 26 deletions lib/asset-manifest-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@ var path = require('path');
var fs = require('fs-extra');

var DEFAULT_SUPPORTED_TYPES = [ 'js', 'css' ];
var DEFAULT_ROOT = '/assets';

function AssetManifestGenerator(inputTree, options) {
function AssetManifestGenerator(inputTrees, options) {
options = options || {};

this.prepend = options.prepend || DEFAULT_ROOT;
this.prepend = options.prepend || '';
this.supportedTypes = options.supportedTypes || DEFAULT_SUPPORTED_TYPES;

Plugin.call(this, [inputTree], {
Plugin.call(this, inputTrees, {
annotation: options.annotation
});
}
Expand All @@ -21,50 +20,82 @@ AssetManifestGenerator.prototype = Object.create(Plugin.prototype);
AssetManifestGenerator.prototype.constructor = AssetManifestGenerator;

/**
* TODO: Determine nested bundles behavior.
* Generate an asset manifest from an inputTree assuming it has the following
* convention for structure:
*
* Generate an asset manifest from an inputTree by following these rules for
* each entry:
* root/
* <bundle-name>/
* dependencies.manifest.json
* <asset>.<type>
* assets/
* <asset>.<type>
*
* 1. If the entry is a directory, create a new bundle
* 2. If the entry is an asset not in the root of the tree, add it to a bundle
* 3. If the entry is an asset in the root of the tree, do nothing
* This means that for each entry in the inputTree:
*
* The assumption is that root-level assets should be those initially loaded
* with an application while those in sub-directories are candidates for lazy
* loading.
* 1. If the entry is a root-level directory, create a new bundle
* 2. If the entry is an asset named "dependencies.manifest.json", add its
* contents as the "dependencies" for the bundle
* 3. If the entry is an asset within a bundle, add it to the bundles "assets"
* 4. Otherwise, do nothing
*
* One important note is that bundles should be flattened in the inputTree. If
* a directory is found within a bundle, it will not be treated as an additional
* bundle.
*/
AssetManifestGenerator.prototype.build = function() {
var supportedTypes = this.supportedTypes;
var prepend = this.prepend;
var manifest = walk(this.inputPaths).reduce(function(manifest, entry) {
var inputPath = this.inputPaths[0];
var existingManifestPath = this.inputPaths[1];
var existingManifest;

try {
existingManifest = fs.readJsonSync(path.join(existingManifestPath, 'asset-manifest.json'))
} catch (err) {
existingManifest = { bundles: {} };
}

var manifest = walk(inputPath).reduce(function(manifest, entry) {
var pathParts = entry.split('/');
var assetName = pathParts.pop();
var directoryName = pathParts.pop();
var bundleName = pathParts.shift();

// If there is no assetName but there is a directoryName, then we have a
// directory per the rules of walk-sync.
// If there is no assetName, then we have a directory per the rules of
// walk-sync. And, if there are no pathParts left we have a root directory.
// https://github.com/joliss/node-walk-sync/blob/master/README.md#usage
if (!assetName && directoryName) {
manifest.bundles[directoryName] = {
assets: [],
dependencies: pathParts
var isNewBundle = !assetName && pathParts.length === 0;
if (isNewBundle) {
if (manifest.bundles[bundleName]) {
throw new Error('Attempting to add bundle "' + bundleName + '" to manifest but a bundle with that name already exists.');
}

manifest.bundles[bundleName] = {
assets: []
};
} else if (assetName && directoryName) {
// If there's an assetName and a directoryName then we check that it is a
// supported type we should generate an entry for.
}

// If the asset is named "dependencies.manifest.json" then we should read
// the json in and set it as "dependencies" on the corresponding bundle.
else if (bundleName && assetName && assetName === 'dependencies.manifest.json') {
var dependencies = fs.readJsonSync(path.join(inputPath, entry));
manifest.bundles[bundleName].dependencies = dependencies;
}

// If the asset is in a bundle, then attempt to add it to the manifest by
// checking if it is a supported type.
else if (assetName && bundleName) {
var assetType = assetName.split('.').pop();

if (supportedTypes.indexOf(assetType) !== -1) {
manifest.bundles[directoryName].assets.push({
manifest.bundles[bundleName].assets.push({
uri: path.join(prepend, entry),
type: assetType
});
}
}

return manifest;
}, { bundles: {} });
}, existingManifest);

var manifestFile = path.join(this.outputPath, 'asset-manifest.json');

Expand Down
40 changes: 31 additions & 9 deletions lib/generate-asset-manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,44 @@ var Funnel = require('broccoli-funnel');
var mergeTrees = require('broccoli-merge-trees');
var AssetManifestGenerator = require('./asset-manifest-generator');

module.exports = function generateAssetManifest(tree, supportedTypes, prepend) {
// Get all the assets for this application
var assets = new Funnel(tree, {
srcDir: 'assets',
annotation: 'Assets Funnel'
/**
* Given a tree, this function will generate an asset manifest and merge it back
* into the tree.
*
* The `bundlesLocation` option specifies which directory in the tree contains
* the bundles to be placed into the asset manifest.
*
* The `supportedTypes` option specifies which types of files should be included
* into the bundles for the asset manifest.
*/
module.exports = function generateAssetManifest(tree, options) {
options = options || {};

var bundlesLocation = options.bundlesLocation || 'bundles';
var supportedTypes = options.supportedTypes;

// Get all the bundles for this application
var bundles = new Funnel(tree, {
srcDir: bundlesLocation,
annotation: 'Bundles Location Funnel'
});

// Get an existing asset-manifest if it exists
var existingManifest = new Funnel(tree, {
include: [ 'asset-manifest.json' ],
annotation: 'Existing Manifest Funnel'
});

// Generate a manifest from the assets
var manifest = new AssetManifestGenerator(assets, {
// Generate a manifest from the bundles
var manifest = new AssetManifestGenerator([ bundles, existingManifest ], {
supportedTypes: supportedTypes,
prepend: prepend,
prepend: '/' + bundlesLocation,
annotation: 'Asset Manifest Generator'
});

// Merge the manifest back into the build
return mergeTrees([ tree, manifest ], {
annotation: 'Merge Asset Manifest'
annotation: 'Merge Asset Manifest',
overwrite: true
});
};
63 changes: 63 additions & 0 deletions lib/manifest-generator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
var path = require('path');
var fs = require('fs-extra');
var Addon = require('ember-cli/lib/models/addon');

var ManifestGenerator = Addon.extend({
/**
* Insert a meta tag to hold the manifest in the DOM. We won't insert the
* manifest until after postprocessing so the content is a placeholder value.
*/
contentFor: function(type, config) {
var options = this.app && this.app.options && this.app.options.assetLoader;
if (type === 'head-footer' && !(options && options.noManifest)) {
var metaName = config.modulePrefix + '/asset-manifest';
return '<meta name="' + metaName + '" content="%GENERATED_ASSET_MANIFEST%" />';
}
},

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;
},

/**
* 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 manifestFile = path.join(result.directory, 'asset-manifest.json');
var manifest;

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: {} };
}

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

replaceAssetManifestPlaceholder: function(filePath, manifest) {
var resolvedFile = fs.readFileSync(filePath, { encoding: 'utf8' });
fs.outputFileSync(filePath, resolvedFile.replace(/%GENERATED_ASSET_MANIFEST%/, manifest));
}
});

module.exports = ManifestGenerator;
Loading