diff --git a/README.md b/README.md index 8dfdca2..1279761 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ [![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com) [![CircleCI](https://circleci.com/gh/elastic-coders/serverless-webpack.svg?style=shield)](https://circleci.com/gh/elastic-coders/serverless-webpack) -A Serverless v1.0 plugin to build your lambda functions with [Webpack](https://webpack.github.io). +A Serverless v1.0 plugin to build your lambda functions with [Webpack](https://webpack.github.io). -This plugin is for you if you want to use the latest Javascript version with [Babel](https://babeljs.io/); +This plugin is for you if you want to use the latest Javascript version with [Babel](https://babeljs.io/); use custom [resource loaders](https://webpack.github.io/docs/loaders.html); try your lambda functions locally and much more! @@ -67,26 +67,46 @@ module.exports = { By default, the plugin will try to bundle all dependencies. However, you don't want to include all modules in some cases such as selectively import, excluding -builtin package (aws-sdk) and handling webpack-incompatible modules. In this case, -you add all the modules, you want to exclude from bundled files, into `externals` field -of your `webpack.config.js` and add those, you want to include in final distribution, -into `serverless.yml`: +builtin package (ie: `aws-sdk`) and handling webpack-incompatible modules. -```javascript +In this case you might add external modules in +[Webpack `externals` configuration](https://webpack.github.io/docs/configuration.html#externals). +Those modules can be included in the Serverless bundle with the `webpackIncludeModules` +option in `serverless.yml`: + +```js // webpack.config.js -{ - externals: ["module1", "module2"] // modules to be excluded from bundled file +var nodeExternals = require('webpack-node-externals') + +modules.export = { + // we use webpack-node-externals to excludes all node deps. + // You can manually set the externals too. + externals: [nodeExternals()], } ``` +```yaml +# serverless.yml +custom: + webpackIncludeModules: true # enable auto-packing of external modules +``` + +All modules stated in `externals` will be excluded from bundled files. If an excluded module +is stated as `dependencies` in `package.json`, it will be packed into the Serverless +artifact under the `node_modules` directory. + +By default, the plugin will use the `package.json` file in working directory, If you want to +use a different package conf, set `packagePath` to your custom package.json. eg: + ```yaml # serverless.yml custom: webpackIncludeModules: - - module1 # modules to be included in distribution + packagePath: '../package.json' # relative path to custom package.json file. ``` +> Note that only relative path is supported at the moment. -You can find an example setup in the [`examples`](./examples) folder. +You can find an example setups in the [`examples`](./examples) folder. ## Usage @@ -112,7 +132,7 @@ Options are: - `--port` or `-p` (optional) The local server port. Defaults to `8000` -The `serve` command will automatically look for the local `serverless.yml` and serve +The `serve` command will automatically look for the local `serverless.yml` and serve all the `http` events. For example this configuration will generate a GET enpoint: ```yaml diff --git a/examples/include-external-npm-packages/package.json b/examples/include-external-npm-packages/package.json index b13ee30..8601b2f 100644 --- a/examples/include-external-npm-packages/package.json +++ b/examples/include-external-npm-packages/package.json @@ -10,10 +10,11 @@ "license": "MIT", "devDependencies": { "serverless-webpack": "^1.0.0-beta.2", - "webpack": "^1.13.1" + "webpack": "^1.13.1", + "webpack-node-externals": "^1.3.3", + "aws-sdk": "^2.5.3" // packages in devDependencies won't be included in distribution }, - "dependencies": { - "aws-sdk": "^2.5.3", + "dependencies": { // packages in dependencies will be included in distribution if required "fbgraph": "^1.3.0" } } diff --git a/examples/include-external-npm-packages/serverless.yml b/examples/include-external-npm-packages/serverless.yml index d27dd6a..5cfa396 100644 --- a/examples/include-external-npm-packages/serverless.yml +++ b/examples/include-external-npm-packages/serverless.yml @@ -9,8 +9,7 @@ provider: runtime: nodejs4.3 custom: - webpackIncludeModules: # modules to be included in distribution - - fbgraph + webpackIncludeModules: true # enable auto including modules functions: first: diff --git a/examples/include-external-npm-packages/webpack.config.js b/examples/include-external-npm-packages/webpack.config.js index 846684e..02f1fed 100644 --- a/examples/include-external-npm-packages/webpack.config.js +++ b/examples/include-external-npm-packages/webpack.config.js @@ -1,7 +1,7 @@ -var path = require('path'); +var nodeExternals = require('webpack-node-externals'); module.exports = { entry: './handler.js', target: 'node', - externals: ["fbgraph", "aws-sdk"] // modules to be excluded from bundled file + externals: [nodeExternals()] // exclude external modules }; diff --git a/index.js b/index.js index 73160b6..bdb70fd 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,7 @@ const compile = require('./lib/compile'); const cleanup = require('./lib/cleanup'); const run = require('./lib/run'); const serve = require('./lib/serve'); -const packExternalModules = require('./lib/packExternalModules') +const packExternalModules = require('./lib/packExternalModules'); class ServerlessWebpack { constructor(serverless, options) { @@ -30,7 +30,6 @@ class ServerlessWebpack { lifecycleEvents: [ 'validate', 'compile', - 'packExternalModules', ], options: { out: { @@ -102,9 +101,7 @@ class ServerlessWebpack { .then(this.validate), 'webpack:compile': () => BbPromise.bind(this) - .then(this.compile), - - 'webpack:packExternalModules': () => BbPromise.bind(this) + .then(this.compile) .then(this.packExternalModules), 'webpack:invoke:invoke': () => BbPromise.bind(this) diff --git a/lib/compile.js b/lib/compile.js index ccee537..259c125 100644 --- a/lib/compile.js +++ b/lib/compile.js @@ -12,6 +12,7 @@ module.exports = { return BbPromise .fromCallback(cb => compiler.run(cb)) .then(stats => { + this.serverless.cli.consoleLog(stats.toString({ colors: true, hash: false, diff --git a/lib/packExternalModules.js b/lib/packExternalModules.js index 0e8e9c0..3843051 100644 --- a/lib/packExternalModules.js +++ b/lib/packExternalModules.js @@ -5,38 +5,111 @@ const fs = require('fs'); const path = require('path'); const npm = require('npm-programmatic'); +function getProdModules(externalModules, packagePath) { + + const packageJson = require(path.join(process.cwd(), packagePath)); + + const prodModules = []; + + // only process the module stated in dependencies section + if (!packageJson.dependencies) { + return [] + } + + externalModules.forEach(module => { + + const moduleVersion = packageJson.dependencies[module]; + + if (moduleVersion) { + prodModules.push(`${module}@${moduleVersion}`); + } + }); + + return prodModules; +} + +function getExternalModuleName(module) { + + const path = /^external "(.*)"$/.exec(module.identifier())[1]; + + + const pathComponents = path.split('/'); + + const main = pathComponents[0]; + + // this is a package within a namespace + if (main.charAt(0) == '@') { + return `${main}/${pathComponents[1]}` + } + + return main +} + +function isExternalModule(module) { + return module.identifier().indexOf('external ') === 0; +} + +function getExternalModules(stats) { + + const externals = new Set(); + + stats.compilation.chunks.forEach(function(chunk) { + // Explore each module within the chunk (built inputs): + chunk.modules.forEach(function(module) { + // Explore each source file path that was included into the module: + if (isExternalModule(module)) { + externals.add(getExternalModuleName(module)); + } + }); + }); + + return Array.from(externals); +} + module.exports = { - packExternalModules() { + packExternalModules(stats) { const includes = ( - this.serverless.service.custom && - this.serverless.service.custom.webpackIncludeModules + this.serverless.service.custom && + this.serverless.service.custom.webpackIncludeModules ); return BbPromise.resolve().then(() => { - if (!includes || includes.length === 0) { - return; - } - - this.serverless.cli.log('Packing external modules: ' + includes.join(",")); - const tmpPackageJson = path.join(this.serverless.config.servicePath, 'package.json'); + if (!includes) { + return; + } + + const packagePath = includes.packagePath || './package.json'; + + const externalModules = getExternalModules(stats); + + // this plugin will only install modules stated in dependencies section of package.json + const prodModules = getProdModules(externalModules, packagePath); + + if (prodModules.length === 0) { + return; + } + + this.serverless.cli.log('Packing external modules: ' + prodModules.join(", ")); + + const tmpPackageJson = path.join(this.serverless.config.servicePath, 'package.json'); - // create a temp package.json in dist directory so that we can install the dependencies later. - fs.writeFileSync(tmpPackageJson, "{}"); - - return new BbPromise((resolve, reject) => { - npm.install(includes, { - cwd: this.serverless.config.servicePath, - save: false - }).then(() => { - fs.unlink(tmpPackageJson); - resolve() - }).catch(e => { - fs.unlink(tmpPackageJson); - reject(e); - }) + // create a temp package.json in dist directory so that we can install the dependencies later. + fs.writeFileSync(tmpPackageJson, "{}"); + + return new BbPromise((resolve, reject) => { + npm.install(prodModules, { + cwd: this.serverless.config.servicePath, + save: true + }).then(() => { + // fs.unlink(tmpPackageJson); + resolve() + }).catch(e => { + // fs.unlink(tmpPackageJson); + reject(e); }) - }) - }, + }) + }); + } };