Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.843
diff -u -r1.843 common.inc
--- includes/common.inc	8 Jan 2009 19:09:49 -0000	1.843
+++ includes/common.inc	9 Jan 2009 18:52:40 -0000
@@ -2535,8 +2535,12 @@
   $no_preprocess = '';
   $files = array();
   $preprocess_js = (variable_get('preprocess_js', FALSE) && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update'));
-  $directory = file_directory_path();
-  $is_writable = is_dir($directory) && is_writable($directory) && (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PUBLIC);
+  if ($preprocess_js) {
+  	// Only check the preprocessor requirements if it's going to be
+  	// preprocessing. If these requirements are not met, preprocessing
+  	// will be disabled.
+  	$preprocess_js = drupal_js_preprocessor()->requirements();
+  }
 
   // A dummy query-string is added to filenames, to gain control over
   // browser-caching. The string changes on every update or full cache
@@ -2567,7 +2571,7 @@
         break;
 
       case 'file':
-        if (!$item['preprocess'] || !$is_writable || !$preprocess_js) {
+        if (!$item['preprocess'] || !$preprocess_js) {
           $no_preprocess .= '<script type="text/javascript"' . ($item['defer'] ? ' defer="defer"' : '') . ' src="' . base_path() . $item['data'] . ($item['cache'] ? $query_string : '?' . REQUEST_TIME) . "\"></script>\n";
         }
         else {
@@ -2578,10 +2582,8 @@
   }
 
   // Aggregate any remaining JS files that haven't already been output.
-  if ($is_writable && $preprocess_js && count($files) > 0) {
-    $filename = md5(serialize($files) . $query_string) . '.js';
-    $preprocess_file = drupal_build_js_cache($files, $filename);
-    $preprocessed .= '<script type="text/javascript" src="' . base_path() . $preprocess_file . '"></script>' . "\n";
+  if ($preprocess_js && count($files)) {
+    $preprocessed .= drupal_js_preprocessor()->preprocess($files);
   }
 
   // Keep the order of JS files consistent as some are preprocessed and others are not.
@@ -2721,36 +2723,54 @@
 }
 
 /**
- * Aggregate JS files, putting them in the files directory.
- *
- * @param $files
- *   An array of JS files to aggregate and compress into one file.
- * @param $filename
- *   The name of the aggregate JS file.
- * @return
- *   The name of the JS file.
+ * Returns a JavaScript preprocessing object that implements JSPreprocessingInterface.
  */
-function drupal_build_js_cache($files, $filename) {
-  $contents = '';
+function drupal_js_preprocessor() {
+  static $instance;
 
-  // Create the js/ within the files folder.
-  $jspath = file_create_path('js');
-  file_check_directory($jspath, FILE_CREATE_DIRECTORY);
-
-  if (!file_exists($jspath . '/' . $filename)) {
-    // Build aggregate JS file.
-    foreach ($files as $path => $info) {
-      if ($info['preprocess']) {
-        // Append a ';' after each JS file to prevent them from running together.
-        $contents .= file_get_contents($path) . ';';
-      }
+  if (empty($instance)) {
+  	// Retrieve the preprocess system.
+    $class = variable_get('preprocess_js_system', 'DrupalPreprocessJS');
+
+    // Allow for lazy loading of the class.
+    if (drupal_autoload_class($class)) {
+	    $interfaces = class_implements($class['name']);
+	    if (isset($interfaces['JSPreprocessingInterface'])) {
+	      $instance = new $class['name'];
+	    }
+	    else {
+	      throw new Exception(t('Class %class does not implement interface %interface', array('%class' => $class, '%interface' => 'JSPreprocessingInterface')));
+	    }
+    }
+    else {
+      throw new Exception(t('Class %class was not found.', array('%class' => $class)));
     }
-
-    // Create the JS file.
-    file_unmanaged_save_data($contents, $jspath . '/' . $filename, FILE_EXISTS_REPLACE);
   }
+  return $instance;
+}
 
-  return $jspath . '/' . $filename;
+/**
+ * The interface for preprocessing JavaScript.
+ */
+interface JSPreprocessingInterface {
+  /**
+   * Check preprocessing requirements.
+   *
+   * @return
+   *   TRUE if requirements met to preform preprocessing.
+   */
+  public function requirements();
+
+  /**
+   * Preprocess JavaScript files.
+   *
+   * @param $files
+   *   An array of JavaScript files.
+   *
+   * @return
+   *   HTML for preprocessed JavaScript files.
+   */
+  public function preprocess($files);
 }
 
 /**
Index: modules/simpletest/tests/common.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/common.test,v
retrieving revision 1.20
diff -u -r1.20 common.test
--- modules/simpletest/tests/common.test	8 Jan 2009 19:09:49 -0000	1.20
+++ modules/simpletest/tests/common.test	9 Jan 2009 18:52:40 -0000
@@ -329,9 +329,9 @@
 /**
  * Tests for the JavaScript system.
  */
-class JavaScriptTestCase extends DrupalWebTestCase {
+class JavaScriptTestCase extends DrupalWebTestCase implements JSPreprocessingInterface {
   /**
-   * Store configured value for JavaScript preprocessing.
+   * Store configured values for JavaScript preprocessing.
    */
   var $preprocess_js = NULL;
 
@@ -347,9 +347,10 @@
     // Enable Locale and SimpleTest in the test environment.
     parent::setUp('locale', 'simpletest');
 
-    // Disable preprocessing
+    // Disable preprocessing and use the default preprocessor system.
     $this->preprocess_js = variable_get('preprocess_js', 0);
     variable_set('preprocess_js', 0);
+    variable_del('preprocess_js_system');
 
     // Reset drupal_add_js() before each test.
     drupal_add_js(NULL, NULL, TRUE);
@@ -358,6 +359,7 @@
   function tearDown() {
     // Restore configured value for JavaScript preprocessing.
     variable_set('preprocess_js', $this->preprocess_js);
+    variable_del('preprocess_js_system');
     parent::tearDown();
   }
 
@@ -470,6 +472,37 @@
     $javascript = drupal_get_js();
     $this->assertTrue(strpos($javascript, 'simpletest.js') < strpos($javascript, 'misc/tableselect.js'), t('Altering JavaScript weight through the alter hook.'));
   }
+
+  // Static variables required to test the pluggable preprocessor system.
+  static $testFiles = NULL;
+  static $testRequirements = FALSE;
+
+  /**
+   * Tests the pluggable JavaScript preprocessor system.
+   */
+  function testPluggablePreprocessor() {
+  	// Switch the preprocessor.
+    variable_set('preprocess_js', TRUE);
+  	variable_set('preprocess_js_system', 'JavaScriptTestCase');
+
+  	// Add some JavaScript to test the preprocessor system.
+    drupal_add_js('misc/tableselect.js');
+
+    // Retrieve the JavaScript, running through the JavaScriptTestCase preprocessor.
+    $javascript = drupal_get_js();
+    $this->assertTrue(JavaScriptTestCase::$testRequirements, t('Pluggable preprocessor system requirements are checked.'));
+    $this->assertTrue(strpos($javascript, 'has been preprocessed') > 0 && !empty(JavaScriptTestCase::$testRequirements), t('Preprocessor system is pluggable.'));
+  }
+
+  public function requirements() {
+    JavaScriptTestCase::$testRequirements = TRUE;
+    return TRUE;
+  }
+
+  public function preprocess($files) {
+  	JavaScriptTestCase::$testFiles = $files;
+  	return 'JavaScript has been preprocessed.';
+  }
 }
 
 /**
Index: includes/preprocess.inc
===================================================================
RCS file: includes/preprocess.inc
diff -N includes/preprocess.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ includes/preprocess.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,66 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Aggregate JavaScript files. 
+ */
+
+/**
+ * Drupal's default JavaScript preprocessor.
+ */
+class DrupalPreprocessJS implements JSPreprocessingInterface {
+  /**
+   * Cache if preprocessed files should be saved to the files directory. 
+   */
+  protected $is_writable = NULL;
+
+  /**
+   * Check directory to save files to and for public file downloads.
+   *
+   * @return
+   *   TRUE if files directory is writable and file downloads are public.
+   */
+  public function requirements() {
+    if (is_null($this->is_writable)) {
+       $directory = file_directory_path();
+       $this->is_writable = is_dir($directory) && is_writable($directory) && (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PUBLIC);
+    }
+    return $this->is_writable;
+  }
+
+  /**
+   * Aggregate JS files, putting them in the files directory.
+   *
+   * @param $files
+   *   An array of JavaScript files to aggregate and compress into one file.
+   * 
+   * @return
+   *   The script tag containing the path to the cached JavaScript file.
+   */
+  public function preprocess($files) {
+    $contents = '';
+
+    $query_string = '?' . substr(variable_get('css_js_query_string', '0'), 0, 1);
+    $filename = md5(serialize($files) . $query_string) . '.js';    
+
+    // Ensure there is a js directory within the files directory that it is writable.
+    $jspath = file_create_path('js');
+    file_check_directory($jspath, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+
+    if (!file_exists($jspath . '/' . $filename)) {
+      // Build aggregate JS file.
+      foreach ($files as $path => $info) {
+        if ($info['preprocess']) {
+          // Append a ';' after each JS file to prevent them from running together.
+          $contents .= file_get_contents($path) . ';';
+        }
+      }
+
+      // Create the JS file.
+      file_unmanaged_save_data($contents, $jspath . '/' . $filename, FILE_EXISTS_REPLACE);
+    }
+
+    return '<script type="text/javascript" src="' . base_path() . $jspath . '/' . $filename . '"></script>' . "\n";
+  }
+}
