Skip to content

Commit

Permalink
feat: bundle functions with rollup
Browse files Browse the repository at this point in the history
  • Loading branch information
Luciano Fantone committed Jan 8, 2019
0 parents commit 00ac34c
Show file tree
Hide file tree
Showing 11 changed files with 7,450 additions and 0 deletions.
16 changes: 16 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org

root = true

[*]
indent_style = spaces
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
18 changes: 18 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": [
"standard",
"plugin:prettier/recommended"
],
"parserOptions": {
"sourceType": "script"
},
"env": {
"node": true,
"jest": true
},
"rules": {
"semi": [2, "always"],
"no-extra-semi": 2,
"strict": ["error", "global"]
}
}
63 changes: 63 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
*.DS_Store

# Serverless directories
.serverless

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Typescript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
4 changes: 4 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"printWidth": 100
}
Empty file added README.md
Empty file.
216 changes: 216 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
'use strict';
const { rollup } = require('rollup');
const { F, has, ifElse, is, path, pathOr, propEq } = require('ramda');
const { arrayOutputSchema, inputSchema, outputSchema } = require('./src/schemas');
const fse = require('fs-extra');
const Promise = require('bluebird');

/**
* Check if given value is an instance of array
* @func
* @param {*} value to check
* @returns boolean
*/
const isArray = is(Array);
/**
* Check if the `verbose` options equals true.
* @func
* @param {object} value to check the property verbose
* @returns boolean
*/
const isVerbose = propEq('verbose', true);
/**
* Check if given object has the property `watch`
* @func
* @param {object} value to check property `watch`
* @returns boolean
*/
const hasWatch = has('watch');
/**
* This method will a function that will we use to validate the config schema.
* @func
* @param {array|object} value to check if is an array
* @returns function to validate schema
*/
const validateOutput = ifElse(isArray, arrayOutputSchema.validateSync, outputSchema.validateSync);
/**
* Method to return a logger function that will only be used in case that the verbose options is true
* @param {Serverless} serverless instance of serverless
* @param {Serverless.options} options serverless option object
* @returns function to log on console
*/
const createLogger = (serverless, options) =>
ifElse(isVerbose, () => path(['cli', 'log'], serverless), () => F)(options);

/**
*
*
* @class ServerlessRollupPlugin
*/
class ServerlessRollupPlugin {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
this.commands = {
rollup: {
usage: 'Bundle your lambda function with rollup',
lifecycleEvents: ['compile'],
options: {
config: {
usage:
'Specify the rollup config you want to use' +
'(e.g. "--config \'rollup.config.js\'" or "-c \'rollup.config.js\'")',
required: true,
shortcut: 'c'
}
},
commands: {
compile: {
type: 'entrypoint',
lifecycleEvents: ['bundle']
},
prepare: {
type: 'entrypoint',
lifecycleEvents: ['validate', 'serverless']
},
clean: {
type: 'entrypoint',
lifecycleEvents: ['delete']
}
}
}
};

this.hooks = {
// Serverless deploy
'before:package:createDeploymentArtifacts': () =>
Promise.bind(this)
.then(() => this.serverless.pluginManager.spawn('rollup:prepare'))
.then(() => this.serverless.pluginManager.spawn('rollup:compile')),
'after:package:createDeploymentArtifacts': () =>
Promise.bind(this).then(() => this.serverless.pluginManager.spawn('rollup:clean')),

// Serverless deploy function
'before:package:function:package': () =>
Promise.bind(this)
.then(() => this.serverless.pluginManager.spawn('rollup:prepare'))
.then(() => this.serverless.pluginManager.spawn('rollup:compile')),
'after:package:function:package': () =>
Promise.bind(this).then(() => this.serverless.pluginManager.spawn('rollup:clean')),

// Serverless rollup
'before:rollup:compile': this.validate.bind(this),
'rollup:compile': this.bundle.bind(this),
'rollup:clean': this.clean.bind(this),

// Internal events
'rollup:prepare:validate': this.validate.bind(this),
'rollup:prepare:serverless': this.prepare.bind(this),
'rollup:compile:bundle': this.bundle.bind(this),
'rollup:clean:delete': this.clean.bind(this)
};

this.log = createLogger(this.serverless, this.options).bind(this.serverless.cli);
}

getConfigPath() {
return `${this.serverless.config.servicePath}/${pathOr(
this.options.config,
['service', 'custom', 'rollup', 'config'],
this.serverless
)}`;
}

getOutputFolder() {
const { output } = this.config;
// FIXME(lf): this will only work on unix envs.
return output.dir || output.file.split('/')[0];
}

async bundle() {
this.log('Rollup: Config file is valid, about to bundle lambda function');
try {
const bundle = await rollup(this.config);
await bundle.write(this.config.output);

this.log('Rollup: Bundle created successfully!');
} catch (error) {
this.log('Rollup: There was an error while bundling the service with rollup.');
throw error;
}
}

clean() {
const folderPath = this.getOutputFolder();
this.log(`Rollup: Removing temporary folder ${folderPath}`);

if (this.serverless.utils.dirExistsSync(folderPath)) {
fse.removeSync(folderPath);
}
}

prepare() {
// Grab all functions data and mapped their package setting use the rollup output if in the serverless config is set to package individually,
// otherwise, set the global package setting.
const functionNames = this.serverless.service.getAllFunctions();
const functions = {};
const output = this.getOutputFolder();

// Rollup will bundle all in a single file, so individual package should be override to false and include only the output folder;
const originalPackage = this.serverless.service.package;
const include = originalPackage.include || [];
// Add Rollup output folder so serverles include it in the package.
this.log('Rollup: Setting package options');
include.push(`${this.getOutputFolder()}/**`);

// Modify functions handler to use the rollup output folder
this.log('Rollup: Prepare functions handler location');

functionNames.forEach(name => {
const func = this.serverless.service.getFunction(name);
const handler = `${output}/${func.handler}`;
this.log(`Rollup: Preparing ${name} function, setting handler to ${handler}`);
functions[name] = { ...func, handler };
});

this.log(`Rollup: Overriding service options`);
this.serverless.service.update({
package: {
...originalPackage,
individually: false,
exclude: ['**/*'],
include
},
functions
});
}

validate() {
const path = this.getConfigPath();
this.log(`Rollup: Starting rollup plugin with config located at ${path}`);

const config = require(path);
if (isArray(config)) {
this.log(
'Rollup: This plugin does not support multiple inputs, please check the rollup config file'
);
throw new Error('Invalid rollup config');
}

if (hasWatch(config)) {
this.log(
'Rollup: This plugin does not support watch mode, please check the rollup config file'
);
throw new Error('Watch property is not supported');
}

if (inputSchema.validateSync(config) && validateOutput(config.output)) {
this.config = config;
} else {
this.log('Given config file is not valid, please check the rollup config file');
throw new Error('Rollup config is not valid');
}
}
}

module.exports = ServerlessRollupPlugin;
28 changes: 28 additions & 0 deletions index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';
const Plugin = require('./index');

const mockServerless = {
cli: {
log: jest.fn()
}
};
const mockOptions = {
config: 'rollup.config.test.js'
};

test('should create a new plugin instance', () => {
const plugin = new Plugin(mockServerless, mockOptions);

expect(plugin).toHaveProperty('serverless', mockServerless);
expect(plugin).toHaveProperty('options', mockOptions);
expect(plugin).toHaveProperty('commands');
expect(plugin).toHaveProperty('hooks');
});

test('should set input and output options', () => {
const plugin = new Plugin(mockServerless, mockOptions);

plugin.beforeRollupStart();
expect(plugin).toHaveProperty('inputOptions');
expect(plugin).toHaveProperty('outputOptions');
});
Loading

0 comments on commit 00ac34c

Please sign in to comment.