diff --git a/composer.json b/composer.json
index 436dae9..b45baf8 100644
--- a/composer.json
+++ b/composer.json
@@ -21,7 +21,8 @@
     "symfony-cmf/routing": "1.1.*@alpha",
     "easyrdf/easyrdf": "0.8.*@beta",
     "phpunit/phpunit": "3.7.*",
-    "zendframework/zend-feed": "2.2.*"
+    "zendframework/zend-feed": "2.2.*",
+    "sdboyer/gliph": "0.1.*"
   },
   "autoload": {
     "psr-0": {
diff --git a/core/core.services.yml b/core/core.services.yml
index 608c439..9dd036c 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -631,3 +631,4 @@ services:
     class: Drupal\Core\Asset\JsCollectionGrouper
   asset.js.dumper:
     class: Drupal\Core\Asset\AssetDumper
+
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 77ea718..687eeb9 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -1631,6 +1631,9 @@ function drupal_add_css($data = NULL, $options = NULL) {
   // Create an array of CSS files for each media type first, since each type needs to be served
   // to the browser differently.
   if (isset($data)) {
+    $options['type'] = isset($options['type']) ? $options['type'] : 'file';
+    drupal_collect_assets($data, $options, 'css');
+
     $options += array(
       'type' => 'file',
       'group' => CSS_AGGREGATE_DEFAULT,
@@ -1682,6 +1685,29 @@ function drupal_add_css($data = NULL, $options = NULL) {
   return $css;
 }
 
+function drupal_collect_assets($data, $options, $type = '') {
+  $collection = &drupal_static('global_asset_bag', FALSE);
+  $collector = &drupal_static('global_asset_collector', FALSE);
+
+  $collection = ($collection instanceof \Drupal\Core\Asset\Collection\AssetCollection) ? $collection : new \Drupal\Core\Asset\Collection\AssetCollection();
+  if (!$collector instanceof \Drupal\Core\Asset\Factory\AssetCollector) {
+    $collector = new \Drupal\Core\Asset\Factory\AssetCollector();
+    $collector->setCollection($collection);
+  }
+
+  if ($data instanceof \Drupal\Core\Asset\AssetInterface) {
+    $collector->add($data);
+    return;
+  }
+
+  if ($type == 'js-setting') {
+    // TODO handle js settings
+    return;
+  }
+
+  $collector->create($type, $options['type'], $data, $options);
+}
+
 /**
  * Returns a themed representation of all stylesheets to attach to the page.
  *
@@ -2212,6 +2238,11 @@ function drupal_add_js($data = NULL, $options = NULL) {
   }
   $options += drupal_js_defaults($data);
 
+  if (isset($data) && is_array($options)) {
+    $options['type'] = isset($options['type']) ? $options['type'] : 'file';
+    drupal_collect_assets($data, $options, $options['type'] == 'setting' ? 'js-setting' : 'js');
+  }
+
   // Preprocess can only be set if caching is enabled and no attributes are set.
   $options['preprocess'] = $options['cache'] && empty($options['attributes']) ? $options['preprocess'] : FALSE;
 
diff --git a/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregateInterface.php b/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregateInterface.php
new file mode 100644
index 0000000..8d37137
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregateInterface.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Aggregate\AssetAggregateInterface.
+ */
+
+namespace Drupal\Core\Asset\Aggregate;
+use Assetic\Asset\AssetCollectionInterface as AsseticAssetCollectionInterface;
+use Drupal\Core\Asset\Collection\AssetCollectionBasicInterface;
+use Drupal\Core\Asset\AssetInterface;
+
+/**
+ * Describes an aggregate asset: a logical asset composed of other assets.
+ *
+ * This interface extends to Assetic's AssetCollectionInterface, but is intended
+ * for a more narrow purpose than it. Whereas Assetic uses AssetCollections as
+ * both a container for assets (a collection in the conventional sense) *and* as
+ * a renderable unit, implementors of AssetAggregateInterface are considered to
+ * be solely the latter.
+ *
+ * This approach was taken because these two are discrete responsibilities, and
+ * while the conflation of the two is not problematic for most contexts in which
+ * Assetic is used, Drupal's complex asset declaration and rendering environment
+ * necessitates a clear differentiation between the two.
+ *
+ * In the end, aggregates are exactly what the interface composition looks like:
+ * a real, functioning asset, and a basic container for other assets.
+ *
+ * @see \Assetic\Asset\AssetCollectionInterface
+ * @see \Drupal\Core\Asset\Collection\AssetCollectionInterface
+ */
+interface AssetAggregateInterface extends AssetInterface, AssetCollectionBasicInterface, AsseticAssetCollectionInterface {
+
+  /**
+   * Replaces an existing asset in the aggregate with a new one.
+   *
+   * This maintains ordering of the assets within the aggregate; the new asset
+   * will occupy the same position as the old asset.
+   *
+   * @param AssetInterface|string $needle
+   *   Either an AssetInterface instance, or the string id of an asset.
+   * @param AssetInterface $replacement
+   *   The new asset to swap into place.
+   * @param bool $graceful
+   *   Whether failure should return FALSE or throw an exception.
+   *
+   * @return bool
+   *
+   * @throws \OutOfBoundsException
+   */
+  public function replace($needle, AssetInterface $replacement, $graceful = FALSE);
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/Aggregate/BaseAggregateAsset.php b/core/lib/Drupal/Core/Asset/Aggregate/BaseAggregateAsset.php
new file mode 100644
index 0000000..65928b7
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Aggregate/BaseAggregateAsset.php
@@ -0,0 +1,355 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\BaseAggregateAsset.
+ */
+
+namespace Drupal\Core\Asset\Aggregate;
+
+use Assetic\Filter\FilterCollection;
+use Assetic\Filter\FilterInterface;
+use Drupal\Core\Asset\AsseticAdapterAsset;
+use Drupal\Core\Asset\AssetInterface;
+use Assetic\Asset\AssetInterface as AsseticAssetInterface;
+use Drupal\Core\Asset\Aggregate\AssetAggregateInterface;
+use Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException;
+use Drupal\Core\Asset\Metadata\AssetMetadataBag;
+
+/**
+ * Base class for representing aggregate assets.
+ *
+ */
+abstract class BaseAggregateAsset extends AsseticAdapterAsset implements \IteratorAggregate, AssetInterface, AssetAggregateInterface {
+
+  /**
+   * @var \Drupal\Core\Asset\Metadata\AssetMetadataBag
+   */
+  protected $metadata;
+
+  /**
+   * Container for all assets attached to this object.
+   *
+   * @var \SplObjectStorage
+   */
+  protected $assetStorage;
+
+  /**
+   * @var \SplObjectStorage
+   */
+  protected $nestedStorage;
+
+  /**
+   * A string identifier for this aggregate.
+   *
+   * This is calculated based on
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * Maintains a map, keyed by id, of all assets.
+   *
+   * This map is also the canonical source for ordering information.
+   *
+   * @var array
+   */
+  protected $assetIdMap = array();
+
+  protected $content;
+
+  /**
+   * @param AssetMetadataBag $metadata
+   *   The metadata bag for this aggregate.
+   * @param array $assets
+   *   Assets to add to this aggregate.
+   * @param array $filters
+   *   Filters to apply to this aggregate.
+   */
+  public function __construct(AssetMetadataBag $metadata, $assets = array(), $filters = array()) {
+    parent::__construct($filters);
+
+    $this->metadata = $metadata;
+    $this->assetStorage = new \SplObjectStorage();
+    $this->nestedStorage = new \SplObjectStorage();
+
+    foreach ($assets as $asset) {
+      $this->add($asset);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function id() {
+    if (empty($this->id)) {
+      $this->calculateId();
+    }
+
+    return $this->id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAssetType() {
+    return $this->metadata->getType();
+  }
+
+  /**
+   * Calculates and stores an id for this aggregate from the contained assets.
+   *
+   * @return void
+   */
+  protected function calculateId() {
+    $id = '';
+    foreach ($this->assetStorage as $asset) {
+      // Preserve a little id stability by not composing id from aggregates
+      if (!$asset instanceof AssetAggregateInterface) {
+        $id .= $asset->id();
+      }
+    }
+    // TODO come up with something stabler/more serialization friendly than object hash
+    $this->id = hash('sha256', $id ?: spl_object_hash($this));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMetadata() {
+    // TODO should this immutable? doable if we further granulate the interfaces
+    return $this->metadata;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function add(AsseticAssetInterface $asset) {
+    if (!$asset instanceof AssetInterface) {
+      throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset provided; Drupal aggregates require Drupal-flavored assets.');
+    }
+    $this->ensureCorrectType($asset);
+
+    $this->assetStorage->attach($asset);
+    $this->assetIdMap[$asset->id()] = $asset;
+
+    if ($asset instanceof AssetAggregateInterface) {
+      $this->nestedStorage->attach($asset);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function contains(AssetInterface $asset) {
+    if ($this->assetStorage->contains($asset)) {
+      return TRUE;
+    }
+
+    foreach ($this->nestedStorage as $aggregate) {
+      if ($aggregate->contains($asset)) {
+        return TRUE;
+      }
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getById($id, $graceful = TRUE) {
+    if (isset($this->assetIdMap[$id])) {
+      return $this->assetIdMap[$id];
+    }
+    else {
+      // Recursively search for the id
+      foreach ($this->nestedStorage as $aggregate) {
+        if ($found = $aggregate->getById($id)) {
+          return $found;
+        }
+      }
+    }
+
+    if ($graceful) {
+      return FALSE;
+    }
+
+    throw new \OutOfBoundsException(sprintf('This aggregate does not contain an asset with id %s.', $id));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function remove($needle, $graceful = TRUE) {
+    if (is_string($needle)) {
+      if (!$needle = $this->getById($needle, $graceful)) {
+        return FALSE;
+      }
+    }
+
+    return $this->removeLeaf($needle, $graceful);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function removeLeaf(AsseticAssetInterface $needle, $graceful = FALSE) {
+    if (!$needle instanceof AssetInterface) {
+      throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset provided; Drupal aggregates require Drupal-flavored assets.');
+    }
+    $this->ensureCorrectType($needle);
+
+    foreach ($this->assetIdMap as $id => $asset) {
+      if ($asset === $needle) {
+        unset($this->assetStorage[$asset], $this->assetIdMap[$id], $this->nestedStorage[$asset]);
+
+        return TRUE;
+      }
+
+      if ($asset instanceof AssetAggregateInterface && $asset->removeLeaf($needle, $graceful)) {
+        return TRUE;
+      }
+    }
+
+    if ($graceful) {
+      return FALSE;
+    }
+
+    throw new \OutOfBoundsException('Asset not found.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function replace($needle, AssetInterface $replacement, $graceful = TRUE) {
+    if (is_string($needle)) {
+      if (!$needle = $this->getById($needle, $graceful)) {
+        return FALSE;
+      }
+    }
+
+    return $this->replaceLeaf($needle, $replacement, $graceful);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function replaceLeaf(AsseticAssetInterface $needle, AsseticAssetInterface $replacement, $graceful = FALSE) {
+    if (!($needle instanceof AssetInterface && $replacement instanceof AssetInterface)) {
+      throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset(s) provided; Drupal aggregates require Drupal-flavored assets.');
+    }
+    $this->ensureCorrectType($needle);
+    $this->ensureCorrectType($replacement);
+
+    foreach ($this->assetIdMap as $id => $asset) {
+      if ($asset === $needle) {
+        unset($this->assetStorage[$asset], $this->nestedStorage[$asset]);
+
+        array_splice($this->assetIdMap, $i, 1, array($replacement->id() => $replacement));
+        $this->assetStorage->attach($replacement);
+        if ($replacement instanceof AssetAggregateInterface) {
+          $this->nestedStorage->attach($replacement);
+        }
+
+        return TRUE;
+      }
+
+      if ($asset instanceof AssetAggregateInterface && $asset->replaceLeaf($needle, $replacement, $graceful)) {
+        return TRUE;
+      }
+    }
+
+    if ($graceful) {
+      return FALSE;
+    }
+
+    throw new \OutOfBoundsException('Asset not found.');
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Aggregate assets are inherently eligible for preprocessing, so this is
+   * always true.
+   */
+  public function isPreprocessable() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function all() {
+    return $this->assetIdMap;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function load(FilterInterface $additionalFilter = NULL) {
+    // loop through leaves and load each asset
+    $parts = array();
+    foreach ($this as $asset) {
+      $asset->load($additionalFilter);
+      $parts[] = $asset->getContent();
+    }
+
+    $this->content = implode("\n", $parts);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dump(FilterInterface $additionalFilter = NULL) {
+    // loop through leaves and dump each asset
+    $parts = array();
+    foreach ($this as $asset) {
+      $parts[] = $asset->dump($additionalFilter);
+    }
+
+    return implode("\n", $parts);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContent() {
+    return $this->content;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setContent($content) {
+    $this->content = $content;
+  }
+
+  /**
+   * TODO Assetic uses their iterator to clone, then populate values and return here; is that a good model for us?
+   */
+  public function getIterator() {
+    // TODO this is totally junk
+    return new \ArrayIterator($this->assetIdMap);
+  }
+
+  /**
+   * Indicates whether this collection contains any assets.
+   *
+   * @return bool
+   *   TRUE if contained assets are present, FALSE otherwise.
+   */
+  public function isEmpty() {
+    return $this->assetStorage->count() === 0;
+  }
+
+  /**
+   * Ensures that the asset is of the correct subtype (e.g., css vs. js).
+   *
+   * @param AssetInterface $asset
+   *
+   * @throws \Drupal\Core\Asset\Exception\AssetTypeMismatchException
+   */
+  abstract protected function ensureCorrectType(AssetInterface $asset);
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/Aggregate/CssAggregateAsset.php b/core/lib/Drupal/Core/Asset/Aggregate/CssAggregateAsset.php
new file mode 100644
index 0000000..934317a
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Aggregate/CssAggregateAsset.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\CssAggregateAsset.
+ */
+
+namespace Drupal\Core\Asset\Aggregate;
+use Drupal\Core\Asset\AssetInterface;
+use Drupal\Core\Asset\Exception\AssetTypeMismatchException;
+use Drupal\Core\Asset\Metadata\AssetMetadataBag;
+use Drupal\Core\Asset\Metadata\CssMetadataBag;
+
+/**
+ * A CSS asset that is an aggregate of multiple other CSS assets.
+ */
+class CssAggregateAsset extends BaseAggregateAsset {
+
+  /**
+   * {@inheritdoc}
+   *
+   * @throws \Drupal\Core\Asset\Exception\AssetTypeMismatchException
+   */
+  public function __construct(AssetMetadataBag $metadata, $assets = array(), $filters = array(), $sourceRoot = array()) {
+    if (!$metadata instanceof CssMetadataBag) {
+      throw new AssetTypeMismatchException('CSS aggregates require CSS metadata bags.');
+    }
+
+    parent::__construct($metadata, $assets, $filters, $sourceRoot);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function ensureCorrectType(AssetInterface $asset) {
+    if ($asset->getAssetType() !== 'css') {
+      throw new AssetTypeMismatchException('CSS aggregates can only work with CSS assets.');
+    }
+  }
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/Aggregate/JsAggregateAsset.php b/core/lib/Drupal/Core/Asset/Aggregate/JsAggregateAsset.php
new file mode 100644
index 0000000..37f8a2e
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Aggregate/JsAggregateAsset.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Aggregate\JsAggregateAsset.
+ */
+
+namespace Drupal\Core\Asset\Aggregate;
+use Drupal\Core\Asset\AssetInterface;
+use Drupal\Core\Asset\Exception\AssetTypeMismatchException;
+use Drupal\Core\Asset\Metadata\AssetMetadataBag;
+use Drupal\Core\Asset\Metadata\JsMetadataBag;
+
+/**
+ * A Javascript asset that aggregates together multiple other Javascript assets.
+ */
+class JsAggregateAsset extends BaseAggregateAsset {
+
+  /**
+   * {@inheritdoc}
+   *
+   * @throws \Drupal\Core\Asset\Exception\AssetTypeMismatchException
+   */
+  public function __construct(AssetMetadataBag $metadata, $assets = array(), $filters = array(), $sourceRoot = array()) {
+    if (!$metadata instanceof JsMetadataBag) {
+      throw new AssetTypeMismatchException('JS aggregates require JS metadata bags.');
+    }
+
+    parent::__construct($metadata, $assets, $filters, $sourceRoot);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function ensureCorrectType(AssetInterface $asset) {
+   if ($asset->getAssetType() !== 'js') {
+      throw new AssetTypeMismatchException('JS aggregates can only work with JS assets.');
+    }
+  }
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/AssetCollectionAggregatorInterface.php b/core/lib/Drupal/Core/Asset/AssetCollectionAggregatorInterface.php
new file mode 100644
index 0000000..2037f59
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/AssetCollectionAggregatorInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\AssetCollectionAggregatorInterface.
+ */
+
+namespace Drupal\Core\Asset;
+use Drupal\Core\Asset\Collection\AssetCollectionInterface;
+
+/**
+ * Interface for a service that groups assets into logical aggregates.
+ */
+interface AssetCollectionAggregatorInterface {
+
+  /**
+   * Groups a collection of assets into logical aggregates.
+   *
+   * @param AssetCollectionInterface $collection
+   *   The AssetCollectionInterface to aggregate.
+   *
+   * // TODO AssetCollectionInterface is not the right thing, as it makes no ordering guarantees. Maybe AssetAggregateInterface?
+   * @return AssetCollectionInterface
+   *   A new AssetCollectionInterface containing the aggregated assets,
+   *   represented as objects implementing AssetAggregateInterface.
+   */
+  public function aggregate(AssetCollectionInterface $collection);
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/AssetCollectionOptimizerNouveauxInterface.php b/core/lib/Drupal/Core/Asset/AssetCollectionOptimizerNouveauxInterface.php
new file mode 100644
index 0000000..59ab396
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/AssetCollectionOptimizerNouveauxInterface.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\AssetCollectionOptimizerNouveauxInterface.
+ */
+
+namespace Drupal\Core\Asset;
+use Drupal\Core\Asset\Collection\AssetCollectionInterface;
+
+/**
+ * Interface for a service that optimizes an asset collection.
+ */
+interface AssetCollectionOptimizerNouveauxInterface {
+
+  /**
+   * Optimizes a collection of assets.
+   *
+   * "Asset collection" means an object implementing AssetCollectionInterface.
+   * Optimization encompasses both aggregating assets together into a smaller
+   * set, and performing operations such as minification.
+   *
+   * @param AssetCollectionInterface $collection
+   *   The AssetCollectionInterface to optimize.
+   *
+   * @return AssetCollectionInterface
+   *   An AssetCollectionInterface containing fully optimized AssetInterface
+   *   objects.
+   */
+  public function optimize(AssetCollectionInterface $collection);
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/AssetGraph.php b/core/lib/Drupal/Core/Asset/AssetGraph.php
new file mode 100644
index 0000000..ee94b08
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/AssetGraph.php
@@ -0,0 +1,162 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\AssetGraph.
+ */
+
+namespace Drupal\Core\Asset;
+use Gliph\Exception\InvalidVertexTypeException;
+use Gliph\Graph\DirectedAdjacencyList;
+
+/**
+ * An extension of the DirectedAdjacencyGraph concept designed specifically for
+ * Drupal's asset management use case.
+ *
+ * Drupal allows for two types of sequencing declarations:
+ *
+ *   - Dependencies, which guarantee that dependent asset must be present and
+ *     that it must precede the asset declaring it as a dependency.
+ *   - Ordering, which can guarantee that asset A will be either preceded or
+ *     succeeded by asset B, but does NOT guarantee that B will be present.
+ *
+ * The impact of a dependency can be calculated myopically (without knowledge of
+ * the full set), as a dependency inherently guarantees the presence of the
+ * other vertex in the set.
+ *
+ * For ordering, however, the full set must be inspected to determine whether or
+ * not the other asset is already present. If it is, a directed edge can be
+ * declared; if it is not.
+ *
+ * This class eases the process of determining what to do with ordering
+ * declarations by implementing a more sophisticated addVertex() mechanism,
+ * which incrementally sets up (and triggers) watches for any ordering
+ * declarations that have not yet been realized.
+ *
+ * TODO add stuff that tracks data about unresolved successors/predecessors
+ */
+class AssetGraph extends DirectedAdjacencyList {
+
+  protected $before = array();
+  protected $after = array();
+  protected $verticesById = array();
+  protected $process;
+
+  /**
+   * Creates a new AssetGraph object.
+   *
+   * AssetGraphs are a specialization of DirectedAdjacencyGraph that is tailored
+   * to handling the sequencing information carried by AssetOrderingInterface
+   * instances.
+   *
+   * @param bool $process
+   *   Whether or not to automatically process sequencing as vertices are added.
+   *   This should be left as TRUE in most every user-facing case; its primary
+   *   audience is for the creation of a graph transpose.
+   */
+  public function __construct($process = TRUE) {
+    parent::__construct();
+    $this->process = $process;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addVertex($vertex) {
+    if (!$vertex instanceof AssetInterface) {
+      throw new InvalidVertexTypeException('AssetGraph requires vertices to implement AssetInterface.');
+    }
+
+    if (!$this->hasVertex($vertex)) {
+      $this->vertices[$vertex] = new \SplObjectStorage();
+      $this->verticesById[$vertex->id()] = $vertex;
+
+      if ($this->process) {
+        $this->processNewVertex($vertex);
+      }
+    }
+  }
+
+  /**
+   * Processes all sequencing information for a given vertex.
+   *
+   * @param AssetInterface $vertex
+   */
+  protected function processNewVertex(AssetInterface $vertex) {
+    $id = $vertex->id();
+    // First, check if anything has a watch out for this vertex.
+    if (isset($this->before[$id])) {
+      foreach ($this->before[$id] as $predecessor) {
+        $this->addDirectedEdge($predecessor, $vertex);
+      }
+      unset($this->before[$id]);
+    }
+
+    if (isset($this->after[$id])) {
+      foreach ($this->after[$id] as $successor) {
+        $this->addDirectedEdge($vertex, $successor);
+      }
+      unset($this->after[$id]);
+    }
+
+    // Add watches for this vertex, if it implements the interface.
+    if ($vertex instanceof AssetOrderingInterface) {
+      // TODO this logic assumes collections enforce uniqueness - ensure that's the case.
+      foreach ($vertex->getPredecessors() as $predecessor) {
+        // Normalize to id string.
+        $predecessor = is_string($predecessor) ? $predecessor : $predecessor->id();
+
+        if (isset($this->verticesById[$predecessor])) {
+          $this->addDirectedEdge($vertex, $this->verticesById[$predecessor]);
+        }
+        else {
+          if (!isset($this->before[$predecessor])) {
+            $this->before[$predecessor] = array();
+          }
+          $this->before[$predecessor][] = $vertex;
+        }
+      }
+
+      foreach ($vertex->getSuccessors() as $successor) {
+        // Normalize to id string.
+        $successor = is_string($successor) ? $successor : $successor->id();
+
+        if (isset($this->verticesById[$successor])) {
+          $this->addDirectedEdge($this->verticesById[$successor], $vertex);
+        }
+        else {
+          if (!isset($this->before[$successor])) {
+            $this->after[$successor] = array();
+          }
+          $this->after[$successor][] = $vertex;
+        }
+      }
+    }
+  }
+
+  /**
+   * Remove a vertex from the graph. Unsupported in AssetGraph.
+   *
+   * Vertex removals are unsupported because it would necessitate permanent
+   * bookkeeping on sequencing data. With forty or fifty assets, each having
+   * only a few dependencies, there would be a fair bit of pointless iterating.
+   *
+   * @throws \LogicException
+   *   This exception will always be thrown.
+   */
+  public function removeVertex($vertex) {
+    throw new \LogicException('AssetGraph does not support vertex removals.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function transpose() {
+    $graph = new self(FALSE);
+    $this->eachEdge(function($edge) use (&$graph) {
+        $graph->addDirectedEdge($edge[1], $edge[0]);
+    });
+
+    return $graph;
+  }
+}
diff --git a/core/lib/Drupal/Core/Asset/AssetInterface.php b/core/lib/Drupal/Core/Asset/AssetInterface.php
new file mode 100644
index 0000000..1d21dac
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/AssetInterface.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\AssetInterface.
+ */
+
+namespace Drupal\Core\Asset;
+
+use Assetic\Asset\AssetInterface as AsseticAssetInterface;
+use Drupal\Core\Asset\Metadata\AssetMetadataBag;
+
+/**
+ * Represents a CSS or Javascript asset.
+ *
+ * This interface extends the AssetInterface provided by Assetic to facilitate
+ * different behaviors by individual assets.
+ */
+interface AssetInterface extends AsseticAssetInterface {
+
+  /**
+   * Returns the metadata bag for this asset.
+   *
+   * @return AssetMetadataBag
+   */
+  public function getMetadata();
+
+  /**
+   * Indicates whether or not this asset is eligible for preprocessing.
+   *
+   * Assets that are marked as not preprocessable will always be passed directly
+   * to the browser without aggregation or minification. Assets that are marked
+   * as eligible for preprocessing will be included in any broader aggregation
+   * logic that has been configured.
+   *
+   * @return bool
+   */
+  public function isPreprocessable();
+
+  /**
+   * Returns a unique string identifier that uniquely identifies this asset.
+   *
+   * Note that this id IS subject to change, if certain internal object
+   * properties change.
+   *
+   * // TODO if it's subject to change, 'id' is misleading
+   *
+   * @return string
+   *   The asset id.
+   */
+  public function id();
+
+  /**
+   * Returns a string identifying the type of asset - i.e., 'css' or 'js'.
+   *
+   * @return string
+   */
+  public function getAssetType();
+}
diff --git a/core/lib/Drupal/Core/Asset/AssetOrderingInterface.php b/core/lib/Drupal/Core/Asset/AssetOrderingInterface.php
new file mode 100644
index 0000000..7fe5835
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/AssetOrderingInterface.php
@@ -0,0 +1,111 @@
+<?php
+/**
+ * @file
+ * Contains Drupal\Core\Asset\AssetOrderingInterface.
+ */
+
+namespace Drupal\Core\Asset;
+
+/**
+ * Describes an asset or asset-like object that can declare dependencies.
+ */
+interface AssetOrderingInterface {
+
+  /**
+   * Indicates whether this asset has one or more library dependencies.
+   *
+   * @return boolean
+   */
+  public function hasDependencies();
+
+  /**
+   * Retrieve this asset's dependencies.
+   *
+   * @return mixed
+   *   An array of dependencies if they exist,
+   */
+  public function getDependencyInfo();
+
+  /**
+   * Add a dependency on a library for this asset.
+   *
+   * @param string $module
+   *   The name of the module declaring the library.
+   * @param string $name
+   *   The name of the library.
+   *
+   * @return void
+   */
+  public function addDependency($module, $name);
+
+  /**
+   * Clears (removes) all library dependencies for this asset.
+   *
+   * This does not affect ordering data.
+   *
+   * @return void
+   */
+  public function clearDependencies();
+
+  /**
+   * Declare that an asset should, if present, succeed this asset on output.
+   *
+   * Either the string identifier for the other asset, or the asset object
+   * itself, should be provided.
+   *
+   * @param string|AssetInterface $asset
+   *   The asset to succeed the current asset.
+   *
+   * @return void
+   */
+  public function before($asset);
+
+  /**
+   * Declare that an asset should, if present, precede this asset on output.
+   *
+   * Either the string identifier for the other asset, or the asset object
+   * itself, should be provided.
+   *
+   * @param string|AssetInterface $asset
+   *   The asset to precede the current asset.
+   *
+   * @return void
+   */
+  public function after($asset);
+
+  /**
+   * Returns ordering info declared by after().
+   *
+   * @return array
+   *   An array of strings or AssetInterface instances that must precede this
+   *   object on final output.
+   */
+  public function getPredecessors();
+
+  /**
+   * Returns ordering info declared by before().
+   *
+   * @return array
+   *   An array of strings or AssetInterface instances that must succeed this
+   *   object on final output.
+   */
+  public function getSuccessors();
+
+  /**
+   * Clears (removes) all ordering info declared by before() for this asset.
+   *
+   * This does not affect dependency data.
+   *
+   * @return void
+   */
+  public function clearSuccessors();
+
+  /**
+   * Clears (removes) all ordering info declared by after() for this asset.
+   *
+   * This does not affect dependency data.
+   *
+   * @return void
+   */
+  public function clearPredecessors();
+}
diff --git a/core/lib/Drupal/Core/Asset/AsseticAdapterAsset.php b/core/lib/Drupal/Core/Asset/AsseticAdapterAsset.php
new file mode 100644
index 0000000..de2980c
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/AsseticAdapterAsset.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\AsseticAdapterAsset.
+ */
+
+namespace Drupal\Core\Asset;
+
+use Assetic\Asset\AssetInterface;
+use Assetic\Asset\BaseAsset as AsseticBaseAsset;
+use Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException;
+
+/**
+ * A class that reduces boilerplate code by centrally disabling the Assetic
+ * properties and methods Drupal does not support.
+ */
+abstract class AsseticAdapterAsset extends AsseticBaseAsset implements AssetInterface {
+
+  /**
+   * @throws \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException
+   */
+  public final function getVars() {
+    throw new UnsupportedAsseticBehaviorException("Drupal does not use or support Assetic's 'vars' concept.");
+  }
+
+  /**
+   * @throws \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException
+   */
+  public final function setValues(array $values) {
+    throw new UnsupportedAsseticBehaviorException("Drupal does not use or support Assetic's 'values' concept.");
+  }
+
+  /**
+   * @throws \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException
+   */
+  public final function getValues() {
+    throw new UnsupportedAsseticBehaviorException("Drupal does not use or support Assetic's 'values' concept.");
+  }
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/BaseAsset.php b/core/lib/Drupal/Core/Asset/BaseAsset.php
new file mode 100644
index 0000000..d42f9a2
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/BaseAsset.php
@@ -0,0 +1,148 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\BaseAsset.
+ */
+
+namespace Drupal\Core\Asset;
+
+use Drupal\Core\Asset\AssetInterface;
+use Drupal\Core\Asset\Metadata\AssetMetadataBag;
+
+/**
+ * A base abstract asset.
+ *
+ * This is an amalgam of Assetic\Asset\BaseAsset (copied directly) with
+ * implementations of the additional methods specified by Drupal's own
+ * Drupal\Core\Asset\AssetInterface.
+ *
+ * The methods load() and getLastModified() are left undefined, although a
+ * reusable doLoad() method is available to child classes.
+ */
+abstract class BaseAsset extends AsseticAdapterAsset implements AssetInterface, AssetOrderingInterface {
+
+  /**
+   * @var AssetMetadataBag
+   */
+  protected $metadata;
+
+  protected $dependencies = array();
+
+  protected $successors = array();
+
+  protected $predecessors = array();
+
+  public function __construct(AssetMetadataBag $metadata, $filters = array(), $sourceRoot = NULL, $sourcePath = NULL) {
+    $this->metadata = $metadata;
+    parent::__construct($filters, $sourceRoot, $sourcePath);
+  }
+
+  public function __clone() {
+    parent::__clone();
+    $this->metadata = clone $this->metadata;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMetadata() {
+    return $this->metadata;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAssetType() {
+    return $this->metadata->getType();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isPreprocessable() {
+    return (bool) $this->metadata->get('preprocess');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasDependencies() {
+    return !empty($this->dependencies);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addDependency($module, $name) {
+    if (!(is_string($module) && is_string($name))) {
+      throw new \InvalidArgumentException('Dependencies must be expressed as 2-tuple with the first element being owner/module, and the second being name.');
+    }
+
+    $this->dependencies[] = array($module, $name);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearDependencies() {
+    $this->dependencies = array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDependencyInfo() {
+    return $this->dependencies;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function before($asset) {
+    if (!($asset instanceof AssetInterface || is_string($asset))) {
+      throw new \InvalidArgumentException('Ordering information must be declared using either an asset string id or the full AssetInterface object.');
+    }
+
+    $this->successors[] = $asset;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function after($asset) {
+    if (!($asset instanceof AssetInterface || is_string($asset))) {
+      throw new \InvalidArgumentException('Ordering information must be declared using either an asset string id or the full AssetInterface object.');
+    }
+
+    $this->predecessors[] = $asset;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPredecessors() {
+    return $this->predecessors;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSuccessors() {
+    return $this->successors;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearSuccessors() {
+    $this->successors = array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearPredecessors() {
+    $this->predecessors = array();
+  }
+}
diff --git a/core/lib/Drupal/Core/Asset/Collection/AssetCollection.php b/core/lib/Drupal/Core/Asset/Collection/AssetCollection.php
new file mode 100644
index 0000000..0fe0a90
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Collection/AssetCollection.php
@@ -0,0 +1,184 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Collection\AssetCollection.
+ */
+
+namespace Drupal\Core\Asset\Collection;
+use Drupal\Core\Asset\Collection\AssetCollectionInterface;
+use Drupal\Core\Asset\AssetInterface;
+use Drupal\Core\Asset\Collection\Iterator\AssetSubtypeFilterIterator;
+
+/**
+ * A container for assets.
+ *
+ * @see CssCollection
+ * @see JsCollection
+ *
+ * TODO allow direct adding of libraries
+ */
+class AssetCollection implements \IteratorAggregate, AssetCollectionInterface {
+
+  protected $assetStorage;
+
+  protected $assetIdMap = array();
+
+  protected $frozen = FALSE;
+
+  public function __construct() {
+    $this->assetStorage = new \SplObjectStorage();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function add(AssetInterface $asset) {
+    $this->attemptWrite();
+
+    if (!$this->contains($asset)) {
+      $this->assetStorage->attach($asset);
+      $this->assetIdMap[$asset->id()] = $asset;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function contains(AssetInterface $asset) {
+    // TODO decide whether to do this by id or object instance
+    return $this->assetStorage->contains($asset);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getById($id, $graceful = TRUE) {
+    if (isset($this->assetIdMap[$id])) {
+      return $this->assetIdMap[$id];
+    }
+    else if ($graceful) {
+      return FALSE;
+    }
+
+    throw new \OutOfBoundsException(sprintf('This collection does not contain an asset with id %s.', $id));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function remove($needle, $graceful = TRUE) {
+    // TODO fix horrible complexity of conditionals, exceptions, and returns.
+    $this->attemptWrite();
+
+    // Validate and normalize type to AssetInterface
+    if (is_string($needle)) {
+      if (!$needle = $this->getById($needle, $graceful)) {
+        // Asset couldn't be found but we're in graceful mode - return FALSE.
+        return FALSE;
+      }
+    }
+    else if (!$needle instanceof AssetInterface) {
+      throw new \InvalidArgumentException('Invalid type provided to AssetCollection::remove(); must provide either a string asset id or AssetInterface instance.');
+    }
+
+    // Check for membership
+    if ($this->contains($needle)) {
+      unset($this->assetIdMap[$needle->id()], $this->assetStorage[$needle]);
+      return TRUE;
+    }
+    else if (!$graceful) {
+      throw new \OutOfBoundsException(sprintf('This collection does not contain an asset with id %s.', $needle->id()));
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function all() {
+    return $this->assetIdMap;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function mergeCollection(AssetCollectionInterface $collection, $freeze = TRUE) {
+    $this->attemptWrite();
+
+    foreach ($collection as $asset) {
+      if (!$this->contains($asset)) {
+        $this->add($asset);
+      }
+    }
+
+    if ($freeze) {
+      $collection->freeze();
+    }
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function freeze() {
+    $this->frozen = TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isFrozen() {
+    return $this->frozen;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIterator() {
+    return new \ArrayIterator($this->assetIdMap);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEmpty() {
+    return empty($this->assetIdMap);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCss() {
+    // TODO evaluate potential performance impact if this is done a lot...
+    $collection = new self();
+    foreach (new AssetSubtypeFilterIterator($this->getIterator(), 'css') as $asset) {
+      $collection->add($asset);
+    }
+
+    return $collection;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getJs() {
+    $collection = new self();
+    foreach (new AssetSubtypeFilterIterator($this->getIterator(), 'js') as $asset) {
+      $collection->add($asset);
+    }
+
+    return $collection;
+  }
+
+  /**
+   * Checks if the asset library is frozen, throws an exception if it is.
+   */
+  protected function attemptWrite() {
+    if ($this->isFrozen()) {
+      throw new \LogicException('Cannot write to a frozen AssetCollection.');
+    }
+  }
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/Collection/AssetCollectionBasicInterface.php b/core/lib/Drupal/Core/Asset/Collection/AssetCollectionBasicInterface.php
new file mode 100644
index 0000000..e4cebc2
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Collection/AssetCollectionBasicInterface.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Collection\AssetCollectionBasicInterface.
+ */
+
+namespace Drupal\Core\Asset\Collection;
+use Drupal\Core\Asset\AssetInterface;
+use Assetic\Asset\AssetInterface as AsseticAssetInterface;
+use Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException;
+
+/**
+ * Describes an asset collection: a container for assets.
+ *
+ * Asset collections are nothing more than a mechanism for holding and easily
+ * moving a set of a specific type of asset around.
+ *
+ * This interface contains the subset of methods that feasible for
+ * AssetAggregateInterface to share; because certain internal sequencing and
+ * state is important to aggregates, they cannot behave like a full collection.
+ *
+ * @see \Drupal\Core\Asset\Aggregate\AssetAggregateInterface
+ * @see \Drupal\Core\Asset\Collection\AssetCollectionInterface
+ */
+interface AssetCollectionBasicInterface extends \Traversable {
+
+  /**
+   * Removes an asset from the aggregate.
+   *
+   * Wraps Assetic's AssetCollection::removeLeaf() to ease removal of keys.
+   *
+   * @param AssetInterface|string $needle
+   *   Either an AssetInterface instance, or the string id of an asset.
+   * @param bool $graceful
+   *   Whether failure should return FALSE or throw an exception.
+   *
+   * @return bool
+   *
+   * @throws \OutOfBoundsException
+   */
+  public function remove($needle, $graceful = FALSE);
+
+  /**
+   * Indicates whether this collection contains the provided asset.
+  *
+   * @param AssetInterface $asset
+   *   Either an AssetInterface instance, or the string id of an asset.
+   *
+   * @return bool
+   */
+  public function contains(AssetInterface $asset);
+
+  /**
+   * Retrieves a contained asset by its string identifier.
+   *
+   * Call this with $graceful = TRUE as an equivalent to contains() if all you
+   * have is a string id.
+   *
+   * @param string $id
+   *   The id of the asset to retrieve.
+   * @param bool $graceful
+   *   Whether failure should return FALSE or throw an exception.
+   *
+   * @return AssetInterface|bool
+   *   FALSE if no asset could be found with that id, or an AssetInterface.
+   *
+   * @throws \OutOfBoundsException
+   */
+  public function getById($id, $graceful = TRUE);
+
+  /**
+   * Indicates whether this collection contains any assets.
+   *
+   * @return bool
+   *   TRUE if contained assets are present, FALSE otherwise.
+   */
+  public function isEmpty();
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/Collection/AssetCollectionInterface.php b/core/lib/Drupal/Core/Asset/Collection/AssetCollectionInterface.php
new file mode 100644
index 0000000..a4547a8
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Collection/AssetCollectionInterface.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Collection\AssetCollectionInterface.
+ */
+
+namespace Drupal\Core\Asset\Collection;
+use Drupal\Core\Asset\AssetInterface;
+use Drupal\Core\Asset\AssetLibraryRepository;
+
+/**
+ * Describes an asset collection.
+ *
+ * TODO we need a few more methods here to deal with asset type disambiguation and library resolution
+ *
+ * @see \Drupal\Core\Asset\Collection\AssetCollectionBasicInterface
+ */
+interface AssetCollectionInterface extends AssetCollectionBasicInterface {
+
+  /**
+   * Returns all assets contained in this collection.
+   *
+   * @return AssetInterface[]
+   */
+  public function all();
+
+  /**
+   * Adds an asset to the collection.
+   *
+   * @param \Drupal\Core\Asset\AssetInterface $asset
+   *   The asset to add.
+   *
+   * @return bool
+   *   TRUE if the asset was already added, FALSE if it was already present in
+   *   the collection.
+   */
+  public function add(AssetInterface $asset);
+
+  /**
+   * Merges another asset collection into this one.
+   *
+   * If an asset is present in both collections, as identified by
+   * AssetInterface::id(), the asset from the passed collection will
+   * supercede the asset in this collection.
+   *
+   * @param AssetCollectionInterface $collection
+   *   The collection to merge.
+   *
+   * @param bool $freeze
+   *   Whether to freeze the provided collection after merging. Defaults to TRUE.
+   *
+   * @return void
+   */
+  public function mergeCollection(AssetCollectionInterface $collection, $freeze = TRUE);
+
+  /**
+   * Freeze this asset collection, preventing asset additions or removals.
+   *
+   * This does not prevent modification of assets already contained within the
+   * collection.
+   *
+   * TODO put this on the basic interface so aggregates have it, too?
+   *
+   * @return void
+   */
+  public function freeze();
+
+  /**
+   * Indicates whether or not this collection is frozen.
+   *
+   * @return bool
+   */
+  public function isFrozen();
+
+  /**
+   * Returns all contained CSS assets in a traversable form.
+   *
+   * @return \Traversable
+   */
+  public function getCss();
+
+  /**
+   * Returns all contained JS assets in a traversable form.
+   *
+   * @return \Traversable
+   */
+  public function getJs();
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/Collection/AssetLibrary.php b/core/lib/Drupal/Core/Asset/Collection/AssetLibrary.php
new file mode 100644
index 0000000..ce40bfc
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Collection/AssetLibrary.php
@@ -0,0 +1,211 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\AssetLibrary.
+ */
+
+namespace Drupal\Core\Asset\Collection;
+
+use Drupal\Core\Asset\AssetOrderingInterface;
+use Drupal\Core\Asset\Collection\AssetCollection;
+
+class AssetLibrary extends AssetCollection implements AssetOrderingInterface {
+
+  /**
+   * The asset library's title.
+   *
+   * @var string
+   */
+  protected $title = '';
+
+  /**
+   * The asset library's version.
+   *
+   * @var string
+   */
+  protected $version;
+
+  /**
+   * The asset library's website.
+   *
+   * @var string
+   */
+  protected $website = '';
+
+  /**
+   * The asset library's dependencies (on other asset libraries).
+   *
+   * @var array
+   */
+  protected $dependencies = array();
+
+  protected $predecessors = array();
+
+  protected $successors = array();
+
+  public function __construct(array $values = array()) {
+    parent::__construct();
+    // TODO do it right.
+    $vals = array_intersect_key($values, array_flip(array('title', 'version', 'website', 'dependencies')));
+    foreach ($vals as $key => $val) {
+      $this->$key = $val;
+    }
+  }
+
+  /**
+   * Set the asset library's title.
+   *
+   * @param string $title
+   *   The title of the asset library.
+   *
+   * @return \Drupal\Core\Asset\AssetLibrary
+   *   The asset library, to allow for chaining.
+   */
+  public function setTitle($title) {
+    $this->attemptWrite();
+    $this->title = $title;
+    return $this;
+  }
+
+  /**
+   * Get the asset library's title.
+   *
+   * @return string
+   *   The title of the asset library.
+   */
+  public function getTitle() {
+    return $this->title;
+  }
+
+  /**
+   * Set the asset library's website.
+   *
+   * @param string $website
+   *   The website of the asset library.
+   *
+   * @return \Drupal\Core\Asset\AssetLibrary
+   *   The asset library, to allow for chaining.
+   */
+  public function setWebsite($website) {
+    $this->attemptWrite();
+    $this->website = $website;
+    return $this;
+  }
+
+  /**
+   * Get the asset library's website.
+   *
+   * @return string
+   *   The website of the asset library.
+   */
+  public function getWebsite() {
+    return $this->website;
+  }
+
+  /**
+   * Set the asset library's version.
+   *
+   * @param string $version
+   *   The version of the asset library.
+   *
+   * @return \Drupal\Core\Asset\AssetLibrary
+   *   The asset library, to allow for chaining.
+   */
+  public function setVersion($version) {
+    $this->attemptWrite();
+    $this->version = $version;
+    return $this;
+  }
+
+  /**
+   * Get the asset library's version.
+   *
+   * @return string
+   *   The version of the asset library.
+   */
+  public function getVersion() {
+    return $this->version;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasDependencies() {
+    return !empty($this->dependencies);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addDependency($module, $name) {
+    $this->attemptWrite();
+    $this->dependencies[] = array($module, $name);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearDependencies() {
+    $this->attemptWrite();
+    $this->dependencies = array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDependencyInfo() {
+    return $this->dependencies;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function before($asset) {
+    $this->successors[] = $asset;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function after($asset) {
+    $this->predecessors[] = $asset;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPredecessors() {
+    return $this->predecessors;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSuccessors() {
+    return $this->successors;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearSuccessors() {
+    $this->successors = array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearPredecessors() {
+    $this->predecessors = array();
+  }
+
+  /**
+   * Checks if the asset library is frozen, throws an exception if it is.
+   */
+  protected function attemptWrite() {
+    if ($this->isFrozen()) {
+      throw new \LogicException('Metadata cannot be modified on a frozen AssetLibrary.');
+    }
+  }
+}
diff --git a/core/lib/Drupal/Core/Asset/Collection/Iterator/AssetSubtypeFilterIterator.php b/core/lib/Drupal/Core/Asset/Collection/Iterator/AssetSubtypeFilterIterator.php
new file mode 100644
index 0000000..5b2c1e5
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Collection/Iterator/AssetSubtypeFilterIterator.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Collection\Iterator\AssetSubtypeFilterIterator.
+ */
+
+namespace Drupal\Core\Asset\Collection\Iterator;
+
+/**
+ * Given an Iterator whose elements are AssetInterface instances, this iterator
+ * will only accept those assets whose type string matches the string passed
+ * to this instance's constructor.
+ */
+class AssetSubtypeFilterIterator extends \FilterIterator {
+
+  /**
+   * The type string against which assets should be compared.
+   *
+   * @var string
+   */
+  protected $match;
+
+  public function __construct(\Iterator $iterator, $match) {
+    parent::__construct($iterator);
+    $this->match = $match;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function accept() {
+    return $this->current()->getAssetType() === $this->match;
+  }
+
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/CssCollectionAggregator.php b/core/lib/Drupal/Core/Asset/CssCollectionAggregator.php
new file mode 100644
index 0000000..846dd84
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/CssCollectionAggregator.php
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\CssCollectionAggregator.
+ */
+
+namespace Drupal\Core\Asset;
+
+use Drupal\Core\Asset\Aggregate\CssAggregateAsset;
+use Drupal\Core\Asset\Collection\AssetCollection;
+use Drupal\Core\Asset\Collection\AssetCollectionInterface;
+use Drupal\Core\Asset\GroupSort\AssetGroupSorterInterface;
+use Gliph\Traversal\DepthFirst;
+use Gliph\Visitor\DepthFirstBasicVisitor;
+use Drupal\Core\Asset\AssetGraph;
+
+/**
+ * Aggregates CSS assets.
+ */
+class CssCollectionAggregator implements AssetCollectionAggregatorInterface {
+
+  /**
+   * The group-and-sorter to use to produce the optimal aggregable list.
+   *
+   * @var AssetGroupSorterInterface
+   */
+  protected $sorter;
+
+  /**
+   * An array of optimal groups for the assets currently being processed.
+   *
+   * This is ephemeral state; it is only stored as an object property in order
+   * to avoid doing certain processing twice.
+   *
+   * @var array
+   */
+  protected $optimal;
+
+  /**
+   * @var \SplObjectStorage;
+   */
+  protected $optimal_lookup;
+
+  public function __construct(AssetGroupSorterInterface $sorter) {
+    $this->sorter = $sorter;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function aggregate(AssetCollectionInterface $collection) {
+    $tsl = $this->sorter->groupAndSort($collection);
+
+    // TODO ordering suddenly matters here. replace with an order-guaranteed construct.
+    $processed = new AssetCollection();
+    $last_key = FALSE;
+    foreach ($tsl as $asset) {
+      $key = $this->sorter->getGroupingKey($asset);
+
+      if ($key && $key !== $last_key) {
+        $aggregate = new CssAggregateAsset($asset->getMetadata());
+        $processed->add($aggregate);
+      }
+
+      $aggregate->add($asset);
+      $last_key = $key;
+    }
+
+    return $processed;
+  }
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizerNouveaux.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerNouveaux.php
new file mode 100644
index 0000000..1e64377
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerNouveaux.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\CssCollectionOptimizerNouveaux.
+ */
+
+namespace Drupal\Core\Asset;
+
+use Drupal\Core\Asset\Collection\AssetCollectionInterface;
+use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
+
+/**
+ * Optimizes a collection of CSS assets.
+ */
+class CssCollectionOptimizerNouveaux implements AssetCollectionOptimizerNouveauxInterface {
+
+  /**
+   * A CSS asset aggregator.
+   *
+   * @var \Drupal\Core\Asset\AssetCollectionAggregatorInterface
+   */
+  protected $aggregator;
+
+  /**
+   * A CSS asset optimizer.
+   *
+   * @var \Drupal\Core\Asset\CssOptimizer
+   */
+  protected $optimizer;
+
+  /**
+   * An asset dumper.
+   *
+   * @var \Drupal\Core\Asset\AssetDumper
+   */
+  protected $dumper;
+
+  /**
+   * The state key/value store.
+   *
+   * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
+   */
+  protected $state;
+
+  /**
+   * Constructs a CssCollectionOptimizerNouveaux.
+   *
+   * @param \Drupal\Core\Asset\AssetCollectionAggregatorInterface
+   *   The aggregator for CSS assets.
+   * @param \Drupal\Core\Asset\AssetOptimizerInterface
+   *   The optimizer for a single CSS asset.
+   * @param \Drupal\Core\Asset\AssetDumperInterface
+   *   The dumper for optimized CSS assets.
+   * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface
+   *   The state key/value store.
+   */
+  public function __construct(AssetCollectionAggregatorInterface $aggregator, AssetOptimizerInterface $optimizer, AssetDumperInterface $dumper, KeyValueStoreInterface $state) {
+    $this->aggregator = $aggregator;
+    $this->optimizer = $optimizer;
+    $this->dumper = $dumper;
+    $this->state = $state;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function optimize(AssetCollectionInterface $collection) {
+    $collection = $this->aggregator->aggregate($collection);
+
+    // Get the map of all aggregates that have been generated so far.
+    $map = $this->state->get('drupal_css_cache_files') ?: array();
+    foreach ($collection as $asset) {
+      if ($asset->isPreprocessable()) {
+        $id = $asset->id();
+        $uri = isset($map[$id]) ? $map[$id] : '';
+        if (empty($uri) || !file_exists($uri)) {
+          // TODO optimizer needs to be refactored to basically just set filters.
+          $this->optimizer->optimize($asset);
+          // TODO refactor dumper to not need second param
+          $this->dumper->dump($asset, 'css');
+        }
+      }
+    }
+
+    return $collection;
+  }
+
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/Exception/AssetTypeMismatchException.php b/core/lib/Drupal/Core/Asset/Exception/AssetTypeMismatchException.php
new file mode 100644
index 0000000..ed651bf
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Exception/AssetTypeMismatchException.php
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Exception\AssetTypeMismatchException.
+ */
+
+namespace Drupal\Core\Asset\Exception;
+
+/**
+ * Thrown when asset subtypes (i.e., CSS vs. JS) are incorrectly mixed.
+ *
+ * For example, if a CSS asset is added to a JS collection, this should be
+ * thrown.
+ */
+class AssetTypeMismatchException extends \InvalidArgumentException {}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/Exception/LockedObjectException.php b/core/lib/Drupal/Core/Asset/Exception/LockedObjectException.php
new file mode 100644
index 0000000..192928c
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Exception/LockedObjectException.php
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Exception\LockedObjectException.
+ */
+
+namespace Drupal\Core\Asset\Exception;
+
+/**
+ * Exception thrown when a locking-protected operation is attempted on a locked
+ * object, or if a locking/unlocking operation is performed incorrectly.
+ */
+class LockedObjectException extends \LogicException {}
diff --git a/core/lib/Drupal/Core/Asset/Exception/UnsupportedAsseticBehaviorException.php b/core/lib/Drupal/Core/Asset/Exception/UnsupportedAsseticBehaviorException.php
new file mode 100644
index 0000000..c25473c
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Exception/UnsupportedAsseticBehaviorException.php
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\UnsupportedAsseticBehaviorException.
+ */
+
+namespace Drupal\Core\Asset\Exception;
+
+/**
+ * Assetic supports certain interactions with methods that we do not. This
+ * exception is thrown when such methods are touched.
+ */
+class UnsupportedAsseticBehaviorException extends \LogicException {}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/ExternalAsset.php b/core/lib/Drupal/Core/Asset/ExternalAsset.php
new file mode 100644
index 0000000..2d9705c
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/ExternalAsset.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\ExternalAsset.
+ */
+
+namespace Drupal\Core\Asset;
+
+use Assetic\Util\PathUtils;
+use Assetic\Filter\FilterInterface;
+use Drupal\Core\Asset\BaseAsset;
+use Drupal\Core\Asset\Metadata\AssetMetadataBag;
+use Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException;
+
+class ExternalAsset extends BaseAsset {
+
+  protected $sourceUrl;
+
+  public function __construct(AssetMetadataBag $metadata, $sourceUrl, $filters = array()) {
+    if (FALSE === strpos($sourceUrl, '://')) {
+      throw new \InvalidArgumentException(sprintf('"%s" is not a valid URL.', $sourceUrl));
+    }
+
+    $this->sourceUrl = $sourceUrl;
+    $this->ignoreErrors = FALSE; // TODO expose somehow
+
+    list($scheme, $url) = explode('://', $sourceUrl, 2);
+    list($host, $path) = explode('/', $url, 2);
+
+    parent::__construct($metadata, $filters, $scheme.'://'.$host, $path);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function id() {
+    return $this->sourceUrl;
+  }
+
+  /**
+   * Returns the time the current asset was last modified.
+   *
+   * @todo copied right from Assetic. needs to be made more Drupalish.
+   *
+   * @return integer|null A UNIX timestamp
+   */
+  public function getLastModified() {
+    if (false !== @file_get_contents($this->sourceUrl, false, stream_context_create(array('http' => array('method' => 'HEAD'))))) {
+      foreach ($http_response_header as $header) {
+        if (0 === stripos($header, 'Last-Modified: ')) {
+          list(, $mtime) = explode(':', $header, 2);
+
+          return strtotime(trim($mtime));
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function load(FilterInterface $additionalFilter = NULL) {
+    // TODO dumb and kinda wrong, decide how to do this right.
+    throw new UnsupportedAsseticBehaviorException('Drupal does not support the retrieval or manipulation of remote assets.');
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Asset/Factory/AssetCollector.php b/core/lib/Drupal/Core/Asset/Factory/AssetCollector.php
new file mode 100644
index 0000000..3125b26
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Factory/AssetCollector.php
@@ -0,0 +1,267 @@
+<?php
+/**
+ * @file
+ * Contains Drupal\Core\Asset\AssetCollector.
+ */
+
+namespace Drupal\Core\Asset\Factory;
+use Drupal\Core\Asset\AssetInterface;
+use Drupal\Core\Asset\Collection\AssetCollectionInterface;
+use Drupal\Core\Asset\Exception\LockedObjectException;
+use Drupal\Core\Asset\Metadata\AssetMetadataBag;
+use Drupal\Core\Asset\Metadata\CssMetadataBag;
+use Drupal\Core\Asset\Metadata\JsMetadataBag;
+
+/**
+ * A class that helps to create and collect assets.
+ *
+ * This class should be set with appropriate defaults, injected with an AssetBag
+ * for collection, then injected into an asset-producing segment of code in
+ * order to ease the creation and collection of asset information.
+ */
+class AssetCollector implements AssetCollectorInterface {
+
+  /**
+   * The collection used to store any assets that are added.
+   *
+   * @var \Drupal\Core\Asset\Collection\AssetCollectionInterface
+   */
+  protected $collection;
+
+  /**
+   * Flag indicating whether or not the object is locked.
+   *
+   * Locking prevents modifying the underlying defaults or swapping in/out the
+   * contained collection.
+   *
+   * @var bool
+   */
+  protected $locked = FALSE;
+
+  /**
+   * The key with which the lock was set.
+   *
+   * An identical value (===) must be provided to unlock the collector.
+   *
+   * There are no type restrictions.
+   *
+   * @var mixed
+   */
+  protected $lockKey;
+
+  /**
+   * The default metadata bag that will be cloned and injected into all CSS
+   * assets that are created.
+   *
+   * @var CssMetadataBag
+   */
+  protected $defaultCssMetadata;
+
+  /**
+   * The default metadata bag that will be cloned and injected into all JS
+   * assets that are created.
+   *
+   * @var JsMetadataBag
+   */
+  protected $defaultJsMetadata;
+
+  /**
+   * The last CSS asset created by this collector, if any.
+   *
+   * This is used to conveniently create sequencing relationships between CSS
+   * assets as they pass through the collector.
+   *
+   * @var AssetInterface
+   */
+  protected $lastCss;
+
+  /**
+   * A map of asset source type string ids to their fully qualified classes.
+   *
+   * @var array
+   */
+  protected $classMap = array(
+    'file' => 'Drupal\\Core\\Asset\\FileAsset',
+    'external' => 'Drupal\\Core\\Asset\\ExternalAsset',
+    'string' => 'Drupal\\Core\\Asset\\StringAsset',
+  );
+
+  public function __construct(AssetCollectionInterface $collection = NULL) {
+    $this->restoreDefaults();
+
+    if (!is_null($collection)) {
+      $this->setCollection($collection);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function add(AssetInterface $asset) {
+    if (empty($this->collection)) {
+      throw new \RuntimeException('No collection is currently attached to this collector.');
+    }
+    $this->collection->add($asset);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function create($asset_type, $source_type, $data, $options = array(), $filters = array(), $keep_last = TRUE) {
+    // TODO this normalization points to a deeper modeling problem.
+    $source_type = $source_type == 'inline' ? 'string' : $source_type;
+
+    if (!in_array($asset_type, array('css', 'js'))) {
+      throw new \InvalidArgumentException(sprintf('Only assets of type "js" or "css" are allowed, "%s" requested.', $asset_type));
+    }
+    if (!isset($this->classMap[$source_type])) {
+      throw new \InvalidArgumentException(sprintf('Only sources of type "file", "string", or "external" are allowed, "%s" requested.', $source_type));
+    }
+
+    $metadata = $this->getMetadataDefaults($asset_type);
+    if (!empty($options)) {
+      $metadata->replace($options);
+    }
+
+    $class = $this->classMap[$source_type];
+    $asset = new $class($metadata, $data, $filters);
+
+    if (!empty($this->collection)) {
+      $this->add($asset);
+    }
+
+    if ($asset_type == 'css' && !empty($this->lastCss)) {
+      $asset->after($this->lastCss);
+    }
+
+    if ($keep_last) {
+      $this->lastCss = $asset;
+    }
+
+    return $asset;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearLastCss() {
+    unset($this->lastCss);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCollection(AssetCollectionInterface $collection) {
+    if ($this->isLocked()) {
+      throw new LockedObjectException('The collector instance is locked. A new collection cannot be attached to a locked collector.');
+    }
+    $this->collection = $collection;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearCollection() {
+    if ($this->isLocked()) {
+      throw new LockedObjectException('The collector instance is locked. Collections cannot be cleared on a locked collector.');
+    }
+    $this->collection = NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasCollection() {
+    return $this->collection instanceof AssetCollectionInterface;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function lock($key) {
+    if ($this->isLocked()) {
+      throw new LockedObjectException('Collector is already locked.', E_WARNING);
+    }
+
+    $this->locked = TRUE;
+    $this->lockKey = $key;
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function unlock($key) {
+    if (!$this->isLocked()) {
+      throw new LockedObjectException('Collector is not locked', E_WARNING);
+    }
+
+    if ($this->lockKey !== $key) {
+      throw new LockedObjectException('Attempted to unlock Collector with incorrect key.', E_WARNING);
+    }
+
+    $this->locked = FALSE;
+    $this->lockKey = NULL;
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isLocked() {
+    return $this->locked;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown if an invalid metadata type is provided (i.e., not 'css' or 'js').
+   */
+  public function setDefaultMetadata(AssetMetadataBag $metadata) {
+    if ($this->isLocked()) {
+      throw new LockedObjectException('The collector instance is locked. Asset defaults cannot be modified on a locked collector.');
+    }
+
+    $type = $metadata->getType();
+
+    if ($type === 'css') {
+      $this->defaultCssMetadata = $metadata;
+    }
+    elseif ($type === 'js') {
+      $this->defaultJsMetadata = $metadata;
+    }
+    else {
+      throw new \InvalidArgumentException(sprintf('Only assets of type "js" or "css" are supported, "%s" requested.', $type));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMetadataDefaults($type) {
+    if ($type === 'css') {
+      return clone $this->defaultCssMetadata;
+    }
+    elseif ($type === 'js') {
+      return clone $this->defaultJsMetadata;
+    }
+    else {
+      throw new \InvalidArgumentException(sprintf('Only assets of type "js" or "css" are supported, "%s" requested.', $type));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function restoreDefaults() {
+    if ($this->isLocked()) {
+      throw new LockedObjectException('The collector instance is locked. Asset defaults cannot be modified on a locked collector.');
+    }
+    $this->defaultCssMetadata = new CssMetadataBag();
+    $this->defaultJsMetadata = new JsMetadataBag();
+  }
+}
+
diff --git a/core/lib/Drupal/Core/Asset/Factory/AssetCollectorInterface.php b/core/lib/Drupal/Core/Asset/Factory/AssetCollectorInterface.php
new file mode 100644
index 0000000..2c5a817
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Factory/AssetCollectorInterface.php
@@ -0,0 +1,210 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Factory\AssetCollectorInterface.
+ */
+
+namespace Drupal\Core\Asset\Factory;
+
+use Drupal\Core\Asset\Exception\LockedObjectException;
+use Drupal\Core\Asset\Metadata\AssetMetadataBag;
+use Drupal\Core\Asset\AssetInterface;
+use Drupal\Core\Asset\Collection\AssetCollectionInterface;
+
+/**
+ * Interface for asset collectors, which help to create and collect assets.
+ *
+ * A "collector" is an elaboration on a factory pattern. Collectors can
+ * optionally contain a collection that is designed to accommodate the type of
+ * asset produced by the factory. If the collector has a collection, then
+ * calling its factory methods will cause the created object to automatically
+ * be added to the contained collection. Thus, the collector can be safely
+ * injected into code whose only responsibility should be to append new items
+ * to the collection.
+ */
+interface AssetCollectorInterface {
+
+  /**
+   * Adds an asset to the contained collection.
+   *
+   * It is not necessary to call this method on assets that were created via the
+   * create() method; that is done implicitly.
+   *
+   * @param AssetInterface $asset
+   *   The asset to add to the contained collection.
+   *
+   * @throws \RuntimeException
+   *   Thrown if the collector has no contained collection.
+   */
+  public function add(AssetInterface $asset);
+
+  /**
+   * Creates an asset, stores it in the collector's collection, and returns it.
+   *
+   * TODO flesh out these docs to be equivalent to drupal_add_css/js()
+   *
+   * @param string $asset_type
+   *      A string indicating the asset type - must be 'css' or 'js'.
+   * @param string $source_type
+   *      A string indicating the source type - 'file', 'external' or 'string'.
+   * @param string $data
+   *      A string containing data that defines the asset. Appropriate values vary
+   *      depending on the source_type param:
+   *      - 'file': the relative path to the file, or a stream wrapper URI.
+   *      - 'external': the absolute path to the external asset.
+   *      - 'string': a string containing valid CSS or Javascript to be injected
+   *      directly onto the page.
+   * @param array $options
+   *      (optional) An array of metadata to explicitly set on the asset. These
+   *      will override metadata defaults that are injected onto the asset at
+   *      creation time.
+   * @param array $filters
+   *      (optional) An array of filters to apply to the object
+   *      TODO this should, maybe, be removed entirely
+   * @param bool $keep_last
+   *      (optional) Whether or not to retain the created asset for automated
+   *      ordering purposes. Only applies to CSS. Note that passing FALSE will not
+   *      prevent a CSS asset that is being created from automatically being
+   *      after() the existing lastCss asset, if one exists. For that,
+   *
+   * @see clearLastCss().
+   *
+   * @return \Drupal\Core\Asset\AssetInterface
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown if an invalid asset type or source type is passed.
+   */
+  public function create($asset_type, $source_type, $data, $options = array(), $filters = array(), $keep_last = TRUE);
+
+  /**
+   * Clears the asset stored in lastCss.
+   *
+   * Ordinarily, using the create() factory to generate a CSS asset object will
+   * automatically set up an ordering relationship between that asset and the
+   * previous CSS asset that was created. This is intended to facilitate the
+   * rigid ordering that authors likely expect for CSS assets declared together
+   * in a contiguous series.
+   *
+   * This method clears the last stored CSS asset. It should be called when the
+   * end of such a contiguous series is reached, or by the asset creator
+   * themselves if they want to avoid the creation of the ordering relationship.
+   *
+   * @return AssetCollector
+   *   The current AssetCollector instance, for easy chaining.
+   */
+  public function clearLastCss();
+
+  /**
+   * Sets the internal collection for this collector.
+   *
+   * As long as this collection is present, the collector will automatically add
+   * all assets generated via its create() method to the collection.
+   *
+   * @param AssetCollectionInterface $collection
+   *
+   * @return void
+   *
+   * @throws LockedObjectException
+   *   Thrown if the collector is locked when this method is called.
+   */
+  public function setCollection(AssetCollectionInterface $collection);
+
+  /**
+   * Clears the internal collection for this collector.
+   *
+   * @return void
+   *
+   * @throws LockedObjectException
+   *   Thrown if the collector is locked when this method is called.
+   */
+  public function clearCollection();
+
+  /**
+   * Indicates whether or not this collector currently contains a collection.
+   *
+   * @return bool
+   */
+  public function hasCollection();
+
+  /**
+   * Locks this collector, using the provided key.
+   *
+   * The collector can only be unlocked by providing the same key. Key
+   * comparison is done using the identity operator (===), so avoid using an
+   * object as a key if there is any chance the collector will be serialized.
+   *
+   * @param mixed $key
+   *   The key used to lock the collector.
+   *
+   * @return void
+   *
+   * @throws LockedObjectException
+   *   Thrown if the collector is already locked.
+   */
+  public function lock($key);
+
+  /**
+   * Attempts to unlock the collector with the provided key.
+   *
+   * Key comparison is done using the identity operator (===).
+   *
+   * @param mixed $key
+   *   The key with which to unlock the collector.
+   *
+   * @return void
+   *
+   * @throws LockedObjectException
+   *   Thrown if the incorrect key is provided, or if the collector is not
+   *   locked.
+   */
+  public function unlock($key);
+
+  /**
+   * Indicates whether this collector is currently locked.
+   *
+   * @return bool
+   */
+  public function isLocked();
+
+  /**
+   * Sets the default metadata for a particular type.
+   *
+   * The type of metadata is determined internally by calling
+   * AssetMetadataBag::getType().
+   *
+   * @param AssetMetadataBag $metadata
+   *   The default metadata object.
+   *
+   * @return void
+   */
+  public function setDefaultMetadata(AssetMetadataBag $metadata);
+
+  /**
+   * Gets a clone of the metadata bag for a given asset type.
+   *
+   * Clones are returned in order to ensure there is a unique metadata object
+   * for every asset, and that the default metadata contained in the collector
+   * cannot be modified externally.
+   *
+   * @param $type
+   *   A string, 'css' or 'js', indicating the type of metadata to retrieve.
+   *
+   * @return AssetMetadataBag
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown if a type other than 'css' or 'js' is provided.
+   */
+  public function getMetadataDefaults($type);
+
+  /**
+   * Restores metadata default bags to their default state.
+   *
+   * This simply creates new instances of CssMetadataBag and JsMetadataBag, as
+   * those classes have the normal defaults as hardmapped properties.
+   *
+   * @throws LockedObjectException
+   *   Thrown if the incorrect key is provided.
+   */
+  public function restoreDefaults();
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/FileAsset.php b/core/lib/Drupal/Core/Asset/FileAsset.php
new file mode 100644
index 0000000..6fede36
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/FileAsset.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\FileAsset.
+ */
+
+namespace Drupal\Core\Asset;
+
+use Assetic\Util\PathUtils;
+use Assetic\Filter\FilterInterface;
+use Drupal\Core\Asset\BaseAsset;
+use Drupal\Core\Asset\Metadata\AssetMetadataBag;
+
+class FileAsset extends BaseAsset {
+
+  protected $source;
+
+  public function __construct(AssetMetadataBag $metadata, $source, $filters = array()) {
+    $sourceRoot = dirname($source);
+    $sourcePath = basename($source);
+    $this->source = $source;
+
+    parent::__construct($metadata, $filters, $sourceRoot, $sourcePath);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function id() {
+    return $this->source;
+  }
+
+  /**
+   * Returns the time the current asset was last modified.
+   *
+   * @return integer|null A UNIX timestamp
+   */
+  public function getLastModified() {
+    if (!is_file($this->source)) {
+      throw new \RuntimeException(sprintf('The source file "%s" does not exist.', $this->source));
+    }
+
+    return filemtime($this->source);
+  }
+
+  /**
+   * Loads the asset into memory and applies load filters.
+   *
+   * You may provide an additional filter to apply during load.
+   *
+   * @todo copied right from Assetic. needs to be made more Drupalish.
+   *
+   * @param FilterInterface $additionalFilter An additional filter
+   */
+  public function load(FilterInterface $additionalFilter = NULL) {
+    if (!is_file($this->source)) {
+      throw new \RuntimeException(sprintf('The source file "%s" does not exist.', $this->source));
+    }
+
+    $this->doLoad(file_get_contents($this->source), $additionalFilter);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Asset/GroupSort/AssetGraphSorter.php b/core/lib/Drupal/Core/Asset/GroupSort/AssetGraphSorter.php
new file mode 100644
index 0000000..6311f9f
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/GroupSort/AssetGraphSorter.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Sort\AssetGraphSorter.
+ */
+
+namespace Drupal\Core\Asset\GroupSort;
+
+use Drupal\Core\Asset\AssetGraph;
+use Gliph\Traversal\DepthFirst;
+use Gliph\Visitor\DepthFirstBasicVisitor;
+
+/**
+ * Sorts an AssetCollectionInterface's contents into a list using a graph.
+ */
+abstract class AssetGraphSorter implements AssetGroupSorterInterface {
+
+  /**
+   * Creates a queue of starting vertices that will facilitate an ideal TSL.
+   *
+   * @param AssetGraph $original
+   * @param AssetGraph $transpose
+   *
+   * @return \SplQueue $queue
+   *   A queue of vertices
+   */
+  protected function createSourceQueue(AssetGraph $original, AssetGraph $transpose) {
+    $reach_visitor = new DepthFirstBasicVisitor();
+
+    // Find source vertices (outdegree 0) in the original graph
+    $sources = DepthFirst::find_sources($original, $reach_visitor);
+
+    // Traverse the transposed graph to get reachability data on each vertex
+    DepthFirst::traverse($transpose, $reach_visitor, clone $sources);
+
+    // Sort vertices via a PriorityQueue based on total reach
+    $pq = new \SplPriorityQueue();
+    foreach ($sources as $vertex) {
+      $pq->insert($vertex, count($reach_visitor->getReachable($vertex)));
+    }
+
+    // Dump the priority queue into a normal queue
+    // TODO maybe gliph should support pq/heaps as a queue type on which to operate?
+    $queue = new \SplQueue();
+    foreach ($pq as $vertex) {
+      $queue->push($vertex);
+    }
+
+    return $queue;
+  }
+
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/GroupSort/AssetGroupSorterInterface.php b/core/lib/Drupal/Core/Asset/GroupSort/AssetGroupSorterInterface.php
new file mode 100644
index 0000000..eeb771c
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/GroupSort/AssetGroupSorterInterface.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Sort\AssetGroupSorterInterface.
+ */
+
+namespace Drupal\Core\Asset\GroupSort;
+use Drupal\Core\Asset\AssetInterface;
+use Drupal\Core\Asset\Collection\AssetCollectionInterface;
+
+/**
+ * Interface for classes that sort asset collections for output.
+ */
+interface AssetGroupSorterInterface {
+
+  /**
+   * Sorts the provided collection into an output-safe linear list.
+   *
+   * Accounts for dependency and ordering metadata.
+   *
+   * @param AssetCollectionInterface $collection
+   *   The collection to group and sort.
+   *
+   * @return array
+   *   A sorted, linear list of assets that respects all necessary dependency
+   *   information.
+   */
+  public function groupAndSort(AssetCollectionInterface $collection);
+
+  /**
+   * Provides a string key identifying the grouping parameters for an asset.
+   *
+   * Assets with the same grouping key are in alignment, meaning that they can
+   * be safely aggregated together into a single, composite asset.
+   *
+   * @param AssetInterface $asset
+   *   The asset for which to produce a grouping key.
+   *
+   * @return string|FALSE
+   *   A string containing grouping parameters, or FALSE if the asset is
+   *   ineligible for grouping.
+   */
+  public static function getGroupingKey(AssetInterface $asset);
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/GroupSort/CssGraphSorter.php b/core/lib/Drupal/Core/Asset/GroupSort/CssGraphSorter.php
new file mode 100644
index 0000000..3f373c5
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/GroupSort/CssGraphSorter.php
@@ -0,0 +1,109 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\GroupSort\CssGraphSorter.
+ */
+
+namespace Drupal\Core\Asset\GroupSort;
+
+use Drupal\Core\Asset\OptimallyGroupedTSLVisitor;
+use Drupal\Core\Asset\ExternalAsset;
+use Drupal\Core\Asset\AssetInterface;
+use Drupal\Core\Asset\FileAsset;
+use Drupal\Core\Asset\Collection\AssetCollectionInterface;
+use Drupal\Core\Asset\AssetGraph;
+use Gliph\Traversal\DepthFirst;
+use Drupal\Core\Asset\StringAsset;
+
+/**
+ * Performs a graph sort on CSS assets.
+ */
+class CssGraphSorter extends AssetGraphSorter {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getGroupingKey(AssetInterface $asset) {
+    $meta = $asset->getMetadata();
+    // The browsers for which the CSS item needs to be loaded is part of the
+    // information that determines when a new group is needed, but the order
+    // of keys in the array doesn't matter, and we don't want a new group if
+    // all that's different is that order.
+    $browsers = $meta->get('browsers');
+    ksort($browsers);
+
+    if ($asset instanceof FileAsset) {
+      // Compose a string key out of the set of relevant properties.
+      // TODO - this ignores group, which is used in core's current implementation. wishful thinking? maybe, maybe not.
+      // TODO media has been pulled out - needs to be handled by the aggregator, wrapping css in media queries
+      $k = $asset->isPreprocessable()
+        ? implode(':', array('file', $meta->get('every_page'), implode('', $browsers)))
+        : FALSE;
+    }
+    else if ($asset instanceof StringAsset) {
+      // String items are always grouped.
+      // TODO use the term 'inline' here? do "string" and "inline" necessarily mean the same?
+      $k = implode(':', 'string', implode('', $browsers));
+    }
+    else if ($asset instanceof ExternalAsset) {
+      // Never group external assets.
+      $k = FALSE;
+    }
+    else {
+      throw new \UnexpectedValueException(sprintf('Unknown CSS asset type "%s" somehow made it into the CSS collection during grouping.', get_class($asset)));
+    }
+
+    return $k;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function groupAndSort(AssetCollectionInterface $collection) {
+    // We need to define the optimum minimal group set, given metadata
+    // boundaries across which aggregates cannot be safely made.
+    $optimal = array();
+
+    // Also create an SplObjectStorage to act as a lookup table on an asset to
+    // its group, if any.
+    // TODO try and find an elegant way to pass this out so we don't have to calculate keys twice
+    $optimal_lookup = new \SplObjectStorage();
+
+    // Finally, create a specialized directed adjacency list that will capture
+    // all ordering information.
+    $graph = new AssetGraph();
+
+    foreach ($collection->getCss() as $asset) {
+      $graph->addVertex($asset);
+
+      $k = self::getGroupingKey($asset);
+
+      if ($k === FALSE) {
+        // Record no optimality information for ungroupable assets; they will
+        // be visited normally and rearranged as needed.
+        continue;
+      }
+
+      if (!isset($optimal[$k])) {
+        // Create an SplObjectStorage to represent each set of assets that would
+        // optimally be grouped together.
+        $optimal[$k] = new \SplObjectStorage();
+      }
+      $optimal[$k]->attach($asset, $k);
+      $optimal_lookup->attach($asset, $optimal[$k]);
+    }
+
+    // First, transpose the graph in order to get an appropriate answer
+    $transpose = $graph->transpose();
+
+    // Create a queue of start vertices to prime the traversal.
+    $queue = $this->createSourceQueue($graph, $transpose);
+
+    // Now, create the visitor and walk the graph to get an optimal TSL.
+    $visitor = new OptimallyGroupedTSLVisitor($optimal, $optimal_lookup);
+    DepthFirst::traverse($transpose, $visitor, $queue);
+
+    return $visitor->getTSL();
+  }
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataBag.php b/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataBag.php
new file mode 100644
index 0000000..4681ea9
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataBag.php
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\AssetMetadataBag.
+ */
+
+namespace Drupal\Core\Asset\Metadata;
+
+/**
+ * A bag for holding asset metadata.
+ *
+ * For each declared property, this bag keeps track of both a default value and
+ * an explicit value. Defaults can only be set in the constructor, explicit
+ * values can be set at any time. Explicit values are coalesced over default
+ * values.
+ *
+ * TODO this is totally not specific to assets - move it somewhere more generic?
+ * TODO it's maybe not so important to rigorously control access to the defaults data
+ */
+abstract class AssetMetadataBag implements \IteratorAggregate, \Countable {
+
+  /**
+   * Contains default values.
+   *
+   * @var array
+   */
+  protected $default = array();
+
+  /**
+   * Contains explicitly set values.
+   *
+   * @var array
+   */
+  protected $explicit = array();
+
+  public function __construct(array $default = array()) {
+    $this->default = array_replace_recursive($this->default, $default);
+  }
+
+  /**
+   * Indicates the type of asset for which this metadata is intended.
+   *
+   * @return string
+   *   A string indicating type - 'js' or 'css' are the expected values.
+   */
+  abstract public function getType();
+
+  public function all() {
+    return array_replace_recursive($this->default, $this->explicit);
+  }
+
+  public function keys() {
+    return array_keys($this->all());
+  }
+
+  public function has($key) {
+    return array_key_exists($key, $this->explicit) ||
+      array_key_exists($key, $this->default);
+  }
+
+  public function set($key, $value) {
+    $this->explicit[$key] = $value;
+  }
+
+  /**
+   * Reverts the associated with the passed key back to its default.
+   *
+   * If no default is set, the value for that key simply disappears.
+   *
+   * @param $key
+   *   The key identifying the value to revert.
+   *
+   * @return void
+   */
+  public function revert($key) {
+    unset($this->explicit[$key]);
+  }
+
+  public function isDefault($key) {
+    return !array_key_exists($key, $this->explicit) &&
+      array_key_exists($key, $this->default);
+  }
+
+  public function add(array $values = array()) {
+    $this->explicit = array_replace_recursive($this->explicit, $values);
+  }
+
+  public function replace(array $values = array()) {
+    $this->explicit = $values;
+  }
+
+  public function get($key) {
+    if (array_key_exists($key, $this->explicit)) {
+      return $this->explicit[$key];
+    }
+
+    if (array_key_exists($key, $this->default)) {
+      return $this->default[$key];
+    }
+  }
+
+  public function getIterator() {
+    return new \ArrayIterator($this->all());
+  }
+
+  public function count() {
+    return count($this->all());
+  }
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/Metadata/CssMetadataBag.php b/core/lib/Drupal/Core/Asset/Metadata/CssMetadataBag.php
new file mode 100644
index 0000000..d4c6322
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Metadata/CssMetadataBag.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\CssMetadataBag.
+ */
+
+namespace Drupal\Core\Asset\Metadata;
+use Drupal\Core\Asset\Metadata\AssetMetadataBag;
+
+/**
+ * Manages CSS asset default and explicit metadata.
+ */
+class CssMetadataBag extends AssetMetadataBag {
+
+  protected $default = array(
+    'group' => CSS_AGGREGATE_DEFAULT, // TODO Just removing this would be *awesome*.
+    'every_page' => FALSE,
+    'media' => 'all',
+    'preprocess' => TRUE,
+    'browsers' => array(
+      'IE' => TRUE,
+      '!IE' => TRUE,
+    ),
+  );
+
+  public function __construct(array $default = array()) {
+    $this->default = array_replace_recursive($this->default, $default);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getType() {
+    return 'css';
+  }
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/Metadata/JsMetadataBag.php b/core/lib/Drupal/Core/Asset/Metadata/JsMetadataBag.php
new file mode 100644
index 0000000..39927c0
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Metadata/JsMetadataBag.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\JsMetadataBag.
+ */
+
+namespace Drupal\Core\Asset\Metadata;
+use Drupal\Core\Asset\Metadata\AssetMetadataBag;
+
+/**
+ * Manages Javascript asset default and explicit metadata.
+ */
+class JsMetadataBag extends AssetMetadataBag {
+
+  protected $default = array(
+    'group' => JS_DEFAULT,
+    'every_page' => FALSE,
+    'scope' => 'header',
+    'cache' => TRUE,
+    'preprocess' => TRUE,
+    'attributes' => array(),
+    'version' => NULL,
+    'browsers' => array(),
+  );
+
+  public function __construct(array $default = array()) {
+    $this->default = array_replace_recursive($this->default, $default);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getType() {
+    return 'js';
+  }
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/OptimallyGroupedTSLVisitor.php b/core/lib/Drupal/Core/Asset/OptimallyGroupedTSLVisitor.php
new file mode 100644
index 0000000..cc77d0e
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/OptimallyGroupedTSLVisitor.php
@@ -0,0 +1,111 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\OptimallyGroupedTSLVisitor.
+ */
+
+namespace Drupal\Core\Asset;
+use Gliph\Visitor\DepthFirstVisitorInterface;
+
+/**
+ * DepthFirst visitor intended for use with a asset data that will select the
+ * optimal valid TSL, given a preferred grouping of vertices.
+ */
+class OptimallyGroupedTSLVisitor implements DepthFirstVisitorInterface {
+
+  /**
+   * @var array
+   */
+  protected $tsl;
+
+  /**
+   * @var array
+   */
+  protected $groups;
+
+  /**
+   * @var \SplObjectStorage
+   */
+  protected $vertexMap;
+
+  /**
+   * Creates a new optimality visitor.
+   *
+   * @param array $groups
+   *   An array of SplObjectStorage, the contents of each representing an
+   *   optimal grouping.
+   *
+   * @param \SplObjectStorage $vertex_map
+   *   A map of vertices to the group in which they reside, if any.
+   */
+  public function __construct($groups, \SplObjectStorage $vertex_map) {
+    $this->tsl = array();
+    $this->groups = $groups;
+    $this->vertexMap = $vertex_map;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onInitializeVertex($vertex, $source, \SplQueue $queue) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onBackEdge($vertex, \Closure $visit) {
+    // TODO: Implement onBackEdge() method.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onStartVertex($vertex, \Closure $visit) {
+    $this->active->attach($vertex);
+
+    // If there's a record in the vertex map, it means this vertex has an
+    // optimal group. Remove it from that group, as it being here means it's
+    // been visited.
+    if ($this->vertexMap->contains($vertex)) {
+      $this->vertexMap[$vertex]->detach($vertex);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onExamineEdge($from, $to, \Closure $visit) {}
+
+  /**
+   * Here be the unicorns.
+   *
+   * Once the depth-first traversal is done for a vertex, rather than
+   * simply pushing it onto the TSL and moving on (as in a basic depth-first
+   * traversal), if the finished vertex is a member of an optimality group, then
+   * visit all other (unvisited) members of that optimality group.
+   *
+   * This ensures the final TSL has the tightest possible adherence to the
+   * defined optimal groupings while still respecting the DAG.
+   *
+   */
+  public function onFinishVertex($vertex, \Closure $visit) {
+    // TODO this still isn't quite optimal; it can split groups unnecessarily. tweak a little more.
+    // TODO explore risk of hitting the 100 call stack limit
+    if ($this->vertexMap->contains($vertex)) {
+      foreach ($this->vertexMap[$vertex] as $vertex) {
+        $visit($vertex);
+      }
+    }
+    $this->tsl[] = $vertex;
+  }
+
+  /**
+   * Returns the TSL produced by a depth-first traversal.
+   *
+   * @return array
+   *   A topologically sorted list of vertices.
+   */
+  public function getTSL() {
+    return $this->tsl;
+  }
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/StringAsset.php b/core/lib/Drupal/Core/Asset/StringAsset.php
new file mode 100644
index 0000000..e98000e
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/StringAsset.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\StringAsset.
+ */
+
+namespace Drupal\Core\Asset;
+
+use Assetic\Filter\FilterInterface;
+use Drupal\Component\Utility\Crypt;
+use Drupal\Core\Asset\BaseAsset;
+use Drupal\Core\Asset\Metadata\AssetMetadataBag;
+
+class StringAsset extends BaseAsset {
+
+  /**
+   * The string id of this asset.
+   *
+   * This is generated by hashing the content of the asset when the object is
+   * first created. The id does NOT change if the content is changed later.
+   *
+   * @var string
+   */
+  protected $id;
+
+  protected $lastModified;
+
+  public function __construct(AssetMetadataBag $metadata, $content, $filters = array()) {
+    if (!is_string($content)) {
+      throw new \InvalidArgumentException('StringAsset requires a string for its content.');
+    }
+
+    $this->id= empty($content) ? Crypt::randomStringHashed(32) : hash('sha256', $content);
+    $this->setContent($content);
+    $this->lastModified = REQUEST_TIME; // TODO this is terrible
+
+    parent::__construct($metadata, $filters);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function id() {
+    return $this->id;
+  }
+
+  public function setLastModified($last_modified) {
+    $this->lastModified = $last_modified;
+  }
+
+  public function getLastModified() {
+    return $this->lastModified;
+  }
+
+  public function load(FilterInterface $additionalFilter = NULL) {
+    $this->doLoad($this->getContent(), $additionalFilter);
+  }
+}
diff --git a/core/modules/block/lib/Drupal/block/BlockBase.php b/core/modules/block/lib/Drupal/block/BlockBase.php
index 579e841..1768991 100644
--- a/core/modules/block/lib/Drupal/block/BlockBase.php
+++ b/core/modules/block/lib/Drupal/block/BlockBase.php
@@ -11,6 +11,7 @@
 use Drupal\block\BlockInterface;
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Language\Language;
+use Drupal\Core\Asset\Factory\AssetCollector;
 
 /**
  * Defines a base block implementation that most blocks plugins will extend.
@@ -181,5 +182,10 @@ public function getMachineNameSuggestion() {
 
     return $transliterated;
   }
-
+  /**
+   * {@inheritdoc}
+   */
+  public function declareAssets(AssetCollector $collector) {}
 }
+
+
diff --git a/core/modules/block/lib/Drupal/block/BlockPluginInterface.php b/core/modules/block/lib/Drupal/block/BlockPluginInterface.php
index b5433e6..d625616 100644
--- a/core/modules/block/lib/Drupal/block/BlockPluginInterface.php
+++ b/core/modules/block/lib/Drupal/block/BlockPluginInterface.php
@@ -10,6 +10,7 @@
 use Drupal\Component\Plugin\PluginInspectionInterface;
 use Drupal\Component\Plugin\ConfigurablePluginInterface;
 use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\Core\Asset\Factory\AssetCollector;
 
 /**
  * Defines the required interface for all block plugins.
@@ -122,4 +123,12 @@ public function blockSubmit($form, &$form_state);
    */
   public function getMachineNameSuggestion();
 
+  /**
+   * Declares the assets required by this block to a collector.
+   *
+   * @param \Drupal\Core\Asset\Factory\AssetCollector $collector
+   *
+   * @return void
+   */
+  public function declareAssets(AssetCollector $collector);
 }
diff --git a/core/tests/Drupal/Tests/Core/Asset/Aggregate/BaseAggregateAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/Aggregate/BaseAggregateAssetTest.php
new file mode 100644
index 0000000..5a04a0c
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/Aggregate/BaseAggregateAssetTest.php
@@ -0,0 +1,107 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\Aggregate\BaseAggregateAssetTest.
+ */
+
+namespace Drupal\Tests\Core\Asset\Aggregate;
+
+use Drupal\Tests\Core\Asset\AssetUnitTest;
+
+/**
+ *
+ * @group Asset
+ */
+class BaseAggregateAssetTest extends AssetUnitTest {
+
+  protected $aggregate;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Asset aggregate tests',
+      'description' => 'Unit tests on BaseAggregateAsset',
+      'group' => 'Asset',
+    );
+  }
+
+  public function getAggregate($defaults = array()) {
+    $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag', $defaults);
+    $this->aggregate = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Aggregate\\BaseAggregateAsset');
+  }
+
+  public function testId() {
+  }
+
+  public function testGetAssetType() {
+
+  }
+
+  public function testGetMetadata() {
+
+  }
+
+  public function testContains() {
+
+  }
+
+  public function testGetById() {
+
+  }
+
+  public function testIsPreprocessable() {
+
+  }
+
+  public function testAll() {
+
+  }
+
+  public function testEnsureFilter() {
+
+  }
+
+  public function testGetFilters() {
+
+  }
+
+  public function testClearFilters() {
+
+  }
+
+  public function testGetContent() {
+
+  }
+
+  public function testSetContent() {
+
+  }
+
+  public function testGetSourceRoot() {
+
+  }
+
+  public function testGetSourcePath() {
+
+  }
+
+  public function testGetTargetPath() {
+
+  }
+
+  public function testSetTargetPath() {
+
+  }
+
+  public function testGetLastModified() {
+
+  }
+
+  public function testGetIterator() { // ??
+
+  }
+
+  public function testIsEmpty() {
+
+  }
+}
diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetGraphTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetGraphTest.php
new file mode 100644
index 0000000..b83698d
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/AssetGraphTest.php
@@ -0,0 +1,258 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\AssetGraphTest.
+ */
+
+namespace Drupal\Tests\Core\Asset;
+
+use Drupal\Core\Asset\AssetGraph;
+use Drupal\Core\Asset\BaseAsset;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ *
+ * @group Asset
+ */
+class AssetGraphTest extends AssetUnitTest {
+
+  /**
+   * @var AssetGraph
+   */
+  protected $graph;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Asset graph test',
+      'description' => 'Tests that custom additions in the asset graph work correctly.',
+      'group' => 'Asset',
+    );
+  }
+
+  public function setUp() {
+    parent::setUp();
+    $this->graph = new AssetGraph();
+  }
+
+  /**
+   * Generates a simple mock asset object.
+   *
+   * @param string $id
+   *   An id to give the asset; it will returned from the mocked
+   *   AssetInterface::id() method.
+   *
+   * @return \PHPUnit_Framework_MockObject_MockObject
+   *   A mock of a BaseAsset object.
+   */
+  public function createBasicAssetMock($id = 'foo') {
+    $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag');
+    $mock = $this->getMockBuilder('\\Drupal\\Core\\Asset\\BaseAsset')
+      ->setConstructorArgs(array($mockmeta))
+      ->getMock();
+
+    $mock->expects($this->any())
+      ->method('id')
+      ->will($this->returnValue($id));
+
+    $mock->expects($this->once())
+      ->method('getPredecessors')
+      ->will($this->returnValue(array()));
+
+    $mock->expects($this->once())
+      ->method('getSuccessors')
+      ->will($this->returnValue(array()));
+
+    return $mock;
+  }
+
+  public function doCheckVertexCount($count, AssetGraph $graph = NULL) {
+    $found = array();
+    $graph = is_null($graph) ? $this->graph : $graph;
+
+    $graph->eachVertex(function ($vertex) use (&$found) {
+      $found[] = $vertex;
+    });
+
+    $this->assertCount($count, $found);
+  }
+
+  public function doCheckVerticesEqual($vertices, AssetGraph $graph = NULL) {
+    $found = array();
+    $graph = is_null($graph) ? $this->graph : $graph;
+
+    $graph->eachVertex(function ($vertex) use (&$found) {
+      $found[] = $vertex;
+    });
+
+    $this->assertEquals($vertices, $found);
+  }
+
+  public function testAddSingleVertex() {
+    $mock = $this->createBasicAssetMock();
+
+    $mock->expects($this->exactly(2))
+      ->method('id')
+      ->will($this->returnValue('foo'));
+
+    $this->graph->addVertex($mock);
+
+    $this->doCheckVerticesEqual(array($mock));
+  }
+
+  /**
+   * @expectedException \Gliph\Exception\InvalidVertexTypeException
+   */
+  public function testAddInvalidVertexType() {
+    $this->graph->addVertex(new \stdClass());
+  }
+
+  /**
+   * @expectedException \LogicException
+   */
+  public function testExceptionOnRemoval() {
+    $mock = $this->createBasicAssetMock();
+    $this->graph->addVertex($mock);
+    $this->graph->removeVertex($mock);
+  }
+
+  public function testAddUnconnectedVertices() {
+    $foo = $this->createBasicAssetMock('foo');
+    $bar = $this->createBasicAssetMock('bar');
+
+    $this->graph->addVertex($foo);
+    $this->graph->addVertex($bar);
+
+    $this->doCheckVerticesEqual(array($foo, $bar));
+  }
+
+  /**
+   * Tests that edges are automatically created correctly when assets have
+   * sequencing information.
+   */
+  public function testAddConnectedVertices() {
+    $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag');
+    $foo = $this->getMockBuilder('\\Drupal\\Core\\Asset\\BaseAsset')
+      ->setConstructorArgs(array($mockmeta))
+      ->getMock();
+
+    $foo->expects($this->exactly(3))
+      ->method('id')
+      ->will($this->returnValue('foo'));
+
+    $foo->expects($this->once())
+      ->method('getPredecessors')
+      ->will($this->returnValue(array('bar')));
+
+    $foo->expects($this->once())
+      ->method('getSuccessors')
+      ->will($this->returnValue(array('baz')));
+
+    $bar = $this->createBasicAssetMock('bar');
+    $baz = $this->createBasicAssetMock('baz');
+
+    $this->graph->addVertex($foo);
+    $this->graph->addVertex($bar);
+    $this->graph->addVertex($baz);
+
+    $this->doCheckVerticesEqual(array($foo, $bar, $baz));
+
+    $lister = function($vertex) use (&$out) {
+      $out[] = $vertex;
+    };
+
+    $out = array();
+    $this->graph->eachAdjacent($foo, $lister);
+    $this->assertEquals(array($bar), $out);
+
+    $out = array();
+    $this->graph->eachAdjacent($baz, $lister);
+    $this->assertEquals(array($foo), $out);
+
+    $out = array();
+    $this->graph->eachAdjacent($bar, $lister);
+    $this->assertEmpty($out);
+
+    // Now add another vertex with sequencing info that targets already-inserted
+    // vertices.
+
+    $qux = $this->getMockBuilder('\\Drupal\\Core\\Asset\\BaseAsset')
+      ->setConstructorArgs(array($mockmeta))
+      ->getMock();
+
+    $qux->expects($this->exactly(2))
+      ->method('id')
+      ->will($this->returnValue('qux'));
+
+    // Do this one with the foo vertex itself, not its string id.
+    $qux->expects($this->once())
+      ->method('getPredecessors')
+      ->will($this->returnValue(array($foo)));
+
+    $qux->expects($this->once())
+      ->method('getSuccessors')
+      ->will($this->returnValue(array('bar', 'baz')));
+
+    $this->graph->addVertex($qux);
+
+    $this->doCheckVerticesEqual(array($foo, $bar, $baz, $qux));
+
+    $out = array();
+    $this->graph->eachAdjacent($qux, $lister);
+    $this->assertEquals(array($foo), $out);
+
+    $out = array();
+    $this->graph->eachAdjacent($bar, $lister);
+    $this->assertEquals(array($qux), $out);
+
+    $out = array();
+    $this->graph->eachAdjacent($baz, $lister);
+    $this->assertEquals(array($foo, $qux), $out);
+  }
+
+  public function testTranspose() {
+    $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag');
+    $foo = $this->getMockBuilder('\\Drupal\\Core\\Asset\\BaseAsset')
+      ->setConstructorArgs(array($mockmeta))
+      ->getMock();
+
+    $foo->expects($this->exactly(3))
+      ->method('id')
+      ->will($this->returnValue('foo'));
+
+    $foo->expects($this->once())
+      ->method('getPredecessors')
+      ->will($this->returnValue(array('bar')));
+
+    $foo->expects($this->once())
+      ->method('getSuccessors')
+      ->will($this->returnValue(array('baz')));
+
+    $bar = $this->createBasicAssetMock('bar');
+    $baz = $this->createBasicAssetMock('baz');
+
+    $this->graph->addVertex($foo);
+    $this->graph->addVertex($bar);
+    $this->graph->addVertex($baz);
+
+    $transpose = $this->graph->transpose();
+    $this->doCheckVerticesEqual(array($foo, $bar, $baz), $transpose);
+
+    // Verify that the transpose has a fully inverted edge set.
+    $lister = function($vertex) use (&$out) {
+      $out[] = $vertex;
+    };
+
+    $out = array();
+    $transpose->eachAdjacent($bar, $lister);
+    $this->assertEquals(array($foo), $out);
+
+    $out = array();
+    $transpose->eachAdjacent($foo, $lister);
+    $this->assertEquals(array($baz), $out);
+
+    $out = array();
+    $transpose->eachAdjacent($baz, $lister);
+    $this->assertEmpty($out);
+  }
+}
diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetUnitTest.php
new file mode 100644
index 0000000..dab09b3
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/AssetUnitTest.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\AssetUnitTest.
+ */
+
+namespace Drupal\Tests\Core\Asset;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Provides base standard fixtures and mocks for Asset tests.
+ */
+abstract class AssetUnitTest extends UnitTestCase {
+
+  public function createMockFileAsset($type) {
+    $asset = $this->getMock('Drupal\\Core\\Asset\\FileAsset', array(), array(), '', FALSE);
+    $asset->expects($this->any())
+      ->method('getAssetType')
+      ->will($this->returnValue($type));
+
+    $asset->expects($this->any())
+      ->method('id')
+      ->will($this->returnValue($this->randomName()));
+
+    return $asset;
+  }
+}
\ No newline at end of file
diff --git a/core/tests/Drupal/Tests/Core/Asset/AsseticAdapterAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/AsseticAdapterAssetTest.php
new file mode 100644
index 0000000..69a2bdb
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/AsseticAdapterAssetTest.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: sdboyer
+ * Date: 9/19/13
+ * Time: 2:13 PM
+ */
+
+namespace Drupal\Tests\Core\Asset;
+use Drupal\Core\Asset\AsseticAdapterAsset;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests for the AsseticAdapterAsset, which ensures certain Assetic methods
+ * cannot be called by any child method.
+ *
+ * @group Asset
+ */
+class AsseticAdapterAssetTest extends UnitTestCase {
+
+  /**
+   * @var AsseticAdapterAsset
+   */
+  protected $mock;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Assetic adapter asset test',
+      'description' => 'Tests that certain Assetic methods throw known exceptions in a Drupal context',
+      'group' => 'Asset',
+    );
+  }
+
+  public function setUp() {
+    $this->mock = $this->getMockForAbstractClass('Drupal\\Core\\Asset\\AsseticAdapterAsset');
+  }
+
+  /**
+   * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException
+   */
+  public function testGetVars() {
+    $this->mock->getVars();
+  }
+
+  /**
+   * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException
+   */
+  public function testSetValues() {
+    $this->mock->setValues(array());
+  }
+
+  /**
+   * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException
+   */
+  public function testGetValues() {
+    $this->mock->getValues();
+  }
+}
diff --git a/core/tests/Drupal/Tests/Core/Asset/BaseAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/BaseAssetTest.php
new file mode 100644
index 0000000..f40fc23
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/BaseAssetTest.php
@@ -0,0 +1,138 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\BaseAssetTest.
+ */
+
+namespace Drupal\Tests\Core\Asset;
+use Drupal\Core\Asset\BaseAsset;
+
+/**
+ *
+ * @group Asset
+ */
+class BaseAssetTest extends AssetUnitTest {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Base Asset tests',
+      'description' => 'Unit tests for Drupal\'s BaseAsset.',
+      'group' => 'Asset',
+    );
+  }
+
+  /**
+   * Creates a BaseAsset for testing purposes.
+   *
+   * @param $type
+   *
+   * @return BaseAsset;
+   */
+  public function createBaseAsset($defaults = array()) {
+    $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag', $defaults);
+    return $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\BaseAsset', array($mockmeta));
+  }
+
+  public function testGetMetadata() {
+    $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag');
+    $asset = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\BaseAsset', array($mockmeta));
+
+    $this->assertSame($mockmeta, $asset->getMetadata());
+  }
+
+  public function testGetAssetType() {
+    $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag');
+    $mockmeta->expects($this->once())
+      ->method('getType')
+      ->will($this->returnValue('css'));
+    $asset = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\BaseAsset', array($mockmeta));
+
+    $this->assertEquals('css', $asset->getAssetType());
+  }
+
+  public function testIsPreprocessable() {
+    $mockmeta = $this->getMock('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag');
+    $mockmeta->expects($this->once())
+      ->method('get')
+      ->with('preprocess')
+      ->will($this->returnValue(TRUE));
+    $asset = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\BaseAsset', array($mockmeta));
+
+    $this->assertTrue($asset->isPreprocessable());
+  }
+
+  /**
+   * Tests all dependency-related methods.
+   */
+  public function testDependencies() {
+    $asset = $this->createBaseAsset();
+
+    $asset->addDependency('foo', 'bar');
+    $this->assertEquals(array(array('foo', 'bar')), $asset->getDependencyInfo());
+    $this->assertTrue($asset->hasDependencies());
+
+    $asset->clearDependencies();
+    $this->assertEmpty($asset->getDependencyInfo());
+
+    $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass);
+
+    try {
+      foreach ($invalid as $val) {
+        $asset->addDependency($val, $val);
+        $this->fail('Was able to create an ordering relationship with an inappropriate value.');
+      }
+    } catch (\InvalidArgumentException $e) {}
+  }
+
+  public function testSuccessors() {
+    $asset = $this->createBaseAsset();
+    $dep = $this->createBaseAsset();
+
+    $asset->before('foo');
+    $asset->before($dep);
+
+    $this->assertEquals(array('foo', $dep), $asset->getSuccessors());
+
+    $asset->clearSuccessors();
+    $this->assertEmpty($asset->getSuccessors());
+
+    $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass);
+
+    try {
+      foreach ($invalid as $val) {
+        $asset->before($val);
+        $this->fail('Was able to create an ordering relationship with an inappropriate value.');
+      }
+    } catch (\InvalidArgumentException $e) {}
+  }
+
+  public function testPredecessors() {
+    $asset = $this->createBaseAsset();
+    $dep = $this->createBaseAsset();
+
+    $asset->after('foo');
+    $asset->after($dep);
+    $this->assertEquals(array('foo', $dep), $asset->getPredecessors());
+
+    $asset->clearPredecessors();
+    $this->assertEmpty($asset->getPredecessors());
+
+    $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass);
+
+    try {
+      foreach ($invalid as $val) {
+        $asset->after($val);
+        $this->fail('Was able to create an ordering relationship with an inappropriate value.');
+      }
+    } catch (\InvalidArgumentException $e) {}
+  }
+
+  public function testClone() {
+    $mockmeta = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag');
+    $asset = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\BaseAsset', array($mockmeta));
+
+    $clone = clone $asset;
+    $this->assertNotSame($mockmeta, $clone->getMetadata());
+  }
+}
diff --git a/core/tests/Drupal/Tests/Core/Asset/Collection/AssetCollectionTest.php b/core/tests/Drupal/Tests/Core/Asset/Collection/AssetCollectionTest.php
new file mode 100644
index 0000000..22edcc2
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/Collection/AssetCollectionTest.php
@@ -0,0 +1,203 @@
+<?php
+/**
+ * @file
+ * Contains Drupal\Tests\Core\Asset\AssetCollectionTest.
+ */
+
+
+namespace Drupal\Tests\Core\Asset\Collection;
+
+use Drupal\Core\Asset\Collection\AssetCollection;
+use Drupal\Tests\Core\Asset\AssetUnitTest;
+
+/**
+ * @group Asset
+ */
+class AssetCollectionTest extends AssetUnitTest {
+
+  /**
+   * @var AssetCollection
+   */
+  protected $collection;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Asset collection tests',
+      'description' => 'Unit tests on AssetCollection',
+      'group' => 'Asset',
+    );
+  }
+
+  public function setUp() {
+    $this->collection = new AssetCollection();
+  }
+
+  public function testAdd() {
+    $css = $this->createMockFileAsset('css');
+    $js = $this->createMockFileAsset('js');
+
+    $this->collection->add($css);
+    $this->collection->add($js);
+
+    $this->assertContains($css, $this->collection);
+    $this->assertContains($js, $this->collection);
+  }
+
+  public function testGetCss() {
+    $css = $this->createMockFileAsset('css');
+    $js = $this->createMockFileAsset('js');
+
+    $this->collection->add($css);
+    $this->collection->add($js);
+
+    $css_result = array();
+    foreach ($this->collection->getCss() as $asset) {
+      $css_result[] = $asset;
+    }
+
+    $this->assertEquals(array($css), $css_result);
+  }
+
+  public function testGetJs() {
+    $css = $this->createMockFileAsset('css');
+    $js = $this->createMockFileAsset('js');
+
+    $this->collection->add($css);
+    $this->collection->add($js);
+
+    $js_result = array();
+    foreach ($this->collection->getJs() as $asset) {
+      $js_result[] = $asset;
+    }
+
+    $this->assertEquals(array($js), $js_result);
+  }
+
+  public function testAll() {
+    $css = $this->createMockFileAsset('css');
+    $js = $this->createMockFileAsset('js');
+
+    $this->collection->add($css);
+    $this->collection->add($js);
+
+    $this->assertEquals(array($css->id() => $css, $js->id() => $js), $this->collection->all());
+  }
+
+  public function testRemoveByAsset() {
+    $stub = $this->createMockFileAsset('css');
+
+    $this->collection->add($stub);
+    $this->collection->remove($stub);
+
+    $this->assertNotContains($stub, $this->collection);
+  }
+
+  public function testRemoveById() {
+    $stub = $this->createMockFileAsset('css');
+
+    $this->collection->add($stub);
+    $this->collection->remove($stub->id());
+
+    $this->assertNotContains($stub, $this->collection);
+  }
+
+  /**
+   * @expectedException OutOfBoundsException
+   */
+  public function testRemoveNonexistentId() {
+    $this->assertFalse($this->collection->remove('foo'));
+    $this->collection->remove('foo', FALSE);
+  }
+
+  /**
+   * @expectedException OutOfBoundsException
+   */
+  public function testRemoveNonexistentAsset() {
+    $stub = $this->createMockFileAsset('css');
+    $this->assertFalse($this->collection->remove($stub));
+    $this->collection->remove($stub, FALSE);
+  }
+
+  public function testRemoveInvalidType() {
+    $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass);
+    try {
+      foreach ($invalid as $val) {
+        $this->collection->remove($val);
+        $this->fail('AssetCollection::remove() did not throw exception on invalid argument type.');
+      }
+    } catch (\InvalidArgumentException $e) {}
+  }
+
+  public function testMergeCollection() {
+    $coll2 = new AssetCollection();
+    $stub1 = $this->createMockFileAsset('css');
+    $stub2 = $this->createMockFileAsset('js');
+
+    $coll2->add($stub1);
+    $this->collection->mergeCollection($coll2);
+
+    $this->assertContains($stub1, $this->collection);
+    $this->assertTrue($coll2->isFrozen());
+
+    $coll3 = new AssetCollection();
+    $coll3->add($stub1);
+    $coll3->add($stub2);
+    // Ensure no duplicates, and don't freeze merged bag
+    $this->collection->mergeCollection($coll3, FALSE);
+
+    $contained = array(
+      $stub1->id() => $stub1,
+      $stub2->id() => $stub2,
+    );
+    $this->assertEquals($contained, $this->collection->all());
+    $this->assertFalse($coll3->isFrozen());
+  }
+
+  /**
+   * Tests that all methods should be disabled by freezing the collection
+   * correctly trigger an exception.
+   */
+  public function testExceptionOnWriteWhenFrozen() {
+    $stub = $this->createMockFileAsset('css');
+    $write_protected = array(
+      'add' => $stub,
+      'remove' => $stub,
+      'mergeCollection' => $this->getMock('\\Drupal\\Core\\Asset\\Collection\\AssetCollection'),
+    );
+
+    $this->collection->freeze();
+    foreach ($write_protected as $method => $arg) {
+      try {
+        $this->collection->$method($arg);
+        $this->fail('Was able to run writable method on frozen AssetCollection');
+      }
+      catch (\LogicException $e) {}
+    }
+  }
+
+  /**
+   * @expectedException OutOfBoundsException
+   */
+  public function testGetById() {
+    $metamock = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag');
+
+    $asset = $this->getMock('\\Drupal\\Core\\Asset\\FileAsset', array(), array($metamock, 'foo'));
+    $asset->expects($this->once())
+      ->method('id')
+      ->will($this->returnValue('foo'));
+
+    $this->collection->add($asset);
+    $this->assertSame($asset, $this->collection->getById('foo'));
+
+    // Nonexistent asset
+    $this->assertFalse($this->collection->getById('bar'));
+
+    // Nonexistent asset, non-graceful
+    $this->collection->getById('bar', FALSE);
+  }
+
+  public function testIsEmpty() {
+    $this->assertTrue($this->collection->isEmpty());
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Asset/Collection/AssetLibraryTest.php b/core/tests/Drupal/Tests/Core/Asset/Collection/AssetLibraryTest.php
new file mode 100644
index 0000000..5608951
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/Collection/AssetLibraryTest.php
@@ -0,0 +1,96 @@
+<?php
+/**
+ * @file
+ * Contains Drupal\Tests\Core\Asset\AssetLibraryTest.
+ */
+
+namespace Drupal\Tests\Core\Asset\Collection;
+
+use Drupal\Core\Asset\Collection\AssetLibrary;
+use Drupal\Tests\Core\Asset\AssetUnitTest;
+
+/**
+ *
+ * @group Asset
+ */
+class AssetLibraryTest extends AssetUnitTest {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Asset Library tests',
+      'description' => 'Tests that the AssetLibrary behaves correctly.',
+      'group' => 'Asset',
+    );
+  }
+
+  public function getLibraryFixture() {
+    $library = new AssetLibrary();
+    $library->setTitle('foo')
+      ->setVersion('1.2.3')
+      ->setWebsite('http://foo.bar')
+      ->addDependency('foo', 'bar');
+    return $library;
+  }
+
+  public function testConstructorValueInjection() {
+    $values = array(
+      'title' => 'foo',
+      'version' => '1.2.3',
+      'website' => 'http://foo.bar',
+      'dependencies' => array(array('foo', 'bar')),
+    );
+    $library = new AssetLibrary($values);
+
+    $fixture = $this->getLibraryFixture();
+    $this->assertEquals($fixture->getTitle(), $library->getTitle(), 'Title passed correctly through the constructor.');
+    $this->assertEquals($fixture->getVersion(), $library->getVersion(), 'Version passed correctly through the constructor.');
+    $this->assertEquals($fixture->getWebsite(), $library->getWebsite(), 'Website passed correctly through the constructor.');
+    $this->assertEquals($fixture->getDependencyInfo(), $library->getDependencyInfo(), 'Dependencies information passed correctly through the constructor.');
+  }
+
+  public function testAddDependency() {
+    $library = $this->getLibraryFixture();
+    $library->addDependency('baz', 'bing');
+    $this->assertEquals($library->getDependencyInfo(), array(array('foo', 'bar'), array('baz', 'bing')), 'Dependencies added to library successfully.');
+  }
+
+  public function testClearDependencies() {
+    $library = $this->getLibraryFixture();
+    $library->clearDependencies();
+    $this->assertEmpty($library->getDependencyInfo(), 'Dependencies recorded in the library were cleared correctly.');
+  }
+
+  public function testFrozenNonwriteability() {
+    $library = $this->getLibraryFixture();
+    $library->freeze();
+    try {
+      $library->setTitle('bar');
+      $this->fail('No exception thrown when attempting to set a new title on a frozen library.');
+    }
+    catch (\LogicException $e) {}
+
+    try {
+      $library->setVersion('2.3.4');
+      $this->fail('No exception thrown when attempting to set a new version on a frozen library.');
+    }
+    catch (\LogicException $e) {}
+
+    try {
+      $library->setWebsite('http://bar.baz');
+      $this->fail('No exception thrown when attempting to set a new website on a frozen library.');
+    }
+    catch (\LogicException $e) {}
+
+    try {
+      $library->addDependency('bing', 'bang');
+      $this->fail('No exception thrown when attempting to add a new dependency on a frozen library.');
+    }
+    catch (\LogicException $e) {}
+
+    try {
+      $library->clearDependencies();
+      $this->fail('No exception thrown when attempting to clear dependencies from a frozen library.');
+    }
+    catch (\LogicException $e) {}
+  }
+}
diff --git a/core/tests/Drupal/Tests/Core/Asset/Factory/AssetCollectorTest.php b/core/tests/Drupal/Tests/Core/Asset/Factory/AssetCollectorTest.php
new file mode 100644
index 0000000..a853afe
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/Factory/AssetCollectorTest.php
@@ -0,0 +1,289 @@
+<?php
+/**
+ * @file
+ * Contains Drupal\Tests\Core\Asset\AssetCollectorTest.
+ */
+
+namespace Drupal\Tests\Core\Asset\Factory;
+
+if (!defined('CSS_AGGREGATE_THEME')) {
+  define('CSS_AGGREGATE_THEME', 100);
+}
+
+if (!defined('CSS_AGGREGATE_DEFAULT')) {
+  define('CSS_AGGREGATE_DEFAULT', 0);
+}
+
+if (!defined('JS_DEFAULT')) {
+  define('JS_DEFAULT', 0);
+}
+
+use Drupal\Core\Asset\Collection\AssetCollection;
+use Drupal\Core\Asset\Factory\AssetCollector;
+use Drupal\Core\Asset\Metadata\CssMetadataBag;
+use Drupal\Core\Asset\Metadata\JsMetadataBag;
+use Drupal\Tests\Core\Asset\AssetUnitTest;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Unit tests for AssetCollector.
+ *
+ * @group Asset
+ */
+class AssetCollectorTest extends AssetUnitTest {
+
+  /**
+   * @var \Drupal\Core\Asset\Factory\AssetCollector
+   */
+  protected $collector;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Asset Collector tests',
+      'description' => 'Tests that the AssetCollector system works correctly.',
+      'group' => 'Asset',
+    );
+  }
+
+  public function setUp() {
+    parent::setUp();
+    $this->collector = new AssetCollector();
+  }
+
+  /**
+   * Tests that the collector injects provided metadata to created assets.
+   */
+  public function testMetadataInjection() {
+    $asset = $this->collector->create('css', 'file', 'foo', array('group' => CSS_AGGREGATE_THEME));
+    $meta = $asset->getMetadata();
+    $this->assertEquals(CSS_AGGREGATE_THEME, $meta->get('group'), 'Collector injected user-passed parameters into the created asset.');
+    $this->assertFalse($meta->isDefault('group'));
+  }
+
+  public function testDefaultPropagation() {
+    // Test that defaults are correctly applied by the factory.
+    $meta = new CssMetadataBag(array('every_page' => TRUE, 'group' => CSS_AGGREGATE_THEME));
+    $this->collector->setDefaultMetadata($meta);
+    $css1 = $this->collector->create('css', 'file', 'foo');
+
+    $asset_meta = $css1->getMetadata();
+    $this->assertTrue($asset_meta->get('every_page'));
+    $this->assertEquals(CSS_AGGREGATE_THEME, $asset_meta->get('group'));
+  }
+
+  /**
+   * @expectedException \RuntimeException
+   */
+  public function testExceptionOnAddingAssetWithoutCollectionPresent() {
+    $asset = $this->collector->create('css', 'string', 'foo');
+    $this->collector->add($asset);
+  }
+
+  /**
+   * TODO separate test for an explicit add() call.
+   */
+  public function testAssetsImplicitlyArriveInInjectedCollection() {
+    $collection = new AssetCollection();
+    $this->collector->setCollection($collection);
+
+    $asset = $this->collector->create('css', 'file', 'bar');
+    $this->assertContains($asset, $collection->getCss(), 'Created asset was implicitly added to collection.');
+  }
+
+  public function testAddAssetExplicitly() {
+    $collection = new AssetCollection();
+    $this->collector->setCollection($collection);
+
+    $mock = $this->createMockFileAsset('css');
+    $this->collector->add($mock);
+
+    $this->assertContains($mock, $collection);
+  }
+
+  public function testSetCollection() {
+    $collection = new AssetCollection();
+    $this->collector->setCollection($collection);
+    $this->assertTrue($this->collector->hasCollection());
+  }
+
+  public function testClearCollection() {
+    $collection = new AssetCollection();
+    $this->collector->setCollection($collection);
+    $this->collector->clearCollection();
+    $this->assertFalse($this->collector->hasCollection());
+  }
+
+  public function testLock() {
+    $this->assertTrue($this->collector->lock($this), 'Collector locked successfully.');
+    $this->assertTrue($this->collector->isLocked(), 'Collector accurately reports that it is locked via isLocked() method.');
+  }
+
+  public function testUnlock() {
+    $this->collector->lock($this);
+    $this->assertTrue($this->collector->unlock($this), 'Collector unlocked successfully when appropriate key was provided.');
+    $this->assertFalse($this->collector->isLocked(), 'Collector correctly reported unlocked state via isLocked() method after unlocking.');
+  }
+
+  /**
+   * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException
+   */
+  public function testUnlockFailsWithoutCorrectSecret() {
+    $this->collector->lock('foo');
+    $this->collector->unlock('bar');
+  }
+
+  /**
+   * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException
+   */
+  public function testUnlockFailsIfNotLocked() {
+    $this->collector->unlock('foo');
+  }
+
+  /**
+   * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException
+   */
+  public function testLockFailsIfLocked() {
+    $this->collector->lock('foo');
+    $this->collector->lock('error');
+  }
+
+  /**
+   * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException
+   */
+  public function testLockingPreventsSettingDefaults() {
+    $this->collector->lock($this);
+    $this->collector->setDefaultMetadata(new CssMetadataBag());
+  }
+
+  /**
+   * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException
+   */
+  public function testLockingPreventsRestoringDefaults() {
+    $this->collector->lock($this);
+    $this->collector->restoreDefaults();
+  }
+
+  /**
+   * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException
+   */
+  public function testLockingPreventsClearingCollection() {
+    $this->collector->lock($this);
+    $this->collector->clearCollection();
+  }
+
+  /**
+   * @expectedException \Drupal\Core\Asset\Exception\LockedObjectException
+   */
+  public function testLockingPreventsSettingCollection() {
+    $this->collector->lock($this);
+    $this->collector->setCollection(new AssetCollection());
+  }
+
+  public function testBuiltinDefaultAreTheSame() {
+    $this->assertEquals(new CssMetadataBag(), $this->collector->getMetadataDefaults('css'));
+    $this->assertEquals(new JsMetadataBag(), $this->collector->getMetadataDefaults('js'));
+  }
+
+  public function testChangeAndRestoreDefaults() {
+    $changed_css = new CssMetadataBag(array('foo' => 'bar', 'every_page' => TRUE));
+    $this->collector->setDefaultMetadata($changed_css);
+
+    $this->assertEquals($changed_css, $this->collector->getMetadataDefaults('css'));
+    $this->assertNotSame($changed_css, $this->collector->getMetadataDefaults('css'), 'Metadata is cloned on retrieval from collector.');
+
+    $this->collector->restoreDefaults();
+    $this->assertEquals(new CssMetadataBag(), $this->collector->getMetadataDefaults('css'));
+
+    // Do another check to ensure that both metadata bags are correctly reset
+    $changed_js = new JsMetadataBag(array('scope' => 'footer', 'fizzbuzz' => 'llama'));
+    $this->collector->setDefaultMetadata($changed_css);
+    $this->collector->setDefaultMetadata($changed_js);
+
+    $this->assertEquals($changed_css, $this->collector->getMetadataDefaults('css'));
+    $this->assertEquals($changed_js, $this->collector->getMetadataDefaults('js'));
+
+    $this->collector->restoreDefaults();
+    $this->assertEquals(new CssMetadataBag(), $this->collector->getMetadataDefaults('css'));
+    $this->assertEquals(new JsMetadataBag(), $this->collector->getMetadataDefaults('js'));
+  }
+
+  /**
+   * @expectedException \InvalidArgumentException
+   */
+  public function testMetadataTypeMustBeCorrect() {
+    $mock = $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag');
+    $mock->expects($this->once())
+      ->method('getType')
+      ->will($this->returnValue('foo'));
+
+    $this->collector->setDefaultMetadata($mock);
+  }
+
+  /**
+   * @expectedException \InvalidArgumentException
+   */
+  public function testGetNonexistentDefault() {
+    $this->collector->getMetadataDefaults('foo');
+  }
+
+
+  public function testCreateCssFileAsset() {
+    $css_file = $this->collector->create('css', 'file', 'foo');
+    $this->assertInstanceOf('\Drupal\Core\Asset\FileAsset', $css_file);
+    $this->assertEquals('css', $css_file->getAssetType());
+  }
+
+  public function testCreateStylesheetExternalAsset() {
+    $css_external = $this->collector->create('css', 'external', 'http://foo.bar/path/to/asset.css');
+    $this->assertInstanceOf('\Drupal\Core\Asset\ExternalAsset', $css_external);
+    $this->assertEquals('css', $css_external->getAssetType());
+  }
+
+  public function testCreateStylesheetStringAsset() {
+    $css_string = $this->collector->create('css', 'string', 'foo');
+    $this->assertInstanceOf('\Drupal\Core\Asset\StringAsset', $css_string);
+    $this->assertEquals('css', $css_string->getAssetType());
+  }
+
+  public function testCreateJavascriptFileAsset() {
+    $js_file = $this->collector->create('js', 'file', 'foo');
+    $this->assertInstanceOf('\Drupal\Core\Asset\FileAsset', $js_file);
+    $this->assertEquals('js', $js_file->getAssetType());
+  }
+
+  public function testCreateJavascriptExternalAsset() {
+    $js_external = $this->collector->create('js', 'external', 'http://foo.bar/path/to/asset.js');
+    $this->assertInstanceOf('\Drupal\Core\Asset\ExternalAsset', $js_external);
+    $this->assertEquals('js', $js_external->getAssetType());
+  }
+
+  public function testCreateJavascriptStringAsset() {
+    $js_string = $this->collector->create('js', 'string', 'foo');
+    $this->assertInstanceOf('\Drupal\Core\Asset\StringAsset', $js_string);
+    $this->assertEquals('js', $js_string->getAssetType());
+  }
+
+  public function testLastCssAutoAfter() {
+    $css1 = $this->collector->create('css', 'file', 'foo.css');
+    $css2 = $this->collector->create('css', 'file', 'foo2.css');
+    $this->assertEquals(array($css1), $css2->getPredecessors());
+
+    $this->collector->clearLastCss();
+    $css3 = $this->collector->create('css', 'file', 'foo3.css');
+    $this->assertEmpty($css3->getPredecessors());
+  }
+
+  /**
+   * @expectedException \InvalidArgumentException
+   */
+  public function testExceptionOnInvalidSourceType() {
+    $this->collector->create('foo', 'bar', 'baz');
+  }
+
+  /**
+   * @expectedException \InvalidArgumentException
+   */
+  public function testExceptionOnInvalidAssetType() {
+    $this->collector->create('css', 'bar', 'qux');
+  }
+}
diff --git a/core/tests/Drupal/Tests/Core/Asset/Metadata/AssetMetadataBagTest.php b/core/tests/Drupal/Tests/Core/Asset/Metadata/AssetMetadataBagTest.php
new file mode 100644
index 0000000..00876f8
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/Metadata/AssetMetadataBagTest.php
@@ -0,0 +1,95 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\AssetMetadataBagTest.
+ */
+
+namespace Drupal\Tests\Core\Asset\Metadata;
+
+use Drupal\Core\Asset\Metadata\AssetMetadataBag;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ *
+ * @group Asset
+ */
+class AssetMetadataBagTest extends UnitTestCase {
+
+  /**
+   * @var AssetMetadataBag
+   */
+  protected $mock;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Asset Metadata bag test',
+      'description' => 'Tests various methods of AssetMetadatabag',
+      'group' => 'Asset',
+    );
+  }
+
+  public function createBag($args = array(array('foo' => 'bar', 'baz' => 'qux'))) {
+    return $this->getMockForAbstractClass('\\Drupal\\Core\\Asset\\Metadata\\AssetMetadataBag', $args);
+  }
+
+  /**
+   * A unified test for all operations that rely on calling get() in order to
+   * verify their correctness.
+   */
+  public function testGetValueOperations() {
+    // First, ensure that constructor-injected defaults are correctly tracked.
+    $mock = $this->createBag();
+    $this->assertEquals('bar', $mock->get('foo'));
+    // Ensure that constructor-injected defaults are correctly reported as such.
+    $this->assertTrue($mock->isDefault('foo'));
+
+    // Set an explicit value, and ensure that it comes back out correctly.
+    $mock->set('bing', 'bang');
+    $this->assertEquals('bang', $mock->get('bing'));
+    $this->assertFalse($mock->isDefault('bing'));
+
+    // Set an explicit value that overrides a default, this time.
+    $mock->set('foo', 'kablow');
+    $this->assertEquals('kablow', $mock->get('foo'));
+    $this->assertFalse($mock->isDefault('foo'));
+
+    // Revert the set value, and ensure the old default comes through.
+    $mock->revert('foo');
+    $this->assertEquals('bar', $mock->get('foo'));
+    $this->assertTrue($mock->isDefault('foo'));
+
+    // Add value via add(), now
+    $mock->add(array('llama' => 'a pink one'));
+    $this->assertEquals('a pink one', $mock->get('llama'));
+    $this->assertFalse($mock->isDefault('llama'));
+
+    // Finally, check that getting an unknown key returns nothing
+    $this->assertNull($mock->get('nonexistent'));
+  }
+
+  public function testAll() {
+    $this->assertEquals(array('foo' => 'bar', 'baz' => 'qux'), $this->createBag()->all());
+  }
+
+  public function testKeys() {
+    $this->assertEquals(array('foo', 'baz'), $this->createBag()->keys());
+  }
+
+  public function testHas() {
+    $this->assertTrue($this->createBag()->has('foo'));
+  }
+
+  public function testIteration() {
+    $found = array();
+    foreach ($this->createBag() as $val) {
+      $found[] = $val;
+    }
+
+    $this->assertEquals(array('bar', 'qux'), $found);
+  }
+
+  public function testCount() {
+    $this->assertCount(2, $this->createBag());
+  }
+}
diff --git a/core/tests/Drupal/Tests/Core/Asset/Metadata/CssMetadataBagTest.php b/core/tests/Drupal/Tests/Core/Asset/Metadata/CssMetadataBagTest.php
new file mode 100644
index 0000000..08fbb83
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/Metadata/CssMetadataBagTest.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\Metadata\CssMetadataBagTest.
+ */
+
+namespace Drupal\Tests\Core\Asset\Metadata;
+
+use Drupal\Core\Asset\Metadata\CssMetadataBag;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ *
+ * @group Asset
+ */
+class CssMetadataBagTest extends UnitTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'CSS Metadata bag test',
+      'description' => 'Tests various methods of CssMetadatabag',
+      'group' => 'Asset',
+    );
+  }
+
+  public function testGetType() {
+    $bag = new CssMetadataBag();
+    $this->assertEquals('css', $bag->getType());
+  }
+}
+
diff --git a/core/tests/Drupal/Tests/Core/Asset/Metadata/JsMetadataBagTest.php b/core/tests/Drupal/Tests/Core/Asset/Metadata/JsMetadataBagTest.php
new file mode 100644
index 0000000..7cc5600
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/Metadata/JsMetadataBagTest.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\Metadata\JsMetadataBagTest.
+ */
+
+namespace Drupal\Tests\Core\Asset\Metadata;
+
+use Drupal\Core\Asset\Metadata\JsMetadataBag;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ *
+ * @group Asset
+ */
+class JsMetadataBagTest extends UnitTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'JS Metadata bag test',
+      'description' => 'Tests various methods of JsMetadatabag',
+      'group' => 'Asset',
+    );
+  }
+
+  public function testGetType() {
+    $bag = new JsMetadataBag();
+    $this->assertEquals('js', $bag->getType());
+  }
+}
+
