From 4ecc4ead39c67a3ea63bec6ce3510610cc4fde33 Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Thu, 4 Dec 2014 11:37:23 +0000
Subject: [PATCH] Issue #2382557 by Wim Leers: Change JS settings into a
 separate asset type

---
 core/core.libraries.yml                       |  13 +-
 core/includes/batch.inc                       |  19 +--
 core/includes/common.inc                      | 136 +++++++-----------
 core/lib/Drupal/Core/Ajax/AjaxResponse.php    |  18 ++-
 .../Core/Asset/JsCollectionRenderer.php       |   2 +-
 .../Core/Asset/LibraryDiscoveryParser.php     |  10 +-
 .../Core/Installer/Form/SiteConfigureForm.php |   3 +-
 .../Core/Render/Element/MachineName.php       |  12 +-
 .../Core/Render/Element/RenderElement.php     |   5 +-
 core/modules/block/src/BlockListBuilder.php   |   5 +-
 .../block/src/Controller/BlockController.php  |  19 ++-
 .../ckeditor/src/Plugin/Editor/CKEditor.php   |  22 +--
 core/modules/color/color.module               |  19 +--
 .../comment/src/CommentViewBuilder.php        |  27 ++--
 .../content_translation.admin.inc             |   8 +-
 .../editor/src/Plugin/EditorManager.php       |   5 +-
 .../editor/src/Tests/EditorManagerTest.php    |  19 ++-
 core/modules/field_ui/src/OverviewBase.php    |   5 +-
 .../Controller/FileWidgetAjaxController.php   |   2 +-
 core/modules/file/src/Element/ManagedFile.php |   5 +-
 core/modules/history/history.module           |  13 +-
 core/modules/locale/locale.module             |   6 +-
 .../src/Form/SimpletestTestForm.php           |  15 +-
 core/modules/simpletest/src/WebTestBase.php   |   2 +-
 core/modules/statistics/statistics.module     |   5 +-
 .../system/src/Tests/Ajax/CommandsTest.php    |   6 +-
 .../src/Tests/Common/JavaScriptTest.php       | 104 +++-----------
 .../src/Tests/Common/MergeAttachmentsTest.php | 131 +++++++++++------
 .../system/src/Tests/Common/RenderTest.php    | 117 +++++++--------
 core/modules/system/system.module             |  43 ++++++
 .../ajax_forms_test/ajax_forms_test.module    |  26 ----
 .../src/Form/AjaxFormsTestCommandsForm.php    |  12 --
 .../src/Form/AjaxFormsTestLazyLoadForm.php    |  12 +-
 .../tests/modules/ajax_test/ajax_test.module  |  18 +--
 .../modules/common_test/common_test.module    |  36 +++--
 .../test_page_test/test_page_test.module      |   5 +-
 core/modules/system/theme.api.php             |  27 +++-
 .../taxonomy/src/Form/OverviewTerms.php       |   8 +-
 core/modules/toolbar/src/Element/Toolbar.php  |   9 +-
 core/modules/toolbar/toolbar.module           |  11 +-
 core/modules/user/user.module                 |  29 +---
 .../views_test_data.views_execution.inc       |   1 +
 core/modules/views/views.module               |   3 +-
 core/modules/views_ui/src/ViewEditForm.php    |  15 +-
 .../Core/Asset/LibraryDiscoveryParserTest.php |   3 +-
 .../css_js_settings.libraries.yml             |   2 +-
 core/themes/bartik/color/color.inc            |   3 +-
 47 files changed, 428 insertions(+), 588 deletions(-)

diff --git a/core/core.libraries.yml b/core/core.libraries.yml
index 5f327210e158..0c212ce587bd 100644
--- a/core/core.libraries.yml
+++ b/core/core.libraries.yml
@@ -52,7 +52,18 @@ drupal:
 
 drupalSettings:
   version: VERSION
-  settings: {}
+  drupalSettings:
+    # These placeholder values will be set by system_js_settings_alter().
+    path:
+      baseUrl: null
+      scriptPath: null
+      pathPrefix: null
+      currentPath: null
+      currentPathIsAdmin: null
+      isFront: null
+      currentLanguage: null
+    locale:
+      pluralDelimiter: null
 
 drupal.active-link:
   version: VERSION
diff --git a/core/includes/batch.inc b/core/includes/batch.inc
index 460c1afb4069..d1902e0d7d7b 100644
--- a/core/includes/batch.inc
+++ b/core/includes/batch.inc
@@ -179,18 +179,13 @@ function _batch_progress_page() {
         ),
       ),
       // Adds JavaScript code and settings for clients where JavaScript is enabled.
-      'js' => array(
-        array(
-          'type' => 'setting',
-          'data' => array(
-            'batch' => array(
-              'errorMessage' => $current_set['error_message'] . '<br />' . $batch['error_message'],
-              'initMessage' => $current_set['init_message'],
-              'uri' => $url,
-            ),
-          ),
-        ),
-      ),
+      'drupalSettings' => [
+        'batch' => [
+          'errorMessage' => $current_set['error_message'] . '<br />' . $batch['error_message'],
+          'initMessage' => $current_set['init_message'],
+          'uri' => $url,
+        ],
+      ],
       'library' => array(
         'core/drupal.batch',
       ),
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 99a56bd2c940..612546161da0 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -1456,50 +1456,20 @@ function _drupal_add_js($data = NULL, $options = NULL) {
     switch ($options['type']) {
       case 'setting':
         // If the setting array doesn't exist, add defaults values.
-        if (!isset($javascript['settings'])) {
-          $javascript['settings'] = array(
+        if (!isset($javascript['drupalSettings'])) {
+          $javascript['drupalSettings'] = array(
             'type' => 'setting',
             'scope' => 'header',
             'group' => JS_SETTING,
             'every_page' => TRUE,
             'weight' => 0,
             'browsers' => array(),
-          );
-          // url() generates the script and prefix using hook_url_outbound_alter().
-          // Instead of running the hook_url_outbound_alter() again here, extract
-          // them from url().
-          // @todo Make this less hacky: http://drupal.org/node/1547376.
-          $request = \Drupal::request();
-          $scriptPath = $request->getScriptName();
-
-          $pathPrefix = '';
-          $current_query = $request->query->all();
-          _url('', array('script' => &$scriptPath, 'prefix' => &$pathPrefix));
-          $current_path = \Drupal::routeMatch()->getRouteName() ? Url::fromRouteMatch(\Drupal::routeMatch())->getInternalPath() : '';
-          $current_path_is_admin = \Drupal::service('router.admin_context')->isAdminRoute();
-          $path = array(
-            'baseUrl' => $request->getBaseUrl() . '/',
-            'scriptPath' => $scriptPath,
-            'pathPrefix' => $pathPrefix,
-            'currentPath' => $current_path,
-            'currentPathIsAdmin' => $current_path_is_admin,
-            'isFront' => drupal_is_front_page(),
-            'currentLanguage' => \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId(),
-          );
-          if (!empty($current_query)) {
-            ksort($current_query);
-            $path['currentQuery'] = (object) $current_query;
-          }
-          $javascript['settings']['data'][] = array(
-            'path' => $path,
-            'locale' => array(
-              'pluralDelimiter' => LOCALE_PLURAL_DELIMITER,
-            ),
+            'data' => array(),
           );
         }
         // All JavaScript settings are placed in the header of the page with
         // the library weight so that inline scripts appear afterwards.
-        $javascript['settings']['data'][] = $data;
+        $javascript['drupalSettings']['data'] = NestedArray::mergeDeepArray([$javascript['drupalSettings']['data'], $data], TRUE);
         break;
 
       case 'inline':
@@ -1603,21 +1573,21 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
     uasort($items, 'drupal_sort_css_js');
     // Don't add settings if there is no other JavaScript on the page, unless
     // this is an AJAX request.
-    if (!empty($items['settings']) || $is_ajax) {
+    if (!empty($items['drupalSettings']) || $is_ajax) {
       $theme_key = \Drupal::theme()->getActiveTheme()->getName();
       // Provide the page with information about the theme that's used, so that
       // a later AJAX request can be rendered using the same theme.
       // @see \Drupal\Core\Theme\AjaxBasePageNegotiator
-      $setting['ajaxPageState']['theme'] = $theme_key;
+      $ajaxPageState['theme'] = $theme_key;
       // Checks that the DB is available before filling theme_token.
       if (!defined('MAINTENANCE_MODE')) {
-        $setting['ajaxPageState']['theme_token'] = \Drupal::csrfToken()->get($theme_key);
+        $ajaxPageState['theme_token'] = \Drupal::csrfToken()->get($theme_key);
       }
 
       // Provide the page with information about the individual JavaScript files
       // used, information not otherwise available when aggregation is enabled.
-      $setting['ajaxPageState']['js'] = array_fill_keys(array_keys($javascript), 1);
-      unset($setting['ajaxPageState']['js']['settings']);
+      $ajaxPageState['js'] = array_fill_keys(array_keys($javascript), 1);
+      unset($ajaxPageState['js']['drupalSettings']);
 
       // Provide the page with information about the individual CSS files used,
       // information not otherwise available when CSS aggregation is enabled.
@@ -1631,23 +1601,35 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
       $css = _drupal_add_css();
       if (!empty($css)) {
         // Cast the array to an object to be on the safe side even if not empty.
-        $setting['ajaxPageState']['css'] = (object) array_fill_keys(array_keys($css), 1);
+        $ajaxPageState['css'] = (object) array_fill_keys(array_keys($css), 1);
       }
 
-      _drupal_add_js($setting, 'setting');
+      _drupal_add_js(['ajaxPageState' => $ajaxPageState], 'setting');
 
       // If we're outputting the header scope, then this might be the final time
       // that drupal_get_js() is running, so add the settings to this output as well
-      // as to the _drupal_add_js() cache. If $items['settings'] doesn't exist, it's
-      // because drupal_get_js() was intentionally passed a $javascript argument
-      // stripped of settings, potentially in order to override how settings get
-      // output, so in this case, do not add the setting to this output.
-      if ($scope == 'header' && isset($items['settings'])) {
-        $items['settings']['data'][] = $setting;
+      // as to the _drupal_add_js() cache. If $items['drupalSettings'] doesn't
+      // exist, it's because drupal_get_js() was intentionally passed a
+      // $javascript argument stripped of settings, potentially in order to
+      // override how settings get output, so in this case, do not add the
+      // setting to this output.
+      if ($scope == 'header' && isset($items['drupalSettings'])) {
+        $items['drupalSettings']['data']['ajaxPageState'] = $ajaxPageState;
       }
     }
   }
 
+  // Process the 'drupalSettings' JavaScript asset, if any.
+  if (!empty($items['drupalSettings'])) {
+    $settings = $items['drupalSettings']['data'];
+
+    // Allow modules and themes to alter the JavaScript settings.
+    \Drupal::moduleHandler()->alter('js_settings', $settings);
+    \Drupal::theme()->alter('js_settings', $settings);
+
+    $items['drupalSettings']['data'] = $settings;
+  }
+
   // Render the HTML needed to load the JavaScript.
   $elements = array(
     '#type' => 'scripts',
@@ -1658,30 +1640,25 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
 }
 
 /**
- * Merges an array of settings arrays into a single settings array.
+ * Merges two #attached arrays.
  *
- * This function merges the items in the same way that
+ * The values under the 'drupalSettings' key are merged in a special way, to
+ * match the behavior of
  *
  * @code
  *   jQuery.extend(true, {}, $settings_items[0], $settings_items[1], ...)
  * @endcode
  *
- * would. This means integer indices are preserved just like string indices are,
+ * This means integer indices are preserved just like string indices are,
  * rather than re-indexed as is common in PHP array merging.
  *
  * Example:
  * @code
  * function module1_page_attachments(&$page) {
- *   $page['#attached']['js'][] = array(
- *     'type' => 'setting',
- *     'data' => array('foo' => array('a', 'b', 'c')),
- *   );
+ *   $page['a']['#attached']['drupalSettings']['foo'] = ['a', 'b', 'c'];
  * }
  * function module2_page_attachments(&$page) {
- *   $page['#attached']['js'][] = array(
- *     'type' => 'setting',
- *     'data' => array('foo' => array('d')),
- *   );
+ *   $page['#attached']['drupalSettings']['foo'] = ['d'];
  * }
  * // When the page is rendered after the above code, and the browser runs the
  * // resulting <SCRIPT> tags, the value of drupalSettings.foo is
@@ -1690,32 +1667,12 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
  *
  * By following jQuery.extend() merge logic rather than common PHP array merge
  * logic, the following are ensured:
- * - _drupal_add_js() is idempotent: calling it twice with the same parameters
- *   does not change the output sent to the browser.
+ * - Attaching JavaScript settings is idempotent: attaching the same settings
+ *   twice does not change the output sent to the browser.
  * - If pieces of the page are rendered in separate PHP requests and the
  *   returned settings are merged by JavaScript, the resulting settings are the
  *   same as if rendered in one PHP request and merged by PHP.
  *
- * @param $settings_items
- *   An array of settings arrays, as returned by:
- *   @code
- *     $js = _drupal_add_js();
- *     $settings_items = $js['settings']['data'];
- *   @endcode
- *
- * @return
- *   A merged $settings array, suitable for JSON encoding and returning to the
- *   browser.
- *
- * @see _drupal_add_js()
- */
-function drupal_merge_js_settings($settings_items) {
-  return NestedArray::mergeDeepArray($settings_items, TRUE);
-}
-
-/**
- * Merges two #attached arrays.
- *
  * @param array $a
  *   An #attached array.
  * @param array $b
@@ -1725,6 +1682,12 @@ function drupal_merge_js_settings($settings_items) {
  *   The merged #attached array.
  */
 function drupal_merge_attached(array $a, array $b) {
+  // If both #attached arrays contain drupalSettings, then merge them correctly;
+  // adding the same settings multiple times needs to behave idempotently.
+  if (!empty($a['drupalSettings']) && !empty($b['drupalSettings'])) {
+    $a['drupalSettings'] = NestedArray::mergeDeepArray([$a['drupalSettings'], $b['drupalSettings']], TRUE);
+    unset($b['drupalSettings']);
+  }
   return NestedArray::mergeDeep($a, $b);
 }
 
@@ -1820,6 +1783,13 @@ function drupal_process_attached($elements, $dependency_check = FALSE) {
     unset($elements['#attached'][$type]);
   }
 
+  // Convert every JavaScript settings asset into a regular JavaScript asset.
+  // @todo Clean this up in https://www.drupal.org/node/2382533
+  if (!empty($elements['#attached']['drupalSettings'])) {
+    _drupal_add_js($elements['#attached']['drupalSettings'], ['type' => 'setting']);
+    unset($elements['#attached']['drupalSettings']);
+  }
+
   // Add additional types of attachments specified in the render() structure.
   // Libraries, JavaScript and CSS have been added already, as they require
   // special handling.
@@ -2027,6 +1997,9 @@ function _drupal_add_library($library_name, $every_page = NULL) {
         'js' => $library['js'],
         'css' => $library['css'],
       );
+      if (isset($library['drupalSettings'])) {
+        $elements['#attached']['drupalSettings'] = $library['drupalSettings'];
+      }
       foreach (array('js', 'css') as $type) {
         foreach ($elements['#attached'][$type] as $data => $options) {
           // Set the every_page flag if one was passed.
@@ -2186,7 +2159,7 @@ function drupal_attach_tabledrag(&$element, array $options) {
   // If a subgroup or source isn't set, assume it is the same as the group.
   $target = isset($options['subgroup']) ? $options['subgroup'] : $group;
   $source = isset($options['source']) ? $options['source'] : $target;
-  $settings['tableDrag'][$options['table_id']][$group][$tabledrag_id] = array(
+  $element['#attached']['drupalSettings'][$options['table_id']][$group][$tabledrag_id] = array(
     'target' => $target,
     'source' => $source,
     'relationship' => $options['relationship'],
@@ -2196,7 +2169,6 @@ function drupal_attach_tabledrag(&$element, array $options) {
   );
 
   $element['#attached']['library'][] = 'core/drupal.tabledrag';
-  $element['#attached']['js'][] = array('data' => $settings, 'type' => 'setting');
 }
 
 /**
diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponse.php b/core/lib/Drupal/Core/Ajax/AjaxResponse.php
index 2ae18536c18e..f69af8329d48 100644
--- a/core/lib/Drupal/Core/Ajax/AjaxResponse.php
+++ b/core/lib/Drupal/Core/Ajax/AjaxResponse.php
@@ -129,8 +129,8 @@ protected function ajaxRender(Request $request) {
     // HTML in the page. We pass TRUE as the $skip_alter argument to prevent the
     // data from being altered again, as we already altered it above. Settings
     // are handled separately, afterwards.
-    if (isset($items['js']['settings'])) {
-      unset($items['js']['settings']);
+    if (isset($items['js']['drupalSettings'])) {
+      unset($items['js']['drupalSettings']);
     }
     $styles = drupal_get_css($items['css'], TRUE);
     $scripts_footer = drupal_get_js('footer', $items['js'], TRUE, TRUE);
@@ -153,17 +153,15 @@ protected function ajaxRender(Request $request) {
 
     // Prepend a command to merge changes and additions to drupalSettings.
     $scripts = _drupal_add_js();
-    if (!empty($scripts['settings'])) {
-      $settings = drupal_merge_js_settings($scripts['settings']['data']);
+    if (!empty($scripts['drupalSettings'])) {
+      $settings = $scripts['drupalSettings']['data'];
       // During Ajax requests basic path-specific settings are excluded from
       // new drupalSettings values. The original page where this request comes
-      // from already has the right values for the keys below. An Ajax request
-      // would update them with values for the Ajax request and incorrectly
-      // override the page's values.
+      // from already has the right values. An Ajax request would update them
+      // with values for the Ajax request and incorrectly override the page's
+      // values.
       // @see _drupal_add_js()
-      foreach (array('basePath', 'currentPath', 'scriptPath', 'pathPrefix') as $item) {
-        unset($settings[$item]);
-      }
+      unset($settings['path']);
       $this->addCommand(new SettingsCommand($settings, TRUE), TRUE);
     }
 
diff --git a/core/lib/Drupal/Core/Asset/JsCollectionRenderer.php b/core/lib/Drupal/Core/Asset/JsCollectionRenderer.php
index 807179f0d53b..36f012be9003 100644
--- a/core/lib/Drupal/Core/Asset/JsCollectionRenderer.php
+++ b/core/lib/Drupal/Core/Asset/JsCollectionRenderer.php
@@ -68,7 +68,7 @@ public function render(array $js_assets) {
       switch ($js_asset['type']) {
         case 'setting':
           $element['#value_prefix'] = $embed_prefix;
-          $element['#value'] = 'var drupalSettings = ' . Json::encode(drupal_merge_js_settings($js_asset['data'])) . ";";
+          $element['#value'] = 'var drupalSettings = ' . Json::encode($js_asset['data']) . ";";
           $element['#value_suffix'] = $embed_suffix;
           break;
 
diff --git a/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
index a28ca6e8369c..ec5f457ba40c 100644
--- a/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
+++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
@@ -84,7 +84,7 @@ public function buildByExtension($extension) {
     }
 
     foreach ($libraries as $id => &$library) {
-      if (!isset($library['js']) && !isset($library['css']) && !isset($library['settings'])) {
+      if (!isset($library['js']) && !isset($library['css']) && !isset($library['drupalSettings'])) {
         throw new IncompleteLibraryDefinitionException(sprintf("Incomplete library definition for '%s' in %s", $id, $library_file));
       }
       $library += array('dependencies' => array(), 'js' => array(), 'css' => array());
@@ -198,14 +198,6 @@ public function buildByExtension($extension) {
         }
       }
 
-      // @todo Introduce drupal_add_settings().
-      if (isset($library['settings'])) {
-        $library['js'][] = array(
-          'type' => 'setting',
-          'data' => $library['settings'],
-        );
-        unset($library['settings']);
-      }
       // @todo Convert all uses of #attached[library][]=array('provider','name')
       //   into #attached[library][]='provider/name' and remove this.
       foreach ($library['dependencies'] as $i => $dependency) {
diff --git a/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php b/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php
index 167421155f30..750e96777124 100644
--- a/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php
+++ b/core/lib/Drupal/Core/Installer/Form/SiteConfigureForm.php
@@ -123,8 +123,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     $form['#attached']['library'][] = 'core/drupal.timezone';
     // We add these strings as settings because JavaScript translation does not
     // work during installation.
-    $js = array('copyFieldValue' => array('edit-site-mail' => array('edit-account-mail')));
-    $form['#attached']['js'][] = array('data' => $js, 'type' => 'setting');
+    $form['#attached']['drupalSettings']['copyFieldValue']['edit-site-mail'] = ['edit-account-mail'];
 
     // Cache a fully-built schema. This is necessary for any invocation of
     // index.php because: (1) setting cache table entries requires schema
diff --git a/core/lib/Drupal/Core/Render/Element/MachineName.php b/core/lib/Drupal/Core/Render/Element/MachineName.php
index f0c376853bea..e678b7bb9aab 100644
--- a/core/lib/Drupal/Core/Render/Element/MachineName.php
+++ b/core/lib/Drupal/Core/Render/Element/MachineName.php
@@ -163,17 +163,9 @@ public static function processMachineName(&$element, FormStateInterface $form_st
       NestedArray::setValue($form_state->getCompleteForm(), $parents, $source['#field_suffix']);
     }
 
-    $js_settings = array(
-      'type' => 'setting',
-      'data' => array(
-        'machineName' => array(
-          '#' . $source['#id'] => $element['#machine_name'],
-        ),
-        'langcode' => $language->getId(),
-      ),
-    );
     $element['#attached']['library'][] = 'core/drupal.machine-name';
-    $element['#attached']['js'][] = $js_settings;
+    $element['#attached']['drupalSettings']['machineName']['#' . $source['#id']] = $element['#machine_name'];
+    $element['#attached']['drupalSettings']['langcode'] = $language->getId();
 
     return $element;
   }
diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php
index bb8af732be6f..c0c982a79f60 100644
--- a/core/lib/Drupal/Core/Render/Element/RenderElement.php
+++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php
@@ -296,10 +296,7 @@ public static function preRenderAjaxForm($element) {
         unset($settings['progress']['path']);
       }
 
-      $element['#attached']['js'][] = array(
-        'type' => 'setting',
-        'data' => array('ajax' => array($element['#id'] => $settings)),
-      );
+      $element['#attached']['drupalSettings']['ajax'][$element['#id']] = $settings;
 
       // Indicate that Ajax processing was successful.
       $element['#ajax_processed'] = TRUE;
diff --git a/core/modules/block/src/BlockListBuilder.php b/core/modules/block/src/BlockListBuilder.php
index 1d11f9979a18..4f430044b833 100644
--- a/core/modules/block/src/BlockListBuilder.php
+++ b/core/modules/block/src/BlockListBuilder.php
@@ -139,10 +139,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     $placement = FALSE;
     if ($this->request->query->has('block-placement')) {
       $placement = $this->request->query->get('block-placement');
-      $form['#attached']['js'][] = array(
-        'type' => 'setting',
-        'data' => array('blockPlacement' => $placement),
-      );
+      $form['#attached']['drupalSettings']['blockPlacement'] = $placement;
     }
     $entities = $this->load();
     $form['#theme'] = array('block_list');
diff --git a/core/modules/block/src/Controller/BlockController.php b/core/modules/block/src/Controller/BlockController.php
index 958b3b2fd0e9..5ed1ce031550 100644
--- a/core/modules/block/src/Controller/BlockController.php
+++ b/core/modules/block/src/Controller/BlockController.php
@@ -59,17 +59,14 @@ public function demo($theme) {
       '#title' => $this->themeHandler->getName($theme),
       '#type' => 'page',
       '#attached' => array(
-        'js' => array(
-          array(
-            // The block demonstration page is not marked as an administrative
-            // page by \Drupal::service('router.admin_context')->isAdminRoute()
-            // function in order to use the frontend theme. Since JavaScript
-            // relies on a proper separation of admin pages, it needs to know
-            // this is an actual administrative page.
-            'data' => array('path' => array('currentPathIsAdmin' => TRUE)),
-            'type' => 'setting',
-          )
-        ),
+        'drupalSettings' => [
+          // The block demonstration page is not marked as an administrative
+          // page by \Drupal::service('router.admin_context')->isAdminRoute()
+          // function in order to use the frontend theme. Since JavaScript
+          // relies on a proper separation of admin pages, it needs to know this
+          // is an actual administrative page.
+          'path' => ['currentPathIsAdmin' => TRUE],
+        ],
         'library' => array(
           'block/drupal.block.admin',
         ),
diff --git a/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php b/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php
index 41259d5bb3f3..c8cb9321c9e6 100644
--- a/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php
+++ b/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php
@@ -140,14 +140,11 @@ public function settingsForm(array $form, FormStateInterface $form_state, Editor
       '#type' => 'container',
       '#attached' => array(
         'library' => array('ckeditor/drupal.ckeditor.admin'),
-        'js' => array(
-          array(
-            'type' => 'setting',
-            'data' => array('ckeditor' => array(
-              'toolbarAdmin' => drupal_render($ckeditor_settings_toolbar),
-            )),
-          )
-        ),
+        'drupalSettings' => [
+          'ckeditor' => [
+            'toolbarAdmin' => drupal_render($ckeditor_settings_toolbar),
+          ],
+        ],
       ),
       '#attributes' => array('class' => array('ckeditor-toolbar-configuration')),
     );
@@ -218,14 +215,7 @@ public function settingsForm(array $form, FormStateInterface $form_state, Editor
     $form['hidden_ckeditor'] = array(
       '#markup' => '<div id="ckeditor-hidden" class="hidden"></div>',
       '#attached' => array(
-        'js' => array(
-          array(
-            'type' => 'setting',
-            'data' => array('ckeditor' => array(
-              'hiddenCKEditorConfig' => $config,
-            )),
-          ),
-        ),
+        'drupalSettings' => ['ckeditor' => ['hiddenCKEditorConfig' => $config]],
       ),
     );
 
diff --git a/core/modules/color/color.module b/core/modules/color/color.module
index 5b008693b120..e9cc0a695098 100644
--- a/core/modules/color/color.module
+++ b/core/modules/color/color.module
@@ -213,18 +213,13 @@ function color_scheme_form($complete_form, FormStateInterface $form_state, $them
         'color/admin',
       ),
       // Add custom JavaScript.
-      'js' => array(
-        array(
-          'data' => array(
-            'color' => array(
-              'reference' => color_get_palette($theme, TRUE),
-              'schemes' => $schemes,
-            ),
-            'gradients' => $info['gradients'],
-          ),
-          'type' => 'setting',
-        ),
-      ),
+      'drupalSettings' => [
+        'color' => [
+          'reference' => color_get_palette($theme, TRUE),
+          'schemes' => $schemes,
+        ],
+        'gradients' => $info['gradients'],
+      ],
     ),
   );
 
diff --git a/core/modules/comment/src/CommentViewBuilder.php b/core/modules/comment/src/CommentViewBuilder.php
index 0e453aee3a10..b903250a92a6 100644
--- a/core/modules/comment/src/CommentViewBuilder.php
+++ b/core/modules/comment/src/CommentViewBuilder.php
@@ -364,23 +364,16 @@ public static function attachNewCommentsLinkMetadata(array $element, array $cont
     $query = $page_number ? array('page' => $page_number) : NULL;
 
     // Attach metadata.
-    $element['#attached']['js'][] = array(
-      'type' => 'setting',
-      'data' => array(
-        'comment' => array(
-          'newCommentsLinks' => array(
-            $context['entity_type'] => array(
-              $context['field_name'] => array(
-                $context['entity_id'] => array(
-                  'new_comment_count' => (int)$new,
-                  'first_new_comment_link' => \Drupal::urlGenerator()->generateFromPath('node/' . $entity->id(), array('query' => $query, 'fragment' => 'new')),
-                )
-              )
-            ),
-          )
-        ),
-      ),
-    );
+    $element['#attached']['drupalSettings']['comment']['newCommentsLinks'] = [
+      $context['entity_type'] => [
+        $context['field_name'] => [
+          $context['entity_id'] => [
+            'new_comment_count' => (int)$new,
+            'first_new_comment_link' => \Drupal::urlGenerator()->generateFromPath('node/' . $entity->id(), ['query' => $query, 'fragment' => 'new']),
+          ],
+        ],
+      ],
+    ];
 
     return $element;
   }
diff --git a/core/modules/content_translation/content_translation.admin.inc b/core/modules/content_translation/content_translation.admin.inc
index b1d1a2a00684..3f08c9a8cbc6 100644
--- a/core/modules/content_translation/content_translation.admin.inc
+++ b/core/modules/content_translation/content_translation.admin.inc
@@ -52,9 +52,9 @@ function content_translation_field_sync_widget(FieldDefinitionInterface $field)
         'library' => array(
           'content_translation/drupal.content_translation.admin',
         ),
-        'js' => array(
-          array('data' => array('contentTranslationDependentOptions' => $settings), 'type' => 'setting'),
-        ),
+        'drupalSettings' => [
+          'contentTranslationDependentOptions' => $settings,
+        ],
       ),
     );
   }
@@ -126,7 +126,7 @@ function _content_translation_form_language_content_settings_form_alter(array &$
   }
 
   $settings = array('dependent_selectors' => $dependent_options_settings);
-  $form['#attached']['js'][] = array('data' => array('contentTranslationDependentOptions' => $settings), 'type' => 'setting');
+  $form['#attached']['drupalSettings']['contentTranslationDependentOptions'] = $settings;
   $form['#validate'][] = 'content_translation_form_language_content_settings_validate';
   $form['#submit'][] = 'content_translation_form_language_content_settings_submit';
 }
diff --git a/core/modules/editor/src/Plugin/EditorManager.php b/core/modules/editor/src/Plugin/EditorManager.php
index da0081e5aebc..6fc04d070d52 100644
--- a/core/modules/editor/src/Plugin/EditorManager.php
+++ b/core/modules/editor/src/Plugin/EditorManager.php
@@ -96,10 +96,7 @@ public function getAttachments(array $format_ids) {
       return array();
     }
 
-    $attachments['js'][] = array(
-      'type' => 'setting',
-      'data' => $settings,
-    );
+    $attachments['drupalSettings'] = $settings;
 
     return $attachments;
   }
diff --git a/core/modules/editor/src/Tests/EditorManagerTest.php b/core/modules/editor/src/Tests/EditorManagerTest.php
index 66804ef3b931..c28d99dc83d1 100644
--- a/core/modules/editor/src/Tests/EditorManagerTest.php
+++ b/core/modules/editor/src/Tests/EditorManagerTest.php
@@ -90,27 +90,26 @@ public function testManager() {
       'library' => array(
         0 => 'editor_test/unicorn',
       ),
-      'js' => array(
-        0 => array(
-          'type' => 'setting',
-          'data' => array('editor' => array('formats' => array(
-            'full_html' => array(
+      'drupalSettings' => [
+        'editor' => [
+          'formats' => [
+            'full_html' => [
               'format'  => 'full_html',
               'editor' => 'unicorn',
               'editorSettings' => $unicorn_plugin->getJSSettings($editor),
               'editorSupportsContentFiltering' => TRUE,
               'isXssSafe' => FALSE,
-            )
-          )))
-        )
-      ),
+            ],
+          ],
+        ],
+      ],
     );
     $this->assertIdentical($expected, $this->editorManager->getAttachments(array('filtered_html', 'full_html')), 'Correct attachments when one text editor is enabled and retrieving attachments for multiple text formats.');
 
     // Case 4: a text editor available associated, but now with its JS settings
     // being altered via hook_editor_js_settings_alter().
     \Drupal::state()->set('editor_test_js_settings_alter_enabled', TRUE);
-    $expected['js'][0]['data']['editor']['formats']['full_html']['editorSettings']['ponyModeEnabled'] = FALSE;
+    $expected['drupalSettings']['editor']['formats']['full_html']['editorSettings']['ponyModeEnabled'] = FALSE;
     $this->assertIdentical($expected, $this->editorManager->getAttachments(array('filtered_html', 'full_html')), 'hook_editor_js_settings_alter() works correctly.');
   }
 
diff --git a/core/modules/field_ui/src/OverviewBase.php b/core/modules/field_ui/src/OverviewBase.php
index 2de43eeb7535..93adaa8c3932 100644
--- a/core/modules/field_ui/src/OverviewBase.php
+++ b/core/modules/field_ui/src/OverviewBase.php
@@ -210,10 +210,7 @@ public function tablePreRender($elements) {
       $elements['#regions'][$region_name]['rows_order'] = array_reduce($trees[$region_name], array($this, 'reduceOrder'));
     }
 
-    $elements['#attached']['js'][] = array(
-      'type' => 'setting',
-      'data' => array('fieldUIRowsData' => $js_settings),
-    );
+    $elements['#attached']['drupalSettings']['fieldUIRowsData'] = $js_settings;
 
     // If the custom #tabledrag is set and there is a HTML ID, add the table's
     // HTML ID to the options and attach the behavior.
diff --git a/core/modules/file/src/Controller/FileWidgetAjaxController.php b/core/modules/file/src/Controller/FileWidgetAjaxController.php
index 3187c3b7972e..3adc43a58d31 100644
--- a/core/modules/file/src/Controller/FileWidgetAjaxController.php
+++ b/core/modules/file/src/Controller/FileWidgetAjaxController.php
@@ -81,7 +81,7 @@ public function upload(Request $request) {
     $output = drupal_render($form);
     drupal_process_attached($form);
     $js = _drupal_add_js();
-    $settings = drupal_merge_js_settings($js['settings']['data']);
+    $settings = $js['drupalSettings']['data'];
 
     $response = new AjaxResponse();
     foreach ($commands as $command) {
diff --git a/core/modules/file/src/Element/ManagedFile.php b/core/modules/file/src/Element/ManagedFile.php
index 34c20e8cc921..40d0c3113014 100644
--- a/core/modules/file/src/Element/ManagedFile.php
+++ b/core/modules/file/src/Element/ManagedFile.php
@@ -256,10 +256,7 @@ public static function processManagedFile(&$element, FormStateInterface $form_st
     // Add the extension list to the page as JavaScript settings.
     if (isset($element['#upload_validators']['file_validate_extensions'][0])) {
       $extension_list = implode(',', array_filter(explode(' ', $element['#upload_validators']['file_validate_extensions'][0])));
-      $element['upload']['#attached']['js'] = [[
-        'type' => 'setting',
-        'data' => ['file' => ['elements' => ['#' . $element['#id'] => $extension_list]]],
-      ]];
+      $element['upload']['#attached']['drupalSettings']['file']['elements']['#' . $element['#id']] = $extension_list;
     }
 
     // Prefix and suffix used for Ajax replacement.
diff --git a/core/modules/history/history.module b/core/modules/history/history.module
index f4c9742926b4..bd8ca71b7054 100644
--- a/core/modules/history/history.module
+++ b/core/modules/history/history.module
@@ -193,16 +193,7 @@ function history_user_delete($account) {
  *   The updated $element.
  */
 function history_attach_timestamp(array $element, array $context) {
-  $element['#attached']['js'][] = array(
-    'type' => 'setting',
-    'data' => array(
-      'history' => array(
-        'lastReadTimestamps' => array(
-          $context['node_id'] => (int) history_read($context['node_id']),
-        )
-      ),
-    ),
-  );
-
+  $node_id = $context['node_id'];
+  $element['#attached']['drupalSettings']['history']['lastReadTimestamps'][$node_id] = (int) history_read($node_id);
   return $element;
 }
diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module
index c5add1d21b05..72f0e23e5477 100644
--- a/core/modules/locale/locale.module
+++ b/core/modules/locale/locale.module
@@ -535,14 +535,10 @@ function locale_library_alter(array &$library, $name) {
     $library['dependencies'][] = 'locale/drupal.locale.datepicker';
 
     $language_interface = \Drupal::languageManager()->getCurrentLanguage();
-    $settings['jquery']['ui']['datepicker'] = array(
+    $library['drupalSettings']['jquery']['ui']['datepicker'] = array(
       'isRTL' => $language_interface->getDirection() == LanguageInterface::DIRECTION_RTL,
       'firstDay' => \Drupal::config('system.date')->get('first_day'),
     );
-    $library['js'][] = array(
-      'type' => 'setting',
-      'data' => $settings,
-    );
   }
 }
 
diff --git a/core/modules/simpletest/src/Form/SimpletestTestForm.php b/core/modules/simpletest/src/Form/SimpletestTestForm.php
index b55de4c33208..03f16f511713 100644
--- a/core/modules/simpletest/src/Form/SimpletestTestForm.php
+++ b/core/modules/simpletest/src/Form/SimpletestTestForm.php
@@ -98,17 +98,10 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#title' => $this->t('Collapse'),
       '#suffix' => '<a href="#" class="simpletest-collapse">(' . $this->t('Collapse') . ')</a>',
     );
-    $form['tests']['#attached']['js'][] = array(
-      'type' => 'setting',
-      'data' => array(
-        'simpleTest' => array(
-          'images' => array(
-            drupal_render($image_collapsed),
-            drupal_render($image_extended),
-          ),
-        ),
-      ),
-    );
+    $form['tests']['#attached']['drupalSettings']['simpleTest']['images'] = [
+      drupal_render($image_collapsed),
+      drupal_render($image_extended),
+    ];
 
     // Generate the list of tests arranged by group.
     $groups = simpletest_test_get_all();
diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php
index 93a5047f1a89..44af34caad09 100644
--- a/core/modules/simpletest/src/WebTestBase.php
+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -1844,7 +1844,7 @@ protected function drupalProcessAjaxResponse($content, array $ajax_response, arr
       }
       switch ($command['command']) {
         case 'settings':
-          $drupal_settings = drupal_merge_js_settings(array($drupal_settings, $command['settings']));
+          $drupal_settings = NestedArray::mergeDeepArray([$drupal_settings, $command['settings']], TRUE);
           break;
 
         case 'insert':
diff --git a/core/modules/statistics/statistics.module b/core/modules/statistics/statistics.module
index 00ca78de2708..53dd91f4e198 100644
--- a/core/modules/statistics/statistics.module
+++ b/core/modules/statistics/statistics.module
@@ -40,10 +40,7 @@ function statistics_node_view(array &$build, EntityInterface $node, EntityViewDi
   if (!$node->isNew() && $view_mode == 'full' && node_is_page($node) && empty($node->in_preview)) {
     $build['statistics_content_counter']['#attached']['library'][] = 'statistics/drupal.statistics';
     $settings = array('data' => array('nid' => $node->id()), 'url' => _url(drupal_get_path('module', 'statistics') . '/statistics.php'));
-    $build['statistics_content_counter']['#attached']['js'][] = array(
-      'data' => array('statistics' => $settings),
-      'type' => 'setting',
-    );
+    $build['statistics_content_counter']['#attached']['drupalSettings']['statistics'] = $settings;
   }
 }
 
diff --git a/core/modules/system/src/Tests/Ajax/CommandsTest.php b/core/modules/system/src/Tests/Ajax/CommandsTest.php
index 945a763651b8..ec981a7efd69 100644
--- a/core/modules/system/src/Tests/Ajax/CommandsTest.php
+++ b/core/modules/system/src/Tests/Ajax/CommandsTest.php
@@ -118,10 +118,6 @@ function testAjaxCommands() {
     $commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'settings' command")));
     $expected = new SettingsCommand(array('ajax_forms_test' => array('foo' => 42)));
     $this->assertCommand($commands, $expected->render(), "'settings' AJAX command issued with correct data.");
-
-    // Test that the settings command merges settings properly.
-    $commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'settings' command with setting merging")));
-    $expected = new SettingsCommand(array('ajax_forms_test' => array('foo' => 9001)), TRUE);
-    $this->assertCommand($commands, $expected->render(), "'settings' AJAX command with setting merging.");
   }
+
 }
diff --git a/core/modules/system/src/Tests/Common/JavaScriptTest.php b/core/modules/system/src/Tests/Common/JavaScriptTest.php
index 2196de6dc0c1..a59d50effa6a 100644
--- a/core/modules/system/src/Tests/Common/JavaScriptTest.php
+++ b/core/modules/system/src/Tests/Common/JavaScriptTest.php
@@ -77,16 +77,14 @@ function testAddFile() {
    */
   function testAddSetting() {
     // Add a file in order to test default settings.
-    $attached['#attached']['library'][] = 'core/drupalSettings';
-    $this->render($attached);
+    $build['#attached']['library'][] = 'core/drupalSettings';
+    drupal_process_attached($build);
     $javascript = _drupal_add_js();
-    $last_settings = reset($javascript['settings']['data']);
-    $this->assertTrue(array_key_exists('currentPath', $last_settings['path']), 'The current path JavaScript setting is set correctly.');
+    $this->assertTrue(array_key_exists('currentPath', $javascript['drupalSettings']['data']['path']), 'The current path JavaScript setting is set correctly.');
 
     $javascript = _drupal_add_js(array('drupal' => 'rocks', 'dries' => 280342800), 'setting');
-    $last_settings = end($javascript['settings']['data']);
-    $this->assertEqual(280342800, $last_settings['dries'], 'JavaScript setting is set correctly.');
-    $this->assertEqual('rocks', $last_settings['drupal'], 'The other JavaScript setting is set correctly.');
+    $this->assertEqual(280342800, $javascript['drupalSettings']['data']['dries'], 'JavaScript setting is set correctly.');
+    $this->assertEqual('rocks', $javascript['drupalSettings']['data']['drupal'], 'The other JavaScript setting is set correctly.');
   }
 
   /**
@@ -156,81 +154,11 @@ function testAggregatedAttributes() {
   function testHeaderSetting() {
     $attached = array();
     $attached['#attached']['library'][] = 'core/drupalSettings';
+    // Nonsensical value to verify if it's possible to override path settings.
+    $attached['#attached']['drupalSettings']['path']['pathPrefix'] = 'yarhar';
     $this->render($attached);
 
     $javascript = drupal_get_js('header');
-    $this->assertTrue(strpos($javascript, 'baseUrl') > 0, 'Rendered JavaScript header returns baseUrl setting.');
-    $this->assertTrue(strpos($javascript, 'scriptPath') > 0, 'Rendered JavaScript header returns scriptPath setting.');
-    $this->assertTrue(strpos($javascript, 'pathPrefix') > 0, 'Rendered JavaScript header returns pathPrefix setting.');
-    $this->assertTrue(strpos($javascript, 'currentPath') > 0, 'Rendered JavaScript header returns currentPath setting.');
-
-    // Only the second of these two entries should appear in drupalSettings.
-    $attached = array();
-    $attached['#attached']['js'][] = array(
-      'type' => 'setting',
-      'data' => array('commonTest' => 'commonTestShouldNotAppear'),
-    );
-    $attached['#attached']['js'][] = array(
-      'type' => 'setting',
-      'data' => array('commonTest' => 'commonTestShouldAppear'),
-    );
-    // Only the second of these entries should appear in drupalSettings.
-    $attached['#attached']['js'][] = array(
-      'type' => 'setting',
-      'data' => array('commonTestJsArrayLiteral' => array('commonTestJsArrayLiteralOldValue')),
-    );
-    $attached['#attached']['js'][] = array(
-      'type' => 'setting',
-      'data' => array('commonTestJsArrayLiteral' => array('commonTestJsArrayLiteralNewValue')),
-    );
-    // Only the second of these two entries should appear in drupalSettings.
-    $attached['#attached']['js'][] = array(
-      'type' => 'setting',
-      'data' => array('commonTestJsObjectLiteral' => array('key' => 'commonTestJsObjectLiteralOldValue')),
-    );
-    $attached['#attached']['js'][] = array(
-      'type' => 'setting',
-      'data' => array('commonTestJsObjectLiteral' => array('key' => 'commonTestJsObjectLiteralNewValue')),
-    );
-    // Real world test case: multiple elements in a render array are adding the
-    // same (or nearly the same) JavaScript settings. When merged, they should
-    // contain all settings and not duplicate some settings.
-    $settings_one = array('moduleName' => array('ui' => array('button A', 'button B'), 'magical flag' => 3.14159265359));
-    $attached['#attached']['js'][] = array(
-      'type' => 'setting',
-      'data' => array('commonTestRealWorldIdentical' => $settings_one),
-    );
-    $attached['#attached']['js'][] = array(
-      'type' => 'setting',
-      'data' => array('commonTestRealWorldIdentical' => $settings_one),
-    );
-    $settings_two = array('moduleName' => array('ui' => array('button A', 'button B'), 'magical flag' => 3.14159265359, 'thingiesOnPage' => array('id1' => array())));
-    $attached['#attached']['js'][] = array(
-      'type' => 'setting',
-      'data' => array('commonTestRealWorldAlmostIdentical' => $settings_two),
-    );
-    $settings_two = array('moduleName' => array('ui' => array('button C', 'button D'), 'magical flag' => 3.14, 'thingiesOnPage' => array('id2' => array())));
-    $attached['#attached']['js'][] = array(
-      'type' => 'setting',
-      'data' => array('commonTestRealWorldAlmostIdentical' => $settings_two),
-    );
-
-    $this->render($attached);
-    $javascript = drupal_get_js('header');
-
-    // Test whether _drupal_add_js can be used to override a previous setting.
-    $this->assertTrue(strpos($javascript, 'commonTestShouldAppear') > 0, 'Rendered JavaScript header returns custom setting.');
-    $this->assertTrue(strpos($javascript, 'commonTestShouldNotAppear') === FALSE, '_drupal_add_js() correctly overrides a custom setting.');
-
-    // Test whether _drupal_add_js can be used to add and override a JavaScript
-    // array literal (an indexed PHP array) values.
-    $array_override = strpos($javascript, 'commonTestJsArrayLiteralNewValue') > 0 && strpos($javascript, 'commonTestJsArrayLiteralOldValue') === FALSE;
-    $this->assertTrue($array_override, '_drupal_add_js() correctly overrides settings within an array literal (indexed array).');
-
-    // Test whether _drupal_add_js can be used to add and override a JavaScript
-    // object literal (an associate PHP array) values.
-    $associative_array_override = strpos($javascript, 'commonTestJsObjectLiteralNewValue') > 0 && strpos($javascript, 'commonTestJsObjectLiteralOldValue') === FALSE;
-    $this->assertTrue($associative_array_override, '_drupal_add_js() correctly overrides settings within an object literal (associative array).');
 
     // Parse the generated drupalSettings <script> back to a PHP representation.
     $startToken = 'drupalSettings = ';
@@ -240,10 +168,20 @@ function testHeaderSetting() {
     $json  = Unicode::substr($javascript, $start, $end - $start + 1);
     $parsed_settings = Json::decode($json);
 
-    // Test whether the two real world cases are handled correctly.
-    $settings_two['moduleName']['thingiesOnPage']['id1'] = array();
-    $this->assertIdentical($settings_one, $parsed_settings['commonTestRealWorldIdentical'], '_drupal_add_js handled real world test case 1 correctly.');
-    $this->assertEqual($settings_two, $parsed_settings['commonTestRealWorldAlmostIdentical'], '_drupal_add_js handled real world test case 2 correctly.');
+    // Test whether the settings for core/drupalSettings are available.
+    $this->assertTrue(isset($parsed_settings['path']['baseUrl']), 'drupalSettings.path.baseUrl is present.');
+    $this->assertTrue(isset($parsed_settings['path']['scriptPath']), 'drupalSettings.path.scriptPath is present.');
+    $this->assertIdentical($parsed_settings['path']['pathPrefix'], 'yarhar', 'drupalSettings.path.pathPrefix is present and has the correct (overridden) value.');
+    $this->assertIdentical($parsed_settings['path']['currentPath'], '', 'drupalSettings.path.currentPath is present and has the correct value.');
+    $this->assertIdentical($parsed_settings['path']['currentPathIsAdmin'], FALSE, 'drupalSettings.path.currentPathIsAdmin is present and has the correct value.');
+    $this->assertIdentical($parsed_settings['path']['isFront'], FALSE, 'drupalSettings.path.isFront is present and has the correct value.');
+    $this->assertIdentical($parsed_settings['path']['currentLanguage'], 'en', 'drupalSettings.path.currentLanguage is present and has the correct value.');
+
+    // Tests whether altering JavaScript settings via hook_js_settings_alter()
+    // is working as expected.
+    // @see common_test_js_settings_alter()
+    $this->assertIdentical($parsed_settings['locale']['pluralDelimiter'], '☃');
+    $this->assertIdentical($parsed_settings['foo'], 'bar');
   }
 
   /**
diff --git a/core/modules/system/src/Tests/Common/MergeAttachmentsTest.php b/core/modules/system/src/Tests/Common/MergeAttachmentsTest.php
index 7eebe329624b..51d7c6234b77 100644
--- a/core/modules/system/src/Tests/Common/MergeAttachmentsTest.php
+++ b/core/modules/system/src/Tests/Common/MergeAttachmentsTest.php
@@ -12,6 +12,8 @@
 /**
  * Tests the merging of attachments.
  *
+ * @see drupal_merge_attached()
+ *
  * @group Common
  */
 class MergeAttachmentsTest extends DrupalUnitTestBase {
@@ -165,36 +167,30 @@ function testJsSettingMerging() {
     $a['#attached'] = array(
       'js' => array(
         'foo.js' => array(),
-        array(
-          'type' => 'setting',
-          'data' => array('foo' => array('d')),
-        ),
         'bar.js' => array(),
       ),
+      'drupalSettings' => [
+        'foo' => ['d'],
+      ],
     );
     $b['#attached'] = array(
       'js' => array(
-        83 => array(
-          'type' => 'setting',
-          'data' => array('bar' => array('a', 'b', 'c')),
-        ),
         'baz.js' => array(),
       ),
+      'drupalSettings' => [
+        'bar' => ['a', 'b', 'c'],
+      ],
     );
     $expected['#attached'] = array(
       'js' => array(
         'foo.js' => array(),
-        0 => array(
-          'type' => 'setting',
-          'data' => array('foo' => array('d')),
-        ),
         'bar.js' => array(),
-        1 => array(
-          'type' => 'setting',
-          'data' => array('bar' => array('a', 'b', 'c')),
-        ),
         'baz.js' => array(),
       ),
+      'drupalSettings' => [
+        'foo' => ['d'],
+        'bar' => ['a', 'b', 'c'],
+      ],
     );
     $this->assertIdentical($expected['#attached'], drupal_merge_attached($a['#attached'], $b['#attached']), 'Attachments merged correctly.');
 
@@ -202,48 +198,95 @@ function testJsSettingMerging() {
     // order.
     $expected['#attached'] = array(
       'js' => array(
-        0 => array(
-          'type' => 'setting',
-          'data' => array('bar' => array('a', 'b', 'c')),
-        ),
         'baz.js' => array(),
         'foo.js' => array(),
-        1 => array(
-          'type' => 'setting',
-          'data' => array('foo' => array('d')),
-        ),
         'bar.js' => array(),
       ),
+      'drupalSettings' => [
+        'bar' => ['a', 'b', 'c'],
+        'foo' => ['d'],
+      ],
     );
     $this->assertIdentical($expected['#attached'], drupal_merge_attached($b['#attached'], $a['#attached']), 'Attachments merged correctly; opposite merging yields opposite order.');
 
-    // Merging with duplicates: JavaScript setting duplicates are simply
-    // retained, it's up to the rest of the system (drupal_merge_js_settings())
-    // to handle duplicates.
-    $b['#attached']['js'][] = array(
-      'type' => 'setting',
-      'data' => array('foo' => array('a', 'b', 'c')),
-    );
+    // Merging with duplicates (simple case).
+    $b['#attached']['drupalSettings']['foo'] = ['a', 'b', 'c'];
     $expected['#attached'] = array(
       'js' => array(
         'foo.js' => array(),
-        0 => array(
-          'type' => 'setting',
-          'data' => array('foo' => array('d')),
-        ),
         'bar.js' => array(),
-        1 => array(
-          'type' => 'setting',
-          'data' => array('bar' => array('a', 'b', 'c')),
-        ),
         'baz.js' => array(),
-        2 => array(
-          'type' => 'setting',
-          'data' => array('foo' => array('a', 'b', 'c')),
-        ),
       ),
+      'drupalSettings' => [
+        'foo' => ['a', 'b', 'c'],
+        'bar' => ['a', 'b', 'c'],
+      ],
+    );
+    $this->assertIdentical($expected['#attached'], drupal_merge_attached($a['#attached'], $b['#attached']));
+
+    // Merging with duplicates (simple case) in the opposite direction yields
+    // the opposite JS setting asset order, but also opposite overriding order.
+    $expected['#attached'] = array(
+      'js' => array(
+        'baz.js' => array(),
+        'foo.js' => array(),
+        'bar.js' => array(),
+      ),
+      'drupalSettings' => [
+        'bar' => ['a', 'b', 'c'],
+        'foo' => ['d', 'b', 'c'],
+      ],
     );
-    $this->assertIdentical($expected['#attached'], drupal_merge_attached($a['#attached'], $b['#attached']), 'Attachments merged correctly; JavaScript asset duplicates removed, JavaScript setting asset duplicates retained.');
+    $this->assertIdentical($expected['#attached'], drupal_merge_attached($b['#attached'], $a['#attached']));
+
+    // Merging with duplicates: complex case.
+    // Only the second of these two entries should appear in drupalSettings.
+    $build = array();
+    $build['a']['#attached']['drupalSettings']['commonTest'] = 'firstValue';
+    $build['b']['#attached']['drupalSettings']['commonTest'] = 'secondValue';
+    // Only the second of these entries should appear in drupalSettings.
+    $build['a']['#attached']['drupalSettings']['commonTestJsArrayLiteral'] = ['firstValue'];
+    $build['b']['#attached']['drupalSettings']['commonTestJsArrayLiteral'] = ['secondValue'];
+    // Only the second of these two entries should appear in drupalSettings.
+    $build['a']['#attached']['drupalSettings']['commonTestJsObjectLiteral'] = ['key' => 'firstValue'];
+    $build['b']['#attached']['drupalSettings']['commonTestJsObjectLiteral'] = ['key' => 'secondValue'];
+    // Real world test case: multiple elements in a render array are adding the
+    // same (or nearly the same) JavaScript settings. When merged, they should
+    // contain all settings and not duplicate some settings.
+    $settings_one = array('moduleName' => array('ui' => array('button A', 'button B'), 'magical flag' => 3.14159265359));
+    $build['a']['#attached']['drupalSettings']['commonTestRealWorldIdentical'] = $settings_one;
+    $build['b']['#attached']['drupalSettings']['commonTestRealWorldIdentical'] = $settings_one;
+    $settings_two_a = array('moduleName' => array('ui' => array('button A', 'button B', 'button C'), 'magical flag' => 3.14159265359, 'thingiesOnPage' => array('id1' => array())));
+    $build['a']['#attached']['drupalSettings']['commonTestRealWorldAlmostIdentical'] = $settings_two_a;
+    $settings_two_b = array('moduleName' => array('ui' => array('button D', 'button E'), 'magical flag' => 3.14, 'thingiesOnPage' => array('id2' => array())));
+    $build['b']['#attached']['drupalSettings']['commonTestRealWorldAlmostIdentical'] = $settings_two_b;
+
+    $merged = drupal_merge_attached($build['a']['#attached'], $build['b']['#attached']);
+
+    // Test whether #attached can be used to override a previous setting.
+    $this->assertIdentical('secondValue', $merged['drupalSettings']['commonTest']);
+
+    // Test whether #attached can be used to add and override a JavaScript
+    // array literal (an indexed PHP array) values.
+    $this->assertIdentical('secondValue', $merged['drupalSettings']['commonTestJsArrayLiteral'][0]);
+
+    // Test whether #attached can be used to add and override a JavaScript
+    // object literal (an associate PHP array) values.
+    $this->assertIdentical('secondValue', $merged['drupalSettings']['commonTestJsObjectLiteral']['key']);
+
+    // Test whether the two real world cases are handled correctly: the first
+    // adds the exact same settings twice and hence tests idempotency, the
+    // second adds *almost* the same settings twice: the second time, some
+    // values are altered, and some key-value pairs are added.
+    $settings_two['moduleName']['thingiesOnPage']['id1'] = array();
+    $this->assertIdentical($settings_one, $merged['drupalSettings']['commonTestRealWorldIdentical']);
+    $expected_settings_two = $settings_two_a;
+    $expected_settings_two['moduleName']['ui'][0] = 'button D';
+    $expected_settings_two['moduleName']['ui'][1] = 'button E';
+    $expected_settings_two['moduleName']['ui'][2] = 'button C';
+    $expected_settings_two['moduleName']['magical flag'] = 3.14;
+    $expected_settings_two['moduleName']['thingiesOnPage']['id2'] = [];
+    $this->assertIdentical($expected_settings_two, $merged['drupalSettings']['commonTestRealWorldAlmostIdentical']);
   }
 
 }
diff --git a/core/modules/system/src/Tests/Common/RenderTest.php b/core/modules/system/src/Tests/Common/RenderTest.php
index 8f801d88eea0..206f0a568a62 100644
--- a/core/modules/system/src/Tests/Common/RenderTest.php
+++ b/core/modules/system/src/Tests/Common/RenderTest.php
@@ -493,7 +493,7 @@ function testDrupalRenderPostRenderCache() {
     $context = array('foo' => $this->randomContextValue());
     $test_element = array();
     $test_element['#markup'] = '';
-    $test_element['#attached']['js'][] = array('type' => 'setting', 'data' => array('foo' => 'bar'));
+    $test_element['#attached']['drupalSettings']['foo'] = 'bar';
     $test_element['#post_render_cache']['common_test_post_render_cache'] = array(
       $context
     );
@@ -504,11 +504,11 @@ function testDrupalRenderPostRenderCache() {
     $output = drupal_render_root($element);
     $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
     $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden.');
-    $expected_js = [
-      ['type' => 'setting', 'data' => ['foo' => 'bar']],
-      ['type' => 'setting', 'data' => ['common_test' => $context]],
+    $expected_js_settings = [
+      'foo' => 'bar',
+      'common_test' => $context,
     ];
-    $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; both the original JavaScript setting and the one added by the #post_render_cache callback exist.');
+    $this->assertIdentical($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the #post_render_cache callback exist.');
 
     // The cache system is turned off for POST requests.
     $request_method = \Drupal::request()->getMethod();
@@ -522,11 +522,11 @@ function testDrupalRenderPostRenderCache() {
     $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
     $this->assertTrue(isset($element['#printed']), 'No cache hit');
     $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden.');
-    $expected_js = [
-      ['type' => 'setting', 'data' => ['foo' => 'bar']],
-      ['type' => 'setting', 'data' => ['common_test' => $context]],
+    $expected_js_settings = [
+      'foo' => 'bar',
+      'common_test' => $context,
     ];
-    $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; both the original JavaScript setting and the one added by the #post_render_cache callback exist.');
+    $this->assertIdentical($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the #post_render_cache callback exist.');
 
     // GET request: validate cached data.
     $element = array('#cache' => array('cid' => 'post_render_cache_test_GET'));
@@ -546,11 +546,11 @@ function testDrupalRenderPostRenderCache() {
     $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
     $this->assertFalse(isset($element['#printed']), 'Cache hit');
     $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden.');
-    $expected_js = [
-      ['type' => 'setting', 'data' => ['foo' => 'bar']],
-      ['type' => 'setting', 'data' => ['common_test' => $context]],
+    $expected_js_settings = [
+      'foo' => 'bar',
+      'common_test' => $context,
     ];
-    $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; both the original JavaScript setting and the one added by the #post_render_cache callback exist.');
+    $this->assertIdentical($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the #post_render_cache callback exist.');
 
     // Verify behavior when handling a non-GET request, e.g. a POST request:
     // also in that case, #post_render_cache callbacks must be called.
@@ -564,11 +564,11 @@ function testDrupalRenderPostRenderCache() {
     $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
     $this->assertTrue(isset($element['#printed']), 'No cache hit');
     $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden.');
-    $expected_js = [
-      ['type' => 'setting', 'data' => ['foo' => 'bar']],
-      ['type' => 'setting', 'data' => ['common_test' => $context]],
+    $expected_js_settings = [
+      'foo' => 'bar',
+      'common_test' => $context,
     ];
-    $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; both the original JavaScript setting and the one added by the #post_render_cache callback exist.');
+    $this->assertIdentical($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the #post_render_cache callback exist.');
 
     // POST request: Ensure no data was cached.
     $element = array('#cache' => array('cid' => 'post_render_cache_test_POST'));
@@ -604,9 +604,9 @@ function testDrupalRenderChildrenPostRenderCache() {
       ),
       '#title' => 'Parent',
       '#attached' => array(
-        'js' => array(
-          array('type' => 'setting', 'data' => array('foo' => 'bar'))
-        ),
+        'drupalSettings' => [
+          'foo' => 'bar',
+        ],
       ),
     );
     $test_element['child'] = array(
@@ -628,22 +628,20 @@ function testDrupalRenderChildrenPostRenderCache() {
     $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
     $this->assertTrue(isset($element['#printed']), 'No cache hit');
     $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden.');
-    $expected_js = [
-      ['type' => 'setting', 'data' => ['foo' => 'bar']],
-      ['type' => 'setting', 'data' => ['common_test' => $context_1 ]],
-      ['type' => 'setting', 'data' => ['common_test' => $context_2 ]],
-      ['type' => 'setting', 'data' => ['common_test' => $context_3 ]],
+    $expected_js_settings = [
+      'foo' => 'bar',
+      'common_test' => $context_1 + $context_2 + $context_3,
     ];
-    $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; both the original JavaScript setting and the ones added by each #post_render_cache callback exist.');
+    $this->assertIdentical($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each #post_render_cache callback exist.');
 
     // GET request: validate cached data.
     $element = array('#cache' => $element['#cache']);
     $cached_element = \Drupal::cache('render')->get(drupal_render_cid_create($element))->data;
     $expected_element = array(
       '#attached' => array(
-        'js' => array(
-          array('type' => 'setting', 'data' => array('foo' => 'bar'))
-        ),
+        'drupalSettings' => [
+          'foo' => 'bar',
+        ],
         'library' => array(
           'core/drupal.collapse',
           'core/drupal.collapse',
@@ -675,7 +673,7 @@ function testDrupalRenderChildrenPostRenderCache() {
     $output = drupal_render_root($element);
     $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
     $this->assertFalse(isset($element['#printed']), 'Cache hit');
-    $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; both the original JavaScript setting and the ones added by each #post_render_cache callback exist.');
+    $this->assertIdentical($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each #post_render_cache callback exist.');
 
     // Test case 2.
     // Use the exact same element, but now unset #cache.
@@ -684,7 +682,7 @@ function testDrupalRenderChildrenPostRenderCache() {
     $output = drupal_render_root($element);
     $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
     $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden.');
-    $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; both the original JavaScript setting and the ones added by each #post_render_cache callback exist.');
+    $this->assertIdentical($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each #post_render_cache callback exist.');
 
     // Test case 3.
     // Create an element with a child and subchild. Each element has the same
@@ -701,7 +699,7 @@ function testDrupalRenderChildrenPostRenderCache() {
     $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
     $this->assertTrue(isset($element['#printed']), 'No cache hit');
     $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden.');
-    $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; both the original JavaScript setting and the ones added by each #post_render_cache callback exist.');
+    $this->assertIdentical($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each #post_render_cache callback exist.');
 
     // GET request: validate cached data for both the parent and child.
     $element = $test_element;
@@ -711,9 +709,9 @@ function testDrupalRenderChildrenPostRenderCache() {
     $cached_child_element = \Drupal::cache('render')->get(drupal_render_cid_create($element['child']))->data;
     $expected_parent_element = array(
       '#attached' => array(
-        'js' => array(
-          array('type' => 'setting', 'data' => array('foo' => 'bar'))
-        ),
+        'drupalSettings' => [
+          'foo' => 'bar',
+        ],
         'library' => array(
           'core/drupal.collapse',
           'core/drupal.collapse',
@@ -771,7 +769,7 @@ function testDrupalRenderChildrenPostRenderCache() {
     $output = drupal_render_root($element);
     $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
     $this->assertFalse(isset($element['#printed']), 'Cache hit');
-    $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; both the original JavaScript setting and the ones added by each #post_render_cache callback exist.');
+    $this->assertIdentical($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each #post_render_cache callback exist.');
 
     // GET request: #cache enabled, cache hit, child element.
     $element = $test_element;
@@ -780,11 +778,10 @@ function testDrupalRenderChildrenPostRenderCache() {
     $output = drupal_render_root($element);
     $this->assertIdentical($output, '<p>overridden</p>', 'Output is overridden.');
     $this->assertFalse(isset($element['#printed']), 'Cache hit');
-    $expected_js = [
-      ['type' => 'setting', 'data' => ['common_test' => $context_2 ]],
-      ['type' => 'setting', 'data' => ['common_test' => $context_3 ]],
+    $expected_js_settings = [
+      'common_test' => $context_2 + $context_3,
     ];
-    $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; both the original JavaScript setting and the ones added by each #post_render_cache callback exist.');
+    $this->assertIdentical($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each #post_render_cache callback exist.');
 
     // Restore the previous request method.
     \Drupal::request()->setMethod($request_method);
@@ -821,10 +818,10 @@ function testDrupalRenderRenderCachePlaceholder() {
     $element = $test_element;
     $output = drupal_render_root($element);
     $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
-    $expected_js = [
-      ['type' => 'setting', 'data' => ['common_test' => $context]],
+    $expected_js_settings = [
+      'common_test' => $context,
     ];
-    $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; JavaScript setting is added to page.');
+    $this->assertIdentical($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; JavaScript setting is added to page.');
 
     // The cache system is turned off for POST requests.
     $request_method = \Drupal::request()->getMethod();
@@ -838,7 +835,7 @@ function testDrupalRenderRenderCachePlaceholder() {
     $this->assertTrue(isset($element['#printed']), 'No cache hit');
     $this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.');
     $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
-    $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; JavaScript setting is added to page.');
+    $this->assertIdentical($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; JavaScript setting is added to page.');
 
     // GET request: validate cached data.
     $expected_token = $context['token'];
@@ -874,7 +871,7 @@ function testDrupalRenderRenderCachePlaceholder() {
     $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
     $this->assertFalse(isset($element['#printed']), 'Cache hit');
     $this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.');
-    $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; JavaScript setting is added to page.');
+    $this->assertIdentical($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; JavaScript setting is added to page.');
 
     // Restore the previous request method.
     \Drupal::request()->setMethod($request_method);
@@ -913,10 +910,10 @@ function testDrupalRenderChildElementRenderCachePlaceholder() {
     $element = $test_element;
     $output = drupal_render_root($element);
     $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
-    $expected_js = [
-      ['type' => 'setting', 'data' => ['common_test' => $context]],
+    $expected_js_settings = [
+      'common_test' => $context,
     ];
-    $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; JavaScript setting is added to page.');
+    $this->assertIdentical($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; JavaScript setting is added to page.');
 
     // The cache system is turned off for POST requests.
     $request_method = \Drupal::request()->getMethod();
@@ -931,7 +928,7 @@ function testDrupalRenderChildElementRenderCachePlaceholder() {
     $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
     $this->assertTrue(isset($element['#printed']), 'No cache hit');
     $this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.');
-    $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; JavaScript setting is added to page.');
+    $this->assertIdentical($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; JavaScript setting is added to page.');
 
     // GET request: validate cached data for child element.
     $expected_token = $context['token'];
@@ -1023,7 +1020,7 @@ function testDrupalRenderChildElementRenderCachePlaceholder() {
     $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
     $this->assertFalse(isset($element['#printed']), 'Cache hit');
     $this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.');
-    $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified; JavaScript setting is added to page.');
+    $this->assertIdentical($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; JavaScript setting is added to page.');
 
     // Restore the previous request method.
     \Drupal::request()->setMethod($request_method);
@@ -1048,10 +1045,10 @@ function testRecursivePostRenderCache() {
     $output = drupal_render_root($element);
     $this->assertEqual('<p>overridden</p>', $output, 'The output has been modified by the indirect, recursive #post_render_cache callback.');
     $this->assertIdentical($element['#markup'], '<p>overridden</p>', '#markup is overridden by the indirect, recursive #post_render_cache callback.');
-    $expected_js = [
-      ['type' => 'setting', 'data' => ['common_test' => $context]],
+    $expected_js_settings = [
+      'common_test' => $context,
     ];
-    $this->assertIdentical($element['#attached']['js'], $expected_js, '#attached is modified by the indirect, recursive #post_render_cache callback.');
+    $this->assertIdentical($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified by the indirect, recursive #post_render_cache callback.');
   }
 
   /**
@@ -1073,12 +1070,7 @@ public static function bubblingPreRender($elements) {
       ),
       'child_asset' => array(
         '#attached' => array(
-          'js' => array(
-            array(
-              'type' => 'setting',
-              'data' => array('foo' => 'bar'),
-            )
-          ),
+          'drupalSettings' => array('foo' => 'bar'),
         ),
         '#markup' => 'Asset!',
       ),
@@ -1145,12 +1137,7 @@ function testDrupalRenderBubbling() {
       $this->assertEqual('Cache tag!Asset!Post-render cache!barquxNested!Cached nested!', trim($output), 'Expected HTML generated.');
       $this->assertEqual(array('child:cache_tag'), $test_element['#cache']['tags'], 'Expected cache tags found.');
       $expected_attached = array(
-        'js' => array(
-          0 => array(
-            'type' => 'setting',
-            'data' => array('foo' => 'bar'),
-          ),
-        ),
+        'drupalSettings' => array('foo' => 'bar'),
       );
       $this->assertEqual($expected_attached, $test_element['#attached'], 'Expected assets found.');
       $this->assertEqual([], $test_element['#post_render_cache'], '#post_render_cache property is empty after rendering');
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 4eaf7bb13b2c..b9ca4c80823a 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -629,6 +629,49 @@ function system_page_attachments(array &$page) {
   }
 }
 
+/**
+ * Implements hook_js_settings_alter().
+ *
+ * Generates the values for the core/drupalSettings library.
+ */
+function system_js_settings_alter(&$settings) {
+  // url() generates the script and prefix using hook_url_outbound_alter().
+  // Instead of running the hook_url_outbound_alter() again here, extract
+  // them from url().
+  // @todo Make this less hacky: http://drupal.org/node/1547376.
+  $request = \Drupal::request();
+  $scriptPath = $request->getScriptName();
+
+  $pathPrefix = '';
+  $current_query = $request->query->all();
+  _url('', array('script' => &$scriptPath, 'prefix' => &$pathPrefix));
+  $current_path = \Drupal::routeMatch()->getRouteName() ? Url::fromRouteMatch(\Drupal::routeMatch())->getInternalPath() : '';
+  $current_path_is_admin = \Drupal::service('router.admin_context')->isAdminRoute();
+  $path_settings = [
+    'baseUrl' => $request->getBaseUrl() . '/',
+    'scriptPath' => $scriptPath,
+    'pathPrefix' => $pathPrefix,
+    'currentPath' => $current_path,
+    'currentPathIsAdmin' => $current_path_is_admin,
+    'isFront' => drupal_is_front_page(),
+    'currentLanguage' => \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId(),
+  ];
+  if (!empty($current_query)) {
+    ksort($current_query);
+    $path_settings['currentQuery'] = (object) $current_query;
+  }
+
+  // Only set core/drupalSettings values that haven't been set already.
+  foreach ($path_settings as $key => $value) {
+    if (!isset($settings['path'][$key])) {
+      $settings['path'][$key] = $value;
+    }
+  }
+  if (!isset($settings['locale']['pluralDelimiter'])) {
+    $settings['locale']['pluralDelimiter'] = LOCALE_PLURAL_DELIMITER;
+  }
+}
+
 /**
  * Implements hook_form_FORM_ID_alter().
  */
diff --git a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module
index 2ecbe2a0cb88..2665c30d9953 100644
--- a/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module
+++ b/core/modules/system/tests/modules/ajax_forms_test/ajax_forms_test.module
@@ -162,32 +162,6 @@ function ajax_forms_test_advanced_commands_add_css_callback($form, FormStateInte
   return $response;
 }
 
-/**
- * Ajax callback for 'settings' but with setting overrides.
- */
-function ajax_forms_test_advanced_commands_settings_with_merging_callback($form, FormStateInterface $form_state) {
-  $attached = array(
-    '#attached' => array(
-      'js' => array(
-        0 => array(
-          'type' => 'setting',
-          'data' => array('ajax_forms_test' => array('foo' => 42)),
-        ),
-        '1' => array(
-          'type' => 'setting',
-          'data' => array('ajax_forms_test' => array('foo' => 9001)),
-        ),
-      ),
-    ),
-  );
-  // @todo Why is this being tested via an explicit drupal_render() call?
-  drupal_render($attached);
-  drupal_process_attached($attached);
-
-  $response = new AjaxResponse();
-  return $response;
-}
-
 /**
  * Ajax form callback: Selects the 'drivertext' element of the validation form.
  */
diff --git a/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestCommandsForm.php b/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestCommandsForm.php
index 46525e72082e..f0a3811f6517 100644
--- a/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestCommandsForm.php
+++ b/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestCommandsForm.php
@@ -194,18 +194,6 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       ),
     );
 
-    // Tests the 'settings' command with a callback which sets the same
-    // setting multiple times. This is used to check that settings are
-    // merged properly (e.g., array_merge_recursive() merges settings
-    // incorrectly, #1356170).
-    $form['settings_command_with_merging_example'] = array(
-      '#type' => 'submit',
-      '#value' => $this->t("AJAX 'settings' command with setting merging"),
-      '#ajax' => array(
-        'callback' => 'ajax_forms_test_advanced_commands_settings_with_merging_callback',
-      ),
-    );
-
     $form['submit'] = array(
       '#type' => 'submit',
       '#value' => $this->t('Submit'),
diff --git a/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestLazyLoadForm.php b/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestLazyLoadForm.php
index 32b476b32c98..41e8499bd59a 100644
--- a/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestLazyLoadForm.php
+++ b/core/modules/system/tests/modules/ajax_forms_test/src/Form/AjaxFormsTestLazyLoadForm.php
@@ -30,10 +30,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     // commands will be a settings command. We can then check the settings
     // command to ensure that the 'currentPath' setting is not part
     // of the Ajax response.
-    $form['#attached']['js'][] = array(
-      'type' => 'setting',
-      'data' => array('test' => 'currentPathUpdate'),
-    );
+    $form['#attached']['drupalSettings']['test'] = 'currentPathUpdate';
     $form['add_files'] = array(
       '#title' => $this->t('Add files'),
       '#type' => 'checkbox',
@@ -63,11 +60,8 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
             'system/admin',
             'system/drupal.system',
           ],
-          'js' => [
-            0 => [
-              'type' => 'setting',
-              'data' => ['ajax_forms_test_lazy_load_form_submit' => 'executed'],
-            ],
+          'drupalSettings' => [
+            'ajax_forms_test_lazy_load_form_submit' => 'executed',
           ],
         ],
       ];
diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.module b/core/modules/system/tests/modules/ajax_test/ajax_test.module
index 870a1577b749..1f19aacbbe8c 100644
--- a/core/modules/system/tests/modules/ajax_test/ajax_test.module
+++ b/core/modules/system/tests/modules/ajax_test/ajax_test.module
@@ -26,11 +26,8 @@
 function ajax_test_render() {
   $attached = array(
     '#attached' => array(
-      'js' => array(
-        0 => array(
-          'type' => 'setting',
-          'data' => array('ajax' => 'test'),
-        ),
+      'drupalSettings' => array(
+        'ajax' => 'test',
       ),
     ),
   );
@@ -64,17 +61,14 @@ function ajax_test_order() {
         // Add two JavaScript files (first to the footer, should appear last).
         $path . '/system.modules.js' => array('scope' => 'footer'),
         $path . '/system.js' => array(),
-        // Finally, add a JavaScript setting.
-        0 => array(
-          'type' => 'setting',
-          'data' => array('ajax' => 'test'),
-        ),
+      ),
+      // Finally, add a JavaScript setting.
+      'drupalSettings' => array(
+        'ajax' => 'test',
       ),
     ),
   );
 
-  // @todo Why is this being tested via an explicit drupal_render() call?
-  drupal_render($attached);
   drupal_process_attached($attached);
 
   return $response;
diff --git a/core/modules/system/tests/modules/common_test/common_test.module b/core/modules/system/tests/modules/common_test/common_test.module
index 24aa3da889ad..8dbf98a236b3 100644
--- a/core/modules/system/tests/modules/common_test/common_test.module
+++ b/core/modules/system/tests/modules/common_test/common_test.module
@@ -208,12 +208,10 @@ function common_test_post_render_cache(array $element, array $context) {
   $element['#markup'] = '<p>overridden</p>';
 
   // Extend #attached.
-  $element['#attached']['js'][] = array(
-    'type' => 'setting',
-    'data' => array(
-      'common_test' => $context
-    ),
-  );
+  if (!isset($element['#attached']['drupalSettings']['common_test'])) {
+    $element['#attached']['drupalSettings']['common_test'] = [];
+  }
+  $element['#attached']['drupalSettings']['common_test'] += $context;
 
   // Set new property.
   $element['#context_test'] = $context;
@@ -238,14 +236,9 @@ function common_test_post_render_cache_placeholder(array $element, array $contex
   $replace_element = array(
     '#markup' => '<bar>' . $context['bar'] . '</bar>',
     '#attached' => array(
-      'js' => array(
-        array(
-          'type' => 'setting',
-          'data' => array(
-            'common_test' => $context,
-          ),
-        ),
-      ),
+      'drupalSettings' => [
+        'common_test' => $context,
+      ],
     ),
   );
   $markup = drupal_render($replace_element);
@@ -325,3 +318,18 @@ function common_test_page_attachments_alter(array &$page) {
     ];
   }
 }
+
+/**
+ * Implements hook_js_settings_alter().
+ *
+ * @see \Drupal\system\Tests\Common\JavaScriptTest::testHeaderSetting()
+ */
+function common_test_js_settings_alter(&$settings) {
+  // Modify an existing setting.
+  if (array_key_exists('pluralDelimiter', $settings['locale'])) {
+    $settings['locale']['pluralDelimiter'] = '☃';
+  }
+
+  // Add a setting.
+  $settings['foo'] = 'bar';
+}
diff --git a/core/modules/system/tests/modules/test_page_test/test_page_test.module b/core/modules/system/tests/modules/test_page_test/test_page_test.module
index 93c10c5f4186..f2967cfaf4da 100644
--- a/core/modules/system/tests/modules/test_page_test/test_page_test.module
+++ b/core/modules/system/tests/modules/test_page_test/test_page_test.module
@@ -6,10 +6,7 @@
  * @deprecated Use \Drupal\test_page_test\Controller\TestPageTestController::testPage()
  */
 function test_page_test_page() {
-  $attached['js'][] = array(
-    'data' => array('test-setting' => 'azAZ09();.,\\\/-_{}'),
-    'type' => 'setting',
-  );
+  $attached['drupalSettings']['test-setting'] = 'azAZ09();.,\\\/-_{}';
   return array(
     '#title' => t('Test page'),
     '#markup' => t('Test page text.'),
diff --git a/core/modules/system/theme.api.php b/core/modules/system/theme.api.php
index 5eeeeea4d867..a335e3868e07 100644
--- a/core/modules/system/theme.api.php
+++ b/core/modules/system/theme.api.php
@@ -717,6 +717,27 @@ function hook_js_alter(&$javascript) {
   $javascript['core/assets/vendor/jquery/jquery.js']['data'] = drupal_get_path('module', 'jquery_update') . '/jquery.js';
 }
 
+/**
+ * Perform necessary alterations to the JavaScript settings (drupalSettings).
+ *
+ * @param array &$settings
+ *   An array of all JavaScript settings (drupalSettings) being presented on the
+ *   page.
+ *
+ * @see _drupal_add_js()
+ * @see drupal_get_js()
+ * @see drupal_js_defaults()
+ */
+function hook_js_settings_alter(array &$settings) {
+  // Add settings.
+  $settings['user']['uid'] = \Drupal::currentUser();
+
+  // Manipulate settings.
+  if (isset($settings['dialog'])) {
+    $settings['dialog']['autoResize'] = FALSE;
+  }
+}
+
 /**
  * Alters the JavaScript/CSS library registry.
  *
@@ -785,14 +806,10 @@ function hook_library_alter(array &$library, $name) {
     $library['dependencies'][] = 'locale/drupal.locale.datepicker';
 
     $language_interface = \Drupal::languageManager()->getCurrentLanguage();
-    $settings['jquery']['ui']['datepicker'] = array(
+    $library['drupalSettings']['jquery']['ui']['datepicker'] = array(
       'isRTL' => $language_interface->getDirection() == LanguageInterface::DIRECTION_RTL,
       'firstDay' => \Drupal::config('system.date')->get('first_day'),
     );
-    $library['js'][] = array(
-      'type' => 'setting',
-      'data' => $settings,
-    );
   }
 }
 
diff --git a/core/modules/taxonomy/src/Form/OverviewTerms.php b/core/modules/taxonomy/src/Form/OverviewTerms.php
index 65ed68ac7db0..e4c55048b735 100644
--- a/core/modules/taxonomy/src/Form/OverviewTerms.php
+++ b/core/modules/taxonomy/src/Form/OverviewTerms.php
@@ -333,10 +333,10 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular
         'hidden' => FALSE,
       );
       $form['terms']['#attached']['library'][] = 'taxonomy/drupal.taxonomy';
-      $form['terms']['#attached']['js'][] = array(
-        'data' => array('taxonomy' => array('backStep' => $back_step, 'forwardStep' => $forward_step)),
-        'type' => 'setting',
-      );
+      $form['terms']['#attached']['drupalSettings']['taxonomy'] = [
+        'backStep' => $back_step,
+        'forwardStep' => $forward_step,
+      ];
     }
     $form['terms']['#tabledrag'][] = array(
       'action' => 'order',
diff --git a/core/modules/toolbar/src/Element/Toolbar.php b/core/modules/toolbar/src/Element/Toolbar.php
index 660ce323f04a..0db11666e966 100644
--- a/core/modules/toolbar/src/Element/Toolbar.php
+++ b/core/modules/toolbar/src/Element/Toolbar.php
@@ -78,14 +78,7 @@ public static function preRenderToolbar($element) {
         $media_queries[$id] = $breakpoint->getMediaQuery();
       }
 
-      $element['#attached']['js'][] = array(
-        'data' => array(
-          'toolbar' => array(
-            'breakpoints' => $media_queries,
-          )
-        ),
-        'type' => 'setting',
-      );
+      $element['#attached']['drupalSettings']['toolbar']['breakpoints'] = $media_queries;
     }
 
     $module_handler = static::moduleHandler();
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index f95a890f3b16..7b75e8940719 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -149,13 +149,10 @@ function toolbar_toolbar() {
   // script here with the hash parameter that is needed for that route.
   // @see toolbar_subtrees_jsonp()
   $langcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
-  $subtrees_attached['js'][] = array(
-    'type' => 'setting',
-    'data' => array('toolbar' => array(
-      'subtreesHash' => _toolbar_get_subtrees_hash($langcode),
-      'langcode' => $langcode,
-    )),
-  );
+  $subtrees_attached['drupalSettings']['toolbar'] = [
+    'subtreesHash' => _toolbar_get_subtrees_hash($langcode),
+    'langcode' => $langcode,
+  ];
 
   // The administration element has a link that is themed to correspond to
   // a toolbar tray. The tray contains the full administrative menu of the site.
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 31d7054bbc35..82845c6b6334 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -108,27 +108,17 @@ function user_theme() {
 }
 
 /**
- * Implements hook_js_alter().
+ * Implements hook_js_settings_alter().
  */
-function user_js_alter(&$javascript) {
-  // If >=1 JavaScript asset has declared a dependency on drupalSettings, the
-  // 'settings' key will exist. Thus when that key does not exist, return early.
-  if (!isset($javascript['settings'])) {
-    return;
-  }
-
+function user_js_settings_alter(&$settings) {
   // Provide the user ID in drupalSettings to allow JavaScript code to customize
   // the experience for the end user, rather than the server side, which would
   // break the render cache.
   // Similarly, provide a permissions hash, so that permission-dependent data
   // can be reliably cached on the client side.
   $user = \Drupal::currentUser();
-  $javascript['settings']['data'][] = array(
-    'user' => array(
-      'uid' => $user->id(),
-      'permissionsHash' => \Drupal::service('user.permissions_hash')->generate($user),
-    ),
-  );
+  $settings['user']['uid'] = $user->id();
+  $settings['user']['permissionsHash'] = \Drupal::service('user.permissions_hash')->generate($user);
 }
 
 /**
@@ -1363,17 +1353,8 @@ function user_form_process_password_confirm($element) {
     );
   }
 
-  $js_settings = array(
-    'password' => $password_settings,
-  );
-
   $element['#attached']['library'][] = 'user/drupal.user';
-  // Ensure settings are only added once per page.
-  static $already_added = FALSE;
-  if (!$already_added) {
-    $already_added = TRUE;
-    $element['#attached']['js'][] = array('data' => $js_settings, 'type' => 'setting');
-  }
+  $element['#attached']['drupalSettings']['password'] = $password_settings;
 
   return $element;
 }
diff --git a/core/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc b/core/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc
index a72201eeb8be..8f0df189193e 100644
--- a/core/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc
+++ b/core/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc
@@ -49,6 +49,7 @@ function views_test_data_views_pre_render(ViewExecutable $view) {
   if (isset($view) && ($view->storage->id() == 'test_cache_header_storage')) {
     $path = drupal_get_path('module', 'views_test_data');
     $view->element['#attached']['library'][] = 'views_test_data/test';
+    $view->element['#attached']['drupalSettings']['foo'] = 'bar';
     $view->element['#attached']['js'][] = "$path/views_cache.test.js";
     $view->element['#attached']['css'][] = "$path/views_cache.test.css";
     $view->element['#cache']['tags'][] = 'views_test_data:1';
diff --git a/core/modules/views/views.module b/core/modules/views/views.module
index 1afd0e18245c..2efa2d92c864 100644
--- a/core/modules/views/views.module
+++ b/core/modules/views/views.module
@@ -54,7 +54,7 @@ function views_help($route_name, RouteMatchInterface $route_match) {
 function views_views_pre_render($view) {
   // If using AJAX, send identifying data about this view.
   if ($view->ajaxEnabled() && empty($view->is_attachment) && empty($view->live_preview)) {
-    $settings = array(
+    $view->element['#attached']['drupalSettings']['views'] = array(
       'views' => array(
         'ajax_path' => \Drupal::url('views.ajax'),
         'ajaxViews' => array(
@@ -72,7 +72,6 @@ function views_views_pre_render($view) {
         ),
       ),
     );
-    $view->element['#attached']['js'][] = array('type' => 'setting', 'data' => $settings);
     $view->element['#attached']['library'][] = 'views/views.ajax';
   }
 
diff --git a/core/modules/views_ui/src/ViewEditForm.php b/core/modules/views_ui/src/ViewEditForm.php
index 6ec6aac2d1b2..780058039657 100644
--- a/core/modules/views_ui/src/ViewEditForm.php
+++ b/core/modules/views_ui/src/ViewEditForm.php
@@ -109,15 +109,12 @@ public function form(array $form, FormStateInterface $form_state) {
     $form['#attached']['library'][] = 'views_ui/views_ui.admin';
     $form['#attached']['library'][] = 'views_ui/admin.styling';
 
-    $form['#attached']['js'][] = array(
-      'data' => array('views' => array('ajax' => array(
-        'id' => '#views-ajax-body',
-        'title' => '#views-ajax-title',
-        'popup' => '#views-ajax-popup',
-        'defaultForm' => $view->getDefaultAJAXMessage(),
-      ))),
-      'type' => 'setting',
-    );
+    $form['#attached']['drupalSettings']['views']['ajax'] = [
+      'id' => '#views-ajax-body',
+      'title' => '#views-ajax-title',
+      'popup' => '#views-ajax-popup',
+      'defaultForm' => $view->getDefaultAJAXMessage(),
+    ];
 
     $form += array(
       '#prefix' => '',
diff --git a/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryParserTest.php b/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryParserTest.php
index 076b2cf994a9..141d43274fce 100644
--- a/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryParserTest.php
+++ b/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryParserTest.php
@@ -285,8 +285,7 @@ public function testLibraryWithCssJsSetting() {
     $this->assertEquals('file', $library['css'][0]['type']);
     $this->assertEquals($path . '/css/base.css', $library['css'][0]['data']);
 
-    $this->assertEquals('setting', $library['js'][1]['type']);
-    $this->assertEquals(array('key' => 'value'), $library['js'][1]['data']);
+    $this->assertEquals(array('key' => 'value'), $library['drupalSettings']);
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Core/Asset/library_test_files/css_js_settings.libraries.yml b/core/tests/Drupal/Tests/Core/Asset/library_test_files/css_js_settings.libraries.yml
index fdb479ff1e29..4b3970482d8f 100644
--- a/core/tests/Drupal/Tests/Core/Asset/library_test_files/css_js_settings.libraries.yml
+++ b/core/tests/Drupal/Tests/Core/Asset/library_test_files/css_js_settings.libraries.yml
@@ -4,5 +4,5 @@ example:
       css/base.css: {}
   js:
     js/example.js: {}
-  settings:
+  drupalSettings:
     key: value
diff --git a/core/themes/bartik/color/color.inc b/core/themes/bartik/color/color.inc
index 25e630d89e53..b2c854c7d9c2 100644
--- a/core/themes/bartik/color/color.inc
+++ b/core/themes/bartik/color/color.inc
@@ -6,8 +6,7 @@
  */
 
 // Put the logo path into JavaScript for the live preview.
-$js = array('color' => array('logo' => theme_get_setting('logo.url', 'bartik')));
-$js_attached['#attached']['js'][] = array('data' => $js, 'type' => 'setting');
+$js_attached['#attached']['drupalSettings']['color']['logo'] = theme_get_setting('logo.url', 'bartik');
 drupal_render($js_attached);
 
 $info = array(
-- 
GitLab