diff --git a/README.md b/README.md index bd45b85c..c1a9fd84 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ clean runs git clean -fxd deploy [app] runs haikro deployment scripts with sensible defaults for Next projects + deploy-hashed-assets deploys hashed asset files to github.io and S3 (if AWS keys set correctly) configure [options] [source] [target] downloads environment variables from next-config-vars and uploads them to the current app download-configuration downloads environment variables from app from Heroku to make adding them to the next-config-vars service easier provision [app] provisions a new instance of an application server diff --git a/bin/next-build-tools.js b/bin/next-build-tools.js index 7c32966e..5bb971cd 100755 --- a/bin/next-build-tools.js +++ b/bin/next-build-tools.js @@ -15,6 +15,7 @@ var destroy = require('../tasks/destroy'); var nightwatch = require('../tasks/nightwatch'); var downloadConfiguration = require('../tasks/download-configuration'); var deployHashedAssets = require('../tasks/deploy-hashed-assets'); +var deployHashedAssetsToS3 = require('../tasks/deploy-hashed-assets-s3'); function list(val) { return val.split(','); @@ -107,7 +108,10 @@ program .command('deploy-hashed-assets') .description('deploys ./hashed-assets/ to on GitHub') .action(function() { - deployHashedAssets().catch(exit); + Promise.all([ + deployHashedAssets(), + deployHashedAssetsToS3() + ]).catch(exit); }); program diff --git a/package.json b/package.json index d723779c..7d5e7ce5 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "test": "./bin/next-build-tools.js verify" }, "dependencies": { + "aws-sdk": "^2.1.19", "commander": "^2.6.0", "debug": "^2.1.1", "denodeify": "^1.2.0", diff --git a/tasks/deploy-hashed-assets-s3.js b/tasks/deploy-hashed-assets-s3.js new file mode 100644 index 00000000..3417dfe6 --- /dev/null +++ b/tasks/deploy-hashed-assets-s3.js @@ -0,0 +1,106 @@ +"use strict"; + +var packageJson = require(process.cwd() + '/package.json'); +var denodeify = require('denodeify'); +var normalizeName = require('../lib/normalize-name'); +var readFile = denodeify(require('fs').readFile); +var writeFile = denodeify(require('fs').writeFile); +var glob = denodeify(require('glob')); +var crypto = require('crypto'); +var basename = require('path').basename; +var aws = require('aws-sdk'); + +aws.config.update({ + accessKeyId: process.env.aws_access_hashed_assets, + secretAccessKey: process.env.aws_secret_hashed_assets, + region: 'eu-west-1' +}); + +function hashAndUpload(opts) { + + var file = opts.file; + var app = opts.app; + var bucket = 'ft-next-hashed-assets-prod'; + var key = app + '/' + file.hashedName; + + return new Promise(function(resolve, reject) { + var s3bucket = new aws.S3({params: {Bucket: bucket}}); + var params = { + Key: key, + Body: file.content, + ACL: 'public-read', + CacheControl: 'public, max-age=604800000' + }; + s3bucket.upload(params, function(err, data) { + if (err) { + console.log("Error uploading data: ", err); + reject(err); + } else { + resolve({ + name: file.name, + hashedName: file.hashedName + }); + } + }); + }); +} + +module.exports = function(app) { + if(!(process.env.aws_access && process.env.aws_secret)) { + console.warn("Missing AWS keys. Hashed assets will not deploy to S3"); + return Promise.resolve(); + } + + app = app || normalizeName(packageJson.name, { version: false }); + + console.log('Deploying hashed assets to S3...'); + return glob(process.cwd() + '/public/*.@(css|js|map)') + .then(function(files) { + return Promise.all(files.map(function(file) { + return readFile(file) + .then(function(content) { + return { + name: basename(file), + content: content + }; + }); + })); + }) + .then(function(files) { + var mapHashName = ''; + return files + .map(function(file) { + var hash = crypto.createHash('sha1').update(file.content.toString('utf8')).digest('hex'); + file.hashedName = file.name.replace(/(.*)(\.[a-z0-9])/i, '$1-' + hash.substring(0, 8) + '$2'); + if (file.name === 'main.js.map') { + mapHashName = file.hashedName; + } + return file; + }) + .map(function(file) { + var content; + if (file.name === 'main.js') { + content = file.content.toString('utf8'); + content = content.replace('/# sourceMappingURL=/' + app + '/' + file.name + '.map', '/# sourceMappingURL=/next-hashed-assets/' + app + '/' + mapHashName); + file.content = new Buffer(content, 'utf8'); + } + return file; + }); + }) + .then(function(files) { + var promise = files.reduce(function(promise, file) { + return promise.then(function(obj) { + return hashAndUpload({ app: app, file: file }) + .then(function(file) { + obj[file.name] = file.hashedName; + return obj; + }); + }); + }, Promise.resolve({})); + return promise; + }) + .then(function(hashes) { + console.log("Writing public/asset-hashes.json"); + return writeFile(process.cwd() + '/public/asset-hashes.json', JSON.stringify(hashes, undefined, 2)); + }); +};