diff --git a/index.js b/index.js index fc38211..2698013 100644 --- a/index.js +++ b/index.js @@ -3,85 +3,84 @@ var Promise = require('ember-cli/lib/ext/promise'); var minimatch = require('minimatch'); - -var chalk = require('chalk'); -var blue = chalk.blue; -var red = chalk.red; - -var validateConfig = require('./lib/utilities/validate-config'); +var DeployPluginBase = require('ember-cli-deploy-plugin'); var S3 = require('./lib/s3'); module.exports = { name: 'ember-cli-deploy-s3', createDeployPlugin: function(options) { - function _beginMessage(ui, filesToUpload, bucket) { - ui.write(blue('| ')); - ui.writeLine(blue('- preparing to upload to S3 bucket `' + bucket + '`')); - - return Promise.resolve(); - } - - function _successMessage(ui, filesUploaded) { - ui.write(blue('| ')); - ui.writeLine(blue('- uploaded ' + filesUploaded.length + ' files ok')); - - return Promise.resolve(); - } - - function _errorMessage(ui, error) { - ui.write(blue('| ')); - ui.writeLine(red('- ' + error)); - - return Promise.reject(error); - } - - return { + var DeployPlugin = DeployPluginBase.extend({ name: options.name, - - willDeploy: function(context) { - var deployment = context.deployment; - var ui = deployment.ui; - var config = deployment.config[this.name] = deployment.config[this.name] || {}; - - return validateConfig(ui, config) - .then(function() { - ui.write(blue('| ')); - ui.writeLine(blue('- config ok')); - }); + defaultConfig: { + region: 'us-east-1', + filePattern: '**/*.{js,css,png,gif,jpg,map,xml,txt,svg,eot,ttf,woff,woff2}', + prefix: '', + distDir: function(context) { + return context.distDir; + }, + distFiles: function(context) { + return context.distFiles || []; + }, + gzippedFiles: function(context) { + return context.gzippedFiles || []; // e.g. from ember-cli-deploy-gzip + }, + manifestPath: function(context) { + return context.manifestPath; // e.g. from ember-cli-deploy-manifest + }, + uploadClient: function(context) { + return context.uploadClient; // if you want to provide your own upload client to be used instead of one from this plugin + }, + s3Client: function(context) { + return context.s3Client; // if you want to provide your own S3 client to be used instead of one from aws-sdk + } }, + requiredConfig: ['accessKeyId', 'secretAccessKey', 'bucket'], upload: function(context) { - var deployment = context.deployment; - var ui = deployment.ui; - var config = deployment.config[this.name]; + debugger; + var self = this; + + var filePattern = this.readConfig('filePattern'); + var distDir = this.readConfig('distDir'); + var distFiles = this.readConfig('distFiles'); + var gzippedFiles = this.readConfig('gzippedFiles'); + var bucket = this.readConfig('bucket'); + var prefix = this.readConfig('prefix'); + var manifestPath = this.readConfig('manifestPath'); - var filePattern = config.filePattern; - var distDir = context.distDir; - var distFiles = context.distFiles || []; - var gzippedFiles = context.gzippedFiles || []; // e.g. from ember-cli-deploy-gzip var filesToUpload = distFiles.filter(minimatch.filter(filePattern, { matchBase: true })); - var s3 = context.s3Client || new S3({ - ui: ui, - config: config, - client: context.client + var s3 = this.readConfig('uploadClient') || new S3({ + plugin: this }); var options = { cwd: distDir, filePaths: filesToUpload, gzippedFilePaths: gzippedFiles, - prefix: config.prefix, - bucket: config.bucket, - manifestPath: context.manifestPath + prefix: prefix, + bucket: bucket, + manifestPath: manifestPath }; - return _beginMessage(ui, filesToUpload, config.bucket) - .then(s3.upload.bind(s3, options)) - .then(_successMessage.bind(this, ui)) - .catch(_errorMessage.bind(this, ui)); + this.log('preparing to upload to S3 bucket `' + bucket + '`'); + + return s3.upload(options) + .then(function(filesUploaded){ + self.log('uploaded ' + filesUploaded.length + ' files ok'); + return { filesUploaded: filesUploaded }; + }) + .catch(this._errorMessage.bind(this)); + }, + _errorMessage: function(error) { + this.log(error, { color: 'red' }); + if (error) { + this.log(error.stack, { color: 'red' }); + } + return Promise.reject(error); } - }; + }); + return new DeployPlugin(); } }; diff --git a/lib/s3.js b/lib/s3.js index 6204768..a538ab6 100644 --- a/lib/s3.js +++ b/lib/s3.js @@ -7,23 +7,18 @@ var mime = require('mime'); var Promise = require('ember-cli/lib/ext/promise'); var _ = require('lodash'); -var chalk = require('chalk'); -var blue = chalk.blue; var EXPIRE_IN_2030 = new Date('2030'); var TWO_YEAR_CACHE_PERIOD_IN_SEC = 60 * 60 * 24 * 365 * 2; module.exports = CoreObject.extend({ init: function(options) { - var config = options.config; - + this._plugin = options.plugin; var AWS = require('aws-sdk'); - this._client = options.client || new AWS.S3({ - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - region: config.region + this._client = this._plugin.readConfig('s3Client') || new AWS.S3({ + accessKeyId: this._plugin.readConfig('accessKeyId'), + secretAccessKey: this._plugin.readConfig('secretAccessKey'), + region: this._plugin.readConfig('region') }); - - this._ui = options.ui; }, upload: function(options) { @@ -34,7 +29,7 @@ module.exports = CoreObject.extend({ }, _determineFilePaths: function(options) { - var ui = this._ui; + var plugin = this._plugin; var filePaths = options.filePaths || []; if (typeof filePaths === 'string') { filePaths = [filePaths]; @@ -43,8 +38,7 @@ module.exports = CoreObject.extend({ var manifestPath = options.manifestPath; if (manifestPath) { var key = path.join(prefix, manifestPath); - ui.write(blue('| ')); - ui.writeLine(blue("- Downloading manifest for differential deploy from `" + key + "`...")); + plugin.log('Downloading manifest for differential deploy from `' + key + '`...'); return new Promise(function(resolve, reject){ var params = { Bucket: options.bucket, Key: key}; this._client.getObject(params, function(error, data) { @@ -55,13 +49,10 @@ module.exports = CoreObject.extend({ } }.bind(this)); }.bind(this)).then(function(manifestEntries){ - ui.write(blue('| ')); - ui.writeLine(blue("- Manifest found. Differential deploy will be applied.")); + plugin.log("Manifest found. Differential deploy will be applied."); return _.difference(filePaths, manifestEntries); }).catch(function(reason){ - console.log(reason); - ui.write(blue('| ')); - ui.writeLine(blue("- Manifest not found. Disabling differential deploy.")); + plugin.log("Manifest not found. Disabling differential deploy.", { color: 'yellow' }); return Promise.resolve(filePaths); }); } else { @@ -70,7 +61,7 @@ module.exports = CoreObject.extend({ }, _putObjects: function(filePaths, options) { - var ui = this._ui; + var plugin = this._plugin; var cwd = options.cwd; var bucket = options.bucket; var prefix = options.prefix; @@ -110,8 +101,7 @@ module.exports = CoreObject.extend({ if (error) { reject(error); } else { - ui.write(blue('| ')); - ui.writeLine(blue('- ✔ ' + key)); + plugin.log('✔ ' + key); resolve(filePath); } }); diff --git a/lib/utilities/validate-config.js b/lib/utilities/validate-config.js deleted file mode 100644 index b068f20..0000000 --- a/lib/utilities/validate-config.js +++ /dev/null @@ -1,39 +0,0 @@ -var Promise = require('ember-cli/lib/ext/promise'); - -var chalk = require('chalk'); -var yellow = chalk.yellow; -var blue = chalk.blue; -var red = chalk.red; - -module.exports = function(ui, config) { - ui.write(blue('| ')); - ui.write(blue('- validating config\n')); - - var defaultConfig = { - region: 'us-east-1', - filePattern: '**/*.{js,css,png,gif,jpg,map,xml,txt,svg,eot,ttf,woff,woff2}', - prefix: '' - }; - - ['region', 'filePattern', 'prefix'].forEach(function(prop) { - if (!config[prop]) { - var value = defaultConfig[prop]; - config[prop] = value; - ui.write(blue('| ')); - ui.writeLine(yellow('- Missing config: ' + prop + ', using default: `' + value + '`')); - } - }); - - var promise = Promise.resolve(); - - ['accessKeyId', 'secretAccessKey', 'bucket'].forEach(function(prop) { - if (!config[prop]) { - ui.write(blue('| ')); - ui.writeLine(red('- Missing config: `' + prop + '`')); - - promise = Promise.reject(); - } - }); - - return promise; -} diff --git a/package.json b/package.json index ae3808e..0b1724a 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "aws-sdk": "^2.1.25", "chalk": "^1.0.0", "core-object": "^1.1.0", + "ember-cli-deploy-plugin": "0.1.1", "ember-cli-babel": "^5.0.0", "lodash": "^3.9.3", "mime": "^1.3.4", diff --git a/tests/unit/index-nodetest.js b/tests/unit/index-nodetest.js index 1d249b4..6de2774 100644 --- a/tests/unit/index-nodetest.js +++ b/tests/unit/index-nodetest.js @@ -1,7 +1,7 @@ 'use strict'; var assert = require('ember-cli/tests/helpers/assert'); -var Promise = require('ember-cli'); +var Promise = require('ember-cli/lib/ext/promise'); describe('s3 plugin', function() { var subject; @@ -24,16 +24,37 @@ describe('s3 plugin', function() { context = { distDir: process.cwd() + '/tests/fixtures/dist', distFiles: ['app.css', 'app.js'], - deployment: { - ui: mockUi, - config: { - s3: { - accessKeyId: 'aaaa', - secretAccessKey: 'bbbb', - bucket: 'cccc', - region: 'dddd', - filePattern: '*.{css,js}', - prefix: '' + ui: mockUi, + uploadClient: { + upload: function(options) { + return Promise.resolve(['app.css', 'app.js']); + } + }, + config: { + s3: { + accessKeyId: 'aaaa', + secretAccessKey: 'bbbb', + bucket: 'cccc', + region: 'dddd', + filePattern: '*.{css,js}', + prefix: '', + distDir: function(context) { + return context.distDir; + }, + distFiles: function(context) { + return context.distFiles || []; + }, + gzippedFiles: function(context) { + return context.gzippedFiles || []; // e.g. from ember-cli-deploy-gzip + }, + manifestPath: function(context) { + return context.manifestPath; // e.g. from ember-cli-deploy-manifest + }, + uploadClient: function(context) { + return context.uploadClient; // if you want to provide your own upload client to be used instead of one from this addon + }, + s3Client: function(context) { + return context.s3Client; // if you want to provide your own s3 client to be used instead of one from aws-sdk } } } @@ -53,27 +74,94 @@ describe('s3 plugin', function() { name: 's3' }); - assert.equal(typeof plugin.willDeploy, 'function'); - assert.equal(typeof plugin.upload, 'function'); + assert.typeOf(plugin.configure, 'function'); + assert.typeOf(plugin.upload, 'function'); }); - describe('willDeploy hook', function() { - it('resolves if config is ok', function() { + describe('configure hook', function() { + it('does not throw if config is ok', function() { + var plugin = subject.createDeployPlugin({ + name: 's3' + }); + plugin.beforeHook(context); + plugin.configure(context); + assert.ok(true); // it didn't throw + }); + + it('throws if config is not valid', function() { var plugin = subject.createDeployPlugin({ name: 's3' }); - return assert.isFulfilled(plugin.willDeploy.call(plugin, context)) + context.config.s3 = {}; + + plugin.beforeHook(context); + assert.throws(function(){ + plugin.configure(context); + }); }); + it('warns about missing optional config', function() { + delete context.config.s3.region; + delete context.config.s3.filePattern; + delete context.config.s3.prefix; - it('rejects if config is not valid', function() { var plugin = subject.createDeployPlugin({ name: 's3' }); + plugin.beforeHook(context); + plugin.configure(context); + var messages = mockUi.messages.reduce(function(previous, current) { + if (/- Missing config:\s.*, using default:\s/.test(current)) { + previous.push(current); + } - context.deployment.config.s3 = {}; + return previous; + }, []); - return assert.isRejected(plugin.willDeploy.call(plugin, context)) + assert.equal(messages.length, 3); + }); + + it('warns about missing required config', function() { + delete context.config.s3.accessKeyId; + delete context.config.s3.secretAccessKey; + delete context.config.s3.bucket; + + var plugin = subject.createDeployPlugin({ + name: 's3' + }); + plugin.beforeHook(context); + assert.throws(function(error){ + plugin.configure(context); + }); + var messages = mockUi.messages.reduce(function(previous, current) { + if (/- Missing config:\s.*/.test(current)) { + previous.push(current); + } + + return previous; + }, []); + + assert.equal(messages.length, 1); // doesn't log all failures, just first one + }); + + it('adds default config to the config object', function() { + delete context.config.s3.region; + delete context.config.s3.filePattern; + delete context.config.s3.prefix; + + assert.isUndefined(context.config.s3.region); + assert.isUndefined(context.config.s3.filePattern); + assert.isUndefined(context.config.s3.prefix); + + var plugin = subject.createDeployPlugin({ + name: 's3' + }); + plugin.beforeHook(context); + plugin.configure(context); + + assert.isDefined(context.config.s3.region); + assert.isDefined(context.config.s3.filePattern); + assert.isDefined(context.config.s3.prefix); }); }); @@ -83,15 +171,10 @@ describe('s3 plugin', function() { name: 's3' }); - context.client = { - putObject: function(params, cb) { - cb(); - } - }; - - return assert.isFulfilled(plugin.upload.call(plugin, context)) + plugin.beforeHook(context); + return assert.isFulfilled(plugin.upload(context)) .then(function() { - assert.equal(mockUi.messages.length, 4); + assert.equal(mockUi.messages.length, 2); assert.match(mockUi.messages[0], /preparing to upload to S3 bucket `cccc`/); }); }); @@ -101,15 +184,10 @@ describe('s3 plugin', function() { name: 's3' }); - context.client = { - putObject: function(params, cb) { - cb(); - } - }; - - return assert.isFulfilled(plugin.upload.call(plugin, context)) + plugin.beforeHook(context); + return assert.isFulfilled(plugin.upload(context)) .then(function() { - assert.equal(mockUi.messages.length, 4); + assert.equal(mockUi.messages.length, 2); var messages = mockUi.messages.reduce(function(previous, current) { if (/- uploaded 2 files ok/.test(current)) { @@ -128,16 +206,17 @@ describe('s3 plugin', function() { name: 's3' }); - context.client = { - putObject: function(params, cb) { - cb('something bad went wrong'); + context.uploadClient = { + upload: function(opts) { + return Promise.reject(new Error('something bad went wrong')); } }; - return assert.isRejected(plugin.upload.call(plugin, context)) + plugin.beforeHook(context); + return assert.isRejected(plugin.upload(context)) .then(function() { - assert.equal(mockUi.messages.length, 2); - assert.match(mockUi.messages[1], /- something bad went wrong/); + assert.equal(mockUi.messages.length, 3); + assert.match(mockUi.messages[1], /- Error: something bad went wrong/); }); }); @@ -148,8 +227,10 @@ describe('s3 plugin', function() { context.gzippedFiles = ['app.css']; var assertionCount = 0; - context.client = { + context.uploadClient = null; + context.s3Client = { putObject: function(params, cb) { + debugger; if (params.Key === 'app.css') { assert.equal(params.ContentEncoding, 'gzip'); assertionCount++; @@ -158,10 +239,14 @@ describe('s3 plugin', function() { assertionCount++; } cb(); + }, + getObject: function(params, cb){ + cb(new Error("File not found")); } }; - return assert.isFulfilled(plugin.upload.call(plugin, context)).then(function(){ + plugin.beforeHook(context); + return assert.isFulfilled(plugin.upload(context)).then(function(){ assert.equal(assertionCount, 2); done(); }).catch(function(reason){ diff --git a/tests/unit/lib/s3-nodetest.js b/tests/unit/lib/s3-nodetest.js index c0cf374..863fa7a 100644 --- a/tests/unit/lib/s3-nodetest.js +++ b/tests/unit/lib/s3-nodetest.js @@ -1,14 +1,21 @@ var assert = require('ember-cli/tests/helpers/assert'); describe('s3', function() { - var S3; - var mockUi; + var S3, mockUi, s3Client, plugin, subject; before(function() { S3 = require('../../../lib/s3'); }); beforeEach(function() { + s3Client = { + putObject: function(params, cb) { + cb(); + }, + getObject: function(params, cb){ + cb(new Error("File not found")); + } + }; mockUi = { messages: [], write: function() {}, @@ -16,19 +23,25 @@ describe('s3', function() { this.messages.push(message); } }; + plugin = { + ui: mockUi, + readConfig: function(propertyName) { + if (propertyName === 's3Client') { + return s3Client; + } + }, + log: function(message, opts) { + this.ui.write('| '); + this.ui.writeLine('- ' + message); + } + }; + subject = new S3({ + plugin: plugin + }); }); describe('#upload', function() { it('resolves if all uploads succeed', function() { - var subject = new S3({ - ui: mockUi, - client: { - putObject: function(params, cb) { - cb(); - } - } - }); - var options = { filePaths: ['app.js', 'app.css'], cwd: process.cwd() + '/tests/fixtures/dist', @@ -54,14 +67,9 @@ describe('s3', function() { }); it('rejects if an upload fails', function() { - var subject = new S3({ - ui: mockUi, - client: { - putObject: function(params, cb) { - cb('error uploading'); - } - } - }); + s3Client.putObject = function(params, cb) { + cb('error uploading'); + }; var options = { filePaths: ['app.js', 'app.css'], @@ -79,16 +87,10 @@ describe('s3', function() { describe('sending the object to s3', function() { it('sends the correct params', function() { var s3Params; - - var subject = new S3({ - ui: mockUi, - client: { - putObject: function(params, cb) { - s3Params = params; - cb(); - } - } - }); + s3Client.putObject = function(params, cb) { + s3Params = params; + cb(); + }; var options = { filePaths: ['app.js'], @@ -114,17 +116,6 @@ describe('s3', function() { describe('with a manifestPath specified', function () { it('uploads all files when manifest is missing from server', function (done) { - var subject = new S3({ - ui: mockUi, - client: { - putObject: function(params, cb) { - cb(); - }, - getObject: function(params, cb){ - cb(new Error("File not found")); - } - } - }); var options = { filePaths: ['app.js', 'app.css'], cwd: process.cwd() + '/tests/fixtures/dist', @@ -149,19 +140,11 @@ describe('s3', function() { }); it('only uploads missing files when manifest is present on server', function (done) { - var subject = new S3({ - ui: mockUi, - client: { - putObject: function(params, cb) { - cb(); - }, - getObject: function(params, cb){ - cb(undefined, { - Body: "app.js" - }); - } - } - }); + s3Client.getObject = function(params, cb){ + cb(undefined, { + Body: "app.js" + }); + }; var options = { filePaths: ['app.js', 'app.css'], diff --git a/tests/unit/lib/utilities/validate-config-nodetest.js b/tests/unit/lib/utilities/validate-config-nodetest.js deleted file mode 100644 index c22f244..0000000 --- a/tests/unit/lib/utilities/validate-config-nodetest.js +++ /dev/null @@ -1,97 +0,0 @@ -var assert = require('ember-cli/tests/helpers/assert'); - -describe('validate-config', function() { - var subject; - var config; - var mockUi; - - before(function() { - subject = require('../../../../lib/utilities/validate-config'); - }); - - beforeEach(function() { - config = { - accessKeyId: 'aaaa', - secretAccessKey: 'bbbb', - bucket: 'cccc', - region: 'dddd', - filePattern: 'eeee', - prefix: 'ffff' - }; - - mockUi = { - messages: [], - write: function() {}, - writeLine: function(message) { - this.messages.push(message); - } - }; - }); - - it('warns about missing optional config', function() { - delete config.region; - delete config.filePattern; - delete config.prefix; - - return assert.isFulfilled(subject(mockUi, config)) - .then(function() { - var messages = mockUi.messages.reduce(function(previous, current) { - if (/- Missing config:\s.*, using default:\s/.test(current)) { - previous.push(current); - } - - return previous; - }, []); - - assert.equal(messages.length, 3); - }); - }); - - it('warns about missing required config', function() { - delete config.accessKeyId; - delete config.secretAccessKey; - delete config.bucket; - - return assert.isRejected(subject(mockUi, config)) - .then(function() { - var messages = mockUi.messages.reduce(function(previous, current) { - if (/- Missing config:\s.*/.test(current)) { - previous.push(current); - } - - return previous; - }, []); - - assert.equal(messages.length, 3); - }); - }); - - it('adds default config to the config object', function() { - delete config.region; - delete config.filePattern; - delete config.prefix; - - assert.isUndefined(config.region); - assert.isUndefined(config.filePattern); - assert.isUndefined(config.prefix); - - return assert.isFulfilled(subject(mockUi, config)) - .then(function() { - assert.isDefined(config.region); - assert.isDefined(config.filePattern); - assert.isDefined(config.prefix); - }); - }); - - it('resolves if config is ok', function() { - return assert.isFulfilled(subject(mockUi, config)); - }) - - it('rejects if config is invalid', function() { - delete config.accessKeyId; - delete config.secretAccessKey; - delete config.bucket; - - return assert.isRejected(subject(mockUi, config)); - }); -});