Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Released 0.0.1

commit 93356c758f9828927738224a70498450108ecc48 0 parents
@niftylettuce authored
103 .gitignore
@@ -0,0 +1,103 @@
+
+# ------------------------------------------------------------------------------
+# Common Node files and folders
+# https://github.com/github/gitignore/blob/master/Node.gitignore
+# ------------------------------------------------------------------------------
+
+lib-cov
+*.seed
+*.log
+*.csv
+*.dat
+*.out
+*.pid
+*.gz
+
+pids
+logs
+results
+
+node_modules
+npm-debug.log
+
+
+# ------------------------------------------------------------------------------
+# Nodemon
+# ------------------------------------------------------------------------------
+
+.monitor
+
+
+# ------------------------------------------------------------------------------
+# JSHint
+# ------------------------------------------------------------------------------
+
+jshint
+
+
+# ------------------------------------------------------------------------------
+# Numerous always-ignore extensions
+# ------------------------------------------------------------------------------
+
+*.diff
+*.err
+*.orig
+*.rej
+*.swo
+*.swp
+*.vi
+*~
+*.sass-cache
+
+
+# ------------------------------------------------------------------------------
+# Files from other repository control systems
+# ------------------------------------------------------------------------------
+
+.hg
+.svn
+.CVS
+
+
+# ------------------------------------------------------------------------------
+# Your ideas
+# ------------------------------------------------------------------------------
+
+.idea
+
+
+# ------------------------------------------------------------------------------
+# OS X and other IDE's folder attributes
+# ------------------------------------------------------------------------------
+
+.DS_Store
+Thumbs.db
+.cache
+.project
+.settings
+.tmproj
+*.esproj
+nbproject
+*.sublime-project
+*.sublime-workspace
+
+
+# ------------------------------------------------------------------------------
+# Dreamweaver added files
+# ------------------------------------------------------------------------------
+
+_notes
+dwsync.xml
+
+
+# ------------------------------------------------------------------------------
+# Komodo
+# ------------------------------------------------------------------------------
+
+*.komodoproject
+.komodotools
+
+
+# ------------------------------------------------------------------------------
+# Add your custom excludes below
+# ------------------------------------------------------------------------------
296 Readme.md
@@ -0,0 +1,296 @@
+
+# express-cdn <sup>0.0.1</sup>
+
+Node.js module for delivering optimized, minified, mangled, gzipped, and CDN-hosted assets in Express using S3 and CloudFront.
+
+Follow <a href="http://twitter.com/niftylettuce" target="_blank">@niftylettuce</a> on Twitter for updates.
+
+
+
+## Features
+
+* Built-in optimization of images in production mode using [OptiPNG][1] and [JPEGTran][2].
+* Supports [Sass][3], [LESS][4], and [Stylus][5] using respective stylesheet compilers.
+* JavaScript assets are mangled and minified using [UglifyJS][6].
+* Automatic detection of asset changes and will only upload changed assets to S3 in production mode.
+* Utilizes cachebusting, which is inspired by [express-cachebuster][7] and [h5bp][8].
+* All assets are compressed using [zlib][9] into a gzip buffer for S3 uploading with `Content-Encoding` header set to `gzip`.
+* Embed multiple assets as a single `<script>` or `<link>` tag using the built-in dynamic view helper.
+* Loads and processes assets per view (allowing you to minimize client HTTP requests).
+* Combine commonly used assets together using a simple array argument.
+* Uploads changed assets automatically and asynchronously to Amazon S3 (only in production mode) using [knox][10].
+
+
+
+## Lazy Web Requests
+
+* Automatic parsing of `CDN(...)` in stylesheets and scripts.
+* Support Express 3.x.x+ and utilize async with view helper.
+* Convert to `fs.statSync` to `fs.stat` with callback for image assets modified timestamp hack.
+* Investigate why Chrome Tools Audit returns leverage proxy cookieless jargon.
+
+
+
+## How does it work?
+
+When the server is first started, the module returns a view helper depending on
+the server environment (production or development). It also recursively
+searches through your `viewsDir` for any views containing instances of the
+`CDN(...)` view helper. After parsing each instance and removing duplicates,
+it will use your S3 credentials to upload a new copy of the production-quality
+assets. Enjoy **:)**.
+
+
+
+## Environment Differences
+
+**Development Mode:**
+
+Assets are untouched, cachebusted, and delivered as typical local files for rapid development.
+
+**Production Mode:**
+
+Assets are optimized, minified, mangled, gzipped, delivered by Amazon CloudFront CDN, and hosted from Amazon S3.
+
+
+
+## CDN Setup Instructions
+
+1. Visit <https://console.aws.amazon.com/s3/home> and click **Create Bucket**.
+ * Bucket Name: `bucket-name`
+ * Region: `US Standard`
+2. Upload <a href="https://raw.github.com/niftylettuce/express-cdn/master/index.html">index.html</a> to your new bucket (this will serve as a placeholder in case someone accesses <http://cdn.your-site.com/>).
+3. Select `index.html` in the Objects and Folders view from your S3 console and click **Actions &rarr; Make Public**.
+4. Visit <https://console.aws.amazon.com/cloudfront/home> and click **Create Distribution**.
+ * Choose an origin:
+ - Origin Domain Name: `bucket-name.s3.amazonaws.com`
+ - Origin ID: `S3-bucket-name`
+ * Create default behavior:
+ - Path Pattern: `Default (*)`
+ - Origin: `S3-bucket-name`
+ - Viewer Protocol Policy: `HTTP and HTTPS`
+ - Object Caching: `Use Origin Cache Headers`
+ - Forward Query String: `No (Improves Caching)`
+ * Distribution details:
+ - Alternate Domain Names (CNAMEs): `cdn.your-domain.com`
+ - Default Root Object: `index.html`
+ - Logging: `Off`
+ - Comments: `Created with express-cdn by @niftylettuce.`
+ - Distribution State: `Enabled`
+5. Copy the generated Domain Name (e.g. `xyz.cloudfront.net`) to your clipboard.
+6. Log in to your-domain.com's DNS manager, add a new CNAME "hostname" of `cdn`, and paste the contents of your clipboard as the the "alias" or "points to" value.
+7. After the DNS change propagates, you can test your new CDN by visiting <http://cdn.your-domain.com> (the `index.html` file should get displayed).
+
+
+
+## Installation
+
+```bash
+# install optipng and jpegtran packages
+sudo apt-get install optipng libjpeg-progs
+
+# install express-cdn module
+npm install express-cdn
+```
+
+
+
+## Usage
+
+### Server
+
+```js
+// # express-cdn
+
+var express = require('express')
+ , path = require('path')
+ , url = require('url')
+ , app = express.createServer();
+
+// Set the CDN options
+var options = {
+ publicDir : path.join(__dirname, 'public')
+ , viewsDir : path.join(__dirname, 'views')
+ , domain : 'cdn.your-domain.com'
+ , bucket : 'your-site'
+ , key : 'AMAZON_S3_KEY'
+ , secret : 'AMAZON_S3_SECRET'
+ , hostname : 'localhost'
+ , port : 1337
+ , ssl : false
+ , production : true
+};
+
+// Initialize the CDN magic
+var CDN = require('../')(app, options);
+
+app.configure(function() {
+ app.set('view engine', 'jade');
+ app.set('view options', { layout: false, pretty: true });
+ app.enable('view cache');
+ app.use(express.bodyParser());
+ app.use(express.static(path.join(__dirname, 'public')));
+});
+
+// Add the dynamic view helper
+app.dynamicHelpers({ CDN: CDN });
+
+app.get('/', function(req, res, next) {
+ res.render('basic');
+ return;
+});
+
+console.log("Server started: http://localhost:1337");
+app.listen(1337);
+```
+
+### View Engine
+
+#### Jade
+
+```jade
+// #1 - Load an image
+!= CDN('/img/sprite.png')
+
+// #2 - Load an image with a custom tag attribute
+!= CDN('/img/sprite.png', { alt: 'Sprite' })
+
+// #3 - Load a script
+!= CDN('/js/script.js')
+
+// #4 - Load a script with a custom tag attribute
+!= CDN('/js/script.js', { 'data-message': 'Hello' })
+
+// #5 - Load and concat two scripts
+!= CDN([ '/js/plugins.js', '/js/script.js' ])
+
+// #6 - Load and concat two scripts with custom tag attributes
+!= CDN([ '/js/plugins.js', '/js/script.js' ], { 'data-message': 'Hello' })
+
+// #7 - Load a stylesheet
+!= CDN('/css/style.css')
+
+// #8 - Load and concat two stylesheets
+!= CDN([ '/css/style.css', '/css/extra.css' ])
+```
+
+#### EJS
+
+```ejs
+<!-- #1 - Load an image -->
+<%- CDN('/img/sprite.png') %>
+
+<!-- #2 - Load an image with a custom tag attribute -->
+<%- CDN('/img/sprite.png', { alt: 'Sprite' }) %>
+
+<!-- #3 - Load a script -->
+<%- CDN('/js/script.js') %>
+
+<!-- #4 - Load a script with a custom tag attribute -->
+<%- CDN('/js/script.js', { 'data-message': 'Hello' }) %>
+
+<!-- #5 - Load and concat two scripts -->
+<%- CDN([ '/js/plugins.js', '/js/script.js' ]) %>
+
+<!-- #6 - Load and concat two scripts with custom tag attributes -->
+<%- CDN([ '/js/plugins.js', '/js/script.js' ], { 'data-message': 'Hello' }) %>
+
+<!-- #7 - Load a stylesheet -->
+<%- CDN('/css/style.css') %>
+
+<!-- #8 - Load and concat two stylesheets -->
+<%- CDN([ '/css/style.css', '/css/extra.css' ]) %>
+```
+
+### Automatically Rendered HTML
+
+#### Development Mode
+
+```html
+<!-- #1 - Load an image -->
+<img src="/img/sprite.png?v=1341214029" />
+
+<!-- #2 - Load an image with a custom tag attribute -->
+<img src="/img/sprite.png?v=1341214029" alt="Sprite" />
+
+<!-- #3 - Load a script -->
+<script src="/js/script.js?v=1341214029" type="text/javascript"></script>
+
+<!-- #4 - Load a script with a custom tag attribute -->
+<script src="/js/script.js?v=1341214029" type="text/javascript" data-message="Hello"></script>
+
+<!-- #5 - Load and concat two scripts -->
+<script src="/js/plugins.js?v=1341214029" type="text/javascript"></script>
+<script src="/js/script.js?v=1341214029" type="text/javascript"></script>
+
+<!-- #6 - Load and concat two scripts with custom tag attributes -->
+<script src="/js/plugins.js?v=1341214029" type="text/javascript" data-message="Hello"></script>
+<script src="/js/script.js?v=1341214029" type="text/javascript" data-message="Hello"></script>
+
+<!-- #7 - Load a stylesheet -->
+<link href="/css/style.css?v=1341214029" rel="stylesheet" type="text/css" />
+
+<!-- #8 - Load and concat two stylesheets -->
+<link href="/css/style.css?v=1341214029" rel="stylesheet" type="text/css" />
+<link href="/css/extra.css?v=1341214029" rel="stylesheet" type="text/css" />
+```
+
+#### Production Mode
+
+The protocol will automatically change to "https" or "http" depending on the SSL option.
+
+The module will automatically upload and detect new/modified assets based off timestamp,
+as it utilizes the timestamp for version control! There is built-in magic to detect if
+individual assets were changed when concatenating multiple assets together (it adds the
+timestamps together and checks if the combined asset timestamp on S3 exists!).
+
+```html
+<!-- #1 - Load an image -->
+<img src="https://cdn.your-site.com/img/sprite.1341382571.png" />
+
+<!-- #2 - Load an image with a custom tag attribute -->
+<img src="https://cdn.your-site.com/img/sprite.1341382571.png" alt="Sprite" />
+
+<!-- #3 - Load a script -->
+<script src="https://cdn.your-site.com/js/script.1341382571.js" type="text/javascript"></script>
+
+<!-- #4 - Load a script with a custom tag attribute -->
+<script src="https://cdn.your-site.com/js/script.1341382571.js" type="text/javascript" data-message="Hello"></script>
+
+<!-- #5 - Load and concat two scripts -->
+<script src="https://cdn.your-site.com/plugins%2Bscript.1341382571.js" type="text/javascript"></script>
+
+<!-- #6 - Load and concat two scripts with custom tag attributes -->
+<script src="https://cdn.your-site.com/plugins%2Bscript.1341382571.js" type="text/javascript" data-message="Hello"></script>
+
+<!-- #7 - Load a stylesheet -->
+<link href="https://cdn.your-site.com/css/style.1341382571.css" rel="stylesheet" type="text/css" />
+
+<!-- #8 - Load and concat two stylesheets -->
+<link href="https://cdn.your-site.com/style%2Bextra.1341382571.css" rel="stylesheet" type="text/css" />
+```
+
+
+
+## Contributors
+
+* Nick Baugh <niftylettuce@gmail.com>
+
+
+
+## License
+
+MIT Licensed
+
+
+
+[1]: http://optipng.sourceforge.net/
+[2]: http://jpegclub.org/jpegtran/
+[3]: http://sass-lang.com/
+[4]: http://lesscss.org/
+[5]: http://learnboost.github.com/stylus/
+[6]: https://github.com/mishoo/UglifyJS/
+[7]: https://github.com/niftylettuce/express-cachebuster/
+[8]: http://h5bp.com/
+[9]: http://nodejs.org/api/zlib.html
+[10]: https://github.com/LearnBoost/knox/
43 examples/basic.js
@@ -0,0 +1,43 @@
+
+// # express-cdn
+
+var express = require('express')
+ , path = require('path')
+ , url = require('url')
+ , app = express.createServer();
+
+// Set the CDN options
+var options = {
+ publicDir : path.join(__dirname, 'public')
+ , viewsDir : path.join(__dirname, 'views')
+ , domain : 'cdn.your-domain.com'
+ , bucket : 'your-site'
+ , key : 'AMAZON_S3_KEY'
+ , secret : 'AMAZON_S3_SECRET'
+ , hostname : 'localhost'
+ , port : 1337
+ , ssl : false
+ , production : true
+};
+
+// Initialize the CDN magic
+var CDN = require('../')(app, options);
+
+app.configure(function() {
+ app.set('view engine', 'jade');
+ app.set('view options', { layout: false, pretty: true });
+ app.enable('view cache');
+ app.use(express.bodyParser());
+ app.use(express.static(path.join(__dirname, 'public')));
+});
+
+// Add the dynamic view helper
+app.dynamicHelpers({ CDN: CDN });
+
+app.get('/', function(req, res, next) {
+ res.render('basic');
+ return;
+});
+
+console.log("Server started: http://localhost:1337");
+app.listen(1337);
3  examples/public/css/extra.css
@@ -0,0 +1,3 @@
+h2 {
+ color: purple;
+}
6 examples/public/css/style.css
@@ -0,0 +1,6 @@
+h1 {
+ color: red;
+}
+h3 {
+ color: green;
+}
BIN  examples/public/img/box.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  examples/public/img/random.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 examples/public/js/plugins.js
@@ -0,0 +1,6 @@
+var plugins = function(something) {
+ var test = 20, color = 0;
+ for (var i=0; i<test.length; i+=1) {
+ color += 1;
+ }
+};
14 examples/public/js/scripts.js
@@ -0,0 +1,14 @@
+
+// # express-cdn-assets
+
+var test = {
+ one: {
+ name: 'Object 1'
+ },
+ two: {
+ name: 'Object 2'
+ },
+ three: {
+ name: 'Object 3'
+ }
+};
20 examples/views/basic.jade
@@ -0,0 +1,20 @@
+!!! 5
+head
+ title express-cdn
+ != CDN('/css/style.css')
+ != CDN('/css/style.css')
+ != CDN('/css/style.css', { "data-main": 'Hello' })
+ != CDN(['/css/style.css', '/css/extra.css'])
+ != CDN(['/css/style.css', '/css/extra.css'], { "data-main": 'Goodbye' })
+ != CDN('/js/scripts.js')
+ != CDN('/js/scripts.js', { "data-main": 'Evening' })
+ != CDN(['/js/scripts.js', '/js/plugins.js' ])
+ != CDN(['/js/scripts.js', '/js/plugins.js' ], { "data-main": 'Yay' })
+body
+ h1 express-cdn
+ p
+ | Basic example (
+ a(href='view-source:http://localhost:1337') view source
+ | ).
+ != CDN('/img/box.png')
+ != CDN('/img/random.jpg')
4 examples/views/stylesheets/Readme.md
@@ -0,0 +1,4 @@
+
+# Stylesheets
+
+You could put your LESS, Sass, or Stylus files in here and add your compiler to the server.
76 index.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Express CDN</title>
+ <style>
+ ::-moz-selection {
+ background: #b3d4fc;
+ text-shadow: none;
+ }
+ ::selection {
+ background: #b3d4fc;
+ text-shadow: none;
+ }
+ html {
+ padding: 30px 10px;
+ font-size: 20px;
+ line-height: 1.4;
+ color: #737373;
+ background: #f0f0f0;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ background: #CFF191;
+ }
+ body {
+ max-width: 500px;
+ _width: 500px;
+ padding: 30px 20px 50px;
+ border-radius: 4px;
+ margin: 0 auto;
+ }
+ h1 {
+ margin: 0 10px;
+ font-size: 50px;
+ text-align: center;
+ }
+ img {
+ margin-top: 30px;
+ -webkit-transition-duration: 0.8s;
+ -moz-transition-duration: 0.8s;
+ -o-transition-duration: 0.8s;
+ transition-duration: 0.8s;
+ -webkit-transition-property: -webkit-transform;
+ -moz-transition-property: -moz-transform;
+ -o-transition-property: -o-transform;
+ transition-property: transform;
+ overflow:hidden;
+ }
+ img:hover {
+ -webkit-transform:rotate(360deg);
+ -moz-transform:rotate(360deg);
+ -o-transform:rotate(360deg);
+ }
+ a {
+ color: #c83737;
+ text-shadow: 0 -1px 0 rgba(0,0,0,0.75);
+ }
+ a, a:visited, a:active, a:focus, a:hover {
+ text-decoration: none;
+ }
+ .container {
+ max-width: 380px;
+ _width: 380px;
+ margin: 0 auto;
+ text-align: center;
+ }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <h1><a href="https://github.com/niftylettuce/express-cdn">Express CDN</a></h1>
+ <a href="https://github.com/niftylettuce"><img src="https://secure.gravatar.com/avatar/bab3bf4d998bb4605602134e8027b558?s=200" alt="Created by @niftylettuce" /></a>
+ </div>
+</body>
+</html>
2  index.js
@@ -0,0 +1,2 @@
+
+module.exports = require('./lib/main');
437 lib/main.js
@@ -0,0 +1,437 @@
+
+// express-cdn
+// Copyright (c) 2012 Nick Baugh <niftylettuce@gmail.com>
+// MIT Licensed
+
+// Node.js module for delivering optimized, minified, mangled, gzipped,
+// and CDN-hosted assets in Express using S3 and CloudFront.
+
+// * Author: [@niftylettuce](https://twitter.com/#!/niftylettuce)
+// * Source: <https://github.com/niftylettuce/express-cdn>
+
+// # express-cdn
+
+var fs = require('fs')
+ , url = require('url')
+ , path = require('path')
+ , mime = require('mime')
+ , knox = require('knox')
+ , walk = require('walk')
+ , zlib = require('zlib')
+ , async = require('async')
+ , request = require('request')
+ , cleanCSS = require('clean-css')
+ , _ = require('underscore')
+ , jsp = require('uglify-js').parser
+ , pro = require('uglify-js').uglify
+ , spawn = require('child_process').spawn;
+
+_.str = require('underscore.string');
+_.mixin(_.str.exports());
+
+var throwError = function(msg) {
+ throw new Error('CDN: ' + msg);
+};
+
+// `escape` function from Lo-Dash v0.2.2 <http://lodash.com>
+// and Copyright 2012 John-David Dalton <http://allyoucanleet.com/>
+// MIT licensed <http://lodash.com/license>
+var escape = function(string) {
+ return (string + '')
+ .replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&#x27;');
+};
+
+var renderAttributes = function(attributes) {
+ var str = [];
+ for(var name in attributes) {
+ if (_.has(attributes, name)) {
+ str.push(escape(name) + '="' + escape(attributes[name]) + '"');
+ }
+ }
+ return str.sort().join(" ");
+};
+
+var createTag = function(src, asset, attributes, version) {
+ // Cachebusting
+ version = version || '';
+ // Check mime type
+ switch(mime.lookup(asset)) {
+ case 'application/javascript':
+ case 'text/javascript':
+ attributes.type = attributes.type || 'text/javascript';
+ attributes.src = src + asset + version;
+ return '<script ' + renderAttributes(attributes) + '></script>';
+ case 'text/css':
+ attributes.rel = attributes.rel || 'stylesheet';
+ attributes.href = src + asset + version;
+ return '<link ' + renderAttributes(attributes) + ' />';
+ case 'image/png':
+ case 'image/jpg':
+ case 'image/jpeg':
+ case 'image/pjpeg':
+ case 'image/gif':
+ attributes.src = src + asset + version;
+ return '<img ' + renderAttributes(attributes) + ' />';
+ default:
+ throwError('unknown asset type');
+ }
+};
+
+var renderTag = function(options, assets, attributes) {
+ // Set attributes
+ attributes = attributes || {};
+ // In production mode, check for SSL
+ var src = '', position, timestamp = 0;
+ if (options.production) {
+ if (options.ssl) {
+ src = 'https://' + options.domain;
+ } else {
+ src = 'http://' + options.domain;
+ }
+ // Process array by breaking file names into parts
+ // and check that array mime types are all equivalent
+ if (typeof assets === 'object') {
+ var concat = [], type = '';
+ for (var b=0; b<assets.length; b+=1) {
+ if (type === '') type = mime.lookup(assets[b]);
+ else if (mime.lookup(assets[b]) !== type)
+ throwError('mime types in CDN array of assets must all be the same');
+ // Push just the file name to the concat array
+ concat.push(path.basename(assets[b]));
+ timestamp += fs.statSync(path.join(options.publicDir, assets[b])).mtime.getTime();
+ }
+ var name = concat.join("%2B");
+ position = name.lastIndexOf('.');
+ name = _(name).splice(position, 0, '.' + timestamp);
+ return createTag(src, "/" + name, attributes) + "\n";
+ } else {
+ timestamp = fs.statSync(path.join(options.publicDir, assets)).mtime.getTime();
+ position = assets.lastIndexOf('.');
+ return createTag(src, _(assets).splice(position, 0, '.' + timestamp), attributes) + "\n";
+ }
+ } else {
+ // Development mode just pump out assets normally
+ var version = '?v=' + new Date().getTime();
+ var buf = [];
+ if (typeof assets === 'object') {
+ for (var i=0; i<assets.length; i+=1) {
+ buf.push(createTag(src, assets[i], attributes, version));
+ if ( (i + 1) === assets.length) return buf.join("\n") + "\n";
+ }
+ } else if (typeof assets === 'string') {
+ return createTag(src, assets, attributes, version) + "\n";
+ } else {
+ throwError('asset was not a string or an array');
+ }
+ }
+
+};
+
+var compile = function(fileName, assets, S3, options, method, type, timestamp) {
+ return function(err, results) {
+ if (err) throwError(err);
+ if (results instanceof Array) results = results.join("\n");
+ var expires = new Date(new Date().getTime() + (31556926 * 1000)).toUTCString();
+ var headers = {
+ 'Set-Cookie' : ''
+ , 'response-content-type' : type
+ , 'Content-Type' : type
+ , 'response-cache-control' : 'maxage=31556926'
+ , 'Cache-Control' : 'maxage=31556926'
+ , 'response-expires' : expires
+ , 'Expires' : expires
+ , 'response-content-encoding' : 'gzip'
+ , 'Content-Encoding' : 'gzip'
+ };
+ switch(method) {
+ case 'uglify':
+ var ast = jsp.parse(results);
+ ast = pro.ast_mangle(ast);
+ ast = pro.ast_squeeze(ast);
+ var final_code = pro.gen_code(ast);
+ zlib.gzip(final_code, function(err, buffer) {
+ if (err) throwError(err);
+ S3.putGzipBuffer(buffer, encodeURIComponent(fileName), headers, function(err, response) {
+ if (err) throwError(err);
+ if (response.statusCode !== 200)
+ throwError('unsuccessful upload of script "' + fileName + '" to S3');
+ else
+ console.log('successfully uploaded script "' + fileName + '" to S3');
+ });
+ });
+ break;
+ case 'minify':
+ var minify = cleanCSS.process(results);
+ zlib.gzip(minify, function(err, buffer) {
+ if (err) throwError(err);
+ S3.putGzipBuffer(buffer, encodeURIComponent(fileName), headers, function(err, response) {
+ if (err) throwError(err);
+ if (response.statusCode !== 200)
+ throwError('unsuccessful upload of stylesheet "' + fileName + '" to S3');
+ else
+ console.log('successfully uploaded stylesheet "' + fileName + '" to S3');
+ });
+ });
+ break;
+ case 'optipng':
+ var img = path.join(options.publicDir, assets);
+ var optipng = spawn('optipng', [img]);
+ optipng.stdout.on('data', function(data) {
+ console.log('optipng: ' + data);
+ });
+ optipng.stderr.on('data', function(data) {
+ throwError(data);
+ });
+ optipng.on('exit', function(code) {
+ console.log('optipng exited with code ' + code);
+ fs.readFile(img, function(err, data) {
+ zlib.gzip(data, function(err, buffer) {
+ S3.putGzipBuffer(buffer, encodeURIComponent(fileName), headers, function(err, response) {
+ if (err) throwError(err);
+ if (response.statusCode !== 200)
+ throwError('unsuccessful upload of image "' + fileName + '" to S3');
+ else
+ console.log('successfully uploaded image "' + fileName + '" to S3');
+ // Hack to preserve original timestamp for view helper
+ fs.utimesSync(img, new Date(timestamp), new Date(timestamp));
+ });
+ });
+ });
+ });
+ break;
+ case 'jpegtran':
+ var jpg = path.join(options.publicDir, assets);
+ var jpegtran = spawn('jpegtran', [ '-copy', 'none', '-optimize', '-outfile', jpg, jpg ]);
+ jpegtran.stdout.on('data', function(data) {
+ console.log('jpegtran: ' + data);
+ });
+ jpegtran.stderr.on('data', function(data) {
+ throwError(data);
+ });
+ jpegtran.on('exit', function(code) {
+ console.log('jpegtran exited with code ' + code);
+ fs.readFile(jpg, function(err, data) {
+ zlib.gzip(data, function(err, buffer) {
+ S3.putGzipBuffer(buffer, encodeURIComponent(fileName), headers, function(err, response) {
+ if (err) throwError(err);
+ if (response.statusCode !== 200)
+ throwError('unsuccessful upload of image "' + fileName + '" to S3');
+ else
+ console.log('successfully uploaded image "' + fileName + '" to S3');
+ // Hack to preserve original timestamp for view helper
+ fs.utimesSync(jpg, new Date(timestamp), new Date(timestamp));
+ });
+ });
+ });
+ });
+ break;
+ }
+ };
+};
+
+var readUtf8 = function(file, callback) {
+ fs.readFile(file, 'utf8', callback);
+};
+
+var readCSS = function(file, callback) {
+ request.get(file, function(error, response, body) {
+ callback(error, body);
+ });
+};
+
+var js = ['application/javascript', 'text/javascript'];
+
+// Check if the file already exists
+var checkArrayIfModified = function(assets, fileName, S3, options, type) {
+ return function(err, response) {
+ if (err) throwError(err);
+ if (response.statusCode === 200) {
+ console.log('"' + fileName + '" not modified and is already stored on S3');
+ } else {
+ console.log('"' + fileName + '" was not found on S3 or was modified recently');
+ // Check file type
+ switch(type) {
+ case 'application/javascript':
+ case 'text/javascript':
+ async.map(assets, readUtf8, compile(fileName, assets, S3, options, 'uglify', type));
+ return;
+ case 'text/css':
+ async.map(assets, readCSS, compile(fileName, assets, S3, options, 'minify', type));
+ return;
+ default:
+ throwError('unsupported mime type array "' + type + '"');
+ }
+ }
+ };
+};
+
+var checkStringIfModified = function(assets, fileName, S3, options, timestamp) {
+ return function(err, response) {
+ if (err) throwError(err);
+ if (response.statusCode === 200) {
+ console.log('"' + fileName + '" not modified and is already stored on S3');
+ } else {
+ console.log('"' + fileName + '" was not found on S3 or was modified recently');
+ // Check file type
+ var type = mime.lookup(assets);
+ switch(type) {
+ case 'application/javascript':
+ case 'text/javascript':
+ readUtf8(path.join(options.publicDir, assets), compile(fileName, assets, S3, options, 'uglify', type));
+ return;
+ case 'text/css':
+ readCSS(url.format({
+ protocol: (options.ssl) ? 'https' : 'http',
+ hostname: options.hostname,
+ port: options.port,
+ pathname: fileName
+ }), compile(fileName, assets, S3, options, 'minify', type));
+ return;
+ case 'image/png':
+ case 'image/gif':
+ compile(fileName, assets, S3, options, 'optipng', type, timestamp)(null, null);
+ return;
+ case 'image/jpg':
+ case 'image/jpeg':
+ case 'image/pjpeg':
+ compile(fileName, assets, S3, options, 'jpegtran', type, timestamp)(null, null);
+ return;
+ default:
+ throwError('unsupported mime type "' + type + '"');
+ }
+ }
+ };
+};
+
+var processAssets = function(options, results) {
+ // Create knox instance
+ var S3 = knox.createClient({
+ key: options.key
+ , secret: options.secret
+ , bucket: options.bucket
+ });
+ // Go through each result and process it
+ for(var i=0; i<results.length; i+=1) {
+ var assets = results[i], type = '', fileName = '', position, timestamp = 0;
+ // Combine the assets if it is an array
+ if (assets instanceof Array) {
+ // Concat the file names together
+ var concat = [];
+ // Ensure all assets are of the same type
+ for (var k=0; k<assets.length; k+=1) {
+ if (type === '') type = mime.lookup(assets[k]);
+ else if (mime.lookup(assets[k]) !== type)
+ throwError('mime types in array do not match');
+ timestamp += fs.statSync(path.join(options.publicDir, assets[k])).mtime.getTime();
+ if (_.indexOf(js, type) !== -1)
+ assets[k] = path.join(options.publicDir, assets[k]);
+ else if (type === 'text/css')
+ assets[k] = url.format({
+ protocol: (options.ssl) ? 'https' : 'http',
+ hostname: options.hostname,
+ port: options.port,
+ pathname: assets[k]
+ });
+ concat.push(path.basename(assets[k]));
+ }
+ // Set the file name
+ fileName = concat.join("+");
+ position = fileName.lastIndexOf('.');
+ fileName = _(fileName).splice(position, 0, '.' + timestamp);
+ S3.headFile(encodeURIComponent(fileName), checkArrayIfModified(assets, fileName, S3, options, type));
+ } else {
+ // Set the file name
+ fileName = assets.substr(1);
+ position = fileName.lastIndexOf('.');
+ timestamp = fs.statSync(path.join(options.publicDir, assets)).mtime.getTime();
+ fileName = _(fileName).splice(position, 0, '.' + timestamp);
+ S3.headFile(encodeURIComponent(fileName), checkStringIfModified(assets, fileName, S3, options, timestamp));
+ }
+ }
+};
+
+var CDN = function(app, options) {
+
+ // Validate express
+ if (typeof app !== 'object') throwError('requires express');
+
+ // Validate options
+ var required = [
+ 'publicDir'
+ , 'viewsDir'
+ , 'domain'
+ , 'bucket'
+ , 'key'
+ , 'secret'
+ , 'hostname'
+ , 'port'
+ , 'ssl'
+ , 'production'
+ ];
+ required.forEach(function(index) {
+ if (typeof options[index] === 'undefined')
+ throwError('missing option "' + options[index] + '"');
+ });
+
+ if (options.production) {
+
+ var walker = walk.walk(options.viewsDir)
+ , results = []
+ , regexCDN = /CDN\(([^)]+)\)/g;
+ walker.on('file', function(root, stat, next) {
+ var ext = path.extname(stat.name), text;
+ if (ext === '.jade' || ext === '.ejs') {
+ fs.readFile(path.join(root, stat.name), 'utf8', function(err, data) {
+ if (err) throwError(err);
+ var match;
+ while( (match = regexCDN.exec(data)) ) {
+ results.push(match[1]);
+ }
+ next();
+ });
+ }
+ });
+ walker.on('end', function() {
+ // Clean the array
+ for (var i=0; i<results.length; i+=1) {
+ // Convert all apostrophes
+ results[i] = results[i].replace(/\'/g, '"');
+ // Insert assets property name
+ results[i] = _(results[i]).splice(0, 0, '"assets": ');
+ // Check for attributes
+ var attributeIndex = results[i].indexOf('{');
+ if (attributeIndex !== -1)
+ results[i] = _(results[i]).splice(attributeIndex,0,'"attributes": ');
+ // Convert to an object
+ results[i] = '{ ' + results[i] + ' }';
+ results[i] = JSON.parse(results[i]);
+ }
+ // Convert to an array of only assets
+ var out = [];
+ for (var k=0; k<results.length; k+=1) {
+ out[results[k].assets] = results[k].assets;
+ }
+ var clean = [];
+ for (var c in out) {
+ clean.push(out[c]);
+ }
+ // Process the results
+ if (clean.length > 0) processAssets(options, clean);
+ else throwError('empty results');
+ });
+ }
+
+ // Return the dynamic view helper
+ return function(req, res) {
+ return function(assets, attributes) {
+ if (typeof assets === 'undefined') throwError('assets undefined');
+ return renderTag(options, assets, attributes);
+ };
+ };
+
+};
+
+module.exports = CDN;
44 package.json
@@ -0,0 +1,44 @@
+{
+ "name": "express-cdn"
+ , "description": "Node.js module for delivering optimized, minified, mangled, gzipped, and CDN-hosted assets in Express using S3 and CloudFront."
+ , "version": "0.0.1"
+ , "author": "Nick Baugh <niftylettuce@gmail.com>"
+ , "contributors": [
+ { "name": "Nick Baugh", "email": "niftylettuce@gmail.com" }
+ ]
+ , "keywords": [ "express-cdn", "cdn", "assets", "cloudfront", "s3", "amazon", "minify", "mangle", "obfuscate", "optipng", "jpegtran", "optimize", "gzip", "zlib", "uglify", "express" ]
+ , "homepage": "https://github.com/niftylettuce/express-cdn"
+ , "repository": {
+ "type": "git"
+ , "url": "https://github.com/niftylettuce/express-cdn.git"
+ }
+ , "engines": {
+ "node": ">= 0.6.0"
+ }
+ , "main": "lib/main.js"
+ , "devDependencies": {
+ "express" : "2.5.11"
+ , "ejs" : "0.7.2"
+ , "jade" : "0.26.3"
+ , "mime" : "1.2.6"
+ , "underscore" : "1.3.3"
+ , "underscore.string" : "2.2.0rc"
+ , "walk" : "2.2.1"
+ , "knox" : "https://github.com/niftylettuce/knox/tarball/master"
+ , "async" : "0.1.22"
+ , "request" : "2.9.203"
+ , "clean-css" : "0.4.2"
+ , "uglify-js" : "1.3.2"
+ }
+ , "dependencies": {
+ "mime" : "1.2.6"
+ , "underscore" : "1.3.3"
+ , "underscore.string" : "2.2.0rc"
+ , "walk" : "2.2.1"
+ , "knox" : "https://github.com/niftylettuce/knox/tarball/master"
+ , "async" : "0.1.22"
+ , "request" : "2.9.203"
+ , "clean-css" : "0.4.2"
+ , "uglify-js" : "1.3.2"
+ }
+}
Please sign in to comment.
Something went wrong with that request. Please try again.