AssetGraph documentation

Asset

lib/assets/Asset.js
  • class: Asset

  • extends: EventEmitter

An asset object represents a single node in an AssetGraph, but can be used and manipulated on its own outside the graph context. It can even be present in multiple AssetGraphs at once.

var EventEmitter = require('events').EventEmitter,
    path = require('path'),
    util = require('util'),
    _ = require('underscore'),
    extendWithGettersAndSetters = require('../util/extendWithGettersAndSetters'),
    passError = require('../util/passError'),
    uniqueId = require('../util/uniqueId'),
    urlEndsWithSlashRegExp = /\/(?:[?#].*)?$/;

new Asset(options)

Create a new Asset instance.

Most of the time it's unnecessary to create asset objects directly. When you need to manipulate assets that already exist on disc or on a web server, the loadAssets and populate transforms are the easiest way to get the objects created. See the section about transforms below.

Note that the Asset base class is only intended to be used to represent assets for which there's no specific subclass.

Options

  • rawSrc Buffer object containing the raw source of the asset. Mandatory unless the rawSrcProxy option is provided.
  • rawSrcProxy Function that provides the raw source of the asset to a callback (and optionally a metadata object), for example by loading it from disc or fetching it via http. Mandatory unless the rawSrc option is provided.
  • contentType (optional) The Content-Type (MIME type) of the asset. For subclasses of Asset there will be a reasonable default. Can also be provided by the rawSrcProxy in the metadata object.
  • url (optional) The fully qualified (absolute) url of the asset. If not provided, the asset will be considered inline. Can also be provided by the rawSrcProxy in the `metadata' object (think HTTP redirects).
  • extension The desired file name extension of the asset. Will be extracted from the url option if possible, and in that case, the extension option will be ignored.
function Asset(config) {
    EventEmitter.call(this);
    if (config.rawSrc) {
        this._rawSrc = config.rawSrc;
        delete config.rawSrc;
    }
    if (config.parseTree) {
        this._parseTree = config.parseTree;
        delete config.parseTree;
    }
    if (config.url) {
        this._url = config.url;
        if (!urlEndsWithSlashRegExp.test(this._url)) {
            this._extension = path.extname(this.url.replace(/[?#].*$/, ''));
        }
        delete config.url;
    } else if (config.extension) {
        if (!this._extension) {
            this._extension = config.extension;
        }
        delete config.extension;
    }
    if (config.outgoingRelations) {
        this._outgoingRelations = config.outgoingRelations;
        delete config.outgoingRelations;
    }
    _.extend(this, config);
    this.id = uniqueId();
}

util.inherits(Asset, EventEmitter);

extendWithGettersAndSetters(Asset.prototype, {

asset.isAsset

{Boolean} Property that's true for all Asset instances. Avoids reliance on the instanceof operator.

  • api: public

isAsset: true,

asset.contentType

{String} The Content-Type (MIME type) of the asset.

  • api: public

contentType: 'application/octet-stream',

asset.defaultExtension

{String} The default extension for the asset type.

defaultExtension: '',

asset.parseTree (getter)

Some asset classes support inspection and manipulation using a high level interface. If you modify the parse tree, you have to call asset.markDirty() so any cached serializations of the asset are invalidated.

These are the formats you'll get:

assets.Html and assets.Xml: jsdom <https://github.com/tmpvar/jsdom>_ document object.

assets.Css CSSOM <https://github.com/NV/CSSOM>_ CSSStyleSheet object.

assets.JavaScript UglifyJS <https://github.com/mishoo/UglifyJS>_ AST object.

assets.Json Regular JavaScript object (the result of JSON.parse on the decoded source).

assets.CacheManifest A JavaScript object with a key for each section present in the manifest (CACHE, NETWORK, REMOTE). The value is an array with an item for each entry in the section. Refer to the source for details.

  • api: public

asset.load(cb)

Makes sure the asset is loaded, then calls the supplied callback. This is Asset's only async method, as soon as it is loaded, everything can happen synchronously.

Usually you'll want to use transforms.loadAssets, which will handle this automatically.

  • param: Function cb The callback to invoke when the asset is

loaded.

load: function (cb) {
        var that = this;
        if (that._rawSrc || that._parseTree) {
            process.nextTick(cb);
        } else if (that.rawSrcProxy) {
            that.rawSrcProxy(passError(cb, function (rawSrc, metadata) {
                that._rawSrc = rawSrc;
                if (metadata) {
                    _.extend(this, metadata); // Might change url, contentType and encoding
                }
                delete that.rawSrcProxy;
                cb();
            }));
        } else {
            process.nextTick(function () {
                cb(new Error("Asset.load: No rawSrc or rawSrcProxy found, cannot load"));
            });
        }
    },

asset.extension (getter/setter)

The file name extension for the asset (String). It is automatically kept in sync with the url, but preserved if the asset is inlined or set to a value that ends with a slash.

If updated, the url of the asset will also be updated.

The extension includes the leading dot and is thus kept in the same format as require('path').extname and the basename command line utility use.

  • return: String The extension, eg. ".html" or ".css".

get extension() {
        if ('_extension' in this) {
            return this._extension;
        } else {
            return this.defaultExtension;
        }
    },

    set extension(extension) {
        this._extension = extension;
        if (this.url) {
            this.url = this.url.replace(/(?:\.\w+)?([?#]|$)/, this._extension + "$1");
        }
    },

asset.rawSrc (getter/setter)

Get or set the raw source of the asset.

If the internal state has been changed since the asset was initialized, it will automatically be reserialized when this property is retrieved, for example:

var htmlAsset = new assets.Html({
    rawSrc: new Buffer('<html><body>Hello!</body></html>')
});
htmlAsset.parseTree.body.innerHTML = "Bye!";
htmlAsset.markDirty();
htmlAsset.rawSrc.toString(); // "<body>Bye!</body>"

Setting this property after the outgoing relations have been accessed currently leads to undefined behavior.

  • return: Buffer The raw source.

get rawSrc() {
        if (!this._rawSrc) {
            throw new Error("Asset.rawSrc getter: Asset isn't loaded");
        }
        return this._rawSrc;
    },

    set rawSrc(rawSrc) {
        this._rawSrc = rawSrc;
        this.markDirty();
    },

asset.url (getter/setter)

Get or set the absolute url of the asset (String).

The url will use the file: schema if loaded from disc. Will be falsy for inline assets.

Setting the url causes the asset to emit the setUrl event with the asset object, the new url, and the old url as parameters, in that order. It will also update the extension property if one can be extracted from the new url.

get url() {
        return this._url;
    },

    set url(url) {
        var oldUrl = this._url;
        this._url = url;
        this.emit('setUrl', this, url, oldUrl);

        if (url && !urlEndsWithSlashRegExp.test(url)) {
            var extension = path.extname(this.url.replace(/[?#].*$/, ''));
            if (extension) {
                this._extension = extension;
            }
        }
    },

asset.isInline (getter)

Determine whether the asset is inline (shorthand for checking whether it has a url).

  • return: Boolean Whether the asset is inline.

get isInline() {
        return !this.url;
    },

asset.markDirty()

Sets the dirty flag of the asset, which is the way to say that the asset has been manipulated since it was first loaded (read from disc or loaded via http). For inline assets the flag is set if the asset has been manipulated since it was last synchronized with (copied into) its containing asset.

For assets that support a text or parseTree property, calling markDirty() will invalidate any cached serializations of the asset,

markDirty: function () {
        this.isDirty = true;
        delete this._rawSrc; // Hmm, this doesn't make sense for assets.Asset, now does it?
        this.emit('dirty', this);
    },

asset.outgoingRelations (getter)

Get the outgoing relations of the asset. Only supported by a few subclasses (Css, Html, CacheManifest, and JavaScript), all others return an empty array.

  • return: Array[Relation] The outgoing relations.

get outgoingRelations() {
        if (!this._outgoingRelations) {
            this._outgoingRelations = [];
        }
        return this._outgoingRelations;
    },

asset.toString()

Get a brief text containing the type, id, and url (if not inline) of the asset.

  • return: String The string, eg. "[JavaScript/141 file:///the/thing.js]"

toString: function () {
        return "[" + this.type + "/" + this.id + (this.url ? " " + this.url : "") + "]";
    }
});

module.exports = Asset;

Text

lib/assets/Text.js
  • class: Text

  • extends: Asset

var util = require('util'),
    _ = require('underscore'),
    iconv = require('iconv'),
    extendWithGettersAndSetters = require('../util/extendWithGettersAndSetters'),
    Asset = require('./Asset');

new Text(options)

Create a new Text asset instance.

Adds text encoding and decoding support to Asset. Serves as a superclass for Html, Xml, Css, JavaScript, CoffeeScript, Json, and CacheManifest.

In addition to the options already supported by the Asset base class, these options are supported:

  • text (String) The decoded source of the asset. Can be used instead of rawSrc and rawSrcProxy.
  • encoding (String) Used to decode and reencode the rawSrc. Can be any encoding supported by the iconv module. Can be changed later using the encoding getter/setter. Defaults to "utf-8" (see the docs for defaultEncoding below).

Example

var textAsset = new Text({
    // "æøå" in iso-8859-1:
    rawSrc: new Buffer([0xe6, 0xf8, 0xe5]),
    encoding: "iso-8859-1"
});
textAsset.text; // "æøå" (decoded JavaScript string)
textAsset.encoding = 'utf-8';
textAsset.rawSrc; // <Buffer c3 a6 c3 b8 c3 a5>

function Text(config) {
    if ('text' in config) {
        this._text = config.text;
        delete config.text;
    }
    if (config.encoding) {
        this._encoding = config.encoding;
        delete config.encoding;
    }
    Asset.call(this, config);
}

util.inherits(Text, Asset);

extendWithGettersAndSetters(Text.prototype, {

text.isText

Property that's true for all Text instances. Avoids reliance on the instanceof operator.

isText: true,

text.defaultEncoding

The default encoding for the Text (sub)class. Used for decoding the raw source when the encoding cannot be determined by other means, such as a Content-Type header (when the asset was fetched via http), or another indicator specific to the given asset type (@charset for Css, <meta http-equiv="Content-Type" ...> for Html).

Factory setting is "utf-8", but you can override it by setting Text.prototype.defaultEncoding to another value supported by the iconv module.

defaultEncoding: 'utf-8',

    defaultExtension: '.txt',

    alternativeExtensions: ['.xtemplate'], // For Ext.XTemplate + one.getText

    contentType: 'text/plain',

    load: function (cb) {
        if ('_text' in this || this._parseTree) {
            process.nextTick(cb);
        } else {
            Asset.prototype.load.call(this, cb);
        }
    },

Text.encoding (getter/setter)

Get or set the encoding (charset) used for re-encoding the raw source of the asset. To affect the initial decoding of the rawSrc option, provide the encoding option to the constructor.

get encoding() {
        if (!this._encoding) {
            this._encoding = this.defaultEncoding;
        }
        return this._encoding;
    },

    set encoding(encoding) {
        if (encoding !== this.encoding) {
            var text = this.text; // Make sure this._text exists so the rawSrc is decoded before the original encoding is thrown away
            delete this._rawSrc;
            this._encoding = encoding;
            this.markDirty();
        }
    },

    get rawSrc() {
        if (!this._rawSrc) {
            if ('_text' in this || this._parseTree) {
                if (/^utf-?8$/i.test(this.encoding)) {
                    this._rawSrc = new Buffer(this.text);
                } else {
                    this._rawSrc = new iconv.Iconv('utf-8', this.encoding).convert(this.text);
                }
            } else {
                throw new Error("assets.Text.rawSrc getter: No _rawSrc or _text property found, asset not loaded?");
            }
        }
        return this._rawSrc;
    },

    set rawSrc(rawSrc) {
        this._rawSrc = rawSrc;
        delete this._parseTree;
        delete this._text;
        this.markDirty();
    },

text.text (getter/setter)

Get or set the decoded text contents of the of the asset as a JavaScript string. Unlike browsers AssetGraph doesn't try to sniff the charset of your text-based assets. It will fall back to assuming utf-8 if it's unable to determine the encoding/charset from HTTP headers, <meta http-equiv='Content-Type'> tags (Html), @charset (Css), so if for some reason you're not using utf-8 for all your text-based assets, make sure to provide those hints. Other asset types provide no standard way to specify the charset within the file itself, so presently there's no way to load eg. JavaScript from disc if it's not utf-8 or ASCII, except by overriding Text.prototype.defaultEncoding globally.

If the internal state has been changed since the asset was initialized, it will automatically be reserialized when the text property is retrieved, for example:

var htmlAsset = new Html({
    rawSrc: new Buffer("<body>hello</body>");
});
htmlAsset.text; // "<body>hello</body>"
htmlAsset.parseTree.body.innerHTML = "bye";
htmlAsset.markDirty();
htmlAsset.text; // "<body>bye</body>"

Setting this property after the outgoing relations have been accessed currently leads to undefined behavior.

get text() {
        if (!('_text' in this)) {
            this._text = this._getTextFromRawSrc();
        }
        return this._text;
    },

    set text(text) {
        this._text = text;
        delete this._rawSrc;
        delete this._parseTree;
        this.markDirty();
    },

    markDirty: function () {
        if (this._parseTree) {
            delete this._text;
        }
        Asset.prototype.markDirty.call(this);
    },

    _getTextFromRawSrc: function () {
        if (!this._rawSrc) {
            throw new Error("assets.Text._getTextFromRawSrc(): No _rawSrc property found.");
        }
        if (/^utf-?8$/i.test(this.encoding)) {
            return this._rawSrc.toString('utf-8');
        } else {
            return new iconv.Iconv(this.encoding, 'utf-8').convert(this._rawSrc).toString('utf-8');
        }
    },

    // More efficient than Asset._clone for text-based assets:
    _clone: function () {
        return new this.constructor({
            encoding: this.encoding,
            text: this.text
        });
    }
});

module.exports = Text;

Relation

lib/relations/Relation.js
  • class: Relation

In graph terminology a relation represents a directed edge, a reference from one asset to another. For the purpose of being able to treat all relations equally, there's a subclass for each supported relation type, encapsulating the details of how to retrieve, update, and (optionally) inline the asset being pointed to.

These are some examples of included subclasses:

  • relations.HtmlAnchor An anchor tag in an HTML document <a href='...'>.
  • relations.HtmlImage An <img src='...'> tag in an HTML document.
  • relations.CssImport An @import declaration in a CSS asset.
  • relations.CacheManifestEntry A line in a cache manifest.
var _ = require('underscore'),
    extendWithGettersAndSetters = require('../util/extendWithGettersAndSetters'),
    urlTools = require('../util/urlTools'),
    uniqueId = require('../util/uniqueId'),
    query = require('../query');

new Relation(options)

Create a new Relation instance. For existing assets the instantiation of relations happens automatically if you use the populate transform. You only need to create relations manually when you need to introduce new ones.

Note that the base Relation class should be considered abstract. Please instantiate the appropriate subclass.

Options

  • from The source asset of the relation.
  • to The target asset of the relation, or an asset configuration object if the target asset hasn't yet been resolved and created.
function Relation(config) {
    _.extend(this, config);
    this.id = uniqueId();
}

Relation.prototype = {

relation.from (Asset)

The source asset of the relation.

relation.to (Asset or asset config object)

The target asset of the relation. If the relation hasn't yet been resolved, it can also be a relative url string or an asset configuration object.

relation.href (getter/setter)

Get or set the href of the relation. The relation must be attached to an asset.

What is actually retrieved or updated depends on the relation type. For HtmlImage the src attribute of the HTML element is changed, for CssImport the parsed representation of the @import rule is updated, etc.

Most of the time you don't need to think about this property, as the href is automatically updated when the url of the source or target asset is changed, or an intermediate asset is inlined.

  • api: public

relation.isRelation (boolean)

Property that's true for all relation instances. Avoids reliance on the instanceof operator.

isRelation: true,

relation.baseAssetQuery (Object)

Subclass-specific query object used for finding the base asset for the relation (the asset whose url should be the basis for resolving the href of the relation). This is usually the first non-inline asset, but for some relation types it's the first Html document.

You shouldn't need to worry about this.

  • api: public

baseAssetQuery: {isInline: false},

relation.detach()

Detaches the relation from the asset it is currently attached to. You probably want to use AssetGraph.detachAndRemoveRelation() instead.

  • api: public

detach: function () {
        var indexInOutgoingRelations = this.from.outgoingRelations.indexOf(this);
        if (indexInOutgoingRelations === -1) {
            throw new Error("relations.Relation.detach: Relation " + this + " not found in the outgoingRelations array");
        }
        this.from.outgoingRelations.splice(indexInOutgoingRelations, 1);
        this.from.markDirty();
    },

relation.toString()

Get a brief text containing the type, id of the relation. Will also contain the .toString() of the relation's source and target assets if available.

  • return: String The string, eg. "[HtmlAnchor/141: [Html/40 file:///foo/bar/index.html] => [Html/76 file:///foo/bar/otherpage.html]]"

toString: function () {
        return "[" + this.type + "/" + this.id + ": " + ((this.from && this.to) ? this.from.toString() + " => " + this.to.toString() : "unattached") + "]";
    }
};

module.exports = Relation;

AssetGraph

lib/AssetGraph.js
  • class: AssetGraph

  • extends: EventEmitter

var util = require('util'),
    events = require('events'),
    Path = require('path'),
    _ = require('underscore'),
    urlTools = require('./util/urlTools'),
    passError = require('./util/passError');

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.

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/"
  • constructor: AssetGraph

  • param: Object options

  • api: public

function AssetGraph(options) {
    if (!(this instanceof AssetGraph)) {
        return new AssetGraph(options);
    }
    events.EventEmitter.call(this);
    _.extend(this, options);
    if (!('root' in this)) {
        this.root = urlTools.fsDirToFileUrl(process.cwd());
    } else if (!/^[a-z0-9]+:/.test(this.root)) { // No protocol, assume local file system path
        this.root = urlTools.fsDirToFileUrl(this.root);
    } else {
        this.root = urlTools.ensureTrailingSlash(this.root);
    }
    this._assets = [];
    this._relations = [];
    this._baseAssetPathForRelation = {};
    this._objInBaseAssetPaths = {};
    this._relationsWithNoBaseAsset = [];
    this._indices = {};
    this.urlIndex = {};
    this.idIndex = {};
    this.resolverByProtocol = {
        data: AssetGraph.resolvers.data(),
        file: AssetGraph.resolvers.file(),
        http: AssetGraph.resolvers.http(),
        https: AssetGraph.resolvers.http()
    };
    _.each(AssetGraph.query.indices, function (indexNames, indexType) {
        this._indices[indexType] = {};
        indexNames.forEach(function (indexName) {
            this._indices[indexType][indexName] = {};
        }, this);
    }, this);
};

util.inherits(AssetGraph, events.EventEmitter);

AssetGraph.assets = require('./assets');
AssetGraph.relations = require('./relations');
AssetGraph.transforms = require('./transforms');
AssetGraph.query = require('./query');
AssetGraph.resolvers = require('./resolvers');

_.extend(AssetGraph.prototype, {

assetGraph.root

The absolute root url of the graph, always includes a trailing slash. A normalized version of the root option provided to the constructor.

assetGraph.addAsset(asset)

Add an asset to the graph.

  • param: Asset asset The asset to add.

  • api: public

addAsset: function (asset) {
        if (!asset || !asset.id || !asset.isAsset) {
            throw new Error("AssetGraph.addAsset: " + asset + " is not an asset");
        }
        if (asset.id in this.idIndex) {
            throw new Error("AssetGraph.addAsset: " + asset + " already in graph");
        }
        if (asset.url && asset.url in this.urlIndex) {
            throw new Error("AssetGraph.addAsset: " + asset.url + " already loaded");
        }
        this._assets.push(asset);
        this._addToIndices(asset);
        this._objInBaseAssetPaths[asset.id] = [];

        var bindAndMark = function (fn, scope) {
            var bound = fn.bind(scope);
            bound._belongsToAssetGraph = this; // So we can recognize our listener when unsubscribing to the events in removeAsset
            return bound;
        }.bind(this);

        asset
            .on('setUrl', bindAndMark(this._updateUrlIndex, this))
            .on('serialize', bindAndMark(this._refreshInlineRelations, this))
            .on('dirty', bindAndMark(this._markContainingAssetDirtyIfInline, this));
        this.emit('addAsset', asset);
    },

assetGraph.removeAsset(asset[, detachIncomingRelations])

Remove an asset from the graph. Also removes the incoming and outgoing relations of the asset.

  • param: Asset asset The asset to remove.

  • param: Boolean detachIncomingRelations Whether to also detach the incoming relations before removal (defaults to false).

removeAsset: function (asset, detachIncomingRelations) {
        if (!(asset.id in this.idIndex)) {
            throw new Error("AssetGraph.removeAsset: " + asset + " not in graph");
        }
        ['setUrl', 'serialize', 'dirty'].forEach(function (eventName) {
            var listeners = asset.listeners(eventName);
            for (var i = 0 ; i < listeners.length ; i += 1) {
                if (listeners[i]._belongsToAssetGraph === this) {
                    listeners.splice(i, 1);
                    i -= 1;
                }
            }
        }, this);
        var incomingRelations = this.findRelations({to: asset});
        this.findRelations({from: asset}, true).forEach(function (outgoingRelation) {
            this.removeRelation(outgoingRelation);
            if (outgoingRelation.to.isAsset && outgoingRelation.to.isInline) {
                // Remove inline asset
                this.removeAsset(outgoingRelation.to);
            }
        }, this);
        if (incomingRelations.length) {
            incomingRelations.forEach(function (incomingRelation) {
                if (detachIncomingRelations) {
                    this.detachAndRemoveRelation(incomingRelation);
                } else {
                    this.removeRelation(incomingRelation);
                }
            }, this);
        }
        var affectedRelations = [].concat(this._objInBaseAssetPaths[asset.id]);
        affectedRelations.forEach(function (affectedRelation) {
            this._unregisterBaseAssetPath(affectedRelation);
        }, 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);
        }
        this._removeFromIndices(asset);
        affectedRelations.forEach(function (affectedRelation) {
            this._registerBaseAssetPath(affectedRelation);
        }, this);
        this.emit('removeAsset', asset);
    },

assetGraph.removeRelation(relation)

Remove a relation from the graph. Leaves the relation attached to the source asset (compare with the detachAndRemoveRelation method).

  • param: Relation relation The relation to remove.

  • api: public

removeRelation: function (relation) {
        if (!relation || !relation.isRelation) {
            throw new Error("AssetGraph.removeRelation: Not a relation: ", relation);
        }
        if (!(relation.id in this.idIndex)) {
            throw new Error("AssetGraph.removeRelation: " + relation + " not in graph");
        }
        var affectedRelations = [].concat(this._objInBaseAssetPaths[relation.id]);
        affectedRelations.forEach(function (affectedRelation) {
            this._unregisterBaseAssetPath(affectedRelation);
        }, this);
        this._unregisterBaseAssetPath(relation);
        this._removeFromIndices(relation);
        var relationIndex = this._relations.indexOf(relation);
        if (relationIndex === -1) {
            throw new Error("removeRelation: " + relation + " not in graph");
        } else {
            this._relations.splice(relationIndex, 1);
        }
        delete this._objInBaseAssetPaths[relation.id];
        affectedRelations.forEach(function (affectedRelation) {
            this._registerBaseAssetPath(affectedRelation);
        }, this);
        this.emit('removeRelation', relation);
    },

assetGraph.detachAndRemoveRelation(relation)

Detach a relation from its source asset, then remove it from the graph.

  • param: Relation relation The relation to detach and remove.

  • api: public

detachAndRemoveRelation: function (relation) {
        if (!relation || !relation.isRelation) {
            throw new Error("AssetGraph.detachAndRemoveRelation: Not a relation: ", relation);
        }
        relation.detach(relation);
        relation.from.markDirty();
        this.removeRelation(relation);
    },

assetGraph.findAssets([queryObj])

Query assets in the graph.

Example usage:

var allAssetsInGraph = ag.findAssets();

var htmlAssets = ag.findAssets({type: 'Html'});

var localImageAssets = ag.findAssets({
    url: /^file:.*\.(?:png|gif|jpg)$/
});

var orphanedJavaScriptAssets = ag.findAssets(function (asset) {
    return asset.type === 'JavaScript' &&
           ag.findRelations({to: asset}).length === 0;
});

var textBasedAssetsOnGoogleCom = ag.findAssets({
    isText: true,
    url: /^https?:\/\/(?:www\.)google\.com\//
});

  • param: Object queryObj (optional). Will match all assets if not provided.

  • return: Array The found assets.

  • api: public

findAssets: function (queryObj) {
        return AssetGraph.query.queryAssetGraph(this, 'asset', queryObj);
    },

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:/}
});

  • param: Object queryObj (optional). Will match all relations if not provided.

  • param: Boolean includeUnpopulated (optional). Whether to also consider relations that weren't followed during population. Defaults to false.

  • return: Array The found relations.

  • api: public

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.inlineRelation(relation)

Inline a relation. This is only supported by certain relation types and will produce different results depending on the type (data: url, inline script, inline stylesheet...).

Will make a clone of the target asset if it has more incoming relations than the one being inlined.

  • param: Relation relation The relation to inline.

  • api: public

inlineRelation: function (relation) {
        if (!relation || !relation.isRelation) {
            throw new Error("AssetGraph.inlineRelation: Not a relation: ", relation);
        }
        var relationsToAssetToBeInlined = this.findRelations({to: relation.to});
        if (relationsToAssetToBeInlined.length !== 1) {
            // This isn't the only incoming relation, clone the asset before inlining it.
            this.cloneAsset(relation.to, [relation]);
        }
        if (!relation.to.isInline) {
            var affectedRelations = [].concat(this._objInBaseAssetPaths[relation.to.id]);
            affectedRelations.forEach(function (affectedRelation) {
                this._unregisterBaseAssetPath(affectedRelation);
            }, this);

            relation.to.url = null;
            affectedRelations.forEach(function (affectedRelation) {
                this._registerBaseAssetPath(affectedRelation);
                this.refreshRelationHref(affectedRelation);
            }, this);
        }
        this._refreshInlineRelations(relation.to);
        relation._inline();
        relation.from.markDirty();
    },

assetGraph.refreshRelationHref(relation)

Update href of a relation to make sure it points at the current url of its target asset.

It's not necessary to call this function manually as long as the source and target assets of the relation have only been moved by having their url property changed (the recommended way), but some transforms will need this after some low-level surgery, such as attaching an existing relation to a different asset.

  • param: Relation relation The relation that should have its href refreshed.

  • api: public

refreshRelationHref: function (relation) {
        if (!relation || !relation.isRelation || !relation.to) {
            throw new Error('AssetGraph.refreshRelationHref: Not a relation: ', relation);
        }
        if (!relation.to.isInline) {
            var relativeUrl = urlTools.buildRelativeUrl(this.getBaseAssetForRelation(relation).url, relation.to.url);
            if (relation.href !== relativeUrl) {
                relation.href = relativeUrl;
                relation.from.markDirty();
            }
        }
    },

assetGraph.clone()

Clone the AssetGraph instance. The set of assets and relations won't be cloned, they will be shared between this the original and cloned graph, so changing urls and hrefs or attaching and detaching of relations will affect both graphs.

  • return: AssetGraph The clone.

  • api: public

clone: function () {
        var clone = new AssetGraph({root: this.root});
        this._assets.forEach(function (asset) {
            clone.addAsset(asset);
        });
        this._relations.forEach(function (relation) {
            clone.addRelation(relation, 'last'); // Preserve original order
        });
        return clone;
    },

assetGraph.updateRelationTarget(relation, newTargetAsset)

Point a relation at a different asset. Saves you from having to remove the relation, update its to property, then add it again at the right position.

  • param: Relation relation The relation to update.

  • param: Asset newTargetAsset The new target asset.

  • api: public

updateRelationTarget: function (relation, newTargetAsset) {
        if (!relation || !relation.isRelation) {
            throw new Error("AssetGraph.updateRelationTarget: Not a relation: ", relation);
        }
        if (!newTargetAsset || !newTargetAsset.isAsset) {
            throw new Error("AssetGraph.updateRelationTarget: Target is not an asset: ", newTargetAsset);
        }
        var oldGlobalPosition = this._relations.indexOf(relation),
            oldTypeIndexPosition = this._indices.relation.type[relation.type].indexOf(relation),
            oldFromIndexPosition = this._indices.relation.from[relation.from.id].indexOf(relation);
        this.removeRelation(relation);
        relation.to = newTargetAsset;
        this.addRelation(relation);
        this._indices.relation.from[relation.from.id].splice(this._indices.relation.from[relation.from.id].indexOf(relation), 1);
        this._indices.relation.from[relation.from.id].splice(oldFromIndexPosition, 0, relation);
        this._indices.relation.type[relation.type].splice(this._indices.relation.type[relation.type].indexOf(relation), 1);
        this._indices.relation.type[relation.type].splice(oldTypeIndexPosition, 0, relation);
        this._relations.splice(this._relations.indexOf(relation), 1);
        this._relations.splice(oldGlobalPosition, 0, relation);
        this.refreshRelationHref(relation);
    },

assetGraph.replaceAsset(oldAsset, newAsset)

Replace an asset in the graph with another asset, then remove the old asset from the graph.

Updates the incoming relations of the old asset to point at the new one and preserves the url of the old asset if it's not inline.

  • param: Asset oldAsset The asset to replace.

  • param: Asset newAsset The asset to put in its place.

  • api: public

replaceAsset: function (oldAsset, newAsset) {
        if (!oldAsset || !oldAsset.isAsset) {
            throw new Error("AssetGraph.replaceAsset: oldAsset is not an asset: ", oldAsset);
        }
        if (!newAsset || !newAsset.isAsset) {
            throw new Error("AssetGraph.replaceAsset: newAsset is not an asset: ", newAsset);
        }
        if (!(oldAsset.id in this.idIndex)) {
            throw new Error("AssetGraph.replaceAsset: Old asset isn't in the graph: " + oldAsset);
        }
        if (newAsset.id in this.idIndex) {
            throw new Error("AssetGraph.replaceAsset: New asset is already in the graph: " + newAsset);
        }
        this.addAsset(newAsset);
        this.findRelations({to: oldAsset}).forEach(function (incomingRelation) {
            this.updateRelationTarget(incomingRelation, newAsset);
        }, this);
        this.removeAsset(oldAsset);
        if (oldAsset.url) {
            newAsset.url = oldAsset.url;
        }
    },

assetGraph.cloneAsset(asset[, incomingRelations])

Clone an asset and add the clone to the graph. As an extra service, optionally update some caller-specified relations to point at the clone.

Makes up an url for the clone if the original asset wasn't inline.

  • param: Asset asset The asset to clone.

  • param: Array incomingRelations (optional) Some incoming relations that should be pointed at the clone.

  • return: Asset The cloned asset.

  • api: public

cloneAsset: function (asset, incomingRelations) {
        if (!asset || !asset.isAsset) {
            throw new Error("AssetGraph.cloneAsset: Not an asset: ", asset);
        }
        this._refreshInlineRelations(asset);
        var clone = asset._clone();
        if (!asset.isInline) {
            clone.url = urlTools.resolveUrl(asset.url, clone.id + asset.extension);
        }
        if (asset.isInitial) {
            clone.isInitial = true;
        }
        this.addAsset(clone);

        if (incomingRelations) {
            incomingRelations.forEach(function (incomingRelation) {
                if (!incomingRelation || !incomingRelation.isRelation) {
                    throw new Error("AssetGraph.cloneAsset: Incoming relation is not a relation: ", incomingRelation);
                }
                if (!(incomingRelation.id in this.idIndex)) {
                    throw new Error("AssetGraph.cloneAsset: Incoming relation is not in the graph: ", incomingRelation);
                }
                this.updateRelationTarget(incomingRelation, clone);
            }, this);
        }
        this.populateRelationsToExistingAssets(clone);
        return clone;
    },

assetGraph.populateRelationsToExistingAssets(asset)

Go through the unresolved outgoing relations of an asset and add the ones that refer to assets that are already part of the graph. Recurses into inline assets.

You shouldn't need to worry about this. It's mostly useful as a helper for the cloneAsset method, but also used by the compressJavaScript transform.

  • param: Asset asset The asset.

  • api: public

populateRelationsToExistingAssets: function (asset) {
        if (!asset || !asset.isAsset) {
            throw new Error("AssetGraph.populateRelationsToExistingAssets: Not an asset: ", asset);
        }
        asset.outgoingRelations.forEach(function (outgoingRelation) {
            var relativeUrl;
            if (outgoingRelation.to.url || typeof outgoingRelation.to === 'string') {
                var targetUrl = urlTools.resolveUrl(this.getBaseAssetForRelation(outgoingRelation).url, outgoingRelation.to.url || outgoingRelation.to);
                if (targetUrl in this.urlIndex) {
                    outgoingRelation.to = this.urlIndex[targetUrl];
                    this.addRelation(outgoingRelation);
                }
            } else {
                // Inline asset
                outgoingRelation.to = AssetGraph.assets.create(outgoingRelation.to);
                this.addAsset(outgoingRelation.to);
                this.addRelation(outgoingRelation);
                this.populateRelationsToExistingAssets(outgoingRelation.to);
            }
        }, this);
    },

assetGraph.getBaseAssetForRelation(relation)

Find the asset from which the url of a given relation is to be resolved. This is usually the first non-inline containing asset, but for some relation types it's the first Html ancestor -- infamously CssAlphaImageLoader and CssBehavior, but also JavaScriptOneGetStaticUrl.

The relation doesn't have to be in the graph, so this can be used during population of the graph.

  • param: Relation relation The relation to find the base asset path for.

  • return: Asset The base asset for the relation.

  • api: public

getBaseAssetForRelation: function (relation) {
        if (!relation || !relation.isRelation) {
            throw new Error("AssetGraph.getBaseAssetForRelation: Not a relation: ", relation);
        }
        if (!(relation.from.id in this.idIndex)) {
            throw new Error("AssetGraph.getBaseAssetForRelation: The 'from' asset of the relation is not in the graph: ", relation.from);
        }
        // Will return undefined if no path is found
        if (relation.id in this.idIndex) {
            return this._baseAssetPathForRelation[relation.id][0];
        } else {
            // The relation isn't in the graph (yet), we'll have to do the computation:
            return this._findBaseAssetPathForRelation(relation)[0];
        }
    },

assetGraph.recomputeBaseAssets()

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. - api: public

 
recomputeBaseAssets: function () {
        [].concat(this._relationsWithNoBaseAsset).forEach(function (relation) {
            this._unregisterBaseAssetPath(relation);
            if (!this._registerBaseAssetPath(relation)) {
                throw new Error("recomputeBaseAssets: Couldn't find base asset for " + relation);
            }
        }, this);
    },

assetGraph._refreshInlineRelations(asset)

Prepare an asset for serialization by re-inlining its inline assets that have been marked dirty.

  • param: Asset asset The asset.

_refreshInlineRelations: function (asset) {
        if (!asset || !asset.isAsset) {
            throw new Error("AssetGraph._refreshInlineRelations: Not an asset: ", asset);
        }
        this.findRelations({from: asset, to: {isInline: true}}).forEach(function (relation) {
            this._refreshInlineRelations(relation.to);
            if (relation.to.isDirty) {
                relation.to.isDirty = false;
                if (relation._inline) {
                    relation._inline();
                    relation.from.markDirty();
                }
            }
        }, this);
    },