diff --git a/core/core.services.yml b/core/core.services.yml
index 882cf9f..ce68186 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -637,6 +637,14 @@ services:
   asset.css.collection_optimizer:
     class: Drupal\Core\Asset\CssCollectionOptimizer
     arguments: [ '@asset.css.collection_grouper', '@asset.css.optimizer', '@asset.css.dumper', '@state' ]
+  asset.css.collection_optimizer_nv:
+    class: Drupal\Core\Asset\Optimize\CssCollectionOptimizer
+    arguments: [ '@asset.css.collection_aggregator', '@asset.css.optimizer', '@asset.css.dumper', '@state' ]
+  asset.css.collection_aggregator:
+    class: Drupal\Core\Asset\Optimize\CssCollectionAggregator
+    arguments: [ '@asset.css.graph_sorter' ]
+  asset.css.graph_sorter:
+    class: Drupal\Core\Asset\GroupSort\CssGraphSorter
   asset.css.optimizer:
     class: Drupal\Core\Asset\CssOptimizer
   asset.css.collection_grouper:
@@ -655,5 +663,14 @@ services:
     class: Drupal\Core\Asset\JsCollectionGrouper
   asset.js.dumper:
     class: Drupal\Core\Asset\AssetDumper
+  asset.library_factory:
+    class: Drupal\Core\Asset\Factory\AssetLibraryFactory
+    arguments: ['@module_handler']
+  asset.library_repository:
+    class: Drupal\Core\Asset\AssetLibraryRepository
+    arguments: ['@asset.library_factory']
+  asset.css.collection_renderer_nv:
+    class: Drupal\Core\Asset\Render\CssCollectionRenderer
+    arguments: [ '@state', '@asset.css.graph_sorter' ]
   info_parser:
     class: Drupal\Core\Extension\InfoParser
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 5749ffe..670a2c3 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -9,6 +9,7 @@
 use Drupal\Component\Utility\Tags;
 use Drupal\Component\Utility\Url;
 use Drupal\Component\Utility\Xss;
+use Drupal\Core\Asset\Optimize\CssCollectionOptimizer;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Language\Language;
 use Symfony\Component\HttpFoundation\Response;
@@ -1591,7 +1592,7 @@ function drupal_add_html_head_link($attributes, $header = FALSE) {
  *
  * @see drupal_get_css()
  */
-function drupal_add_css($data = NULL, $options = NULL) {
+function drupal_add_css($data = NULL, $options = NULL, $collect = TRUE) {
   $css = &drupal_static(__FUNCTION__, array());
 
   // Construct the options, taking the defaults into consideration.
@@ -1607,6 +1608,11 @@ 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';
+    if ($collect) {
+      drupal_collect_assets($data, $options, 'css');
+    }
+
     $options += array(
       'type' => 'file',
       'group' => CSS_AGGREGATE_DEFAULT,
@@ -1658,6 +1664,35 @@ function drupal_add_css($data = NULL, $options = NULL) {
   return $css;
 }
 
+function drupal_collect_assets($data, $options, $type = '') {
+  $collection = &drupal_static('global_asset_collection', 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($collection);
+  }
+
+  if ($data instanceof \Drupal\Core\Asset\AssetInterface) {
+    $collector->add($data);
+    return;
+  }
+
+  if ($type == 'js-setting') {
+    // TODO handle js settings
+    return;
+  }
+
+  if ($type == 'library') {
+    $collection->addUnresolvedLibrary($data);
+    return;
+  }
+
+  // TODO: simplify
+  // $type is 'css' or 'js', $options['type'] is 'file', 'external' or 'inline'.
+  $collector->create($type, $options['type'], $data, $options);
+}
+
 /**
  * Returns a themed representation of all stylesheets to attach to the page.
  *
@@ -1838,11 +1873,23 @@ function drupal_sort_css_js($a, $b) {
 function drupal_pre_render_styles($elements) {
   $css_assets = $elements['#items'];
 
+  $collection = &drupal_static('global_asset_collection', FALSE);
+
+  /** @var $collection \Drupal\Core\Asset\Collection\AssetCollectionInterface */
+  $collection->resolveLibraries(\Drupal::service('asset.library_repository'));
+  $collection->ksort();
+
+  $sorter = \Drupal::service('asset.css.graph_sorter');
+  $sorted_css = $sorter->groupAndSort($collection);
+  $rendered = \Drupal::service('asset.css.collection_renderer_nv')->render($sorted_css);
+
   // Aggregate the CSS if necessary, but only during normal site operation.
   if (!defined('MAINTENANCE_MODE') && \Drupal::config('system.performance')->get('css.preprocess')) {
     $css_assets = \Drupal::service('asset.css.collection_optimizer')->optimize($css_assets);
+    return \Drupal::service('asset.css.collection_renderer')->render($css_assets);
   }
-  return \Drupal::service('asset.css.collection_renderer')->render($css_assets);
+
+  return $rendered;
 }
 
 /**
@@ -2174,7 +2221,7 @@ function drupal_html_id($id) {
  *
  * @see drupal_get_js()
  */
-function drupal_add_js($data = NULL, $options = NULL) {
+function drupal_add_js($data = NULL, $options = NULL, $collect = TRUE) {
   $javascript = &drupal_static(__FUNCTION__, array());
 
   // Construct the options, taking the defaults into consideration.
@@ -2188,6 +2235,13 @@ 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';
+    if ($collect) {
+      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;
 
@@ -2535,7 +2589,17 @@ function drupal_pre_render_scripts($elements) {
  * @see drupal_add_css()
  * @see drupal_render()
  */
-function drupal_process_attached($elements, $dependency_check = FALSE) {
+function drupal_process_attached($elements, $dependency_check = FALSE, $collect = TRUE) {
+  // TODO duplicates the start of drupal_collect_assets()...just for now.
+  $collection = &drupal_static('global_asset_collection', 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);
+  }
+
   // Add defaults to the special attached structures that should be processed differently.
   $elements['#attached'] += array(
     'library' => array(),
@@ -2556,6 +2620,9 @@ function drupal_process_attached($elements, $dependency_check = FALSE) {
   }
   unset($elements['#attached']['library']);
 
+  // Clear the last CSS before we start to avoid unintentional relative positioning
+  $collector->clearLastCss();
+
   // Add both the JavaScript and the CSS.
   // The parameters for drupal_add_js() and drupal_add_css() require special
   // handling.
@@ -2573,10 +2640,12 @@ function drupal_process_attached($elements, $dependency_check = FALSE) {
         $data = $options['data'];
         unset($options['data']);
       }
-      call_user_func('drupal_add_' . $type, $data, $options);
+      call_user_func('drupal_add_' . $type, $data, $options, $collect);
     }
     unset($elements['#attached'][$type]);
   }
+  // Clear the last CSS asset, ending the auto-predecessor-creation chain.
+  $collector->clearLastCss();
 
   // Add additional types of attachments specified in the render() structure.
   // Libraries, JavaScript and CSS have been added already, as they require
@@ -2745,6 +2814,7 @@ function drupal_process_states(&$elements) {
  */
 function drupal_add_library($module, $name, $every_page = NULL) {
   $added = &drupal_static(__FUNCTION__, array());
+  drupal_collect_assets("$module/$name", NULL, 'library');
 
   // Only process the library if it exists and it was not added already.
   if (!isset($added[$module][$name])) {
@@ -2768,7 +2838,7 @@ function drupal_add_library($module, $name, $every_page = NULL) {
         }
       }
 
-      $added[$module][$name] = drupal_process_attached($elements, TRUE);
+      $added[$module][$name] = drupal_process_attached($elements, TRUE, FALSE);
     }
     else {
       // Requested library does not exist.
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 9159f71..d551ad3 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -470,10 +470,16 @@ function install_begin_request(&$install_state) {
     CoreServiceProvider::registerUuid($container);
 
     // Register the CSS and JavaScript asset collection renderers.
-    $container->register('asset.css.collection_renderer', 'Drupal\Core\Asset\CssCollectionRenderer')
-      ->addArgument(new Reference('state'));
     $container->register('asset.js.collection_renderer', 'Drupal\Core\Asset\JsCollectionRenderer')
       ->addArgument(new Reference('state'));
+    $container->register('asset.library_factory', 'Drupal\Core\Asset\Factory\AssetLibraryFactory')
+      ->addArgument(new Reference('module_handler'));
+    $container->register('asset.library_repository', 'Drupal\Core\Asset\AssetLibraryRepository')
+      ->addArgument(new Reference('asset.library_factory'));
+    $container->register('asset.css.graph_sorter', 'Drupal\Core\Asset\GroupSort\CssGraphSorter');
+    $container->register('asset.css.collection_renderer_nv', 'Drupal\Core\Asset\Optimize\CssCollectionRenderer')
+      ->addArgument(new Reference('state'))
+      ->addArgument(new Reference('asset.css.graph_sorter'));
 
     // Register the info parser.
     $container->register('info_parser', 'Drupal\Core\Extension\InfoParser');
diff --git a/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregate.php b/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregate.php
new file mode 100644
index 0000000..b0fa33d
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregate.php
@@ -0,0 +1,294 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\AssetAggregate.
+ */
+
+namespace Drupal\Core\Asset\Aggregate;
+
+use Assetic\Filter\FilterCollection;
+use Assetic\Filter\FilterInterface;
+use Drupal\Core\Asset\AssetInterface;
+use Assetic\Asset\AssetInterface as AsseticAssetInterface;
+use Drupal\Core\Asset\Aggregate\AssetAggregateInterface;
+use Drupal\Core\Asset\Collection\BasicAssetCollection;
+use Drupal\Core\Asset\Exception\AssetTypeMismatchException;
+use Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException;
+use Drupal\Core\Asset\Metadata\AssetMetadataInterface;
+
+/**
+ * Base class for representing aggregate assets.
+ */
+class AssetAggregate extends BasicAssetCollection implements \IteratorAggregate, AssetInterface, AssetAggregateInterface {
+
+  /**
+   * @var \Drupal\Core\Asset\Metadata\AssetMetadataInterface
+   */
+  protected $metadata;
+
+  /**
+   * A string identifier for this aggregate.
+   *
+   * For how this is calculated, see:
+   * @see AssetAggregate::calculateId()
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The body of the aggregate asset. This is lazy-loaded.
+   *
+   * @var string
+   */
+  protected $content;
+
+  /**
+   * A collection of filters to be applied to this asset.
+   *
+   * @var FilterCollection
+   */
+  protected $filters;
+
+  /**
+   * The relative path to the asset.
+   *
+   * @var string
+   */
+  protected $sourcePath;
+
+  /**
+   * The desired path at which the asset should be dumped.
+   *
+   * @var string
+   */
+  protected $targetPath;
+
+  /**
+   * Internal state flag indicating whether or not load filters have been run.
+   *
+   * @var bool
+   */
+  protected $loaded = FALSE;
+
+  /**
+   * @param AssetMetadataInterface $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(AssetMetadataInterface $metadata, $assets = array(), $filters = array()) {
+    $this->metadata = $metadata;
+    parent::__construct($assets);
+    $this->filters = new FilterCollection($filters);
+  }
+
+  /**
+   * {@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->eachLeaf() as $asset) {
+      $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 removeLeaf(AsseticAssetInterface $needle, $graceful = FALSE) {
+    if (!$needle instanceof AssetInterface) {
+      throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset provided; Drupal aggregates require Drupal-flavored assets.');
+    }
+
+    return $this->doRemove($needle, $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($replacement);
+    if ($this->contains($replacement)) {
+      throw new \LogicException('Asset to be swapped in is already present in the collection.');
+    }
+
+    return $this->doReplace($needle, $replacement, $graceful);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function ensureCorrectType(AssetInterface $asset) {
+    if ($asset->getAssetType() != $this->getAssetType()) {
+      throw new AssetTypeMismatchException(sprintf('Aggregate/asset incompatibility, aggregate of type "%s", asset of type "%s". Aggregates and their contained assets must be of the same type.', $this->getAssetType(), $asset->getAssetType()));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Aggregate assets are inherently eligible for preprocessing, so this is
+   * always true.
+   */
+  public function isPreprocessable() {
+    return TRUE;
+  }
+
+  /**
+   * {@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;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLastModified() {
+    // TODO: Implement getLastModified() method.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSourceRoot() {
+    // Drupal doesn't use this in general, and especially not for aggregates.
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSourcePath() {
+    return $this->sourcePath;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTargetPath() {
+    return $this->targetPath;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setTargetPath($targetPath) {
+    $this->targetPath = $targetPath;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function ensureFilter(FilterInterface $filter) {
+    $this->filters->ensure($filter);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFilters() {
+    $this->filters->all();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearFilters() {
+    $this->filters->clear();
+  }
+
+  /**
+   * @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.");
+  }
+
+}
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..1a71540
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregateInterface.php
@@ -0,0 +1,38 @@
+<?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 Assetic\Asset\AssetInterface as AsseticAssetInterface;
+use Drupal\Core\Asset\AssetInterface;
+use Drupal\Core\Asset\Exception\AssetTypeMismatchException;
+use Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException;
+
+/**
+ * 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 {
+
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/AssetInterface.php b/core/lib/Drupal/Core/Asset/AssetInterface.php
new file mode 100644
index 0000000..e35eba1
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/AssetInterface.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\AssetInterface.
+ */
+
+namespace Drupal\Core\Asset;
+
+use Assetic\Asset\AssetInterface as AsseticAssetInterface;
+use Drupal\Core\Asset\Metadata\AssetMetadataInterface;
+
+/**
+ * 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 \Drupal\Core\Asset\Metadata\AssetMetadataInterface
+   */
+  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.
+   *
+   * @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/AssetLibraryRepository.php b/core/lib/Drupal/Core/Asset/AssetLibraryRepository.php
new file mode 100644
index 0000000..d2974bd
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/AssetLibraryRepository.php
@@ -0,0 +1,144 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\AssetLibraryRepository.
+ */
+
+namespace Drupal\Core\Asset;
+
+use Drupal\Core\Asset\Collection\AssetLibrary;
+use Drupal\Core\Asset\Factory\AssetLibraryFactory;
+
+/**
+ * TODO the flow here is completely wrong. the state contained here needs proper management, beyond a single request.
+ */
+class AssetLibraryRepository {
+
+  /**
+   * An array of loaded \Drupal\Core\Asset\Collection\AssetLibrary objects.
+   *
+   * @var \Drupal\Core\Asset\Collection\AssetLibrary[]
+   */
+  protected $libraries;
+
+  /**
+   * The library collector responsible for lazy-loading libraries.
+   *
+   * @var
+   */
+  protected $factory;
+
+  function __construct(AssetLibraryFactory $factory) {
+    $this->factory = $factory;
+  }
+
+  /**
+   * Gets a library by its composite key.
+   *
+   * @param string $key
+   *   The key of the library, as a string of the form "$module/$name".
+   *
+   * @return \Drupal\Core\Asset\Collection\AssetLibrary
+   *   The requested library.
+   *
+   * @throws \OutOfBoundsException
+   *   Thrown if no library can be found with the given key.
+   */
+  public function get($key) {
+    if ($this->has($key)) {
+      return $this->libraries[$key];
+    }
+
+    if ($library = $this->factory->getLibrary($key)) {
+      $this->set($key, $library);
+    }
+    else {
+      throw new \OutOfBoundsException(sprintf('No library could be found with the key "%s".', $key));
+    }
+
+    return $this->libraries[$key];
+  }
+
+  public function set($key, AssetLibrary $library) {
+    if (!preg_match('/^[0-9A-Za-z_]*\/[0-9A-Za-z._-]*$/', $key)) {
+      throw new \InvalidArgumentException(sprintf('The name "%s" is invalid.', $key));
+    }
+
+    $this->libraries[$key] = $library;
+  }
+
+  /**
+   * Checks if the current library repository contains a certain library.
+   *
+   * Note that this does not verify whether or not such a library could be
+   * created from declarations elsewhere in the system - only if it HAS been
+   * created already.
+   *
+   * @param string $key
+   *   The key of the library, as a string of the form "$module/$name".
+   *
+   * @return bool
+   *   TRUE if the library has been built, FALSE otherwise.
+   */
+  public function has($key) {
+    return isset($this->libraries[$key]);
+  }
+
+  /**
+   * Resolves declared dependencies into an array of library objects.
+   *
+   * @param \Drupal\Core\Asset\DependencyInterface $asset
+   *   The asset whose dependencies should be resolved.
+   *
+   * @param bool $attach
+   *   Whether to automatically attach resolved dependencies to the given asset.
+   *
+   * @return \Drupal\Core\Asset\Collection\AssetLibrary[]
+   *   An array of AssetLibraryInterface objects if any dependencies were found;
+   *   otherwise, an empty array.
+   */
+  public function resolveDependencies(DependencyInterface $asset, $attach = TRUE) {
+    $dependencies = array();
+
+    if ($asset->hasDependencies()) {
+      foreach ($asset->getDependencyInfo() as $key) {
+        $dependencies[] = $library = $this->get($key);
+
+        // Only auto-attach if the argument is capable of it.
+        if ($attach && $asset instanceof RelativePositionInterface) {
+          foreach ($library as $libasset) {
+            // If operating on a proper AssetInterface object, only attach if
+            // the dependency and the given asset are of the same type.
+            if ($asset instanceof AssetInterface &&
+                $asset->getAssetType() !== $libasset->getAssetType()) {
+              continue;
+            }
+
+            $asset->after($libasset);
+          }
+        }
+      }
+    }
+
+    return $dependencies;
+  }
+
+  /**
+   * Returns an array of library names.
+   *
+   * @return array
+   *   An array of library names.
+   */
+  public function getNames() {
+    return array_keys($this->libraries);
+  }
+
+  /**
+   * Clears all libraries.
+   */
+  public function clear() {
+    $this->libraries = array();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Asset/AsseticAdapterAsset.php b/core/lib/Drupal/Core/Asset/AsseticAdapterAsset.php
new file mode 100644
index 0000000..a1b28fc
--- /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 as AsseticAssetInterface;
+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 AsseticAssetInterface {
+
+  /**
+   * @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..6bcf64b
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/BaseAsset.php
@@ -0,0 +1,186 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\BaseAsset.
+ */
+
+namespace Drupal\Core\Asset;
+
+use Drupal\Core\Asset\AssetInterface;
+use Drupal\Core\Asset\Metadata\AssetMetadataInterface;
+
+/**
+ * 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, DependencyInterface, RelativePositionInterface {
+
+  /**
+   * @var AssetMetadataInterface
+   */
+  protected $metadata;
+
+  /**
+   * The asset library's dependencies (on other asset libraries).
+   *
+   * @var array
+   */
+  protected $dependencies = array();
+
+  /**
+   * The asset library's predecing assets (not asset libraries!).
+   *
+   * @var array
+   */
+  protected $predecessors = array();
+
+  /**
+   * The asset library's succeeding assets (not asset libraries!).
+   *
+   * @var array
+   */
+  protected $successors = array();
+
+  public function __construct(AssetMetadataInterface $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 addDependency($key) {
+    if (!is_string($key) || substr_count($key, '/') !== 1) {
+      throw new \InvalidArgumentException('Dependencies must be expressed as a string key identifying the depended-upon library.');
+    }
+
+    // The library key is stored as the key for cheap deduping.
+    $this->dependencies[$key] = TRUE;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasDependencies() {
+    return !empty($this->dependencies);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDependencyInfo() {
+    return array_keys($this->dependencies);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearDependencies() {
+    $this->dependencies = array();
+    return $this;
+  }
+
+  /**
+   * {@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;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasPredecessors() {
+    return !empty($this->predecessors);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPredecessors() {
+    return $this->predecessors;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearPredecessors() {
+    $this->predecessors = array();
+    return $this;
+  }
+
+  /**
+   * {@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;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasSuccessors() {
+    return !empty($this->successors);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSuccessors() {
+    return $this->successors;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearSuccessors() {
+    $this->successors = array();
+    return $this;
+  }
+
+}
+
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..da581df
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Collection/AssetCollection.php
@@ -0,0 +1,249 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Collection\AssetCollection.
+ */
+
+namespace Drupal\Core\Asset\Collection;
+
+use Assetic\Asset\AssetInterface as AsseticAssetInterface;
+use Drupal\Core\Asset\Collection\AssetCollectionInterface;
+use Drupal\Core\Asset\AssetInterface;
+use Drupal\Core\Asset\AssetLibraryRepository;
+use Drupal\Core\Asset\Collection\Iterator\AssetSubtypeFilterIterator;
+use Drupal\Core\Asset\DependencyInterface;
+use Drupal\Core\Asset\Exception\FrozenObjectException;
+use Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException;
+
+/**
+ * A container for assets.
+ *
+ * TODO js settings...
+ *
+ * TODO With PHP5.4, refactor out AssetCollectionBasicInterface into a trait.
+ */
+class AssetCollection extends BasicAssetCollection implements AssetCollectionInterface {
+
+  /**
+   * State flag indicating whether or not this collection is frozen.
+   *
+   * @var bool
+   */
+  protected $frozen = FALSE;
+
+  /**
+   * The list of unresolved library keys attached directly to this collection.
+   *
+   * Note that libraries declared in this way have no defined positioning
+   * relationship with respect to any of the collection's normal assets.
+   *
+   * @var array
+   */
+  protected $libraries = array();
+
+  /**
+   * {@inheritdoc}
+   */
+  public function add(AsseticAssetInterface $asset) {
+    $this->attemptWrite(__METHOD__);
+    return parent::add($asset);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function mergeCollection(AssetCollectionInterface $collection, $freeze = TRUE) {
+    $this->attemptWrite(__METHOD__);
+
+    foreach ($collection as $asset) {
+      if (!$this->contains($asset)) {
+        $this->add($asset);
+      }
+    }
+
+    foreach ($collection->getUnresolvedLibraries() as $library) {
+      // TODO just cheat and merge these in?
+      $this->addUnresolvedLibrary($library);
+    }
+
+    if ($freeze) {
+      $collection->freeze();
+    }
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function remove($needle, $graceful = FALSE) {
+    $this->attemptWrite(__METHOD__);
+    return parent::remove($needle, $graceful);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function replace($needle, AssetInterface $replacement, $graceful = FALSE) {
+    $this->attemptWrite(__METHOD__);
+    return parent::replace($needle, $replacement, $graceful);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function freeze() {
+    $this->frozen = TRUE;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isFrozen() {
+    return $this->frozen;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCss() {
+    $collection = new self();
+    foreach (new AssetSubtypeFilterIterator(new \ArrayIterator($this->all()), 'css') as $asset) {
+      $collection->add($asset);
+    }
+
+    return $collection;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getJs() {
+    $collection = new self();
+    foreach (new AssetSubtypeFilterIterator(new \ArrayIterator($this->all()), 'js') as $asset) {
+      $collection->add($asset);
+    }
+
+    return $collection;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function uksort($callback) {
+    $this->attemptWrite(__METHOD__);
+    uksort($this->assetIdMap, $callback);
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function ksort() {
+    $this->attemptWrite(__METHOD__);
+    ksort($this->assetIdMap);
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * TODO deal with nested assets - should they also be reversed?
+   */
+  public function reverse() {
+    $this->attemptWrite(__METHOD__);
+    $this->assetIdMap = array_reverse($this->assetIdMap);
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addUnresolvedLibrary($key) {
+    $this->attemptWrite(__METHOD__);
+    // The library key is stored as the key for cheap deduping.
+    $this->libraries[$key] = TRUE;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasUnresolvedLibraries() {
+    return !empty($this->libraries);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getUnresolvedLibraries() {
+    return array_keys($this->libraries);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearUnresolvedLibraries() {
+    $this->attemptWrite(__METHOD__);
+    $this->libraries = array();
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function resolveLibraries(AssetLibraryRepository $repository) {
+    $this->attemptWrite(__METHOD__);
+
+    // Resolving directly added libraries first ensures their contained assets
+    // are processed in the next loop.
+    foreach ($this->getUnresolvedLibraries() as $key) {
+      $library = $repository->get($key);
+      foreach ($library as $asset) {
+        $this->add($asset);
+      }
+    }
+
+    $this->clearUnresolvedLibraries();
+
+    // By iterating the assetStorage SPLOS, we guarantee that this loop won't
+    // finish until every added asset has been processed - including ones
+    // attached to the SPLOS during the loop. The alternative is a recursive
+    // closure - far more complex, and slower.
+    foreach ($this->assetStorage as $asset) {
+      if ($asset instanceof DependencyInterface) {
+        foreach ($repository->resolveDependencies($asset) as $library) {
+          foreach ($library as $libasset) {
+            // The repository already attached positioning info for us; just add.
+            $this->add($libasset);
+          }
+        }
+      }
+    }
+
+    return $this;
+  }
+
+  /**
+   * Checks if the asset collection is frozen, throws an exception if it is.
+   *
+   * @param string $method
+   *   The name of the method that was originally called.
+   *
+   * @throws FrozenObjectException
+   */
+  protected function attemptWrite($method) {
+    if ($this->isFrozen()) {
+      throw new FrozenObjectException(sprintf('AssetCollectionInterface::%s was called; writes cannot be performed on a frozen collection.', $method));
+    }
+  }
+
+}
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..3db227c
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Collection/AssetCollectionBasicInterface.php
@@ -0,0 +1,172 @@
+<?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\AssetTypeMismatchException;
+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 are shared with
+ * AssetAggregateInterface. Because certain internal ordering 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, \Countable {
+
+  /**
+   * Adds an asset to this aggregate.
+   *
+   * @param AsseticAssetInterface $asset
+   *   The asset to add. Note that, despite the type requirements, it must
+   *   conform to Drupal's AssetInterface.
+   *
+   * @return AssetCollectionInterface
+   *   The current asset collection.
+   *
+   * @throws UnsupportedAsseticBehaviorException
+   *   Thrown if a vanilla Assetic asset is provided.
+   *
+   * @throws AssetTypeMismatchException
+   *   Thrown if the provided asset is not the correct type for the aggregate
+   *   (e.g., CSS file in a JS aggregate).
+   */
+  public function add(AsseticAssetInterface $asset);
+
+  /**
+   * Indicates whether this collection contains the given asset.
+   *
+   * @param AssetInterface $asset
+   *   The asset to check for membership in the collection.
+   *
+   * @return bool
+   *   TRUE if the asset is present in the collection, FALSE otherwise.
+   */
+  public function contains(AssetInterface $asset);
+
+  /**
+   * Searches for and 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 search for.
+   * @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
+   *   Thrown if no asset could be found by the given id and $graceful = FALSE.
+   */
+  public function find($id, $graceful = TRUE);
+
+  /**
+   * Removes an asset from the collection.
+   *
+   * @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
+   *   TRUE on success, FALSE on failure to locate the given asset (or an
+   *   exception, depending on the value of $graceful).
+   *
+   * @throws \OutOfBoundsException
+   *   Thrown if $needle could not be located and $graceful = FALSE.
+   */
+  public function remove($needle, $graceful = FALSE);
+
+  /**
+   * Replaces an existing asset in the aggregate with a new one.
+   *
+   * This preserves ordering of the assets within the collection: 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
+   *   TRUE on success, FALSE on failure to locate the given asset (or an
+   *   exception, depending on the value of $graceful).
+   *
+   * @throws \OutOfBoundsException
+   *   Thrown if $needle could not be located and $graceful = FALSE.
+   */
+  public function replace($needle, AssetInterface $replacement, $graceful = FALSE);
+
+  /**
+   * Indicates whether the collection contains any assets.
+   *
+   * Note that this will only return TRUE if leaf assets are present - that is,
+   * assets that do NOT implement AssetCollectionBasicInterface.
+   *
+   * @return bool
+   *   TRUE if the collection is devoid of any leaf assets, FALSE otherwise.
+   */
+  public function isEmpty();
+
+  /**
+   * Returns all top-level child assets as an array.
+   *
+   * To retrieve assets regardless of nesting level, see the iterators:
+   *
+   * @see AssetCollectionBasicInterface::each()
+   * @see AssetCollectionBasicInterface::eachLeaf()
+   *
+   * @return AssetInterface[]
+   */
+  public function all();
+
+  /**
+   * Returns the total number of leaf assets in this collection.
+   *
+   * Non-leaf assets - objects implementing AssetCollectionBasicInterface - are
+   * not included in the count.
+   *
+   * @return int
+   */
+  public function count();
+
+  /**
+   * Retrieves a traversable that will return all contained assets.
+   *
+   * 'All' assets includes both AssetCollectionBasicInterface objects and plain
+   * AssetInterface objects.
+   *
+   * @return \Traversable
+   */
+  public function each();
+
+  /**
+   * Retrieves a traversable that returns only contained leaf assets.
+   *
+   * Leaf assets are objects that only implement AssetInterface, not
+   * AssetCollectionBasicInterface.
+   *
+   * @return \Traversable
+   */
+  public function eachLeaf();
+
+}
+
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..9bd3027
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Collection/AssetCollectionInterface.php
@@ -0,0 +1,156 @@
+<?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.
+ *
+ * @see \Drupal\Core\Asset\Collection\AssetCollectionBasicInterface
+ */
+interface AssetCollectionInterface extends AssetCollectionBasicInterface {
+
+  /**
+   * 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
+   * supersede the asset in this collection.
+   *
+   * @param \Drupal\Core\Asset\Collection\AssetCollectionInterface $collection
+   *   The collection to merge.
+   *
+   * @param bool $freeze
+   *   Whether to freeze the provided collection after merging. Defaults to TRUE.
+   *
+   * @return \Drupal\Core\Asset\Collection\AssetCollectionInterface
+   *   The current asset collection.
+   */
+  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 \Drupal\Core\Asset\Collection\AssetCollectionInterface
+   *   The current asset collection.
+   */
+  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 JavaScript assets in a traversable form.
+   *
+   * @return \Traversable
+   */
+  public function getJs();
+
+  /**
+   * Sorts contained assets by id by passing the provided callback to uksort().
+   *
+   * @param $callback
+   *
+   * @return \Drupal\Core\Asset\Collection\AssetCollectionInterface
+   *   The current asset collection.
+   */
+  public function uksort($callback);
+
+  /**
+   * Sorts contained assets via ksort() on their ids.
+   *
+   * @return \Drupal\Core\Asset\Collection\AssetCollectionInterface
+   *   The current asset collection.
+   */
+  public function ksort();
+
+  /**
+   * Reverses the sort order of the contained assets.
+   *
+   * @return \Drupal\Core\Asset\Collection\AssetCollectionInterface
+   *   The current asset collection.
+   */
+  public function reverse();
+
+  /**
+   * Adds a key identifying a library to this collection.
+   *
+   * Resolving this key into a real AssetLibrary is the responsibility of the
+   * resolveLibraries() method.
+   *
+   * @param string $key
+   *   The string identifying the library. It should be two-part composite key,
+   *   slash-delimited, with the first part being the module owner and the
+   *   second part being the library name.
+   *
+   * @return \Drupal\Core\Asset\Collection\AssetCollectionInterface
+   *   The current asset collection.
+   *
+   * @see \Drupal\Core\Asset\Collection\AssetCollectionInterface::resolveLibraries()
+   */
+  public function addUnresolvedLibrary($key);
+
+  /**
+   * Indicates whether the collection has any unresolved library keys.
+   *
+   * @return bool
+   *   TRUE if unresolved keys are present, FALSE otherwise.
+   */
+  public function hasUnresolvedLibraries();
+
+  /**
+   * Gets the unresolved library keys from this collection.
+   *
+   * @return array
+   *   An indexed array of library keys.
+   */
+  public function getUnresolvedLibraries();
+
+  /**
+   * Empties the collection of its unresolved library keys.
+   *
+   * @return \Drupal\Core\Asset\Collection\AssetCollectionInterface
+   *   The current asset collection.
+   */
+  public function clearUnresolvedLibraries();
+
+  /**
+   * Resolves all contained library references and adds them to this collection.
+   *
+   * "References" refers to library keys. This includes both libraries added
+   * directly to this collection, as well as those libraries included indirectly
+   * via a contained asset's declared dependencies.
+   *
+   * @param \Drupal\Core\Asset\AssetLibraryRepository $repository
+   *   The AssetLibraryRepository to use for resolving library keys into
+   *   AssetLibrary objects.
+   *
+   * @return \Drupal\Core\Asset\Collection\AssetCollectionInterface
+   *   The current asset collection.
+   */
+  public function resolveLibraries(AssetLibraryRepository $repository);
+
+}
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..ce08b07
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Collection/AssetLibrary.php
@@ -0,0 +1,186 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\AssetLibrary.
+ */
+
+namespace Drupal\Core\Asset\Collection;
+
+use Drupal\Core\Asset\AssetInterface;
+use Drupal\Core\Asset\DependencyInterface;
+use Drupal\Core\Asset\Collection\AssetCollection;
+use Drupal\Core\Asset\Exception\FrozenObjectException;
+use Drupal\Core\Asset\RelativePositionInterface;
+
+/**
+ * An asset library is a named collection of assets.
+ *
+ * The primary role of an asset library is to be declared as a dependency by
+ * other assets (including assets declared by other libraries).
+ */
+class AssetLibrary extends AssetCollection implements DependencyInterface {
+
+  /**
+   * 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();
+
+  /**
+   * The asset library's predecing assets (not asset libraries!).
+   *
+   * @var array
+   */
+  protected $predecessors = array();
+
+  /**
+   * The asset library's succeeding assets (not asset libraries!).
+   *
+   * @var array
+   */
+  protected $successors = array();
+
+  /**
+   * 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(__METHOD__);
+    $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(__METHOD__);
+    $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(__METHOD__);
+    $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 addDependency($key) {
+    $this->attemptWrite(__METHOD__);
+    if (!is_string($key) || substr_count($key, '/') !== 1) {
+      throw new \InvalidArgumentException('Dependencies must be expressed as a string key identifying the depended-upon library.');
+    }
+
+    // The library key is stored as the key for cheap deduping.
+    $this->dependencies[$key] = TRUE;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasDependencies() {
+    return !empty($this->dependencies);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDependencyInfo() {
+    return array_keys($this->dependencies);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearDependencies() {
+    $this->attemptWrite(__METHOD__);
+    $this->dependencies = array();
+    return $this;
+  }
+
+  /**
+   * Checks if the asset library is frozen, throws an exception if it is.
+   */
+  protected function attemptWrite($method) {
+    if ($this->isFrozen()) {
+      throw new FrozenObjectException('Metadata cannot be modified on a frozen AssetLibrary.');
+    }
+  }
+}
diff --git a/core/lib/Drupal/Core/Asset/Collection/BasicAssetCollection.php b/core/lib/Drupal/Core/Asset/Collection/BasicAssetCollection.php
new file mode 100644
index 0000000..85ce488
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Collection/BasicAssetCollection.php
@@ -0,0 +1,320 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Collection\BasicAssetCollection.
+ */
+
+namespace Drupal\Core\Asset\Collection;
+
+use Drupal\Core\Asset\Metadata\AssetMetadataInterface;
+use Drupal\Core\Asset\Collection\Iterator\RecursiveBasicCollectionIterator;
+use Drupal\Core\Asset\AssetInterface;
+use Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException;
+use Assetic\Asset\AssetInterface as AsseticAssetInterface;
+
+/**
+ * Base class implementing AssetCollectionBasicInterface.
+ *
+ * This class provides the essentials of the asset collection implementation,
+ * common to all of the collection flavors.
+ *
+ * TODO With PHP5.4, refactor this entire thing into a trait.
+ */
+abstract class BasicAssetCollection implements \IteratorAggregate, AssetCollectionBasicInterface {
+
+  /**
+   * A map of all assets, keyed by asset id.
+   *
+   * This map is also the canonical source for ordering information.
+   *
+   * @var array
+   */
+  protected $assetIdMap = array();
+
+  /**
+   * Container for all assets held within this object.
+   *
+   * @var \SplObjectStorage
+   */
+  protected $assetStorage;
+
+  /**
+   * Container for all nested asset collections held within in this object.
+   *
+   * @var \SplObjectStorage
+   */
+  protected $nestedStorage;
+
+  /**
+   * @param AssetInterface[] $assets
+   *   (optional) An array of assets to immediately add to this collection.
+   */
+  public function __construct($assets = array()) {
+    $this->assetStorage = new \SplObjectStorage();
+    $this->nestedStorage = new \SplObjectStorage();
+
+    foreach ($assets as $asset) {
+      $this->add($asset);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function add(AsseticAssetInterface $asset) {
+    if (!$asset instanceof AssetInterface) {
+      throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset provided; Drupal collections require Drupal-flavored assets.');
+    }
+    $this->ensureCorrectType($asset);
+
+    if (!($this->contains($asset) || $this->find($asset->id()))) {
+      $this->assetStorage->attach($asset);
+      $this->assetIdMap[$asset->id()] = $asset;
+
+      if ($asset instanceof AssetCollectionBasicInterface) {
+        $this->nestedStorage->attach($asset);
+      }
+    }
+
+    return $this;
+  }
+
+  /**
+   * {@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 find($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->find($id)) {
+          return $found;
+        }
+      }
+    }
+
+    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 = FALSE) {
+    if (is_string($needle)) {
+      if (!$needle = $this->find($needle, $graceful)) {
+        return FALSE;
+      }
+    }
+    else if (!$needle instanceof AssetInterface) {
+      throw new \InvalidArgumentException('Invalid type provided to AssetCollectionBasicInterface::replace(); must provide either a string asset id or AssetInterface instance.');
+    }
+
+    return $this->doRemove($needle, $graceful);
+  }
+
+  /**
+   * Performs the actual work of removing an asset from the collection.
+   *
+   * @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
+   *   TRUE on success, FALSE on failure to locate the given asset (or an
+   *   exception, depending on the value of $graceful).
+   *
+   * @throws \OutOfBoundsException
+   *   Thrown if $needle could not be located and $graceful = FALSE.
+   */
+  protected function doRemove(AssetInterface $needle, $graceful) {
+    foreach ($this->assetIdMap as $id => $asset) {
+      if ($asset === $needle) {
+        unset($this->assetStorage[$asset], $this->assetIdMap[$id], $this->nestedStorage[$asset]);
+
+        return TRUE;
+      }
+
+      // TODO wtf, that's protected
+      if ($asset instanceof AssetCollectionBasicInterface && $asset->doRemove($needle, TRUE)) {
+        return TRUE;
+      }
+    }
+
+    if ($graceful) {
+      return FALSE;
+    }
+
+    throw new \OutOfBoundsException('Provided asset was not found in the collection.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function replace($needle, AssetInterface $replacement, $graceful = FALSE) {
+    if (is_string($needle)) {
+      if (!$needle = $this->find($needle, $graceful)) {
+        return FALSE;
+      }
+    }
+    else if (!$needle instanceof AssetInterface) {
+      throw new \InvalidArgumentException('Invalid type provided to AssetCollectionBasicInterface::replace(); must provide either a string asset id or AssetInterface instance.');
+    }
+
+    $this->ensureCorrectType($replacement);
+    if ($this->contains($replacement)) {
+      throw new \LogicException('Asset to be swapped in is already present in the collection.');
+    }
+
+    return $this->doReplace($needle, $replacement, $graceful);
+  }
+
+  /**
+   * Performs the actual work of replacing one asset with another.
+   *
+   * @param AssetInterface $needle
+   *   The AssetInterface instance to swap out.
+   * @param AssetInterface $replacement
+   *   The new asset to swap in.
+   * @param bool $graceful
+   *   Whether failure should return FALSE or throw an exception.
+   *
+   * @return bool
+   *   TRUE on success, FALSE on failure to locate the given asset (or an
+   *   exception, depending on the value of $graceful).
+   *
+   * @throws \OutOfBoundsException
+   */
+  protected function doReplace(AssetInterface $needle, AssetInterface $replacement, $graceful) {
+    $i = 0;
+    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 AssetCollectionBasicInterface) {
+          $this->nestedStorage->attach($replacement);
+        }
+
+        return TRUE;
+      }
+
+      if ($asset instanceof AssetCollectionBasicInterface && $asset->doReplace($needle, $replacement, TRUE)) {
+        return TRUE;
+      }
+      $i++;
+    }
+
+    if ($graceful) {
+      return FALSE;
+    }
+
+    throw new \OutOfBoundsException('Provided asset was not found in the collection.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function all() {
+    return $this->assetIdMap;
+  }
+
+  /**
+   * {@inheritdoc}
+   * TODO Assetic uses their iterator to clone, then populate values and return here; is that a good model for us?
+   */
+  public function getIterator() {
+    return new \RecursiveIteratorIterator(new RecursiveBasicCollectionIterator($this), \RecursiveIteratorIterator::SELF_FIRST);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function each() {
+    return $this->getIterator();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function eachLeaf() {
+    return new \RecursiveIteratorIterator(new RecursiveBasicCollectionIterator($this));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEmpty() {
+    $maincount = $this->assetStorage->count();
+    if ($maincount === 0) {
+      return TRUE;
+    }
+
+    $i = 0;
+    foreach ($this->nestedStorage as $aggregate) {
+      if (!$aggregate->isEmpty()) {
+        return FALSE;
+      }
+      $i++;
+    }
+
+    return $i === $maincount;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function count() {
+    if ($this->nestedStorage->count() === 0) {
+      return $this->assetStorage->count();
+    }
+
+    $c = $i = 0;
+    foreach ($this->nestedStorage as $collection) {
+      $c += $collection->count();
+      $i++;
+    }
+
+    return $this->assetStorage->count() - $i + $c;
+  }
+
+  /**
+   * Ensures that the asset is the correct type for this collection.
+   *
+   * "Type" here refers to 'css' vs. 'js'.
+   *
+   * BasicAssetCollection's implementation has no body because it has no type
+   * restrictions; only aggregates do.
+   *
+   * @param AssetInterface $asset
+   *
+   * @throws \Drupal\Core\Asset\Exception\AssetTypeMismatchException
+   */
+  protected function ensureCorrectType(AssetInterface $asset) {}
+}
+
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..b7a0395
--- /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;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Asset/Collection/Iterator/RecursiveBasicCollectionIterator.php b/core/lib/Drupal/Core/Asset/Collection/Iterator/RecursiveBasicCollectionIterator.php
new file mode 100644
index 0000000..f3b83fa
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Collection/Iterator/RecursiveBasicCollectionIterator.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Aggregate\Iterator\RecursiveBasicCollectionIterator.
+ */
+
+namespace Drupal\Core\Asset\Collection\Iterator;
+
+use Drupal\Core\Asset\Collection\AssetCollectionBasicInterface;
+
+/**
+ * Iterates over an AssetCollectionBasicInterface, treating only assets
+ * that themselves implement AssetCollectionBasicInterface as having children.
+ */
+class RecursiveBasicCollectionIterator extends \RecursiveArrayIterator {
+  public function __construct(AssetCollectionBasicInterface $collection) {
+    parent::__construct($collection->all());
+  }
+
+  public function hasChildren() {
+    return $this->current() instanceof AssetCollectionBasicInterface;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Asset/DependencyInterface.php b/core/lib/Drupal/Core/Asset/DependencyInterface.php
new file mode 100644
index 0000000..d1d78c7
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/DependencyInterface.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * @file
+ * Contains Drupal\Core\Asset\DependencyInterface.
+ */
+
+namespace Drupal\Core\Asset;
+
+/**
+ * Describes assets that can declare dependencies on asset libraries.
+ */
+interface DependencyInterface {
+
+  /**
+   * Indicates whether this asset has one or more library dependencies.
+   *
+   * @return bool
+   */
+  public function hasDependencies();
+
+  /**
+   * Retrieve this asset's dependencies.
+   *
+   * @return array
+   *   An array of dependencies if they exist,
+   */
+  public function getDependencyInfo();
+
+  /**
+   * Add a dependency on a library for this asset.
+   *
+   * @param string $key
+   *   The string identifying the library. This should be a two-part composite
+   *   key, slash-delimited, with the first part being the module owner and the
+   *   second part being the library name.
+   *
+   * @return \Drupal\Core\Asset\DependencyInterface
+   *   The current DependencyInterface object.
+   */
+  public function addDependency($key);
+
+  /**
+   * Clears (removes) all library dependencies for this asset.
+   *
+   * This does not affect ordering (relative positioning) data.
+   *
+   * @return \Drupal\Core\Asset\DependencyInterface
+   *   The current DependencyInterface object.
+   */
+  public function clearDependencies();
+
+}
+
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..16edd41
--- /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 {}
diff --git a/core/lib/Drupal/Core/Asset/Exception/FrozenObjectException.php b/core/lib/Drupal/Core/Asset/Exception/FrozenObjectException.php
new file mode 100644
index 0000000..6315caa
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Exception/FrozenObjectException.php
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Exception\FrozenObjectException.
+ */
+
+namespace Drupal\Core\Asset\Exception;
+
+/**
+ * Exception thrown when a write-protected operation is attempted on a frozen
+ * object.
+ */
+class FrozenObjectException extends \LogicException {}
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..c09b571
--- /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 {}
diff --git a/core/lib/Drupal/Core/Asset/ExternalAsset.php b/core/lib/Drupal/Core/Asset/ExternalAsset.php
new file mode 100644
index 0000000..7e50611
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/ExternalAsset.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\ExternalAsset.
+ */
+
+namespace Drupal\Core\Asset;
+
+use Assetic\Filter\FilterInterface;
+use Drupal\Core\Asset\BaseAsset;
+use Drupal\Core\Asset\Metadata\AssetMetadataInterface;
+use Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException;
+
+class ExternalAsset extends BaseAsset {
+
+  /**
+   * The URL of the asset.
+   *
+   * @var string
+   */
+  protected $sourceUrl;
+
+  /**
+   * Creates a new external asset object.
+   *
+   * @param \Drupal\Core\Asset\Metadata\AssetMetadataInterface $metadata
+   *   The metadata object for the new external asset.
+   * @param array $sourceUrl
+   *   The URL at which the external asset lives.
+   * @param \Assetic\Filter\FilterInterface[] $filters
+   *   (optional) An array of FilterInterface objects to apply to this asset.
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown if an invalid URL is provided for $sourceUrl.
+   */
+  public function __construct(AssetMetadataInterface $metadata, $sourceUrl, $filters = array()) {
+    // Protocol- and root-relative URLs are not acceptable, because such an
+    // asset would not actually be external to this Drupal site.
+    if (FALSE === strpos($sourceUrl, '://')) {
+      throw new \InvalidArgumentException(sprintf('"%s" is not a valid URL.', $sourceUrl));
+    }
+
+    $this->sourceUrl = $sourceUrl;
+    $this->setTargetPath($sourceUrl); // TODO do this immediately...for now.
+
+    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;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLastModified() {
+    // TODO very wrong. decide how to do this right.
+    throw new UnsupportedAsseticBehaviorException('Drupal does not support the retrieval or manipulation of remote assets.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function load(FilterInterface $additionalFilter = NULL) {
+    // TODO very wrong. decide how to do this right.
+    throw new UnsupportedAsseticBehaviorException('Drupal does not support the retrieval or manipulation of remote assets.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dump(FilterInterface $additionalFilter = NULL) {
+    // TODO very 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..d35a5e1
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Factory/AssetCollector.php
@@ -0,0 +1,251 @@
+<?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\DefaultAssetMetadataFactory;
+use Drupal\Core\Asset\Metadata\MetadataFactoryInterface;
+
+/**
+ * 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 factory that creates metadata bags for assets.
+   *
+   * @var \Drupal\Core\Asset\Metadata\MetadataFactoryInterface
+   */
+  protected $metadataFactory;
+
+  /**
+   * 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 \Drupal\Core\Asset\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, MetadataFactoryInterface $factory = NULL) {
+    if (!is_null($factory)) {
+      $this->metadataFactory = $factory;
+    }
+    else {
+      $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, $source_type, $data);
+    if (!empty($options)) {
+      $metadata->add($options);
+    }
+
+    $class = $this->classMap[$source_type];
+    $asset = new $class($metadata, $data, $filters);
+
+    if (!empty($this->collection)) {
+      $this->add($asset);
+    }
+
+    if ($asset_type == 'css') {
+      if (!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}
+   */
+  public function setMetadataFactory(MetadataFactoryInterface $factory) {
+    if ($this->isLocked()) {
+      throw new LockedObjectException('The collector instance is locked. Asset defaults cannot be modified on a locked collector.');
+    }
+
+    $this->metadataFactory = $factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMetadataDefaults($asset_type, $source_type, $data) {
+    if ($asset_type === 'css') {
+      return $this->metadataFactory->createCssMetadata($source_type, $data);
+    }
+    elseif ($asset_type === 'js') {
+      return $this->metadataFactory->createJsMetadata($source_type, $data);
+    }
+    else {
+      throw new \InvalidArgumentException(sprintf('Only assets of type "js" or "css" are supported, "%s" requested.', $asset_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->metadataFactory = new DefaultAssetMetadataFactory();
+  }
+
+}
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..5664f51
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Factory/AssetCollectorInterface.php
@@ -0,0 +1,229 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Factory\AssetCollectorInterface.
+ */
+
+namespace Drupal\Core\Asset\Factory;
+
+use Drupal\Core\Asset\Exception\LockedObjectException;
+use Drupal\Core\Asset\Metadata\AssetMetadataInterface;
+use Drupal\Core\Asset\AssetInterface;
+use Drupal\Core\Asset\Collection\AssetCollectionInterface;
+use Drupal\Core\Asset\Metadata\MetadataFactoryInterface;
+
+/**
+ * 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 URL 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,
+   *      TODO finish this comment
+   *
+   * @see clearLastCss().
+   *
+   * @return \Drupal\Core\Asset\AssetInterface
+   *   The created AssetInterface object.
+   *
+   * @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 AssetCollectorInterface
+   *   The current asset collector.
+   */
+  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
+   *   The collection the collector should use internally.
+   *
+   * @return AssetCollectorInterface
+   *   The current asset collector.
+   *
+   * @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 AssetCollectorInterface
+   *   The current asset collector.
+   *
+   * @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 AssetCollectorInterface
+   *   The current asset collector.
+   *
+   * @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 AssetCollectorInterface
+   *   The current asset collector.
+   *
+   * @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 metadata factory to use for generating asset metadata.
+   *
+   * @param MetadataFactoryInterface $factory
+   *   The factory to use.
+   *
+   * @return AssetCollectorInterface
+   *   The current asset collector.
+   *
+   * @throws LockedObjectException
+   *   Thrown if the collector is locked when this method is called.
+   */
+  public function setMetadataFactory(MetadataFactoryInterface $factory);
+
+  /**
+   * 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 string $asset_type
+   *   A string, 'css' or 'js', indicating the type of metadata to retrieve.
+   *
+   * @param string $source_type
+   *   The source type for the asset that will receive this metadata: 'file',
+   *   'external', or 'string'.
+   *
+   * @param string $data
+   *   For 'file' or 'external' source types, this is the path to the asset. For
+   *   'string' source types, it is the whole body of the asset.
+   *
+   * @return AssetMetadataInterface
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown if a type other than 'css' or 'js' is provided.
+   */
+  public function getMetadataDefaults($asset_type, $source_type, $data);
+
+  /**
+   * Restores metadata factory to the default factory.
+   *
+   * This simply changes the metadata factory to
+   * \Drupal\Core\Asset\Metadata\DefaultAssetMetadataFactory, which will cause
+   * future create() calls to use the default metadata.
+   *
+   * @throws \Drupal\Core\Asset\Exception\LockedObjectException
+   *   Thrown if the incorrect key is provided.
+   */
+  public function restoreDefaults();
+
+}
diff --git a/core/lib/Drupal/Core/Asset/Factory/AssetLibraryFactory.php b/core/lib/Drupal/Core/Asset/Factory/AssetLibraryFactory.php
new file mode 100644
index 0000000..9a580f3
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Factory/AssetLibraryFactory.php
@@ -0,0 +1,142 @@
+<?php
+/**
+ * @file
+ * Contains Drupal\Core\Asset\AssetLibraryFactory.
+ */
+
+namespace Drupal\Core\Asset\Factory;
+
+use Drupal\Core\Asset\Factory\AssetCollector;
+use Drupal\Core\Asset\Collection\AssetLibrary;
+use Drupal\Core\Asset\Metadata\DefaultAssetMetadataFactory;
+use Drupal\Core\Asset\Metadata\MetadataFactoryInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+
+class AssetLibraryFactory {
+
+  /**
+   * The module handler. Used to collect library data from hook_library_info().
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The asset collector to use in populating the libraries.
+   *
+   * @var \Drupal\Core\Asset\Factory\AssetCollector
+   */
+  protected $collector;
+
+  /**
+   * The metadata factory to provide to the collector
+   *
+   * @var \Drupal\Core\Asset\Metadata\MetadataFactoryInterface
+   */
+  protected $metadataFactory;
+
+  /**
+   * Creates a new AssetLibraryFactory.
+   *
+   * @param ModuleHandlerInterface $moduleHandler
+   *   The module handler. The factory uses this to collect hook_library_info()
+   *   declaration data.
+   * @param AssetCollectorInterface $collector
+   *   (optional) The collector to use in populating the asset library with
+   *   asset objects. If not provided, core's default AssetCollector will be
+   *   used.
+   * @param MetadataFactoryInterface $metadataFactory
+   *   (optional) A metadata factory to provide to the collector. Note that this
+   *   will NOT be used if a collector is given.
+   *
+   * @throws \RuntimeException
+   *   Thrown if a locked collector is given.
+   */
+  public function __construct(ModuleHandlerInterface $moduleHandler, AssetCollectorInterface $collector = NULL, MetadataFactoryInterface $metadataFactory = NULL) {
+    $this->moduleHandler = $moduleHandler;
+    $this->metadataFactory = $metadataFactory ?: new DefaultAssetMetadataFactory();
+    $this->collector = $collector ?: new AssetCollector(NULL, $this->metadataFactory);
+
+    if ($this->collector->isLocked()) {
+      throw new \RuntimeException('The collector provided to an AssetLibraryFactory was locked; it must be unlocked so the factory can fully control it.');
+    }
+  }
+
+  /**
+   * Returns an AssetLibrary based on data declared in hook_library_info().
+   *
+   * @param $key
+   *
+   * @return AssetLibrary|bool
+   *   An AssetLibrary instance, or FALSE if the key did not resolve to library
+   *   data.
+   */
+  public function getLibrary($key) {
+    list($module, $name) = preg_split('/\//', $key);
+
+    if (!$this->moduleHandler->implementsHook($module, 'library_info')) {
+      // Module doesn't implement hook_library_info(), a library can't exist.
+      return FALSE;
+    }
+
+    $declarations = call_user_func($module . '_library_info') ?: array();
+
+    $this->moduleHandler->alter('library_info', $declarations, $module);
+
+    if (!isset($declarations[$name])) {
+      // No library by the given name.
+      return FALSE;
+    }
+
+    // Normalize the data - hook_library_info() allows sloppiness
+    $info = $declarations[$name] + array('dependencies' => array(), 'js' => array(), 'css' => array());
+    $library = new AssetLibrary();
+
+    if (isset($info['title'])) {
+      $library->setTitle($info['title']);
+    }
+    if (isset($info['version'])) {
+      $library->setVersion($info['version']);
+    }
+    if (isset($info['website'])) {
+      $library->setWebsite($info['website']);
+    }
+
+    // Record dependencies on the library, if any.
+    foreach ($info['dependencies'] as $dep) {
+      // TODO remove this, this is the wrong level at which to declare.
+      $library->addDependency($dep[0] . '/' . $dep[1]);
+    }
+
+    // Populate the library with asset objects.
+    $this->collector->setCollection($library);
+    foreach (array('js', 'css') as $type) {
+      foreach ($info[$type] as $data => $options) {
+        if (is_scalar($options)) {
+          $data = $options;
+          $options = array();
+        }
+
+        $source_type = isset($options['type']) ? $options['type'] : 'file';
+        unset($options['type']);
+
+        if ($type == 'js' && $source_type == 'setting') {
+          // TODO temporarily continue/skip if it's a js setting, can't handle those yet
+          continue;
+        }
+
+        $asset = $this->collector->create($type, $source_type, $data, $options);
+        foreach ($info['dependencies'] as $dep) {
+          $asset->addDependency($dep[0] . '/' . $dep[1]);
+        }
+      }
+    }
+
+    // Ensure that auto-aftering of CSS doesn't bleed across libraries.
+    $this->collector->clearLastCss();
+
+    $library->freeze();
+    return $library;
+  }
+}
+
diff --git a/core/lib/Drupal/Core/Asset/FileAsset.php b/core/lib/Drupal/Core/Asset/FileAsset.php
new file mode 100644
index 0000000..2d65ad6
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/FileAsset.php
@@ -0,0 +1,82 @@
+<?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\AssetMetadataInterface;
+
+class FileAsset extends BaseAsset {
+
+  /**
+   * The path, relative to DRUPAL_ROOT, to the file asset.
+   *
+   * @var string
+   */
+  protected $source;
+
+  /**
+   * Creates a new file asset object.
+   *
+   * @param \Drupal\Core\Asset\Metadata\AssetMetadataInterface $metadata
+   *   The metadata object for the new file asset.
+   * @param array $source
+   *   The path at which the file asset lives. This should be the path relative
+   *   to DRUPAL_ROOT, not an absolute path.
+   * @param \Assetic\Filter\FilterInterface[] $filters
+   *   (optional) An array of FilterInterface objects to apply to this asset.
+   *
+   * TODO https://drupal.org/node/1308152 would make $source MUCH clearer
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown if an invalid URL is provided for $source.
+   */
+  public function __construct(AssetMetadataInterface $metadata, $source, $filters = array()) {
+    if (!is_string($source)) {
+      throw new \InvalidArgumentException('File assets require a string filepath for their $source parameter.');
+    }
+
+    $sourceRoot = dirname($source);
+    $sourcePath = basename($source);
+    $this->source = $source;
+    $this->setTargetPath($source); // TODO do this immediately...for now.
+
+    parent::__construct($metadata, $filters, $sourceRoot, $sourcePath);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function id() {
+    return $this->source;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  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);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  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/AssetGraph.php b/core/lib/Drupal/Core/Asset/GroupSort/AssetGraph.php
new file mode 100644
index 0000000..20a6954
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/GroupSort/AssetGraph.php
@@ -0,0 +1,182 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\AssetGraph.
+ */
+
+namespace Drupal\Core\Asset\GroupSort;
+
+use Drupal\Core\Asset\AssetInterface;
+use Drupal\Core\Asset\RelativePositionInterface;
+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 ordering declarations:
+ *
+ *   - Dependencies, which guarantee that dependent asset must be present and
+ *     that it must precede the asset declaring it as a dependency. Expressed by
+ *     methods on \Drupal\Core\Asset\DependencyInterface.
+ *   - Positioning, which can guarantee that asset A will be either preceded or
+ *     succeeded by asset B, but does NOT guarantee that B will be present.
+ *     Expressed by methods on \Drupal\Core\Asset\RelativePositionInterface.
+ *
+ * The first, dependencies, are NOT dealt with by AssetGraph; dependency
+ * resolution requires collaboration with other fixed services. For that,
+ * @see \Drupal\Core\Asset\Collection\AssetCollection::resolveLibraries()
+ *
+ * AssetGraph deals only with positioning data. As asset vertices are added to
+ * the graph via addVertex(), AssetGraph checks their predecessor and successor
+ * lists. If an asset in either of those lists is already present in the graph,
+ * then AssetGraph will automatically create a directed edge between the two. If
+ * a vertex from those lists is not already present, then a 'watch' is
+ * created for it, such that if that vertex is added at a later time then the
+ * appropriate directed edge will be created automatically.
+ *
+ * This makes it much easier for calling code to construct the correct graph -
+ * it needs merely add all the asset vertices one by one, and the correct graph
+ * is guaranteed to be created.
+ *
+ * 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 DirectedAdjacencyList that is tailored
+   * to handling the ordering information carried by RelativePositionInterface
+   * instances.
+   *
+   * @param bool $process
+   *   Whether or not to automatically process positioning metadata as vertices
+   *   are added. This should be left as TRUE in almost every user-facing case;
+   *   the primary use case for setting FALSE is 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 positioning 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 RelativePositionInterface) {
+      // 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();
+
+        // Add a directed edge indicating that this asset vertex succeeds
+        // another asset vertex. Or, if that other asset does not yet have a
+        // vertex in the AssetGraph, set up a watch for it.
+        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();
+
+        // Add a directed edge indicating that this asset vertex preceeds
+        // another asset vertex. Or, if that other asset does not yet have a
+        // vertex in the AssetGraph, set up a watch for it.
+        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 positioning 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->eachVertex(function($v, $adjacent) use (&$graph) {
+      $graph->addVertex($v);
+
+      foreach ($adjacent as $adj) {
+        $graph->addDirectedEdge($adj, $v);
+      }
+    });
+
+    return $graph;
+  }
+
+}
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..1968730
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/GroupSort/AssetGraphSorter.php
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Sort\AssetGraphSorter.
+ */
+
+namespace Drupal\Core\Asset\GroupSort;
+
+use Drupal\Core\Asset\GroupSort\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 {
+
+  public function __construct() {
+    // By default, xdebug prevents a call stack depth of greater than 100
+    // function calls as a protection against recursion. The graph traversal
+    // used here utilizes a deep recursive walker that exceeds this limit in
+    // most cases - though typically not by much. So, if xdebug is enabled, we
+    // extend this call stack limit if it is less than 300. Exceeding a max
+    // stack depth of 300 would require there to be at least 90, but possibly as
+    // many as around 140, discrete css OR js assets (not combined). Even in the
+    // most complex of sites, such a high number is unlikely.
+    if (extension_loaded('xdebug') && ini_get('xdebug.max_nesting_level') < 300) {
+      ini_set('xdebug.max_nesting_level', 300);
+    }
+  }
+
+  /**
+   * Creates a queue of starting vertices that will facilitate an ideal TSL.
+   *
+   * As a strategy, we assume that the source vertices (tops of the trees
+   * embedded in the graph) that have the greatest reach (and hence would result
+   * in the largest "asset groups") will be the best starting points for
+   * building asset groups: we assume they are more stable and yield the minimal
+   * number of asset groups overall.
+   *
+   * @param \Drupal\Core\Asset\GroupSort\AssetGraph $graph
+   *   The graph from which to create a starting queue.
+   *
+   * @return \SplQueue $queue
+   *   A queue of vertices for traversal, the first one being the one with the
+   *   greatest reach.
+   */
+  protected function createSourceQueue(AssetGraph $graph) {
+    $reach_visitor = new DepthFirstBasicVisitor();
+
+    // Find source vertices (outdegree 0) in the graph
+    $sources = DepthFirst::find_sources($graph, $reach_visitor);
+
+    // Traverse the transposed graph to get reachability data on each vertex
+    DepthFirst::traverse($graph, $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;
+  }
+
+}
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..cd7d74d
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/GroupSort/AssetGroupSorterInterface.php
@@ -0,0 +1,47 @@
+<?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 \Drupal\Core\Asset\Collection\AssetCollectionInterface $collection
+   *   The collection to group and sort.
+   *
+   * @return array
+   *   A sorted, linear list of assets that respects all dependency and ordering
+   *   metadata.
+   */
+  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 \Drupal\Core\Asset\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);
+
+}
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..b5b4bfc
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/GroupSort/CssGraphSorter.php
@@ -0,0 +1,117 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\GroupSort\CssGraphSorter.
+ */
+
+namespace Drupal\Core\Asset\GroupSort;
+
+use Drupal\Core\Asset\GroupSort\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\GroupSort\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();
+
+    // TODO Would probably be better to inject the right collection rather than asking for it here
+    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.
+    // (In the AssetGraph, if asset A comes before asset B, a directed edge
+    // exists from B to A. By transposing the graph, all directed edges are
+    // reversed, so that a directed edge exists from A to B.
+    // A topological sort on a graph will provide a linear ordering of all
+    // vertices, in our example: "A, B". Without performing the transpose
+    // operation, we'd get "B, A", which is the inverse of what we need.)
+    $transpose = $graph->transpose();
+
+    // Create a queue of start vertices to prime the traversal.
+    $queue = $this->createSourceQueue($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()->reverse();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Asset/GroupSort/OptimallyGroupedTSLVisitor.php b/core/lib/Drupal/Core/Asset/GroupSort/OptimallyGroupedTSLVisitor.php
new file mode 100644
index 0000000..9b46e25
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/GroupSort/OptimallyGroupedTSLVisitor.php
@@ -0,0 +1,118 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\OptimallyGroupedTSLVisitor.
+ */
+
+namespace Drupal\Core\Asset\GroupSort;
+
+use Drupal\Core\Asset\Collection\AssetCollection;
+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 = new AssetCollection();
+    $this->groups = $groups;
+    $this->vertexMap = $vertex_map;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function beginTraversal() {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function endTraversal() {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onInitializeVertex($vertex, $source, \SplQueue $queue) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onBackEdge($vertex, \Closure $visit) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onStartVertex($vertex, \Closure $visit) {
+    // If there's a record in the vertex map, it means this vertex has an
+    // optimal group. Remove it from that group, as being provided to this
+    // visitor method indicates the vertex is being 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) {
+    if ($this->vertexMap->contains($vertex)) {
+      foreach ($this->vertexMap[$vertex] as $adjacent) {
+        $visit($adjacent);
+      }
+    }
+    $this->tsl->add($vertex);
+  }
+
+  /**
+   * Returns the TSL produced by a depth-first traversal.
+   *
+   * @return \Drupal\Core\Asset\Collection\AssetCollection
+   *   A topologically sorted list of vertices.
+   */
+  public function getTSL() {
+    return $this->tsl;
+  }
+
+}
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..7de5fd1
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataBag.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\AssetMetadataBag.
+ */
+
+namespace Drupal\Core\Asset\Metadata;
+
+use Symfony\Component\HttpFoundation\ParameterBag;
+
+/**
+ * A bag that holds asset metadata as key/value pairs.
+ */
+class AssetMetadataBag extends ParameterBag implements AssetMetadataInterface {
+
+  /**
+   * A string identifying the asset type for which this metadata is intended.
+   *
+   * Drupal core expects 'css' or 'js'.
+   *
+   * @var string
+   */
+  protected $type;
+
+  public function __construct($type, array $values = array()) {
+    $this->type = $type;
+    parent::__construct($values);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getType() {
+    return $this->type;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataInterface.php b/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataInterface.php
new file mode 100644
index 0000000..1f77f65
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataInterface.php
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Metadata\AssetMetadataInterface.
+ */
+
+namespace Drupal\Core\Asset\Metadata;
+
+/**
+ * Interface describing asset metadata bags.
+ */
+interface AssetMetadataInterface extends \Countable, \IteratorAggregate {
+
+  /**
+   * Indicates the type of asset for which this metadata is intended.
+   *
+   * @return string
+   *   A string indicating type - 'js' or 'css' are the expected values.
+   */
+  public function getType();
+
+  /**
+   * Returns all values in the metadata bag as an associative array.
+   *
+   * @return array
+   */
+  public function all();
+
+  /**
+   * Returns the keys of all values in the bag as an indexed array.
+   *
+   * @return array
+   */
+  public function keys();
+
+  /**
+   * Indicates whether or not a value is present in the bag.
+   *
+   * @param $key
+   *
+   * @return bool
+   */
+  public function has($key);
+
+  /**
+   * Sets the provided key to the provided value.
+   *
+   * @param $key
+   * @param $value
+   *
+   * @return void
+   */
+  public function set($key, $value);
+
+  /**
+   * Adds a set of key/value pairs into the bag. Replaces existing keys.
+   *
+   * @param array $values
+   *
+   * @return void
+   */
+  public function add(array $values = array());
+
+  /**
+   * Wholly replaces all explicit values in the bag with the provided values.
+   *
+   * @param array $values
+   *
+   * @return void
+   */
+  public function replace(array $values = array());
+
+  /**
+   * Gets the value for the provided key from the bag.
+   *
+   * @param $key
+   *
+   * @return mixed
+   */
+  public function get($key);
+
+}
diff --git a/core/lib/Drupal/Core/Asset/Metadata/DefaultAssetMetadataFactory.php b/core/lib/Drupal/Core/Asset/Metadata/DefaultAssetMetadataFactory.php
new file mode 100644
index 0000000..7c32f90
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Metadata/DefaultAssetMetadataFactory.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\Metadata\DefaultAssetMetadataFactory.
+ */
+
+namespace Drupal\Core\Asset\Metadata;
+
+/**
+ * Factory for asset metadata.
+ */
+class DefaultAssetMetadataFactory implements MetadataFactoryInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createCssMetadata($source_type, $data) {
+    return new AssetMetadataBag('css', array(
+      'every_page' => FALSE,
+      'media' => 'all',
+      'preprocess' => TRUE,
+      'browsers' => array(
+        'IE' => TRUE,
+        '!IE' => TRUE,
+      ),
+    ));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createJsMetadata($source_type, $data) {
+    return new AssetMetadataBag('js', array(
+      'every_page' => FALSE,
+      'scope' => 'footer',
+      'cache' => TRUE,
+      'preprocess' => TRUE,
+      'attributes' => array(),
+      'version' => NULL,
+      'browsers' => array(),
+    ));
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Asset/Metadata/MetadataFactoryInterface.php b/core/lib/Drupal/Core/Asset/Metadata/MetadataFactoryInterface.php
new file mode 100644
index 0000000..0e3b866
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Metadata/MetadataFactoryInterface.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\Metadata\MetadataFactoryInterface.
+ */
+
+namespace Drupal\Core\Asset\Metadata;
+
+/**
+ * Interface for factories that create asset metadata.
+ */
+interface MetadataFactoryInterface {
+
+  /**
+   * Creates an asset metadata object for use in a CSS AssetInterface object.
+   *
+   * @param string $source_type
+   *   The source type for the asset that will receive this metadata: 'file',
+   *   'external', or 'string'.
+   *
+   * @param string $data
+   *   For 'file' or 'external' source types, this is the path to the asset. For
+   *   'string' source types, it is the whole body of the asset.
+   *
+   * @return \Drupal\Core\Asset\Metadata\AssetMetadataInterface
+   */
+  public function createCssMetadata($source_type, $data);
+
+  /**
+   * Creates an asset metadata object for use in a JS AssetInterface object.
+   *
+   * @param string $source_type
+   *   The source type for the asset that will receive this metadata: 'file',
+   *   'external', or 'string'.
+   *
+   * @param string $data
+   *   For 'file' or 'external' source types, this is the path to the asset. For
+   *   'string' source types, it is the whole body of the asset.
+   *
+   * @return \Drupal\Core\Asset\Metadata\AssetMetadataInterface
+   */
+  public function createJsMetadata($source_type, $data);
+
+}
diff --git a/core/lib/Drupal/Core/Asset/Optimize/AssetCollectionAggregatorInterface.php b/core/lib/Drupal/Core/Asset/Optimize/AssetCollectionAggregatorInterface.php
new file mode 100644
index 0000000..41972de
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Optimize/AssetCollectionAggregatorInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\AssetCollectionAggregatorInterface.
+ */
+
+namespace Drupal\Core\Asset\Optimize;
+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.
+   *
+   * @return AssetCollectionInterface
+   *   A new AssetCollectionInterface containing the aggregated assets. The
+   *   collection is populated by objects implementing at least AssetInterface,
+   *   and possibly also AssetAggregateInterface.
+   */
+  public function aggregate(AssetCollectionInterface $collection);
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/Optimize/AssetCollectionOptimizerInterface.php b/core/lib/Drupal/Core/Asset/Optimize/AssetCollectionOptimizerInterface.php
new file mode 100644
index 0000000..f86d2a4
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Optimize/AssetCollectionOptimizerInterface.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\AssetCollectionOptimizerInterface.
+ */
+
+namespace Drupal\Core\Asset\Optimize;
+use Drupal\Core\Asset\Collection\AssetCollectionInterface;
+
+/**
+ * Interface for a service that optimizes an asset collection.
+ */
+interface AssetCollectionOptimizerInterface {
+
+  /**
+   * 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/Optimize/CssCollectionAggregator.php b/core/lib/Drupal/Core/Asset/Optimize/CssCollectionAggregator.php
new file mode 100644
index 0000000..c9839ea
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Optimize/CssCollectionAggregator.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\CssCollectionAggregator.
+ */
+
+namespace Drupal\Core\Asset\Optimize;
+
+use Drupal\Core\Asset\Aggregate\AssetAggregate;
+use Drupal\Core\Asset\Collection\AssetCollection;
+use Drupal\Core\Asset\Collection\AssetCollectionInterface;
+use Drupal\Core\Asset\GroupSort\AssetGroupSorterInterface;
+use Drupal\Core\Asset\Optimize\AssetCollectionAggregatorInterface;
+
+/**
+ * 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);
+
+    $processed = new AssetCollection();
+    $last_key = FALSE;
+    foreach ($tsl as $asset) {
+      $key = $this->sorter->getGroupingKey($asset);
+
+      if ($key && $key !== $last_key) {
+        $aggregate = new AssetAggregate($asset->getMetadata());
+        $processed->add($aggregate);
+      }
+
+      $key ? $aggregate->add($asset) : $processed->add($asset);
+      $last_key = $key;
+    }
+
+    return $processed;
+  }
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/Optimize/CssCollectionOptimizer.php b/core/lib/Drupal/Core/Asset/Optimize/CssCollectionOptimizer.php
new file mode 100644
index 0000000..74a6c34
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Optimize/CssCollectionOptimizer.php
@@ -0,0 +1,95 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\CssCollectionOptimizer.
+ */
+
+namespace Drupal\Core\Asset\Optimize;
+
+use Drupal\Core\Asset\Optimize\AssetCollectionAggregatorInterface;
+use Drupal\Core\Asset\AssetDumperInterface;
+use Drupal\Core\Asset\AssetOptimizerInterface;
+use Drupal\Core\Asset\Collection\AssetCollectionInterface;
+use Drupal\Core\Asset\Optimize\AssetCollectionOptimizerInterface;
+use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
+
+/**
+ * Optimizes a collection of CSS assets.
+ */
+class CssCollectionOptimizer implements AssetCollectionOptimizerInterface {
+
+  /**
+   * A CSS asset aggregator.
+   *
+   * @var \Drupal\Core\Asset\Optimize\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 CssCollectionOptimizer.
+   *
+   * @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');
+
+          $map[$id] = $asset->getTargetPath();
+        }
+      }
+    }
+
+    return $collection;
+  }
+
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/RelativePositionInterface.php b/core/lib/Drupal/Core/Asset/RelativePositionInterface.php
new file mode 100644
index 0000000..fa49ab7
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/RelativePositionInterface.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * @file
+ * Contains Drupal\Core\Asset\RelativePositionInterface.
+ */
+
+namespace Drupal\Core\Asset;
+
+/**
+ * Describes an asset or asset-like object that can declare relative positions.
+ */
+interface RelativePositionInterface {
+
+  /**
+   * 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|\Drupal\Core\Asset\AssetInterface $asset
+   *   The asset to precede the current asset.
+   *
+   * @return \Drupal\Core\Asset\RelativePositionInterface
+   *   The current RelativePositionInterface object.
+   */
+  public function after($asset);
+
+  /**
+   * Indicates whether this asset has one or more asset predecessors.
+   *
+   * @return bool
+   */
+  public function hasPredecessors();
+
+  /**
+   * 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();
+
+  /**
+   * Clears all ordering info declared by after() for this asset.
+   *
+   * This does not affect dependency data.
+   *
+   * @return \Drupal\Core\Asset\RelativePositionInterface
+   *   The current RelativePositionInterface object.
+   */
+  public function clearPredecessors();
+
+  /**
+   * 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|\Drupal\Core\Asset\AssetInterface $asset
+   *   The asset to succeed the current asset.
+   *
+   * @return \Drupal\Core\Asset\RelativePositionInterface
+   *   The current RelativePositionInterface object.
+   */
+  public function before($asset);
+
+  /**
+   * Indicates whether this asset has one or more asset successors.
+   *
+   * @return bool
+   */
+  public function hasSuccessors();
+
+  /**
+   * 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 \Drupal\Core\Asset\RelativePositionInterface
+   *   The current RelativePositionInterface object.
+   */
+  public function clearSuccessors();
+
+}
+
diff --git a/core/lib/Drupal/Core/Asset/Render/AssetCollectionRendererInterface.php b/core/lib/Drupal/Core/Asset/Render/AssetCollectionRendererInterface.php
new file mode 100644
index 0000000..5b586e1
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Render/AssetCollectionRendererInterface.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Render\AssetCollectionRendererInterface.
+ */
+
+namespace Drupal\Core\Asset\Render;
+
+use Drupal\Core\Asset\Collection\AssetCollectionInterface;
+
+/**
+ * Renders a collection of assets to HTML.
+ */
+interface AssetCollectionRendererInterface {
+
+  /**
+   * Renders the given asset collection into HTML.
+   *
+   * @param AssetCollectionInterface $collection
+   *   The collection whose assets should be rendered.
+   *
+   * @return array
+   *   A renderable array (for now). TODO: string containing html tags!
+   */
+  public function render(AssetCollectionInterface $collection);
+}
\ No newline at end of file
diff --git a/core/lib/Drupal/Core/Asset/Render/CssCollectionRenderer.php b/core/lib/Drupal/Core/Asset/Render/CssCollectionRenderer.php
new file mode 100644
index 0000000..ac9f07f
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Render/CssCollectionRenderer.php
@@ -0,0 +1,181 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\CssCollectionRenderer.
+ */
+
+namespace Drupal\Core\Asset\Render;
+
+use Drupal\Component\Utility\String;
+use Drupal\Core\Asset\Aggregate\AssetAggregate;
+use Drupal\Core\Asset\Aggregate\AssetAggregateInterface;
+use Drupal\Core\Asset\Collection\AssetCollectionInterface;
+use Drupal\Core\Asset\ExternalAsset;
+use Drupal\Core\Asset\FileAsset;
+use Drupal\Core\Asset\GroupSort\AssetGroupSorterInterface;
+use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
+use Drupal\Core\Asset\StringAsset;
+
+/**
+ * Renders a collection of CSS assets into a set of HTML tags.
+ */
+class CssCollectionRenderer implements AssetCollectionRendererInterface {
+
+  /**
+   * The state key/value store.
+   *
+   * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
+   */
+  protected $state;
+
+  /**
+   * The CSS asset sorter.
+   *
+   * @var AssetGroupSorterInterface
+   */
+  protected $sorter;
+
+  /**
+   * Default render array properties for link tag elements.
+   *
+   * @var array
+   */
+  protected $linkElementDefaults = array(
+    '#type' => 'html_tag',
+    '#tag' => 'link',
+    '#attributes' => array(
+      'rel' => 'stylesheet',
+    ),
+  );
+
+  /**
+   * Default render array properties for style tag elements.
+   *
+   * @var array
+   */
+  protected $styleElementDefaults = array(
+    '#type' => 'html_tag',
+    '#tag' => 'style',
+  );
+
+  /**
+   * Constructs a CssCollectionRenderer.
+   *
+   * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface
+   *   The state key/value store.
+   *
+   * @param \Drupal\Core\Asset\GroupSort\AssetGroupSorterInterface $sorter
+   *   The CSS sorter service. Used only to reduce stylesheet count below
+   *   31 for <IE10.
+   */
+  public function __construct(KeyValueStoreInterface $state, AssetGroupSorterInterface $sorter) {
+    $this->state = $state;
+    $this->sorter = $sorter;
+  }
+
+  public function render(AssetCollectionInterface $collection) {
+    // Deal with <IE10's limit of 31 stylesheets.
+    $all = $collection->all();
+    if (count($all) > 31) {
+      $link_count = 0;
+      foreach ($all as $asset) {
+        if ($asset instanceof FileAsset || $asset instanceof ExternalAsset ||
+            $asset instanceof AssetAggregateInterface) {
+          $link_count++;
+        }
+      }
+
+      if ($link_count > 31) {
+        $asset = reset($all);
+
+        do {
+          $key = $this->sorter->getGroupingKey($asset);
+
+          if ($key) {
+            $add = array();
+
+            $group_count = 0;
+            do {
+              $group_count++;
+              $add[] = $asset;
+              $asset = next($all);
+              $nkey = $this->sorter->getGroupingKey($asset);
+            } while ($key == $nkey && $group_count < 31); // IE has max of 31 @imports per style tag
+
+            if (count($add) > 1) {
+              // only make aggregate if there's more than 1
+              $aggregate = new AssetAggregate(reset($add)->getMetadata(), $add);
+              $meta = $aggregate->getMetadata();
+              $meta->set('light_grouping', TRUE);
+
+              $first = array_shift($add);
+              foreach ($add as $added) {
+                $collection->remove($added);
+              }
+              // Have to replace after removing, otherwise they'll be removed
+              // from the aggregate.
+              $collection->replace($first, $aggregate);
+            }
+
+            $link_count -= $group_count - 1; // add one to account for aggregate
+            prev($all); // rewind for next loop
+
+            if ($link_count <= 31) {
+              break;
+            }
+          }
+        // It's possible to still more than 31 assets here. If so...oh well.
+        } while ($asset = next($all));
+      }
+    }
+
+    $elements = array();
+
+    // A dummy query-string is added to filenames, to gain control over
+    // browser-caching. The string changes on every update or full cache
+    // flush, forcing browsers to load a new copy of the files, as the
+    // URL changed.
+    $query_string = $this->state->get('system.css_js_query_string') ?: '0';
+
+    foreach ($collection->all() as $asset) {
+      $meta = $asset->getMetadata();
+
+      if ($asset instanceof StringAsset) {
+        $element = $this->styleElementDefaults;
+        $element['#value'] = $asset->getContent();
+        // For inline CSS to validate as XHTML, all CSS containing XHTML needs
+        // to be wrapped in CDATA. To make that backwards compatible with HTML
+        // 4, we need to comment out the CDATA-tag.
+        $element['#value_prefix'] = "\n/* <![CDATA[ */\n";
+        $element['#value_suffix'] = "\n/* ]]> */\n";
+      }
+      elseif ($asset instanceof AssetAggregateInterface && $meta->get('light_grouping')) {
+        $import = array();
+        foreach ($asset as $subasset) {
+          $import[] = '@import url("' . String::checkPlain(file_create_url($subasset->getTargetPath()) . '?' . $query_string) . '");';
+        }
+
+        $element = $this->styleElementDefaults;
+        $element['#value'] = "\n" . implode("\n", $import) . "\n";
+      }
+      elseif ($asset instanceof ExternalAsset) {
+        $element = $this->linkElementDefaults;
+        $element['#attributes']['href'] = $asset->getTargetPath();
+      }
+      else {
+        // individual files and aggregates
+        $query_string_separator = (strpos($asset->getTargetPath(), '?') !== FALSE) ? '&' : '?';
+        $element = $this->linkElementDefaults;
+        $element['#attributes']['href'] = file_create_url($asset->getTargetPath()) . $query_string_separator . $query_string;;
+      }
+
+      $element['#attributes']['media'] = $meta->get('media');
+      $element['#browsers'] = $meta->get('browsers');
+      $elements[] = $element;
+    }
+
+    return $elements;
+  }
+}
+
diff --git a/core/lib/Drupal/Core/Asset/StringAsset.php b/core/lib/Drupal/Core/Asset/StringAsset.php
new file mode 100644
index 0000000..2c690e9
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/StringAsset.php
@@ -0,0 +1,81 @@
+<?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\AssetMetadataInterface;
+
+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 = FALSE; // TODO this is terrible
+
+  /**
+   * Creates a new string asset object.
+   *
+   * @param \Drupal\Core\Asset\Metadata\AssetMetadataInterface $metadata
+   *   The metadata object for the new string asset.
+   * @param string $content
+   *   The content of the new string asset.
+   * @param \Assetic\Filter\FilterInterface[] $filters
+   *   (optional) An array of FilterInterface objects to apply to this asset.
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown if a non-string is provided as content.
+   */
+  public function __construct(AssetMetadataInterface $metadata, $content, $filters = array()) {
+    if (!is_string($content) || empty($content)) {
+      throw new \InvalidArgumentException('StringAsset requires a non-empty string for its content.');
+    }
+
+    $this->id = hash('sha256', $content);
+    $this->setContent($content);
+
+    parent::__construct($metadata, $filters);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function id() {
+    return $this->id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setLastModified($last_modified) {
+    $this->lastModified = $last_modified;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLastModified() {
+    return $this->lastModified;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  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 a602ab9..0746b8d 100644
--- a/core/modules/block/lib/Drupal/block/BlockBase.php
+++ b/core/modules/block/lib/Drupal/block/BlockBase.php
@@ -12,6 +12,7 @@
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Asset\Factory\AssetCollector;
 
 /**
  * Defines a base block implementation that most blocks plugins will extend.
@@ -179,5 +180,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 047efd9..82943d8 100644
--- a/core/modules/block/lib/Drupal/block/BlockPluginInterface.php
+++ b/core/modules/block/lib/Drupal/block/BlockPluginInterface.php
@@ -11,6 +11,7 @@
 use Drupal\Component\Plugin\ConfigurablePluginInterface;
 use Drupal\Core\Plugin\PluginFormInterface;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Asset\Factory\AssetCollector;
 
 /**
  * Defines the required interface for all block plugins.
@@ -126,4 +127,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/AssetAggregateTest.php b/core/tests/Drupal/Tests/Core/Asset/Aggregate/AssetAggregateTest.php
new file mode 100644
index 0000000..51eaea6
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/Aggregate/AssetAggregateTest.php
@@ -0,0 +1,325 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\Aggregate\AssetAggregateTest.
+ */
+
+namespace Drupal\Tests\Core\Asset\Aggregate;
+
+use Drupal\Core\Asset\Collection\AssetCollectionBasicInterface;
+use Drupal\Core\Asset\Exception\AssetTypeMismatchException;
+use Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException;
+use Drupal\Tests\Core\Asset\AssetUnitTest;
+use Drupal\Tests\Core\Asset\Collection\BasicAssetCollectionTest;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Asset\Aggregate\AssetAggregate
+ * @group Asset
+ */
+class AssetAggregateTest extends AssetUnitTest {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Asset aggregate tests',
+      'description' => 'Unit tests on AssetAggregate',
+      'group' => 'Asset',
+    );
+  }
+
+  /**
+   * Generates a AssetAggregate mock with three leaf assets.
+   */
+  public function getThreeLeafAggregate() {
+    $aggregate = $this->getAggregate();
+    $nested_aggregate = $this->getAggregate();
+
+    foreach (array('foo', 'bar', 'baz') as $var) {
+      $$var = $this->createStubFileAsset('css', $var);
+    }
+
+    $nested_aggregate->add($foo);
+    $nested_aggregate->add($bar);
+    $aggregate->add($nested_aggregate);
+    $aggregate->add($baz);
+
+    return array($aggregate, $foo, $bar, $baz, $nested_aggregate);
+  }
+
+  /**
+   * Returns an AssetAggregate, the base collection type for this unit test.
+   *
+   * @return AssetCollectionBasicInterface
+   */
+  public function getCollection() {
+    return $this->getAggregate();
+  }
+
+  public function testGetAssetType() {
+    $mockmeta = $this->getMock('Drupal\Core\Asset\Metadata\AssetMetadataBag', array(), array(), '', FALSE);
+    $mockmeta->expects($this->once())
+      ->method('getType')
+      ->will($this->returnValue('unicorns'));
+    $aggregate = $this->getMockForAbstractClass('Drupal\Core\Asset\Aggregate\AssetAggregate', array($mockmeta));
+
+    $this->assertEquals('unicorns', $aggregate->getAssetType());
+  }
+
+  public function testGetMetadata() {
+    $mockmeta = $this->createStubAssetMetadata();
+    $aggregate = $this->getMockForAbstractClass('Drupal\Core\Asset\Aggregate\AssetAggregate', array($mockmeta));
+
+    $this->assertSame($mockmeta, $aggregate->getMetadata());
+  }
+
+  /**
+   * This uses PHPUnit's reflection-based assertions rather than assertContains
+   * so that this test can honestly sit at the root of the test method
+   * dependency tree.
+   *
+   * @covers ::add
+   */
+  public function testAdd() {
+    $aggregate = $this->getAggregate();
+    $asset = $this->createStubFileAsset();
+    $this->assertSame($aggregate, $aggregate->add($asset));
+
+    $this->assertAttributeContains($asset, 'assetStorage', $aggregate);
+    $this->assertAttributeContains($asset, 'assetIdMap', $aggregate);
+
+    // Nesting: add an aggregate to the first aggregate.
+    $nested_aggregate = $this->getAggregate();
+    $aggregate->add($nested_aggregate);
+
+    $this->assertAttributeContains($nested_aggregate, 'assetStorage', $aggregate);
+    $this->assertAttributeContains($nested_aggregate, 'assetIdMap', $aggregate);
+    $this->assertAttributeContains($nested_aggregate, 'nestedStorage', $aggregate);
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::ensureCorrectType
+   * @expectedException \Drupal\Core\Asset\Exception\AssetTypeMismatchException
+   */
+  public function testAddEnsureCorrectType() {
+    $aggregate = $this->getAggregate();
+    $aggregate->add($this->createStubFileAsset('js'));
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::each
+   * @covers ::getIterator
+   * @covers \Drupal\Core\Asset\Collection\Iterator\RecursiveBasicCollectionIterator
+   */
+  public function testEach() {
+    list($aggregate, $foo, $bar, $baz, $nested_aggregate) = $this->getThreeLeafAggregate();
+
+    $contained = array();
+    foreach ($aggregate->each() as $leaf) {
+      $contained[] = $leaf;
+    }
+    $this->assertEquals(array($nested_aggregate, $foo, $bar, $baz), $contained);
+  }
+
+  /**
+   * @depends testAdd
+   * @depends testEach
+   * @covers ::__construct
+   */
+  public function testCreateWithAssets() {
+    $asset1 = $this->createStubFileAsset();
+    $asset2 = $this->createStubFileAsset();
+    $meta = $this->createStubAssetMetadata();
+    $collection = $this->getMockForAbstractClass('Drupal\Core\Asset\Aggregate\AssetAggregate', array($meta, array($asset1, $asset2)));
+
+    $this->assertContains($asset1, $collection);
+    $this->assertContains($asset2, $collection);
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::id
+   * @covers ::calculateId
+   */
+  public function testId() {
+    // Simple case - test with one contained asset first.
+    $aggregate = $this->getAggregate();
+    $asset1 = $this->createStubFileAsset();
+    $aggregate->add($asset1);
+
+    $this->assertEquals(hash('sha256', $asset1->id()), $aggregate->id());
+
+    // Now use two contained assets, one nested in another aggregate.
+    $aggregate = $this->getAggregate();
+    $aggregate->add($asset1);
+
+    $aggregate2 = $this->getAggregate();
+    $asset2 = $this->createStubFileAsset();
+    $aggregate2->add($asset2);
+
+    $aggregate->add($aggregate2);
+
+    // The aggregate only uses leaf, non-aggregate assets to determine its id.
+    $this->assertEquals(hash('sha256', $asset1->id() . $asset2->id()), $aggregate->id());
+  }
+
+  public function testIsPreprocessable() {
+    $this->assertTrue($this->getAggregate()->isPreprocessable());
+  }
+
+  /**
+   * @depends testEach
+   * @covers ::removeLeaf
+   * @expectedException \OutOfBoundsException
+   */
+  public function testRemoveNonexistentNeedle() {
+    list($aggregate) = $this->getThreeLeafAggregate();
+    // Nonexistent leaf removal returns FALSE in graceful mode
+    $this->assertFalse($aggregate->removeLeaf($this->createStubFileAsset(), TRUE));
+
+    // In non-graceful mode, an exception is thrown.
+    $aggregate->removeLeaf($this->createStubFileAsset());
+  }
+
+  /**
+   * @covers ::removeLeaf
+   * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException
+   */
+  public function testRemoveLeafVanillaAsseticAsset() {
+    $aggregate = $this->getAggregate();
+    $vanilla = $this->getMock('\Assetic\Asset\BaseAsset', array(), array(), '', FALSE);
+    $aggregate->removeLeaf($vanilla);
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::ensureCorrectType
+   * @expectedException \Drupal\Core\Asset\Exception\AssetTypeMismatchException
+   */
+  public function testReplaceLeafEnsureCorrectType() {
+    $aggregate = $this->getAggregate();
+    $asset1 = $this->createStubFileAsset();
+    $aggregate->add($asset1);
+
+    $asset2 = $this->createStubFileAsset('js');
+    $aggregate->replaceLeaf($asset1, $asset2);
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::ensureCorrectType
+   * @expectedException \Drupal\Core\Asset\Exception\AssetTypeMismatchException
+   */
+  public function testReplaceEnsureCorrectType() {
+    $aggregate = $this->getAggregate();
+    $asset1 = $this->createStubFileAsset();
+    $aggregate->add($asset1);
+
+    $asset2 = $this->createStubFileAsset('js');
+    $aggregate->replace($asset1, $asset2);
+  }
+
+  /**
+   * @depends testEach
+   * @covers ::replaceLeaf
+   * @expectedException \OutOfBoundsException
+   */
+  public function testReplaceLeafNonexistentNeedle() {
+    list($aggregate) = $this->getThreeLeafAggregate();
+    // Nonexistent leaf replacement returns FALSE in graceful mode
+    $qux = $this->createStubFileAsset();
+    $this->assertFalse($aggregate->replaceLeaf($this->createStubFileAsset(), $qux, TRUE));
+    $this->assertNotContains($qux, $aggregate);
+
+    // In non-graceful mode, an exception is thrown.
+    $aggregate->replaceLeaf($this->createStubFileAsset(), $qux);
+  }
+
+  /**
+   * @depends testEach
+   * @covers ::replaceLeaf
+   * @expectedException \LogicException
+   */
+  public function testReplaceLeafWithAlreadyPresentAsset() {
+    list($aggregate, $foo) = $this->getThreeLeafAggregate();
+    $aggregate->replaceLeaf($this->createStubFileAsset(), $foo);
+  }
+
+  /**
+   * @depends testAdd
+   * @depends testReplaceLeafWithAlreadyPresentAsset
+   * @covers ::replace
+   * @expectedException \LogicException
+   *
+   * This fails on the same check that testReplaceLeafWithAlreadyPresentAsset,
+   * but it is demonstrated as its own test for clarity.
+   */
+  public function testReplaceLeafWithSelf() {
+    list($aggregate, $foo) = $this->getThreeLeafAggregate();
+    $aggregate->replaceLeaf($foo, $foo);
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::replaceLeaf
+   */
+  public function testReplaceLeafVanillaAsseticAsset() {
+    $aggregate = $this->getAggregate();
+    $vanilla = $this->getMock('\Assetic\Asset\BaseAsset', array(), array(), '', FALSE);
+    $drupally = $this->createStubFileAsset();
+
+    try {
+      $aggregate->replaceLeaf($vanilla, $drupally);
+      $this->fail('AssetAggregate::removeLeaf() did not throw an UnsupportedAsseticBehaviorException when provided a vanilla asset leaf.');
+    } catch (UnsupportedAsseticBehaviorException $e) {}
+
+    try {
+      $aggregate->replaceLeaf($vanilla, $vanilla);
+      $this->fail('AssetAggregate::removeLeaf() did not throw an UnsupportedAsseticBehaviorException when provided a vanilla asset leaf.');
+    } catch (UnsupportedAsseticBehaviorException $e) {}
+
+    try {
+      $aggregate->replaceLeaf($drupally, $vanilla);
+      $this->fail('AssetAggregate::removeLeaf() did not throw an UnsupportedAsseticBehaviorException when provided a vanilla asset leaf.');
+    } catch (UnsupportedAsseticBehaviorException $e) {}
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::load
+   */
+  public function testLoad() {
+    $this->fail();
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::dump
+   */
+  public function testDump() {
+    $this->fail();
+  }
+
+  /**
+   * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException
+   */
+  public function testGetVars() {
+    $this->getAggregate()->getVars();
+  }
+
+  /**
+   * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException
+   */
+  public function testSetValues() {
+    $this->getAggregate()->setValues(array());
+  }
+
+  /**
+   * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException
+   */
+  public function testGetValues() {
+    $this->getAggregate()->getValues();
+  }
+}
+
diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetLibraryRepositoryTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetLibraryRepositoryTest.php
new file mode 100644
index 0000000..657e89f
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/AssetLibraryRepositoryTest.php
@@ -0,0 +1,253 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\AssetLibraryRepositoryTest.
+ */
+
+namespace Drupal\Tests\Core\Asset;
+
+if (!defined('CSS_AGGREGATE_THEME')) {
+  define('CSS_AGGREGATE_THEME', 100);
+}
+
+if (!defined('CSS_AGGREGATE_DEFAULT')) {
+  define('CSS_AGGREGATE_DEFAULT', 0);
+}
+
+if (!defined('JS_LIBRARY')) {
+  define('JS_LIBRARY', -100);
+}
+
+if (!defined('JS_DEFAULT')) {
+  define('JS_DEFAULT', 0);
+}
+
+if (!defined('JS_THEME')) {
+  define('JS_THEME', 100);
+}
+
+use Drupal\Core\Asset\AssetLibraryRepository;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Asset\AssetLibraryRepository
+ * @group Asset
+ */
+class AssetLibraryRepositoryTest extends AssetUnitTest {
+
+  /**
+   * @var AssetLibraryRepository
+   */
+  protected $repository;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Asset library repository test',
+      'description' => 'Exercises methods on AssetLibraryRepository.',
+      'group' => 'Asset',
+    );
+  }
+
+  public function createAssetLibraryRepository() {
+    $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
+    $module_handler->expects($this->any())
+      ->method('getImplementations')
+      ->with('library_info')
+      ->will($this->returnValue(array('stub1', 'stub2')));
+
+    $factory = $this->getMock('Drupal\Core\Asset\Factory\AssetLibraryFactory', array(), array($module_handler));
+    return new AssetLibraryRepository($factory);
+  }
+
+  /**
+   * @covers ::__construct
+   * @covers ::set
+   */
+  public function testSet() {
+    $repository = $this->createAssetLibraryRepository();
+    $library = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary');
+    $repository->set('foo0_qux/bar0.baz', $library);
+
+    $this->assertAttributeContains($library, 'libraries', $repository);
+  }
+
+  /**
+   * @covers ::set
+   * @expectedException \InvalidArgumentException
+   */
+  public function testSetNoSlash() {
+    $repository = $this->createAssetLibraryRepository();
+    $library = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary');
+
+    $repository->set('foo0_quxbar0.baz', $library);
+  }
+
+  /**
+   * @covers ::set
+   * @expectedException \InvalidArgumentException
+   */
+  public function testSetTooManySlashes() {
+    $repository = $this->createAssetLibraryRepository();
+    $library = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary');
+
+    $repository->set('foo0_qux//bar0.baz', $library);
+  }
+
+  /**
+   * @covers ::set
+   * @expectedException \InvalidArgumentException
+   */
+  public function testSetInvalidKeyChars() {
+    $repository = $this->createAssetLibraryRepository();
+    $library = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary');
+
+    $repository->set("\$∫≤:ˆ\"'\n\t\r", $library);
+  }
+
+  /**
+   * @depends testSet
+   * @covers ::has
+   */
+  public function testHas() {
+    $repository = $this->createAssetLibraryRepository();
+    $library = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary');
+
+    $this->assertFalse($repository->has('foo/bar'));
+
+    $repository->set('foo/bar', $library);
+    $this->assertTrue($repository->has('foo/bar'));
+  }
+
+  /**
+   * @depends testSet
+   * @covers ::getNames
+   */
+  public function testGetNames() {
+    $repository = $this->createAssetLibraryRepository();
+    $library = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary');
+
+    $repository->set('foo/bar', $library);
+    $repository->set('baz/bing', $library);
+
+    $this->assertEquals(array('foo/bar', 'baz/bing'), $repository->getNames());
+  }
+
+  /**
+   * @depends testSet
+   * @covers ::get
+   */
+  public function testGet() {
+    $library = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary');
+    $factory = $this->getMock('Drupal\Core\Asset\Factory\AssetLibraryFactory', array(), array(), '', FALSE);
+    $factory->expects($this->once())
+      ->method('getLibrary')
+      ->with($this->equalTo('foo/bar'))
+      ->will($this->returnValue($library));
+
+    $repository = new AssetLibraryRepository($factory);
+    $this->assertSame($library, $repository->get('foo/bar'));
+    // Do it twice, for cache hit coverage.
+    $this->assertSame($library, $repository->get('foo/bar'));
+  }
+
+  /**
+   * @depends testSet
+   * @covers ::get
+   * @expectedException \OutOfBoundsException
+   */
+  public function testGetMissing() {
+    $repository = $this->createAssetLibraryRepository();
+    $repository->get('foo/bar');
+  }
+
+  /**
+   * @depends testSet
+   * @covers ::clear
+   */
+  public function testClear() {
+    $repository = $this->createAssetLibraryRepository();
+    $library = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary');
+
+    $repository->set('foo/bar', $library);
+    $this->assertAttributeContains($library, 'libraries', $repository);
+
+    $repository->clear();
+
+    $this->setExpectedException('\OutOfBoundsException');
+    $repository->get('foo/bar');
+  }
+
+  /**
+   * @depends testSet
+   * @covers ::resolveDependencies
+   */
+  public function testResolveDependencies() {
+    $repository = $this->createAssetLibraryRepository();
+
+    $compatible_dep = $this->createStubFileAsset();
+    $incompatible_dep = $this->createStubFileAsset('js');
+    $lib_dep = $this->createStubFileAsset();
+
+    $main_asset = $this->getMock('Drupal\Core\Asset\FileAsset', array(), array(), '', FALSE);
+    $main_asset->expects($this->exactly(2))
+      ->method('getAssetType')
+      ->will($this->returnValue('css'));
+    $main_asset->expects($this->exactly(2))
+      ->method('hasDependencies')
+      ->will($this->returnValue(TRUE));
+    $main_asset->expects($this->exactly(2))
+      ->method('getDependencyInfo')
+      ->will($this->returnValue(array('foo/bar', 'foo/baz')));
+    $main_asset->expects($this->once())
+      ->method('after')->with($compatible_dep);
+
+    $library1 = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary');
+    $library1->expects($this->once())
+      ->method('hasDependencies')
+      ->will($this->returnValue(TRUE));
+    $library1->expects($this->once())
+      ->method('getDependencyInfo')
+      ->will($this->returnValue(array('foo/baz', 'qux/bing')));
+
+    $it = new \ArrayIterator(array($compatible_dep, $incompatible_dep));
+
+    $library1->expects($this->any())
+      ->method('getIterator')
+      ->will($this->returnValue($it));
+
+    $library2 = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary');
+    $library2->expects($this->once())
+      ->method('getIterator')
+      ->will($this->returnValue(new \ArrayIterator(array())));
+    // Never to ensure resolution is non-recursive
+    $library2->expects($this->never())
+      ->method('hasDependencies');
+
+    $library3 = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary');
+    // Never because !$library1 instanceof RelativePositionInterface
+    $library3->expects($this->never())
+      ->method('getIterator')
+      ->will($this->returnValue(new \ArrayIterator(array($lib_dep))));
+    // Never to ensure resolution is non-recursive
+    $library3->expects($this->never())
+      ->method('hasDependencies')
+      ->will($this->returnValue(array('qux/quark')));
+
+
+    $repository->set('foo/bar', $library1);
+    $repository->set('foo/baz', $library2);
+    $repository->set('qux/bing', $library3);
+
+    // Ensure no auto-attach when the second param turns it off.
+    $this->assertEquals(array($library1, $library2), $repository->resolveDependencies($main_asset, FALSE));
+
+    // Now, let it auto-attach.
+    $this->assertEquals(array($library1, $library2), $repository->resolveDependencies($main_asset));
+    // The correctness of $main_asset's predecessor data is guaranteed by the
+    // method counts on the mock; no direct validation is necessary.
+
+    // This ensures that dependency resolution is non-recursive.
+    $this->assertEquals(array($library2, $library3), $repository->resolveDependencies($library1));
+  }
+}
+
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..6a06499
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/AssetUnitTest.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\AssetUnitTest.
+ */
+
+namespace Drupal\Tests\Core\Asset;
+
+use Drupal\Core\Asset\Aggregate\AssetAggregate;
+use Drupal\Core\Asset\FileAsset;
+use Drupal\Core\Asset\Metadata\AssetMetadataBag;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Provides base standard fixtures and mocks for Asset tests.
+ */
+abstract class AssetUnitTest extends UnitTestCase {
+
+  /**
+   * Creates a mock file asset.
+   *
+   * The mock will respond only to getAssetType() (with the provided type) and
+   * id(), with a randomly generated name.
+   *
+   * @param string $type
+   *   'css' or 'js'. Defaults to 'css' if not given.
+   *
+   * @param string $id
+   *   A string id for the asset, to return from AssetInterface::id(). Defaults
+   *   to a random string if not given.
+   *
+   * @return FileAsset
+   */
+  public function createStubFileAsset($type = 'css', $id = '') {
+    $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($id ?: $this->randomName()));
+
+    return $asset;
+  }
+
+  /**
+   * Creates an asset metadata stub with basic values.
+   *
+   * @param string $type
+   * @param array $values
+   *
+   * @return AssetMetadataBag
+   */
+  public function createStubAssetMetadata($type = 'css', $values = array()) {
+    $stub = $this->getMockBuilder('Drupal\Core\Asset\Metadata\AssetMetadataBag')
+      ->setConstructorArgs(array($type, $values))
+      ->getMock();
+
+    $stub->expects($this->any())
+      ->method('getType')
+      ->will($this->returnValue($type));
+
+    return $stub;
+  }
+
+
+  /**
+   * Generates a simple AssetAggregate mock.
+   *
+   * @param array $defaults
+   *   Defaults to inject into the aggregate's metadata bag.
+   *
+   * @return AssetAggregate
+   */
+  public function getAggregate($defaults = array()) {
+    $mockmeta = $this->createStubAssetMetadata();
+    return $this->getMockForAbstractClass('Drupal\Core\Asset\Aggregate\AssetAggregate', array($mockmeta));
+  }
+
+}
\ 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..7f2ee4e
--- /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..aa7187e
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/BaseAssetTest.php
@@ -0,0 +1,245 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\BaseAssetTest.
+ */
+
+namespace Drupal\Tests\Core\Asset;
+use Drupal\Core\Asset\BaseAsset;
+
+/**
+ * @coversDefaultClass \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 array $defaults
+   *
+   * @return BaseAsset
+   */
+  public function createBaseAsset($defaults = array()) {
+    $mockmeta = $this->createStubAssetMetadata(NULL, $defaults);
+    return $this->getMockForAbstractClass('Drupal\Core\Asset\BaseAsset', array($mockmeta));
+  }
+
+  public function testGetMetadata() {
+    $mockmeta = $this->createStubAssetMetadata();
+    $asset = $this->getMockForAbstractClass('Drupal\Core\Asset\BaseAsset', array($mockmeta));
+
+    $this->assertSame($mockmeta, $asset->getMetadata());
+  }
+
+  public function testGetAssetType() {
+    $mockmeta = $this->getMock('Drupal\Core\Asset\Metadata\AssetMetadataBag', array(), array(), '', FALSE);
+    $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', array(), array(), '', FALSE);
+    $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());
+  }
+
+  /**
+   * @covers ::addDependency
+   */
+  public function testAddDependency() {
+    $asset = $this->createBaseAsset();
+
+    $this->assertSame($asset, $asset->addDependency('foo/bar'));
+    $this->assertAttributeContains('foo/bar', 'dependencies', $asset);
+
+    $invalid = array('foo', 'foo//bar', 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) {}
+  }
+
+  /**
+   * @depends testAddDependency
+   * @covers ::hasDependencies
+   */
+  public function testHasDependencies() {
+    $asset = $this->createBaseAsset();
+    $this->assertFalse($asset->hasDependencies());
+
+    $asset->addDependency('foo/bar');
+    $this->assertTrue($asset->hasDependencies());
+  }
+
+  /**
+   * @depends testAddDependency
+   * @covers ::getDependencyInfo
+   */
+  public function testGetDependencyInfo() {
+    $asset = $this->createBaseAsset();
+    $this->assertEmpty($asset->getDependencyInfo());
+
+    $asset->addDependency('foo/bar');
+    $this->assertEquals(array('foo/bar'), $asset->getDependencyInfo());
+  }
+
+  /**
+   * @depends testAddDependency
+   * @depends testHasDependencies
+   * @covers ::clearDependencies
+   */
+  public function testClearDependencies() {
+    $asset = $this->createBaseAsset();
+    $asset->addDependency('foo/bar');
+
+    $this->assertSame($asset, $asset->clearDependencies());
+    $this->assertFalse($asset->hasDependencies());
+  }
+
+  /**
+   * @covers ::after
+   */
+  public function testAfter() {
+    $asset = $this->createBaseAsset();
+    $dep = $this->createBaseAsset();
+
+    $this->assertSame($asset, $asset->after('foo'));
+    $this->assertSame($asset, $asset->after($dep));
+
+    $this->assertAttributeContains($dep, 'predecessors', $asset);
+
+    $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) {}
+  }
+
+  /**
+   * @depends testAfter
+   * @covers ::hasPredecessors
+   */
+  public function testHasPredecessors() {
+    $asset = $this->createBaseAsset();
+    $this->assertFalse($asset->hasPredecessors());
+
+    $asset->after('foo');
+    $this->assertTrue($asset->hasPredecessors());
+  }
+
+  /**
+   * @depends testAfter
+   * @covers ::getPredecessors
+   */
+  public function testGetPredecessors() {
+    $asset = $this->createBaseAsset();
+    $this->assertEmpty($asset->getPredecessors());
+
+    $asset->after('foo');
+    $this->assertEquals(array('foo'), $asset->getPredecessors());
+  }
+
+  /**
+   * @depends testAfter
+   * @depends testHasPredecessors
+   * @covers ::clearPredecessors
+   */
+  public function testClearPredecessors() {
+    $asset = $this->createBaseAsset();
+    $asset->after('foo');
+
+    $this->assertSame($asset, $asset->clearPredecessors());
+    $this->assertFalse($asset->hasPredecessors());
+  }
+
+  /**
+   * @covers ::before
+   */
+  public function testBefore() {
+    $asset = $this->createBaseAsset();
+    $dep = $this->createBaseAsset();
+
+    $this->assertSame($asset, $asset->before('foo'));
+    $this->assertSame($asset, $asset->before($dep));
+
+    $this->assertAttributeContains($dep, 'successors', $asset);
+
+    $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) {}
+  }
+
+  /**
+   * @depends testBefore
+   * @covers ::hasSuccessors
+   */
+  public function testHasSuccessors() {
+    $asset = $this->createBaseAsset();
+    $this->assertFalse($asset->hasSuccessors());
+
+    $asset->before('foo');
+    $this->assertTrue($asset->hasSuccessors());
+  }
+
+  /**
+   * @depends testBefore
+   * @covers ::getSuccessors
+   */
+  public function testGetSuccessors() {
+    $asset = $this->createBaseAsset();
+    $this->assertEmpty($asset->getSuccessors());
+
+    $asset->before('foo');
+    $this->assertEquals(array('foo'), $asset->getSuccessors());
+  }
+
+   /**
+   * @depends testBefore
+   * @covers ::clearSuccessors
+   */
+  public function testClearSuccessors() {
+    $asset = $this->createBaseAsset();
+    $asset->before('foo');
+
+    $this->assertSame($asset, $asset->clearSuccessors());
+    $this->assertFalse($asset->hasSuccessors());
+  }
+
+  public function testClone() {
+    $mockmeta = $this->createStubAssetMetadata();
+    $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..06e6d0a
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/Collection/AssetCollectionTest.php
@@ -0,0 +1,514 @@
+<?php
+/**
+ * @file
+ * Contains Drupal\Tests\Core\Asset\AssetCollectionTest.
+ */
+
+namespace Drupal\Tests\Core\Asset\Collection;
+
+use Drupal\Core\Asset\Collection\AssetCollection;
+use Drupal\Core\Asset\Collection\AssetCollectionBasicInterface;
+use Drupal\Core\Asset\Exception\FrozenObjectException;
+use Drupal\Tests\Core\Asset\AssetUnitTest;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Asset\Collection\AssetCollection
+ * @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();
+  }
+
+  /**
+   * Returns an AssetCollection, the base collection type for this unit test.
+   *
+   * @return AssetCollectionBasicInterface
+   */
+  public function getCollection() {
+    return new AssetCollection();
+  }
+
+  /**
+   * @covers ::add
+   */
+  public function testAdd() {
+    $asset1 = $this->createStubFileAsset();
+    $asset2 = $this->createStubFileAsset();
+
+    // test fluency
+    $this->assertSame($this->collection, $this->collection->add($asset1));
+    $this->assertSame($this->collection, $this->collection->add($asset2));
+
+    $this->assertContains($asset1, $this->collection);
+    $this->assertContains($asset2, $this->collection);
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::contains
+   */
+  public function testContains() {
+    $asset = $this->createStubFileAsset();
+    $this->collection->add($asset);
+    $this->assertTrue($this->collection->contains($asset));
+  }
+
+  /**
+   * @depends testAdd
+   * @depends testContains
+   * @covers ::__construct
+   */
+  public function testCreateWithAssets() {
+    $asset1 = $this->createStubFileAsset();
+    $asset2 = $this->createStubFileAsset();
+    $collection = new AssetCollection(array($asset1, $asset2));
+
+    $this->assertContains($asset1, $collection);
+    $this->assertContains($asset2, $collection);
+  }
+
+
+  /**
+   * @depends testAdd
+   * @covers ::getCss
+   */
+  public function testGetCss() {
+    $css = $this->createStubFileAsset('css');
+    $js = $this->createStubFileAsset('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);
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::getJs
+   */
+  public function testGetJs() {
+    $css = $this->createStubFileAsset('css');
+    $js = $this->createStubFileAsset('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);
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::all
+   */
+  public function testAll() {
+    $css = $this->createStubFileAsset('css');
+    $js = $this->createStubFileAsset('js');
+
+    $this->collection->add($css);
+    $this->collection->add($js);
+
+    $this->assertEquals(array($css->id() => $css, $js->id() => $js), $this->collection->all());
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::remove
+   */
+  public function testRemoveByAsset() {
+    $stub = $this->createStubFileAsset();
+
+    $this->collection->add($stub);
+    $this->collection->remove($stub);
+
+    $this->assertNotContains($stub, $this->collection);
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::remove
+   */
+  public function testRemoveById() {
+    $stub = $this->createStubFileAsset();
+
+    $this->collection->add($stub);
+    $this->collection->remove($stub->id());
+
+    $this->assertNotContains($stub, $this->collection);
+  }
+
+  /**
+   * @expectedException \OutOfBoundsException
+   * @covers ::remove
+   */
+  public function testRemoveNonexistentId() {
+    $this->assertFalse($this->collection->remove('foo', TRUE));
+    $this->collection->remove('foo');
+  }
+
+  /**
+   * @expectedException \OutOfBoundsException
+   * @covers ::remove
+   */
+  public function testRemoveNonexistentAsset() {
+    $stub = $this->createStubFileAsset();
+    $this->assertFalse($this->collection->remove($stub, TRUE));
+    $this->collection->remove($stub);
+  }
+
+  /**
+   * Tests that all methods that should be disabled by freezing the collection
+   * correctly trigger an exception.
+   *
+   * @covers ::freeze
+   * @covers ::isFrozen
+   * @covers ::attemptWrite
+   */
+  public function testExceptionOnWriteWhenFrozen() {
+    $stub = $this->createStubFileAsset();
+    $write_protected = array(
+      'add' => array($stub),
+      'remove' => array($stub),
+      'replace' => array($stub, $this->createStubFileAsset()),
+      'mergeCollection' => array($this->getMock('Drupal\Core\Asset\Collection\AssetCollection')),
+      'uksort' => array(function() {}),
+      'ksort' => array(),
+      'reverse' => array(),
+      'addUnresolvedLibrary' => array('foo/bar'),
+      'clearUnresolvedLibraries' => array(),
+      'resolveLibraries' => array($this->getMock('Drupal\Core\Asset\AssetLibraryRepository', array(), array(), '', FALSE)),
+    );
+
+    // No exception before freeze
+    list($method, $args) = each($write_protected);
+    call_user_func_array(array($this->collection, $method), $args);
+
+    $this->collection->freeze();
+    foreach ($write_protected as $method => $args) {
+      try {
+        call_user_func_array(array($this->collection, $method), $args);
+        $this->fail(sprintf('Was able to run write method "%s" on frozen AssetCollection', $method));
+      } catch (FrozenObjectException $e) {}
+    }
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::find
+   * @expectedException OutOfBoundsException
+   */
+  public function testFind() {
+    $metamock = $this->createStubAssetMetadata();
+
+    $asset = $this->getMock('Drupal\Core\Asset\FileAsset', array(), array($metamock, 'foo'));
+    $asset->expects($this->exactly(2)) // once on add, once on searching
+      ->method('id')
+      ->will($this->returnValue('foo'));
+
+    $this->collection->add($asset);
+    $this->assertSame($asset, $this->collection->find('foo'));
+
+    // Nonexistent asset
+    $this->assertFalse($this->collection->find('bar'));
+
+    // Nonexistent asset, non-graceful
+    $this->collection->find('bar', FALSE);
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::uksort
+   */
+  public function testUkSort() {
+    $stub1 = $this->createStubFileAsset();
+    $stub2 = $this->createStubFileAsset();
+    $stub3 = $this->createStubFileAsset();
+
+    $this->collection->add($stub1);
+    $this->collection->add($stub2);
+    $this->collection->add($stub3);
+
+    $assets = array(
+      $stub1->id() => $stub1,
+      $stub2->id() => $stub2,
+      $stub3->id() => $stub3,
+    );
+
+    $dummysort = function ($a, $b) {
+      return strnatcasecmp($a, $b);
+    };
+
+    $this->assertSame($this->collection, $this->collection->uksort($dummysort));
+    uksort($assets, $dummysort);
+    $this->assertEquals($assets, $this->collection->all());
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::ksort
+   */
+  public function testKsort() {
+    $stub1 = $this->createStubFileAsset();
+    $stub2 = $this->createStubFileAsset();
+    $stub3 = $this->createStubFileAsset();
+
+    $this->collection->add($stub1);
+    $this->collection->add($stub2);
+    $this->collection->add($stub3);
+
+    $assets = array(
+      $stub1->id() => $stub1,
+      $stub2->id() => $stub2,
+      $stub3->id() => $stub3,
+    );
+
+    $this->assertSame($this->collection, $this->collection->ksort());
+    ksort($assets);
+    $this->assertEquals($assets, $this->collection->all());
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::reverse
+   */
+  public function testReverse() {
+    $stub1 = $this->createStubFileAsset();
+    $stub2 = $this->createStubFileAsset();
+    $stub3 = $this->createStubFileAsset();
+
+    $this->collection->add($stub1);
+    $this->collection->add($stub2);
+    $this->collection->add($stub3);
+
+    $assets = array(
+      $stub3->id() => $stub3,
+      $stub2->id() => $stub2,
+      $stub1->id() => $stub1,
+    );
+
+    $this->assertSame($this->collection, $this->collection->reverse());
+    $this->assertEquals($assets, $this->collection->all());
+  }
+
+  /**
+   * @covers ::addUnresolvedLibrary
+   */
+  public function testAddUnresolvedLibrary() {
+    $this->assertSame($this->collection, $this->collection->addUnresolvedLibrary('foo/bar'));
+
+    $this->assertAttributeContains('foo/bar', 'libraries', $this->collection);
+  }
+
+  /**
+   * @depends testAddUnresolvedLibrary
+   * @covers ::hasUnresolvedLibraries
+   */
+  public function testHasUnresolvedLibraries() {
+    $this->assertFalse($this->collection->hasUnresolvedLibraries());
+
+    $this->collection->addUnresolvedLibrary('foo/bar');
+
+    $this->assertTrue($this->collection->hasUnresolvedLibraries());
+  }
+
+  /**
+   * @depends testAddUnresolvedLibrary
+   * @depends testHasUnresolvedLibraries
+   * @covers ::clearUnresolvedLibraries
+   */
+  public function testClearUnresolvedLibraries() {
+    $this->collection->addUnresolvedLibrary('foo/bar');
+    $this->assertSame($this->collection, $this->collection->clearUnresolvedLibraries());
+
+    $this->assertFalse($this->collection->hasUnresolvedLibraries());
+  }
+
+  /**
+   * @depends testAddUnresolvedLibrary
+   * @covers ::getUnresolvedLibraries
+   */
+  public function testGetUnresolvedLibraries() {
+    $this->collection->addUnresolvedLibrary('foo/bar');
+
+    $this->assertEquals(array('foo/bar'), $this->collection->getUnresolvedLibraries());
+  }
+
+  /**
+   * @depends testAdd
+   * @depends testContains
+   * @depends testAddUnresolvedLibrary
+   * @depends testClearUnresolvedLibraries
+   * @depends testGetUnresolvedLibraries
+   * @covers ::resolveLibraries
+   */
+  public function testResolveLibrariesDirectLibraries() {
+    $lib_asset1 = $this->getMockBuilder('Drupal\Core\Asset\AssetInterface')
+      ->disableOriginalConstructor()
+      ->setMethods(array('id'))
+      ->setMockClassName('lib_asset_mock1')
+      ->getMockForAbstractClass();
+    $lib_asset1->expects($this->any())
+      ->method('id')
+      ->will($this->returnValue($this->randomName()));
+
+    $lib_asset2 = $this->getMockBuilder('Drupal\Core\Asset\AssetInterface')
+      ->disableOriginalConstructor()
+      ->setMethods(array('id'))
+      ->setMockClassName('lib_asset_mock2')
+      ->getMockForAbstractClass();
+    $lib_asset2->expects($this->any())
+      ->method('id')
+      ->will($this->returnValue($this->randomName()));
+
+    $it1 = new \ArrayIterator(array($lib_asset1, $lib_asset2));
+    $lib1 = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary');
+    $lib1->expects($this->once())
+      ->method('getIterator')
+      ->will($this->returnValue($it1));
+
+    $repository = $this->getMock('Drupal\Core\Asset\AssetLibraryRepository', array(), array(), '', FALSE);
+    $repository->expects($this->once())
+      ->method('get')->with('foo/bar')
+      ->will($this->returnValue($lib1));
+
+    $this->collection->addUnresolvedLibrary('foo/bar');
+    $this->collection->resolveLibraries($repository);
+
+    $expected = array(
+      $lib_asset1->id() => $lib_asset1,
+      $lib_asset2->id() => $lib_asset2,
+    );
+    $this->assertEquals($expected, $this->collection->all());
+    $this->assertFalse($this->collection->hasUnresolvedLibraries());
+  }
+
+  /**
+   * @depends testAdd
+   * @depends testAll
+   * @depends testAddUnresolvedLibrary
+   * @depends testClearUnresolvedLibraries
+   * @depends testGetUnresolvedLibraries
+   * @covers ::resolveLibraries
+   */
+  public function testResolveLibrariesAgain() {
+    $coll_asset = $this->getMockBuilder('Drupal\Core\Asset\BaseAsset')
+      ->disableOriginalConstructor()
+      ->setMethods(array('id'))
+      ->setMockClassName('coll_asset')
+      ->getMockForAbstractClass();
+    $coll_asset->expects($this->any())
+      ->method('id')
+      ->will($this->returnValue($this->randomName()));
+
+    $direct_lib_asset = $this->getMockBuilder('Drupal\Core\Asset\BaseAsset')
+      ->disableOriginalConstructor()
+      ->setMethods(array('id'))
+      ->setMockClassName('direct_lib_asset')
+      ->getMockForAbstractClass();
+    $direct_lib_asset->expects($this->any())
+      ->method('id')
+      ->will($this->returnValue($this->randomName()));
+
+    $indirect_lib_asset = $this->getMockBuilder('Drupal\Core\Asset\BaseAsset')
+      ->disableOriginalConstructor()
+      ->setMethods(array('id'))
+      ->setMockClassName('indirect_lib_asset')
+      ->getMockForAbstractClass();
+    $indirect_lib_asset->expects($this->any())
+      ->method('id')
+      ->will($this->returnValue($this->randomName()));
+
+    $direct_lib = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary');
+    $direct_lib->expects($this->once())
+      ->method('getIterator')
+      ->will($this->returnValue(new \ArrayIterator(array($direct_lib_asset))));
+
+    $indirect_lib = $this->getMock('Drupal\Core\Asset\Collection\AssetLibrary');
+    $indirect_lib->expects($this->once())
+      ->method('getIterator')
+      ->will($this->returnValue(new \ArrayIterator(array($indirect_lib_asset))));
+
+    $repository = $this->getMock('Drupal\Core\Asset\AssetLibraryRepository', array(), array(), '', FALSE);
+    $repository->expects($this->at(0))
+      ->method('resolveDependencies')->with($coll_asset)
+      ->will($this->returnValue(array($direct_lib)));
+    $repository->expects($this->at(1))
+      ->method('resolveDependencies')->with($direct_lib_asset)
+      ->will($this->returnValue(array($indirect_lib)));
+    $repository->expects($this->at(2))
+      ->method('resolveDependencies')->with($indirect_lib_asset)
+      ->will($this->returnValue(array()));
+
+    $this->collection->add($coll_asset);
+    $this->assertSame($this->collection, $this->collection->resolveLibraries($repository));
+
+    $expected = array(
+      $coll_asset->id() => $coll_asset,
+      $direct_lib_asset->id() => $direct_lib_asset,
+      $indirect_lib_asset->id() => $indirect_lib_asset,
+    );
+
+    $this->assertEquals($expected, $this->collection->all());
+  }
+
+  /**
+   * @depends testAdd
+   * @depends testAddUnresolvedLibrary
+   * @depends testGetUnresolvedLibraries
+   * @covers ::mergeCollection
+   */
+  public function testMergeCollection() {
+    $coll2 = new AssetCollection();
+    $stub1 = $this->createStubFileAsset();
+    $stub2 = $this->createStubFileAsset();
+
+    $coll2->add($stub1);
+    $coll2->addUnresolvedLibrary('foo/bar');
+    // Assert same to check fluency
+    $this->assertSame($this->collection, $this->collection->mergeCollection($coll2));
+
+    $this->assertEquals(array('foo/bar'), $this->collection->getUnresolvedLibraries());
+    $this->assertContains($stub1, $this->collection);
+    $this->assertTrue($coll2->isFrozen());
+
+    $coll3 = new AssetCollection();
+    $coll3->add($stub1);
+    $coll3->add($stub2);
+    $coll3->addUnresolvedLibrary('foo/bar');
+    // Ensure no duplicates, and don't freeze merged bag
+    $this->collection->mergeCollection($coll3, FALSE);
+
+    $this->assertEquals(array('foo/bar'), $this->collection->getUnresolvedLibraries());
+    $contained = array(
+      $stub1->id() => $stub1,
+      $stub2->id() => $stub2,
+    );
+    $this->assertEquals($contained, $this->collection->all());
+    $this->assertFalse($coll3->isFrozen());
+  }
+}
+
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..6a36f5e
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/Collection/AssetLibraryTest.php
@@ -0,0 +1,139 @@
+<?php
+/**
+ * @file
+ * Contains Drupal\Tests\Core\Asset\AssetLibraryTest.
+ */
+
+namespace Drupal\Tests\Core\Asset\Collection;
+
+use Drupal\Core\Asset\Collection\AssetLibrary;
+use Drupal\Core\Asset\Exception\FrozenObjectException;
+use Drupal\Tests\Core\Asset\AssetUnitTest;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Asset\Collection\AssetLibrary
+ * @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');
+    return $library;
+  }
+
+  /**
+   * These simply don't merit individual tests.
+   *
+   * @covers ::setWebsite
+   * @covers ::getWebsite
+   * @covers ::setVersion
+   * @covers ::getVersion
+   * @covers ::setTitle
+   * @covers ::getTitle
+   */
+  public function testMetadataProps() {
+    $library = $this->getLibraryFixture();
+
+    $this->assertEquals('foo', $library->getTitle());
+    $this->assertEquals('1.2.3', $library->getVersion());
+    $this->assertEquals('http://foo.bar', $library->getWebsite());
+  }
+
+  /**
+   * @covers ::addDependency
+   */
+  public function testAddDependency() {
+    $library = $this->getLibraryFixture();
+
+    $this->assertSame($library, $library->addDependency('foo/bar'));
+    $this->assertAttributeContains('foo/bar', 'dependencies', $library);
+
+    $invalid = array('foo', 'foo//bar', 0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass);
+
+    try {
+      foreach ($invalid as $val) {
+        $library->addDependency($val, $val);
+        $this->fail('Was able to create an ordering relationship with an inappropriate value.');
+      }
+    } catch (\InvalidArgumentException $e) {}
+  }
+
+  /**
+   * @depends testAddDependency
+   * @covers ::hasDependencies
+   */
+  public function testHasDependencies() {
+    $library = $this->getLibraryFixture();
+    $this->assertFalse($library->hasDependencies());
+
+    $library->addDependency('foo/bar');
+    $this->assertTrue($library->hasDependencies());
+  }
+
+  /**
+   * @depends testAddDependency
+   * @covers ::getDependencyInfo
+   */
+  public function testGetDependencyInfo() {
+    $library = $this->getLibraryFixture();
+    $this->assertEmpty($library->getDependencyInfo());
+
+    $library->addDependency('foo/bar');
+    $this->assertEquals(array('foo/bar'), $library->getDependencyInfo());
+  }
+
+  /**
+   * @depends testAddDependency
+   * @depends testHasDependencies
+   * @covers ::clearDependencies
+   */
+  public function testClearDependencies() {
+    $library = $this->getLibraryFixture();
+    $library->addDependency('foo/bar');
+
+    $this->assertSame($library, $library->clearDependencies());
+    $this->assertFalse($library->hasDependencies());
+  }
+
+  /**
+   * Tests that all methods that should be disabled by freezing the collection
+   * correctly trigger an exception.
+   *
+   * @covers ::freeze
+   * @covers ::isFrozen
+   * @covers ::attemptWrite
+   */
+  public function testExceptionOnWriteWhenFrozen() {
+    $library = new AssetLibrary();
+    $write_protected = array(
+      'setTitle' => array('foo'),
+      'setVersion' => array('foo'),
+      'setWebsite' => array('foo'),
+      'addDependency' => array('foo/bar'),
+      'clearDependencies' => array(function() {}),
+    );
+
+    // No exception before freeze
+    list($method, $args) = each($write_protected);
+    call_user_func_array(array($library, $method), $args);
+
+    $library->freeze();
+    foreach ($write_protected as $method => $args) {
+      try {
+        call_user_func_array(array($library, $method), $args);
+        $this->fail(sprintf('Was able to run write method "%s" on frozen AssetLibrary', $method));
+      } catch (FrozenObjectException $e) {}
+    }
+  }
+}
diff --git a/core/tests/Drupal/Tests/Core/Asset/Collection/BasicAssetCollectionTest.php b/core/tests/Drupal/Tests/Core/Asset/Collection/BasicAssetCollectionTest.php
new file mode 100644
index 0000000..1eb7df8
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/Collection/BasicAssetCollectionTest.php
@@ -0,0 +1,466 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\Collection\BasicAssetCollectionTest.
+ */
+
+namespace Drupal\Tests\Core\Asset\Collection;
+
+use Drupal\Core\Asset\Collection\AssetCollectionBasicInterface;
+use Drupal\Core\Asset\Collection\BasicAssetCollection;
+use Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException;
+use Drupal\Tests\Core\Asset\AssetUnitTest;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Asset\Collection\BasicAssetCollection
+ * @group Asset
+ */
+class BasicAssetCollectionTest extends AssetUnitTest {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'BasicAssetCollection unit tests',
+      'description' => 'Unit tests for BasicAssetCollection',
+      'group' => 'Asset',
+    );
+  }
+
+  /**
+   * Generates a simple BasicAssetCollection mock.
+   *
+   * @return BasicAssetCollection
+   */
+  public function getBasicCollection() {
+    return $this->getMockForAbstractClass('Drupal\Core\Asset\Collection\BasicAssetCollection');
+  }
+
+  /**
+   * Method to return the appropriate collection type for the current test.
+   *
+   * If demonstrating adherence to Liskov is desired, this test class can be
+   * extended and this method swapped out to provide the correct
+   * AssetCollectionBasicInterface object for testing.
+   *
+   * @return AssetCollectionBasicInterface
+   */
+  public function getCollection() {
+    return $this->getBasicCollection();
+  }
+
+  /**
+   * Generates a AssetAggregate mock with three leaf assets.
+   */
+  public function getThreeLeafBasicCollection() {
+    $collection = $this->getCollection();
+    $nested_aggregate = $this->getAggregate();
+
+    foreach (array('foo', 'bar', 'baz') as $var) {
+      $$var = $this->createStubFileAsset('css', $var);
+    }
+
+    $nested_aggregate->add($foo);
+    $nested_aggregate->add($bar);
+    $collection->add($nested_aggregate);
+    $collection->add($baz);
+
+    return array($collection, $foo, $bar, $baz, $nested_aggregate);
+  }
+
+  /**
+   * This uses PHPUnit's reflection-based assertions rather than assertContains
+   * so that this test can honestly sit at the root of the test method
+   * dependency tree.
+   *
+   * @covers ::add
+   */
+  public function testAdd() {
+    $collection = $this->getCollection();
+    $asset = $this->createStubFileAsset();
+    $this->assertSame($collection, $collection->add($asset));
+
+    $this->assertAttributeContains($asset, 'assetStorage', $collection);
+    $this->assertAttributeContains($asset, 'assetIdMap', $collection);
+
+    // Nesting: add an aggregate to the first aggregate.
+    $nested_aggregate = $this->getAggregate();
+    $collection->add($nested_aggregate);
+
+    $this->assertAttributeContains($nested_aggregate, 'assetStorage', $collection);
+    $this->assertAttributeContains($nested_aggregate, 'assetIdMap', $collection);
+    $this->assertAttributeContains($nested_aggregate, 'nestedStorage', $collection);
+  }
+
+  /**
+   * @expectedException \Drupal\Core\Asset\Exception\UnsupportedAsseticBehaviorException
+   * @covers ::add
+   */
+  public function testVanillaAsseticAdd() {
+    $vanilla = $this->getMock('\Assetic\Asset\BaseAsset', array(), array(), '', FALSE);
+    $this->getCollection()->add($vanilla);
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::each
+   * @covers ::getIterator
+   * @covers \Drupal\Core\Asset\Collection\Iterator\RecursiveBasicCollectionIterator
+   */
+  public function testEach() {
+    list($collection, $foo, $bar, $baz, $nested_aggregate) = $this->getThreeLeafBasicCollection();
+
+    $contained = array();
+    foreach ($collection->each() as $leaf) {
+      $contained[] = $leaf;
+    }
+    $this->assertEquals(array($nested_aggregate, $foo, $bar, $baz), $contained);
+  }
+
+  /**
+   * @depends testAdd
+   * @depends testEach
+   * @covers ::__construct
+   */
+  public function testCreateWithAssets() {
+    $asset1 = $this->createStubFileAsset();
+    $asset2 = $this->createStubFileAsset();
+    $collection = $this->getMockForAbstractClass('Drupal\Core\Asset\Collection\BasicAssetCollection', array(array($asset1, $asset2)));
+
+    $this->assertContains($asset1, $collection);
+    $this->assertContains($asset2, $collection);
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::eachLeaf
+   * @covers \Drupal\Core\Asset\Collection\Iterator\RecursiveBasicCollectionIterator
+   */
+  public function testEachLeaf() {
+    list($collection, $foo, $bar, $baz) = $this->getThreeLeafBasicCollection();
+
+    $contained = array();
+    foreach ($collection->eachLeaf() as $leaf) {
+      $contained[] = $leaf;
+    }
+    $this->assertEquals(array($foo, $bar, $baz), $contained);
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::contains
+   */
+  public function testContains() {
+    $collection = $this->getCollection();
+    $asset = $this->createStubFileAsset();
+    $collection->add($asset);
+
+    $this->assertTrue($collection->contains($asset));
+
+    // Nesting: add an aggregate to the first aggregate.
+    $nested_aggregate = $this->getAggregate();
+    $nested_asset = $this->createStubFileAsset();
+
+    $nested_aggregate->add($nested_asset);
+    $collection->add($nested_aggregate);
+
+    $this->assertTrue($collection->contains($nested_asset));
+  }
+
+  /**
+   * @covers ::find
+   * @expectedException \OutOfBoundsException
+   */
+  public function testFind() {
+    $collection = $this->getCollection();
+
+    $asset = $this->createStubFileAsset();
+    $collection->add($asset);
+    $this->assertSame($asset, $collection->find($asset->id()));
+
+    $nested_aggregate = $this->getAggregate();
+    $nested_asset = $this->createStubFileAsset();
+
+    $nested_aggregate->add($nested_asset);
+    $collection->add($nested_aggregate);
+
+    $this->assertSame($nested_asset, $collection->find($nested_asset->id()));
+
+    // Nonexistent asset
+    $this->assertFalse($collection->find('bar'));
+
+    // Nonexistent asset, non-graceful
+    $collection->find('bar', FALSE);
+  }
+
+  /**
+   * @depends testAdd
+   * @covers ::all
+   */
+  public function testAll() {
+    $collection = $this->getCollection();
+
+    $asset1 = $this->createStubFileAsset();
+    $asset2 = $this->createStubFileAsset();
+    $collection->add($asset1);
+    $collection->add($asset2);
+
+    $output = array(
+      $asset1->id() => $asset1,
+      $asset2->id() => $asset2,
+    );
+
+    $this->assertEquals($output, $collection->all());
+
+    // Ensure that only top-level assets are returned.
+    $nested_aggregate = $this->getAggregate();
+    $nested_aggregate->add($this->createStubFileAsset());
+    $collection->add($nested_aggregate);
+
+    $output[$nested_aggregate->id()] = $nested_aggregate;
+    $this->assertEquals($output, $collection->all());
+  }
+
+  /**
+   * @depends testEach
+   * @covers ::remove
+   * @covers ::doRemove
+   */
+  public function testRemove() {
+    list($collection, $foo, $bar, $baz, $nested_aggregate) = $this->getThreeLeafBasicCollection();
+    $this->assertFalse($collection->remove('arglebargle', TRUE));
+    $this->assertTrue($collection->remove('foo'));
+
+    $this->assertNotContains($foo, $collection);
+    $this->assertContains($bar, $collection);
+    $this->assertContains($baz, $collection);
+
+    $this->assertTrue($collection->remove($bar));
+
+    $this->assertNotContains($bar, $collection);
+    $this->assertContains($baz, $collection);
+
+    $this->assertTrue($collection->remove($nested_aggregate));
+    $this->assertNotContains($nested_aggregate, $collection);
+  }
+
+  /**
+   * @depends testAdd
+   * @depends testRemove
+   * @covers ::count
+   */
+  public function testCount() {
+    $collection = $this->getCollection();
+    $this->assertCount(0, $collection);
+
+    $collection->add($this->getAggregate());
+    $this->assertCount(0, $collection);
+
+    $aggregate = $this->getAggregate();
+    $asset = $this->createStubFileAsset();
+    $aggregate->add($asset);
+    $collection->add($aggregate);
+    $this->assertCount(1, $collection);
+
+    $collection->remove($aggregate);
+    $this->assertCount(0, $collection);
+
+    $collection->add($asset);
+    $this->assertCount(1, $collection);
+
+    $collection->remove($asset);
+    $this->assertCount(0, $collection);
+  }
+
+  /**
+   * Tests that adding the same asset twice results in just one asset.
+   *
+   * @depends testAdd
+   * @depends testCount
+   * @covers ::add
+   */
+  public function testDoubleAdd() {
+    $collection = $this->getCollection();
+    $asset = $this->createStubFileAsset();
+
+    $collection->add($asset);
+
+    // Test by object identity
+    $collection->add($asset);
+    $this->assertCount(1, $collection);
+
+    // Test by id
+    $asset2 = $this->createStubFileAsset('css', $asset->id());
+
+    $collection->add($asset2);
+    $this->assertCount(1, $collection);
+  }
+
+  /**
+   * @depends testEach
+   * @covers ::remove
+   * @covers ::doRemove
+   * @expectedException \OutOfBoundsException
+   */
+  public function testRemoveNonexistentNeedle() {
+    list($collection) = $this->getThreeLeafBasicCollection();
+    // Nonexistent leaf removal returns FALSE in graceful mode
+    $this->assertFalse($collection->remove($this->createStubFileAsset(), TRUE));
+
+    // In non-graceful mode, an exception is thrown.
+    $collection->remove($this->createStubFileAsset());
+  }
+
+  /**
+   * @depends testEach
+   * @depends testEachLeaf
+   * @covers ::replace
+   * @covers ::doReplace
+   */
+  public function testReplace() {
+    list($collection, $foo, $bar, $baz, $nested_aggregate) = $this->getThreeLeafBasicCollection();
+    $qux = $this->createStubFileAsset('css', 'qux');
+
+    $this->assertFalse($collection->replace('arglebargle', $qux, TRUE));
+    $this->assertTrue($collection->replace('foo', $qux));
+
+    $this->assertContains($qux, $collection);
+    $this->assertNotContains($foo, $collection);
+
+    $contained = array();
+    foreach ($collection->eachLeaf() as $leaf) {
+      $contained[] = $leaf;
+    }
+    $this->assertEquals(array($qux, $bar, $baz), $contained);
+
+    $this->assertTrue($collection->replace($bar, $foo));
+
+    $this->assertContains($foo, $collection);
+    $this->assertNotContains($bar, $collection);
+
+    $contained = array();
+    foreach ($collection->eachLeaf() as $leaf) {
+      $contained[] = $leaf;
+    }
+    $this->assertEquals(array($qux, $foo, $baz), $contained);
+
+    $aggregate2 = $this->getAggregate();
+    $this->assertTrue($collection->replace($baz, $aggregate2));
+
+    $this->assertContains($aggregate2, $collection);
+    $this->assertNotContains($baz, $collection);
+
+    $contained = array();
+    foreach ($collection->eachLeaf() as $leaf) {
+      $contained[] = $leaf;
+    }
+    $this->assertEquals(array($qux, $foo), $contained);
+
+    $contained = array();
+    foreach ($collection->each() as $leaf) {
+      $contained[] = $leaf;
+    }
+    $this->assertEquals(array($nested_aggregate, $qux, $foo, $aggregate2), $contained);
+  }
+
+  /**
+   * @depends testEach
+   * @covers ::replace
+   * @covers ::doReplace
+   * @expectedException \OutOfBoundsException
+   */
+  public function testReplaceNonexistentNeedle() {
+    list($collection) = $this->getThreeLeafBasicCollection();
+    // Nonexistent leaf replacement returns FALSE in graceful mode
+    $qux = $this->createStubFileAsset();
+    $this->assertFalse($collection->replace($this->createStubFileAsset(), $qux, TRUE));
+    $this->assertNotContains($qux, $collection);
+
+    // In non-graceful mode, an exception is thrown.
+    $collection->replace($this->createStubFileAsset(), $qux);
+  }
+
+  /**
+   * @depends testEach
+   * @covers ::replace
+   * @expectedException \LogicException
+   */
+  public function testReplaceWithAlreadyPresentAsset() {
+    list($aggregate, $foo) = $this->getThreeLeafBasicCollection();
+    $aggregate->replace($this->createStubFileAsset(), $foo);
+  }
+
+  /**
+   * @depends testAdd
+   * @depends testReplaceWithAlreadyPresentAsset
+   * @covers ::replace
+   * @expectedException \LogicException
+   *
+   * This fails on the same check that testReplaceWithAlreadyPresentAsset,
+   * but it is demonstrated as its own test for clarity.
+   */
+  public function testReplaceWithSelf() {
+    list($collection, $foo) = $this->getThreeLeafBasicCollection();
+    $collection->replace($foo, $foo);
+  }
+
+  /**
+   * @depends testAdd
+   * @depends testRemove
+   * @covers ::isEmpty
+   */
+  public function testIsEmpty() {
+    $collection = $this->getCollection();
+    $this->assertTrue($collection->isEmpty());
+
+    // Collections containing only empty collections are considered empty.
+    $collection->add($this->getAggregate());
+    $this->assertTrue($collection->isEmpty());
+
+    $aggregate = $this->getAggregate();
+    $asset = $this->createStubFileAsset();
+    $aggregate->add($asset);
+    $collection->add($aggregate);
+    $this->assertFalse($collection->isEmpty());
+
+    $collection->remove($aggregate);
+    $this->assertTrue($collection->isEmpty());
+
+    $collection->add($asset);
+    $this->assertFalse($collection->isEmpty());
+
+    $collection->remove($asset);
+    $this->assertTrue($collection->isEmpty());
+  }
+
+  /**
+   * @covers ::remove
+   */
+  public function testRemoveInvalidNeedle() {
+    $collection = $this->getCollection();
+    $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass);
+
+    try {
+      foreach ($invalid as $val) {
+        $collection->remove($val);
+        $this->fail('BasicAssetCollection::remove() did not throw exception on invalid argument type for $needle.');
+      }
+    } catch (\InvalidArgumentException $e) {}
+  }
+
+  /**
+   * @covers ::replace
+   */
+  public function testReplaceInvalidNeedle() {
+    $collection = $this->getCollection();
+    $invalid = array(0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass);
+
+    try {
+      foreach ($invalid as $val) {
+        $collection->replace($val, $this->createStubFileAsset());
+        $this->fail('BasicAssetCollection::replace() did not throw exception on invalid argument type for $needle.');
+      }
+    } catch (\InvalidArgumentException $e) {}
+  }
+
+}
+
diff --git a/core/tests/Drupal/Tests/Core/Asset/ExternalAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/ExternalAssetTest.php
new file mode 100644
index 0000000..b4d2618
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/ExternalAssetTest.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\ExternalAssetTest.
+ */
+
+namespace Drupal\Tests\Core\Asset;
+
+use Drupal\Core\Asset\ExternalAsset;
+
+/**
+ * @group Asset
+ */
+class ExternalAssetTest extends AssetUnitTest {
+
+  const JQUERY = 'http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js';
+
+  public static function getInfo() {
+    return array(
+      'name' => 'File asset tests',
+      'description' => 'Unit tests for FileAsset',
+      'group' => 'Asset',
+    );
+  }
+
+  public function testInitialCreation() {
+    $meta = $this->createStubAssetMetadata();
+    $asset = new ExternalAsset($meta, self::JQUERY);
+
+    $this->assertEquals(self::JQUERY, $asset->id());
+  }
+
+  /**
+   * @expectedException \InvalidArgumentException
+   */
+  public function testCreateMalformedUrl() {
+    $meta = $this->createStubAssetMetadata();
+    new ExternalAsset($meta, __FILE__);
+  }
+
+  public function testGetLastModified() {
+    $meta = $this->createStubAssetMetadata();
+    $asset = new ExternalAsset($meta, self::JQUERY);
+
+    // TODO this throws an exception, but it should not. test fails till we fix.
+    $asset->getLastModified();
+  }
+
+  public function testLoad() {
+    $meta = $this->createStubAssetMetadata();
+    $asset = new ExternalAsset($meta, self::JQUERY);
+
+    // TODO this throws an exception, but it should not. test fails till we fix.
+    $asset->load();
+  }
+
+  public function testDump() {
+    $meta = $this->createStubAssetMetadata();
+    $asset = new ExternalAsset($meta, self::JQUERY);
+
+    // TODO this throws an exception, but it should not. test fails till we fix.
+    $asset->dump();
+  }
+}
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..8c01166
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/Factory/AssetCollectorTest.php
@@ -0,0 +1,291 @@
+<?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\AssetMetadataBag;
+use Drupal\Core\Asset\Metadata\DefaultAssetMetadataFactory;
+use Drupal\Tests\Core\Asset\AssetUnitTest;
+
+/**
+ * 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 constructor-injected params end up in the right place.
+   */
+  public function testConstructorInjection() {
+    $factory = $this->getMock('Drupal\Core\Asset\Metadata\DefaultAssetMetadataFactory');
+    $collection = $this->getMock('Drupal\Core\Asset\Collection\AssetCollection');
+
+    $collector = new AssetCollector($collection, $factory);
+
+    $this->assertAttributeSame($collection, 'collection', $collector);
+    $this->assertAttributeSame($factory, 'metadataFactory', $collector);
+  }
+
+  /**
+   * 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.');
+  }
+
+  public function testDefaultPropagation() {
+    // Test that defaults are correctly applied by the factory.
+    $meta = new AssetMetadataBag('css', array('every_page' => TRUE, 'group' => CSS_AGGREGATE_THEME));
+    $factory = $this->getMock('Drupal\Core\Asset\Metadata\DefaultAssetMetadataFactory');
+    $factory->expects($this->once())
+      ->method('createCssMetadata')
+      ->will($this->returnValue($meta));
+
+    $this->collector->setMetadataFactory($factory);
+    $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->createStubFileAsset('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->setMetadataFactory($this->getMock('Drupal\Core\Asset\Metadata\DefaultAssetMetadataFactory'));
+  }
+
+  /**
+   * @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 testChangeAndRestoreDefaults() {
+    // TODO this test is now in fuzzy territory - kinda more the factory's responsibility
+    $default_factory = new DefaultAssetMetadataFactory();
+    // Ensure we're in a good state first
+    $this->assertEquals($default_factory->createCssMetadata('file', 'foo/bar.css'), $this->collector->getMetadataDefaults('css', 'file', 'foo/bar.css'));
+
+    $changed_css = new AssetMetadataBag('css', array('foo' => 'bar', 'every_page' => TRUE));
+    $factory = $this->getMock('Drupal\Core\Asset\Metadata\DefaultAssetMetadataFactory');
+    $factory->expects($this->exactly(2))
+      ->method('createCssMetadata')
+      ->will($this->returnValue(clone $changed_css));
+
+    $this->collector->setMetadataFactory($factory);
+
+    $this->assertEquals($changed_css, $this->collector->getMetadataDefaults('css', 'file', 'foo/bar.css'));
+    // TODO this is totally cheating, only passes because we clone earlier. but it should be a guarantee of the interface...how to test this?
+    $this->assertNotSame($changed_css, $this->collector->getMetadataDefaults('css', 'file', 'foo/bar.css'), 'New metadata instance is created on retrieval from collector.');
+
+    $this->collector->restoreDefaults();
+    $this->assertEquals($default_factory->createCssMetadata('file', 'foo/bar.css'), $this->collector->getMetadataDefaults('css', 'file', 'foo/bar.css'));
+  }
+
+  /**
+   * @expectedException \InvalidArgumentException
+   */
+  public function testGetNonexistentDefault() {
+    $this->collector->getMetadataDefaults('foo', 'file', 'foo/bar.css');
+  }
+
+
+  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() {
+    $js = $this->collector->create('js', 'file', 'foo.js');
+    $css1 = $this->collector->create('css', 'file', 'foo.css');
+    $css2 = $this->collector->create('css', 'file', 'foo2.css', array(), array(), FALSE);
+    $this->assertEquals(array($css1), $css2->getPredecessors());
+
+    $css3 = $this->collector->create('css', 'file', 'foo3.css');
+    $this->assertEquals(array($css1), $css3->getPredecessors());
+
+    $this->collector->clearLastCss();
+    $css4 = $this->collector->create('css', 'file', 'foo4.css');
+    $this->assertEmpty($css4->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/Factory/AssetLibraryFactoryTest.php b/core/tests/Drupal/Tests/Core/Asset/Factory/AssetLibraryFactoryTest.php
new file mode 100644
index 0000000..3a0a07e
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/Factory/AssetLibraryFactoryTest.php
@@ -0,0 +1,305 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\Factory\AssetLibraryFactoryTest.
+ */
+
+namespace Drupal\Tests\Core\Asset\Factory {
+
+use Drupal\Core\Asset\Factory\AssetLibraryFactory;
+use Drupal\Tests\Core\Asset\AssetUnitTest;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Asset\Factory\AssetLibraryFactory
+ * @group Asset
+ */
+class AssetLibraryFactoryTest extends AssetUnitTest {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'AssetLibraryFactory unit tests',
+      'description' => 'Unit tests on AssetLibraryFactory',
+      'group' => 'Asset',
+    );
+  }
+
+  /**
+   * @covers ::__construct
+   */
+  public function testCreateFactory() {
+    $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
+    $collector = $this->getMock('Drupal\Core\Asset\Factory\AssetCollector');
+
+    $factory = new AssetLibraryFactory($module_handler, $collector);
+    $this->assertAttributeSame($collector, 'collector', $factory);
+
+    $metadata_factory = $this->getMock('Drupal\Core\Asset\Metadata\MetadataFactoryInterface');
+
+    $factory = new AssetLibraryFactory($module_handler, NULL, $metadata_factory);
+    $prop = new \ReflectionProperty($factory, 'collector');
+    $prop->setAccessible(TRUE);
+    $collector = $prop->getValue($factory);
+
+    $this->assertAttributeSame($metadata_factory, 'metadataFactory', $collector);
+  }
+
+  /**
+   * @covers ::__construct
+   * @expectedException \RuntimeException
+   */
+  public function testCreateFactoryWithLockedCollector() {
+    $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
+    $collector = $this->getMock('Drupal\Core\Asset\Factory\AssetCollector');
+    $collector->expects($this->once())
+      ->method('isLocked')
+      ->will($this->returnValue(TRUE));
+
+    new AssetLibraryFactory($module_handler, $collector);
+  }
+
+  /**
+   * @covers ::getLibrary
+   */
+  public function testGetLibrary() {
+    $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
+    $module_handler->expects($this->exactly(3))
+      ->method('implementsHook')
+      ->with('stub1', 'library_info')
+      ->will($this->returnValue(TRUE));
+    $module_handler->expects($this->exactly(3))
+      ->method('alter')
+      ->with('library_info') // matching more args is unnecessary and annoying
+      ->will($this->returnArgument(1));
+
+    $collector = $this->getMock('Drupal\Core\Asset\Factory\AssetCollector');
+    $collector->expects($this->exactly(2))
+      ->method('create')
+      ->will($this->returnCallback(array($this, 'createStubFileAsset')));
+    $collector->expects($this->exactly(2))
+      ->method('clearLastCss');
+    $factory = new AssetLibraryFactory($module_handler, $collector);
+
+    $this->assertFalse($factory->getLibrary('stub1/foo'));
+
+    $lib1 = $factory->getLibrary('stub1/solo-nodeps-js');
+
+    $this->assertInstanceOf('Drupal\Core\Asset\Collection\AssetLibrary', $lib1);
+    $this->assertEquals('solo-nodeps-js', $lib1->getTitle());
+    $this->assertEquals('1.2.3', $lib1->getVersion());
+    $this->assertEquals('http://foo.bar', $lib1->getWebsite());
+    $this->assertTrue($lib1->isFrozen());
+
+    $lib2 = $factory->getLibrary('stub1/solo-onedep-same');
+
+    $this->assertInstanceOf('Drupal\Core\Asset\Collection\AssetLibrary', $lib2);
+    $this->assertEquals(array('stub1/solo-nodeps-js'), $lib2->getDependencyInfo());
+
+    foreach ($lib2 as $asset) {
+      $this->assertEquals(array('stub1/solo-nodeps-js'), $asset->getDependencyInfo());
+    }
+  }
+
+  /**
+   * @covers ::getLibrary
+   */
+  public function testGetLibraryModuleDoesNotImplementHook() {
+    $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
+    $module_handler->expects($this->once())
+      ->method('implementsHook')
+      ->with('foo', 'library_info')
+      ->will($this->returnValue(FALSE));
+
+    $collector = $this->getMock('Drupal\Core\Asset\Factory\AssetCollector');
+    $factory = new AssetLibraryFactory($module_handler, $collector);
+
+    $this->assertFalse($factory->getLibrary('foo/bar'));
+  }
+}
+
+}
+namespace {
+
+/*
+ * Several permutations need to be covered:
+ *  - single-asset library | homogeneous multi-asset library | heterogeneous multi-asset library
+ *  - no dependencies | single dep | multi dep
+ *  - dep with same type | dep with cross type | heterogeneous mix
+ */
+function stub1_library_info() {
+  $libraries['solo-nodeps-js'] = array(
+    'title' => 'solo-nodeps-js',
+    'version' => '1.2.3',
+    'website' => 'http://foo.bar',
+    'js' => array(
+      'js/solo/nodeps.js',
+    )
+  );
+
+  $libraries['solo-nodeps-css'] = array(
+    'title' => 'solo-nodeps-css',
+    'css' => array(
+      'css/solo/nodeps.css' => array(),
+    )
+  );
+
+  $libraries['solo-onedep-same'] = array(
+    'title' => 'solo-onedep-same',
+    'js' => array(
+      'js/solo/onedep/same.js' => array(),
+    ),
+    'dependencies' => array(
+      array('stub1', 'solo-nodeps-js'),
+    )
+  );
+
+  $libraries['solo-onedep-diff'] = array(
+    'title' => 'solo-onedep-same',
+    'js' => array(
+      'js/solo/onedep/diff.js' => array(),
+    ),
+    'dependencies' => array(
+      array('stub1', 'solo-nodeps-css'),
+    )
+  );
+
+  $libraries['solo-multidep-same'] = array(
+    'title' => 'solo-multidep-same',
+    'js' => array(
+      'js/solo/multidep/same.js' => array(),
+    ),
+    'dependencies' => array(
+      array('stub1', 'solo-nodeps-js'),
+      array('stub1', 'solo-onedep-same'),
+    )
+  );
+
+  $libraries['solo-multidep-hetero'] = array(
+    'title' => 'solo-multidep-hetero',
+    'js' => array(
+      'js/solo/multidep/hetero.js' => array(),
+    ),
+    'dependencies' => array(
+      array('stub1', 'solo-nodeps-js'),
+      array('stub1', 'solo-nodeps-css'),
+    )
+  );
+
+  return $libraries;
+}
+
+function stub2_library_info() {
+  $libraries['homo-nodeps-js'] = array(
+    'title' => 'homo-nodeps-js',
+    'js' => array(
+      'js/homo/nodeps1.js' => array(),
+      'js/homo/nodeps2.js' => array(),
+    ),
+  );
+
+  $libraries['homo-nodeps-css'] = array(
+    'title' => 'homo-nodeps-css',
+    'css' => array(
+      'css/homo/nodeps1.css' => array(),
+      'css/homo/nodeps2.css' => array(),
+    ),
+  );
+
+  $libraries['hetero-nodeps'] = array(
+    'title' => 'hetero-nodeps',
+    'css' => array(
+      'css/hetero/nodeps.css' => array(),
+    ),
+    'js' => array(
+      'js/hetero/nodeps.js' => array(),
+    ),
+  );
+
+  $libraries['homo-onedep-same'] = array(
+    'title' => 'homo-onedep-same',
+    'css' => array(
+      'css/homo/onedep/same1.css' => array(),
+      'css/homo/onedep/same2.css' => array(),
+    ),
+    'dependencies' => array(
+      array('stub1', 'solo-nodeps-css'),
+    ),
+  );
+
+  $libraries['homo-onedep-diff'] = array(
+    'title' => 'homo-onedep-diff',
+    'css' => array(
+      'css/homo/onedep/diff1.css' => array(),
+      'css/homo/onedep/diff2.css' => array(),
+    ),
+    'dependencies' => array(
+      array('stub1', 'solo-nodeps-js'),
+    ),
+  );
+
+  $libraries['hetero-onedep'] = array(
+    'title' => 'hetero-onedep',
+    'css' => array(
+      'css/hetero/onedep.css' => array(),
+    ),
+    'js' => array(
+      'js/hetero/onedep.js' => array(),
+    ),
+    'dependencies' => array(
+      array('stub2', 'hetero-nodeps'),
+    ),
+  );
+
+  $libraries['homo-multidep-same'] = array(
+    'title' => 'homo-multidep-same',
+    'css' => array(
+      'css/homo/multidep/same1.css' => array(),
+      'css/homo/multidep/same2.css' => array(),
+    ),
+    'dependencies' => array(
+      array('stub1', 'solo-nodeps-css'),
+      array('stub2', 'homo-nodeps-css'),
+    ),
+  );
+
+  $libraries['homo-multidep-diff'] = array(
+    'title' => 'homo-multidep-diff',
+    'js' => array(
+      'js/homo/multidep/diff1.js' => array(),
+      'js/homo/multidep/diff1.js' => array(),
+    ),
+    'dependencies' => array(
+      array('stub1', 'solo-nodeps-css'),
+      array('stub2', 'homo-nodeps-css'),
+    ),
+  );
+
+  $libraries['homo-multidep-hetero'] = array(
+    'title' => 'homo-multidep-hetero',
+    'css' => array(
+      'css/homo/multidep/hetero1.css' => array(),
+      'css/homo/multidep/hetero1.css' => array(),
+    ),
+    'dependencies' => array(
+      array('stub1', 'solo-nodeps-css'),
+      array('stub2', 'homo-nodeps-js'),
+    ),
+  );
+
+  $libraries['hetero-multidep'] = array(
+    'title' => 'hetero-multidep',
+    'css' => array(
+      'css/homo/multidep1.css' => array(),
+    ),
+    'js' => array(
+      'js/homo/multidep1.js' => array(),
+    ),
+    'dependencies' => array(
+      array('stub1', 'solo-nodeps-css'),
+      array('stub2', 'homo-nodeps-js'),
+    ),
+  );
+
+  return $libraries;
+}
+}
\ No newline at end of file
diff --git a/core/tests/Drupal/Tests/Core/Asset/FileAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/FileAssetTest.php
new file mode 100644
index 0000000..759d5d8
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/FileAssetTest.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\FileAssetTest.
+ */
+
+namespace Drupal\Tests\Core\Asset;
+
+use Drupal\Core\Asset\FileAsset;
+
+/**
+ * @group Asset
+ */
+class FileAssetTest extends AssetUnitTest {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'File asset tests',
+      'description' => 'Unit tests for FileAsset',
+      'group' => 'Asset',
+    );
+  }
+
+  public function testInitialCreation() {
+    $meta = $this->createStubAssetMetadata();
+    $asset = new FileAsset($meta, __FILE__);
+
+    $this->assertEquals(__FILE__, $asset->id());
+    $this->assertEquals(dirname(__FILE__), $asset->getSourceRoot());
+    $this->assertEquals(basename(__FILE__), $asset->getSourcePath());
+  }
+
+  /**
+   * @expectedException \InvalidArgumentException
+   */
+  public function testCreateNonString() {
+    $meta = $this->createStubAssetMetadata();
+    new FileAsset($meta, new \stdClass());
+  }
+
+  /**
+   * @expectedException \RuntimeException
+   */
+  public function testLastModified() {
+    // This seems less than ideal, but it's what Assetic does. So...
+    $meta = $this->createStubAssetMetadata();
+    $asset = new FileAsset($meta, __FILE__);
+
+    $this->assertInternalType('integer', $asset->getLastModified());
+
+    $asset = new FileAsset($meta, __FILE__ . '.foo');
+    $asset->getLastModified();
+  }
+
+  /**
+   * @expectedException \RuntimeException
+   */
+  public function testLoad() {
+    $meta = $this->createStubAssetMetadata();
+    $asset = new FileAsset($meta, __FILE__);
+
+    $this->assertEmpty($asset->getContent()); // ensure content is lazy loaded
+
+    $asset->load();
+    $this->assertEquals(file_get_contents(__FILE__), $asset->getContent());
+
+    $asset = new FileAsset($meta, __FILE__ . '.foo');
+    $asset->load();
+  }
+}
diff --git a/core/tests/Drupal/Tests/Core/Asset/GroupSort/AssetGraphTest.php b/core/tests/Drupal/Tests/Core/Asset/GroupSort/AssetGraphTest.php
new file mode 100644
index 0000000..b54378b
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/GroupSort/AssetGraphTest.php
@@ -0,0 +1,259 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\AssetGraphTest.
+ */
+
+namespace Drupal\Tests\Core\Asset\GroupSort;
+
+use Drupal\Core\Asset\GroupSort\AssetGraph;
+use Drupal\Core\Asset\BaseAsset;
+use Drupal\Tests\Core\Asset\AssetUnitTest;
+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->createStubAssetMetadata();
+    $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->createStubAssetMetadata();
+    $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->createStubAssetMetadata();
+    $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/GroupSort/OptimallyGroupedTSLVisitorTest.php b/core/tests/Drupal/Tests/Core/Asset/GroupSort/OptimallyGroupedTSLVisitorTest.php
new file mode 100644
index 0000000..ab377c4
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/GroupSort/OptimallyGroupedTSLVisitorTest.php
@@ -0,0 +1,214 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\GroupSort\OptimallyGroupedTSLVisitorTest.
+ */
+
+namespace Drupal\Tests\Core\Asset\GroupSort;
+
+use Drupal\Core\Asset\GroupSort\AssetGraph;
+use Drupal\Core\Asset\GroupSort\OptimallyGroupedTSLVisitor;
+use Drupal\Tests\Core\Asset\AssetUnitTest;
+use Gliph\Traversal\DepthFirst;
+use Gliph\Visitor\DepthFirstBasicVisitor;
+
+/**
+ * @group Asset
+ */
+class OptimallyGroupedTSLVisitorTest extends AssetUnitTest {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Tests depth first visitor.',
+      'description' => 'Integration tests on OptimallyGroupedTSLVisitor.',
+      'group' => 'Asset',
+    );
+  }
+
+  public function createStubPositioningAsset($id, $predecessors = array(), $successors = array()) {
+    $asset = $this->getMockBuilder('Drupal\Core\Asset\BaseAsset')
+      ->disableOriginalConstructor()
+      ->setMockClassName("mock_asset_$id")
+      ->getMock();
+
+    $asset->expects($this->any())
+      ->method('id')
+      ->will($this->returnValue($id));
+
+    if ($predecessors !== FALSE) {
+      $asset->expects($this->any())
+        ->method('getPredecessors')
+        ->will($this->returnValue($predecessors));
+    }
+
+    if ($successors !== FALSE) {
+      $asset->expects($this->any())
+        ->method('getSuccessors')
+        ->will($this->returnValue($successors));
+    }
+
+    return $asset;
+  }
+
+  /**
+   * Vertices: a, b, c, d, e, f, g, h
+   * Edges:
+   * f -> c
+   * d -> c
+   * e -> d
+   * b -> e
+   * h -> a
+   */
+  public function getVertexSet() {
+    $vertices = array();
+    $vertices['a'] = $this->createStubPositioningAsset('a');
+    $vertices['b'] = $this->createStubPositioningAsset('b', array('e'));
+    $vertices['c'] = $this->createStubPositioningAsset('c');
+    $vertices['d'] = $this->createStubPositioningAsset('d', array($vertices['c']));
+    $vertices['e'] = $this->createStubPositioningAsset('e', array($vertices['d']));
+    $vertices['f'] = $this->createStubPositioningAsset('f', array($vertices['c']));
+    $vertices['g'] = $this->createStubPositioningAsset('g');
+    $vertices['h'] = $this->createStubPositioningAsset('h', array($vertices['a']));
+    return $vertices;
+  }
+
+  /**
+   * Optimality groups:
+   * g1: a, b, c
+   * g2: d, e
+   * g3: f, g
+   *
+   * Ungrouped:
+   * h
+   */
+  public function createSimpleGraph() {
+    $vertices = $this->getVertexSet();
+    extract($vertices);
+
+
+    // Populate the graph
+    $graph = new AssetGraph();
+    foreach ($vertices as $v) {
+      $graph->addVertex($v);
+    }
+
+    return array($graph, $vertices);
+  }
+
+  /**
+   * @covers Drupal\Core\Asset\AssetGraph::addVertex
+   * @covers Drupal\Core\Asset\AssetGraph::processNewVertex
+   */
+  public function testAssetGraphBuildsEdgesCorrectly() {
+    list($graph, $vertices) = $this->createSimpleGraph();
+    extract($vertices);
+
+    $that = $this;
+    // First, take care of vertices that should have no edges
+    foreach (array('a', 'c', 'g') as $vertex_id) {
+      $graph->eachAdjacent($$vertex_id, function($adjacent) use ($that) {
+        $that->fail();
+      });
+    }
+
+    // Now handle the individual cases.
+    $graph->eachAdjacent($b, function($adjacent) use ($that, $vertices) {
+      $that->assertSame($vertices['e'], $adjacent);
+    });
+    $graph->eachAdjacent($d, function($adjacent) use ($that, $vertices) {
+      $that->assertSame($vertices['c'], $adjacent);
+    });
+    $graph->eachAdjacent($e, function($adjacent) use ($that, $vertices) {
+      $that->assertSame($vertices['d'], $adjacent);
+    });
+    $graph->eachAdjacent($f, function($adjacent) use ($that, $vertices) {
+      $that->assertSame($vertices['c'], $adjacent);
+    });
+    $graph->eachAdjacent($h, function($adjacent) use ($that, $vertices) {
+      $that->assertSame($vertices['a'], $adjacent);
+    });
+  }
+
+  /**
+   * @depends testAssetGraphBuildsEdgesCorrectly
+   * @covers Drupal\Core\Asset\GroupSort\OptimallyGroupedTSLVisitor
+   */
+  public function testRealSort() {
+    list($graph, $vertices) = $this->createSimpleGraph();
+    extract($vertices);
+
+    $transpose = $graph->transpose();
+
+    $reach_visitor = new DepthFirstBasicVisitor();
+
+    // Find source vertices (outdegree 0) in the original graph
+    $sources = DepthFirst::find_sources($transpose, $reach_visitor);
+    $this->assertCount(3, $sources);
+    $this->assertContains($c, $sources);
+    $this->assertContains($a, $sources);
+    $this->assertContains($g, $sources);
+
+    // Traverse the transposed graph for reachability data on each vertex
+    DepthFirst::traverse($transpose, $reach_visitor, clone $sources);
+
+    $this->assertCount(4, $reach_visitor->getReachable($c));
+    $this->assertCount(1, $reach_visitor->getReachable($a));
+    $this->assertCount(0, $reach_visitor->getReachable($b));
+    $this->assertCount(2, $reach_visitor->getReachable($d));
+    $this->assertCount(1, $reach_visitor->getReachable($e));
+    $this->assertCount(0, $reach_visitor->getReachable($f));
+    $this->assertCount(0, $reach_visitor->getReachable($g));
+
+    // 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
+    $queue = new \SplQueue();
+    foreach ($pq as $vertex) {
+      $queue->push($vertex);
+    }
+    $optimal = array(
+      'g1' => new \SplObjectStorage(),
+      'g2' => new \SplObjectStorage(),
+      'g3' => new \SplObjectStorage(),
+    );
+    $optimal_lookup = new \SplObjectStorage();
+
+    $optimal['g1']->attach($a, 'g1');
+    $optimal_lookup->attach($a, $optimal['g1']);
+    $optimal['g1']->attach($b, 'g1');
+    $optimal_lookup->attach($b, $optimal['g1']);
+    $optimal['g1']->attach($c, 'g1');
+    $optimal_lookup->attach($c, $optimal['g1']);
+
+    $optimal['g2']->attach($d, 'g2');
+    $optimal_lookup->attach($d, $optimal['g2']);
+    $optimal['g2']->attach($e, 'g2');
+    $optimal_lookup->attach($e, $optimal['g2']);
+
+    $optimal['g3']->attach($f, 'g3');
+    $optimal_lookup->attach($f, $optimal['g3']);
+    $optimal['g3']->attach($g, 'g3');
+    $optimal_lookup->attach($g, $optimal['g3']);
+
+    $vis = new OptimallyGroupedTSLVisitor($optimal, $optimal_lookup);
+    DepthFirst::traverse($transpose, $vis, $queue);
+
+    // Ta-da!
+    $expected = array(
+      'h' => $h,
+      'a' => $a,
+      'b' => $b,
+      'e' => $e,
+      'd' => $d,
+      'g' => $g,
+      'f' => $f,
+      'c' => $c,
+    );
+    $this->assertEquals($expected, $vis->getTSL()->all());
+  }
+}
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..747e966
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/Metadata/AssetMetadataBagTest.php
@@ -0,0 +1,32 @@
+<?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 {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Asset Metadata bag test',
+      'description' => 'Tests various methods of AssetMetadatabag',
+      'group' => 'Asset',
+    );
+  }
+
+
+  public function testGetType() {
+    $bag = new AssetMetadataBag('arglebargle', array());
+    $this->assertEquals('arglebargle', $bag->getType());
+  }
+}
diff --git a/core/tests/Drupal/Tests/Core/Asset/Metadata/DefaultAssetMetadataFactoryTest.php b/core/tests/Drupal/Tests/Core/Asset/Metadata/DefaultAssetMetadataFactoryTest.php
new file mode 100644
index 0000000..8c485b5
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/Metadata/DefaultAssetMetadataFactoryTest.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\Metadata\DefaultAssetMetadataFactoryTest.
+ */
+
+namespace Drupal\Tests\Core\Asset\Metadata;
+
+use Drupal\Core\Asset\Metadata\AssetMetadataBag;
+use Drupal\Core\Asset\Metadata\DefaultAssetMetadataFactory;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ *
+ * @group Asset
+ */
+class DefaultAssetMetadataFactoryTest extends UnitTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'DefaultAssetMetadataFactory test',
+      'description' => 'Unit tests for DefaultAssetMetadataFactory',
+      'group' => 'Asset',
+    );
+  }
+
+  public function testCreateCssMetadata() {
+    $factory = new DefaultAssetMetadataFactory();
+    $bag = new AssetMetadataBag('css', array(
+      'every_page' => FALSE,
+      'media' => 'all',
+      'preprocess' => TRUE,
+      'browsers' => array(
+        'IE' => TRUE,
+        '!IE' => TRUE,
+      ),
+    ));
+
+    $this->assertEquals($bag, $factory->createCssMetadata('file', 'foo/bar.css'));
+  }
+
+  public function testCreateJsMetadata() {
+    $factory = new DefaultAssetMetadataFactory();
+    $bag = new AssetMetadataBag('js', array(
+      'every_page' => FALSE,
+      'scope' => 'footer',
+      'cache' => TRUE,
+      'preprocess' => TRUE,
+      'attributes' => array(),
+      'version' => NULL,
+      'browsers' => array(),
+    ));
+
+    $this->assertEquals($bag, $factory->createJsMetadata('file', 'foo/bar.js'));
+  }
+}
diff --git a/core/tests/Drupal/Tests/Core/Asset/StringAssetTest.php b/core/tests/Drupal/Tests/Core/Asset/StringAssetTest.php
new file mode 100644
index 0000000..2568a52
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/StringAssetTest.php
@@ -0,0 +1,91 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Asset\StringAssetTest.
+ */
+
+namespace Drupal\Tests\Core\Asset;
+
+use Drupal\Core\Asset\StringAsset;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Asset\StringAsset
+ * @group Asset
+ */
+class StringAssetTest extends AssetUnitTest {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'String asset tests',
+      'description' => 'Unit tests for StringAsset',
+      'group' => 'Asset',
+    );
+  }
+
+  /**
+   * @covers ::__construct
+   */
+  public function testInitialCreation() {
+    $meta = $this->createStubAssetMetadata();
+    $content = 'foo bar baz';
+    $asset = new StringAsset($meta, $content);
+
+    $this->assertEquals($content, $asset->getContent());
+    $this->assertFalse($asset->getLastModified()); // TODO change this once we have a better plan
+  }
+
+  /**
+   * @covers ::__construct
+   */
+  public function testCreateInvalidContent() {
+    $meta = $this->createStubAssetMetadata();
+    $invalid = array('', 0, 1.1, fopen(__FILE__, 'r'), TRUE, array(), new \stdClass);
+
+    try {
+      foreach ($invalid as $val) {
+        new StringAsset($meta, $val);
+        $varinfo = (gettype($val) == 'string') ? 'an empty string' : 'of type ' . gettype($val);
+        $this->fail(sprintf('Was able to create a string asset with invalid content; content was %s.', $varinfo));
+      }
+    } catch (\InvalidArgumentException $e) {}
+  }
+
+  /**
+   * @covers ::setLastModified
+   * @covers ::getLastModified
+   */
+  public function testLastModified() {
+    $meta = $this->createStubAssetMetadata();
+    $content = 'foo bar baz';
+    $asset = new StringAsset($meta, $content);
+
+    $asset->setLastModified(100);
+    $this->assertEquals(100, $asset->getLastModified());
+  }
+
+  /**
+   * @covers ::id
+   */
+  public function testId() {
+    $meta = $this->createStubAssetMetadata();
+    $content = 'foo bar baz';
+    $asset = new StringAsset($meta, $content);
+
+    $this->assertEquals(hash('sha256', $content), $asset->id());
+  }
+
+  /**
+   * @covers ::load
+   */
+  public function testLoad() {
+    $meta = $this->createStubAssetMetadata();
+    $content = 'foo bar baz';
+    $asset = new StringAsset($meta, $content);
+
+    // With no filters, loading result in the same content we started with.
+    $asset->load();
+    $this->assertEquals($content, $asset->getContent());
+  }
+}
+
