new AssetGraph([options])
Create a new AssetGraph instance.
Options
root (optional) The root URL of the graph, either as a fully
qualified file: or http: url or file system
path. Defaults to the current directory,
ie. file://<process.cwd()>/ . The purpose of the root
option is to allow resolution of root-relative urls
(eg. <a href="/foo.html"> ) from file: locations.
dieOnError (optional) Whether to throw an exception or keep
going when any error is encountered. Defaults to false.
Examples
new AssetGraph()
// => root: "file:///current/working/dir/"
new AssetGraph({root: '/absolute/fs/path'});
// => root: "file:///absolute/fs/path/"
new AssetGraph({root: 'relative/path'})
// => root: "file:///current/working/dir/relative/path/"
|
function AssetGraph(options) {
if (!(this instanceof AssetGraph)) {
return new AssetGraph(options);
}
events.EventEmitter.call(this);
_.extend(this, options);
// this.root might be undefined, in which case urlTools.urlOrFsPathToUrl will use process.cwd()
this.root = urlTools.urlOrFsPathToUrl(this.root, true); // ensureTrailingSlash
this._assets = [];
this._relations = [];
this._objInBaseAssetPaths = {};
this._relationsWithNoBaseAsset = [];
this._indices = {};
this.idIndex = {};
this.resolverByProtocol = {
data: AssetGraph.resolvers.data(),
file: AssetGraph.resolvers.file(),
javascript: AssetGraph.resolvers.javascript(),
http: AssetGraph.resolvers.http(),
https: AssetGraph.resolvers.http()
};
if (!this.dieOnError) {
this.on('error', function () {});
}
};
util.inherits(AssetGraph, events.EventEmitter);
AssetGraph.assets = require('./assets');
AssetGraph.relations = require('./relations');
AssetGraph.query = require('./query');
AssetGraph.resolvers = require('./resolvers');
_.extend(AssetGraph.prototype, {
|
assetGraph.removeAsset(asset[, detachIncomingRelations])
Remove an asset from the graph. Also removes the incoming and outgoing relations of the asset. |
removeAsset: function (asset, detachIncomingRelations) {
if (!(asset.id in this.idIndex)) {
throw new Error("AssetGraph.removeAsset: " + asset + " not in graph");
}
asset._outgoingRelations = this.findRelations({from: asset}, true);
asset._outgoingRelations.forEach(function (outgoingRelation) {
this.removeRelation(outgoingRelation);
if (outgoingRelation.to.isAsset && outgoingRelation.to.isInline) {
// Remove inline asset
this.removeAsset(outgoingRelation.to);
}
}, this);
this.findRelations({to: asset}).forEach(function (incomingRelation) {
if (detachIncomingRelations) {
incomingRelation.detach();
} else {
incomingRelation.remove();
}
}, this);
var affectedRelations = [].concat(this._objInBaseAssetPaths[asset.id]);
affectedRelations.forEach(function (affectedRelation) {
affectedRelation._unregisterBaseAssetPath();
}, this);
delete this._objInBaseAssetPaths[asset.id];
var assetIndex = this._assets.indexOf(asset);
if (assetIndex === -1) {
throw new Error("removeAsset: " + asset + " not in graph");
} else {
this._assets.splice(assetIndex, 1);
}
delete this.idIndex[asset.id];
affectedRelations.forEach(function (affectedRelation) {
affectedRelation._registerBaseAssetPath();
}, this);
delete asset.assetGraph;
this.emit('removeAsset', asset);
return this;
},
|
assetGraph.addRelation(relation, position[, adjacentRelation])
Add a relation to the graph.
The ordering of certain relation types is significant (HtmlScript , for instance), so it's important that the order isn't scrambled in the indices. Therefore the caller must explicitly specify a position at which to insert the object. |
addRelation: function (relation, position, adjacentRelation) { // position and adjacentRelation are optional
if (Array.isArray(relation)) {
relation.forEach(function (_relation) {
this.addRelation(_relation, position, adjacentRelation);
}, this);
return;
}
if (!relation || !relation.id || !relation.isRelation) {
throw new Error("AssetGraph.addRelation: Not a relation: " + relation);
}
if (relation.id in this.idIndex) {
throw new Error("AssetGraph.addRelation: Relation already in graph: " + relation);
}
if (!relation.from || !relation.from.isAsset) {
throw new Error("AssetGraph.addRelation: 'from' property of relation is not an asset: " + relation.from);
}
if (!(relation.from.id in this.idIndex)) {
throw new Error("AssetGraph.addRelation: 'from' property of relation is not in the graph: " + relation.from);
}
if (!relation.to) {
throw new Error("AssetGraph.addRelation: 'to' property of relation is missing");
}
position = position || 'last';
relation.assetGraph = this;
if (position === 'last') {
this._relations.push(relation);
} else if (position === 'first') {
this._relations.unshift(relation);
} else if (position === 'before' || position === 'after') { // Assume 'before' or 'after'
if (!adjacentRelation || !adjacentRelation.isRelation) {
throw new Error("AssetGraph.addRelation: Adjacent relation is not a relation: " + adjacentRelation);
}
var i = this._relations.indexOf(adjacentRelation) + (position === 'after' ? 1 : 0);
if (i === -1) {
throw new Error("AssetGraph.addRelation: Adjacent relation is not in the graph: " + adjacentRelation);
}
this._relations.splice(i, 0, relation);
} else {
throw new Error("AssetGraph.addRelation: Illegal 'position' argument: " + position);
}
this.idIndex[relation.id] = relation;
this._objInBaseAssetPaths[relation.id] = [];
relation._registerBaseAssetPath();
this.emit('addRelation', relation);
return this;
},
|
assetGraph.findRelations([queryObj[, includeUnpopulated]])
Query relations in the graph.
Example usage
var allRelationsInGraph = ag.findRelations();
var allHtmlScriptRelations = ag.findRelations({
type: 'HtmlScript'
});
var htmlAnchorsPointingAtLocalImages = ag.findRelations({
type: 'HtmlAnchor',
to: {isImage: true, url: /^file:/}
});
|
findRelations: function (queryObj, includeUnpopulated) {
var relations = AssetGraph.query.queryAssetGraph(this, 'relation', queryObj);
if (includeUnpopulated) {
return relations;
} else {
return relations.filter(function (relation) {
return relation.to.isAsset;
});
}
},
|
assetGraph.recomputeBaseAssets([fromScratch])
Recompute the base asset paths for all relations for which the base asset path couldn't be computed due to the graph being incomplete at the time they were added.
Usually you shouldn't have to worry about this. This method is only exposed for transforms that do certain manipulations causing to graph to temporarily be in a state where the base asset of some relations couldn't be computed, e.g. if intermediate relations are been removed and attached again.
Will throw an error if the base asset for any relation couldn't be found. |
recomputeBaseAssets: function (fromScratch) {
if (fromScratch) {
this._objInBaseAssetPaths = {};
this._relationsWithNoBaseAsset = [];
this.findAssets().forEach(function (asset) {
this._objInBaseAssetPaths[asset.id] = [];
}, this);
this.findRelations({}, true).forEach(function (relation) {
this._objInBaseAssetPaths[relation.id] = [];
delete relation._baseAssetPath;
}, this);
this.findRelations({}, true).forEach(function (relation) {
relation._registerBaseAssetPath();
}, this);
} else {
[].concat(this._relationsWithNoBaseAsset).forEach(function (relation) {
relation._unregisterBaseAssetPath();
if (!relation._registerBaseAssetPath()) {
throw new Error("recomputeBaseAssets: Couldn't find base asset for " + relation);
}
}, this);
}
return this;
},
// Traversal:
eachAssetPreOrder: function (startAssetOrRelation, relationQueryObj, lambda) {
if (!lambda) {
lambda = relationQueryObj;
relationQueryObj = null;
}
this._traverse(startAssetOrRelation, relationQueryObj, lambda);
},
eachAssetPostOrder: function (startAssetOrRelation, relationQueryObj, lambda) {
if (!lambda) {
lambda = relationQueryObj;
relationQueryObj = null;
}
this._traverse(startAssetOrRelation, relationQueryObj, null, lambda);
},
_traverse: function (startAssetOrRelation, relationQueryObj, preOrderLambda, postOrderLambda) {
var that = this,
relationQueryMatcher = relationQueryObj && AssetGraph.query.createValueMatcher(relationQueryObj),
startAsset,
startRelation;
if (startAssetOrRelation.isRelation) {
startRelation = startAssetOrRelation;
startAsset = startRelation.to;
} else {
// incomingRelation will be undefined when (pre|post)OrderLambda(startAsset) is called
startAsset = startAssetOrRelation;
}
var seenAssets = {},
assetStack = [];
(function traverse(asset, incomingRelation) {
if (!seenAssets[asset.id]) {
if (preOrderLambda) {
preOrderLambda(asset, incomingRelation);
}
seenAssets[asset.id] = true;
assetStack.push(asset);
that.findRelations(_.extend({from: asset})).forEach(function (relation) {
if (!relationQueryMatcher || relationQueryMatcher(relation)) {
traverse(relation.to, relation);
}
});
var previousAsset = assetStack.pop();
if (postOrderLambda) {
postOrderLambda(previousAsset, incomingRelation);
}
}
}(startAsset, startRelation));
},
collectAssetsPreOrder: function (startAssetOrRelation, relationQueryObj) {
var assetsInOrder = [];
this.eachAssetPreOrder(startAssetOrRelation, relationQueryObj, function (asset) {
assetsInOrder.push(asset);
});
return assetsInOrder;
},
collectAssetsPostOrder: function (startAssetOrRelation, relationQueryObj) {
var assetsInOrder = [];
this.eachAssetPostOrder(startAssetOrRelation, relationQueryObj, function (asset) {
assetsInOrder.push(asset);
});
return assetsInOrder;
},
// Transforms:
_runTransform: function (transform, cb) {
var that = this,
startTime = new Date(),
done = passError(cb, function () {
that.emit('afterTransform', transform, new Date().getTime() - startTime);
cb(null, that);
});
that.emit('beforeTransform', transform);
if (transform.length < 2) {
process.nextTick(function () {
try {
transform(that);
} catch (err) {
return done(err);
}
done();
});
} else {
var callbackCalled = false;
transform(that, function (err) {
if (callbackCalled) {
console.warn("AssetGraph._runTransform: The transform " + transform.name + " called the callback more than once!");
} else {
callbackCalled = true;
done(err);
}
});
}
return that;
}
});
// Add AssetGraph helper methods that implicitly create a new TransformQueue:
['if', 'queue'].forEach(function (methodName) {
AssetGraph.prototype[methodName] = function () { // ...
var transformQueue = new TransformQueue(this);
return transformQueue[methodName].apply(transformQueue, arguments);
};
});
function TransformQueue(assetGraph) {
this.assetGraph = assetGraph;
this.transforms = [];
this.conditions = [];
}
TransformQueue.prototype = {
queue: function () { // ...
if (!this.conditions.length || this.conditions[this.conditions.length - 1]) {
Array.prototype.push.apply(this.transforms, arguments);
}
return this;
},
if: function (condition) {
this.conditions.push(condition);
return this;
},
else: function () {
if (!this.conditions.length) {
throw new Error('else: No condition on the stack');
}
this.conditions.push(!this.conditions.pop());
return this;
},
endif: function () {
if (!this.conditions.length) {
throw new Error('endif: No condition on the stack');
}
this.conditions.pop();
return this;
},
run: function (cb) {
var that = this,
nextTransform;
// Skip past falsy transforms:
do {
nextTransform = that.transforms.shift();
} while (!nextTransform && that.transforms.length);
if (nextTransform) {
that.assetGraph._runTransform(nextTransform, function (err) {
if (err) {
if (cb) {
cb(err);
} else {
that.assetGraph.emit('error', err);
}
} else {
that.run(cb);
}
});
} else if (cb) {
cb(null, that.assetGraph);
}
}
};
// Pre-ES5 alternative for the 'if' method:
TransformQueue.prototype.if_ = TransformQueue.prototype.if;
AssetGraph.prototype.if_ = AssetGraph.prototype.if;
AssetGraph.transforms = {};
AssetGraph.registerTransform = function (functionOrFileName, name) {
if (typeof functionOrFileName === 'function') {
name = name || functionOrFileName.name;
AssetGraph.transforms[name] = functionOrFileName;
} else {
// File name
name = name || Path.basename(functionOrFileName, '.js');
functionOrFileName = Path.resolve(process.cwd(), functionOrFileName); // Absolutify if not already absolute
AssetGraph.transforms.__defineGetter__(name, function () {
return require(functionOrFileName);
});
}
TransformQueue.prototype[name] = function () { // ...
if (!this.conditions.length || this.conditions[this.conditions.length - 1]) {
this.transforms.push(AssetGraph.transforms[name].apply(this, arguments));
}
return this;
};
// Make assetGraph.(options) a shorthand for creating a new TransformQueue:
AssetGraph.prototype[name] = function () { // ...
var transformQueue = new TransformQueue(this);
return transformQueue[name].apply(transformQueue, arguments);
};
};
// Register ./transforms/*:
require('fs').readdirSync(Path.resolve(__dirname, 'transforms')).forEach(function (fileName) {
AssetGraph.registerTransform(Path.resolve(__dirname, 'transforms', fileName));
});
module.exports = AssetGraph;
|