serverless/lib/defaults/actions/CodePackageLambdaNodejs.js
2015-11-17 21:23:18 -08:00

418 lines
12 KiB
JavaScript

'use strict';
/**
* Action: Code Package: Lambda: Nodejs
* - Accepts one function
* - Collects and optimizes the function's Lambda code in a temp folder
*/
const JawsPlugin = require('../../JawsPlugin'),
JawsError = require('../../jaws-error'),
JawsUtils = require('../../utils/index'),
JawsCli = require('../../utils/cli'),
BbPromise = require('bluebird'),
path = require('path'),
fs = require('fs'),
os = require('os'),
babelify = require('babelify'),
browserify = require('browserify'),
UglifyJS = require('uglify-js'),
wrench = require('wrench'),
Zip = require('node-zip');
// Promisify fs module
BbPromise.promisifyAll(fs);
class CodePackageLambdaNodejs extends JawsPlugin {
/**
* Constructor
*/
constructor(Jaws, config) {
super(Jaws, config);
}
/**
* Get Name
*/
static getName() {
return 'jaws.core.' + CodePackageLambdaNodejs.name;
}
/**
* Register Plugin Actions
*/
registerActions() {
this.Jaws.addAction(this.codePackageLambdaNodejs.bind(this), {
handler: 'codePackageLambdaNodejs',
description: 'Deploys the code or endpoint of a function, or both'
});
return BbPromise.resolve();
}
/**
* Function Deploy
*/
codePackageLambdaNodejs(evt) {
let _this = this;
_this.evt = evt;
// Load AWS Service Instances
let awsConfig = {
region: _this.evt.deployRegion.region,
accessKeyId: _this.Jaws._awsAdminKeyId,
secretAccessKey: _this.Jaws._awsAdminSecretKey,
};
_this.S3 = require('../../utils/aws/S3')(awsConfig);
// Flow
return BbPromise.try(function() {})
.bind(_this)
.then(_this._validateAndPrepare)
.then(_this._createDistFolder)
.then(_this._package)
}
/**
* Validate And Prepare
*/
_validateAndPrepare() {
let _this = this,
lambda;
// Require function config
let functionJson = require(_this.evt.currentFunction);
// Skip Function if it does not have a lambda
try {
lambda = functionJson.cloudFormation.lambda;
} catch(error) {
return Promise.reject(new JawsError(_this.evt.currentFunction + 'does not have a lambda property'));
}
// Validate lambda attributes
if (!lambda.Type
|| !lambda.Properties
|| !lambda.Properties.Runtime
|| !lambda.Properties.Handler) {
return Promise.reject(new JawsError('Missing one of many required lambda attributes'));
}
// Add function path to functionJson
functionJson.path = _this.evt.currentFunction;
// Change function path to object
_this.evt.currentFunction = functionJson;
return BbPromise.resolve();
}
/**
* Create Distribution Folder
*/
_createDistFolder() {
let _this = this;
// Create dist folder
let d = new Date();
_this.evt.distDir = path.join(os.tmpdir(), _this.evt.currentFunction.name + '@' + d.getTime());
// Status
JawsCli.log('Lambda Deployer: Packaging "' + _this.evt.currentFunction.name + '"...');
JawsCli.log('Lambda Deployer: Saving in dist dir ' + _this.evt.distDir);
JawsUtils.jawsDebug('copying', _this.Jaws._projectRootPath, 'to', _this.evt.distDir);
// Copy entire test project to temp folder
let excludePatterns = _this.evt.currentFunction.package.excludePatterns || [];
wrench.copyDirSyncRecursive(
_this.Jaws._projectRootPath,
_this.evt.distDir,
{
exclude: function(name, prefix) {
if (!excludePatterns.length) {
return false;
}
let relPath = path.join(
prefix.replace(_this.evt.distDir, ''), name);
return excludePatterns.some(sRegex => {
relPath = (relPath.charAt(0) == path.sep) ? relPath.substr(1) : relPath;
let re = new RegExp(sRegex),
matches = re.exec(relPath),
willExclude = (matches && matches.length > 0);
if (willExclude) {
JawsCLI.log(`Lambda Deployer: Excluding ${relPath}`);
}
return willExclude;
});
},
}
);
JawsUtils.jawsDebug('Packaging stage & region:', _this.evt.stage, _this.evt.deployRegion);
// Get ENV file from S3
return _this.S3.sGetEnvFile(
_this.evt.deployRegion.jawsBucket,
_this.Jaws._projectJson.name,
_this.evt.stage
)
.then(function(s3ObjData) {
fs.writeFileSync(
path.join(_this.evt.distDir,'.env'),
s3ObjData.Body);
});
}
/**
* Package
*/
_package() {
let _this = this,
lambda = _this.evt.currentFunction.cloudFormation.lambda,
deferred = false,
targetZipPath = path.join(_this.evt.distDir, 'package.zip'),
optimizeSettings = _this.evt.currentFunction.package.optimize;
if (optimizeSettings.builder) {
deferred = _this._optimizeNodeJs()
.then(optimizedCodeBuffer => {
let envData = fs.readFileSync(path.join(_this.evt.distDir, '.env')),
handlerFileName = lambda.Function.Properties.Handler.split('.')[0],
compressPaths = [
// handlerFileName is the full path lambda file including dir rel to back
{fileName: handlerFileName + '.js', data: optimizedCodeBuffer},
{fileName: '.env', data: envData},
];
compressPaths = compressPaths.concat(_this._generateIncludePaths());
return compressPaths;
});
} else {
// User chose not to optimize, zip up whatever is in back
optimizeSettings.includePaths = ['.'];
let compressPaths = _this._generateIncludePaths();
deferred = Promise.resolve(compressPaths);
}
return deferred
.then(compressPaths => {
return _this._compress(compressPaths, targetZipPath);
})
.then(zipFilePath => {
return Promise.resolve({awsmFilePath: _this._lambdaAwsmPath, zipFilePath: zipFilePath});
});
}
/**
* Optimize
*/
_optimize() {
let _this = this,
lambda = _this.evt.currentFunction.cloudFormation.lambda;
if (!_this.evt.currentFunction.package.optimize
|| !_this.evt.currentFunction.package.optimize.builder) {
return Promise.reject(new JawsError('Cant optimize for nodejs. lambda jaws.json does not have optimize.builder set'));
}
if (_this.evt.currentFunction.package.optimize.builder.toLowerCase() == 'browserify') {
return _this._browserifyBundle();
} else {
return Promise.reject(new JawsError(`Unsupported builder ${builder}`));
}
}
/**
* Generate Include Paths
*/
_generateIncludePaths() {
let _this = this,
compressPaths = [],
ignore = ['.DS_Store'],
stats,
fullPath;
_this._awsmJson.package.optimize.includePaths.forEach(p => {
try {
fullPath = path.resolve(path.join(_this._distDir, p));
stats = fs.lstatSync(fullPath);
} catch (e) {
console.error('Cant find includePath ', p, e);
throw e;
}
if (stats.isFile()) {
JawsUtils.jawsDebug('INCLUDING', fullPath);
compressPaths.push({fileName: p, data: fs.readFileSync(fullPath)});
} else if (stats.isDirectory()) {
let dirname = path.basename(p);
wrench
.readdirSyncRecursive(fullPath)
.forEach(file => {
// Ignore certain files
for (let i = 0; i < ignore.length; i++) {
if (file.toLowerCase().indexOf(ignore[i]) > -1) return;
}
let filePath = [fullPath, file].join('/');
if (fs.lstatSync(filePath).isFile()) {
let pathInZip = path.join(dirname, file);
JawsUtils.jawsDebug('INCLUDING', pathInZip);
compressPaths.push({fileName: pathInZip, data: fs.readFileSync(filePath)});
}
});
}
});
return compressPaths;
}
/**
* Compress
*/
_compress(compressPaths, targetZipPath) {
let zip = new Zip();
compressPaths.forEach(nc => {
zip.file(nc.fileName, nc.data);
});
let zipBuffer = zip.generate({
type: 'nodebuffer',
compression: 'DEFLATE',
});
if (zipBuffer.length > 52428800) {
Promise.reject(new JawsError(
'Zip file is > the 50MB Lambda deploy limit (' + zipBuffer.length + ' bytes)',
JawsError.errorCodes.ZIP_TOO_BIG)
);
}
fs.writeFileSync(targetZipPath, zipBuffer);
JawsCLI.log(`Lambda Deployer: Compressed code written to ${targetZipPath}`);
return Promise.resolve(targetZipPath);
}
/**
* Browserify the code and return buffer of bundled code
*
* @returns {Promise.Buffer}
* @private
*/
_browserifyBundle() {
let _this = this;
let uglyOptions = {
mangle: true, // @see http://lisperator.net/uglifyjs/compress
compress: {},
};
let b = browserify({
basedir: _this._distDir,
entries: [_this._awsmJson.cloudFormation.lambda.Function.Properties.Handler.split('.')[0] + '.js'],
standalone: 'lambda',
browserField: false, // Setup for node app (copy logic of --node in bin/args.js)
builtins: false,
commondir: false,
ignoreMissing: true, // Do not fail on missing optional dependencies
detectGlobals: true, // Default for bare in cli is true, but we don't care if its slower
insertGlobalVars: { // Handle process https://github.com/substack/node-browserify/issues/1277
//__filename: insertGlobals.lets.__filename,
//__dirname: insertGlobals.lets.__dirname,
process: function() {
},
},
});
if (_this._awsmJson.package.optimize.babel) {
b.transform(babelify);
}
if (_this._awsmJson.package.optimize.transform) {
JawsUtils.jawsDebug('Adding transform', _this._awsmJson.package.optimize.transform);
b.transform(_this._awsmJson.package.optimize.transform);
}
// optimize.exclude
_this._awsmJson.package.optimize.exclude.forEach(file => {
JawsUtils.jawsDebug('EXCLUDING', file);
b.exclude(file);
});
// optimize.ignore
_this._awsmJson.package.optimize.ignore.forEach(file => {
JawsUtils.jawsDebug('IGNORING', file);
b.ignore(file);
});
// Perform Bundle
let bundledFilePath = path.join(_this._distDir, 'bundled.js'); // Save for auditing
let minifiedFilePath = path.join(_this._distDir, 'minified.js'); // Save for auditing
return new Promise(function(resolve, reject) {
b.bundle(function(err, bundledBuf) {
if (err) {
console.error('Error running browserify bundle');
reject(err);
} else {
fs.writeFileSync(bundledFilePath, bundledBuf);
JawsCLI.log(`Lambda Deployer: Bundled file written to ${bundledFilePath}`);
if (_this._awsmJson.package.optimize.minify) {
JawsUtils.jawsDebug('Minifying...');
let result = UglifyJS.minify(bundledFilePath, uglyOptions);
if (!result || !result.code) {
reject(new JawsError('Problem uglifying code'));
}
fs.writeFileSync(minifiedFilePath, result.code);
JawsCLI.log(`Lambda Deployer: Minified file written to ${minifiedFilePath}`);
resolve(result.code);
} else {
resolve(bundledBuf);
}
}
});
});
}
}
module.exports = CodePackageLambdaNodejs;