diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..d5876c1 --- /dev/null +++ b/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015", "stage-0"], + "plugins": ["transform-runtime"] +} diff --git a/.eslintignore b/.eslintignore index 3c3629e..d1094cb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ node_modules +1.0/index.js diff --git a/.gitignore b/.gitignore index f897b5a..56d370a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ logs *.log npm-debug.log +.eslintcache # Runtime data pids @@ -37,4 +38,4 @@ node_modules #SERVERLESS STUFF admin.env -.env \ No newline at end of file +.env diff --git a/1.0/index.js b/1.0/index.js new file mode 100644 index 0000000..92dacd5 --- /dev/null +++ b/1.0/index.js @@ -0,0 +1,178 @@ +'use strict'; + +var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); + +var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); + +var _createClass2 = require('babel-runtime/helpers/createClass'); + +var _createClass3 = _interopRequireDefault(_createClass2); + +var _regenerator = require('babel-runtime/regenerator'); + +var _regenerator2 = _interopRequireDefault(_regenerator); + +var _promise = require('babel-runtime/core-js/promise'); + +var _promise2 = _interopRequireDefault(_promise); + +var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator'); + +var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2); + +var runWebpack = function () { + var _ref = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee(config) { + return _regenerator2.default.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + return _context.abrupt('return', new _promise2.default(function (resolve, reject) { + (0, _webpack2.default)(config).run(function (err, stats) { + if (err) { + return reject(err); + } + return resolve(stats); + }); + })); + + case 1: + case 'end': + return _context.stop(); + } + } + }, _callee, this); + })); + + return function runWebpack(_x) { + return _ref.apply(this, arguments); + }; +}(); + +var _fs = require('fs'); + +var _fs2 = _interopRequireDefault(_fs); + +var _path = require('path'); + +var _path2 = _interopRequireDefault(_path); + +var _webpack = require('webpack'); + +var _webpack2 = _interopRequireDefault(_webpack); + +var _nodeZip = require('node-zip'); + +var _nodeZip2 = _interopRequireDefault(_nodeZip); + +var _fp = require('lodash/fp'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function format(stats) { + return stats.toString({ + colors: true, + hash: false, + version: false, + chunks: false, + children: false + }); +} + +var artifact = 'handler.js'; + +var getConfig = function getConfig(servicePath) { + return require(_path2.default.resolve(servicePath, './webpack.config.js')); +}; // eslint-disable-line global-require + +var zip = function zip(zipper, readFile, dir) { + (0, _fp.forEach)(function (file) { + return zipper.file(file, readFile(_path2.default.resolve(dir, file))); + }, _fs2.default.readdirSync(dir)); + return zipper.generate({ + type: 'nodebuffer', + compression: 'DEFLATE', + platform: process.platform + }); +}; + +module.exports = function () { + function ServerlessWebpack(serverless) { + (0, _classCallCheck3.default)(this, ServerlessWebpack); + + this.serverless = serverless; + this.hooks = { + 'before:deploy:createDeploymentArtifacts': this.optimize.bind(this) + }; + } + + (0, _createClass3.default)(ServerlessWebpack, [{ + key: 'optimize', + value: function () { + var _ref2 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee2() { + var servicePath, serverlessTmpDirPath, handlerNames, entrypoints, webpackConfig, outputDir, stats, data, zipFileName, artifactFilePath; + return _regenerator2.default.wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + if (this.serverless.getVersion().startsWith('1.0')) { + _context2.next = 2; + break; + } + + throw new this.serverless.classes.Error('This version of serverless-webpack-plugin requires Serverless 1.0'); + + case 2: + servicePath = this.serverless.config.servicePath; + serverlessTmpDirPath = _path2.default.join(servicePath, '.serverless'); + handlerNames = (0, _fp.uniq)((0, _fp.map)(function (f) { + return f.handler.split('.')[0]; + }, this.serverless.service.functions)); + entrypoints = (0, _fp.map)(function (h) { + return './' + h + '.js'; + }, handlerNames); + webpackConfig = getConfig(servicePath); + + webpackConfig.context = servicePath; + webpackConfig.entry = (0, _fp.compact)((0, _fp.concat)(webpackConfig.entry, entrypoints)); + + outputDir = _path2.default.join(serverlessTmpDirPath, 'output'); + + webpackConfig.output = { + libraryTarget: 'commonjs', + path: outputDir, + filename: artifact + }; + + _context2.next = 13; + return runWebpack(webpackConfig); + + case 13: + stats = _context2.sent; + + this.serverless.cli.log(format(stats)); + + data = zip(new _nodeZip2.default(), _fs2.default.readFileSync, outputDir); + zipFileName = this.serverless.service.service + '-' + new Date().getTime().toString() + '.zip'; + artifactFilePath = _path2.default.resolve(serverlessTmpDirPath, zipFileName); + + + this.serverless.utils.writeFileSync(artifactFilePath, data); + this.serverless.service.package.artifact = artifactFilePath; + + case 20: + case 'end': + return _context2.stop(); + } + } + }, _callee2, this); + })); + + function optimize() { + return _ref2.apply(this, arguments); + } + + return optimize; + }() + }]); + return ServerlessWebpack; +}(); \ No newline at end of file diff --git a/1.0/src/.eslintrc.yaml b/1.0/src/.eslintrc.yaml new file mode 100644 index 0000000..6d1bb74 --- /dev/null +++ b/1.0/src/.eslintrc.yaml @@ -0,0 +1,9 @@ +--- +parser: babel-eslint +extends: airbnb +env: + es6: true +rules: + strict: 0 + object-shorthand: 0 + react/require-extension: 0 diff --git a/1.0/src/index.js b/1.0/src/index.js new file mode 100644 index 0000000..7ca64b6 --- /dev/null +++ b/1.0/src/index.js @@ -0,0 +1,95 @@ +import fs from 'fs'; +import path from 'path'; +import webpack from 'webpack'; +import Zip from 'node-zip'; + +import { + compact, + concat, + forEach, + map, + uniq, +} from 'lodash/fp'; + +async function runWebpack(config) { + return new Promise((resolve, reject) => { + webpack(config).run((err, stats) => { + if (err) { + return reject(err); + } + return resolve(stats); + }); + }); +} + +function format(stats) { + return stats.toString({ + colors: true, + hash: false, + version: false, + chunks: false, + children: false, + }); +} + +const artifact = 'handler.js'; + +const getConfig = servicePath => + require(path.resolve(servicePath, './webpack.config.js')); // eslint-disable-line global-require + +const zip = (zipper, readFile, dir) => { + forEach(file => + zipper.file(file, readFile(path.resolve(dir, file)) + ), fs.readdirSync(dir)); + return zipper.generate({ + type: 'nodebuffer', + compression: 'DEFLATE', + platform: process.platform, + }); +}; + +module.exports = class ServerlessWebpack { + constructor(serverless) { + this.serverless = serverless; + this.hooks = { + 'before:deploy:createDeploymentArtifacts': this.optimize.bind(this), + }; + } + + async optimize() { + if (!this.serverless.getVersion().startsWith('1.0')) { + throw new this.serverless.classes.Error( + 'This version of serverless-webpack-plugin requires Serverless 1.0' + ); + } + const servicePath = this.serverless.config.servicePath; + const serverlessTmpDirPath = path.join(servicePath, '.serverless'); + + const handlerNames = uniq(map(f => + f.handler.split('.')[0], this.serverless.service.functions)); + const entrypoints = map(h => `./${h}.js`, handlerNames); + + const webpackConfig = getConfig(servicePath); + webpackConfig.context = servicePath; + webpackConfig.entry = compact(concat(webpackConfig.entry, entrypoints)); + + const outputDir = path.join(serverlessTmpDirPath, 'output'); + webpackConfig.output = { + libraryTarget: 'commonjs', + path: outputDir, + filename: artifact, + }; + + const stats = await runWebpack(webpackConfig); + this.serverless.cli.log(format(stats)); + + const data = zip(new Zip(), fs.readFileSync, outputDir); + + const zipFileName = + `${this.serverless.service.service}-${(new Date).getTime().toString()}.zip`; + const artifactFilePath = path.resolve(serverlessTmpDirPath, zipFileName); + + this.serverless.utils.writeFileSync(artifactFilePath, data); + this.serverless.service.package.artifact = artifactFilePath; + } +}; diff --git a/README.md b/README.md index 1ab5943..1840e51 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,37 @@ Serverless Webpack Plugin ============================= -Forked from [serverless-optimizer-plugin](https://github.com/serverless/serverless-optimizer-plugin) this plugin uses +Forked from [serverless-optimizer-plugin](https://github.com/serverless/serverless-optimizer-plugin) this plugin uses webpack to optimize your Serverless Node.js Functions on deployment. -Reducing the file size of your AWS Lambda Functions allows AWS to provision them more quickly, speeding up the response +Reducing the file size of your AWS Lambda Functions allows AWS to provision them more quickly, speeding up the response time of your Lambdas. Smaller Lambda sizes also helps you develop faster because you can upload them faster. This Severless Plugin is absolutely recommended for every project including Lambdas with Node.js. -**Note:** Requires Serverless *v0.5.0*. +**Note:** Requires Serverless *v0.5.0* or Serverless *v1.0* ### Setup * Install the plugin and webpack in the root of your Serverless Project: -``` + +```sh npm install serverless-webpack-plugin webpack --save-dev ``` -* Add the plugin to the `plugins` array in your Serverless Project's `s-project.json`, like this: +#### Using with Serverless 1.0... +* Add the plugin to the `plugins` array in `serverless.yml`: + +```yaml +plugins: + - serverless-webpack-plugin/1.0 ``` + +#### Using with Serverless 0.5... + +* Add the plugin to the `plugins` array in your Serverless Project's `s-project.json`, like this: + +```json plugins: [ "serverless-webpack-plugin" ] @@ -85,13 +97,13 @@ module.exports = { } }; ``` -**Note:** Some node modules don't play nicely with `webpack.optimize.UglifyJsPlugin` in this case, you can omit it from +**Note:** Some node modules don't play nicely with `webpack.optimize.UglifyJsPlugin` in this case, you can omit it from your config, or add the offending modules to `externals`. For more on externals see below. ### Externals -Externals specified in your webpack config will be properly packaged into the deployment. -This is useful when working with modules that have binary dependencies, are incompatible with `webpack.optimize.UglifyJsPlugin` -or if you simply want to improve build performance. Check out [webpack-node-externals](https://github.com/liady/webpack-node-externals) +Externals specified in your webpack config will be properly packaged into the deployment. +This is useful when working with modules that have binary dependencies, are incompatible with `webpack.optimize.UglifyJsPlugin` +or if you simply want to improve build performance. Check out [webpack-node-externals](https://github.com/liady/webpack-node-externals) for an easy way to externalize all node modules. ### Source Maps @@ -103,8 +115,8 @@ you can specify those modules with entry option in your webpack config. For example if you need to load the babel-polyfill, you can do that by adding `entry: ['babel-polyfill']` to your webpack config. This will first load the babel-polyfill module and then your lambda function module. - + ### Improving deploy performance - -The plugin builds directly from the source files, using "magic handlers" to include the parent directory (as mentioned in -the [0.5.0 release notes](https://github.com/serverless/serverless/releases/tag/v0.5.0)) is unnecessary. + +The plugin builds directly from the source files, using "magic handlers" to include the parent directory (as mentioned in +the [0.5.0 release notes](https://github.com/serverless/serverless/releases/tag/v0.5.0)) is unnecessary. diff --git a/index.js b/index.js index aef6db0..99f2656 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,3 @@ -'use strict'; const path = require('path'); const webpack = require('webpack'); const Promise = require('bluebird'); @@ -15,13 +14,13 @@ function runWebpack(config) { if (err) { return reject(err); } - resolve(stats); + return resolve(stats); }); }); } module.exports = function getPlugin(S) { - const SCli = require(S.getServerlessPath('utils/cli')); + const SCli = require(S.getServerlessPath('utils/cli')); // eslint-disable-line global-require function logStats(stats) { SCli.log(stats.toString({ @@ -50,7 +49,7 @@ module.exports = function getPlugin(S) { optimize(evt) { // Validate: Check Serverless version - if (parseInt(S._version.split('.')[1], 10) < 5) { + if (parseInt(S._version.split('.')[1], 10) < 5) { // eslint-disable-line no-underscore-dangle SCli.log('WARNING: This version of the Serverless Optimizer Plugin ' + 'will not work with a version of Serverless that is less than v0.5'); } diff --git a/lib/copyModules.js b/lib/copyModules.js index 10030c4..5445956 100644 --- a/lib/copyModules.js +++ b/lib/copyModules.js @@ -1,4 +1,3 @@ -'use strict'; const path = require('path'); const childProcess = require('child_process'); const Promise = require('bluebird'); @@ -11,7 +10,7 @@ module.exports = function copyModules(projectPath, moduleNames, dest) { return Promise.resolve(); } - const pkg = require(path.join(projectPath, 'package.json')); + const pkg = require(path.join(projectPath, 'package.json')); // eslint-disable-line global-require const modulesAndVersions = moduleNames.map(moduleName => { const moduleVersion = pkg.dependencies[moduleName]; diff --git a/lib/getConfig.js b/lib/getConfig.js index 0a1396e..46fb8e3 100644 --- a/lib/getConfig.js +++ b/lib/getConfig.js @@ -1,4 +1,3 @@ -'use strict'; const path = require('path'); const clone = require('clone'); @@ -25,9 +24,9 @@ module.exports = function getConfig(projectPath, project, func) { if (config.configPath) { try { const configPath = path.join(projectPath, config.configPath); - config.webpackConfig = clone(require(configPath)); + config.webpackConfig = clone(require(configPath)); // eslint-disable-line global-require } catch (e) { - console.log(e); + console.log(e); // eslint-disable-line no-console } } diff --git a/lib/getExternalsFromStats.js b/lib/getExternalsFromStats.js index 97bb89e..6ce9e53 100644 --- a/lib/getExternalsFromStats.js +++ b/lib/getExternalsFromStats.js @@ -1,4 +1,3 @@ -'use strict'; const natives = process.binding('natives'); module.exports = function getExternalsFromStats(stats) { diff --git a/package.json b/package.json index 93864a4..114b14f 100644 --- a/package.json +++ b/package.json @@ -27,22 +27,48 @@ "serverless.com" ], "main": "index.js", + "files": [ + "index.js", + "lib", + "1.0/index.js" + ], "bin": {}, "scripts": { + "add-artifacts": "git add 1.0/index.js", + "build": "babel -d 1.0 1.0/src/", + "lint": "eslint --quiet --cache .", + "prepublish": "npm run build", "test": "NODE_PATH=. mocha \"test/**/*.test.js\"" }, "peerDependencies": { - "webpack": "^1.12.14" + "webpack": "^1.13.1" }, + "pre-commit": [ + "lint", + "test", + "build", + "add-artifacts" + ], "devDependencies": { + "babel-cli": "^6.11.4", + "babel-eslint": "^6.1.2", + "babel-plugin-transform-runtime": "^6.12.0", + "babel-preset-es2015": "^6.13.2", + "babel-preset-stage-0": "^6.5.0", "chai": "^3.4.1", + "eslint": "^2.13.1", + "eslint-config-airbnb": "^9.0.1", + "eslint-plugin-import": "^1.12.0", + "eslint-plugin-jsx-a11y": "^2.0.1", + "eslint-plugin-react": "^5.2.2", "mocha": "^2.3.4", - "eslint": "^1.10.3", - "eslint-config-airbnb": "^5.0.0" + "pre-commit": "^1.1.3" }, "dependencies": { "bluebird": "^3.1.1", "clone": "^1.0.2", - "fs-extra": "^0.26.7" + "fs-extra": "^0.26.7", + "lodash": "^4.14.1", + "node-zip": "^1.1.1" } } diff --git a/test/getConfig.test.js b/test/getConfig.test.js index 3d8cb09..a20088d 100644 --- a/test/getConfig.test.js +++ b/test/getConfig.test.js @@ -92,7 +92,7 @@ describe.only('getConfig', () => { }; const func = {}; const result = getConfig(projectPath, project, func); - const webpackConfig = require('./data/webpack.conf.js'); + const webpackConfig = require('./data/webpack.conf.js'); // eslint-disable-line global-require assert.notStrictEqual(result.webpackConfig, webpackConfig); assert.deepEqual(result, { diff --git a/test/getExternalsFromStats.test.js b/test/getExternalsFromStats.test.js index 058f9f3..b680824 100644 --- a/test/getExternalsFromStats.test.js +++ b/test/getExternalsFromStats.test.js @@ -4,7 +4,9 @@ const assert = require('chai').assert; function getStatsMock() { return { toJson() { - return { modules: require('./data/stats.json').slice(0) }; + return { + modules: require('./data/stats.json').slice(0) // eslint-disable-line global-require + }; } }; }