Index: includes/ajax.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/ajax.inc,v
retrieving revision 1.31
diff -u -p -r1.31 ajax.inc
--- includes/ajax.inc	30 Apr 2010 08:07:54 -0000	1.31
+++ includes/ajax.inc	12 Aug 2010 11:11:48 -0000
@@ -193,12 +193,11 @@
  *   functions.
  */
 function ajax_render($commands = array()) {
-  // Automatically extract any 'settings' added via drupal_add_js() and make
-  // them the first command.
-  $scripts = drupal_add_js(NULL, NULL);
-  if (!empty($scripts['settings'])) {
-    array_unshift($commands, ajax_command_settings(call_user_func_array('array_merge_recursive', $scripts['settings']['data'])));
-  }
+  // AJAX responses are returned as JSON and not themed via html.tpl.php. Thus,
+  // markup returned by drupal_get_js() is not contained, but the client needs
+  // to know the JavaScript settings and files required by the new content, so
+  // we call ajax_get_js() to get that.
+  $commands = array_merge(ajax_get_js(), $commands);
 
   // Allow modules to alter any AJAX response.
   drupal_alter('ajax_render', $commands);
@@ -207,6 +206,113 @@ function ajax_render($commands = array()
 }
 
 /**
+ * Returns an AJAX commands array used by the client to load JavaScript code for the current page.
+ *
+ * This function is only invoked for AJAX requests and is the equivalent of
+ * drupal_get_js() for full page requests, with the following differences:
+ * - By default, all scopes are processed at once, as AJAX responses typically
+ *   do not contain the same kind of 'header' and 'footer' scopes like full
+ *   page responses.
+ * - Files are not aggregated, because we do not know, which source files have
+ *   already been loaded as part of the base page. The client-side code will
+ *   determine new files and only load those.
+ * - The files are returned as AJAX commands for use by ajax_render() and to be
+ *   processed by client-side JavaScript code, rather than raw HTML SCRIPT tags.
+ *
+ * @param $scope
+ *   (optional) The scope for which the JavaScript code should be returned. If
+ *   not specified, returns the code for all scopes.
+ * @param $javascript
+ *   (optional) An array with all JavaScript code. Defaults to the default
+ *   JavaScript array for the given scope.
+ *
+ * @return
+ *   An array of commands suitable for use with the ajax_render() function.
+ *
+ * @see drupal_get_js()
+ */
+function ajax_get_js($scope = NULL, $javascript = NULL) {
+  if (!isset($javascript)) {
+    $javascript = drupal_add_js();
+  }
+  if (empty($javascript)) {
+    return array();
+  }
+
+  // Allow modules to alter the JavaScript.
+  drupal_alter('js', $javascript);
+
+  // Filter out elements of the given scope.
+  if (isset($scope)) {
+    $items = array();
+    foreach ($javascript as $item) {
+      if ($item['scope'] == $scope) {
+        $items[] = $item;
+      }
+    }
+  }
+  else {
+    $items = array_values($javascript);
+  }
+
+  uasort($items, 'drupal_sort_weight');
+
+  // @see drupal_get_js()
+  $default_query_string = variable_get('css_js_query_string', '0');
+  $js_version_string = variable_get('drupal_js_version_query_string', 'v=');
+
+  foreach ($items as $item) {
+    switch ($item['type']) {
+      case 'setting':
+        // drupal_add_js() puts all settings into a single $item of type
+        // 'setting', so we can do a simple assignment here, rather than adding
+        // to an array.
+        $settings = $item['data'];
+        break;
+
+      case 'inline':
+        // @todo Presently, inline JavaScript code is ignored and not returned
+        //   to the client. Evaluate the use-cases of AJAX responses that depend
+        //   on inline JavaScript code to determine how to best handle that.
+        break;
+
+      case 'file':
+        // To force browsers to refresh stale files, determine a suitable
+        // query string to append to the file URL.
+        // Volatile files use a query string that changes on every request.
+        if (!$item['cache']) {
+          $query_string = REQUEST_TIME;
+        }
+        // Versioned files use their version information to control caching.
+        elseif (!empty($item['version'])) {
+          $query_string = $js_version_string . $item['version'];
+        }
+        // Non-volatile non-versioned files use a query string that only changes
+        // when caches are flushed.
+        else {
+          $query_string = $default_query_string;
+        }
+        $query_string_separator = (strpos($item['data'], '?') !== FALSE) ? '&' : '?';
+        $js_files[] = file_create_url($item['data']) . $query_string_separator . $query_string;
+        break;
+
+      case 'external':
+        $js_files[] = $item['data'];
+        break;
+    }
+  }
+
+  $commands = array();
+  if (!empty($settings)) {
+    $commands[] = ajax_command_settings(call_user_func_array('array_merge_recursive', $settings));
+  }
+  if (!empty($js_files)) {
+    $commands[] = ajax_command_scripts($js_files);
+  }
+  return $commands;
+}
+
+/**
  * Get a form submitted via #ajax during an AJAX callback.
  *
  * This will load a form from the form cache used during AJAX operations. It
@@ -955,3 +1061,26 @@ function ajax_command_restripe($selector
   );
 }
 
+/**
+ * Creates scripts command for the AJAX responder.
+ *
+ * This will add JavaScript files to the output. Files that have already been
+ * processed will not be processed again.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.scripts()
+ * defined in misc/ajax.js.
+ *
+ * @param $files
+ *   The $files array is a simple array of filenames that match, exactly, the
+ *   scripts as they are added to the page or will be added to the page in the
+ *   future.
+ *
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ */
+function ajax_command_scripts($files) {
+  return array(
+    'command' => 'scripts',
+    'files' => $files,
+  );
+}
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.1204
diff -u -p -r1.1204 common.inc
--- includes/common.inc	10 Aug 2010 01:00:42 -0000	1.1204
+++ includes/common.inc	12 Aug 2010 11:37:37 -0000
@@ -3827,6 +3827,7 @@ function drupal_get_js($scope = 'header'
       'type' => 'text/javascript',
     ),
   );
+
   foreach ($items as $item) {
     $query_string =  empty($item['version']) ? $default_query_string : $js_version_string . $item['version'];
 
@@ -3852,12 +3853,13 @@ function drupal_get_js($scope = 'header'
 
       case 'file':
         $js_element = $element;
+        if ($item['defer']) {
+          $js_element['#attributes']['defer'] = 'defer';
+        }
+        $query_string_separator = (strpos($item['data'], '?') !== FALSE) ? '&' : '?';
+        $uri = file_create_url($item['data']) . $query_string_separator . ($item['cache'] ? $query_string : REQUEST_TIME);
         if (!$item['preprocess'] || !$preprocess_js) {
-          if ($item['defer']) {
-            $js_element['#attributes']['defer'] = 'defer';
-          }
-          $query_string_separator = (strpos($item['data'], '?') !== FALSE) ? '&' : '?';
-          $js_element['#attributes']['src'] = file_create_url($item['data']) . $query_string_separator . ($item['cache'] ? $query_string : REQUEST_TIME);
+          $js_element['#attributes']['src'] = $uri;
           $processed[$index++] = theme('html_tag', array('element' => $js_element));
         }
         else {
@@ -3866,6 +3868,7 @@ function drupal_get_js($scope = 'header'
           $key = 'aggregate' . $index;
           $processed[$key] = '';
           $files[$key][$item['data']] = $item;
+          $reported_files[$uri] = 1;
         }
         break;
 
@@ -3885,7 +3888,7 @@ function drupal_get_js($scope = 'header'
   if ($preprocess_js && count($files) > 0) {
     foreach ($files as $key => $file_set) {
       $uri = drupal_build_js_cache($file_set);
-      // Only include the file if was written successfully. Errors are logged
+      // Only include the file if it was written successfully. Errors are logged
       // using watchdog.
       if ($uri) {
         $preprocess_file = file_create_url($uri);
@@ -3894,6 +3897,15 @@ function drupal_get_js($scope = 'header'
         $processed[$key] = theme('html_tag', array('element' => $js_element));
       }
     }
+
+    // Report the list of aggregated JavaScript files. Required to allow AJAX
+    // processing on the client-side to keep track of all files already loaded.
+    // Non-aggregated files can be enumerated directly on the page.
+    $js_element = $element;
+    $js_element['#value_prefix'] = $embed_prefix;
+    $js_element['#value'] = 'jQuery.extend(Drupal.ajaxFiles.scripts, ' . drupal_json_encode($reported_files) . ");";
+    $js_element['#value_suffix'] = $embed_suffix;
+    $processed[] = theme('html_tag', array('element' => $js_element));
   }
 
   // Keep the order of JS files consistent as some are preprocessed and others are not.
Index: misc/ajax.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/ajax.js,v
retrieving revision 1.18
diff -u -p -r1.18 ajax.js
--- misc/ajax.js	25 Jun 2010 20:34:07 -0000	1.18
+++ misc/ajax.js	12 Aug 2010 11:46:52 -0000
@@ -16,6 +16,14 @@
 Drupal.ajax = Drupal.ajax || {};
 
 /**
+ * Holds a list of files known to be on the page so that AJAX requests can
+ * selectively load additionally required files later on.
+ *
+ * @see Drupal.ajax.prototype.commands.scripts()
+ */
+Drupal.ajaxFiles = Drupal.ajaxFiles || { scripts: {}, css: {} };
+
+/**
  * Attaches the AJAX behavior to each AJAX form element.
  */
 Drupal.behaviors.AJAX = {
@@ -427,6 +435,36 @@ Drupal.ajax.prototype.commands = {
   },
 
   /**
+   * Command to add scripts to the page.
+   *
+   * Keeps track of files already on the page and attempts to only load
+   * additionally required scripts.
+   */
+  scripts: function (ajax, response, status) {
+    // Build a list of scripts already loaded. Aggregated files have already
+    // been registered in Drupal.ajaxFiles.scripts via drupal_get_js().
+    // Non-aggregated files can simply be enumerated by script tags on the page.
+    $('script').each(function () {
+      Drupal.ajaxFiles.scripts[this.src] = this.src;
+    });
+
+    var html = '';
+    for (var file in response.files) {
+      // @todo Files in this stack are based on their 'src' attribute and/or a
+      //   previous AJAX request. Their URIs may contain query strings, domain
+      //   names, and other additions that can conflict with this simple key
+      //   check. Consider to store and use basenames?
+      if (!Drupal.ajaxFiles.scripts[file]) {
+        Drupal.ajaxFiles.scripts[file] = file;
+        html += '<script type="text/javascript" src="' + file + '"></script>';
+      }
+    }
+    if (html) {
+      $('html').prepend(html);
+    }
+  },
+
+  /**
    * Command to attach data using jQuery's data API.
    */
   data: function (ajax, response, status) {
Index: modules/field/tests/field.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/tests/field.test,v
retrieving revision 1.37
diff -u -p -r1.37 field.test
--- modules/field/tests/field.test	8 Aug 2010 02:18:53 -0000	1.37
+++ modules/field/tests/field.test	12 Aug 2010 10:53:40 -0000
@@ -1439,7 +1439,7 @@ class FieldFormTestCase extends FieldTes
     // Press 'add more' button through AJAX, and place the expected HTML result
     // as the tested content.
     $commands = $this->drupalPostAJAX(NULL, $edit, $this->field_name . '_add_more');
-    $this->content = $commands[1]['data'];
+    $this->content = $commands[2]['data'];
 
     for ($delta = 0; $delta <= $delta_range; $delta++) {
       $this->assertFieldByName("$this->field_name[$langcode][$delta][value]", $values[$delta], "Widget $delta is displayed and has the right value");
Index: modules/poll/poll.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/poll/poll.test,v
retrieving revision 1.37
diff -u -p -r1.37 poll.test
--- modules/poll/poll.test	5 Aug 2010 23:53:38 -0000	1.37
+++ modules/poll/poll.test	12 Aug 2010 10:53:40 -0000
@@ -342,7 +342,7 @@ class PollJSAddChoice extends DrupalWebT
     // Press 'add choice' button through AJAX, and place the expected HTML result
     // as the tested content.
     $commands = $this->drupalPostAJAX(NULL, $edit, array('op' => t('More choices')));
-    $this->content = $commands[1]['data'];
+    $this->content = $commands[2]['data'];
 
     $this->assertFieldByName('choice[chid:0][chtext]', $edit['choice[new:0][chtext]'], t('Field !i found', array('!i' => 0)));
     $this->assertFieldByName('choice[chid:1][chtext]', $edit['choice[new:1][chtext]'], t('Field !i found', array('!i' => 1)));
Index: modules/simpletest/tests/ajax.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/ajax.test,v
retrieving revision 1.14
diff -u -p -r1.14 ajax.test
--- modules/simpletest/tests/ajax.test	5 Aug 2010 23:53:38 -0000	1.14
+++ modules/simpletest/tests/ajax.test	12 Aug 2010 11:57:19 -0000
@@ -11,17 +11,25 @@ class AJAXTestCase extends DrupalWebTest
   }
 
   /**
-   * Returns the passed-in commands array without the initial settings command.
+   * Returns the passed-in commands array without the commands automatically prepended by ajax_render().
    *
    * Depending on factors that may be irrelevant to a particular test,
-   * ajax_render() may prepend a settings command. This function allows the test
-   * to only have to concern itself with the commands that were passed to
-   * ajax_render().
+   * ajax_render() may prepend some commands for adding JavaScript files and
+   * settings. This function allows the test to only have to concern itself with
+   * the commands that were passed to ajax_render().
+   *
+   * @todo This function is named discardSettings() for legacy reasons, because
+   *   at one time, ajax_render() only added a 'settings' command. For Drupal 8,
+   *   or when a BC break of Drupal 7 tests is acceptable, rename this function
+   *   to be more accurate.
    */
   protected function discardSettings($commands) {
     if ($commands[0]['command'] == 'settings') {
       array_shift($commands);
     }
+    if ($commands[0]['command'] == 'scripts') {
+      array_shift($commands);
+    }
     return $commands;
   }
 }
@@ -43,10 +51,17 @@ class AJAXFrameworkTestCase extends AJAX
    */
   function testAJAXRender() {
     $result = $this->drupalGetAJAX('ajax-test/render');
-    // Verify that JavaScript settings are contained (always first).
-    $this->assertIdentical($result[0]['command'], 'settings', t('drupal_add_js() settings are contained first.'));
-    // Verify that basePath is contained in JavaScript settings.
-    $this->assertEqual($result[0]['settings']['basePath'], base_path(), t('Base path is contained in JavaScript settings.'));
+
+    // Verify that there is a command to load settings added with
+    // drupal_add_js().
+    $this->assertIdentical($result[0]['command'], 'settings', t('ajax_render() added a settings command to load settings added with drupal_add_js().'));
+    $this->assertIdentical($result[0]['settings']['basePath'], base_path(), t('The %setting setting is included.', array('%setting' => 'basePath')));
+
+    // Verify that there is a command to load script files added with
+    // drupal_add_js().
+    $this->assertIdentical($result[1]['command'], 'scripts', t('ajax_render() added a scripts command to load files added with drupal_add_js().'));
+    $file = file_create_url('misc/drupal.js') . '?' . variable_get('css_js_query_string', '0');
+    $this->assertTrue(in_array($file, $result[1]['files']), t('The %file file is included.', array('%file' => 'misc/drupal.js')));
   }
 
   /**
@@ -84,11 +99,11 @@ class AJAXCommandsTestCase extends AJAXT
   function testAJAXRender() {
     $commands = array();
     $commands[] = ajax_command_settings(array('foo' => 42));
-    $result = $this->drupalGetAJAX('ajax-test/render', array('query' => array('commands' => $commands)));
-    // Verify that JavaScript settings are contained (always first).
-    $this->assertIdentical($result[0]['command'], 'settings', t('drupal_add_js() settings are contained first.'));
+    // discardSettings() discards only what is automatically added by
+    // ajax_render(), not the one added by the ajax-test/render callback.
+    $result = $this->discardSettings($this->drupalGetAJAX('ajax-test/render', array('query' => array('commands' => $commands))));
     // Verify that the custom setting is contained.
-    $this->assertEqual($result[1]['settings']['foo'], 42, t('Custom setting is output.'));
+    $this->assertEqual($result[0]['settings']['foo'], 42, t('Custom setting is output.'));
   }
 
   /**
Index: modules/simpletest/tests/ajax_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/ajax_test.module,v
retrieving revision 1.3
diff -u -p -r1.3 ajax_test.module
--- modules/simpletest/tests/ajax_test.module	13 Mar 2010 06:55:50 -0000	1.3
+++ modules/simpletest/tests/ajax_test.module	12 Aug 2010 10:53:40 -0000
@@ -31,7 +31,8 @@ function ajax_test_menu() {
  * Menu callback; Returns $_GET['commands'] suitable for use by ajax_deliver().
  *
  * Additionally ensures that ajax_render() incorporates JavaScript settings
- * by invoking drupal_add_js() with a dummy setting.
+ * and files by invoking drupal_add_js() with a dummy setting, which causes
+ * drupal_add_js() to also automatically add "misc/drupal.js".
  */
 function ajax_test_render() {
   // Prepare AJAX commands.
