Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
'use strict'
/**
* @module serverless-plugin-optimize
*
* @see {@link https://serverless.com/framework/docs/providers/aws/guide/plugins/}
*
* @requires 'babelify'
* @requires 'browserify'
* @requires 'bluebird'
* @requires 'fs-extra'
* @requires 'path'
* @requires 'resolve-from'
* */
const babelify = require('babelify')
const browserify = require('browserify')
const BbPromise = require('bluebird')
const fs = BbPromise.promisifyAll(require('fs-extra'))
const path = require('path')
const resolveFrom = require('resolve-from')
const exorcist = require('exorcist')
/**
* @classdesc Bundle, transpile to ES5 and minify your Serverless functions
* @class Optimize
* */
class Optimize {
/**
* @description Serverless Optimize
* @constructor
*
* @param {!Object} serverless - Serverless object
* @param {!Object} options - Serverless options
* */
constructor (serverless, options) {
/** Serverless variables */
this.serverless = serverless
this.options = options
this.custom = this.serverless.service.custom
this.provider = this.serverless.getProvider('aws')
/** Runtime >=node4.3 */
const validRunTime = (!this.serverless.service.provider.runtime ||
this.serverless.service.provider.runtime === 'nodejs4.3' ||
this.serverless.service.provider.runtime === 'nodejs6.10' ||
this.serverless.service.provider.runtime === 'nodejs8.10' ||
this.serverless.service.provider.runtime === 'nodejs10.x' ||
this.serverless.service.provider.runtime === 'nodejs12.x' ||
this.serverless.service.provider.runtime === 'nodejs14.x')
/** AWS provider and valid runtime check */
if (validRunTime) {
let nodeVersion = 'current'
if (this.serverless.service.provider.runtime) {
nodeVersion = this.serverless.service.provider.runtime.split('nodejs')[1]
}
if (nodeVersion.endsWith('.x')) {
nodeVersion = nodeVersion.replace(/\.x$/, '')
}
/** Optimize variables with default options */
this.optimize = {
functions: [],
options: {
debug: false,
exclude: ['aws-sdk'],
external: [],
externalPaths: {},
extensions: [],
global: false,
includePaths: [],
ignore: [],
minify: true,
plugins: [],
prefix: '_optimize',
presets: [[require.resolve('@babel/preset-env'), {
targets: {
node: nodeVersion
}
}]],
sourceMaps: false
}
}
/** Set custom options */
if (this.custom && this.custom.optimize) {
/** Debug flag */
if (this.custom.optimize.debug === true) {
this.optimize.options.debug = this.custom.optimize.debug
}
/** Exclude */
if (Array.isArray(this.custom.optimize.exclude)) {
this.optimize.options.exclude = this.custom.optimize.exclude
}
/** External */
if (Array.isArray(this.custom.optimize.external)) {
this.optimize.options.external = this.custom.optimize.external
}
/** External Paths */
if (typeof this.custom.optimize.externalPaths === 'object') {
this.optimize.options.externalPaths = this.custom.optimize.externalPaths
}
/** Extensions */
if (Array.isArray(this.custom.optimize.extensions)) {
this.optimize.options.extensions = this.custom.optimize.extensions
}
/** Global transforms */
if (typeof this.custom.optimize.global === 'boolean') {
this.optimize.options.global = this.custom.optimize.global
}
/** Include paths */
if (Array.isArray(this.custom.optimize.includePaths)) {
this.optimize.options.includePaths = this.custom.optimize.includePaths
}
/** Ignore */
if (Array.isArray(this.custom.optimize.ignore)) {
this.optimize.options.ignore = this.custom.optimize.ignore
}
/** Minify flag */
if (typeof this.custom.optimize.minify === 'boolean') {
this.optimize.options.minify = this.custom.optimize.minify
}
/** Babel plugins */
if (Array.isArray(this.custom.optimize.plugins)) {
this.optimize.options.plugins = this.custom.optimize.plugins
}
/** Optimize prefix */
if (typeof this.custom.optimize.prefix === 'string') {
this.optimize.options.prefix = this.custom.optimize.prefix
}
/** Babel presets */
if (Array.isArray(this.custom.optimize.presets)) {
this.optimize.options.presets = this.custom.optimize.presets
}
/** Source maps */
if (typeof this.custom.optimize.sourceMaps === 'boolean') {
this.optimize.options.sourceMaps = this.custom.optimize.sourceMaps
}
}
/** Serverless hooks */
this.hooks = {
'after:package:function:package': this.afterCreateDeploymentArtifacts.bind(this),
'before:package:function:package': this.beforeCreateDeploymentArtifacts.bind(this),
'after:package:createDeploymentArtifacts': this.afterCreateDeploymentArtifacts.bind(this),
'before:package:createDeploymentArtifacts': this.beforeCreateDeploymentArtifacts.bind(this),
'after:invoke:local:invoke': this.afterCreateDeploymentArtifacts.bind(this),
'before:invoke:local:invoke': this.beforeCreateDeploymentArtifacts.bind(this)
}
}
}
/**
* @description Before create deployment artifacts
*
* @fulfil {} — Functions optimized
* @reject {Error} Optimization error
*
* @return {Promise}
* */
beforeCreateDeploymentArtifacts () {
/** Log optimize start */
this.serverless.cli.log('Optimize: starting engines')
/** Get prefix path */
this.path = this.getPath(this.optimize.options.prefix)
/** Package globally or individually */
this.optimize.options.individually = !!(this.serverless.service.package && this.serverless.service.package.individually)
if (!this.optimize.options.individually) {
this.optimize.package = {
exclude: ['**'],
include: [this.optimize.options.prefix + '/**']
}
this.serverless.service.package = this.serverless.service.package || {}
Object.assign(this.serverless.service.package, this.optimize.package)
}
/** Clean prefix folder */
return this.cleanFolder().then(() => {
/** Optimize one function */
if (this.options.function) {
return this.optimizeFunction(this.options.function)
} else {
/** Optimize all functions */
return this.optimizeAllFunctions()
}
})
}
/**
* @description After create deployment artifacts
*
* @fulfil {} — Optimization finished
* @reject {Error} Optimization error
*
* @return {Promise}
* */
afterCreateDeploymentArtifacts () {
/** Log optimize object if debug flag is set */
if (this.optimize.options.debug) {
this.serverless.cli.log('Optimize: debug ' + JSON.stringify(this.optimize, null, 2))
} else {
/** Clean prefix folder */
return this.cleanFolder()
}
}
/**
* @description After create deployment artifacts
*
* @param {string} file — File path
*
* @return {String} Absolute file path
* */
getPath (file) {
return path.join(this.serverless.config.servicePath, file)
}
/**
* @description Create prefix folder
*
* @fulfil {} — Folder created
* @reject {Error} File system error
*
* @return {Promise}
* */
createFolder () {
return fs.mkdirsAsync(this.path)
}
/**
* @description Clean prefix folder
*
* @fulfil {} — Folder cleaned
* @reject {Error} File system error
*
* @return {Promise}
* */
cleanFolder () {
return fs.removeAsync(this.path)
}
/**
* @description Optimize all functions
*
* @fulfil {} — All functions optimized
* @reject {Error} Optimization error
*
* @return {Promise}
* */
optimizeAllFunctions () {
/** Get functions */
const allFunctions = this.serverless.service.getAllFunctions()
/** Create prefix folder */
return this.createFolder().then(() => {
/** Optimize each function */
return BbPromise.map(allFunctions, (functionName) => {
return this.optimizeFunction(functionName)
})
})
}
/**
* @description Optimize function
*
* @param {string} functionName - Function name
*
* @fulfil {} — Function optimized
* @reject {Error} Optimization error
*
* @return {Promise}
* */
async optimizeFunction (functionName) {
/** Function object variables */
const functionObject = this.serverless.service.getFunction(functionName)
functionObject.package = functionObject.package || {}
const functionFileIndex = functionObject.handler.lastIndexOf('.')
const functionPath = functionObject.handler.substring(0, functionFileIndex)
const functionFile = this.getPath(functionPath + '.js')
const functionOptimizePath = this.optimize.options.prefix + '/' + functionObject.name
const functionOptimizeHandler = functionOptimizePath + '/' + functionPath
const functionBundle = this.getPath(functionOptimizeHandler + '.js')
const functionDir = functionPath.substring(0, functionPath.lastIndexOf('/'))
const functionModulesOptimizeDir = functionOptimizePath + '/' + functionDir + '/' + 'node_modules'
/** Skip function */
if (functionObject.optimize === false) {
return BbPromise.resolve('optimization skipped')
}
/** Log function to optimize */
this.serverless.cli.log('Optimize: ' + functionObject.name)
/** Optimize object */
let optimize = {
bundle: functionBundle,
handlerOriginal: functionObject.handler,
handlerOptimize: functionOptimizeHandler + functionObject.handler.substring(functionFileIndex),
package: {
exclude: ['**'],
include: [functionOptimizePath + '/**']
}
}
/** Function optimize options */
let functionOptions = {
exclude: this.optimize.options.exclude,
external: this.optimize.options.external,
externalPaths: Object.assign({}, this.optimize.options.externalPaths),
extensions: this.optimize.options.extensions,
global: this.optimize.options.global,
includePaths: this.optimize.options.includePaths,
ignore: this.optimize.options.ignore,
minify: this.optimize.options.minify,
plugins: this.optimize.options.plugins,
presets: this.optimize.options.presets,
sourceMaps: this.optimize.options.sourceMaps
}
if (functionObject.optimize) {
/** Exclude */
if (Array.isArray(functionObject.optimize.exclude)) {
functionOptions.exclude = optimize.exclude = functionObject.optimize.exclude
}
/** External */
if (Array.isArray(functionObject.optimize.external)) {
functionOptions.external = optimize.external = functionObject.optimize.external
}
/** External paths */
if (typeof functionObject.optimize.externalPaths === 'object') {
functionOptions.externalPaths = optimize.externalPaths = functionObject.optimize.externalPaths
}
/** Extensions */
if (Array.isArray(functionObject.optimize.extensions)) {
functionOptions.extensions = optimize.extensions = functionObject.optimize.extensions
}
/** Global transforms */
if (typeof functionObject.optimize.global === 'boolean') {
functionOptions.global = optimize.global = functionObject.optimize.global
}
/** Include paths */
if (Array.isArray(functionObject.optimize.includePaths)) {
functionOptions.includePaths = optimize.includePaths = functionObject.optimize.includePaths
}
/** Ignore */
if (Array.isArray(functionObject.optimize.ignore)) {
functionOptions.ignore = optimize.ignore = functionObject.optimize.ignore
}
/** Minify flag */
if (typeof functionObject.optimize.minify === 'boolean') {
functionOptions.minify = optimize.minify = functionObject.optimize.minify
}
/** Babel plugins */
if (Array.isArray(functionObject.optimize.plugins)) {
functionOptions.plugins = optimize.plugins = functionObject.optimize.plugins
}
/** Babel presets */
if (Array.isArray(functionObject.optimize.presets)) {
functionOptions.presets = optimize.presets = functionObject.optimize.presets
}
}
/** Browserify */
const bundler = browserify({
entries: [functionFile],
extensions: functionOptions.extensions,
standalone: 'handler',
browserField: false,
builtins: false,
debug: functionOptions.sourceMaps,
commondir: false,
ignoreMissing: true,
detectGlobals: true,
insertGlobalVars: { // https://github.com/substack/node-browserify/issues/1472
process: undefined,
global: undefined,
'Buffer.isBuffer': undefined,
Buffer: undefined
}
})
/** Browserify exclude */
functionOptions.exclude.forEach((exclusion) => {
bundler.exclude(exclusion)
})
/** Browserify external */
functionOptions.external.forEach((external) => {
bundler.external(external)
})
/** Browserify Babili minification preset */
if (functionOptions.minify) {
functionOptions.presets = [[require.resolve('babel-preset-minify'), {
builtIns: false,
mangle: false
}]].concat(functionOptions.presets)
}
/** Browserify babelify transform */
bundler.transform(babelify, {
comments: false,
global: functionOptions.global,
ignore: functionOptions.ignore,
plugins: functionOptions.plugins,
presets: functionOptions.presets
})
var _fs = require('fs');
await fs.outputFileSync( functionBundle, "" );
let writeStream = _fs.createWriteStream( functionBundle );
/** Generate bundle */
return new Promise( ( resolve ) => {
const bundled = bundler.bundle();
if ( functionOptions.sourceMaps ) {
bundled
.pipe( exorcist( functionBundle + '.map' ) )
.pipe( writeStream, 'utf8');
} else {
bundled.pipe( writeStream, 'utf8');
}
resolve();
}).then(() => {
/** Copy includePaths files to prefix folder */
if (functionOptions.includePaths.length) {
return BbPromise.map(functionOptions.includePaths, (includePath) => {
/** Remove relative dot */
if (includePath.substring(0, 2) === './') {
includePath = includePath.substring(2)
}
/** Copy file */
return fs.copyAsync(this.getPath(includePath), this.getPath(functionOptimizePath + '/' + includePath))
})
}
}).then(() => {
/** Copy external files to prefix folder */
if (functionOptions.external.length) {
return BbPromise.map(functionOptions.external, (external) => {
/** Remove relative dot */
if (external.substring(0, 2) === './') {
external = external.substring(2)
}
/** Copy file */
const externalEntry = resolveFrom(functionFile, external) || functionDir + '/'
const externalDir = externalEntry.substring(
this.serverless.config.servicePath.length,
externalEntry.lastIndexOf('node_modules/' + external)
) + 'node_modules/' + external
return fs.copyAsync(
this.getPath(functionOptions.externalPaths[external] || externalDir),
this.getPath(functionModulesOptimizeDir + '/' + external)
)
})
}
})
}
}
/** Export optimize class */
module.exports = Optimize
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment