Skip to content

Commit

Permalink
Merge pull request #18 from digitalmaas/feature/improvements
Browse files Browse the repository at this point in the history
Feature/improvements
  • Loading branch information
nolde authored Mar 23, 2020
2 parents 24f58c6 + b61797e commit 8465e10
Show file tree
Hide file tree
Showing 7 changed files with 965 additions and 1,371 deletions.
70 changes: 56 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ Serverless Browserifier Plugin

> A [Serverless](https://serverless.com) v1 plugin that uses [`browserify`][browserify-url] to bundle your Node.js Lambda functions.
1. [Motivation](#motivation)
1. [Installation](#installation)
1. [Basic Setup](#basic-setup)
1. [Advanced Configuration](#advanced-configuration)
1. [FAQ](#faq)
1. [Useful Information](#useful-information)
1. [License](#license)


Motivation
----------

Expand Down Expand Up @@ -41,6 +50,9 @@ From your target serverless project, run:
npm install serverless-plugin-browserifier --save-dev
```

Basic Setup
-----------

Add the plugin to your `serverless.yml`:

```yaml
Expand All @@ -52,11 +64,13 @@ package:
The `package.individually` setting must be set -- either on global or function level -- to allow minimal bundle size based on each lambda's entrypoint.

You're all set! Use your normal serverless commands to package and deploy.

Configuration
-------------

For most use cases you should **NOT** need to do any configuration. You can, however, introduce custom configuration.
Advanced Configuration
----------------------

For most use cases you should **NOT** need to do any extra configuration. That said, the ability is present if you need it.

The base config for browserify is read from the `custom.browserify` section of `serverless.yml`. All [browserify options][browserify-options] are supported (most are auto configured by this plugin). This plugin adds one special option `disable` which if `true` will bypass this plugin.

Expand Down Expand Up @@ -99,22 +113,21 @@ functions:
```


Usage
-----
### Debugging

When this plugin is enabled, and `package.individually` is `true`, running `serverless deploy` and `serverless deploy -f <funcName>` will automatically browserify your Node.js lambda code.

If you want to see more information about the process, simply set `SLS_DEBUG=*`. Example:
If you want to see more information about the process, simply set envvar `SLS_DEBUG=*` for full serverless debug output, or `SLS_BROWSERIFIER_DEBUG=*` for plugin only debug messages. Example:

```
$ export SLS_DEBUG=*
$ sls deploy function -v -f usersGet
```

You can also verify your bundles by simply using `sls package`, which bundles everything up but does not deploy.
You may also verify your bundles by simply using `sls package`, which bundles everything up but does not deploy.


Using browserify plugins/transforms
-----------------------------------
### Using browserify plugins/transforms

If you want to use browserify plugins, you can easily do that by using the global browserify options. As the plugin merely passes that up to browserify, as if it is calling the main [`browserify`][browserify-options] function, you can use it to add any transformations you want.

Expand Down Expand Up @@ -146,10 +159,9 @@ custom:
For an in-depth example, please check [this issue](https://github.com/digitalmaas/serverless-plugin-browserifier/issues/8).


Best practices
--------------
### Best practices

__If using it with AWS, use discrete SDK clients!__
#### If using it with AWS, use discrete SDK clients!

The official [aws-sdk-js][aws-sdk] officially [supports browserify][aws-sdk-support]. That allows us to further reduce the size of our bundles (and Lambda memory usage and speed) by loading only what is strictly needed.

Expand All @@ -162,7 +174,7 @@ const S3 = require('aws-sdk/clients/s3')
const s3 = new S3()
```

__Ignore AWS SDK!__
#### Ignore AWS SDK!

Although you can use discrete clients (see item above), AWS Lambda service always bundles up the latest SDK version in its Lambda container image. That means that, even if you don't add AWS SDK to your bundle, it will still be available in runtime.

Expand All @@ -176,6 +188,36 @@ custom:
- aws-sdk/clients/s3
```

To help you out, here's a script you can use to hide `aws-sdk` and all its clients from browserify. You can use it in your custom config for the plugin in _serverless.yml_:

```yml
# serverless.yml
custom:
browserify: browserify: ${file(./custom.browserify.js)}
```

```js
// custom.browserify.js
//
const fs = require('fs')
const path = require('path')
module.exports = function browserifyOptions () {
return {
// any other valid browserify configuration...
noParse: ['/**/*.json'],
exclude: ['aws-sdk', ...getAllAwsSdkClients()]
}
}
function getAllAwsSdkClients () {
return fs
.readdirSync('./node_modules/aws-sdk/clients', { withFileTypes: true })
.filter(file => file.isFile() && path.extname(file.name) === '.js')
.map(file => `aws-sdk/clients/${path.basename(file.name, '.js')}`)
}
```

FAQ
---
Expand All @@ -193,7 +235,7 @@ __Avoid mixing this plugin with other plugins that modify serverless' packaging
This plugin _hijacks_ the normal serverless packaging process, so it will probably conflict with other plugins that use similar mechanisms.


Useful information
Useful Information
------------------

- [List of browserify's transforms][useful-transforms-list]
Expand Down
9 changes: 5 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class BrowserifierPlugin {
this.S = serverless
this._b = {
options,
debugOn: Boolean(process.env.SLS_DEBUG) || Boolean(process.env.SLS_BROWSERIFIER_DEBUG),
isDisabled: get(this.S, 'service.custom.browserify.disable', false),
servicePath: path.join(this.S.config.servicePath || os.tmpdir(), '.serverless'),
runtimeIsNode: get(this.S, 'service.provider.runtime', '').indexOf('nodejs') !== -1,
Expand All @@ -39,13 +40,13 @@ class BrowserifierPlugin {
.then(() => {
const fns = this._getAllFunctions()
this.S.cli.log(`Browserifier: Preparing ${fns.length} function(s)...`)
return Promise.all(fns.map(name => this._bootstrap(name).reflect()))
return Promise.map(fns, name => this._bootstrap(name).reflect())
})
.then(results =>
results
.then(results => {
return results
.filter(inspection => inspection.isRejected())
.forEach(inspection => this._handleSkip(inspection.reason()))
)
})
.catch(this._handleSkip)
.tapCatch(this._warnFailure)
}
Expand Down
102 changes: 55 additions & 47 deletions lib/bundle.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
'use strict'

const Promise = require('bluebird')
const browserify = require('browserify')

const archiver = require('archiver')
const browserify = require('browserify')
const filesize = require('filesize')
const globby = require('globby')
const path = require('path')
const fs = require('fs-extra')
const semver = require('semver')

const fs = require('./fs')

/// //////////////////////// exports && main functions

Expand All @@ -22,14 +25,15 @@ function bundle (functionName) {
}
return Promise.bind(this)
.return(data)
.then(clean)
.then(prepareIncludes)
.then(runBrowserify)
.then(zip)
.then(zipIt)
.then(clean)
}

function bootstrap (functionName) {
if (process.env.SLS_DEBUG) {
if (this._b.debugOn) {
this.S.cli.log(`Browserifier: Preparing "${functionName}"...`)
}
return Promise.bind(this)
Expand All @@ -56,29 +60,34 @@ function prepareInitialData (functionName) {
}

function cacheConfig (data) {
return (this._b.functionConfigCache[data.functionName] = data)
if (data) {
this._b.functionConfigCache[data.functionName] = data
}
return data
}

function prepareIncludes (data) {
fs.emptyDirSync(data.outputFolder)
const includeFiles = globby.sync(data.functionBrowserifyConfig.include, {
cwd: this.S.config.servicePath,
dot: true,
silent: true,
follow: true
})
if (process.env.SLS_DEBUG && includeFiles && includeFiles.length) {
this.S.cli.log('Browserifier: Copying includes: ' + includeFiles)
if (includeFiles && includeFiles.length) {
if (this._b.debugOn) {
this.S.cli.log('Browserifier: Copying includes: ' + includeFiles)
}
const copyFile = file => {
fs.copySync(path.join(this.S.config.servicePath, file), path.join(data.outputFolder, file))
}
return Promise.each(includeFiles, copyFile).return(data)
}
includeFiles.forEach(file => {
fs.copySync(path.join(this.S.config.servicePath, file), path.join(data.outputFolder, file))
})
return data
}

function runBrowserify (data) {
if (process.env.SLS_DEBUG) {
this.S.cli.log(`Browserifier: Writing browserified bundle to ${data.outputFolder}`)
if (this._b.debugOn) {
this.S.cli.log(`Browserifier: Browserifying ${data.functionName}...`)
}
const cfg = data.functionBrowserifyConfig
const b = browserify(cfg)
Expand All @@ -87,27 +96,36 @@ function runBrowserify (data) {
cfg.external.forEach(file => b.external(file))
return Promise.fromCallback(cb => b.bundle(cb))
.then(bundleBuffer => {
if (this._b.debugOn) {
this.S.cli.log(`Browserifier: Writing browserified bundle to ${data.outputFolder}...`)
}
let handlerPath = data.functionObject.handler.split('.')[0] + '.js'
handlerPath = path.join(data.outputFolder, handlerPath)
fs.mkdirsSync(path.dirname(handlerPath), '0777') // handler may be in a subdir
this.S.utils.writeFileDir(handlerPath)
return Promise.fromCallback(cb => fs.writeFile(handlerPath, bundleBuffer, cb))
})
.tap(() => {
if (this._b.debugOn) {
this.S.cli.log(`Browserifier: Browserified output dumped to ${data.outputFolder}...`)
}
})
.return(data)
}

function zip (data) {
const outputFile = data.functionObject.artifact || data.functionObject.package.artifact
if (process.env.SLS_DEBUG) {
this.S.cli.log(`Browserifier: Zipping ${data.outputFolder} to ${outputFile}`)
function zipIt (data) {
if (this._b.debugOn) {
this.S.cli.log(`Browserifier: Zipping ${data.outputFolder} to ${data.outputBundle}...`)
}
const handleStream = (resolve, reject) => {
const output = fs.createWriteStream(outputFile)
const archive = archiver.create('zip')
output.on('close', () => resolve(archive.pointer()))
archive.on('error', err => reject(err))
archive.pipe(output)
archive.directory(data.outputFolder, '')
archive.finalize()
const output = fs.getNewFileStream(data.outputBundle)
const zip = archiver.create('zip', { zlib: { level: 9 } })
output.on('close', () => resolve(zip.pointer()))
zip.on('error', err => reject(err))
output.on('open', () => {
zip.pipe(output)
zip.directory(data.outputFolder, '')
zip.finalize()
})
}
return new Promise(handleStream).then(sizeInBytes => {
this.S.cli.log(`Browserifier: Created ${data.functionName}.zip (${filesize(sizeInBytes)})...`)
Expand All @@ -116,30 +134,20 @@ function zip (data) {
}

function clean (data) {
fs.removeSync(data.outputFolder)
if (fs.existsSync(data.workaroundFilePath)) {
fs.removeSync(data.workaroundFilePath)
if (fs.existsSync(data.outputFolder)) {
return fs.removeSync(data.outputFolder)
}
delete this._b.functionConfigCache[data.functionName]
return data
}

function fixServerlessConfig (data) {
const wafp = path.join(this._b.servicePath, 'fool-serverless.txt')
data.workaroundFilePath = path.relative(this.S.config.servicePath, wafp)
return fs
.ensureFile(data.workaroundFilePath)
.then(() => {
return Promise.fromCallback(cb => {
return fs.writeFile(data.workaroundFilePath, 'fool packaging step', cb)
})
})
.then(() => {
data.functionObject.package = {
individually: true,
exclude: ['**/*'],
include: [data.workaroundFilePath],
artifact: data.outputBundle
}
return data
})
if (semver.lt(this.S.getVersion(), '1.18.0')) {
data.functionObject.artifact = data.outputBundle
data.functionObject.package = Object.assign({}, data.functionObject.package, { disable: true })
} else {
data.functionObject.package = {
artifact: data.outputBundle
}
}
return data
}
14 changes: 7 additions & 7 deletions lib/configure.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module.exports = {
* Compute the base configuration
*/
_computeGlobalConfig () {
if (process.env.SLS_DEBUG) {
if (this._b.debugOn) {
this.S.cli.log('Browserifier: Computing global config...')
}
const globalCustom = get(this.S, 'service.custom.browserify', {})
Expand All @@ -28,7 +28,7 @@ module.exports = {
this._b.globalBrowserifyConfig.include = this.S.service.package.include
}
}
if (process.env.SLS_DEBUG) {
if (this._b.debugOn) {
const computed = JSON.stringify(this._b.globalBrowserifyConfig)
this.S.cli.log('Browserifier: Computed globalBrowserifyConfig: ' + computed)
}
Expand Down Expand Up @@ -58,15 +58,15 @@ module.exports = {
standalone: 'lambda',
ignoreMissing: true, // Do not fail on missing optional dependencies
detectGlobals: true, // We don't care if its slower, we want more mods to work
debug: Boolean(process.env.SLS_DEBUG)
debug: this._b.debugOn
}
if (semver.gte(version, '16.1.0')) {
if (process.env.SLS_DEBUG) {
if (this._b.debugOn) {
this.S.cli.log('Browserifier: Using browserify "node" option...')
}
config.node = true
} else {
if (process.env.SLS_DEBUG) {
if (this._b.debugOn) {
this.S.cli.log('Browserifier: Using browserify "legacy" options...')
}
Object.assign(config, {
Expand Down Expand Up @@ -110,7 +110,7 @@ module.exports = {
this._b.globalBrowserifyConfig,
functionObject.browserify || {}
)
if (process.env.SLS_DEBUG) {
if (this._b.debugOn) {
const computed = JSON.stringify(functionObject)
this.S.cli.log(`Browserifier: functionObject for ${functionName}: ${computed}`)
}
Expand All @@ -134,7 +134,7 @@ module.exports = {
)
}
}
if (process.env.SLS_DEBUG) {
if (this._b.debugOn) {
const computed = JSON.stringify(functionBrowserifyConfig)
this.S.cli.log('Browserifier: Computed function BrowserifierConfig: ' + computed)
}
Expand Down
Loading

0 comments on commit 8465e10

Please sign in to comment.