Skip to content

Commit 00ac34c

Browse files
author
Luciano Fantone
committed
feat: bundle functions with rollup
0 parents  commit 00ac34c

File tree

11 files changed

+7450
-0
lines changed

11 files changed

+7450
-0
lines changed

.editorconfig

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# EditorConfig helps developers define and maintain consistent
2+
# coding styles between different editors and IDEs
3+
# editorconfig.org
4+
5+
root = true
6+
7+
[*]
8+
indent_style = spaces
9+
indent_size = 2
10+
end_of_line = lf
11+
charset = utf-8
12+
trim_trailing_whitespace = true
13+
insert_final_newline = true
14+
15+
[*.md]
16+
trim_trailing_whitespace = false

.eslintrc.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"extends": [
3+
"standard",
4+
"plugin:prettier/recommended"
5+
],
6+
"parserOptions": {
7+
"sourceType": "script"
8+
},
9+
"env": {
10+
"node": true,
11+
"jest": true
12+
},
13+
"rules": {
14+
"semi": [2, "always"],
15+
"no-extra-semi": 2,
16+
"strict": ["error", "global"]
17+
}
18+
}

.gitignore

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
*.DS_Store
2+
3+
# Serverless directories
4+
.serverless
5+
6+
# Logs
7+
logs
8+
*.log
9+
npm-debug.log*
10+
yarn-debug.log*
11+
yarn-error.log*
12+
13+
# Runtime data
14+
pids
15+
*.pid
16+
*.seed
17+
*.pid.lock
18+
19+
# Directory for instrumented libs generated by jscoverage/JSCover
20+
lib-cov
21+
22+
# Coverage directory used by tools like istanbul
23+
coverage
24+
25+
# nyc test coverage
26+
.nyc_output
27+
28+
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
29+
.grunt
30+
31+
# Bower dependency directory (https://bower.io/)
32+
bower_components
33+
34+
# node-waf configuration
35+
.lock-wscript
36+
37+
# Compiled binary addons (http://nodejs.org/api/addons.html)
38+
build/Release
39+
40+
# Dependency directories
41+
node_modules/
42+
jspm_packages/
43+
44+
# Typescript v1 declaration files
45+
typings/
46+
47+
# Optional npm cache directory
48+
.npm
49+
50+
# Optional eslint cache
51+
.eslintcache
52+
53+
# Optional REPL history
54+
.node_repl_history
55+
56+
# Output of 'npm pack'
57+
*.tgz
58+
59+
# Yarn Integrity file
60+
.yarn-integrity
61+
62+
# dotenv environment variables file
63+
.env

.prettierrc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"singleQuote": true,
3+
"printWidth": 100
4+
}

README.md

Whitespace-only changes.

index.js

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
'use strict';
2+
const { rollup } = require('rollup');
3+
const { F, has, ifElse, is, path, pathOr, propEq } = require('ramda');
4+
const { arrayOutputSchema, inputSchema, outputSchema } = require('./src/schemas');
5+
const fse = require('fs-extra');
6+
const Promise = require('bluebird');
7+
8+
/**
9+
* Check if given value is an instance of array
10+
* @func
11+
* @param {*} value to check
12+
* @returns boolean
13+
*/
14+
const isArray = is(Array);
15+
/**
16+
* Check if the `verbose` options equals true.
17+
* @func
18+
* @param {object} value to check the property verbose
19+
* @returns boolean
20+
*/
21+
const isVerbose = propEq('verbose', true);
22+
/**
23+
* Check if given object has the property `watch`
24+
* @func
25+
* @param {object} value to check property `watch`
26+
* @returns boolean
27+
*/
28+
const hasWatch = has('watch');
29+
/**
30+
* This method will a function that will we use to validate the config schema.
31+
* @func
32+
* @param {array|object} value to check if is an array
33+
* @returns function to validate schema
34+
*/
35+
const validateOutput = ifElse(isArray, arrayOutputSchema.validateSync, outputSchema.validateSync);
36+
/**
37+
* Method to return a logger function that will only be used in case that the verbose options is true
38+
* @param {Serverless} serverless instance of serverless
39+
* @param {Serverless.options} options serverless option object
40+
* @returns function to log on console
41+
*/
42+
const createLogger = (serverless, options) =>
43+
ifElse(isVerbose, () => path(['cli', 'log'], serverless), () => F)(options);
44+
45+
/**
46+
*
47+
*
48+
* @class ServerlessRollupPlugin
49+
*/
50+
class ServerlessRollupPlugin {
51+
constructor(serverless, options) {
52+
this.serverless = serverless;
53+
this.options = options;
54+
this.commands = {
55+
rollup: {
56+
usage: 'Bundle your lambda function with rollup',
57+
lifecycleEvents: ['compile'],
58+
options: {
59+
config: {
60+
usage:
61+
'Specify the rollup config you want to use' +
62+
'(e.g. "--config \'rollup.config.js\'" or "-c \'rollup.config.js\'")',
63+
required: true,
64+
shortcut: 'c'
65+
}
66+
},
67+
commands: {
68+
compile: {
69+
type: 'entrypoint',
70+
lifecycleEvents: ['bundle']
71+
},
72+
prepare: {
73+
type: 'entrypoint',
74+
lifecycleEvents: ['validate', 'serverless']
75+
},
76+
clean: {
77+
type: 'entrypoint',
78+
lifecycleEvents: ['delete']
79+
}
80+
}
81+
}
82+
};
83+
84+
this.hooks = {
85+
// Serverless deploy
86+
'before:package:createDeploymentArtifacts': () =>
87+
Promise.bind(this)
88+
.then(() => this.serverless.pluginManager.spawn('rollup:prepare'))
89+
.then(() => this.serverless.pluginManager.spawn('rollup:compile')),
90+
'after:package:createDeploymentArtifacts': () =>
91+
Promise.bind(this).then(() => this.serverless.pluginManager.spawn('rollup:clean')),
92+
93+
// Serverless deploy function
94+
'before:package:function:package': () =>
95+
Promise.bind(this)
96+
.then(() => this.serverless.pluginManager.spawn('rollup:prepare'))
97+
.then(() => this.serverless.pluginManager.spawn('rollup:compile')),
98+
'after:package:function:package': () =>
99+
Promise.bind(this).then(() => this.serverless.pluginManager.spawn('rollup:clean')),
100+
101+
// Serverless rollup
102+
'before:rollup:compile': this.validate.bind(this),
103+
'rollup:compile': this.bundle.bind(this),
104+
'rollup:clean': this.clean.bind(this),
105+
106+
// Internal events
107+
'rollup:prepare:validate': this.validate.bind(this),
108+
'rollup:prepare:serverless': this.prepare.bind(this),
109+
'rollup:compile:bundle': this.bundle.bind(this),
110+
'rollup:clean:delete': this.clean.bind(this)
111+
};
112+
113+
this.log = createLogger(this.serverless, this.options).bind(this.serverless.cli);
114+
}
115+
116+
getConfigPath() {
117+
return `${this.serverless.config.servicePath}/${pathOr(
118+
this.options.config,
119+
['service', 'custom', 'rollup', 'config'],
120+
this.serverless
121+
)}`;
122+
}
123+
124+
getOutputFolder() {
125+
const { output } = this.config;
126+
// FIXME(lf): this will only work on unix envs.
127+
return output.dir || output.file.split('/')[0];
128+
}
129+
130+
async bundle() {
131+
this.log('Rollup: Config file is valid, about to bundle lambda function');
132+
try {
133+
const bundle = await rollup(this.config);
134+
await bundle.write(this.config.output);
135+
136+
this.log('Rollup: Bundle created successfully!');
137+
} catch (error) {
138+
this.log('Rollup: There was an error while bundling the service with rollup.');
139+
throw error;
140+
}
141+
}
142+
143+
clean() {
144+
const folderPath = this.getOutputFolder();
145+
this.log(`Rollup: Removing temporary folder ${folderPath}`);
146+
147+
if (this.serverless.utils.dirExistsSync(folderPath)) {
148+
fse.removeSync(folderPath);
149+
}
150+
}
151+
152+
prepare() {
153+
// Grab all functions data and mapped their package setting use the rollup output if in the serverless config is set to package individually,
154+
// otherwise, set the global package setting.
155+
const functionNames = this.serverless.service.getAllFunctions();
156+
const functions = {};
157+
const output = this.getOutputFolder();
158+
159+
// Rollup will bundle all in a single file, so individual package should be override to false and include only the output folder;
160+
const originalPackage = this.serverless.service.package;
161+
const include = originalPackage.include || [];
162+
// Add Rollup output folder so serverles include it in the package.
163+
this.log('Rollup: Setting package options');
164+
include.push(`${this.getOutputFolder()}/**`);
165+
166+
// Modify functions handler to use the rollup output folder
167+
this.log('Rollup: Prepare functions handler location');
168+
169+
functionNames.forEach(name => {
170+
const func = this.serverless.service.getFunction(name);
171+
const handler = `${output}/${func.handler}`;
172+
this.log(`Rollup: Preparing ${name} function, setting handler to ${handler}`);
173+
functions[name] = { ...func, handler };
174+
});
175+
176+
this.log(`Rollup: Overriding service options`);
177+
this.serverless.service.update({
178+
package: {
179+
...originalPackage,
180+
individually: false,
181+
exclude: ['**/*'],
182+
include
183+
},
184+
functions
185+
});
186+
}
187+
188+
validate() {
189+
const path = this.getConfigPath();
190+
this.log(`Rollup: Starting rollup plugin with config located at ${path}`);
191+
192+
const config = require(path);
193+
if (isArray(config)) {
194+
this.log(
195+
'Rollup: This plugin does not support multiple inputs, please check the rollup config file'
196+
);
197+
throw new Error('Invalid rollup config');
198+
}
199+
200+
if (hasWatch(config)) {
201+
this.log(
202+
'Rollup: This plugin does not support watch mode, please check the rollup config file'
203+
);
204+
throw new Error('Watch property is not supported');
205+
}
206+
207+
if (inputSchema.validateSync(config) && validateOutput(config.output)) {
208+
this.config = config;
209+
} else {
210+
this.log('Given config file is not valid, please check the rollup config file');
211+
throw new Error('Rollup config is not valid');
212+
}
213+
}
214+
}
215+
216+
module.exports = ServerlessRollupPlugin;

index.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
const Plugin = require('./index');
3+
4+
const mockServerless = {
5+
cli: {
6+
log: jest.fn()
7+
}
8+
};
9+
const mockOptions = {
10+
config: 'rollup.config.test.js'
11+
};
12+
13+
test('should create a new plugin instance', () => {
14+
const plugin = new Plugin(mockServerless, mockOptions);
15+
16+
expect(plugin).toHaveProperty('serverless', mockServerless);
17+
expect(plugin).toHaveProperty('options', mockOptions);
18+
expect(plugin).toHaveProperty('commands');
19+
expect(plugin).toHaveProperty('hooks');
20+
});
21+
22+
test('should set input and output options', () => {
23+
const plugin = new Plugin(mockServerless, mockOptions);
24+
25+
plugin.beforeRollupStart();
26+
expect(plugin).toHaveProperty('inputOptions');
27+
expect(plugin).toHaveProperty('outputOptions');
28+
});

0 commit comments

Comments
 (0)