Skip to content

Commit

Permalink
use content hashes in URL of css/js files to do automatic, granular c…
Browse files Browse the repository at this point in the history
…ache invalidation, instead of incrementing global cache_version
  • Loading branch information
Jesse Young committed Jun 6, 2011
1 parent 88f547e commit f14492d
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 129 deletions.
3 changes: 2 additions & 1 deletion config/default.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<?php
return array(
'cache_version' => 187, // increment when css, or external js (tinymce/swfupload) changes
'debug' => true,

'dbuser' => '',
Expand Down Expand Up @@ -75,4 +74,6 @@

'site_secret' => 'default_secret',
'fallback_theme' => 'simple',

'cache_version' => 188, // increment when all cached objects need to be invalidated (rare)
);
9 changes: 5 additions & 4 deletions engine/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ static function load()
{
static::$base_dir = dirname(__DIR__);
static::load_array(static::get_group('default'));
static::load_array(static::get_group('local'));
static::load_array(static::get_group('local'));
static::load_array(@include(static::$base_dir."/build/config.php"));
static::init_dependent_settings();

// The ENVAYA_CONFIG environment variable may define settings in a JSON string
Expand All @@ -60,11 +61,11 @@ static function load()
}

private static function load_array($settings, $overwrite = true)
{
$all_settings =& static::$settings;

{
if ($settings)
{
$all_settings =& static::$settings;

if (!$all_settings)
{
$all_settings = $settings;
Expand Down
6 changes: 2 additions & 4 deletions lib/util.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,13 @@ function secure_url($url)

function css_url($css_name)
{
$cache_version = Config::get('cache_version');

if (Config::get('debug'))
{
return "/pg/css?name=$css_name&v=$cache_version";
return "/pg/css?name=$css_name&hash=" . md5(view("css/$css_name", 'default'));
}
else
{
return "/_media/css/$css_name.css?$cache_version";
return "/_media/css/$css_name.css?".Config::get("hash:css:$css_name");
}
}

Expand Down
264 changes: 146 additions & 118 deletions make.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,65 +13,48 @@
require_once "scripts/cmdline.php";

class Build
{
private static function module_glob()
{
return "{".implode(',',Config::get('modules'))."}";
}

private static function minify($srcFile, $destFile, $type='js')
{
static function all()
{
$src = file_get_contents($srcFile);

system("java -jar vendors/yuicompressor-2.4.2.jar --type $type -o ".escapeshellarg($destFile). " ".escapeshellarg($srcFile));

$compressed = file_get_contents($destFile);

echo strlen($src)." ".strlen($compressed)." $destFile\n";
Build::path_cache();
Build::media();
Build::css();
Build::js();
}

/*
* Removes all files generated by make.php
*/
static function clean()
{
@unlink("build/lib_cache.php");
@unlink("build/path_cache.php");
@unlink("build/path_cache_info.php");

system('rm -rf build/*.php');
system('rm -rf build/path_cache');
system('rm -rf www/_media/*');
}

/*
* Generates a cache of all the paths of files in the lib/ directory.
*
* Uses relative paths so the same cache files work in any root directory
* and can be copied to different systems without needing to regenerate them.
*/
static function lib_cache()
{
@unlink("build/lib_cache.php");

require_once "start.php";
$paths = Engine::get_lib_paths();
static::write_file("build/lib_cache.php", static::get_array_php($paths));
}

/*
* Generates a cache of all the paths of files in the engine, themes, languages, and views
* directories, which might be referenced via virtual paths in calls to Engine::get_real_path() .
* This cache allows Engine::get_real_path() to work in O(1) time instead of O(num_modules) time.
*
* Also generates a cache of all the paths of files in the lib/ directory.
*
* Uses relative paths so the same cache files work in any root directory
* and can be copied to different systems without needing to regenerate them.
*/
static function path_cache()
{
// remove previous build files before including start.php
// so that Engine class doesn't use previous path cache when building the new path cache
@unlink("build/lib_cache.php");
@unlink("build/path_cache.php");
system('rm -rf build/path_cache');

require_once "start.php";

if (!is_dir('build/path_cache'))
{
mkdir('build/path_cache');
}


$paths = Engine::get_lib_paths();
static::write_file("build/lib_cache.php", static::get_array_php($paths));

$dir_paths = array(
// allows us to test if the path cache actually works like it should
'views/default/admin' => array(
Expand All @@ -98,6 +81,7 @@ static function path_cache()
static::add_nonexistent_view_paths($dir_paths);

// create a cache file for each virtual directory
mkdir('build/path_cache');
foreach ($dir_paths as $dir => $paths)
{
$cache_name = str_replace('/','__', $dir);
Expand Down Expand Up @@ -142,6 +126,101 @@ static function path_cache()
static::write_file("build/path_cache_info.php",
"<div>The path cache is enabled. ($num_default_paths default paths + $num_files files)</div>");
}

/*
* Minifies all CSS files defined in each module's views/default/css/ directory,
* and copies to www/_media/css/.
*/
static function css($name = '*')
{
require_once "start.php";

$modules = static::module_glob();
$css_paths = glob("{views/default/css/$name.php,mod/$modules/views/default/css/$name.php}", GLOB_BRACE);

$output_dir = 'www/_media/css';

if (!is_dir($output_dir))
{
mkdir($output_dir);
}

$build_config = static::get_build_config();

foreach ($css_paths as $css_path)
{
$pathinfo = pathinfo($css_path);
$filename = $pathinfo['filename'];
$css_temp = "scripts/$filename.tmp.css";
$raw_css = view("css/$filename");

if (preg_match('/http(s)?:[^\s\)\"\']*/', $raw_css, $matches))
{
throw new Exception("Absolute URL {$matches[0]} found in $css_path. In order to work on both dev/production without recompiling, CSS files must not contain absolute paths.");
}

file_put_contents($css_temp, $raw_css);
$compressed = static::minify($css_temp, "$output_dir/$filename.css", 'css');
unlink($css_temp);

$build_config["hash:css:$filename"] = static::get_content_hash($compressed);
}

static::write_build_config($build_config);
}

/*
* Minifies Javascript in each module's js/ directory, and copie to www/_media/.
*/
static function js($name = '*')
{
require_once "start.php";
$modules = static::module_glob();

static::js_minify_dir(".", $name);

foreach (Config::get('modules') as $module)
{
static::js_minify_dir("mod/$module", $name);
}
}

/*
* Copies static files from each module's _media/ directory to www/_media/.
*/
static function media()
{
require_once "start.php";

static::system("rsync -rp _media/ www/_media/");

foreach (Config::get('modules') as $module)
{
if (is_dir("mod/$module/_media"))
{
static::system("rsync -rp mod/$module/_media/ www/_media/");
}
}
}

private static function module_glob()
{
return "{".implode(',',Config::get('modules'))."}";
}

private static function minify($srcFile, $destFile, $type='js')
{
$src = file_get_contents($srcFile);

system("java -jar vendors/yuicompressor-2.4.2.jar --type $type -o ".escapeshellarg($destFile). " ".escapeshellarg($srcFile));

$compressed = file_get_contents($destFile);

echo strlen($src)." ".strlen($compressed)." $destFile\n";

return $compressed;
}


private static function add_nonexistent_view_paths(&$dir_paths)
{
Expand Down Expand Up @@ -228,40 +307,25 @@ private static function get_array_php($arr)
return "<?php return ".var_export($arr, true).";";
}

/*
* Minifies all CSS files defined in each module's views/default/css/ directory,
* and copies to www/_media/css/.
*/
static function css($name = '*')
private static function get_build_config()
{
return @include("build/config.php") ?: array();
}

private static function write_build_config($build_config)
{
static::write_file("build/config.php", static::get_array_php($build_config));
}

/*
* Returns a short hash code that can be used as part of a file's URL, to ensure that
* browser/proxy caches are invalidated automatically whenever the content is changed.
*/
private static function get_content_hash($content)
{
require_once "start.php";

$modules = static::module_glob();
$css_paths = glob("{views/default/css/$name.php,mod/$modules/views/default/css/$name.php}", GLOB_BRACE);

$output_dir = 'www/_media/css';

if (!is_dir($output_dir))
{
mkdir($output_dir);
}

foreach ($css_paths as $css_path)
{
$pathinfo = pathinfo($css_path);
$filename = $pathinfo['filename'];
$css_temp = "scripts/$filename.tmp.css";
$raw_css = view("css/$filename");

if (preg_match('/http(s)?:[^\s\)\"\']*/', $raw_css, $matches))
{
throw new Exception("Absolute URL {$matches[0]} found in $css_path. In order to work on both dev/production without recompiling, CSS files must not contain absolute paths.");
}

file_put_contents($css_temp, $raw_css);
static::minify($css_temp, "$output_dir/$filename.css", 'css');
unlink($css_temp);
}
// 16^10 hash values should be enough to avoid collisions within an Expires timeout of 1 year
// (not important to avoid intentional collisions)
return substr(md5($content), 0, 10);
}

private static function js_minify_dir($base, $name = '*', $dir = '')
Expand All @@ -270,7 +334,15 @@ private static function js_minify_dir($base, $name = '*', $dir = '')
foreach ($js_src_files as $js_src_file)
{
$basename = pathinfo($js_src_file, PATHINFO_BASENAME);
static::minify($js_src_file, "www/_media/{$dir}{$basename}");
$compressed = static::minify($js_src_file, "www/_media/{$dir}{$basename}");

if ($dir != 'inline/') // inline JS does not need content hashes because it is never loaded by URL
{
$filename = pathinfo($js_src_file, PATHINFO_FILENAME);
$build_config = static::get_build_config();
$build_config["hash:js:$filename"] = static::get_content_hash($compressed);
static::write_build_config($build_config);
}
}

$subdirs = glob("$base/js/{$dir}*", GLOB_ONLYDIR);
Expand All @@ -285,56 +357,12 @@ private static function js_minify_dir($base, $name = '*', $dir = '')
static::js_minify_dir($base, $name, "{$dir}{$basename}/");
}
}

/*
* Minifies Javascript in each module's js/ directory, and copie to www/_media/.
*/
static function js($name = '*')
{
require_once "start.php";
$modules = static::module_glob();

static::js_minify_dir(".", $name);

foreach (Config::get('modules') as $module)
{
static::js_minify_dir("mod/$module", $name);
}
}

static function system($cmd)
{
echo "$cmd\n";
return system($cmd);
}

/*
* Copies static files from each module's _media/ directory to www/_media/.
*/
static function media()
{
require_once "start.php";

static::system("rsync -rp _media/ www/_media/");

foreach (Config::get('modules') as $module)
{
if (is_dir("mod/$module/_media"))
{
static::system("rsync -rp mod/$module/_media/ www/_media/");
}
}
}

static function all()
{
Build::clean();
Build::media();
Build::lib_cache();
Build::path_cache();
Build::css();
Build::js();
}
}
}

$target = @$argv[1] ?: 'all';
Expand Down
2 changes: 1 addition & 1 deletion views/default/input/tinymce.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
echo view('js/create_modal_box');
echo view('js/dom');
?></script>
<script type='text/javascript' src='/_media/tiny_mce/tiny_mce.js?v<?php echo Config::get('cache_version'); ?>'></script>
<script type='text/javascript' src='/_media/tiny_mce/tiny_mce.js?v<?php echo Config::get('hash:js:tiny_mce'); ?>'></script>
<?php
}

Expand Down
Loading

0 comments on commit f14492d

Please sign in to comment.