"
- - "Arn"
- StartingPosition: "TRIM_HORIZON"
-```
diff --git a/docs/02-providers/aws/events/README.md b/docs/02-providers/aws/events/README.md
index 7ec9b26b9..89fd5d4f7 100644
--- a/docs/02-providers/aws/events/README.md
+++ b/docs/02-providers/aws/events/README.md
@@ -10,5 +10,4 @@ layout: Doc
* [S3](./02-s3.md)
* [Schedule](./03-schedule.md)
* [SNS](./04-sns.md)
-* [Kinesis Streams](./05-kinesis-streams.md)
-* [DynamoDB Streams](./06-dynamodb-streams.md)
+* [DynamoDB / Kinesis Streams](./05-streams.md)
diff --git a/docs/02-providers/aws/examples/.eslintrc.js b/docs/02-providers/aws/examples/.eslintrc.js
new file mode 100644
index 000000000..4609ba030
--- /dev/null
+++ b/docs/02-providers/aws/examples/.eslintrc.js
@@ -0,0 +1,6 @@
+module.exports = {
+ "rules": {
+ "no-console": "off",
+ "import/no-unresolved": "off"
+ }
+};
diff --git a/docs/02-providers/aws/examples/README.md b/docs/02-providers/aws/examples/README.md
index 6aaec3606..0dbe50cc8 100644
--- a/docs/02-providers/aws/examples/README.md
+++ b/docs/02-providers/aws/examples/README.md
@@ -7,4 +7,5 @@ layout: Doc
* [hello-world](./hello-world)
* [using-external-libraries](./using-external-libraries)
-* [web-api](./web-api)
+* [cron](./cron)
+* [web-serving-html](./web-serving-html)
\ No newline at end of file
diff --git a/docs/02-providers/aws/examples/cron/README.md b/docs/02-providers/aws/examples/cron/README.md
new file mode 100644
index 000000000..ded6350b5
--- /dev/null
+++ b/docs/02-providers/aws/examples/cron/README.md
@@ -0,0 +1,14 @@
+
+
+# Schedule Cron
+
+Create a scheduled task with AWS Lambda and automate all the things!
+
+For more information on running cron with serverless check out the [Tutorial: Serverless Scheduled Tasks](https://parall.ax/blog/view/3202/tutorial-serverless-scheduled-tasks) by parallax.
+
+For more information on `schedule` serverless event check out [our docs](/docs/02-providers/aws/events/03-schedule.md).
diff --git a/docs/02-providers/aws/examples/cron/node/README.md b/docs/02-providers/aws/examples/cron/node/README.md
new file mode 100644
index 000000000..3b92da677
--- /dev/null
+++ b/docs/02-providers/aws/examples/cron/node/README.md
@@ -0,0 +1,18 @@
+
+
+# AWS Lambda Node Cron Function
+
+This is an example of creating a function that runs on a scheduled cron.
+
+To see your cron running tail your logs with:
+
+```bash
+serverless logs -function cron -tail
+```
+
+[Tutorial: Serverless Scheduled Tasks](https://parall.ax/blog/view/3202/tutorial-serverless-scheduled-tasks)
\ No newline at end of file
diff --git a/docs/02-providers/aws/examples/cron/node/handler.js b/docs/02-providers/aws/examples/cron/node/handler.js
new file mode 100644
index 000000000..5fb485788
--- /dev/null
+++ b/docs/02-providers/aws/examples/cron/node/handler.js
@@ -0,0 +1,6 @@
+'use strict';
+
+module.exports.run = () => {
+ const time = new Date();
+ console.log(`Your cron ran ${time}`);
+};
diff --git a/docs/02-providers/aws/examples/cron/node/serverless.yml b/docs/02-providers/aws/examples/cron/node/serverless.yml
new file mode 100644
index 000000000..c24b21466
--- /dev/null
+++ b/docs/02-providers/aws/examples/cron/node/serverless.yml
@@ -0,0 +1,11 @@
+service: cron-example
+
+provider:
+ name: aws
+ runtime: nodejs4.3
+
+functions:
+ cron:
+ handler: handler.run
+ events:
+ - schedule: rate(1 minute)
\ No newline at end of file
diff --git a/docs/02-providers/aws/examples/hello-world/node/README.md b/docs/02-providers/aws/examples/hello-world/node/README.md
index 5c4c1896a..b0c486a25 100644
--- a/docs/02-providers/aws/examples/hello-world/node/README.md
+++ b/docs/02-providers/aws/examples/hello-world/node/README.md
@@ -7,24 +7,23 @@ layout: Doc
# Hello World Node.js
-Make sure serverless is installed. [See installation guide](/docs/01-guide/01-installing-serverless.md)
+Make sure `serverless` is installed. [See installation guide](/docs/01-guide/01-installing-serverless.md)
## 1. Deploy
`serverless deploy` or `sls deploy`. `sls` is shorthand for the serverless CLI command
-## 2. Invoke the remote function
+## 2. Invoke deployed function
-`serverless invoke --function hello` or `serverless invoke -f hello`
+`serverless invoke --function helloWorld` or `serverless invoke -f helloWorld`
`-f` is shorthand for `--function`
-In your terminal window you should be the response from AWS Lambda
+In your terminal window you should see the response from AWS Lambda
```bash
{
- "message": "Hello World",
- "event": {}
+ "message": "Hello World"
}
```
diff --git a/docs/02-providers/aws/examples/hello-world/node/handler.js b/docs/02-providers/aws/examples/hello-world/node/handler.js
index 12a311f42..630518208 100644
--- a/docs/02-providers/aws/examples/hello-world/node/handler.js
+++ b/docs/02-providers/aws/examples/hello-world/node/handler.js
@@ -4,7 +4,6 @@
module.exports.helloWorldHandler = function (event, context, callback) {
const message = {
message: 'Hello World',
- event,
};
// callback will send message object back
callback(null, message);
diff --git a/docs/02-providers/aws/examples/hello-world/python/README.md b/docs/02-providers/aws/examples/hello-world/python/README.md
index dbd58fd38..7cad8a620 100644
--- a/docs/02-providers/aws/examples/hello-world/python/README.md
+++ b/docs/02-providers/aws/examples/hello-world/python/README.md
@@ -7,4 +7,24 @@ layout: Doc
# Hello World in Python
-[See installation guide](/docs/01-guide/01-installing-serverless.md)
+Make sure `serverless` is installed. [See installation guide](/docs/01-guide/01-installing-serverless.md)
+
+## 1. Deploy
+
+`serverless deploy` or `sls deploy`. `sls` is shorthand for the serverless CLI command
+
+## 2. Invoke deployed function
+
+`serverless invoke --function helloWorld` or `serverless invoke -f helloWorld`
+
+`-f` is shorthand for `--function`
+
+In your terminal window you should see the response from AWS Lambda
+
+```bash
+{
+ "message": "Hello World"
+}
+```
+
+Congrats you have just deployed and ran your hello world function!
diff --git a/docs/02-providers/aws/examples/hello-world/python/handler.py b/docs/02-providers/aws/examples/hello-world/python/handler.py
index e69de29bb..690e2d012 100644
--- a/docs/02-providers/aws/examples/hello-world/python/handler.py
+++ b/docs/02-providers/aws/examples/hello-world/python/handler.py
@@ -0,0 +1,6 @@
+def helloWorldHandler(event, context):
+ message = {
+ 'message': 'Hello World'
+ }
+
+ return message
\ No newline at end of file
diff --git a/docs/02-providers/aws/examples/hello-world/python/serverless.yml b/docs/02-providers/aws/examples/hello-world/python/serverless.yml
index e69de29bb..0b2f4cae5 100644
--- a/docs/02-providers/aws/examples/hello-world/python/serverless.yml
+++ b/docs/02-providers/aws/examples/hello-world/python/serverless.yml
@@ -0,0 +1,10 @@
+# Hello World for AWS Lambda
+service: hello-world # Service Name
+
+provider:
+ name: aws
+ runtime: python2.7
+
+functions:
+ helloWorld:
+ handler: handler.helloWorldHandler
diff --git a/docs/02-providers/aws/examples/using-external-libraries/node/README.md b/docs/02-providers/aws/examples/using-external-libraries/node/README.md
index 444011fac..fdb510168 100644
--- a/docs/02-providers/aws/examples/using-external-libraries/node/README.md
+++ b/docs/02-providers/aws/examples/using-external-libraries/node/README.md
@@ -1,13 +1,13 @@
-# Using External libraries in Node
+# Using external libraries in Node.js service
-Make sure serverless is installed. [See installation guide](/docs/01-guide/01-installing-serverless.md)
+Make sure `serverless` is installed. [See installation guide](/docs/01-guide/01-installing-serverless.md)
## 1. Install dependencies
@@ -15,22 +15,25 @@ For this example we are going to install the `faker` module from npm.
`npm install faker --save`
-## 2. Install the faker module in your `handler.js` file
+## 2. Use the faker module in your `handler.js` file
Inside of `handler.js` require your module.
`const faker = require('faker');`
-## 1. Deploy
+## 3. Deploy
-`serverless deploy` or `sls deploy`.
+`serverless deploy`
-`sls` is shorthand for the serverless CLI command
+## 4. Invoke
-Alternatively, you can run `npm run deploy` and deploy via NPM script defined in the `package.json` file
+`serverless invoke -f helloRandomName`
-## 2. Invoke
+In your terminal window you should see the response from AWS Lambda
-`serverless invoke --function helloRandomName` or `sls invoke -f helloRandomName`
+```bash
+{
+ "message": "Hello Floyd"
+}
+```
-`-f` is shorthand for `--function`
diff --git a/docs/02-providers/aws/examples/using-external-libraries/node/event.json b/docs/02-providers/aws/examples/using-external-libraries/node/event.json
deleted file mode 100644
index 2ac50a459..000000000
--- a/docs/02-providers/aws/examples/using-external-libraries/node/event.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "key3": "value3",
- "key2": "value2",
- "key1": "value1"
-}
diff --git a/docs/02-providers/aws/examples/using-external-libraries/node/handler.js b/docs/02-providers/aws/examples/using-external-libraries/node/handler.js
index 7d1309b25..bc9d601a3 100644
--- a/docs/02-providers/aws/examples/using-external-libraries/node/handler.js
+++ b/docs/02-providers/aws/examples/using-external-libraries/node/handler.js
@@ -1,13 +1,13 @@
-// 'use strict';
-// // Import faker module from node_modules
-// const faker = require('faker');
-//
-// // Your function handler
-// module.exports.helloRandomNameHandler = function (event, context, callback) {
-// const randomName = faker.name.firstName();
-// const message = {
-// message: 'Hello ' + randomName,
-// event: event
-// };
-// callback(null, message);
-// };
+'use strict';
+
+// Import faker module from node_modules
+const faker = require('faker');
+
+module.exports.helloRandomName = function (event, context, callback) {
+ const name = faker.name.firstName();
+ const message = {
+ message: `Hello ${name}`,
+ };
+
+ callback(null, message);
+};
diff --git a/docs/02-providers/aws/examples/using-external-libraries/node/package.json b/docs/02-providers/aws/examples/using-external-libraries/node/package.json
index 89667c7c2..a7cd0f3af 100644
--- a/docs/02-providers/aws/examples/using-external-libraries/node/package.json
+++ b/docs/02-providers/aws/examples/using-external-libraries/node/package.json
@@ -1,10 +1,7 @@
{
- "name": "hello-world",
- "description": "Serverless using external libraries example with node",
+ "name": "external-library",
+ "description": "Serverless using external libraries example with Node.js",
"private": true,
- "scripts": {
- "deploy": "serverless deploy"
- },
"dependencies": {
"faker": "^3.1.0"
}
diff --git a/docs/02-providers/aws/examples/using-external-libraries/node/serverless.yml b/docs/02-providers/aws/examples/using-external-libraries/node/serverless.yml
index ae8c48d3e..4bbb504b2 100644
--- a/docs/02-providers/aws/examples/using-external-libraries/node/serverless.yml
+++ b/docs/02-providers/aws/examples/using-external-libraries/node/serverless.yml
@@ -1,5 +1,5 @@
# Hello Random Name for AWS Lambda
-service: hello-random-name # Service Name
+service: external-lib # Service Name
provider:
name: aws
@@ -7,4 +7,4 @@ provider:
functions:
helloRandomName:
- handler: handler.helloRandomNameHandler
+ handler: handler.helloRandomName
diff --git a/docs/02-providers/aws/examples/web-api/README.md b/docs/02-providers/aws/examples/web-api/README.md
deleted file mode 100644
index 086655b23..000000000
--- a/docs/02-providers/aws/examples/web-api/README.md
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-# Creating a simple Web API in AWS Lambda
-
-todo
diff --git a/docs/02-providers/aws/examples/web-api/node/README.md b/docs/02-providers/aws/examples/web-api/node/README.md
deleted file mode 100644
index bf6928a3b..000000000
--- a/docs/02-providers/aws/examples/web-api/node/README.md
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-# Web API with AWS Lambda in Node.js
-
-This example demonstrates how to create a web api with AWS Gateway and Lambda.
-
-# Steps
-
-## 1. Configure your endpoint
-
-In your serverless.yml file, configure a function and http to the events with path and method.
-
-
-
-## 2. Deploy
-
-`serverless deploy` or `sls deploy`. `sls` is shorthand for the serverless CLI command.
-
-After you deploy your function. Serverless will setup and configure the AWS
-
-## 2. Invoke the remote function
-
-
-In your terminal window you should be the response from AWS Lambda
-
-```bash
-{
- "message": "Hello World",
- "event": {}
-}
-```
-
-Congrats you have just deployed and ran your hello world function!
diff --git a/docs/02-providers/aws/examples/web-api/node/handler.js b/docs/02-providers/aws/examples/web-api/node/handler.js
deleted file mode 100644
index efa2f7b41..000000000
--- a/docs/02-providers/aws/examples/web-api/node/handler.js
+++ /dev/null
@@ -1,11 +0,0 @@
-'use strict';
-
-// Your function handler
-module.exports.getHelloWorld = function (event, context, callback) {
- const message = {
- message: 'Is it me you`re looking for',
- event,
- };
- // callback will send message object back on Web API request
- callback(null, message);
-};
diff --git a/docs/02-providers/aws/examples/web-api/node/serverless.yml b/docs/02-providers/aws/examples/web-api/node/serverless.yml
deleted file mode 100644
index b357563d8..000000000
--- a/docs/02-providers/aws/examples/web-api/node/serverless.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-# web-api NodeJS example for AWS Lambda
-service: web-api
-
-provider:
- name: aws
- runtime: nodejs4.3
-
-functions:
- getHello:
- handler: handler.getHelloWorld
- events:
- - http:
- path: hello
- method: get
diff --git a/docs/02-providers/aws/examples/web-serving-html/README.md b/docs/02-providers/aws/examples/web-serving-html/README.md
new file mode 100644
index 000000000..aedd806ff
--- /dev/null
+++ b/docs/02-providers/aws/examples/web-serving-html/README.md
@@ -0,0 +1,16 @@
+
+
+# Serving HTML through API Gateway
+
+These examples illustrate how to hookup an API Gateway endpoint to a Lambda function to render HTML on a `GET` request.
+
+So instead of returning the default `json` from requests to an endpoint, you can display custom HTML.
+
+This is useful for dynamic webpages and landing pages for marketing activities.
+
+* [Javascript](./node)
diff --git a/docs/02-providers/aws/examples/web-serving-html/node/README.md b/docs/02-providers/aws/examples/web-serving-html/node/README.md
new file mode 100644
index 000000000..8bbfe1b53
--- /dev/null
+++ b/docs/02-providers/aws/examples/web-serving-html/node/README.md
@@ -0,0 +1,9 @@
+
+
+# Serving Static HTML with NodeJS + API Gateway
+
+This is an example of serving vanilla HTML/CSS/JS through API Gateway
\ No newline at end of file
diff --git a/docs/02-providers/aws/examples/web-serving-html/node/handler.js b/docs/02-providers/aws/examples/web-serving-html/node/handler.js
new file mode 100644
index 000000000..914016966
--- /dev/null
+++ b/docs/02-providers/aws/examples/web-serving-html/node/handler.js
@@ -0,0 +1,34 @@
+'use strict';
+
+// Your function handler
+module.exports.staticHtml = function (event, context, callback) {
+ let dynamicHtml;
+ /* check for GET params and use if available */
+ if (event.queryStringParameters && event.queryStringParameters.name) {
+ // yourendpoint.com/dev/landing-page?name=bob
+ dynamicHtml = `Hey ${event.queryStringParameters.name}
`;
+ } else {
+ dynamicHtml = '';
+ }
+
+ const html = `
+
+
+
+ Landing Page
+ ${dynamicHtml}
+
+ `;
+
+ const response = {
+ statusCode: 200,
+ headers: {
+ 'Content-Type': 'text/html',
+ },
+ body: html,
+ };
+ // callback will send HTML back
+ callback(null, response);
+};
diff --git a/docs/02-providers/aws/examples/web-serving-html/node/serverless.yml b/docs/02-providers/aws/examples/web-serving-html/node/serverless.yml
new file mode 100644
index 000000000..097ced5e3
--- /dev/null
+++ b/docs/02-providers/aws/examples/web-serving-html/node/serverless.yml
@@ -0,0 +1,15 @@
+# Serving HTML through API Gateway for AWS Lambda
+
+service: serve-html
+
+provider:
+ name: aws
+ runtime: nodejs4.3
+
+functions:
+ staticHtml:
+ handler: handler.staticHtml
+ events:
+ - http:
+ method: get
+ path: landing-page
diff --git a/docs/03-cli-reference/01-create.md b/docs/03-cli-reference/01-create.md
index aa5b2410c..855fe543b 100644
--- a/docs/03-cli-reference/01-create.md
+++ b/docs/03-cli-reference/01-create.md
@@ -14,7 +14,7 @@ serverless create --template aws-nodejs
```
## Options
-- `--template` or `-t` The name of your new service. **Required**.
+- `--template` or `-t` The name of one of the available templates. **Required**.
- `--path` or `-p` The path where the service should be created.
- `--name` or `-n` the name of the service in `serverless.yml`.
@@ -31,6 +31,7 @@ Most commonly used templates:
- aws-python
- aws-java-maven
- aws-java-gradle
+- aws-scala-sbt
## Examples
diff --git a/docs/03-cli-reference/02-install.md b/docs/03-cli-reference/02-install.md
new file mode 100644
index 000000000..5aa3e2eca
--- /dev/null
+++ b/docs/03-cli-reference/02-install.md
@@ -0,0 +1,32 @@
+
+
+# Install
+
+Installs a service from a GitHub URL in the current working directory.
+
+```
+serverless install --url https://github.com/some/service
+```
+
+## Options
+- `--url` or `-u` The services GitHub URL. **Required**.
+
+## Provided lifecycle events
+- `install:install`
+
+## Examples
+
+### Installing a service from a GitHub URL
+
+```
+serverless install --url https://github.com/johndoe/authentication
+```
+
+This example will download the .zip file of the `authentication` service from GitHub,
+create a new directory with the name `authentication` in the current working directory
+and unzips the files in this directory.
diff --git a/docs/03-cli-reference/02-deploy.md b/docs/03-cli-reference/03-deploy.md
similarity index 93%
rename from docs/03-cli-reference/02-deploy.md
rename to docs/03-cli-reference/03-deploy.md
index 6b9acfce0..fc3cc579c 100644
--- a/docs/03-cli-reference/02-deploy.md
+++ b/docs/03-cli-reference/03-deploy.md
@@ -19,7 +19,7 @@ serverless deploy [function]
- `--stage` or `-s` The stage in your service that you want to deploy to.
- `--region` or `-r` The region in that stage that you want to deploy to.
- `--noDeploy` or `-n` Skips the deployment steps and leaves artifacts in the `.serverless` directory
-- `--verbose` or `-v` Shows all stack events during deployment.
+- `--verbose` or `-v` Shows all stack events during deployment, and display any Stack Output.
## Examples
diff --git a/docs/03-cli-reference/03-invoke.md b/docs/03-cli-reference/04-invoke.md
similarity index 100%
rename from docs/03-cli-reference/03-invoke.md
rename to docs/03-cli-reference/04-invoke.md
diff --git a/docs/03-cli-reference/05-info.md b/docs/03-cli-reference/05-info.md
deleted file mode 100644
index e8e97af66..000000000
--- a/docs/03-cli-reference/05-info.md
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-# Info
-
-Displays information about the deployed service.
-
-```bash
-serverless info
-```
-
-## Options
-- `--stage` or `-s` The stage in your service you want to display information about.
-- `--region` or `-r` The region in your stage that you want to display information about.
-
-## Provided lifecycle events
-- `info:info`
-
-## Examples
-
-### AWS
-
-On AWS the info plugin uses the `Outputs` section of the CloudFormation stack and the AWS SDK to gather the necessary information.
-See the example below for an example output.
-
-**Example:**
-
-```
-$ serverless info
-
-Service Information
-service: my-serverless-service
-stage: dev
-region: us-east-1
-api keys:
- myKey: some123valid456api789key1011for1213api1415gateway
-endpoints:
- GET - https://dxaynpuzd4.execute-api.us-east-1.amazonaws.com/dev/users
-functions:
- my-serverless-service-dev-hello: arn:aws:lambda:us-east-1:377024778620:function:my-serverless-service-dev-hello
-```
diff --git a/docs/03-cli-reference/04-logs.md b/docs/03-cli-reference/05-logs.md
similarity index 100%
rename from docs/03-cli-reference/04-logs.md
rename to docs/03-cli-reference/05-logs.md
diff --git a/docs/03-cli-reference/06-info.md b/docs/03-cli-reference/06-info.md
new file mode 100644
index 000000000..324eb1c25
--- /dev/null
+++ b/docs/03-cli-reference/06-info.md
@@ -0,0 +1,72 @@
+
+
+# Info
+
+Displays information about the deployed service.
+
+```bash
+serverless info
+```
+
+## Options
+- `--stage` or `-s` The stage in your service you want to display information about.
+- `--region` or `-r` The region in your stage that you want to display information about.
+- `--verbose` or `-v` Shows displays any Stack Output.
+
+## Provided lifecycle events
+- `info:info`
+
+## Examples
+
+### AWS
+
+On AWS the info plugin uses the `Outputs` section of the CloudFormation stack and the AWS SDK to gather the necessary information.
+See the example below for an example output.
+
+**Example:**
+
+```
+$ serverless info
+
+Service Information
+service: my-serverless-service
+stage: dev
+region: us-east-1
+api keys:
+ myKey: some123valid456api789key1011for1213api1415gateway
+endpoints:
+ GET - https://dxaynpuzd4.execute-api.us-east-1.amazonaws.com/dev/users
+functions:
+ my-serverless-service-dev-hello: arn:aws:lambda:us-east-1:377024778620:function:my-serverless-service-dev-hello
+```
+
+#### Verbose
+When using the `--verbose` flag, the `info` command will also append all Stack Outputs to the output:
+```
+$ serverless info --verbose
+
+Service Information
+service: my-serverless-service
+stage: dev
+region: us-east-1
+api keys:
+ myKey: some123valid456api789key1011for1213api1415gateway
+endpoints:
+ GET - https://dxaynpuzd4.execute-api.us-east-1.amazonaws.com/dev/users
+functions:
+ my-serverless-service-dev-hello: arn:aws:lambda:us-east-1:377024778620:function:my-serverless-service-dev-hello
+
+Stack Outputs
+CloudFrontUrl: d2d10e2tyk1pei.cloudfront.net
+ListScreenshotsLambdaFunctionArn: arn:aws:lambda:us-east-1:377024778620:function:lambda-screenshots-dev-listScreenshots
+ScreenshotBucket: dev-svdgraaf-screenshots
+CreateThumbnailsLambdaFunctionArn: arn:aws:lambda:us-east-1:377024778620:function:lambda-screenshots-dev-createThumbnails
+TakeScreenshotLambdaFunctionArn: arn:aws:lambda:us-east-1:377024778620:function:lambda-screenshots-dev-takeScreenshot
+ServiceEndpoint: https://12341jc801.execute-api.us-east-1.amazonaws.com/dev
+ServerlessDeploymentBucketName: lambda-screenshots-dev-serverlessdeploymentbucket-15b7pkc04f98a
+```
diff --git a/docs/03-cli-reference/06-remove.md b/docs/03-cli-reference/07-remove.md
similarity index 100%
rename from docs/03-cli-reference/06-remove.md
rename to docs/03-cli-reference/07-remove.md
diff --git a/docs/03-cli-reference/07-tracking.md b/docs/03-cli-reference/07-tracking.md
deleted file mode 100644
index 7eb702871..000000000
--- a/docs/03-cli-reference/07-tracking.md
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-# Tracking
-
-This plugin implements a way to toggle the [framework usage tracking](../usage-tracking.md) functionality.
-
-```
-serverless tracking --enable
-```
-
-## Options
-- `--enable` or `-e`.
-- `--disable` or `-d`
-
-## Provided lifecycle events
-- `tracking:tracking`
-
-## Examples
-
-### Disable tracking
-
-```
-serverless tracking --disable
-```
-
-This example will disable usage tracking.
diff --git a/docs/03-cli-reference/08-slstats.md b/docs/03-cli-reference/08-slstats.md
new file mode 100644
index 000000000..0395a00d3
--- /dev/null
+++ b/docs/03-cli-reference/08-slstats.md
@@ -0,0 +1,31 @@
+
+
+# SlStats
+
+This plugin implements a way to toggle [framework statistics](../framework-statistics.md).
+
+```
+serverless slstats --enable
+```
+
+## Options
+- `--enable` or `-e`.
+- `--disable` or `-d`
+
+## Provided lifecycle events
+- `slstats:slstats`
+
+## Examples
+
+### Disabling it
+
+```
+serverless slstats --disable
+```
+
+This example will disable framework statistics.
diff --git a/docs/03-cli-reference/README.md b/docs/03-cli-reference/README.md
index d6fc78f31..7946f4146 100644
--- a/docs/03-cli-reference/README.md
+++ b/docs/03-cli-reference/README.md
@@ -9,9 +9,10 @@ layout: Doc
Here you can read through the docs of all commands that come with Serverless.
* [create](./01-create.md)
-* [deploy](./02-deploy.md)
-* [invoke](./03-invoke.md)
-* [logs](./04-logs.md)
-* [info](./05-info.md)
-* [remove](./06-remove.md)
-* [tracking](./07-tracking.md)
+* [install](./02-install.md)
+* [deploy](./03-deploy.md)
+* [invoke](./04-invoke.md)
+* [logs](./05-logs.md)
+* [info](./06-info.md)
+* [remove](./07-remove.md)
+* [slstats](./08-slstats.md)
diff --git a/docs/04-extending-serverless/01-creating-plugins.md b/docs/04-extending-serverless/01-creating-plugins.md
index dc075d4eb..c746d8856 100644
--- a/docs/04-extending-serverless/01-creating-plugins.md
+++ b/docs/04-extending-serverless/01-creating-plugins.md
@@ -437,7 +437,7 @@ custom:
Plugins are registered in the order they are defined through our system and the
`serverless.yml` file. By default we will load the
-[core plugins](https://github.com/serverless/serverless/tree/master/lib/plugins/) first, then we will load all plugins according to the order given in the
+[core plugins](../../lib/plugins/) first, then we will load all plugins according to the order given in the
`serverless.yml` file.
This means the Serverless core plugins will always be executed first for every lifecycle event before 3rd party plugins.
diff --git a/docs/04-extending-serverless/02-creating-provider-plugins.md b/docs/04-extending-serverless/02-creating-provider-plugins.md
index 6288424a8..a99ffa53d 100644
--- a/docs/04-extending-serverless/02-creating-provider-plugins.md
+++ b/docs/04-extending-serverless/02-creating-provider-plugins.md
@@ -19,7 +19,7 @@ Infrastructure provider plugins should bind to specific lifecycle events of the
### Deployment lifecycle
-Let's take a look at the [core `deploy` plugin](https://github.com/serverless/serverless/tree/master/lib/plugins/deploy) and the different lifecycle hooks it provides.
+Let's take a look at the [core `deploy` plugin](../../lib/plugins/deploy) and the different lifecycle hooks it provides.
The following lifecycle events are run in order once the user types `serverless deploy` and hits enter:
@@ -90,4 +90,4 @@ Here are the steps the AWS plugins take to compile and deploy the service on the
You may also take a closer look at the corresponding plugin code to get a deeper knowledge about what's going on behind the scenes.
-The full AWS integration can be found in [`lib/plugins/aws`](https://github.com/serverless/serverless/tree/master/lib/plugins/aws).
+The full AWS integration can be found in [`lib/plugins/aws`](../../lib/plugins/aws).
diff --git a/docs/README.md b/docs/README.md
index 47a190cb3..fa219d4b3 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -61,6 +61,6 @@ We love our contributors! Please read our [Contributing Document](../CONTRIBUTIN
Check out our [help-wanted](https://github.com/serverless/serverless/labels/help-wanted) or [help-wanted-easy](https://github.com/serverless/serverless/labels/help-wanted-easy) labels to find issues we want to move forward on with your help.
-## Usage Tracking
+## Framework statistics
-[Anonymous Usage Tracking](./usage-tracking.md)
+[Framework statistics](./framework-statistics.md)
diff --git a/docs/framework-statistics.md b/docs/framework-statistics.md
new file mode 100644
index 000000000..6adad1c4c
--- /dev/null
+++ b/docs/framework-statistics.md
@@ -0,0 +1,30 @@
+
+
+# Framework statistics
+
+Serverless will automatically collect anonymous framework statistics. This is done so that we better understand the usage and needs
+of our users to improve Serverless in future releases. However you can always [disable it](#how-to-disable-it).
+
+## What we collect
+
+Our main goal is anonymity. All the data is anonymized and won't reveal who you are or what the project you're working on is / looks like.
+
+Please take a look at the [`logStat()` method](../lib/classes/Utils.js) in the `Utils` class to see what (and how) we collect statistics.
+
+## How it's implemented
+
+We encourage you to look into the source to see more details about the actual implementation.
+
+The whole implementation consists of two parts:
+
+1. The [slstats plugin](../lib/plugins/slstats)
+2. The `logStat()` method you can find in the [Utils class](../lib/classes/Utils.js)
+
+## How to disable it
+
+You can disable it by running the following command: `serverless slstats --disable`.
+You can always run `serverless slstats --enable` to enable it again.
diff --git a/docs/usage-tracking.md b/docs/usage-tracking.md
deleted file mode 100644
index b200679d9..000000000
--- a/docs/usage-tracking.md
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-# Usage tracking
-
-Serverless will automatically track anonymous usage data. This is done so that we better understand the usage and needs
-of our users to improve Serverless in future releases. However you can always [disable usage tracking](#how-to-disable-it).
-
-## What we track
-
-Our main goal is anonymity while tracking usage behavior. All the data is anonymized and won't reveal who you are or what
-the project you're working on is / looks like.
-
-Please take a look at the [`track()` method](../lib/classes/Utils.js) in the `Utils` class to see what (and how) we track.
-
-## How tracking is implemented
-
-We encourage you to look into the source to see more details about the actual implementation.
-
-The tracking implementation consists of two parts:
-
-1. The [tracking plugin](../lib/plugins/tracking)
-2. The `track` method you can find in the [Utils class](../lib/classes/Utils.js)
-
-## How to disable it
-
-You can disable usage tracking by running the following command: `serverless tracking --disable`.
-You can always run `serverless tracking --enable` to enable tracking again.
diff --git a/docs/v0-v1-comparison.md b/docs/v0-v1-comparison.md
new file mode 100644
index 000000000..c5a9c4acb
--- /dev/null
+++ b/docs/v0-v1-comparison.md
@@ -0,0 +1,42 @@
+
+
+# Comparison between 0.x and 1.x of Serverless
+
+After the 0.5.6 release of Serverless we sat down with many contributors and users of the Framework to discuss the next steps to improve Serverless.
+Those discussions lead to our decision to completely rewrite Serverless. The configuration is in no way backwards compatible and can basically be seen as a completely new tool.
+
+We've decided to make this step so in the future we have a stronger base to work from and make sure we don't have to do major breaking changes like this anymore.
+
+Let's dig into the main differences between 0.x and 1.x to give you an idea how to start migrating your services. In general we've seen teams move from 0.x to 1.x in a relatively short amount of time, if you have any questions regarding the move please let us know in [our Forum](http://forum.serverless.com) or create [Issues in Github](https://github.com/serverless/serverless/issues).
+
+## Main differences between 0.x and 1.x
+
+As 1.x is a complete reimplementation without backwards compatibility pretty much everything is different. The following features are the most important ones to give you an understanding of where Serverless is moving.
+
+### Central configuration file
+
+In the past configuration was spread out over several configuration files. It was hard for users to have a good overview over all the different configuration values set for different functions. This was now moved into a central [serverless.yml file](./01-guide/12-serverless-yml-reference.md) that stores all configuration for one service. This also means there is no specific folder setup that you have to follow any more. By default Serverless simply zips up the folder your serverless.yml is in and deploys it to any functions defined in that config file (although you can [change the packaging behavior](./01-guide/10-packaging.md)).
+
+### Services are the main unit of deployment
+
+In the past Serverless didn't create a strong connection between functions that were deployed together. It was more for convenience sake that separate functions were grouped together. With 1.x functions now belong to a service. You can implement and deploy different services and while it's still possible to mix functions that are not related into the same service it's discouraged. Serverless wants you to build a micro-service architecture with functions being a part of that, but not the only part. You can read more about this in a past [blog post](https://serverless.com/blog/beginning-serverless-framework-v1/)
+
+### Built on CloudFormation
+
+With the move to a more service oriented style came the decision to move all configuration into CloudFormation. Every resource we create gets created through a central CloudFormation template. Each service gets its own CloudFormation stack, we even deploy new CF stacks if you create a service in a different stage. A very important feature that came with this move to CF was that you can now easily create any other kind of resource in AWS and connect it with your functions. You can read more about custom resources in [our guide](./01-guide/06-custom-provider-resources.md)
+
+### New plugin system
+
+While our old plugin system allowed for a powerful setup we felt we could push it a lot further and went back to the drawing board. We came up with a completely new way to build plugins for Serverless through hooks and lifecycle events. This is a breaking change for any existing plugin. You can read more about our Plugin system in our [extending serverless docs](./04-extending-serverless).
+
+### Endpoints are now events
+
+In 0.x APIG was treated as a separate resource and you could deploy endpoints separately. In 1.x APIG is just another event source that can be configured to trigger Lambda functions. We create one APIG per CloudFormation stack, so if you deploy to different stages we're creating separate API Gateways. You can read all about our [APIG integration in our event docs](./02-providers/aws/events/01-apigateway.md).
+
+## How to upgrade from 0.x to 1.x
+
+As Serverless 1.x is a complete reimplementation and does not implement all the features that were in 0.x (but has a lot more features in general) there is no direct update path. Basically the best way for users to move from 0.x to 1.x is to go through [our guide](./01-guide) and the [AWS provider documentation](./02-providers/aws) that will teach you all the details of Serverless 1.x. This should make it pretty easy to understand how to set up a service for 1.x and move your code over. We've worked with different teams during the Beta phase of Serverless 1.x and they were able to move their services into the new release pretty quickly.
diff --git a/lib/Serverless.js b/lib/Serverless.js
index 6b31a2644..cd4fa678e 100644
--- a/lib/Serverless.js
+++ b/lib/Serverless.js
@@ -4,6 +4,7 @@ require('shelljs/global');
const path = require('path');
const BbPromise = require('bluebird');
+const os = require('os');
const CLI = require('./classes/CLI');
const Config = require('./classes/Config');
const YamlParser = require('./classes/YamlParser');
@@ -40,6 +41,8 @@ class Serverless {
this.classes.Service = Service;
this.classes.Variables = Variables;
this.classes.Error = SError;
+
+ this.serverlessDirPath = path.join(os.homedir(), '.serverless');
}
init() {
@@ -62,28 +65,29 @@ class Serverless {
// load all plugins
this.pluginManager.loadAllPlugins(this.service.plugins);
- // give the CLI the plugins so that it can print out plugin information
- // such as options when the user enters --help
+ // give the CLI the plugins and commands so that it can print out
+ // information such as options when the user enters --help
this.cli.setLoadedPlugins(this.pluginManager.getPlugins());
-
- // populate variables after processing options
- return this.variables.populateService(this.pluginManager.cliOptions);
+ this.cli.setLoadedCommands(this.pluginManager.getCommands());
});
}
run() {
- // check if tracking is enabled (and track if it's enabled)
- const serverlessPath = this.config.serverlessPath;
- if (!this.utils.fileExistsSync(path.join(serverlessPath, 'do-not-track'))) {
- this.utils.track(this);
+ this.utils.logStat(this).catch(() => BbPromise.resolve());
+
+ if (this.cli.displayHelp(this.processedInput)) {
+ return BbPromise.resolve();
}
- if (!this.cli.displayHelp(this.processedInput) && this.processedInput.commands.length) {
- // trigger the plugin lifecycle when there's something which should be processed
- return this.pluginManager.run(this.processedInput.commands);
- }
+ // make sure the command exists before doing anything else
+ this.pluginManager.validateCommand(this.processedInput.commands);
- return BbPromise.resolve();
+ // populate variables after --help, otherwise help may fail to print
+ // (https://github.com/serverless/serverless/issues/2041)
+ this.variables.populateService(this.pluginManager.cliOptions);
+
+ // trigger the plugin lifecycle when there's something which should be processed
+ return this.pluginManager.run(this.processedInput.commands);
}
getVersion() {
diff --git a/lib/classes/CLI.js b/lib/classes/CLI.js
index b5d255661..05698c36d 100644
--- a/lib/classes/CLI.js
+++ b/lib/classes/CLI.js
@@ -11,12 +11,17 @@ class CLI {
this.serverless = serverless;
this.inputArray = inputArray || null;
this.loadedPlugins = [];
+ this.loadedCommands = {};
}
setLoadedPlugins(plugins) {
this.loadedPlugins = plugins;
}
+ setLoadedCommands(commands) {
+ this.loadedCommands = commands;
+ }
+
processInput() {
let inputArray;
@@ -63,6 +68,52 @@ class CLI {
return false;
}
+ displayCommandUsage(commandObject, command) {
+ const dotsLength = 30;
+
+ // check if command has lifecycleEvents (can be executed)
+ if (commandObject.lifecycleEvents) {
+ const usage = commandObject.usage;
+ const dots = _.repeat('.', dotsLength - command.length);
+ this.consoleLog(`${chalk.yellow(command)} ${chalk.dim(dots)} ${usage}`);
+ }
+
+ _.forEach(commandObject.commands, (subcommandObject, subcommand) => {
+ this.displayCommandUsage(subcommandObject, `${command} ${subcommand}`);
+ });
+ }
+
+ displayCommandOptions(commandObject) {
+ const dotsLength = 40;
+ _.forEach(commandObject.options, (optionsObject, option) => {
+ let optionsDots = _.repeat('.', dotsLength - option.length);
+ const optionsUsage = optionsObject.usage;
+
+ if (optionsObject.required) {
+ optionsDots = optionsDots.slice(0, optionsDots.length - 18);
+ } else {
+ optionsDots = optionsDots.slice(0, optionsDots.length - 7);
+ }
+ if (optionsObject.shortcut) {
+ optionsDots = optionsDots.slice(0, optionsDots.length - 5);
+ }
+
+ const optionInfo = ` --${option}`;
+ let shortcutInfo = '';
+ let requiredInfo = '';
+ if (optionsObject.shortcut) {
+ shortcutInfo = ` / -${optionsObject.shortcut}`;
+ }
+ if (optionsObject.required) {
+ requiredInfo = ' (required)';
+ }
+
+ const thingsToLog = `${optionInfo}${shortcutInfo}${requiredInfo} ${
+ chalk.dim(optionsDots)} ${optionsUsage}`;
+ this.consoleLog(chalk.yellow(thingsToLog));
+ });
+ }
+
generateMainHelp() {
this.consoleLog('');
@@ -73,153 +124,36 @@ class CLI {
this.consoleLog('');
- const sortedPlugins = _.sortBy(
- this.loadedPlugins,
- (plugin) => plugin.constructor.name
- );
-
- // TODO: implement recursive command exploration (now only 2 steps are possible)
- const dotsLength = 25;
- sortedPlugins.forEach((plugin) => {
- _.forEach(plugin.commands,
- (firstLevelCommandObject, firstLevelCommand) => {
- // check if command has lifecycleEvents (can be execute)
- if (firstLevelCommandObject.lifecycleEvents) {
- const command = firstLevelCommand;
- const usage = firstLevelCommandObject.usage;
- const dots = _.repeat('.', dotsLength - command.length);
- this.consoleLog(`${chalk
- .yellow(command)} ${chalk
- .dim(dots)} ${usage}`);
- }
- _.forEach(firstLevelCommandObject.commands,
- (secondLevelCommandObject, secondLevelCommand) => {
- // check if command has lifecycleEvents (can be executed)
- if (secondLevelCommandObject.lifecycleEvents) {
- const command = `${firstLevelCommand} ${secondLevelCommand}`;
- const usage = secondLevelCommandObject.usage;
- const dots = _.repeat('.', dotsLength - command.length);
- this.consoleLog(`${chalk
- .yellow(command)} ${chalk
- .dim(dots)} ${usage}`);
- }
- });
- });
+ _.forEach(this.loadedCommands, (details, command) => {
+ this.displayCommandUsage(details, command);
});
this.consoleLog('');
// print all the installed plugins
this.consoleLog(chalk.yellow.underline('Plugins'));
- if (sortedPlugins.length) {
+
+ if (this.loadedPlugins.length) {
+ const sortedPlugins = _.sortBy(
+ this.loadedPlugins,
+ (plugin) => plugin.constructor.name
+ );
+
this.consoleLog(sortedPlugins.map((plugin) => plugin.constructor.name).join(', '));
} else {
this.consoleLog('No plugins added yet');
}
}
- generateCommandsHelp(commands) {
- const dotsLength = 40;
+ generateCommandsHelp(commandsArray) {
+ const command = this.serverless.pluginManager.getCommand(commandsArray);
+ const commandName = commandsArray.join(' ');
- // TODO: use lodash utility functions to reduce loop usage
- // TODO: support more than 2 levels of nested commands
- if (commands.length === 1) {
- this.loadedPlugins.forEach((plugin) => {
- _.forEach(plugin.commands, (commandObject, command) => {
- if (command === commands[0]) {
- if (commandObject.lifecycleEvents) {
- // print the name of the plugin
- this.consoleLog(chalk.yellow.underline(`Plugin: ${plugin.constructor.name}`));
- // print the command with the corresponding usage
- const commandsDots = _.repeat('.', dotsLength - command.length);
- const commandsUsage = commandObject.usage;
- this.consoleLog(`${chalk
- .yellow(command)} ${chalk
- .dim(commandsDots)} ${commandsUsage}`);
- // print all options
- _.forEach(commandObject.options, (optionsObject, option) => {
- let optionsDots = _.repeat('.', dotsLength - option.length);
- const optionsUsage = optionsObject.usage;
+ // print the name of the plugin
+ this.consoleLog(chalk.yellow.underline(`Plugin: ${command.pluginName}`));
- if (optionsObject.required) {
- optionsDots = optionsDots.slice(0, optionsDots.length - 17);
- } else {
- optionsDots = optionsDots.slice(0, optionsDots.length - 7);
- }
- if (optionsObject.shortcut) {
- optionsDots = optionsDots.slice(0, optionsDots.length - 5);
- }
-
- const optionInfo = ` --${option}`;
- let shortcutInfo = '';
- let requiredInfo = '';
- if (optionsObject.shortcut) {
- shortcutInfo = ` / -${optionsObject.shortcut}`;
- }
- if (optionsObject.required) {
- requiredInfo = ' (required)';
- }
-
- const thingsToLog = `${optionInfo}${shortcutInfo}${requiredInfo} ${
- chalk.dim(optionsDots)} ${optionsUsage}`;
- this.consoleLog(chalk.yellow(thingsToLog));
- });
- }
- }
- });
- });
- } else {
- this.loadedPlugins.forEach((plugin) => {
- _.forEach(plugin.commands,
- (firstLevelCommandObject, firstLevelCommand) => {
- if (firstLevelCommand === commands[0]) {
- _.forEach(firstLevelCommandObject.commands,
- (secondLevelCommandObject, secondLevelCommand) => {
- if (secondLevelCommand === commands[1]) {
- if (secondLevelCommandObject.lifecycleEvents) {
- // print the name of the plugin
- this.consoleLog(chalk.yellow.underline(`Plugin: ${plugin.constructor.name}`));
- // print the command with the corresponding usage
- const commandsDots = _.repeat('.', dotsLength - secondLevelCommand.length);
- const commandsUsage = secondLevelCommandObject.usage;
- this.consoleLog(`${chalk
- .yellow(secondLevelCommand)} ${chalk
- .dim(commandsDots)} ${commandsUsage}`);
- // print all options
- _.forEach(secondLevelCommandObject.options, (optionsObject, option) => {
- let optionsDots = _.repeat('.', dotsLength - option.length);
- const optionsUsage = optionsObject.usage;
-
- if (optionsObject.required) {
- optionsDots = optionsDots.slice(0, optionsDots.length - 17);
- } else {
- optionsDots = optionsDots.slice(0, optionsDots.length - 7);
- }
- if (optionsObject.shortcut) {
- optionsDots = optionsDots.slice(0, optionsDots.length - 5);
- }
-
- const optionInfo = ` --${option}`;
- let shortcutInfo = '';
- let requiredInfo = '';
- if (optionsObject.shortcut) {
- shortcutInfo = ` / -${optionsObject.shortcut}`;
- }
- if (optionsObject.required) {
- requiredInfo = ' (required)';
- }
-
- const thingsToLog = `${optionInfo}${shortcutInfo}${requiredInfo} ${
- chalk.dim(optionsDots)} ${optionsUsage}`;
- this.consoleLog(chalk.yellow(thingsToLog));
- });
- }
- }
- });
- }
- });
- });
- }
+ this.displayCommandUsage(command, commandName);
+ this.displayCommandOptions(command);
this.consoleLog('');
}
@@ -236,7 +170,7 @@ class CLI {
art = `${art}|____ |_____|__| \\___/|_____|__| |__|_____|_____|_____|${os.EOL}`;
art = `${art}| | | The Serverless Application Framework${os.EOL}`;
art = `${art}| | serverless.com, v${version}${os.EOL}`;
- art = `${art} -------\'`;
+ art = `${art} -------'`;
this.consoleLog(chalk.yellow(art));
this.consoleLog('');
diff --git a/lib/classes/Error.js b/lib/classes/Error.js
index e754e4d21..6c47ef8af 100644
--- a/lib/classes/Error.js
+++ b/lib/classes/Error.js
@@ -1,5 +1,6 @@
'use strict';
const chalk = require('chalk');
+const version = require('./../../package.json').version;
module.exports.SError = class ServerlessError extends Error {
constructor(message, statusCode) {
@@ -68,6 +69,11 @@ module.exports.logError = (e) => {
consoleLog(chalk.red(' Please report this error. We think it might be a bug.'));
}
+ consoleLog(' ');
+ consoleLog(chalk.yellow(' Your Environment Infomation -----------------------------'));
+ consoleLog(chalk.yellow(` OS: ${process.platform}`));
+ consoleLog(chalk.yellow(` Node Version: ${process.version.replace(/^[v|V]/, '')}`));
+ consoleLog(chalk.yellow(` Serverless Version: ${version}`));
consoleLog(' ');
// Failure exit
diff --git a/lib/classes/PluginManager.js b/lib/classes/PluginManager.js
index c990941c5..ef436481c 100644
--- a/lib/classes/PluginManager.js
+++ b/lib/classes/PluginManager.js
@@ -1,19 +1,20 @@
'use strict';
const path = require('path');
-const _ = require('lodash');
const BbPromise = require('bluebird');
+const _ = require('lodash');
class PluginManager {
constructor(serverless) {
this.serverless = serverless;
this.provider = null;
+
this.cliOptions = {};
this.cliCommands = [];
this.plugins = [];
- this.commandsList = [];
this.commands = {};
+ this.hooks = {};
}
setProvider(provider) {
@@ -28,39 +29,145 @@ class PluginManager {
this.cliCommands = commands;
}
+ addPlugin(Plugin) {
+ const pluginInstance = new Plugin(this.serverless, this.cliOptions);
+
+ // ignore plugins that specify a different provider than the current one
+ if (pluginInstance.provider && (pluginInstance.provider !== this.provider)) {
+ return;
+ }
+
+ this.loadCommands(pluginInstance);
+ this.loadHooks(pluginInstance);
+
+ this.plugins.push(pluginInstance);
+ }
+
loadAllPlugins(servicePlugins) {
this.loadCorePlugins();
this.loadServicePlugins(servicePlugins);
}
- validateCommands(commandsArray) {
- // TODO: implement an option to get deeper than one level
- if (!this.commands[commandsArray[0]]) {
- const errorMessage = [
- `command "${commandsArray[0]}" not found`,
- ' Run "serverless help" for a list of all available commands.',
- ].join();
- throw new this.serverless.classes.Error(errorMessage);
+ loadPlugins(plugins) {
+ plugins.forEach((plugin) => {
+ const Plugin = require(plugin); // eslint-disable-line global-require
+
+ this.addPlugin(Plugin);
+ });
+ }
+
+ loadCorePlugins() {
+ const pluginsDirectoryPath = path.join(__dirname, '../plugins');
+
+ const corePlugins = this.serverless.utils
+ .readFileSync(path.join(pluginsDirectoryPath, 'Plugins.json')).plugins
+ .map((corePluginPath) => path.join(pluginsDirectoryPath, corePluginPath));
+
+ this.loadPlugins(corePlugins);
+ }
+
+ loadServicePlugins(servicePlugs) {
+ const servicePlugins = (typeof servicePlugs !== 'undefined' ? servicePlugs : []);
+
+ // we want to load plugins installed locally in the service
+ if (this.serverless && this.serverless.config && this.serverless.config.servicePath) {
+ module.paths.unshift(path.join(this.serverless.config.servicePath, 'node_modules'));
+ }
+
+ this.loadPlugins(servicePlugins);
+
+ // restore module paths
+ if (this.serverless && this.serverless.config && this.serverless.config.servicePath) {
+ module.paths.shift();
}
}
- validateOptions(commandsArray) {
- let options;
+ loadCommand(pluginName, details, key) {
+ const commands = _.mapValues(details.commands, (subDetails, subKey) =>
+ this.loadCommand(pluginName, subDetails, `${key}:${subKey}`)
+ );
+ return _.assign({}, details, { key, pluginName, commands });
+ }
- // TODO: implement an option to get deeper than two levels
- if (commandsArray.length === 1) {
- options = this.commands[commandsArray[0]].options;
- } else {
- options = this.commands[commandsArray[0]].commands[commandsArray[1]].options;
+ loadCommands(pluginInstance) {
+ const pluginName = pluginInstance.constructor.name;
+ _.forEach(pluginInstance.commands, (details, key) => {
+ const command = this.loadCommand(pluginName, details, key);
+ this.commands[key] = _.merge({}, this.commands[key], command);
+ });
+ }
+
+ loadHooks(pluginInstance) {
+ _.forEach(pluginInstance.hooks, (hook, event) => {
+ this.hooks[event] = this.hooks[event] || [];
+ this.hooks[event].push(hook);
+ });
+ }
+
+ getCommands() {
+ return this.commands;
+ }
+
+ getCommand(commandsArray) {
+ return _.reduce(commandsArray, (current, name, index) => {
+ if (name in current.commands) {
+ return current.commands[name];
+ }
+ const commandName = commandsArray.slice(0, index + 1).join(' ');
+ const errorMessage = [
+ `Command "${commandName}" not found`,
+ ' Run "serverless help" for a list of all available commands.',
+ ].join();
+ throw new this.serverless.classes.Error(errorMessage);
+ }, { commands: this.commands });
+ }
+
+ getEvents(command) {
+ return _.flatMap(command.lifecycleEvents, (event) => [
+ `before:${command.key}:${event}`,
+ `${command.key}:${event}`,
+ `after:${command.key}:${event}`,
+ ]);
+ }
+
+ getPlugins() {
+ return this.plugins;
+ }
+
+ run(commandsArray) {
+ const command = this.getCommand(commandsArray);
+
+ this.convertShortcutsIntoOptions(command);
+ this.validateOptions(command);
+
+ const events = this.getEvents(command);
+ const hooks = _.flatMap(events, (event) => this.hooks[event] || []);
+
+ if (hooks.length === 0) {
+ const errorMessage = 'The command you entered did not catch on any hooks';
+ throw new this.serverless.classes.Error(errorMessage);
}
- _.forEach(options, (value, key) => {
+ return BbPromise.reduce(hooks, (__, hook) => hook(), null);
+ }
+
+ validateCommand(commandsArray) {
+ this.getCommand(commandsArray);
+ }
+
+ validateOptions(command) {
+ _.forEach(command.options, (value, key) => {
if (value.required && (this.cliOptions[key] === true || !(this.cliOptions[key]))) {
let requiredThings = `the --${key} option`;
+
if (value.shortcut) {
requiredThings += ` / -${value.shortcut} shortcut`;
}
- const errorMessage = `This command requires ${requiredThings}.`;
+ let errorMessage = `This command requires ${requiredThings}.`;
+
+ if (value.usage) {
+ errorMessage = `${errorMessage} Usage: ${value.usage}`;
+ }
throw new this.serverless.classes.Error(errorMessage);
}
@@ -74,163 +181,19 @@ class PluginManager {
});
}
- run(commandsArray) {
- // check if the command the user has entered is provided through a plugin
- this.validateCommands(commandsArray);
-
- // check if all options are passed
- this.validateOptions(commandsArray);
-
- const events = this.getEvents(commandsArray, this.commands);
- const hooks = events.reduce((memo, event) => {
- this.plugins.forEach((pluginInstance) => {
- // if a provider is given it should only add the hook when the plugins provider matches
- // the services provider
- if (!pluginInstance.provider || (pluginInstance.provider === this.provider)) {
- _.forEach(pluginInstance.hooks, (hook, hookKey) => {
- if (hookKey === event) {
- memo.push(hook);
- }
- });
- }
- });
- return memo;
- }, []);
-
- if (hooks.length === 0) {
- const errorMessage = `The command you entered was not found.
- Did you spell it correctly?`;
- throw new this.serverless.classes.Error(errorMessage);
- }
-
- return BbPromise.reduce(hooks, (__, hook) => hook(), null);
- }
-
- convertShortcutsIntoOptions(cliOptions, commands) {
- // TODO: implement an option to get deeper than two levels
- // check if the command entered is the one in the commands object which holds all commands
- // this is necessary so that shortcuts are not treated like global citizens but command
- // bound properties
- if (this.cliCommands.length === 1) {
- _.forEach(commands, (firstCommand, firstCommandKey) => {
- if (_.includes(this.cliCommands, firstCommandKey)) {
- _.forEach(firstCommand.options, (optionObject, optionKey) => {
- if (optionObject.shortcut && _.includes(Object.keys(cliOptions),
- optionObject.shortcut)) {
- Object.keys(cliOptions).forEach((option) => {
- if (option === optionObject.shortcut) {
- this.cliOptions[optionKey] = this.cliOptions[option];
- }
- });
- }
- });
- }
- });
- } else if (this.cliCommands.length === 2) {
- _.forEach(commands, (firstCommand) => {
- _.forEach(firstCommand.commands, (secondCommand, secondCommandKey) => {
- if (_.includes(this.cliCommands, secondCommandKey)) {
- _.forEach(secondCommand.options, (optionObject, optionKey) => {
- if (optionObject.shortcut && _.includes(Object.keys(cliOptions),
- optionObject.shortcut)) {
- Object.keys(cliOptions).forEach((option) => {
- if (option === optionObject.shortcut) {
- this.cliOptions[optionKey] = this.cliOptions[option];
- }
- });
- }
- });
+ convertShortcutsIntoOptions(command) {
+ _.forEach(command.options, (optionObject, optionKey) => {
+ if (optionObject.shortcut && _.includes(Object.keys(this.cliOptions),
+ optionObject.shortcut)) {
+ Object.keys(this.cliOptions).forEach((option) => {
+ if (option === optionObject.shortcut) {
+ this.cliOptions[optionKey] = this.cliOptions[option];
}
});
- });
- }
- }
-
- addPlugin(Plugin) {
- const pluginInstance = new Plugin(this.serverless, this.cliOptions);
-
- this.loadCommands(pluginInstance);
-
- // shortcuts should be converted into options so that the plugin
- // author can use the option (instead of the shortcut)
- this.convertShortcutsIntoOptions(this.cliOptions, this.commands);
-
- this.plugins.push(pluginInstance);
- }
-
- loadCorePlugins() {
- const pluginsDirectoryPath = path.join(__dirname, '../plugins');
-
- const corePlugins = this.serverless.utils
- .readFileSync(path.join(pluginsDirectoryPath, 'Plugins.json')).plugins;
-
- corePlugins.forEach((corePlugin) => {
- const Plugin = require(path // eslint-disable-line global-require
- .join(pluginsDirectoryPath, corePlugin));
-
- this.addPlugin(Plugin);
- });
- }
-
- loadServicePlugins(servicePlugs) {
- const servicePlugins = (typeof servicePlugs !== 'undefined' ? servicePlugs : []);
-
- // we want to load plugins installed locally in the service
- if (this.serverless && this.serverless.config && this.serverless.config.servicePath) {
- module.paths.unshift(path.join(this.serverless.config.servicePath, 'node_modules'));
- }
-
- servicePlugins.forEach((servicePlugin) => {
- const Plugin = require(servicePlugin); // eslint-disable-line global-require
-
- this.addPlugin(Plugin);
- });
-
- // restore module paths
- if (this.serverless && this.serverless.config && this.serverless.config.servicePath) {
- module.paths.shift();
- }
- }
-
- loadCommands(pluginInstance) {
- this.commandsList.push(pluginInstance.commands);
-
- // TODO: refactor ASAP as it slows down overall performance
- // rebuild the commands
- _.forEach(this.commandsList, (commands) => {
- _.forEach(commands, (commandDetails, command) => {
- this.commands[command] = commandDetails;
- });
- });
- }
-
- getEvents(commandsArray, availableCommands, pre) {
- const prefix = (typeof pre !== 'undefined' ? pre : '');
- const commandPart = commandsArray[0];
-
- if (_.has(availableCommands, commandPart)) {
- const commandDetails = availableCommands[commandPart];
- if (commandsArray.length === 1) {
- const events = [];
- commandDetails.lifecycleEvents.forEach((event) => {
- events.push(`before:${prefix}${commandPart}:${event}`);
- events.push(`${prefix}${commandPart}:${event}`);
- events.push(`after:${prefix}${commandPart}:${event}`);
- });
- return events;
}
- if (_.has(commandDetails, 'commands')) {
- return this.getEvents(commandsArray.slice(1, commandsArray.length),
- commandDetails.commands, `${commandPart}:`);
- }
- }
-
- return [];
+ });
}
- getPlugins() {
- return this.plugins;
- }
}
module.exports = PluginManager;
diff --git a/lib/classes/Service.js b/lib/classes/Service.js
index 7756e9a93..69136a9bf 100644
--- a/lib/classes/Service.js
+++ b/lib/classes/Service.js
@@ -90,7 +90,6 @@ class Service {
that.package.individually = serverlessFile.package.individually;
that.package.artifact = serverlessFile.package.artifact;
that.package.exclude = serverlessFile.package.exclude;
- that.package.include = serverlessFile.package.include;
}
if (serverlessFile.defaults && serverlessFile.defaults.stage) {
diff --git a/lib/classes/Utils.js b/lib/classes/Utils.js
index 23e97be8e..e4f0a6669 100644
--- a/lib/classes/Utils.js
+++ b/lib/classes/Utils.js
@@ -8,6 +8,7 @@ const fse = BbPromise.promisifyAll(require('fs-extra'));
const _ = require('lodash');
const fetch = require('node-fetch');
const uuid = require('uuid');
+const os = require('os');
class Utils {
constructor(serverless) {
@@ -142,152 +143,167 @@ class Utils {
return servicePath;
}
- track(serverless) {
- const writeKey = 'XXXX'; // TODO: Replace me before release
+ logStat(serverless) {
+ const log = (data) => {
+ const writeKey = 'XXXX'; // TODO: Replace me before release
+ const auth = `${writeKey}:`;
- let userId = uuid.v1();
-
- // create a new file with a uuid as the tracking id if not yet present
- const trackingIdFilePath = path.join(serverless.config.serverlessPath, 'tracking-id');
- if (!this.fileExistsSync(trackingIdFilePath)) {
- fs.writeFileSync(trackingIdFilePath, userId);
- } else {
- userId = fs.readFileSync(trackingIdFilePath).toString();
- }
-
- // function related information retrieval
- const numberOfFunctions = _.size(serverless.service.functions);
-
- const memorySizeAndTimeoutPerFunction = [];
- if (numberOfFunctions) {
- _.forEach(serverless.service.functions, (func) => {
- const memorySize = Number(func.memorySize)
- || Number(this.serverless.service.provider.memorySize)
- || 1024;
- const timeout = Number(func.timeout)
- || Number(this.serverless.service.provider.timeout)
- || 6;
-
- const memorySizeAndTimeoutObject = {
- memorySize,
- timeout,
- };
-
- memorySizeAndTimeoutPerFunction.push(memorySizeAndTimeoutObject);
- });
- }
-
- // event related information retrieval
- const numberOfEventsPerType = [];
- const eventNamesPerFunction = [];
- if (numberOfFunctions) {
- _.forEach(serverless.service.functions, (func) => {
- if (func.events) {
- const funcEventsArray = [];
-
- func.events.forEach((event) => {
- const name = Object.keys(event)[0];
- funcEventsArray.push(name);
-
- const alreadyPresentEvent = _.find(numberOfEventsPerType, { name });
- if (alreadyPresentEvent) {
- alreadyPresentEvent.count++;
- } else {
- numberOfEventsPerType.push({
- name,
- count: 1,
- });
- }
- });
-
- eventNamesPerFunction.push(funcEventsArray);
- }
- });
- }
-
- let hasCustomResourcesDefined = false;
- // check if configuration in resources.Resources is defined
- if ((serverless.service.resources &&
- serverless.service.resources.Resources &&
- Object.keys(serverless.service.resources.Resources).length)) {
- hasCustomResourcesDefined = true;
- }
- // check if configuration in resources.Outputs is defined
- if ((serverless.service.resources &&
- serverless.service.resources.Outputs &&
- Object.keys(serverless.service.resources.Outputs).length)) {
- hasCustomResourcesDefined = true;
- }
-
- let hasCustomVariableSyntaxDefined = false;
- const defaultVariableSyntax = '\\${([ :a-zA-Z0-9._,\\-\\/\\(\\)]+?)}';
- // check if the variableSyntax in the defaults section is defined
- if (serverless.service.defaults &&
- serverless.service.defaults.variableSyntax &&
- serverless.service.defaults.variableSyntax !== defaultVariableSyntax) {
- hasCustomVariableSyntaxDefined = true;
- }
- // check if the variableSyntax in the provider section is defined
- if (serverless.service.provider &&
- serverless.service.provider.variableSyntax &&
- serverless.service.provider.variableSyntax !== defaultVariableSyntax) {
- hasCustomVariableSyntaxDefined = true;
- }
-
- const auth = `${writeKey}:`;
-
- const data = {
- userId,
- event: 'Serverless framework usage',
- properties: {
- command: {
- name: serverless.processedInput.commands.join(' '),
- isRunInService: (!!serverless.config.servicePath),
+ return fetch('https://api.segment.io/v1/track', {
+ headers: {
+ Authorization: `Basic ${new Buffer(auth).toString('base64')}`,
+ 'content-type': 'application/json',
},
- service: {
- numberOfCustomPlugins: _.size(serverless.service.plugins),
- hasCustomResourcesDefined,
- hasVariablesInCustomSectionDefined: (!!serverless.service.custom),
- hasCustomVariableSyntaxDefined,
- },
- provider: {
- name: serverless.service.provider.name,
- runtime: serverless.service.provider.runtime,
- stage: serverless.service.provider.stage,
- region: serverless.service.provider.region,
- },
- functions: {
- numberOfFunctions,
- memorySizeAndTimeoutPerFunction,
- },
- events: {
- numberOfEvents: numberOfEventsPerType.length,
- numberOfEventsPerType,
- eventNamesPerFunction,
- },
- general: {
- userId,
- timestamp: (new Date()).getTime(),
- timezone: (new Date()).toString().match(/([A-Z]+[\+-][0-9]+.*)/)[1],
- operatingSystem: process.platform,
- serverlessVersion: serverless.version,
- nodeJsVersion: process.version,
- },
- },
+ method: 'POST',
+ timeout: '1000',
+ body: JSON.stringify(data),
+ })
+ .then((response) => response.json())
+ .then(() => BbPromise.resolve())
+ .catch(() => BbPromise.resolve());
};
- return fetch('https://api.segment.io/v1/track', {
- headers: {
- Authorization: `Basic ${new Buffer(auth).toString('base64')}`,
- 'content-type': 'application/json',
- },
- method: 'POST',
- timeout: '1000',
- body: JSON.stringify(data),
- })
- .then((response) => response.json())
- .then(() => BbPromise.resolve())
- .catch(() => BbPromise.resolve());
+ return new BbPromise((resolve) => {
+ const serverlessDirPath = path.join(os.homedir(), '.serverless');
+ const statsEnabledFilePath = path.join(serverlessDirPath, 'stats-enabled');
+ const statsDisabledFilePath = path.join(serverlessDirPath, 'stats-disabled');
+
+ if (this.fileExistsSync(statsDisabledFilePath)) {
+ return resolve();
+ }
+
+ let userId = uuid.v1();
+
+ if (!this.fileExistsSync(statsEnabledFilePath)) {
+ this.writeFileSync(statsEnabledFilePath, userId);
+ } else {
+ userId = this.readFileSync(statsEnabledFilePath).toString();
+ }
+
+ // function related information retrieval
+ const numberOfFunctions = _.size(serverless.service.functions);
+
+ const memorySizeAndTimeoutPerFunction = [];
+ if (numberOfFunctions) {
+ _.forEach(serverless.service.functions, (func) => {
+ const memorySize = Number(func.memorySize)
+ || Number(this.serverless.service.provider.memorySize)
+ || 1024;
+ const timeout = Number(func.timeout)
+ || Number(this.serverless.service.provider.timeout)
+ || 6;
+
+ const memorySizeAndTimeoutObject = {
+ memorySize,
+ timeout,
+ };
+
+ memorySizeAndTimeoutPerFunction.push(memorySizeAndTimeoutObject);
+ });
+ }
+
+ // event related information retrieval
+ const numberOfEventsPerType = [];
+ const eventNamesPerFunction = [];
+ if (numberOfFunctions) {
+ _.forEach(serverless.service.functions, (func) => {
+ if (func.events) {
+ const funcEventsArray = [];
+
+ func.events.forEach((event) => {
+ const name = Object.keys(event)[0];
+ funcEventsArray.push(name);
+
+ const alreadyPresentEvent = _.find(numberOfEventsPerType, { name });
+ if (alreadyPresentEvent) {
+ alreadyPresentEvent.count++;
+ } else {
+ numberOfEventsPerType.push({
+ name,
+ count: 1,
+ });
+ }
+ });
+
+ eventNamesPerFunction.push(funcEventsArray);
+ }
+ });
+ }
+
+ let hasCustomResourcesDefined = false;
+ // check if configuration in resources.Resources is defined
+ if ((serverless.service.resources &&
+ serverless.service.resources.Resources &&
+ Object.keys(serverless.service.resources.Resources).length)) {
+ hasCustomResourcesDefined = true;
+ }
+ // check if configuration in resources.Outputs is defined
+ if ((serverless.service.resources &&
+ serverless.service.resources.Outputs &&
+ Object.keys(serverless.service.resources.Outputs).length)) {
+ hasCustomResourcesDefined = true;
+ }
+
+ let hasCustomVariableSyntaxDefined = false;
+ const defaultVariableSyntax = '\\${([ :a-zA-Z0-9._,\\-\\/\\(\\)]+?)}';
+ // check if the variableSyntax in the defaults section is defined
+ if (serverless.service.defaults &&
+ serverless.service.defaults.variableSyntax &&
+ serverless.service.defaults.variableSyntax !== defaultVariableSyntax) {
+ hasCustomVariableSyntaxDefined = true;
+ }
+ // check if the variableSyntax in the provider section is defined
+ if (serverless.service.provider &&
+ serverless.service.provider.variableSyntax &&
+ serverless.service.provider.variableSyntax !== defaultVariableSyntax) {
+ hasCustomVariableSyntaxDefined = true;
+ }
+
+ const data = {
+ userId,
+ event: 'framework_stat',
+ properties: {
+ version: 1,
+ command: {
+ name: serverless.processedInput.commands.join(' '),
+ isRunInService: (!!serverless.config.servicePath),
+ },
+ service: {
+ numberOfCustomPlugins: _.size(serverless.service.plugins),
+ hasCustomResourcesDefined,
+ hasVariablesInCustomSectionDefined: (!!serverless.service.custom),
+ hasCustomVariableSyntaxDefined,
+ },
+ provider: {
+ name: serverless.service.provider.name,
+ runtime: serverless.service.provider.runtime,
+ stage: serverless.service.provider.stage,
+ region: serverless.service.provider.region,
+ },
+ functions: {
+ numberOfFunctions,
+ memorySizeAndTimeoutPerFunction,
+ },
+ events: {
+ numberOfEvents: numberOfEventsPerType.length,
+ numberOfEventsPerType,
+ eventNamesPerFunction,
+ },
+ general: {
+ userId,
+ timestamp: (new Date()).getTime(),
+ timezone: (new Date()).toString().match(/([A-Z]+[\+-][0-9]+.*)/)[1],
+ operatingSystem: process.platform,
+ serverlessVersion: serverless.version,
+ nodeJsVersion: process.version,
+ },
+ },
+ };
+
+ return resolve(data);
+ }).then((data) => {
+ // only log the data if it's there
+ if (data) log(data);
+ });
}
}
diff --git a/lib/plugins/Plugins.json b/lib/plugins/Plugins.json
index 290bd84e5..a5c6bf6f9 100644
--- a/lib/plugins/Plugins.json
+++ b/lib/plugins/Plugins.json
@@ -1,13 +1,14 @@
{
"plugins": [
"./create/create.js",
+ "./install/install.js",
"./package/index.js",
"./deploy/deploy.js",
"./invoke/invoke.js",
"./info/info.js",
"./logs/logs.js",
"./remove/remove.js",
- "./tracking/tracking.js",
+ "./slstats/slstats.js",
"./aws/deploy/index.js",
"./aws/invoke/index.js",
"./aws/info/index.js",
@@ -18,6 +19,7 @@
"./aws/deploy/compile/events/s3/index.js",
"./aws/deploy/compile/events/apiGateway/index.js",
"./aws/deploy/compile/events/sns/index.js",
+ "./aws/deploy/compile/events/stream/index.js",
"./aws/deployFunction/index.js"
]
}
diff --git a/lib/plugins/aws/deploy/README.md b/lib/plugins/aws/deploy/README.md
deleted file mode 100644
index e75f5ab7e..000000000
--- a/lib/plugins/aws/deploy/README.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Deploy
-
-This plugin (re)deploys the service to AWS.
-
-## How it works
-
-`Deploy` starts by hooking into the [`deploy:setupProviderConfiguration`](/lib/plugins/deploy) lifecycle.
-It fetches the basic CloudFormation template from `lib/templates` and replaces the necessary names and definitions
-with the one it gets from the `serverless.yml` file.
-
-Next up it deploys the CloudFormation template (which only includes the Serverless S3 deployment bucket) to AWS.
-
-In the end it hooks into [`deploy:deploy`](/lib/plugins/deploy) lifecycle to update the previously created stack.
-
-The `resources` section of the `serverless.yml` file is parsed and merged into the CloudFormation template.
-This makes sure that custom resources the user has defined inside the `serverless.yml` file are added correctly.
-
-**Note:** Empty, but defined `Resources` or `Outputs` sections are set to an empty object before being merged.
-
-Next up it removes old service directories (with its files) in the services S3 bucket. After that it creates a new directory
-with the current time as the directory name in S3 and uploads the services artifacts (e.g. the .zip file and the CloudFormation
-file) in this directory. Furthermore it updates the stack with all the Resources which are defined in
-`serverless.service.resources.Resources` (this also includes the custom provider resources).
-
-The stack status is checked every 5 seconds with the help of the CloudFormation API. It will return a success message if
-the stack status is `CREATE_COMPLETE` or `UPDATE_COMPLETE` (depends if you deploy your service for the first time or
-redeploy it after making some changes).
diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js
index e150d834e..afe5fa160 100644
--- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js
+++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js
@@ -3,16 +3,375 @@
const BbPromise = require('bluebird');
const _ = require('lodash');
+const NOT_FOUND = -1;
+
module.exports = {
compileMethods() {
- const corsConfig = {};
+ const corsPreflight = {};
+
+ const defaultStatusCodes = {
+ 200: {
+ pattern: '',
+ },
+ 400: {
+ pattern: '.*\\[400\\].*',
+ },
+ 401: {
+ pattern: '.*\\[401\\].*',
+ },
+ 403: {
+ pattern: '.*\\[403\\].*',
+ },
+ 404: {
+ pattern: '.*\\[404\\].*',
+ },
+ 422: {
+ pattern: '.*\\[422\\].*',
+ },
+ 500: {
+ pattern: '.*(Process\\s?exited\\s?before\\s?completing\\s?request|\\[500\\]).*',
+ },
+ 502: {
+ pattern: '.*\\[502\\].*',
+ },
+ 504: {
+ pattern: '.*\\[504\\].*',
+ },
+ };
+ /**
+ * Private helper functions
+ */
+
+ const generateMethodResponseHeaders = (headers) => {
+ const methodResponseHeaders = {};
+
+ Object.keys(headers).forEach(header => {
+ methodResponseHeaders[`method.response.header.${header}`] = true;
+ });
+
+ return methodResponseHeaders;
+ };
+
+ const generateIntegrationResponseHeaders = (headers) => {
+ const integrationResponseHeaders = {};
+
+ Object.keys(headers).forEach(header => {
+ integrationResponseHeaders[`method.response.header.${header}`] = headers[header];
+ });
+
+ return integrationResponseHeaders;
+ };
+
+ const generateCorsPreflightConfig = (corsConfig, corsPreflightConfig, method) => {
+ const headers = [
+ 'Content-Type',
+ 'X-Amz-Date',
+ 'Authorization',
+ 'X-Api-Key',
+ 'X-Amz-Security-Token',
+ ];
+
+ let newCorsPreflightConfig;
+
+ const cors = {
+ origins: ['*'],
+ methods: ['OPTIONS'],
+ headers,
+ };
+
+ if (typeof corsConfig === 'object') {
+ Object.assign(cors, corsConfig);
+
+ cors.methods = [];
+ if (cors.headers) {
+ if (!Array.isArray(cors.headers)) {
+ const errorMessage = [
+ 'CORS header values must be provided as an array.',
+ ' Please check the docs for more info.',
+ ].join('');
+ throw new this.serverless.classes
+ .Error(errorMessage);
+ }
+ } else {
+ cors.headers = headers;
+ }
+
+ if (cors.methods.indexOf('OPTIONS') === NOT_FOUND) {
+ cors.methods.push('OPTIONS');
+ }
+
+ if (cors.methods.indexOf(method.toUpperCase()) === NOT_FOUND) {
+ cors.methods.push(method.toUpperCase());
+ }
+ } else {
+ cors.methods.push(method.toUpperCase());
+ }
+
+ if (corsPreflightConfig) {
+ cors.methods = _.union(cors.methods, corsPreflightConfig.methods);
+ cors.headers = _.union(cors.headers, corsPreflightConfig.headers);
+ cors.origins = _.union(cors.origins, corsPreflightConfig.origins);
+ newCorsPreflightConfig = _.merge(corsPreflightConfig, cors);
+ } else {
+ newCorsPreflightConfig = cors;
+ }
+
+ return newCorsPreflightConfig;
+ };
+
+ const hasDefaultStatusCode = (statusCodes) =>
+ Object.keys(statusCodes).some((statusCode) => (statusCodes[statusCode].pattern === ''));
+
+ const generateResponse = (responseConfig) => {
+ const response = {
+ methodResponses: [],
+ integrationResponses: [],
+ };
+
+ const statusCodes = {};
+ Object.assign(statusCodes, responseConfig.statusCodes);
+
+ if (!hasDefaultStatusCode(statusCodes)) {
+ _.merge(statusCodes, { 200: defaultStatusCodes['200'] });
+ }
+
+ Object.keys(statusCodes).forEach((statusCode) => {
+ const methodResponse = {
+ ResponseParameters: {},
+ ResponseModels: {},
+ StatusCode: parseInt(statusCode, 10),
+ };
+
+ const integrationResponse = {
+ StatusCode: parseInt(statusCode, 10),
+ SelectionPattern: statusCodes[statusCode].pattern || '',
+ ResponseParameters: {},
+ ResponseTemplates: {},
+ };
+
+ _.merge(methodResponse.ResponseParameters,
+ generateMethodResponseHeaders(responseConfig.methodResponseHeaders));
+ if (statusCodes[statusCode].headers) {
+ _.merge(methodResponse.ResponseParameters,
+ generateMethodResponseHeaders(statusCodes[statusCode].headers));
+ }
+
+ _.merge(integrationResponse.ResponseParameters,
+ generateIntegrationResponseHeaders(responseConfig.integrationResponseHeaders));
+ if (statusCodes[statusCode].headers) {
+ _.merge(integrationResponse.ResponseParameters,
+ generateIntegrationResponseHeaders(statusCodes[statusCode].headers));
+ }
+
+ if (responseConfig.integrationResponseTemplate) {
+ _.merge(integrationResponse.ResponseTemplates, {
+ 'application/json': responseConfig.integrationResponseTemplate,
+ });
+ }
+
+ if (statusCodes[statusCode].template) {
+ if (typeof statusCodes[statusCode].template === 'string') {
+ _.merge(integrationResponse.ResponseTemplates, {
+ 'application/json': statusCodes[statusCode].template,
+ });
+ } else {
+ _.merge(integrationResponse.ResponseTemplates, statusCodes[statusCode].template);
+ }
+ }
+
+ response.methodResponses.push(methodResponse);
+ response.integrationResponses.push(integrationResponse);
+ });
+
+ return response;
+ };
+
+ const hasRequestTemplate = (event) => {
+ // check if custom request configuration should be used
+ if (Boolean(event.http.request) === true) {
+ if (typeof event.http.request === 'object') {
+ // merge custom request templates if provided
+ if (Boolean(event.http.request.template) === true) {
+ if (typeof event.http.request.template === 'object') {
+ return true;
+ }
+
+ const errorMessage = [
+ 'Template config must be provided as an object.',
+ ' Please check the docs for more info.',
+ ].join('');
+ throw new this.serverless.classes.Error(errorMessage);
+ }
+ } else {
+ const errorMessage = [
+ 'Request config must be provided as an object.',
+ ' Please check the docs for more info.',
+ ].join('');
+ throw new this.serverless.classes.Error(errorMessage);
+ }
+ }
+
+ return false;
+ };
+
+ const hasRequestParameters = (event) => (event.http.request && event.http.request.parameters);
+
+ const hasPassThroughRequest = (event) => {
+ const requestPassThroughBehaviors = [
+ 'NEVER', 'WHEN_NO_MATCH', 'WHEN_NO_TEMPLATES',
+ ];
+
+ if (event.http.request && Boolean(event.http.request.passThrough) === true) {
+ if (requestPassThroughBehaviors.indexOf(event.http.request.passThrough) === -1) {
+ const errorMessage = [
+ 'Request passThrough "',
+ event.http.request.passThrough,
+ '" is not one of ',
+ requestPassThroughBehaviors.join(', '),
+ ].join('');
+
+ throw new this.serverless.classes.Error(errorMessage);
+ }
+
+ return true;
+ }
+
+ return false;
+ };
+
+ const hasCors = (event) => (Boolean(event.http.cors) === true);
+
+ const hasResponseTemplate = (event) => (event.http.response && event.http.response.template);
+
+ const hasResponseHeaders = (event) => {
+ // check if custom response configuration should be used
+ if (Boolean(event.http.response) === true) {
+ if (typeof event.http.response === 'object') {
+ // prepare the headers if set
+ if (Boolean(event.http.response.headers) === true) {
+ if (typeof event.http.response.headers === 'object') {
+ return true;
+ }
+
+ const errorMessage = [
+ 'Response headers must be provided as an object.',
+ ' Please check the docs for more info.',
+ ].join('');
+ throw new this.serverless.classes.Error(errorMessage);
+ }
+ } else {
+ const errorMessage = [
+ 'Response config must be provided as an object.',
+ ' Please check the docs for more info.',
+ ].join('');
+ throw new this.serverless.classes.Error(errorMessage);
+ }
+ }
+
+ return false;
+ };
+
+ const getAuthorizerName = (event) => {
+ let authorizerName;
+
+ if (typeof event.http.authorizer === 'string') {
+ if (event.http.authorizer.indexOf(':') === -1) {
+ authorizerName = event.http.authorizer;
+ } else {
+ const authorizerArn = event.http.authorizer;
+ const splittedAuthorizerArn = authorizerArn.split(':');
+ const splittedLambdaName = splittedAuthorizerArn[splittedAuthorizerArn
+ .length - 1].split('-');
+ authorizerName = splittedLambdaName[splittedLambdaName.length - 1];
+ }
+ } else if (typeof event.http.authorizer === 'object') {
+ if (event.http.authorizer.arn) {
+ const authorizerArn = event.http.authorizer.arn;
+ const splittedAuthorizerArn = authorizerArn.split(':');
+ const splittedLambdaName = splittedAuthorizerArn[splittedAuthorizerArn
+ .length - 1].split('-');
+ authorizerName = splittedLambdaName[splittedLambdaName.length - 1];
+ } else if (event.http.authorizer.name) {
+ authorizerName = event.http.authorizer.name;
+ }
+ }
+
+ return authorizerName[0].toUpperCase() + authorizerName.substr(1);
+ };
+
+ const configurePreflightMethods = (corsConfig, logicalIds) => {
+ const preflightMethods = {};
+
+ _.forOwn(corsConfig, (config, path) => {
+ const resourceLogicalId = logicalIds[path];
+
+ const preflightHeaders = {
+ 'Access-Control-Allow-Origin': `'${config.origins.join(',')}'`,
+ 'Access-Control-Allow-Headers': `'${config.headers.join(',')}'`,
+ 'Access-Control-Allow-Methods': `'${config.methods.join(',')}'`,
+ };
+
+ const preflightMethodResponse = generateMethodResponseHeaders(preflightHeaders);
+ const preflightIntegrationResponse = generateIntegrationResponseHeaders(preflightHeaders);
+
+ const preflightTemplate = `
+ {
+ "Type" : "AWS::ApiGateway::Method",
+ "Properties" : {
+ "AuthorizationType" : "NONE",
+ "HttpMethod" : "OPTIONS",
+ "MethodResponses" : [
+ {
+ "ResponseModels" : {},
+ "ResponseParameters" : ${JSON.stringify(preflightMethodResponse)},
+ "StatusCode" : "200"
+ }
+ ],
+ "RequestParameters" : {},
+ "Integration" : {
+ "Type" : "MOCK",
+ "RequestTemplates" : {
+ "application/json": "{statusCode:200}"
+ },
+ "IntegrationResponses" : [
+ {
+ "StatusCode" : "200",
+ "ResponseParameters" : ${JSON.stringify(preflightIntegrationResponse)},
+ "ResponseTemplates" : {
+ "application/json": ""
+ }
+ }
+ ]
+ },
+ "ResourceId" : { "Ref": "${resourceLogicalId}" },
+ "RestApiId" : { "Ref": "ApiGatewayRestApi" }
+ }
+ }
+ `;
+ const extractedResourceId = resourceLogicalId.match(/ApiGatewayResource(.*)/)[1];
+
+ _.merge(preflightMethods, {
+ [`ApiGatewayMethod${extractedResourceId}Options`]:
+ JSON.parse(preflightTemplate),
+ });
+ });
+
+ return preflightMethods;
+ };
+
+ /**
+ * Lets start the real work now!
+ */
_.forEach(this.serverless.service.functions, (functionObject, functionName) => {
functionObject.events.forEach(event => {
if (event.http) {
let method;
let path;
let requestPassThroughBehavior = 'NEVER';
+ let integrationType = 'AWS_PROXY';
+ let integrationResponseTemplate = null;
+ // Validate HTTP event object
if (typeof event.http === 'object') {
method = event.http.method;
path = event.http.path;
@@ -30,7 +389,8 @@ module.exports = {
.Error(errorMessage);
}
- // add default request templates
+ // Templates required to generate the cloudformation config
+
const DEFAULT_JSON_REQUEST_TEMPLATE = `
#define( $loop )
{
@@ -72,9 +432,9 @@ module.exports = {
#set( $keyVal = $token.split('=') )
#set( $keyValSize = $keyVal.size() )
#if( $keyValSize >= 1 )
- #set( $key = $util.urlDecode($keyVal[0]) )
+ #set( $key = $util.escapeJavaScript($util.urlDecode($keyVal[0])) )
#if( $keyValSize >= 2 )
- #set( $val = $util.urlDecode($keyVal[1]) )
+ #set( $val = $util.escapeJavaScript($util.urlDecode($keyVal[1])) )
#else
#set( $val = '' )
#end
@@ -117,230 +477,134 @@ module.exports = {
}
`;
+ // default integration request templates
const integrationRequestTemplates = {
'application/json': DEFAULT_JSON_REQUEST_TEMPLATE,
'application/x-www-form-urlencoded': DEFAULT_FORM_URL_ENCODED_REQUEST_TEMPLATE,
};
- const requestPassThroughBehaviors = [
- 'NEVER', 'WHEN_NO_MATCH', 'WHEN_NO_TEMPLATES',
- ];
-
- // check if custom request configuration should be used
- if (Boolean(event.http.request) === true) {
- if (typeof event.http.request === 'object') {
- // merge custom request templates if provided
- if (Boolean(event.http.request.template) === true) {
- if (typeof event.http.request.template === 'object') {
- _.forEach(event.http.request.template, (value, key) => {
- const requestTemplate = {};
- requestTemplate[key] = value;
- _.merge(integrationRequestTemplates, requestTemplate);
- });
- } else {
- const errorMessage = [
- 'Template config must be provided as an object.',
- ' Please check the docs for more info.',
- ].join('');
- throw new this.serverless.classes.Error(errorMessage);
- }
- }
- } else {
- const errorMessage = [
- 'Request config must be provided as an object.',
- ' Please check the docs for more info.',
- ].join('');
- throw new this.serverless.classes.Error(errorMessage);
- }
-
- if (Boolean(event.http.request.passThrough) === true) {
- if (requestPassThroughBehaviors.indexOf(event.http.request.passThrough) === -1) {
- const errorMessage = [
- 'Request passThrough "',
- event.http.request.passThrough,
- '" is not one of ',
- requestPassThroughBehaviors.join(', '),
- ].join('');
-
- throw new this.serverless.classes.Error(errorMessage);
- }
-
- requestPassThroughBehavior = event.http.request.passThrough;
- }
- }
-
- // setup CORS
- let cors;
- let corsEnabled = false;
-
- if (Boolean(event.http.cors) === true) {
- corsEnabled = true;
- const headers = [
- 'Content-Type',
- 'X-Amz-Date',
- 'Authorization',
- 'X-Api-Key',
- 'X-Amz-Security-Token'];
-
- cors = {
- origins: ['*'],
- methods: ['OPTIONS'],
- headers,
- };
-
- if (typeof event.http.cors === 'object') {
- cors = event.http.cors;
- cors.methods = [];
- if (cors.headers) {
- if (!Array.isArray(cors.headers)) {
- const errorMessage = [
- 'CORS header values must be provided as an array.',
- ' Please check the docs for more info.',
- ].join('');
- throw new this.serverless.classes
- .Error(errorMessage);
- }
- } else {
- cors.headers = headers;
- }
-
- if (!cors.methods.indexOf('OPTIONS') > -1) {
- cors.methods.push('OPTIONS');
- }
-
- if (!cors.methods.indexOf(method.toUpperCase()) > -1) {
- cors.methods.push(method.toUpperCase());
- }
- } else {
- cors.methods.push(method.toUpperCase());
- }
-
- if (corsConfig[path]) {
- cors.methods = _.union(cors.methods, corsConfig[path].methods);
- corsConfig[path] = _.merge(corsConfig[path], cors);
- } else {
- corsConfig[path] = cors;
- }
- }
-
+ // configuring logical names for resources
const resourceLogicalId = this.resourceLogicalIds[path];
const normalizedMethod = method[0].toUpperCase() +
method.substr(1).toLowerCase();
const extractedResourceId = resourceLogicalId.match(/ApiGatewayResource(.*)/)[1];
+ const normalizedFunctionName = functionName[0].toUpperCase()
+ + functionName.substr(1);
- // default response configuration
+ // scaffolds for method responses headers
const methodResponseHeaders = [];
const integrationResponseHeaders = [];
- let integrationResponseTemplate = null;
+ const requestParameters = {};
- // check if custom response configuration should be used
- if (Boolean(event.http.response) === true) {
- if (typeof event.http.response === 'object') {
- // prepare the headers if set
- if (Boolean(event.http.response.headers) === true) {
- if (typeof event.http.response.headers === 'object') {
- _.forEach(event.http.response.headers, (value, key) => {
- const methodResponseHeader = {};
- methodResponseHeader[`method.response.header.${key}`] =
- `method.response.header.${value.toString()}`;
- methodResponseHeaders.push(methodResponseHeader);
-
- const integrationResponseHeader = {};
- integrationResponseHeader[`method.response.header.${key}`] =
- `${value}`;
- integrationResponseHeaders.push(integrationResponseHeader);
- });
- } else {
- const errorMessage = [
- 'Response headers must be provided as an object.',
- ' Please check the docs for more info.',
- ].join('');
- throw new this.serverless.classes.Error(errorMessage);
- }
- }
- integrationResponseTemplate = event.http.response.template;
- } else {
- const errorMessage = [
- 'Response config must be provided as an object.',
- ' Please check the docs for more info.',
- ].join('');
- throw new this.serverless.classes.Error(errorMessage);
- }
- }
-
- // scaffolds for method responses
- const methodResponses = [
- {
- ResponseModels: {},
- ResponseParameters: {},
- StatusCode: 200,
- },
- ];
-
- const integrationResponses = [
- {
- StatusCode: 200,
- ResponseParameters: {},
- ResponseTemplates: {},
- },
- ];
-
- // merge the response configuration
- methodResponseHeaders.forEach((header) => {
- _.merge(methodResponses[0].ResponseParameters, header);
- });
- integrationResponseHeaders.forEach((header) => {
- _.merge(integrationResponses[0].ResponseParameters, header);
- });
- if (integrationResponseTemplate) {
- _.merge(integrationResponses[0].ResponseTemplates, {
- 'application/json': integrationResponseTemplate,
+ // 1. Has request template
+ if (hasRequestTemplate(event)) {
+ _.forEach(event.http.request.template, (value, key) => {
+ const requestTemplate = {};
+ requestTemplate[key] = value;
+ _.merge(integrationRequestTemplates, requestTemplate);
});
}
- if (corsEnabled) {
- const corsMethodResponseParameter = {
- 'method.response.header.Access-Control-Allow-Origin':
- 'method.response.header.Access-Control-Allow-Origin',
- };
-
- const corsIntegrationResponseParameter = {
- 'method.response.header.Access-Control-Allow-Origin':
- `'${cors.origins.join('\',\'')}'`,
- };
-
- _.merge(methodResponses[0].ResponseParameters, corsMethodResponseParameter);
- _.merge(integrationResponses[0].ResponseParameters, corsIntegrationResponseParameter);
+ if (hasRequestParameters(event)) {
+ // only these locations are currently supported
+ const locations = ['querystrings', 'paths', 'headers'];
+ _.each(locations, (location) => {
+ // strip the plural s
+ const singular = location.substring(0, location.length - 1);
+ _.each(event.http.request.parameters[location], (value, key) => {
+ requestParameters[`method.request.${singular}.${key}`] = value;
+ });
+ });
}
- // add default status codes
- methodResponses.push(
- { StatusCode: 400 },
- { StatusCode: 401 },
- { StatusCode: 403 },
- { StatusCode: 404 },
- { StatusCode: 422 },
- { StatusCode: 500 },
- { StatusCode: 502 },
- { StatusCode: 504 }
- );
+ // 2. Has pass-through options
+ if (hasPassThroughRequest(event)) {
+ requestPassThroughBehavior = event.http.request.passThrough;
+ }
- integrationResponses.push(
- { StatusCode: 400, SelectionPattern: '.*\\[400\\].*' },
- { StatusCode: 401, SelectionPattern: '.*\\[401\\].*' },
- { StatusCode: 403, SelectionPattern: '.*\\[403\\].*' },
- { StatusCode: 404, SelectionPattern: '.*\\[404\\].*' },
- { StatusCode: 422, SelectionPattern: '.*\\[422\\].*' },
- { StatusCode: 500,
- SelectionPattern:
- // eslint-disable-next-line max-len
- '.*(Process\\s?exited\\s?before\\s?completing\\s?request|Task\\s?timed\\s?out\\s?|\\[500\\]).*' },
- { StatusCode: 502, SelectionPattern: '.*\\[502\\].*' },
- { StatusCode: 504, SelectionPattern: '.*\\[504\\].*' }
- );
+ // 3. Has response template
+ if (hasResponseTemplate(event)) {
+ integrationResponseTemplate = event.http.response.template;
+ }
- const normalizedFunctionName = functionName[0].toUpperCase()
- + functionName.substr(1);
+ // 4. Has CORS enabled?
+ if (hasCors(event)) {
+ corsPreflight[path] = generateCorsPreflightConfig(event.http.cors,
+ corsPreflight[path], method);
+
+ const corsHeader = {
+ 'Access-Control-Allow-Origin':
+ `'${corsPreflight[path].origins.join('\',\'')}'`,
+ };
+
+ _.merge(methodResponseHeaders, corsHeader);
+ _.merge(integrationResponseHeaders, corsHeader);
+ }
+
+ // Sort out response headers
+ if (hasResponseHeaders(event)) {
+ _.merge(methodResponseHeaders, event.http.response.headers);
+ _.merge(integrationResponseHeaders, event.http.response.headers);
+ }
+
+ // Sort out response config
+ const responseConfig = {
+ methodResponseHeaders,
+ integrationResponseHeaders,
+ integrationResponseTemplate,
+ };
+
+ // Merge in any custom response config
+ if (event.http.response && event.http.response.statusCodes) {
+ responseConfig.statusCodes = event.http.response.statusCodes;
+ } else {
+ responseConfig.statusCodes = defaultStatusCodes;
+ }
+
+ const response = generateResponse(responseConfig);
+
+ // check if LAMBDA or LAMBDA-PROXY was used for the integration type
+ if (typeof event.http === 'object') {
+ if (Boolean(event.http.integration) === true) {
+ // normalize the integration for further processing
+ const normalizedIntegration = event.http.integration.toUpperCase();
+ // check if the user has entered a non-valid integration
+ const allowedIntegrations = [
+ 'LAMBDA', 'LAMBDA-PROXY',
+ ];
+ if (allowedIntegrations.indexOf(normalizedIntegration) === -1) {
+ const errorMessage = [
+ `Invalid APIG integration "${event.http.integration}"`,
+ ` in function "${functionName}".`,
+ ' Supported integrations are: lambda, lambda-proxy.',
+ ].join('');
+ throw new this.serverless.classes.Error(errorMessage);
+ }
+ // map the Serverless integration to the corresponding CloudFormation types
+ // LAMBDA --> AWS
+ // LAMBDA-PROXY --> AWS_PROXY
+ if (normalizedIntegration === 'LAMBDA') {
+ integrationType = 'AWS';
+ } else if (normalizedIntegration === 'LAMBDA-PROXY') {
+ integrationType = 'AWS_PROXY';
+ } else {
+ // default to AWS_PROXY (just in case...)
+ integrationType = 'AWS_PROXY';
+ }
+ }
+ }
+
+ // show a warning when request / response config is used with AWS_PROXY (LAMBDA-PROXY)
+ if (integrationType === 'AWS_PROXY' && (
+ (!!event.http.request) || (!!event.http.response)
+ )) {
+ const warningMessage = [
+ 'Warning! You\'re using the LAMBDA-PROXY in combination with request / response',
+ ` configuration in your function "${functionName}".`,
+ ' This configuration will be ignored during deployment.',
+ ].join('');
+ this.serverless.cli.log(warningMessage);
+ }
const methodTemplate = `
{
@@ -348,11 +612,11 @@ module.exports = {
"Properties" : {
"AuthorizationType" : "NONE",
"HttpMethod" : "${method.toUpperCase()}",
- "MethodResponses" : ${JSON.stringify(methodResponses)},
- "RequestParameters" : {},
+ "MethodResponses" : ${JSON.stringify(response.methodResponses)},
+ "RequestParameters" : ${JSON.stringify(requestParameters)},
"Integration" : {
"IntegrationHttpMethod" : "POST",
- "Type" : "AWS",
+ "Type" : "${integrationType}",
"Uri" : {
"Fn::Join": [ "",
[
@@ -366,7 +630,7 @@ module.exports = {
},
"RequestTemplates" : ${JSON.stringify(integrationRequestTemplates)},
"PassthroughBehavior": "${requestPassThroughBehavior}",
- "IntegrationResponses" : ${JSON.stringify(integrationResponses)}
+ "IntegrationResponses" : ${JSON.stringify(response.integrationResponses)}
},
"ResourceId" : { "Ref": "${resourceLogicalId}" },
"RestApiId" : { "Ref": "ApiGatewayRestApi" }
@@ -378,34 +642,9 @@ module.exports = {
// set authorizer config if available
if (event.http.authorizer) {
- let authorizerName;
- if (typeof event.http.authorizer === 'string') {
- if (event.http.authorizer.indexOf(':') === -1) {
- authorizerName = event.http.authorizer;
- } else {
- const authorizerArn = event.http.authorizer;
- const splittedAuthorizerArn = authorizerArn.split(':');
- const splittedLambdaName = splittedAuthorizerArn[splittedAuthorizerArn
- .length - 1].split('-');
- authorizerName = splittedLambdaName[splittedLambdaName.length - 1];
- }
- } else if (typeof event.http.authorizer === 'object') {
- if (event.http.authorizer.arn) {
- const authorizerArn = event.http.authorizer.arn;
- const splittedAuthorizerArn = authorizerArn.split(':');
- const splittedLambdaName = splittedAuthorizerArn[splittedAuthorizerArn
- .length - 1].split('-');
- authorizerName = splittedLambdaName[splittedLambdaName.length - 1];
- } else if (event.http.authorizer.name) {
- authorizerName = event.http.authorizer.name;
- }
- }
+ const authorizerName = getAuthorizerName(event);
- const normalizedAuthorizerName = authorizerName[0]
- .toUpperCase() + authorizerName.substr(1);
-
- const AuthorizerLogicalId = `${
- normalizedAuthorizerName}ApiGatewayAuthorizer`;
+ const AuthorizerLogicalId = `${authorizerName}ApiGatewayAuthorizer`;
methodTemplateJson.Properties.AuthorizationType = 'CUSTOM';
methodTemplateJson.Properties.AuthorizerId = {
@@ -437,76 +676,10 @@ module.exports = {
});
});
- // If no paths have CORS settings, then CORS isn't required.
- if (!_.isEmpty(corsConfig)) {
- const allowOrigin = '"method.response.header.Access-Control-Allow-Origin"';
- const allowHeaders = '"method.response.header.Access-Control-Allow-Headers"';
- const allowMethods = '"method.response.header.Access-Control-Allow-Methods"';
-
- const preflightMethodResponse = `
- ${allowOrigin}: true,
- ${allowHeaders}: true,
- ${allowMethods}: true
- `;
-
- _.forOwn(corsConfig, (config, path) => {
- const resourceLogicalId = this.resourceLogicalIds[path];
- const preflightIntegrationResponse =
- `
- ${allowOrigin}: "'${config.origins.join(',')}'",
- ${allowHeaders}: "'${config.headers.join(',')}'",
- ${allowMethods}: "'${config.methods.join(',')}'"
- `;
-
- const preflightTemplate = `
- {
- "Type" : "AWS::ApiGateway::Method",
- "Properties" : {
- "AuthorizationType" : "NONE",
- "HttpMethod" : "OPTIONS",
- "MethodResponses" : [
- {
- "ResponseModels" : {},
- "ResponseParameters" : {
- ${preflightMethodResponse}
- },
- "StatusCode" : "200"
- }
- ],
- "RequestParameters" : {},
- "Integration" : {
- "Type" : "MOCK",
- "RequestTemplates" : {
- "application/json": "{statusCode:200}"
- },
- "IntegrationResponses" : [
- {
- "StatusCode" : "200",
- "ResponseParameters" : {
- ${preflightIntegrationResponse}
- },
- "ResponseTemplates" : {
- "application/json": ""
- }
- }
- ]
- },
- "ResourceId" : { "Ref": "${resourceLogicalId}" },
- "RestApiId" : { "Ref": "ApiGatewayRestApi" }
- }
- }
- `;
-
- const extractedResourceId = resourceLogicalId.match(/ApiGatewayResource(.*)/)[1];
-
- const preflightObject = {
- [`ApiGatewayMethod${extractedResourceId}Options`]:
- JSON.parse(preflightTemplate),
- };
-
- _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
- preflightObject);
- });
+ if (!_.isEmpty(corsPreflight)) {
+ // If we have some CORS config. configure the preflight method and merge
+ _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
+ configurePreflightMethods(corsPreflight, this.resourceLogicalIds));
}
return BbPromise.resolve();
diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/tests/methods.js b/lib/plugins/aws/deploy/compile/events/apiGateway/tests/methods.js
index ab169a668..948a68d0e 100644
--- a/lib/plugins/aws/deploy/compile/events/apiGateway/tests/methods.js
+++ b/lib/plugins/aws/deploy/compile/events/apiGateway/tests/methods.js
@@ -1,6 +1,7 @@
'use strict';
const expect = require('chai').expect;
+const sinon = require('sinon');
const AwsCompileApigEvents = require('../index');
const Serverless = require('../../../../../../../Serverless');
@@ -88,6 +89,62 @@ describe('#compileMethods()', () => {
expect(() => awsCompileApigEvents.compileMethods()).to.throw(Error);
});
+ it('should have request parameters defined when they are set', () => {
+ awsCompileApigEvents.serverless.service.functions.first.events[0].http.integration = 'lambda';
+
+ const requestConfig = {
+ parameters: {
+ querystrings: {
+ foo: true,
+ bar: false,
+ },
+ headers: {
+ foo: true,
+ bar: false,
+ },
+ paths: {
+ foo: true,
+ bar: false,
+ },
+ },
+ };
+
+ awsCompileApigEvents.serverless.service.functions.first.events[0].http.request = requestConfig;
+
+ awsCompileApigEvents.compileMethods().then(() => {
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersCreatePost.Properties
+ .RequestParameters['method.request.header.foo']
+ ).to.equal(true);
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersCreatePost.Properties
+ .RequestParameters['method.request.header.bar']
+ ).to.equal(false);
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersCreatePost.Properties
+ .RequestParameters['method.request.querystring.foo']
+ ).to.equal(true);
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersCreatePost.Properties
+ .RequestParameters['method.request.querystring.bar']
+ ).to.equal(false);
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersCreatePost.Properties
+ .RequestParameters['method.request.path.foo']
+ ).to.equal(true);
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersCreatePost.Properties
+ .RequestParameters['method.request.path.bar']
+ ).to.equal(false);
+ });
+ });
+
it('should create method resources when http events given', () => awsCompileApigEvents
.compileMethods().then(() => {
expect(
@@ -233,9 +290,41 @@ describe('#compileMethods()', () => {
});
});
- it('should add CORS origins to method only when CORS is enabled', () => {
+ it('should add CORS origins to method only when CORS and LAMBDA integration are enabled', () => {
const origin = '\'*\'';
+ awsCompileApigEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ http: {
+ path: 'users/create',
+ method: 'POST',
+ integration: 'lambda',
+ cors: true,
+ },
+ },
+ {
+ http: {
+ path: 'users/list',
+ method: 'GET',
+ integration: 'lambda',
+ },
+ },
+ {
+ http: {
+ path: 'users/update',
+ method: 'PUT',
+ integration: 'lambda',
+ cors: {
+ origins: ['*'],
+ },
+ },
+ },
+ ],
+ },
+ };
+
return awsCompileApigEvents.compileMethods().then(() => {
// Check origin.
expect(
@@ -332,32 +421,148 @@ describe('#compileMethods()', () => {
});
});
+ it('should merge all preflight origins, method, and headers for a path', () => {
+ awsCompileApigEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ http: {
+ method: 'GET',
+ path: 'users',
+ cors: {
+ origins: [
+ 'http://example.com',
+ ],
+ },
+ },
+ }, {
+ http: {
+ method: 'POST',
+ path: 'users',
+ cors: {
+ origins: [
+ 'http://example2.com',
+ ],
+ },
+ },
+ }, {
+ http: {
+ method: 'PUT',
+ path: 'users/{id}',
+ cors: {
+ headers: [
+ 'TestHeader',
+ ],
+ },
+ },
+ }, {
+ http: {
+ method: 'DELETE',
+ path: 'users/{id}',
+ cors: {
+ headers: [
+ 'TestHeader2',
+ ],
+ },
+ },
+ },
+ ],
+ },
+ };
+ awsCompileApigEvents.resourceLogicalIds = {
+ users: 'ApiGatewayResourceUsers',
+ 'users/{id}': 'ApiGatewayResourceUsersid',
+ };
+ return awsCompileApigEvents.compileMethods().then(() => {
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersidOptions
+ .Properties.Integration.IntegrationResponses[0]
+ .ResponseParameters['method.response.header.Access-Control-Allow-Methods']
+ ).to.equal('\'OPTIONS,DELETE,PUT\'');
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersOptions
+ .Properties.Integration.IntegrationResponses[0]
+ .ResponseParameters['method.response.header.Access-Control-Allow-Origin']
+ ).to.equal('\'http://example2.com,http://example.com\'');
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersidOptions
+ .Properties.Integration.IntegrationResponses[0]
+ .ResponseParameters['method.response.header.Access-Control-Allow-Headers']
+ ).to.equal('\'TestHeader2,TestHeader\'');
+ });
+ });
+
describe('when dealing with request configuration', () => {
- it('should setup a default "application/json" template', () =>
- awsCompileApigEvents.compileMethods().then(() => {
+ it('should setup a default "application/json" template', () => {
+ awsCompileApigEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ http: {
+ method: 'GET',
+ path: 'users/list',
+ integration: 'lambda',
+ },
+ },
+ ],
+ },
+ };
+
+ return awsCompileApigEvents.compileMethods().then(() => {
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersListGet.Properties
.Integration.RequestTemplates['application/json']
).to.have.length.above(0);
- })
- );
+ });
+ });
- it('should setup a default "application/x-www-form-urlencoded" template', () =>
- awsCompileApigEvents.compileMethods().then(() => {
+ it('should setup a default "application/x-www-form-urlencoded" template', () => {
+ awsCompileApigEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ http: {
+ method: 'GET',
+ path: 'users/list',
+ integration: 'lambda',
+ },
+ },
+ ],
+ },
+ };
+
+ return awsCompileApigEvents.compileMethods().then(() => {
expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersListGet.Properties
.Integration.RequestTemplates['application/x-www-form-urlencoded']
).to.have.length.above(0);
- })
- );
+ });
+ });
- it('should use the default request pass-through behavior when none specified', () =>
- awsCompileApigEvents.compileMethods().then(() => {
- expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
- .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.PassthroughBehavior
- ).to.equal('NEVER');
- })
- );
+ it('should use the default request pass-through behavior when none specified', () => {
+ awsCompileApigEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ http: {
+ method: 'GET',
+ path: 'users/list',
+ integration: 'lambda',
+ },
+ },
+ ],
+ },
+ };
+
+ return awsCompileApigEvents.compileMethods().then(() => {
+ expect(awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.PassthroughBehavior
+ ).to.equal('NEVER');
+ });
+ });
it('should use defined pass-through behavior', () => {
awsCompileApigEvents.serverless.service.functions = {
@@ -367,6 +572,7 @@ describe('#compileMethods()', () => {
http: {
method: 'GET',
path: 'users/list',
+ integration: 'lambda',
request: {
passThrough: 'WHEN_NO_TEMPLATES',
},
@@ -391,6 +597,7 @@ describe('#compileMethods()', () => {
http: {
method: 'GET',
path: 'users/list',
+ integration: 'lambda',
request: {
passThrough: 'BOGUS',
},
@@ -411,6 +618,7 @@ describe('#compileMethods()', () => {
http: {
method: 'GET',
path: 'users/list',
+ integration: 'lambda',
request: {
template: {
'template/1': '{ "stage" : "$context.stage" }',
@@ -444,6 +652,7 @@ describe('#compileMethods()', () => {
http: {
method: 'GET',
path: 'users/list',
+ integration: 'lambda',
request: {
template: {
'application/json': 'overwritten-request-template-content',
@@ -471,6 +680,7 @@ describe('#compileMethods()', () => {
http: {
method: 'GET',
path: 'users/list',
+ integration: 'lambda',
request: 'some string',
},
},
@@ -489,6 +699,7 @@ describe('#compileMethods()', () => {
http: {
method: 'GET',
path: 'users/list',
+ integration: 'lambda',
request: {
template: 'some string',
},
@@ -511,6 +722,7 @@ describe('#compileMethods()', () => {
http: {
method: 'GET',
path: 'users/list',
+ integration: 'lambda',
response: {
headers: {
'Content-Type': "'text/plain'",
@@ -545,6 +757,7 @@ describe('#compileMethods()', () => {
http: {
method: 'GET',
path: 'users/list',
+ integration: 'lambda',
response: {
template: "$input.path('$.foo')",
},
@@ -571,6 +784,7 @@ describe('#compileMethods()', () => {
http: {
method: 'GET',
path: 'users/list',
+ integration: 'lambda',
response: 'some string',
},
},
@@ -589,6 +803,7 @@ describe('#compileMethods()', () => {
http: {
method: 'GET',
path: 'users/list',
+ integration: 'lambda',
response: {
headers: 'some string',
},
@@ -602,8 +817,22 @@ describe('#compileMethods()', () => {
});
});
- it('should add method responses for different status codes', () =>
- awsCompileApigEvents.compileMethods().then(() => {
+ it('should add method responses for different status codes', () => {
+ awsCompileApigEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ http: {
+ method: 'GET',
+ path: 'users/list',
+ integration: 'lambda',
+ },
+ },
+ ],
+ },
+ };
+
+ return awsCompileApigEvents.compileMethods().then(() => {
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersListGet.Properties.MethodResponses[1].StatusCode
@@ -636,46 +865,421 @@ describe('#compileMethods()', () => {
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersListGet.Properties.MethodResponses[8].StatusCode
).to.equal(504);
- })
- );
+ });
+ });
- it('should add integration responses for different status codes', () =>
- awsCompileApigEvents.compileMethods().then(() => {
+ it('should add integration responses for different status codes', () => {
+ awsCompileApigEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ http: {
+ method: 'GET',
+ path: 'users/list',
+ integration: 'lambda',
+ },
+ },
+ ],
+ },
+ };
+
+ return awsCompileApigEvents.compileMethods().then(() => {
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1]
- ).to.deep.equal({ StatusCode: 400, SelectionPattern: '.*\\[400\\].*' });
+ ).to.deep.equal({
+ StatusCode: 400,
+ SelectionPattern: '.*\\[400\\].*',
+ ResponseParameters: {},
+ ResponseTemplates: {},
+ });
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[2]
- ).to.deep.equal({ StatusCode: 401, SelectionPattern: '.*\\[401\\].*' });
+ ).to.deep.equal({
+ StatusCode: 401,
+ SelectionPattern: '.*\\[401\\].*',
+ ResponseParameters: {},
+ ResponseTemplates: {},
+ });
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[3]
- ).to.deep.equal({ StatusCode: 403, SelectionPattern: '.*\\[403\\].*' });
+ ).to.deep.equal({
+ StatusCode: 403,
+ SelectionPattern: '.*\\[403\\].*',
+ ResponseParameters: {},
+ ResponseTemplates: {},
+ });
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[4]
- ).to.deep.equal({ StatusCode: 404, SelectionPattern: '.*\\[404\\].*' });
+ ).to.deep.equal({
+ StatusCode: 404,
+ SelectionPattern: '.*\\[404\\].*',
+ ResponseParameters: {},
+ ResponseTemplates: {},
+ });
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[5]
- ).to.deep.equal({ StatusCode: 422, SelectionPattern: '.*\\[422\\].*' });
+ ).to.deep.equal({
+ StatusCode: 422,
+ SelectionPattern: '.*\\[422\\].*',
+ ResponseParameters: {},
+ ResponseTemplates: {},
+ });
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[6]
- ).to.deep.equal({ StatusCode: 500,
- SelectionPattern:
- // eslint-disable-next-line max-len
- '.*(Process\\s?exited\\s?before\\s?completing\\s?request|Task\\s?timed\\s?out\\s?|\\[500\\]).*' });
+ ).to.deep.equal({
+ StatusCode: 500,
+ SelectionPattern: '.*(Process\\s?exited\\s?before\\s?completing\\s?request|\\[500\\]).*',
+ ResponseParameters: {},
+ ResponseTemplates: {},
+ });
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[7]
- ).to.deep.equal({ StatusCode: 502, SelectionPattern: '.*\\[502\\].*' });
+ ).to.deep.equal({
+ StatusCode: 502,
+ SelectionPattern: '.*\\[502\\].*',
+ ResponseParameters: {},
+ ResponseTemplates: {},
+ });
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[8]
- ).to.deep.equal({ StatusCode: 504, SelectionPattern: '.*\\[504\\].*' });
+ ).to.deep.equal({
+ StatusCode: 504,
+ SelectionPattern: '.*\\[504\\].*',
+ ResponseParameters: {},
+ ResponseTemplates: {},
+ });
+ });
+ });
+
+ it('should set "AWS_PROXY" as the default integration type', () =>
+ awsCompileApigEvents.compileMethods().then(() => {
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.Type
+ ).to.equal('AWS_PROXY');
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.Type
+ ).to.equal('AWS_PROXY');
})
);
+
+ it('should set users integration type if specified', () => {
+ awsCompileApigEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ http: {
+ method: 'GET',
+ path: 'users/list',
+ integration: 'lambda',
+ },
+ },
+ {
+ http: {
+ path: 'users/create',
+ method: 'POST',
+ integration: 'LAMBDA-PROXY', // this time use uppercase syntax
+ },
+ },
+ ],
+ },
+ };
+
+ return awsCompileApigEvents.compileMethods().then(() => {
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.Type
+ ).to.equal('AWS');
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersCreatePost.Properties.Integration.Type
+ ).to.equal('AWS_PROXY');
+ });
+ });
+
+ it('should throw an error when an invalid integration type was provided', () => {
+ awsCompileApigEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ http: {
+ method: 'GET',
+ path: 'users/list',
+ integration: 'INVALID',
+ },
+ },
+ ],
+ },
+ };
+
+ expect(() => awsCompileApigEvents.compileMethods()).to.throw(Error);
+ });
+
+ it('should show a warning message when using request / response config with LAMBDA-PROXY', () => {
+ // initialize so we get the log method from the CLI in place
+ serverless.init();
+
+ const logStub = sinon.stub(serverless.cli, 'log');
+
+ awsCompileApigEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ http: {
+ method: 'get',
+ path: 'users/list',
+ integration: 'lambda-proxy', // can be removed as it defaults to this
+ request: {
+ passThrough: 'NEVER',
+ template: {
+ 'template/1': '{ "stage" : "$context.stage" }',
+ 'template/2': '{ "httpMethod" : "$context.httpMethod" }',
+ },
+ },
+ response: {
+ template: "$input.path('$.foo')",
+ },
+ },
+ },
+ ],
+ },
+ };
+
+ return awsCompileApigEvents.compileMethods().then(() => {
+ expect(logStub.calledOnce).to.be.equal(true);
+ expect(logStub.args[0][0].length).to.be.at.least(1);
+ });
+ });
+
+ it('should add custom response codes', () => {
+ awsCompileApigEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ http: {
+ method: 'GET',
+ path: 'users/list',
+ integration: 'lambda',
+ response: {
+ template: '$input.path(\'$.foo\')',
+ headers: {
+ 'Content-Type': 'text/csv',
+ },
+ statusCodes: {
+ 404: {
+ pattern: '.*"statusCode":404,.*',
+ template: '$input.path(\'$.errorMessage\')',
+ headers: {
+ 'Content-Type': 'text/html',
+ },
+ },
+ },
+ },
+ },
+ },
+ ],
+ },
+ };
+
+ return awsCompileApigEvents.compileMethods().then(() => {
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0]
+ .ResponseTemplates['application/json']
+ ).to.equal("$input.path('$.foo')");
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0]
+ .SelectionPattern
+ ).to.equal('');
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0]
+ .ResponseParameters['method.response.header.Content-Type']
+ ).to.equal('text/csv');
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1]
+ .ResponseTemplates['application/json']
+ ).to.equal("$input.path('$.errorMessage')");
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1]
+ .SelectionPattern
+ ).to.equal('.*"statusCode":404,.*');
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1]
+ .ResponseParameters['method.response.header.Content-Type']
+ ).to.equal('text/html');
+ });
+ });
+
+ it('should add multiple response templates for a custom response codes', () => {
+ awsCompileApigEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ http: {
+ method: 'GET',
+ path: 'users/list',
+ integration: 'lambda',
+ response: {
+ template: '$input.path(\'$.foo\')',
+ headers: {
+ 'Content-Type': 'text/csv',
+ },
+ statusCodes: {
+ 404: {
+ pattern: '.*"statusCode":404,.*',
+ template: {
+ 'application/json': '$input.path(\'$.errorMessage\')',
+ 'application/xml': '$input.path(\'$.xml.errorMessage\')',
+ },
+ headers: {
+ 'Content-Type': 'text/html',
+ },
+ },
+ },
+ },
+ },
+ },
+ ],
+ },
+ };
+
+ return awsCompileApigEvents.compileMethods().then(() => {
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0]
+ .ResponseTemplates['application/json']
+ ).to.equal("$input.path('$.foo')");
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0]
+ .SelectionPattern
+ ).to.equal('');
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0]
+ .ResponseParameters['method.response.header.Content-Type']
+ ).to.equal('text/csv');
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1]
+ .ResponseTemplates['application/json']
+ ).to.equal("$input.path('$.errorMessage')");
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1]
+ .ResponseTemplates['application/xml']
+ ).to.equal("$input.path('$.xml.errorMessage')");
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1]
+ .SelectionPattern
+ ).to.equal('.*"statusCode":404,.*');
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1]
+ .ResponseParameters['method.response.header.Content-Type']
+ ).to.equal('text/html');
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1]
+ .ResponseTemplates['application/json']
+ ).to.equal("$input.path('$.errorMessage')");
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1]
+ .SelectionPattern
+ ).to.equal('.*"statusCode":404,.*');
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1]
+ .ResponseParameters['method.response.header.Content-Type']
+ ).to.equal('text/html');
+ });
+ });
+
+ it('should add multiple response templates for a custom response codes', () => {
+ awsCompileApigEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ http: {
+ method: 'GET',
+ path: 'users/list',
+ integration: 'lambda',
+ response: {
+ template: '$input.path(\'$.foo\')',
+ headers: {
+ 'Content-Type': 'text/csv',
+ },
+ statusCodes: {
+ 404: {
+ pattern: '.*"statusCode":404,.*',
+ template: {
+ 'application/json': '$input.path(\'$.errorMessage\')',
+ 'application/xml': '$input.path(\'$.xml.errorMessage\')',
+ },
+ headers: {
+ 'Content-Type': 'text/html',
+ },
+ },
+ },
+ },
+ },
+ },
+ ],
+ },
+ };
+
+ return awsCompileApigEvents.compileMethods().then(() => {
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0]
+ .ResponseTemplates['application/json']
+ ).to.equal("$input.path('$.foo')");
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0]
+ .SelectionPattern
+ ).to.equal('');
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[0]
+ .ResponseParameters['method.response.header.Content-Type']
+ ).to.equal('text/csv');
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1]
+ .ResponseTemplates['application/json']
+ ).to.equal("$input.path('$.errorMessage')");
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1]
+ .ResponseTemplates['application/xml']
+ ).to.equal("$input.path('$.xml.errorMessage')");
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1]
+ .SelectionPattern
+ ).to.equal('.*"statusCode":404,.*');
+ expect(
+ awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources.ApiGatewayMethodUsersListGet.Properties.Integration.IntegrationResponses[1]
+ .ResponseParameters['method.response.header.Content-Type']
+ ).to.equal('text/html');
+ });
+ });
});
diff --git a/lib/plugins/aws/deploy/compile/events/dynamodb/README.md b/lib/plugins/aws/deploy/compile/events/dynamodb/README.md
deleted file mode 100644
index e3af7c1e5..000000000
--- a/lib/plugins/aws/deploy/compile/events/dynamodb/README.md
+++ /dev/null
@@ -1,36 +0,0 @@
-# Compile DynamoDB Stream Events
-
-We're currently gathering feedback regarding the exact implementation of this plugin in the following GitHub issue:
-
-[Issue #1441](https://github.com/serverless/serverless/issues/1441)
-
-It would be great if you can chime in on this and give us feedback on your specific use case and how you think the plugin
-should work.
-
-In the meantime you can simply add the code below to the [custom provider resources](/docs/guide/custom-provider-resources.md)
-section in your [`serverless.yml`](/docs/understanding-serverless/serverless-yml.md) file.
-
-## Template code for DynamoDB Stream support
-
-Add the following code to your [`serverless.yml`](/docs/understanding-serverless/serverless-yml.md) file to setup
-DynamoDB Stream support.
-
-**Note:** You can also create the table in the `resources.Resources` section and use `Fn::GetAtt` to reference the `StreamArn`
-in the mappings `EventSourceArn` definition.
-
-```yml
-# serverless.yml
-
-resources
- Resources:
- mapping:
- Type: AWS::Lambda::EventSourceMapping
- Properties:
- BatchSize: 10
- EventSourceArn: "arn:aws:dynamodb:::table//stream/"
- FunctionName:
- Fn::GetAtt:
- - ""
- - "Arn"
- StartingPosition: "TRIM_HORIZON"
-```
diff --git a/lib/plugins/aws/deploy/compile/events/kinesis/README.md b/lib/plugins/aws/deploy/compile/events/kinesis/README.md
deleted file mode 100644
index aaaa60a55..000000000
--- a/lib/plugins/aws/deploy/compile/events/kinesis/README.md
+++ /dev/null
@@ -1,36 +0,0 @@
-# Compile Kinesis Stream Events
-
-We're currently gathering feedback regarding the exact implementation of this plugin in the following GitHub issue:
-
-[Issue #1608](https://github.com/serverless/serverless/issues/1608)
-
-It would be great if you can chime in on this and give us feedback on your specific use case and how you think the plugin
-should work.
-
-In the meantime you can simply add the code below to the [custom provider resources](/docs/guide/custom-provider-resources.md)
-section in your [`serverless.yml`](/docs/understanding-serverless/serverless-yml.md) file.
-
-## Template code for Kinesis Stream support
-
-Add the following code to your [`serverless.yml`](/docs/understanding-serverless/serverless-yml.md) file to setup
-Kinesis Stream support.
-
-**Note:** You can also create the stream in the `resources.Resources` section and use `Fn::GetAtt` to reference the `Arn`
-in the mappings `EventSourceArn` definition.
-
-```yml
-# serverless.yml
-
-resources:
- Resources:
- mapping:
- Type: AWS::Lambda::EventSourceMapping
- Properties:
- BatchSize: 10
- EventSourceArn: "arn:aws:kinesis:::stream/"
- FunctionName:
- Fn::GetAtt:
- - ""
- - "Arn"
- StartingPosition: "TRIM_HORIZON"
-```
diff --git a/lib/plugins/aws/deploy/compile/events/s3/README.md b/lib/plugins/aws/deploy/compile/events/s3/README.md
deleted file mode 100644
index 002e6cb1c..000000000
--- a/lib/plugins/aws/deploy/compile/events/s3/README.md
+++ /dev/null
@@ -1,76 +0,0 @@
-# Compile S3 Events
-
-This plugins compiles the function related S3 events in `serverless.yml` to CloudFormation resources.
-
-## How it works
-
-`Compile S3 Events` hooks into the [`deploy:compileEvents`](/lib/plugins/deploy) lifecycle.
-
-It loops over all functions which are defined in `serverless.yml`.
-
-Inside the function loop it loops over all the defined `S3` events in the `events` section.
-
-You have two options to define the S3 bucket events:
-
-The first one is to use a simple string as the bucket name. This will create a S3 bucket CloudFormation resource with
-the bucket name you've defined and an additional lambda notification configuration resources for the current
-function and the `s3:objectCreated:*` events.
-
-The second possibility is to configure your S3 event more granular (like the bucket name or the event which this bucket
-should listen to) with the help of key value pairs.
-
-Take a look at the [Event syntax examples](#event-syntax-examples) below to see how you can setup S3 bucket events.
-
-A corresponding lambda permission resource is created for each S3 event.
-
-The created CloudFormation resources are merged into the compiled CloudFormation template after looping
-over all functions has finished.
-
-## Event syntax examples
-
-### Simple bucket setup
-
-In this example we've defined a bucket with the name `profile-pictures` which will cause the function `user` to be run
-whenever something is uploaded or updated in the bucket.
-
-```yml
-# serverless.yml
-functions:
- user:
- handler: user.update
- events:
- - s3: profile-pictures
-```
-
-### Bucket setup with extended event options
-
-Here we've used the extended event options which makes it possible to configure the S3 event in more detail.
-Our bucket is called `confidential-information` and the `mail` function is run every time a user removes something from
-the bucket.
-
-```yml
-# serverless.yml
-functions:
- mail:
- handler: mail.removal
- events:
- - s3:
- bucket: confidential-information
- event: s3:ObjectRemoved:*
-```
-
-We can also specify filter rules.
-
-```yml
-# serverless.yml
-functions:
- mail:
- handler: mail.removal
- events:
- - s3:
- bucket: confidential-information
- event: s3:ObjectRemoved:*
- rules:
- - prefix: inbox/
- - suffix: .eml
-```
\ No newline at end of file
diff --git a/lib/plugins/aws/deploy/compile/events/schedule/README.md b/lib/plugins/aws/deploy/compile/events/schedule/README.md
deleted file mode 100644
index 04d8dae5a..000000000
--- a/lib/plugins/aws/deploy/compile/events/schedule/README.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# Compile Scheduled Events
-
-This plugins compiles the function schedule event to a CloudFormation resource.
-
-## How it works
-
-`Compile Scheduled Events` hooks into the [`deploy:compileEvents`](/lib/plugins/deploy) lifecycle.
-
-It loops over all functions which are defined in `serverless.yml`. For each function that has a schedule event defined,
-a CloudWatch schedule event rule will be created.
-
-You have two options to define the schedule event:
-
-The first one is to use a simple string which defines the rate the function will be executed.
-
-The second option is to define the schedule event more granular (e.g. the rate or if it's enabled) with the help of
-key value pairs.
-
-Take a look at the [Event syntax examples](#event-syntax-examples) below to see how you can setup a schedule event.
-
-A corresponding lambda permission resource is create for the schedule event.
-
-Those two resources are then merged into the compiled CloudFormation template.
-
-## Event syntax examples
-
-### Simple schedule setup
-
-This setup specifies that the `greet` function should be run every 10 minutes.
-
-```yml
-# serverless.yml
-functions:
- greet:
- handler: handler.hello
- events:
- - schedule: rate(10 minutes)
-```
-
-### Schedule setup with extended event options
-
-This configuration sets up a disabled schedule event for the `report` function which will run every 2 minutes once
-enabled.
-
-```yml
-# serverless.yml
-functions:
- report:
- handler: handler.error
- events:
- - schedule:
- rate: rate(2 minutes)
- enabled: false
-```
diff --git a/lib/plugins/aws/deploy/compile/events/sns/README.md b/lib/plugins/aws/deploy/compile/events/sns/README.md
deleted file mode 100644
index 9a657d7db..000000000
--- a/lib/plugins/aws/deploy/compile/events/sns/README.md
+++ /dev/null
@@ -1,81 +0,0 @@
-# Compile SNS Events
-
-This plugins compiles the function SNS event to a CloudFormation resource.
-
-## How it works
-
-`Compile SNS Events` hooks into the [`deploy:compileEvents`](/lib/plugins/deploy) lifecycle.
-
-It loops over all functions which are defined in `serverless.yml`. For each function that has a SNS event defined,
-a corresponding SNS topic will be created.
-
-You have two options to define the SNS event:
-
-The first one is to use a simple string which defines the "Topic name" for SNS. The lambda function will be triggered
-every time a message is sent to this topic.
-
-The second option is to define the SNS event more granular (e.g. the "Topic name" and the "Display name") with the help of
-key value pairs.
-
-Take a look at the [Event syntax examples](#event-syntax-examples) below to see how you can setup a SNS event.
-
-A corresponding lambda permission resource is created for the SNS event.
-
-Those two resources are then merged into the compiled CloudFormation template.
-
-## Event syntax examples
-
-### Simple SNS setup
-
-This setup specifies that the `forward` function should be run every time a message is sent to the "messages" SNS topic.
-
-```yml
-# serverless.yml
-functions:
- forward:
- handler: message.forward
- events:
- - sns: messages
-```
-
-### SNS setup with extended event options
-
-This configuration sets up a SNS topic with the name "lambda-caller". The "Display name" of the topic is "Used to chain
-lambda functions". The `run` function is executed every time a message is sent to the "lambda-caller" SNS topic.
-
-```yml
-# serverless.yml
-functions:
- run:
- handler: event.run
- events:
- - sns:
- topicName: lambda-caller
- displayName: Used to chain lambda functions
-```
-
-### SNS setup with pre-existing topic ARN
-If you already have a topic that you've created manually, you can simply just provide the topic arn instead of the topic name using the `topicArn` property. Here's an example:
-
-```yml
-# serverless.yml
-functions:
- run:
- handler: event.run
- events:
- - sns:
- topicArn: some:arn:xxx
-```
-
-Or as a shortcut you can provide it as a string value to the `sns` key:
-
-```yml
-# serverless.yml
-functions:
- run:
- handler: event.run
- events:
- - sns: some:arn:xxx
-```
-
-The framework will detect that you've provided an ARN and will give permission to SNS to invoke that function. **You need to make sure you subscribe your function to that pre-existing topic manually**, as there's no way to add subscriptions to an existing topic ARN via CloudFormation.
diff --git a/lib/plugins/aws/deploy/compile/events/stream/index.js b/lib/plugins/aws/deploy/compile/events/stream/index.js
new file mode 100644
index 000000000..861103da5
--- /dev/null
+++ b/lib/plugins/aws/deploy/compile/events/stream/index.js
@@ -0,0 +1,145 @@
+'use strict';
+
+const _ = require('lodash');
+
+class AwsCompileStreamEvents {
+ constructor(serverless) {
+ this.serverless = serverless;
+ this.provider = 'aws';
+
+ this.hooks = {
+ 'deploy:compileEvents': this.compileStreamEvents.bind(this),
+ };
+ }
+
+ compileStreamEvents() {
+ this.serverless.service.getAllFunctions().forEach((functionName) => {
+ const functionObj = this.serverless.service.getFunction(functionName);
+
+ if (functionObj.events) {
+ functionObj.events.forEach(event => {
+ if (event.stream) {
+ let EventSourceArn;
+ let BatchSize = 10;
+ let StartingPosition = 'TRIM_HORIZON';
+ let Enabled = 'True';
+
+ // TODO validate arn syntax
+ if (typeof event.stream === 'object') {
+ if (!event.stream.arn) {
+ const errorMessage = [
+ `Missing "arn" property for stream event in function "${functionName}"`,
+ ' The correct syntax is: stream: ',
+ ' OR an object with an "arn" property.',
+ ' Please check the docs for more info.',
+ ].join('');
+ throw new this.serverless.classes
+ .Error(errorMessage);
+ }
+ EventSourceArn = event.stream.arn;
+ BatchSize = event.stream.batchSize
+ || BatchSize;
+ StartingPosition = event.stream.startingPosition
+ || StartingPosition;
+ if (typeof event.stream.enabled !== 'undefined') {
+ Enabled = event.stream.enabled ? 'True' : 'False';
+ }
+ } else if (typeof event.stream === 'string') {
+ EventSourceArn = event.stream;
+ } else {
+ const errorMessage = [
+ `Stream event of function "${functionName}" is not an object nor a string`,
+ ' The correct syntax is: stream: ',
+ ' OR an object with an "arn" property.',
+ ' Please check the docs for more info.',
+ ].join('');
+ throw new this.serverless.classes
+ .Error(errorMessage);
+ }
+
+ const normalizedFunctionName = functionName[0].toUpperCase() + functionName.substr(1);
+
+ const streamTemplate = `
+ {
+ "Type": "AWS::Lambda::EventSourceMapping",
+ "DependsOn": "IamPolicyLambdaExecution",
+ "Properties": {
+ "BatchSize": ${BatchSize},
+ "EventSourceArn": "${EventSourceArn}",
+ "FunctionName": {
+ "Fn::GetAtt": [
+ "${normalizedFunctionName}LambdaFunction",
+ "Arn"
+ ]
+ },
+ "StartingPosition": "${StartingPosition}",
+ "Enabled": "${Enabled}"
+ }
+ }
+ `;
+
+ // get the type (DynamoDB or Kinesis) of the stream
+ const streamType = EventSourceArn.split(':')[2];
+ const normalizedStreamType = streamType[0].toUpperCase() + streamType.substr(1);
+
+ // get the name of the stream (and remove any non-alphanumerics in it)
+ const streamName = EventSourceArn.split('/')[1];
+ const normalizedStreamName = streamName[0].toUpperCase()
+ + streamName.substr(1).replace(/\W/g, '');
+
+ // create type specific PolicyDocument statements
+ let streamStatement = {};
+ if (streamType === 'dynamodb') {
+ streamStatement = {
+ Effect: 'Allow',
+ Action: [
+ 'dynamodb:GetRecords',
+ 'dynamodb:GetShardIterator',
+ 'dynamodb:DescribeStream',
+ 'dynamodb:ListStreams',
+ ],
+ Resource: EventSourceArn,
+ };
+ } else {
+ streamStatement = {
+ Effect: 'Allow',
+ Action: [
+ 'kinesis:GetRecords',
+ 'kinesis:GetShardIterator',
+ 'kinesis:DescribeStream',
+ 'kinesis:ListStreams',
+ ],
+ Resource: EventSourceArn,
+ };
+ }
+
+ // update the PolicyDocument statements
+ const statement = this.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources
+ .IamPolicyLambdaExecution
+ .Properties
+ .PolicyDocument
+ .Statement;
+
+ this.serverless.service.provider.compiledCloudFormationTemplate
+ .Resources
+ .IamPolicyLambdaExecution
+ .Properties
+ .PolicyDocument
+ .Statement = statement.concat([streamStatement]);
+
+ const newStreamObject = {
+ [`${normalizedFunctionName}EventSourceMapping${
+ normalizedStreamType}${normalizedStreamName}`]: JSON.parse(streamTemplate),
+ };
+
+ _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
+ newStreamObject);
+ }
+ });
+ }
+ });
+ }
+}
+
+module.exports = AwsCompileStreamEvents;
diff --git a/lib/plugins/aws/deploy/compile/events/stream/tests/index.js b/lib/plugins/aws/deploy/compile/events/stream/tests/index.js
new file mode 100644
index 000000000..0dcabb175
--- /dev/null
+++ b/lib/plugins/aws/deploy/compile/events/stream/tests/index.js
@@ -0,0 +1,425 @@
+'use strict';
+
+const expect = require('chai').expect;
+const AwsCompileStreamEvents = require('../index');
+const Serverless = require('../../../../../../../Serverless');
+
+describe('AwsCompileStreamEvents', () => {
+ let serverless;
+ let awsCompileStreamEvents;
+
+ beforeEach(() => {
+ serverless = new Serverless();
+ serverless.service.provider.compiledCloudFormationTemplate = {
+ Resources: {
+ IamPolicyLambdaExecution: {
+ Properties: {
+ PolicyDocument: {
+ Statement: [],
+ },
+ },
+ },
+ },
+ };
+ awsCompileStreamEvents = new AwsCompileStreamEvents(serverless);
+ awsCompileStreamEvents.serverless.service.service = 'new-service';
+ });
+
+ describe('#constructor()', () => {
+ it('should set the provider variable to "aws"', () => expect(awsCompileStreamEvents.provider)
+ .to.equal('aws'));
+ });
+
+ describe('#compileStreamEvents()', () => {
+ it('should throw an error if stream event type is not a string or an object', () => {
+ awsCompileStreamEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ stream: 42,
+ },
+ ],
+ },
+ };
+
+ expect(() => awsCompileStreamEvents.compileStreamEvents()).to.throw(Error);
+ });
+
+ it('should throw an error if the "arn" property is not given', () => {
+ awsCompileStreamEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ stream: {
+ arn: null,
+ },
+ },
+ ],
+ },
+ };
+
+ expect(() => awsCompileStreamEvents.compileStreamEvents()).to.throw(Error);
+ });
+
+ describe('when a DynamoDB stream ARN is given', () => {
+ it('should create event source mappings when a DynamoDB stream ARN is given', () => {
+ awsCompileStreamEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ stream: {
+ arn: 'arn:aws:dynamodb:region:account:table/foo/stream/1',
+ batchSize: 1,
+ startingPosition: 'STARTING_POSITION_ONE',
+ enabled: false,
+ },
+ },
+ {
+ stream: {
+ arn: 'arn:aws:dynamodb:region:account:table/bar/stream/2',
+ },
+ },
+ {
+ stream: 'arn:aws:dynamodb:region:account:table/baz/stream/3',
+ },
+ ],
+ },
+ };
+
+ awsCompileStreamEvents.compileStreamEvents();
+
+ // event 1
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbFoo
+ .Type
+ ).to.equal('AWS::Lambda::EventSourceMapping');
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbFoo
+ .DependsOn
+ ).to.equal('IamPolicyLambdaExecution');
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbFoo
+ .Properties.EventSourceArn
+ ).to.equal(
+ awsCompileStreamEvents.serverless.service.functions.first.events[0]
+ .stream.arn
+ );
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbFoo
+ .Properties.BatchSize
+ ).to.equal(
+ awsCompileStreamEvents.serverless.service.functions.first.events[0]
+ .stream.batchSize
+ );
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbFoo
+ .Properties.StartingPosition
+ ).to.equal(
+ awsCompileStreamEvents.serverless.service.functions.first.events[0]
+ .stream.startingPosition
+ );
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbFoo
+ .Properties.Enabled
+ ).to.equal('False');
+
+ // event 2
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBar
+ .Type
+ ).to.equal('AWS::Lambda::EventSourceMapping');
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBar
+ .DependsOn
+ ).to.equal('IamPolicyLambdaExecution');
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBar
+ .Properties.EventSourceArn
+ ).to.equal(
+ awsCompileStreamEvents.serverless.service.functions.first.events[1]
+ .stream.arn
+ );
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBar
+ .Properties.BatchSize
+ ).to.equal(10);
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBar
+ .Properties.StartingPosition
+ ).to.equal('TRIM_HORIZON');
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBar
+ .Properties.Enabled
+ ).to.equal('True');
+
+ // event 3
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBaz
+ .Type
+ ).to.equal('AWS::Lambda::EventSourceMapping');
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBaz
+ .DependsOn
+ ).to.equal('IamPolicyLambdaExecution');
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBaz
+ .Properties.EventSourceArn
+ ).to.equal(
+ awsCompileStreamEvents.serverless.service.functions.first.events[2]
+ .stream
+ );
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBaz
+ .Properties.BatchSize
+ ).to.equal(10);
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBaz
+ .Properties.StartingPosition
+ ).to.equal('TRIM_HORIZON');
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBaz
+ .Properties.Enabled
+ ).to.equal('True');
+ });
+
+ it('should add the necessary IAM role statements', () => {
+ awsCompileStreamEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ stream: 'arn:aws:dynamodb:region:account:table/foo/stream/1',
+ },
+ ],
+ },
+ };
+
+ const iamRoleStatements = [
+ {
+ Effect: 'Allow',
+ Action: [
+ 'dynamodb:GetRecords',
+ 'dynamodb:GetShardIterator',
+ 'dynamodb:DescribeStream',
+ 'dynamodb:ListStreams',
+ ],
+ Resource: 'arn:aws:dynamodb:region:account:table/foo/stream/1',
+ },
+ ];
+
+ awsCompileStreamEvents.compileStreamEvents();
+
+ expect(awsCompileStreamEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .IamPolicyLambdaExecution.Properties
+ .PolicyDocument.Statement
+ ).to.deep.equal(iamRoleStatements);
+ });
+ });
+
+ describe('when a Kinesis stream ARN is given', () => {
+ it('should create event source mappings when a Kinesis stream ARN is given', () => {
+ awsCompileStreamEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ stream: {
+ arn: 'arn:aws:kinesis:region:account:stream/foo',
+ batchSize: 1,
+ startingPosition: 'STARTING_POSITION_ONE',
+ enabled: false,
+ },
+ },
+ {
+ stream: {
+ arn: 'arn:aws:kinesis:region:account:stream/bar',
+ },
+ },
+ {
+ stream: 'arn:aws:kinesis:region:account:stream/baz',
+ },
+ ],
+ },
+ };
+
+ awsCompileStreamEvents.compileStreamEvents();
+
+ // event 1
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisFoo
+ .Type
+ ).to.equal('AWS::Lambda::EventSourceMapping');
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisFoo
+ .DependsOn
+ ).to.equal('IamPolicyLambdaExecution');
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisFoo
+ .Properties.EventSourceArn
+ ).to.equal(
+ awsCompileStreamEvents.serverless.service.functions.first.events[0]
+ .stream.arn
+ );
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisFoo
+ .Properties.BatchSize
+ ).to.equal(
+ awsCompileStreamEvents.serverless.service.functions.first.events[0]
+ .stream.batchSize
+ );
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisFoo
+ .Properties.StartingPosition
+ ).to.equal(
+ awsCompileStreamEvents.serverless.service.functions.first.events[0]
+ .stream.startingPosition
+ );
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisFoo
+ .Properties.Enabled
+ ).to.equal('False');
+
+ // event 2
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBar
+ .Type
+ ).to.equal('AWS::Lambda::EventSourceMapping');
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBar
+ .DependsOn
+ ).to.equal('IamPolicyLambdaExecution');
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBar
+ .Properties.EventSourceArn
+ ).to.equal(
+ awsCompileStreamEvents.serverless.service.functions.first.events[1]
+ .stream.arn
+ );
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBar
+ .Properties.BatchSize
+ ).to.equal(10);
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBar
+ .Properties.StartingPosition
+ ).to.equal('TRIM_HORIZON');
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBar
+ .Properties.Enabled
+ ).to.equal('True');
+
+ // event 3
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBaz
+ .Type
+ ).to.equal('AWS::Lambda::EventSourceMapping');
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBaz
+ .DependsOn
+ ).to.equal('IamPolicyLambdaExecution');
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBaz
+ .Properties.EventSourceArn
+ ).to.equal(
+ awsCompileStreamEvents.serverless.service.functions.first.events[2]
+ .stream
+ );
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBaz
+ .Properties.BatchSize
+ ).to.equal(10);
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBaz
+ .Properties.StartingPosition
+ ).to.equal('TRIM_HORIZON');
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBaz
+ .Properties.Enabled
+ ).to.equal('True');
+ });
+
+ it('should add the necessary IAM role statements', () => {
+ awsCompileStreamEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ stream: 'arn:aws:kinesis:region:account:stream/foo',
+ },
+ ],
+ },
+ };
+
+ const iamRoleStatements = [
+ {
+ Effect: 'Allow',
+ Action: [
+ 'kinesis:GetRecords',
+ 'kinesis:GetShardIterator',
+ 'kinesis:DescribeStream',
+ 'kinesis:ListStreams',
+ ],
+ Resource: 'arn:aws:kinesis:region:account:stream/foo',
+ },
+ ];
+
+ awsCompileStreamEvents.compileStreamEvents();
+
+ expect(awsCompileStreamEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .IamPolicyLambdaExecution.Properties
+ .PolicyDocument.Statement
+ ).to.deep.equal(iamRoleStatements);
+ });
+ });
+
+ it('should not create event source mapping when stream events are not given', () => {
+ awsCompileStreamEvents.serverless.service.functions = {
+ first: {
+ events: [],
+ },
+ };
+
+ awsCompileStreamEvents.compileStreamEvents();
+
+ // should be 1 because we've mocked the IamPolicyLambdaExecution above
+ expect(
+ Object.keys(awsCompileStreamEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources).length
+ ).to.equal(1);
+ });
+
+ it('should not add the IAM role statements when stream events are not given', () => {
+ awsCompileStreamEvents.serverless.service.functions = {
+ first: {
+ events: [],
+ },
+ };
+
+ awsCompileStreamEvents.compileStreamEvents();
+
+ expect(
+ awsCompileStreamEvents.serverless.service.provider
+ .compiledCloudFormationTemplate.Resources
+ .IamPolicyLambdaExecution.Properties
+ .PolicyDocument.Statement.length
+ ).to.equal(0);
+ });
+
+ it('should remove all non-alphanumerics from stream names for the resource logical ids', () => {
+ awsCompileStreamEvents.serverless.service.functions = {
+ first: {
+ events: [
+ {
+ stream: 'arn:aws:kinesis:region:account:stream/some-long-name',
+ },
+ ],
+ },
+ };
+
+ awsCompileStreamEvents.compileStreamEvents();
+
+ expect(awsCompileStreamEvents.serverless.service
+ .provider.compiledCloudFormationTemplate.Resources
+ ).to.have.any.keys('FirstEventSourceMappingKinesisSomelongname');
+ });
+ });
+});
diff --git a/lib/plugins/aws/deploy/compile/functions/README.md b/lib/plugins/aws/deploy/compile/functions/README.md
deleted file mode 100644
index f7a9f509c..000000000
--- a/lib/plugins/aws/deploy/compile/functions/README.md
+++ /dev/null
@@ -1,20 +0,0 @@
-# Compile Functions
-
-This plugins compiles the functions in `serverless.yml` to corresponding lambda CloudFormation resources.
-
-## How it works
-
-`Compile Functions` hooks into the [`deploy:compileFunctions`](/lib/plugins/deploy) lifecycle.
-
-It loops over all functions which are defined in `serverless.yml`.
-
-Inside the function loop it creates corresponding CloudFormation lambda function resources based on the settings
-(e.g. function `name` property or service `defaults`) which are provided in the `serverless.yml` file.
-
-The function will be called `--` by default but you can specify an alternative name
-with the help of the functions `name` property.
-
-The functions `MemorySize` is set to `1024` and `Timeout` to `6`. You can overwrite those defaults by setting
-corresponding entries in the services `provider` or function property.
-
-At the end all CloudFormation function resources are merged inside the compiled CloudFormation template.
diff --git a/lib/plugins/aws/deploy/lib/createStack.js b/lib/plugins/aws/deploy/lib/createStack.js
index b8a82e79a..fd928236f 100644
--- a/lib/plugins/aws/deploy/lib/createStack.js
+++ b/lib/plugins/aws/deploy/lib/createStack.js
@@ -1,5 +1,6 @@
'use strict';
+const _ = require('lodash');
const path = require('path');
const BbPromise = require('bluebird');
@@ -7,6 +8,12 @@ module.exports = {
create() {
this.serverless.cli.log('Creating Stack...');
const stackName = `${this.serverless.service.service}-${this.options.stage}`;
+ let stackTags = { STAGE: this.options.stage };
+
+ // Merge additional stack tags
+ if (typeof this.serverless.service.provider.stackTags === 'object') {
+ stackTags = _.extend(stackTags, this.serverless.service.provider.stackTags);
+ }
const params = {
StackName: stackName,
@@ -18,10 +25,7 @@ module.exports = {
Parameters: [],
TemplateBody: JSON.stringify(this.serverless.service.provider
.compiledCloudFormationTemplate),
- Tags: [{
- Key: 'STAGE',
- Value: this.options.stage,
- }],
+ Tags: Object.keys(stackTags).map((key) => ({ Key: key, Value: stackTags[key] })),
};
return this.sdk.request('CloudFormation',
diff --git a/lib/plugins/aws/deploy/lib/updateStack.js b/lib/plugins/aws/deploy/lib/updateStack.js
index 1a7eafb76..9a7e6d140 100644
--- a/lib/plugins/aws/deploy/lib/updateStack.js
+++ b/lib/plugins/aws/deploy/lib/updateStack.js
@@ -1,7 +1,8 @@
'use strict';
-const BbPromise = require('bluebird');
+const _ = require('lodash');
const path = require('path');
+const BbPromise = require('bluebird');
module.exports = {
update() {
@@ -13,6 +14,13 @@ module.exports = {
this.serverless.cli.log('Updating Stack...');
const stackName = `${this.serverless.service.service}-${this.options.stage}`;
+ let stackTags = { STAGE: this.options.stage };
+
+ // Merge additional stack tags
+ if (typeof this.serverless.service.provider.stackTags === 'object') {
+ stackTags = _.extend(stackTags, this.serverless.service.provider.stackTags);
+ }
+
const params = {
StackName: stackName,
Capabilities: [
@@ -21,8 +29,17 @@ module.exports = {
],
Parameters: [],
TemplateURL: templateUrl,
+ Tags: Object.keys(stackTags).map((key) => ({ Key: key, Value: stackTags[key] })),
};
+ // Policy must have at least one statement, otherwise no updates would be possible at all
+ if (this.serverless.service.provider.stackPolicy &&
+ this.serverless.service.provider.stackPolicy.length) {
+ params.StackPolicyBody = JSON.stringify({
+ Statement: this.serverless.service.provider.stackPolicy,
+ });
+ }
+
return this.sdk.request('CloudFormation',
'updateStack',
params,
diff --git a/lib/plugins/aws/deploy/tests/createStack.js b/lib/plugins/aws/deploy/tests/createStack.js
index 9aae2e1bf..51b9d1e27 100644
--- a/lib/plugins/aws/deploy/tests/createStack.js
+++ b/lib/plugins/aws/deploy/tests/createStack.js
@@ -64,6 +64,22 @@ describe('createStack', () => {
expect(createStackStub.calledWith(awsDeploy.options.stage, awsDeploy.options.region));
});
});
+
+ it('should include custom stack tags', () => {
+ awsDeploy.serverless.service.provider.stackTags = { STAGE: 'overridden', tag1: 'value1' };
+
+ const createStackStub = sinon
+ .stub(awsDeploy.sdk, 'request').returns(BbPromise.resolve());
+
+ return awsDeploy.create().then(() => {
+ expect(createStackStub.args[0][2].Tags)
+ .to.deep.equal([
+ { Key: 'STAGE', Value: 'overridden' },
+ { Key: 'tag1', Value: 'value1' },
+ ]);
+ awsDeploy.sdk.request.restore();
+ });
+ });
});
describe('#createStack()', () => {
diff --git a/lib/plugins/aws/deploy/tests/updateStack.js b/lib/plugins/aws/deploy/tests/updateStack.js
index dc88257df..6f0a1aa37 100644
--- a/lib/plugins/aws/deploy/tests/updateStack.js
+++ b/lib/plugins/aws/deploy/tests/updateStack.js
@@ -47,11 +47,36 @@ describe('updateStack', () => {
expect(updateStackStub.args[0][2].TemplateURL)
.to.be.equal(`https://s3.amazonaws.com/${awsDeploy.bucketName}/${awsDeploy.serverless
.service.package.artifactDirectoryName}/compiled-cloudformation-template.json`);
+ expect(updateStackStub.args[0][2].Tags)
+ .to.deep.equal([{ Key: 'STAGE', Value: awsDeploy.options.stage }]);
expect(updateStackStub.calledWith(awsDeploy.options.stage, awsDeploy.options.region));
awsDeploy.sdk.request.restore();
})
);
+
+ it('should include custom stack tags and policy', () => {
+ awsDeploy.serverless.service.provider.stackTags = { STAGE: 'overridden', tag1: 'value1' };
+ awsDeploy.serverless.service.provider.stackPolicy = [{
+ Effect: 'Allow',
+ Principal: '*',
+ Action: 'Update:*',
+ Resource: '*',
+ }];
+
+ return awsDeploy.update().then(() => {
+ expect(updateStackStub.args[0][2].Tags)
+ .to.deep.equal([
+ { Key: 'STAGE', Value: 'overridden' },
+ { Key: 'tag1', Value: 'value1' },
+ ]);
+ expect(updateStackStub.args[0][2].StackPolicyBody)
+ .to.equal(
+ '{"Statement":[{"Effect":"Allow","Principal":"*","Action":"Update:*","Resource":"*"}]}'
+ );
+ awsDeploy.sdk.request.restore();
+ });
+ });
});
describe('#updateStack()', () => {
diff --git a/lib/plugins/aws/index.js b/lib/plugins/aws/index.js
index 88e7b6646..c3eb9ca80 100644
--- a/lib/plugins/aws/index.js
+++ b/lib/plugins/aws/index.js
@@ -5,6 +5,71 @@ const HttpsProxyAgent = require('https-proxy-agent');
const url = require('url');
const AWS = require('aws-sdk');
+const impl = {
+ /**
+ * Add credentials, if present, from the given credentials configuration
+ * @param credentials The credentials to add credentials configuration to
+ * @param config The credentials configuration
+ */
+ addCredentials: (credentials, config) => {
+ if (credentials &&
+ config &&
+ config.accessKeyId &&
+ config.accessKeyId !== 'undefined' &&
+ config.secretAccessKey &&
+ config.secretAccessKey !== 'undefined') {
+ if (config.accessKeyId) {
+ credentials.accessKeyId = config.accessKeyId; // eslint-disable-line no-param-reassign
+ }
+ if (config.secretAccessKey) {
+ // eslint-disable-next-line no-param-reassign
+ credentials.secretAccessKey = config.secretAccessKey;
+ }
+ if (config.sessionToken) {
+ credentials.sessionToken = config.sessionToken; // eslint-disable-line no-param-reassign
+ } else if (credentials.sessionToken) {
+ delete credentials.sessionToken; // eslint-disable-line no-param-reassign
+ }
+ }
+ },
+ /**
+ * Add credentials, if present, from the environment
+ * @param credentials The credentials to add environment credentials to
+ * @param prefix The environment variable prefix to use in extracting credentials
+ */
+ addEnvironmentCredentials: (credentials, prefix) => {
+ if (prefix) {
+ const environmentCredentials = new AWS.EnvironmentCredentials(prefix);
+ impl.addCredentials(credentials, environmentCredentials);
+ }
+ },
+ /**
+ * Add credentials from a profile, if the profile exists
+ * @param credentials The credentials to add profile credentials to
+ * @param prefix The prefix to the profile environment variable
+ */
+ addProfileCredentials: (credentials, profile) => {
+ if (profile) {
+ const profileCredentials = new AWS.SharedIniFileCredentials({ profile });
+ if (Object.keys(profileCredentials).length) {
+ credentials.profile = profile; // eslint-disable-line no-param-reassign
+ }
+ impl.addCredentials(credentials, profileCredentials);
+ }
+ },
+ /**
+ * Add credentials, if present, from a profile that is specified within the environment
+ * @param credentials The prefix of the profile's declaration in the environment
+ * @param prefix The prefix for the environment variable
+ */
+ addEnvironmentProfile: (credentials, prefix) => {
+ if (prefix) {
+ const profile = process.env[`${prefix}_PROFILE`];
+ impl.addProfileCredentials(credentials, profile);
+ }
+ },
+};
+
class SDK {
constructor(serverless) {
// Defaults
@@ -33,7 +98,7 @@ class SDK {
request(service, method, params, stage, region) {
const that = this;
- const credentials = that.getCredentials(region);
+ const credentials = that.getCredentials(stage, region);
const persistentRequest = (f) => new BbPromise((resolve, reject) => {
const doCall = () => {
f()
@@ -78,15 +143,30 @@ class SDK {
});
}
- getCredentials(region) {
- const credentials = { region };
- const profile = this.serverless.service.provider.profile;
+ /**
+ * Fetch credentials directly or using a profile from serverless yml configuration or from the
+ * well known environment variables
+ * @param stage
+ * @param region
+ * @returns {{region: *}}
+ */
+ getCredentials(stage, region) {
+ const ret = { region };
+ const credentials = {};
+ const stageUpper = stage ? stage.toUpperCase() : null;
- if (typeof profile !== 'undefined' && profile) {
- credentials.credentials = new AWS.SharedIniFileCredentials({ profile });
+ // add specified credentials, overriding with more specific declarations
+ impl.addCredentials(credentials, this.serverless.service.provider.credentials); // config creds
+ impl.addProfileCredentials(credentials, this.serverless.service.provider.profile);
+ impl.addEnvironmentCredentials(credentials, 'AWS'); // creds for all stages
+ impl.addEnvironmentProfile(credentials, 'AWS');
+ impl.addEnvironmentCredentials(credentials, `AWS_${stageUpper}`); // stage specific creds
+ impl.addEnvironmentProfile(credentials, `AWS_${stageUpper}`);
+
+ if (Object.keys(credentials).length) {
+ ret.credentials = credentials;
}
-
- return credentials;
+ return ret;
}
getServerlessDeploymentBucketName(stage, region) {
diff --git a/lib/plugins/aws/info/index.js b/lib/plugins/aws/info/index.js
index dbbf6da54..ab0213c81 100644
--- a/lib/plugins/aws/info/index.js
+++ b/lib/plugins/aws/info/index.js
@@ -88,13 +88,14 @@ class AwsInfo {
return BbPromise.resolve(gatheredData);
})
.then((gatheredData) => this.getApiKeyValues(gatheredData))
- .then((gatheredData) => BbPromise.resolve(gatheredData.info)) // resolve the info at the end
+ .then((gatheredData) => BbPromise.resolve(gatheredData))
.catch((e) => {
let result;
if (e.code === 'ValidationError') {
// stack doesn't exist, provide only the general info
- result = BbPromise.resolve(info);
+ const data = { info, outputs: [] };
+ result = BbPromise.resolve(data);
} else {
// other aws sdk errors
result = BbPromise.reject(new this.serverless.classes
@@ -140,7 +141,8 @@ class AwsInfo {
/**
* Display service information
*/
- display(info) {
+ display(gatheredData) {
+ const info = gatheredData.info;
let message = `
${chalk.yellow.underline('Service Information')}
${chalk.yellow('service:')} ${info.service}
@@ -201,6 +203,14 @@ ${chalk.yellow('region:')} ${info.region}`;
message = message.concat(`${functionsMessage}\n`);
+ // when verbose info is requested, add the stack outputs to the output
+ if (this.options.verbose) {
+ message = message.concat(`${chalk.yellow.underline('\nStack Outputs\n')}`);
+ _.forEach(gatheredData.outputs, (output) => {
+ message = message.concat(`${chalk.yellow(output.OutputKey)}: ${output.OutputValue}\n`);
+ });
+ }
+
this.serverless.cli.consoleLog(message);
return message;
}
diff --git a/lib/plugins/aws/info/tests/index.js b/lib/plugins/aws/info/tests/index.js
index 871cc261b..3481b1450 100644
--- a/lib/plugins/aws/info/tests/index.js
+++ b/lib/plugins/aws/info/tests/index.js
@@ -176,20 +176,20 @@ describe('AwsInfo', () => {
it('should get service name', () => {
serverless.service.service = 'myservice';
- return awsInfo.gather().then((info) => {
- expect(info.service).to.equal('myservice');
+ return awsInfo.gather().then((data) => {
+ expect(data.info.service).to.equal('myservice');
});
});
it('should get stage name', () => {
- awsInfo.gather().then((info) => {
- expect(info.stage).to.equal('dev');
+ awsInfo.gather().then((data) => {
+ expect(data.info.stage).to.equal('dev');
});
});
it('should get region name', () => {
- awsInfo.gather().then((info) => {
- expect(info.region).to.equal('us-east-1');
+ awsInfo.gather().then((data) => {
+ expect(data.info.region).to.equal('us-east-1');
});
});
@@ -205,16 +205,16 @@ describe('AwsInfo', () => {
},
];
- return awsInfo.gather().then((info) => {
- expect(info.functions).to.deep.equal(expectedFunctions);
+ return awsInfo.gather().then((data) => {
+ expect(data.info.functions).to.deep.equal(expectedFunctions);
});
});
it('should get endpoint', () => {
const expectedEndpoint = 'ab12cd34ef.execute-api.us-east-1.amazonaws.com/dev';
- return awsInfo.gather().then((info) => {
- expect(info.endpoint).to.deep.equal(expectedEndpoint);
+ return awsInfo.gather().then((data) => {
+ expect(data.info.endpoint).to.deep.equal(expectedEndpoint);
});
});
@@ -235,8 +235,8 @@ describe('AwsInfo', () => {
region: 'us-east-1',
};
- return awsInfo.gather().then((info) => {
- expect(info).to.deep.equal(expectedInfo);
+ return awsInfo.gather().then((data) => {
+ expect(data.info).to.deep.equal(expectedInfo);
});
});
@@ -334,31 +334,33 @@ describe('AwsInfo', () => {
serverless.cli = new CLI(serverless);
sinon.stub(serverless.cli, 'consoleLog').returns();
- const info = {
- service: 'my-first',
- stage: 'dev',
- region: 'eu-west-1',
- endpoint: 'ab12cd34ef.execute-api.us-east-1.amazonaws.com/dev',
- functions: [
- {
- name: 'function1',
- arn: 'arn:aws:iam::12345678:function:function1',
- },
- {
- name: 'function2',
- arn: 'arn:aws:iam::12345678:function:function2',
- },
- ],
- apiKeys: [
- {
- name: 'first',
- value: 'xxx',
- },
- {
- name: 'second',
- value: 'yyy',
- },
- ],
+ const data = {
+ info: {
+ service: 'my-first',
+ stage: 'dev',
+ region: 'eu-west-1',
+ endpoint: 'ab12cd34ef.execute-api.us-east-1.amazonaws.com/dev',
+ functions: [
+ {
+ name: 'function1',
+ arn: 'arn:aws:iam::12345678:function:function1',
+ },
+ {
+ name: 'function2',
+ arn: 'arn:aws:iam::12345678:function:function2',
+ },
+ ],
+ apiKeys: [
+ {
+ name: 'first',
+ value: 'xxx',
+ },
+ {
+ name: 'second',
+ value: 'yyy',
+ },
+ ],
+ },
};
const expectedMessage = `
@@ -377,17 +379,19 @@ ${chalk.yellow('functions:')}
function2: arn:aws:iam::12345678:function:function2
`;
- expect(awsInfo.display(info)).to.equal(expectedMessage);
+ expect(awsInfo.display(data)).to.equal(expectedMessage);
});
it("should display only general information when stack doesn't exist", () => {
serverless.cli = new CLI(serverless);
sinon.stub(serverless.cli, 'consoleLog').returns();
- const info = {
- service: 'my-first',
- stage: 'dev',
- region: 'eu-west-1',
+ const data = {
+ info: {
+ service: 'my-first',
+ stage: 'dev',
+ region: 'eu-west-1',
+ },
};
const expectedMessage = `
@@ -403,19 +407,21 @@ ${chalk.yellow('functions:')}
None
`;
- expect(awsInfo.display(info)).to.equal(expectedMessage);
+ expect(awsInfo.display(data)).to.equal(expectedMessage);
});
it('should display only general information when no functions, endpoints or api keys', () => {
serverless.cli = new CLI(serverless);
sinon.stub(serverless.cli, 'consoleLog').returns();
- const info = {
- service: 'my-first',
- stage: 'dev',
- region: 'eu-west-1',
- functions: [],
- endpoint: undefined,
+ const data = {
+ info: {
+ service: 'my-first',
+ stage: 'dev',
+ region: 'eu-west-1',
+ functions: [],
+ endpoint: undefined,
+ },
};
const expectedMessage = `
@@ -431,7 +437,62 @@ ${chalk.yellow('functions:')}
None
`;
- expect(awsInfo.display(info)).to.equal(expectedMessage);
+ expect(awsInfo.display(data)).to.equal(expectedMessage);
+ });
+
+ it('should display cloudformation outputs when verbose output is requested', () => {
+ serverless.cli = new CLI(serverless);
+ sinon.stub(serverless.cli, 'consoleLog').returns();
+
+ const verboseOptions = {
+ stage: 'dev',
+ region: 'us-east-1',
+ verbose: true,
+ };
+ const awsVerboseInfo = new AwsInfo(serverless, verboseOptions);
+
+ const verboseData = {
+ info: {
+ service: 'my-first',
+ stage: 'dev',
+ region: 'eu-west-1',
+ endpoint: 'ab12cd34ef.execute-api.us-east-1.amazonaws.com/dev',
+ functions: [
+ {
+ name: 'function1',
+ arn: 'arn:aws:iam::12345678:function:function1',
+ },
+ {
+ name: 'function2',
+ arn: 'arn:aws:iam::12345678:function:function2',
+ },
+ ],
+ apiKeys: [
+ {
+ name: 'first',
+ value: 'xxx',
+ },
+ {
+ name: 'second',
+ value: 'yyy',
+ },
+ ],
+ },
+ outputs: [
+ {
+ Description: 'Lambda function info',
+ OutputKey: 'Function1FunctionArn',
+ OutputValue: 'arn:aws:iam::12345678:function:function1',
+ },
+ {
+ Description: 'Lambda function info',
+ OutputKey: 'Function2FunctionArn',
+ OutputValue: 'arn:aws:iam::12345678:function:function2',
+ },
+ ],
+ };
+
+ expect(awsVerboseInfo.display(verboseData)).to.contain('Stack Outputs');
});
});
});
diff --git a/lib/plugins/aws/lib/monitorStack.js b/lib/plugins/aws/lib/monitorStack.js
index 39e3014ad..d17f363b9 100644
--- a/lib/plugins/aws/lib/monitorStack.js
+++ b/lib/plugins/aws/lib/monitorStack.js
@@ -18,13 +18,6 @@ module.exports = {
'UPDATE_COMPLETE',
'DELETE_COMPLETE',
];
- const invalidStatuses = [
- 'CREATE_FAILED',
- 'DELETE_FAILED',
- 'ROLLBACK_FAILED',
- 'UPDATE_ROLLBACK_COMPLETE',
- 'UPDATE_ROLLBACK_FAILED',
- ];
const loggedEvents = [];
const monitoredSince = new Date();
monitoredSince.setSeconds(monitoredSince.getSeconds() - 5);
@@ -83,7 +76,8 @@ module.exports = {
}
});
// Handle stack create/update/delete failures
- if (invalidStatuses.indexOf(stackStatus) >= 0 && stackLatestError !== null) {
+ if ((stackLatestError && !this.options.verbose)
+ || (stackStatus.endsWith('ROLLBACK_COMPLETE') && this.options.verbose)) {
this.serverless.cli.log('Deployment failed!');
let errorMessage = 'An error occurred while provisioning your stack: ';
errorMessage += `${stackLatestError.LogicalResourceId} - `;
diff --git a/lib/plugins/aws/logs/README.md b/lib/plugins/aws/logs/README.md
deleted file mode 100644
index 98c8bab10..000000000
--- a/lib/plugins/aws/logs/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Logs
-
-This plugin returns the CloudWatch logs of a lambda function. You can simply run `serverless logs -f hello` to test it out.
-
-## How it works
-
-`Logs` hooks into the [`logs:logs`](/lib/plugins/logs) lifecycle. It will fetch the CloudWatch log group of the provided function and outputs all the log stream events in the terminal.
\ No newline at end of file
diff --git a/lib/plugins/aws/remove/README.md b/lib/plugins/aws/remove/README.md
deleted file mode 100644
index 54dd22eaf..000000000
--- a/lib/plugins/aws/remove/README.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Remove
-
-This plugin removes the service from AWS.
-
-## How it works
-
-`Remove` hooks into the [`remove:remove`](/lib/plugins/remove) lifecycle. The first thing the plugin does
-is that it removes all the content in the core S3 bucket (which is used to e.g. store the zipped code of the
-lambda functions) so that the removal won't fail due to still available data in the bucket.
-
-Next up it starts the removal process by utilizing the CloudFormation `deleteStack` API functionality.
-The stack removal process is checked every 5 seconds. The stack is successfully create if a `DELETE_COMPLETE` stack
-status is returned.
diff --git a/lib/plugins/aws/tests/index.js b/lib/plugins/aws/tests/index.js
index e236bfb6f..553d097cf 100644
--- a/lib/plugins/aws/tests/index.js
+++ b/lib/plugins/aws/tests/index.js
@@ -5,17 +5,18 @@ const BbPromise = require('bluebird');
const expect = require('chai').expect;
const Serverless = require('../../../Serverless');
const AwsSdk = require('../');
+const proxyquire = require('proxyquire');
describe('AWS SDK', () => {
let awsSdk;
let serverless;
beforeEach(() => {
- serverless = new Serverless();
const options = {
stage: 'dev',
region: 'us-east-1',
};
+ serverless = new Serverless(options);
awsSdk = new AwsSdk(serverless, options);
awsSdk.serverless.cli = new serverless.classes.CLI();
});
@@ -181,28 +182,162 @@ describe('AWS SDK', () => {
});
describe('#getCredentials()', () => {
+ const mockCreds = (configParam) => {
+ const config = configParam;
+ delete config.credentials;
+ return config;
+ };
+ const awsStub = sinon.stub().returns();
+ const AwsSdkProxyquired = proxyquire('../index.js', {
+ 'aws-sdk': awsStub,
+ });
+
+ let newAwsSdk;
+
+ beforeEach(() => {
+ newAwsSdk = new AwsSdkProxyquired(serverless);
+ });
+
it('should set region for credentials', () => {
- const credentials = awsSdk.getCredentials('testregion');
+ const credentials = newAwsSdk.getCredentials('teststage', 'testregion');
expect(credentials.region).to.equal('testregion');
});
it('should get credentials from provider', () => {
serverless.service.provider.profile = 'notDefault';
- const credentials = awsSdk.getCredentials();
+ const credentials = newAwsSdk.getCredentials();
expect(credentials.credentials.profile).to.equal('notDefault');
});
it('should not set credentials if empty profile is set', () => {
serverless.service.provider.profile = '';
- const credentials = awsSdk.getCredentials('testregion');
+ const credentials = mockCreds(newAwsSdk.getCredentials('teststage', 'testregion'));
expect(credentials).to.eql({ region: 'testregion' });
});
+ it('should not set credentials if credentials is an empty object', () => {
+ serverless.service.provider.credentials = {};
+ const credentials = mockCreds(newAwsSdk.getCredentials('teststage', 'testregion'));
+ expect(credentials).to.eql({ region: 'testregion' });
+ });
+
+ it('should not set credentials if credentials has undefined values', () => {
+ serverless.service.provider.credentials = {
+ accessKeyId: undefined,
+ secretAccessKey: undefined,
+ sessionToken: undefined,
+ };
+ const credentials = mockCreds(newAwsSdk.getCredentials('teststage', 'testregion'));
+ expect(credentials).to.eql({ region: 'testregion' });
+ });
+
+ it('should not set credentials if credentials has empty string values', () => {
+ serverless.service.provider.credentials = {
+ accessKeyId: '',
+ secretAccessKey: '',
+ sessionToken: '',
+ };
+ const credentials = mockCreds(newAwsSdk.getCredentials('teststage', 'testregion'));
+ expect(credentials).to.eql({ region: 'testregion' });
+ });
+
+ it('should get credentials from provider declared credentials', () => {
+ const tmpAccessKeyID = process.env.AWS_ACCESS_KEY_ID;
+ const tmpAccessKeySecret = process.env.AWS_SECRET_ACCESS_KEY;
+ const tmpSessionToken = process.env.AWS_SESSION_TOKEN;
+
+ delete process.env.AWS_ACCESS_KEY_ID;
+ delete process.env.AWS_SECRET_ACCESS_KEY;
+ delete process.env.AWS_SESSION_TOKEN;
+
+ serverless.service.provider.credentials = {
+ accessKeyId: 'accessKeyId',
+ secretAccessKey: 'secretAccessKey',
+ sessionToken: 'sessionToken',
+ };
+ const credentials = newAwsSdk.getCredentials('teststage', 'testregion');
+ expect(credentials.credentials).to.deep.eql(serverless.service.provider.credentials);
+
+ process.env.AWS_ACCESS_KEY_ID = tmpAccessKeyID;
+ process.env.AWS_SECRET_ACCESS_KEY = tmpAccessKeySecret;
+ process.env.AWS_SESSION_TOKEN = tmpSessionToken;
+ });
+
+ it('should get credentials from environment declared for-all-stages credentials', () => {
+ const prevVal = {
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
+ sessionToken: process.env.AWS_SESSION_TOKEN,
+ };
+ const testVal = {
+ accessKeyId: 'accessKeyId',
+ secretAccessKey: 'secretAccessKey',
+ sessionToken: 'sessionToken',
+ };
+ process.env.AWS_ACCESS_KEY_ID = testVal.accessKeyId;
+ process.env.AWS_SECRET_ACCESS_KEY = testVal.secretAccessKey;
+ process.env.AWS_SESSION_TOKEN = testVal.sessionToken;
+ const credentials = newAwsSdk.getCredentials('teststage', 'testregion');
+ process.env.AWS_ACCESS_KEY_ID = prevVal.accessKeyId;
+ process.env.AWS_SECRET_ACCESS_KEY = prevVal.secretAccessKey;
+ process.env.AWS_SESSION_TOKEN = prevVal.sessionToken;
+ expect(credentials.credentials).to.deep.eql(testVal);
+ });
+
+ it('should get credentials from environment declared stage specific credentials', () => {
+ const prevVal = {
+ accessKeyId: process.env.AWS_TESTSTAGE_ACCESS_KEY_ID,
+ secretAccessKey: process.env.AWS_TESTSTAGE_SECRET_ACCESS_KEY,
+ sessionToken: process.env.AWS_TESTSTAGE_SESSION_TOKEN,
+ };
+ const testVal = {
+ accessKeyId: 'accessKeyId',
+ secretAccessKey: 'secretAccessKey',
+ sessionToken: 'sessionToken',
+ };
+ process.env.AWS_TESTSTAGE_ACCESS_KEY_ID = testVal.accessKeyId;
+ process.env.AWS_TESTSTAGE_SECRET_ACCESS_KEY = testVal.secretAccessKey;
+ process.env.AWS_TESTSTAGE_SESSION_TOKEN = testVal.sessionToken;
+ const credentials = newAwsSdk.getCredentials('teststage', 'testregion');
+ process.env.AWS_TESTSTAGE_ACCESS_KEY_ID = prevVal.accessKeyId;
+ process.env.AWS_TESTSTAGE_SECRET_ACCESS_KEY = prevVal.secretAccessKey;
+ process.env.AWS_TESTSTAGE_SESSION_TOKEN = prevVal.sessionToken;
+ expect(credentials.credentials).to.deep.eql(testVal);
+ });
+
it('should not set credentials if profile is not set', () => {
serverless.service.provider.profile = undefined;
- const credentials = awsSdk.getCredentials('testregion');
+ const credentials = mockCreds(newAwsSdk.getCredentials('teststage', 'testregion'));
expect(credentials).to.eql({ region: 'testregion' });
});
+
+ it('should not set credentials if empty profile is set', () => {
+ serverless.service.provider.profile = '';
+ const credentials = mockCreds(newAwsSdk.getCredentials('teststage', 'testregion'));
+ expect(credentials).to.eql({ region: 'testregion' });
+ });
+
+ it('should get credentials from provider declared profile', () => {
+ serverless.service.provider.profile = 'notDefault';
+ const credentials = newAwsSdk.getCredentials();
+ expect(credentials.credentials.profile).to.equal('notDefault');
+ });
+
+ it('should get credentials from environment declared for-all-stages profile', () => {
+ const prevVal = process.env.AWS_PROFILE;
+ process.env.AWS_PROFILE = 'notDefault';
+ const credentials = newAwsSdk.getCredentials();
+ process.env.AWS_PROFILE = prevVal;
+ expect(credentials.credentials.profile).to.equal('notDefault');
+ });
+
+ it('should get credentials from environment declared stage-specific profile', () => {
+ const prevVal = process.env.AWS_TESTSTAGE_PROFILE;
+ process.env.AWS_TESTSTAGE_PROFILE = 'notDefault';
+ const credentials = newAwsSdk.getCredentials('teststage', 'testregion');
+ process.env.AWS_TESTSTAGE_PROFILE = prevVal;
+ expect(credentials.credentials.profile).to.equal('notDefault');
+ });
});
describe('#getServerlessDeploymentBucketName', () => {
diff --git a/lib/plugins/aws/tests/monitorStack.js b/lib/plugins/aws/tests/monitorStack.js
index 92585105a..d07ebf812 100644
--- a/lib/plugins/aws/tests/monitorStack.js
+++ b/lib/plugins/aws/tests/monitorStack.js
@@ -255,22 +255,21 @@ describe('monitorStack', () => {
},
],
};
- const updateRollbackFailedEvent = {
+ const updateRollbackComplete = {
StackEvents: [
{
EventId: '1m2n3o4p',
LogicalResourceId: 'mocha',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
- ResourceStatus: 'UPDATE_ROLLBACK_COMPLETE',
+ ResourceStatus: 'ROLLBACK_COMPLETE',
},
],
};
-
describeStackEventsStub.onCall(0).returns(BbPromise.resolve(updateStartEvent));
describeStackEventsStub.onCall(1).returns(BbPromise.resolve(updateFailedEvent));
describeStackEventsStub.onCall(2).returns(BbPromise.resolve(updateRollbackEvent));
- describeStackEventsStub.onCall(3).returns(BbPromise.resolve(updateRollbackFailedEvent));
+ describeStackEventsStub.onCall(3).returns(BbPromise.resolve(updateRollbackComplete));
return awsPlugin.monitorStack('update', cfDataMock, 10).catch((e) => {
let errorMessage = 'An error occurred while provisioning your stack: ';
@@ -312,7 +311,7 @@ describe('monitorStack', () => {
});
});
- it('should throw an error if CloudFormation returned unusual stack status', () => {
+ it('should throw an error and exit immediataley if statck status is *_FAILED', () => {
const describeStackEventsStub = sinon.stub(awsPlugin.sdk, 'request');
const cfDataMock = {
StackId: 'new-service-dev',
@@ -373,7 +372,8 @@ describe('monitorStack', () => {
errorMessage += 'mochaS3 - Bucket already exists.';
expect(e.name).to.be.equal('ServerlessError');
expect(e.message).to.be.equal(errorMessage);
- expect(describeStackEventsStub.callCount).to.be.equal(4);
+ // callCount is 2 because Serverless will immediately exits and shows the error
+ expect(describeStackEventsStub.callCount).to.be.equal(2);
expect(describeStackEventsStub.args[0][2].StackName)
.to.be.equal(cfDataMock.StackId);
expect(describeStackEventsStub.calledWith(
diff --git a/lib/plugins/create/create.js b/lib/plugins/create/create.js
index d92a78f02..2e2d3b00a 100644
--- a/lib/plugins/create/create.js
+++ b/lib/plugins/create/create.js
@@ -23,7 +23,7 @@ class Create {
this.commands = {
create: {
- usage: 'Create new Serverless Service.',
+ usage: 'Create new Serverless service',
lifecycleEvents: [
'create',
],
diff --git a/lib/plugins/create/templates/README.md b/lib/plugins/create/templates/README.md
deleted file mode 100644
index 6badf52bb..000000000
--- a/lib/plugins/create/templates/README.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# Templates
-
-## Node.js
-
-Follow these simple steps to create and deploy your Node.js service, run your function and remove the service afterwards.
-
-1. install latest version of `serverless`
-2. `mkdir my-first-service && cd my-first-service`
-3. `serverless create --template aws-nodejs`
-4. `serverless deploy`
-5. `serverless invoke --function hello`
-6. `serverless remove`
-
-## Python
-
-Follow these simple steps to create and deploy your Python service, run your function and remove the service afterwards.
-
-1. install latest version of `serverless`
-2. `mkdir my-first-service && cd my-first-service`
-3. `serverless create --template aws-python`
-4. `serverless deploy`
-5. `serverless invoke --function hello`
-6. `serverless remove`
-
-## Java
-
-### Maven - Quick Start
-
-Follow these simple steps to create and deploy your java service using maven, run your function and remove the service
-afterwards.
-
-1. install latest version of `serverless` and `maven`
-2. `mkdir my-first-service && cd my-first-service`
-3. `serverless create --template aws-java-maven`
-4. `mvn package`
-5. `serverless deploy`
-6. `serverless invoke --function hello --path event.json`
-7. `serverless remove`
-
-### Gradle - Quick Start
-
-Follow these simple steps to create and deploy your java service using gradle, run your function and remove the service
-afterwards.
-
-1. install latest version of `serverless` and `gradle`
-2. `mkdir my-first-service && cd my-first-service`
-3. `serverless create --template aws-java-gradle`
-4. `gradle build`
-5. `serverless deploy`
-6. `serverless invoke --function hello --path event.json`
-7. `serverless remove`
diff --git a/lib/plugins/create/templates/aws-java-gradle/.gitignore b/lib/plugins/create/templates/aws-java-gradle/.gitignore
new file mode 100644
index 000000000..f715136e6
--- /dev/null
+++ b/lib/plugins/create/templates/aws-java-gradle/.gitignore
@@ -0,0 +1,11 @@
+*.class
+.gradle
+/build/
+
+# Package Files
+*.jar
+*.war
+*.ear
+
+# Serverless directories
+.serverless
\ No newline at end of file
diff --git a/lib/plugins/create/templates/aws-java-gradle/build.gradle b/lib/plugins/create/templates/aws-java-gradle/build.gradle
index a594de2fc..99b22b225 100644
--- a/lib/plugins/create/templates/aws-java-gradle/build.gradle
+++ b/lib/plugins/create/templates/aws-java-gradle/build.gradle
@@ -30,3 +30,7 @@ task buildZip(type: Zip) {
}
build.dependsOn buildZip
+
+task wrapper(type: Wrapper) {
+ gradleVersion = '3.1'
+}
diff --git a/lib/plugins/create/templates/aws-java-gradle/gradle/wrapper/gradle-wrapper.jar b/lib/plugins/create/templates/aws-java-gradle/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..6ffa23784
Binary files /dev/null and b/lib/plugins/create/templates/aws-java-gradle/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/lib/plugins/create/templates/aws-java-gradle/gradle/wrapper/gradle-wrapper.properties b/lib/plugins/create/templates/aws-java-gradle/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..7b6432035
--- /dev/null
+++ b/lib/plugins/create/templates/aws-java-gradle/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-bin.zip
diff --git a/lib/plugins/create/templates/aws-java-gradle/gradlew b/lib/plugins/create/templates/aws-java-gradle/gradlew
new file mode 100755
index 000000000..9aa616c27
--- /dev/null
+++ b/lib/plugins/create/templates/aws-java-gradle/gradlew
@@ -0,0 +1,169 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/lib/plugins/create/templates/aws-java-gradle/gradlew.bat b/lib/plugins/create/templates/aws-java-gradle/gradlew.bat
new file mode 100755
index 000000000..f9553162f
--- /dev/null
+++ b/lib/plugins/create/templates/aws-java-gradle/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/lib/plugins/create/templates/aws-java-gradle/serverless.yml b/lib/plugins/create/templates/aws-java-gradle/serverless.yml
index 980b0380f..f22d4090a 100644
--- a/lib/plugins/create/templates/aws-java-gradle/serverless.yml
+++ b/lib/plugins/create/templates/aws-java-gradle/serverless.yml
@@ -38,8 +38,6 @@ provider:
# you can add packaging information here
package:
-# include:
-# - include-me.java
# exclude:
# - exclude-me.java
artifact: build/distributions/hello.zip
@@ -48,14 +46,17 @@ functions:
hello:
handler: hello.Handler
-# you can add any of the following events
-# events:
-# - http:
-# path: users/create
-# method: get
-# - s3: ${env:bucket}
-# - schedule: rate(10 minutes)
-# - sns: greeter-topic
+# The following are a few example events you can configure
+# NOTE: Please make sure to change your handler code to work with those events
+# Check the event documentation for details
+# events:
+# - http:
+# path: users/create
+# method: get
+# - s3: ${env:BUCKET}
+# - schedule: rate(10 minutes)
+# - sns: greeter-topic
+# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
# you can add CloudFormation resource templates here
#resources:
diff --git a/lib/plugins/create/templates/aws-java-maven/.gitignore b/lib/plugins/create/templates/aws-java-maven/.gitignore
new file mode 100644
index 000000000..d6f2befce
--- /dev/null
+++ b/lib/plugins/create/templates/aws-java-maven/.gitignore
@@ -0,0 +1,10 @@
+*.class
+target
+
+# Package Files
+*.jar
+*.war
+*.ear
+
+# Serverless directories
+.serverless
\ No newline at end of file
diff --git a/lib/plugins/create/templates/aws-java-maven/serverless.yml b/lib/plugins/create/templates/aws-java-maven/serverless.yml
index 7cf35f58d..ed15a7cf5 100644
--- a/lib/plugins/create/templates/aws-java-maven/serverless.yml
+++ b/lib/plugins/create/templates/aws-java-maven/serverless.yml
@@ -38,8 +38,6 @@ provider:
# you can add packaging information here
package:
-# include:
-# - include-me.java
# exclude:
# - exclude-me.java
artifact: target/hello-dev.jar
@@ -48,7 +46,9 @@ functions:
hello:
handler: hello.Handler
-# you can add any of the following events
+# The following are a few example events you can configure
+# NOTE: Please make sure to change your handler code to work with those events
+# Check the event documentation for details
# events:
# - http:
# path: users/create
@@ -56,6 +56,7 @@ functions:
# - s3: ${env:BUCKET}
# - schedule: rate(10 minutes)
# - sns: greeter-topic
+# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
# you can add CloudFormation resource templates here
#resources:
diff --git a/lib/plugins/create/templates/aws-nodejs/.gitignore b/lib/plugins/create/templates/aws-nodejs/.gitignore
new file mode 100644
index 000000000..2b48c8bd5
--- /dev/null
+++ b/lib/plugins/create/templates/aws-nodejs/.gitignore
@@ -0,0 +1,6 @@
+# package directories
+node_modules
+jspm_packages
+
+# Serverless directories
+.serverless
\ No newline at end of file
diff --git a/lib/plugins/create/templates/aws-nodejs/handler.js b/lib/plugins/create/templates/aws-nodejs/handler.js
index abf8c236a..770e6415d 100644
--- a/lib/plugins/create/templates/aws-nodejs/handler.js
+++ b/lib/plugins/create/templates/aws-nodejs/handler.js
@@ -1,8 +1,16 @@
'use strict';
-// Your first function handler
-module.exports.hello = (event, context, cb) => {
- cb(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event });
-};
+module.exports.hello = (event, context, callback) => {
+ const response = {
+ statusCode: 200,
+ body: JSON.stringify({
+ message: 'Go Serverless v1.0! Your function executed successfully!',
+ input: event,
+ }),
+ };
-// You can add more handlers here, and reference them in serverless.yml
+ callback(null, response);
+
+ // Use this code if you don't use the http event with the LAMBDA-PROXY integration
+ // callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event });
+};
diff --git a/lib/plugins/create/templates/aws-nodejs/serverless.yml b/lib/plugins/create/templates/aws-nodejs/serverless.yml
index d5269276d..ef8ced41a 100644
--- a/lib/plugins/create/templates/aws-nodejs/serverless.yml
+++ b/lib/plugins/create/templates/aws-nodejs/serverless.yml
@@ -38,8 +38,6 @@ provider:
# you can add packaging information here
#package:
-# include:
-# - include-me.js
# exclude:
# - exclude-me.js
# artifact: my-service-code.zip
@@ -48,7 +46,9 @@ functions:
hello:
handler: handler.hello
-# you can add any of the following events
+# The following are a few example events you can configure
+# NOTE: Please make sure to change your handler code to work with those events
+# Check the event documentation for details
# events:
# - http:
# path: users/create
@@ -56,6 +56,7 @@ functions:
# - s3: ${env:BUCKET}
# - schedule: rate(10 minutes)
# - sns: greeter-topic
+# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
# you can add CloudFormation resource templates here
#resources:
diff --git a/lib/plugins/create/templates/aws-python/.gitignore b/lib/plugins/create/templates/aws-python/.gitignore
new file mode 100644
index 000000000..84c61a91b
--- /dev/null
+++ b/lib/plugins/create/templates/aws-python/.gitignore
@@ -0,0 +1,20 @@
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Serverless directories
+.serverless
\ No newline at end of file
diff --git a/lib/plugins/create/templates/aws-python/handler.py b/lib/plugins/create/templates/aws-python/handler.py
index fc62ea2d0..ea831b279 100644
--- a/lib/plugins/create/templates/aws-python/handler.py
+++ b/lib/plugins/create/templates/aws-python/handler.py
@@ -1,5 +1,22 @@
+import json
+
def hello(event, context):
+ body = {
+ "message": "Go Serverless v1.0! Your function executed successfully!",
+ "input": event
+ }
+
+ response = {
+ "statusCode": 200,
+ "body": json.dumps(body)
+ };
+
+ return response
+
+ # Use this code if you don't use the http event with the LAMBDA-PROXY integration
+ """
return {
"message": "Go Serverless v1.0! Your function executed successfully!",
"event": event
}
+ """
diff --git a/lib/plugins/create/templates/aws-python/serverless.yml b/lib/plugins/create/templates/aws-python/serverless.yml
index 61b00bb34..628bf21af 100644
--- a/lib/plugins/create/templates/aws-python/serverless.yml
+++ b/lib/plugins/create/templates/aws-python/serverless.yml
@@ -38,8 +38,6 @@ provider:
# you can add packaging information here
#package:
-# include:
-# - include-me.js
# exclude:
# - exclude-me.js
# artifact: my-service-code.zip
@@ -48,7 +46,9 @@ functions:
hello:
handler: handler.hello
-# you can add any of the following events
+# The following are a few example events you can configure
+# NOTE: Please make sure to change your handler code to work with those events
+# Check the event documentation for details
# events:
# - http:
# path: users/create
@@ -56,6 +56,7 @@ functions:
# - s3: ${env:BUCKET}
# - schedule: rate(10 minutes)
# - sns: greeter-topic
+# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
# you can add CloudFormation resource templates here
#resources:
diff --git a/lib/plugins/create/templates/aws-scala-sbt/.gitignore b/lib/plugins/create/templates/aws-scala-sbt/.gitignore
new file mode 100644
index 000000000..b2677ef71
--- /dev/null
+++ b/lib/plugins/create/templates/aws-scala-sbt/.gitignore
@@ -0,0 +1,16 @@
+*.class
+*.log
+
+# sbt specific
+.cache
+.history
+.lib/
+dist/*
+target/
+lib_managed/
+src_managed/
+project/boot/
+project/plugins/project/
+
+# Serverless directories
+.serverless
\ No newline at end of file
diff --git a/lib/plugins/create/templates/aws-scala-sbt/serverless.yml b/lib/plugins/create/templates/aws-scala-sbt/serverless.yml
index ca47838b1..2b2a55191 100644
--- a/lib/plugins/create/templates/aws-scala-sbt/serverless.yml
+++ b/lib/plugins/create/templates/aws-scala-sbt/serverless.yml
@@ -48,14 +48,17 @@ functions:
hello:
handler: hello.Handler
-# you can add any of the following events
-# events:
-# - http:
-# path: users/create
-# method: get
-# - s3: ${env:bucket}
-# - schedule: rate(10 minutes)
-# - sns: greeter-topic
+# The following are a few example events you can configure
+# NOTE: Please make sure to change your handler code to work with those events
+# Check the event documentation for details
+# events:
+# - http:
+# path: users/create
+# method: get
+# - s3: ${env:BUCKET}
+# - schedule: rate(10 minutes)
+# - sns: greeter-topic
+# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
# you can add CloudFormation resource templates here
#resources:
diff --git a/lib/plugins/create/tests/create.js b/lib/plugins/create/tests/create.js
index 6f6af52ee..234fe20dc 100644
--- a/lib/plugins/create/tests/create.js
+++ b/lib/plugins/create/tests/create.js
@@ -93,6 +93,8 @@ describe('Create', () => {
.to.be.equal(true);
expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'handler.js')))
.to.be.equal(true);
+ expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, '.gitignore')))
+ .to.be.equal(true);
process.chdir(cwd);
});
@@ -109,6 +111,8 @@ describe('Create', () => {
.to.be.equal(true);
expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'handler.py')))
.to.be.equal(true);
+ expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, '.gitignore')))
+ .to.be.equal(true);
process.chdir(cwd);
});
@@ -139,6 +143,8 @@ describe('Create', () => {
'hello', 'Response.java'
)))
.to.be.equal(true);
+ expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, '.gitignore')))
+ .to.be.equal(true);
process.chdir(cwd);
});
@@ -157,6 +163,16 @@ describe('Create', () => {
.to.be.equal(true);
expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'build.gradle')))
.to.be.equal(true);
+ expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'gradlew')))
+ .to.be.equal(true);
+ expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'gradlew.bat')))
+ .to.be.equal(true);
+ expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'gradle', 'wrapper',
+ 'gradle-wrapper.jar')))
+ .to.be.equal(true);
+ expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'gradle', 'wrapper',
+ 'gradle-wrapper.properties')))
+ .to.be.equal(true);
expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, 'src', 'main', 'java',
'hello', 'Handler.java'
)))
@@ -169,6 +185,8 @@ describe('Create', () => {
'hello', 'Response.java'
)))
.to.be.equal(true);
+ expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, '.gitignore')))
+ .to.be.equal(true);
process.chdir(cwd);
});
@@ -199,6 +217,8 @@ describe('Create', () => {
'hello', 'Response.scala'
)))
.to.be.equal(true);
+ expect(create.serverless.utils.fileExistsSync(path.join(tmpDir, '.gitignore')))
+ .to.be.equal(true);
process.chdir(cwd);
});
diff --git a/lib/plugins/deploy/deploy.js b/lib/plugins/deploy/deploy.js
index 6a4354078..e553e3a5b 100644
--- a/lib/plugins/deploy/deploy.js
+++ b/lib/plugins/deploy/deploy.js
@@ -6,7 +6,7 @@ class Deploy {
this.commands = {
deploy: {
- usage: 'Deploy Service.',
+ usage: 'Deploy a Serverless service',
lifecycleEvents: [
'cleanup',
'initialize',
@@ -36,7 +36,7 @@ class Deploy {
},
commands: {
function: {
- usage: 'Deploys a single function from the service',
+ usage: 'Deploy a single function from the service',
lifecycleEvents: [
'deploy',
],
diff --git a/lib/plugins/info/info.js b/lib/plugins/info/info.js
index 97327bd9a..e5889cce7 100644
--- a/lib/plugins/info/info.js
+++ b/lib/plugins/info/info.js
@@ -6,7 +6,7 @@ class Info {
this.commands = {
info: {
- usage: 'Displays information about the service.',
+ usage: 'Display information about the service',
lifecycleEvents: [
'info',
],
@@ -19,6 +19,10 @@ class Info {
usage: 'Region of the service',
shortcut: 'r',
},
+ verbose: {
+ usage: 'Display Stack output',
+ shortcut: 'v',
+ },
},
},
};
diff --git a/lib/plugins/install/install.js b/lib/plugins/install/install.js
new file mode 100644
index 000000000..cf50d0d9f
--- /dev/null
+++ b/lib/plugins/install/install.js
@@ -0,0 +1,91 @@
+'use strict';
+
+const BbPromise = require('bluebird');
+const path = require('path');
+const URL = require('url');
+const download = require('download');
+
+class Install {
+ constructor(serverless, options) {
+ this.serverless = serverless;
+ this.options = options;
+
+ this.commands = {
+ install: {
+ usage: 'Install a Serverless service from GitHub',
+ lifecycleEvents: [
+ 'install',
+ ],
+ options: {
+ url: {
+ usage: 'URL of the Serverless service on GitHub',
+ required: true,
+ shortcut: 'u',
+ },
+ },
+ },
+ };
+
+ this.hooks = {
+ 'install:install': () => BbPromise.bind(this)
+ .then(this.install),
+ };
+ }
+
+ install() {
+ const url = URL.parse(this.options.url);
+
+ // check if url parameter is a valid url
+ if (!url.host) {
+ throw new this.serverless.classes.Error('The URL you passed is not a valid URL');
+ }
+
+ const parts = url.pathname.split('/');
+ const parsedGitHubUrl = {
+ owner: parts[1],
+ repo: parts[2],
+ branch: 'master',
+ };
+
+ // validate if given url is a valid GitHub url
+ if (url.hostname !== 'github.com' || !parsedGitHubUrl.owner || !parsedGitHubUrl.repo) {
+ const errorMessage = [
+ 'The URL must be a valid GitHub URL in the following format:',
+ ' https://github.com/serverless/serverless',
+ ].join('');
+ throw new this.serverless.classes.Error(errorMessage);
+ }
+
+ const downloadUrl = [
+ 'https://github.com/',
+ parsedGitHubUrl.owner,
+ '/',
+ parsedGitHubUrl.repo,
+ '/archive/',
+ parsedGitHubUrl.branch,
+ '.zip',
+ ].join('');
+
+ const servicePath = path.join(process.cwd(), parsedGitHubUrl.repo);
+
+ // throw an error if service path already exists
+ if (this.serverless.utils.dirExistsSync(servicePath)) {
+ const errorMessage = `A folder named "${parsedGitHubUrl.repo}" already exists.`;
+ throw new this.serverless.classes.Error(errorMessage);
+ }
+
+ this.serverless.cli.log(`Downloading and installing "${parsedGitHubUrl.repo}"...`);
+ const that = this;
+
+ // download service
+ return download(
+ downloadUrl,
+ servicePath,
+ { timeout: 30000, extract: true, strip: 1, mode: '755' }
+ ).then(() => {
+ that.serverless.cli.log(`Successfully installed "${parsedGitHubUrl.repo}".`);
+ });
+ }
+}
+
+module.exports = Install;
diff --git a/lib/plugins/install/tests/install.js b/lib/plugins/install/tests/install.js
new file mode 100644
index 000000000..f099a2c21
--- /dev/null
+++ b/lib/plugins/install/tests/install.js
@@ -0,0 +1,81 @@
+'use strict';
+
+const expect = require('chai').expect;
+const Serverless = require('../../../Serverless');
+const sinon = require('sinon');
+const BbPromise = require('bluebird');
+const testUtils = require('../../../../tests/utils');
+const fse = require('fs-extra');
+const path = require('path');
+const proxyquire = require('proxyquire');
+
+const downloadStub = sinon.stub().returns(BbPromise.resolve());
+const Install = proxyquire('../install.js', {
+ download: downloadStub,
+});
+
+describe('Install', () => {
+ let install;
+ let serverless;
+
+ beforeEach(() => {
+ serverless = new Serverless();
+ install = new Install(serverless);
+ serverless.init();
+ });
+
+ describe('#constructor()', () => {
+ it('should have commands', () => expect(install.commands).to.be.not.empty);
+
+ it('should have hooks', () => expect(install.hooks).to.be.not.empty);
+
+ it('should run promise chain in order for "install:install" hook', () => {
+ const installStub = sinon
+ .stub(install, 'install').returns(BbPromise.resolve());
+
+ return install.hooks['install:install']().then(() => {
+ expect(installStub.calledOnce).to.be.equal(true);
+
+ install.install.restore();
+ });
+ });
+ });
+
+ describe('#install()', () => {
+ it('shold throw an error if the passed URL option is not a valid URL', () => {
+ install.options = { url: 'invalidUrl' };
+
+ expect(() => install.install()).to.throw(Error);
+ });
+
+ it('should throw an error if the passed URL is not a valid GitHub URL', () => {
+ install.options = { url: 'http://no-github-url.com/foo/bar' };
+
+ expect(() => install.install()).to.throw(Error);
+ });
+
+ it('should throw an error if a directory with the same service name is already present', () => {
+ install.options = { url: 'https://github.com/johndoe/existing-service' };
+
+ const tmpDir = testUtils.getTmpDirPath();
+ const serviceDirName = path.join(tmpDir, 'existing-service');
+ fse.mkdirsSync(serviceDirName);
+
+ const cwd = process.cwd();
+ process.chdir(tmpDir);
+
+ expect(() => install.install()).to.throw(Error);
+
+ process.chdir(cwd);
+ });
+
+ it('should download the service based on the GitHub URL', () => {
+ install.options = { url: 'https://github.com/johndoe/service-to-be-downloaded' };
+
+ return install.install().then(() => {
+ expect(downloadStub.calledOnce).to.equal(true);
+ expect(downloadStub.args[0][0]).to.equal(`${install.options.url}/archive/master.zip`);
+ });
+ });
+ });
+});
diff --git a/lib/plugins/invoke/invoke.js b/lib/plugins/invoke/invoke.js
index 307b8358f..5434126a6 100644
--- a/lib/plugins/invoke/invoke.js
+++ b/lib/plugins/invoke/invoke.js
@@ -6,7 +6,7 @@ class Invoke {
this.commands = {
invoke: {
- usage: 'Invokes a deployed function.',
+ usage: 'Invoke a deployed function',
lifecycleEvents: [
'invoke',
],
diff --git a/lib/plugins/logs/logs.js b/lib/plugins/logs/logs.js
index d293967e5..3cb9b4db0 100644
--- a/lib/plugins/logs/logs.js
+++ b/lib/plugins/logs/logs.js
@@ -6,7 +6,7 @@ class Logs {
this.commands = {
logs: {
- usage: 'Outputs the logs of a deployed function.',
+ usage: 'Output the logs of a deployed function',
lifecycleEvents: [
'logs',
],
diff --git a/lib/plugins/package/README.md b/lib/plugins/package/README.md
deleted file mode 100644
index 259889fb0..000000000
--- a/lib/plugins/package/README.md
+++ /dev/null
@@ -1,20 +0,0 @@
-# Package
-
-This plugin creates a deployment package on a per service basis (it will zip up all code for the whole service). It does not provide any executable command.
-
-## How it works
-
-`Package` starts by hooking into the [`deploy:createDeploymentArtifacts`](/lib/plugins/deploy) lifecycle.
-
-It will zip the whole service directory. The artifact will be stored in the `.serverless` directory which will be created
-upon zipping the service. The resulting path to the artifact will be appended to the `service.package.artifact` property.
-
-The services `include` and `exclude` arrays are considered during zipping. At first the `exclude` will be applied. After
-that the `include` will be applied to ensure that previously excluded files and folders can be included again.
-
-Serverless will automatically exclude `.git`, `.gitignore`, `serverless.yml`, and `.DS_Store`.
-
-Servlerless will skip this step if the user has defined it's own artifact in the `service.package.artifact` property.
-
-At the end it will do a cleanup by hooking into the `[after:deploy:deploy]`(/lib/plugins/deploy) lifecycle to remove the
-`.serverless` directory.
diff --git a/lib/plugins/package/lib/packageService.js b/lib/plugins/package/lib/packageService.js
index a894fd0e1..fbc0db914 100644
--- a/lib/plugins/package/lib/packageService.js
+++ b/lib/plugins/package/lib/packageService.js
@@ -21,11 +21,6 @@ module.exports = {
return _.union(exclude, packageExcludes, this.defaultExcludes);
},
- getIncludedPaths(include) {
- const packageIncludes = this.serverless.service.package.include || [];
- return _.union(include, packageIncludes);
- },
-
getServiceArtifactName() {
return `${this.serverless.service.service}.zip`;
},
@@ -57,10 +52,9 @@ module.exports = {
const servicePath = this.serverless.config.servicePath;
const exclude = this.getExcludedPaths();
- const include = this.getIncludedPaths();
const zipFileName = this.getServiceArtifactName();
- return this.zipDirectory(servicePath, exclude, include, zipFileName).then(filePath => {
+ return this.zipDirectory(servicePath, exclude, zipFileName).then(filePath => {
this.serverless.service.package.artifact = filePath;
return filePath;
});
@@ -80,10 +74,9 @@ module.exports = {
const servicePath = this.serverless.config.servicePath;
const exclude = this.getExcludedPaths(funcPackageConfig.exclude);
- const include = this.getIncludedPaths(funcPackageConfig.include);
const zipFileName = this.getFunctionArtifactName(functionObject);
- return this.zipDirectory(servicePath, exclude, include, zipFileName).then((artifactPath) => {
+ return this.zipDirectory(servicePath, exclude, zipFileName).then((artifactPath) => {
functionObject.artifact = artifactPath;
return artifactPath;
});
diff --git a/lib/plugins/package/lib/zipService.js b/lib/plugins/package/lib/zipService.js
index 33a30f6d6..c2977174c 100644
--- a/lib/plugins/package/lib/zipService.js
+++ b/lib/plugins/package/lib/zipService.js
@@ -4,13 +4,19 @@ const archiver = require('archiver');
const BbPromise = require('bluebird');
const path = require('path');
const fs = require('fs');
+const glob = require('glob');
module.exports = {
- zipDirectory(servicePath, exclude, include, zipFileName) {
+ zipDirectory(servicePath, exclude, zipFileName) {
+ exclude.push('.serverless/**');
+
const zip = archiver.create('zip');
- const artifactFilePath = path.join(servicePath,
- '.serverless', zipFileName);
+ const artifactFilePath = path.join(
+ servicePath,
+ '.serverless',
+ zipFileName
+ );
this.serverless.utils.writeFileDir(artifactFilePath);
@@ -19,22 +25,26 @@ module.exports = {
output.on('open', () => {
zip.pipe(output);
- this.serverless.utils.walkDirSync(servicePath).forEach((filePath) => {
- const relativeFilePath = path.relative(servicePath, filePath);
+ const files = glob.sync('**', {
+ cwd: servicePath,
+ ignore: exclude,
+ dot: true,
+ silent: true,
+ });
- // ensure we don't include the new zip file in our zip
- if (relativeFilePath.startsWith('.serverless')) return;
+ files.forEach((filePath) => {
+ const fullPath = path.resolve(
+ servicePath,
+ filePath
+ );
- const shouldBeExcluded =
- exclude.some(value => relativeFilePath.toLowerCase().indexOf(value.toLowerCase()) > -1);
+ const stats = fs.statSync(fullPath);
- const shouldBeIncluded =
- include.some(value => relativeFilePath.toLowerCase().indexOf(value.toLowerCase()) > -1);
-
- if (!shouldBeExcluded || shouldBeIncluded) {
- const permissions = fs.statSync(filePath).mode;
-
- zip.append(fs.readFileSync(filePath), { name: relativeFilePath, mode: permissions });
+ if (!stats.isDirectory(fullPath)) {
+ zip.append(fs.readFileSync(fullPath), {
+ name: filePath,
+ mode: stats.mode,
+ });
}
});
diff --git a/lib/plugins/package/tests/packageService.js b/lib/plugins/package/tests/packageService.js
index 74777d813..ce34a45f0 100644
--- a/lib/plugins/package/tests/packageService.js
+++ b/lib/plugins/package/tests/packageService.js
@@ -62,24 +62,6 @@ describe('#packageService()', () => {
});
});
- describe('#getIncludedPaths()', () => {
- it('should include defaults', () => {
- const include = packageService.getIncludedPaths();
- expect(include).to.deep.equal([]);
- });
-
- it('should return package includes', () => {
- const packageIncludes = [
- 'dir', 'file.js',
- ];
-
- serverless.service.package.include = packageIncludes;
-
- const exclude = packageService.getIncludedPaths();
- expect(exclude).to.deep.equal(packageIncludes);
- });
- });
-
describe('#getServiceArtifactName()', () => {
it('should create name with time', () => {
const name = packageService.getServiceArtifactName();
@@ -149,7 +131,6 @@ describe('#packageService()', () => {
it('should call zipService with settings', () => {
const servicePath = 'test';
const exclude = ['test-exclude'];
- const include = ['test-include'];
const artifactName = 'test-artifact.zip';
const artifactFilePath = '/some/fake/path/test-artifact.zip';
@@ -157,8 +138,6 @@ describe('#packageService()', () => {
const getExcludedPathsStub = sinon
.stub(packageService, 'getExcludedPaths').returns(exclude);
- const getIncludedPathsStub = sinon
- .stub(packageService, 'getIncludedPaths').returns(include);
const getServiceArtifactNameStub = sinon
.stub(packageService, 'getServiceArtifactName').returns(artifactName);
@@ -167,14 +146,12 @@ describe('#packageService()', () => {
return packageService.packageAll().then(() => {
expect(getExcludedPathsStub.calledOnce).to.be.equal(true);
- expect(getIncludedPathsStub.calledOnce).to.be.equal(true);
expect(getServiceArtifactNameStub.calledOnce).to.be.equal(true);
expect(zipDirectoryStub.calledOnce).to.be.equal(true);
expect(zipDirectoryStub.args[0][0]).to.be.equal(servicePath);
expect(zipDirectoryStub.args[0][1]).to.be.equal(exclude);
- expect(zipDirectoryStub.args[0][2]).to.be.equal(include);
- expect(zipDirectoryStub.args[0][3]).to.be.equal(artifactName);
+ expect(zipDirectoryStub.args[0][2]).to.be.equal(artifactName);
expect(serverless.service.package.artifact).to.be.equal(artifactFilePath);
});
@@ -187,7 +164,6 @@ describe('#packageService()', () => {
const funcName = 'test-func';
const exclude = ['test-exclude'];
- const include = ['test-include'];
const artifactName = 'test-artifact.zip';
const artifactFilePath = '/some/fake/path/test-artifact.zip';
@@ -197,8 +173,6 @@ describe('#packageService()', () => {
const getExcludedPathsStub = sinon
.stub(packageService, 'getExcludedPaths').returns(exclude);
- const getIncludedPathsStub = sinon
- .stub(packageService, 'getIncludedPaths').returns(include);
const getFunctionArtifactNameStub = sinon
.stub(packageService, 'getFunctionArtifactName').returns(artifactName);
@@ -207,14 +181,12 @@ describe('#packageService()', () => {
return packageService.packageFunction(funcName).then((filePath) => {
expect(getExcludedPathsStub.calledOnce).to.be.equal(true);
- expect(getIncludedPathsStub.calledOnce).to.be.equal(true);
expect(getFunctionArtifactNameStub.calledOnce).to.be.equal(true);
expect(zipDirectoryStub.calledOnce).to.be.equal(true);
expect(zipDirectoryStub.args[0][0]).to.be.equal(servicePath);
expect(zipDirectoryStub.args[0][1]).to.be.equal(exclude);
- expect(zipDirectoryStub.args[0][2]).to.be.equal(include);
- expect(zipDirectoryStub.args[0][3]).to.be.equal(artifactName);
+ expect(zipDirectoryStub.args[0][2]).to.be.equal(artifactName);
expect(filePath).to.be.equal(artifactFilePath);
});
diff --git a/lib/plugins/package/tests/zipService.js b/lib/plugins/package/tests/zipService.js
index 723bab8f2..a9714fb1e 100644
--- a/lib/plugins/package/tests/zipService.js
+++ b/lib/plugins/package/tests/zipService.js
@@ -33,6 +33,14 @@ describe('#zipService()', () => {
permissions: 444,
},
},
+ 'node_modules/include-me': {
+ include: 'some-file-content',
+ 'include-aswell': 'some-file content',
+ },
+ 'node_modules/exclude-me': {
+ exclude: 'some-file-content',
+ 'exclude-aswell': 'some-file content',
+ },
'exclude-me': {
'some-file': 'some-file content',
},
@@ -86,11 +94,10 @@ describe('#zipService()', () => {
it('should zip a whole service', () => {
const exclude = [];
- const include = [];
const zipFileName = getTestArtifactFileName('whole-service');
return packageService
- .zipDirectory(servicePath, exclude, include, zipFileName).then((artifact) => {
+ .zipDirectory(servicePath, exclude, zipFileName).then((artifact) => {
const data = fs.readFileSync(artifact);
return zip.loadAsync(data);
@@ -98,7 +105,7 @@ describe('#zipService()', () => {
const unzippedFileData = unzippedData.files;
expect(Object.keys(unzippedFileData)
- .filter(file => !unzippedFileData[file].dir).length).to.equal(9);
+ .filter(file => !unzippedFileData[file].dir).length).to.equal(13);
expect(unzippedFileData['handler.js'].name)
.to.equal('handler.js');
@@ -126,15 +133,26 @@ describe('#zipService()', () => {
expect(unzippedFileData['a-serverless-plugin.js'].name)
.to.equal('a-serverless-plugin.js');
+
+ expect(unzippedFileData['node_modules/include-me/include'].name)
+ .to.equal('node_modules/include-me/include');
+
+ expect(unzippedFileData['node_modules/include-me/include-aswell'].name)
+ .to.equal('node_modules/include-me/include-aswell');
+
+ expect(unzippedFileData['node_modules/exclude-me/exclude'].name)
+ .to.equal('node_modules/exclude-me/exclude');
+
+ expect(unzippedFileData['node_modules/exclude-me/exclude-aswell'].name)
+ .to.equal('node_modules/exclude-me/exclude-aswell');
});
});
it('should keep file permissions', () => {
const exclude = [];
- const include = [];
const zipFileName = getTestArtifactFileName('file-permissions');
- return packageService.zipDirectory(servicePath, exclude, include, zipFileName)
+ return packageService.zipDirectory(servicePath, exclude, zipFileName)
.then((artifact) => {
const data = fs.readFileSync(artifact);
return zip.loadAsync(data);
@@ -157,45 +175,15 @@ describe('#zipService()', () => {
});
});
- it('should exclude defined files and folders', () => {
- const exclude = ['exclude-me.js', 'exclude-me'];
- const include = [];
- const zipFileName = getTestArtifactFileName('exclude');
+ it('should exclude globs', () => {
+ const exclude = [
+ 'exclude-me*/**',
+ 'node_modules/exclude-me/**',
+ ];
- return packageService.zipDirectory(servicePath, exclude, include, zipFileName)
- .then((artifact) => {
- const data = fs.readFileSync(artifact);
-
- return zip.loadAsync(data);
- }).then(unzippedData => {
- const unzippedFileData = unzippedData.files;
-
- expect(Object.keys(unzippedFileData)
- .filter(file => !unzippedFileData[file].dir).length).to.equal(7);
-
- expect(unzippedFileData['handler.js'].name)
- .to.equal('handler.js');
-
- expect(unzippedFileData['lib/function.js'].name)
- .to.equal('lib/function.js');
-
- expect(unzippedFileData['include-me.js'].name)
- .to.equal('include-me.js');
-
- expect(unzippedFileData['include-me/some-file'].name)
- .to.equal('include-me/some-file');
-
- expect(unzippedFileData['a-serverless-plugin.js'].name)
- .to.equal('a-serverless-plugin.js');
- });
- });
-
- it('should include a previously excluded file', () => {
- const exclude = ['exclude-me.js', 'exclude-me'];
- const include = ['exclude-me.js', 'exclude-me'];
const zipFileName = getTestArtifactFileName('re-include');
- return packageService.zipDirectory(servicePath, exclude, include, zipFileName)
+ return packageService.zipDirectory(servicePath, exclude, zipFileName)
.then((artifact) => {
const data = fs.readFileSync(artifact);
@@ -218,14 +206,14 @@ describe('#zipService()', () => {
expect(unzippedFileData['include-me/some-file'].name)
.to.equal('include-me/some-file');
- expect(unzippedFileData['exclude-me.js'].name)
- .to.equal('exclude-me.js');
-
- expect(unzippedFileData['exclude-me/some-file'].name)
- .to.equal('exclude-me/some-file');
-
expect(unzippedFileData['a-serverless-plugin.js'].name)
.to.equal('a-serverless-plugin.js');
+
+ expect(unzippedFileData['node_modules/include-me/include'].name)
+ .to.equal('node_modules/include-me/include');
+
+ expect(unzippedFileData['node_modules/include-me/include-aswell'].name)
+ .to.equal('node_modules/include-me/include-aswell');
});
});
});
diff --git a/lib/plugins/remove/remove.js b/lib/plugins/remove/remove.js
index 01b7cca14..e7d85cfc4 100644
--- a/lib/plugins/remove/remove.js
+++ b/lib/plugins/remove/remove.js
@@ -6,7 +6,7 @@ class Remove {
this.commands = {
remove: {
- usage: 'Remove resources.',
+ usage: 'Remove Serverless service and all resources',
lifecycleEvents: [
'remove',
],
diff --git a/lib/plugins/slstats/slstats.js b/lib/plugins/slstats/slstats.js
new file mode 100644
index 000000000..cc13b397b
--- /dev/null
+++ b/lib/plugins/slstats/slstats.js
@@ -0,0 +1,56 @@
+'use strict';
+
+const path = require('path');
+const fse = require('fs-extra');
+const os = require('os');
+
+class SlStats {
+ constructor(serverless, options) {
+ this.serverless = serverless;
+ this.options = options;
+
+ this.commands = {
+ slstats: {
+ usage: 'Enable or disable stats',
+ lifecycleEvents: [
+ 'slstats',
+ ],
+ options: {
+ enable: {
+ usage: 'Enable stats ("--enable")',
+ shortcut: 'e',
+ },
+ disable: {
+ usage: 'Disable stats ("--disable")',
+ shortcut: 'd',
+ },
+ },
+ },
+ };
+
+ this.hooks = {
+ 'slstats:slstats': this.toggleStats.bind(this),
+ };
+ }
+
+ toggleStats() {
+ const serverlessDirPath = path.join(os.homedir(), '.serverless');
+ const statsDisabledFilePath = path.join(serverlessDirPath, 'stats-disabled');
+ const statsEnabledFilePath = path.join(serverlessDirPath, 'stats-enabled');
+
+ if (this.options.enable && !this.options.disable) {
+ if (this.serverless.utils.fileExistsSync(statsDisabledFilePath)) {
+ fse.renameSync(statsDisabledFilePath, statsEnabledFilePath);
+ }
+ this.serverless.cli.log('Stats successfully enabled');
+ }
+ if (this.options.disable && !this.options.enable) {
+ if (this.serverless.utils.fileExistsSync(statsEnabledFilePath)) {
+ fse.renameSync(statsEnabledFilePath, statsDisabledFilePath);
+ }
+ this.serverless.cli.log('Stats successfully disabled');
+ }
+ }
+}
+
+module.exports = SlStats;
diff --git a/lib/plugins/slstats/tests/slstats.js b/lib/plugins/slstats/tests/slstats.js
new file mode 100644
index 000000000..f01c22393
--- /dev/null
+++ b/lib/plugins/slstats/tests/slstats.js
@@ -0,0 +1,92 @@
+'use strict';
+
+const expect = require('chai').expect;
+const path = require('path');
+const fse = require('fs-extra');
+const os = require('os');
+const SlStats = require('../slstats');
+const Serverless = require('../../../Serverless');
+const testUtils = require('../../../../tests/utils');
+
+describe('SlStats', () => {
+ let slStats;
+ let serverless;
+ let homeDir;
+ let serverlessDirPath;
+
+ beforeEach(() => {
+ serverless = new Serverless();
+ serverless.init();
+ slStats = new SlStats(serverless);
+ });
+
+ describe('#constructor()', () => {
+ it('should have access to the serverless instance', () => {
+ expect(slStats.serverless).to.deep.equal(serverless);
+ });
+
+ it('should have commands', () => expect(slStats.commands).to.be.not.empty);
+
+ it('should have hooks', () => expect(slStats.hooks).to.be.not.empty);
+ });
+
+ describe('#toogleStats()', () => {
+ beforeEach(() => {
+ const tmpDirPath = testUtils.getTmpDirPath();
+ fse.mkdirsSync(tmpDirPath);
+
+ // save the homeDir so that we can reset this later on
+ homeDir = os.homedir();
+ process.env.HOME = tmpDirPath;
+ process.env.HOMEPATH = tmpDirPath;
+ process.env.USERPROFILE = tmpDirPath;
+
+ serverlessDirPath = path.join(os.homedir(), '.serverless');
+ });
+
+ it('should rename the stats file to stats-disabled if disabled', () => {
+ // create a stats-enabled file
+ serverless.utils.writeFileSync(
+ path.join(serverlessDirPath, 'stats-enabled'),
+ 'some content'
+ );
+
+ slStats.options = { disable: true };
+
+ slStats.toggleStats();
+
+ expect(
+ serverless.utils.fileExistsSync(path.join(serverlessDirPath, 'stats-disabled'))
+ ).to.equal(true);
+ expect(
+ serverless.utils.fileExistsSync(path.join(serverlessDirPath, 'stats-enabled'))
+ ).to.equal(false);
+ });
+
+ it('should rename the stats file to stats-enabled if enabled', () => {
+ // create a stats-disabled file
+ serverless.utils.writeFileSync(
+ path.join(serverlessDirPath, 'stats-disabled'),
+ 'some content'
+ );
+
+ slStats.options = { enable: true };
+
+ slStats.toggleStats();
+
+ expect(
+ serverless.utils.fileExistsSync(path.join(serverlessDirPath, 'stats-enabled'))
+ ).to.equal(true);
+ expect(
+ serverless.utils.fileExistsSync(path.join(serverlessDirPath, 'stats-disabled'))
+ ).to.equal(false);
+ });
+
+ afterEach(() => {
+ // recover the homeDir
+ process.env.HOME = homeDir;
+ process.env.HOMEPATH = homeDir;
+ process.env.USERPROFILE = homeDir;
+ });
+ });
+});
diff --git a/lib/plugins/tracking/tests/tracking.js b/lib/plugins/tracking/tests/tracking.js
deleted file mode 100644
index f11107522..000000000
--- a/lib/plugins/tracking/tests/tracking.js
+++ /dev/null
@@ -1,60 +0,0 @@
-'use strict';
-
-const expect = require('chai').expect;
-const Tracking = require('../tracking');
-const Serverless = require('../../../Serverless');
-const path = require('path');
-const fse = require('fs-extra');
-const testUtils = require('../../../../tests/utils');
-
-describe('Tracking', () => {
- let tracking;
- let serverless;
-
- beforeEach(() => {
- serverless = new Serverless();
- serverless.init();
- tracking = new Tracking(serverless);
- });
-
- describe('#constructor()', () => {
- it('should have access to the serverless instance', () => {
- expect(tracking.serverless).to.deep.equal(serverless);
- });
-
- it('should have commands', () => expect(tracking.commands).to.be.not.empty);
-
- it('should have hooks', () => expect(tracking.hooks).to.be.not.empty);
- });
-
- describe('#tracking()', () => {
- beforeEach(() => {
- const tmpDirPath = testUtils.getTmpDirPath();
- fse.mkdirsSync(tmpDirPath);
-
- serverless.config.serverlessPath = tmpDirPath;
- });
-
- it('should write a file in the Serverless path if tracking is disabled', () => {
- tracking.options = { disable: true };
-
- tracking.toggleTracking();
-
- expect(
- serverless.utils.fileExistsSync(path.join(tracking.serverless.config.serverlessPath,
- 'do-not-track'))
- ).to.equal(true);
- });
-
- it('should remove the file in the Serverless path if tracking is enabled', () => {
- tracking.options = { enable: true };
-
- tracking.toggleTracking();
-
- expect(
- serverless.utils.fileExistsSync(path.join(tracking.serverless.config.serverlessPath,
- 'do-not-track'))
- ).to.equal(false);
- });
- });
-});
diff --git a/lib/plugins/tracking/tracking.js b/lib/plugins/tracking/tracking.js
deleted file mode 100644
index 10115bdaa..000000000
--- a/lib/plugins/tracking/tracking.js
+++ /dev/null
@@ -1,52 +0,0 @@
-'use strict';
-
-const path = require('path');
-const fs = require('fs');
-const fse = require('fs-extra');
-
-class Tracking {
- constructor(serverless, options) {
- this.serverless = serverless;
- this.options = options;
-
- this.commands = {
- tracking: {
- usage: 'Enable or disable usage tracking.',
- lifecycleEvents: [
- 'tracking',
- ],
- options: {
- enable: {
- usage: 'Enable tracking ("--enable")',
- shortcut: 'e',
- },
- disable: {
- usage: 'Disable tracking ("--disable")',
- shortcut: 'd',
- },
- },
- },
- };
-
- this.hooks = {
- 'tracking:tracking': this.toggleTracking.bind(this),
- };
- }
-
- toggleTracking() {
- const serverlessPath = this.serverless.config.serverlessPath;
- const trackingFileName = 'do-not-track';
-
- if (this.options.enable && !this.options.disable) {
- fse.removeSync(path.join(serverlessPath, trackingFileName));
- this.serverless.cli.log('Tracking successfully enabled');
- }
- if (this.options.disable && !this.options.enable) {
- fs.writeFileSync(path.join(serverlessPath, trackingFileName),
- 'Keep this file to disable tracking');
- this.serverless.cli.log('Tracking successfully disabled');
- }
- }
-}
-
-module.exports = Tracking;
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
index 04d1609e1..e71a69bb9 100644
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -1,42 +1,12 @@
{
"name": "serverless",
- "version": "1.0.0-rc.2",
+ "version": "1.0.2",
"dependencies": {
- "abbrev": {
- "version": "1.0.9",
- "from": "abbrev@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz"
- },
- "acorn": {
- "version": "3.3.0",
- "from": "acorn@>=3.3.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz"
- },
- "acorn-jsx": {
- "version": "3.0.1",
- "from": "acorn-jsx@>=3.0.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz"
- },
"agent-base": {
"version": "2.0.1",
"from": "agent-base@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-2.0.1.tgz"
},
- "align-text": {
- "version": "0.1.4",
- "from": "align-text@>=0.1.3 <0.2.0",
- "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz"
- },
- "amdefine": {
- "version": "1.0.0",
- "from": "amdefine@>=0.0.4",
- "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz"
- },
- "ansi-escapes": {
- "version": "1.4.0",
- "from": "ansi-escapes@>=1.1.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz"
- },
"ansi-regex": {
"version": "2.0.0",
"from": "ansi-regex@>=2.0.0 <3.0.0",
@@ -52,93 +22,55 @@
"from": "archiver@>=1.1.0 <2.0.0",
"resolved": "https://registry.npmjs.org/archiver/-/archiver-1.1.0.tgz",
"dependencies": {
- "archiver-utils": {
- "version": "1.3.0",
- "from": "archiver-utils@>=1.3.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-1.3.0.tgz"
- },
"async": {
"version": "2.0.1",
"from": "async@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/async/-/async-2.0.1.tgz"
- },
- "compress-commons": {
- "version": "1.1.0",
- "from": "compress-commons@>=1.1.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.1.0.tgz"
- },
- "zip-stream": {
- "version": "1.1.0",
- "from": "zip-stream@>=1.1.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.1.0.tgz"
}
}
},
+ "archiver-utils": {
+ "version": "1.3.0",
+ "from": "archiver-utils@>=1.3.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-1.3.0.tgz"
+ },
"argparse": {
"version": "1.0.7",
"from": "argparse@>=1.0.7 <2.0.0",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.7.tgz"
},
- "array-union": {
- "version": "1.0.2",
- "from": "array-union@>=1.0.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz"
- },
- "array-uniq": {
- "version": "1.0.3",
- "from": "array-uniq@>=1.0.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz"
- },
- "arrify": {
- "version": "1.0.1",
- "from": "arrify@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz"
- },
- "asn1": {
- "version": "0.2.3",
- "from": "asn1@>=0.2.3 <0.3.0",
- "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz"
- },
- "assert-plus": {
- "version": "0.2.0",
- "from": "assert-plus@>=0.2.0 <0.3.0",
- "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz"
- },
- "assertion-error": {
- "version": "1.0.2",
- "from": "assertion-error@>=1.0.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz"
- },
"async": {
"version": "1.5.2",
"from": "async@>=1.5.2 <2.0.0",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz"
},
- "asynckit": {
- "version": "0.4.0",
- "from": "asynckit@>=0.4.0 <0.5.0",
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
- },
"aws-sdk": {
- "version": "2.5.3",
- "from": "aws-sdk@>=2.3.17 <3.0.0",
- "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.5.3.tgz"
- },
- "aws-sign2": {
- "version": "0.6.0",
- "from": "aws-sign2@>=0.6.0 <0.7.0",
- "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz"
- },
- "aws4": {
- "version": "1.4.1",
- "from": "aws4@>=1.2.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.4.1.tgz"
+ "version": "2.6.7",
+ "from": "aws-sdk@2.6.7",
+ "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.6.7.tgz",
+ "dependencies": {
+ "base64-js": {
+ "version": "1.2.0",
+ "from": "base64-js@>=1.0.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz"
+ },
+ "buffer": {
+ "version": "4.9.1",
+ "from": "buffer@4.9.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz"
+ }
+ }
},
"balanced-match": {
"version": "0.4.2",
"from": "balanced-match@>=0.4.1 <0.5.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz"
},
+ "base64-js": {
+ "version": "0.0.8",
+ "from": "base64-js@0.0.8",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz"
+ },
"bl": {
"version": "1.1.2",
"from": "bl@>=1.1.2 <1.2.0",
@@ -152,24 +84,19 @@
}
},
"bluebird": {
- "version": "3.4.3",
- "from": "bluebird@>=3.4.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.3.tgz"
- },
- "boom": {
- "version": "2.10.1",
- "from": "boom@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz"
+ "version": "3.4.6",
+ "from": "bluebird@3.4.6",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.6.tgz"
},
"brace-expansion": {
"version": "1.1.6",
"from": "brace-expansion@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz"
},
- "browser-stdout": {
- "version": "1.3.0",
- "from": "browser-stdout@1.3.0",
- "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz"
+ "buffer": {
+ "version": "3.6.0",
+ "from": "buffer@>=3.0.1 <4.0.0",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-3.6.0.tgz"
},
"buffer-crc32": {
"version": "0.2.5",
@@ -181,71 +108,21 @@
"from": "buffer-shims@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz"
},
- "builtin-modules": {
- "version": "1.1.1",
- "from": "builtin-modules@>=1.1.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz"
+ "capture-stack-trace": {
+ "version": "1.0.0",
+ "from": "capture-stack-trace@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz"
},
- "caller-id": {
- "version": "0.1.0",
- "from": "caller-id@>=0.1.0 <0.2.0",
- "resolved": "https://registry.npmjs.org/caller-id/-/caller-id-0.1.0.tgz"
- },
- "caller-path": {
- "version": "0.1.0",
- "from": "caller-path@>=0.1.0 <0.2.0",
- "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz"
- },
- "callsites": {
- "version": "0.2.0",
- "from": "callsites@>=0.2.0 <0.3.0",
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz"
- },
- "camelcase": {
- "version": "3.0.0",
- "from": "camelcase@>=3.0.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz"
- },
- "caseless": {
- "version": "0.11.0",
- "from": "caseless@>=0.11.0 <0.12.0",
- "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz"
- },
- "center-align": {
- "version": "0.1.3",
- "from": "center-align@>=0.1.1 <0.2.0",
- "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz"
+ "caw": {
+ "version": "2.0.0",
+ "from": "caw@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.0.tgz"
},
"chalk": {
"version": "1.1.3",
"from": "chalk@>=1.1.0 <2.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz"
},
- "circular-json": {
- "version": "0.3.1",
- "from": "circular-json@>=0.3.0 <0.4.0",
- "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.1.tgz"
- },
- "cli-cursor": {
- "version": "1.0.2",
- "from": "cli-cursor@>=1.0.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz"
- },
- "cli-width": {
- "version": "2.1.0",
- "from": "cli-width@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.1.0.tgz"
- },
- "cliui": {
- "version": "3.2.0",
- "from": "cliui@>=3.2.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz"
- },
- "code-point-at": {
- "version": "1.0.0",
- "from": "code-point-at@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.0.0.tgz"
- },
"combined-stream": {
"version": "1.0.5",
"from": "combined-stream@>=1.0.5 <2.0.0",
@@ -261,38 +138,21 @@
"from": "component-emitter@>=1.2.0 <1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz"
},
+ "compress-commons": {
+ "version": "1.1.0",
+ "from": "compress-commons@>=1.1.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.1.0.tgz"
+ },
"concat-map": {
"version": "0.0.1",
"from": "concat-map@0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
},
- "concat-stream": {
- "version": "1.5.1",
- "from": "concat-stream@>=1.4.6 <2.0.0",
- "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.1.tgz",
- "dependencies": {
- "readable-stream": {
- "version": "2.0.6",
- "from": "readable-stream@>=2.0.0 <2.1.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz"
- }
- }
- },
- "contains-path": {
- "version": "0.1.0",
- "from": "contains-path@>=0.1.0 <0.2.0",
- "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz"
- },
"cookiejar": {
"version": "2.0.6",
"from": "cookiejar@2.0.6",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.0.6.tgz"
},
- "core-js": {
- "version": "2.3.0",
- "from": "core-js@>=2.3.0 <2.4.0",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz"
- },
"core-util-is": {
"version": "1.0.2",
"from": "core-util-is@>=1.0.0 <1.1.0",
@@ -303,84 +163,65 @@
"from": "crc32-stream@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-1.0.0.tgz"
},
- "cryptiles": {
- "version": "2.0.5",
- "from": "cryptiles@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz"
+ "create-error-class": {
+ "version": "3.0.2",
+ "from": "create-error-class@>=3.0.0 <4.0.0",
+ "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz"
},
- "d": {
- "version": "0.1.1",
- "from": "d@>=0.1.1 <0.2.0",
- "resolved": "https://registry.npmjs.org/d/-/d-0.1.1.tgz"
- },
- "damerau-levenshtein": {
- "version": "1.0.0",
- "from": "damerau-levenshtein@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.0.tgz"
- },
- "dashdash": {
- "version": "1.14.0",
- "from": "dashdash@>=1.12.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.0.tgz",
- "dependencies": {
- "assert-plus": {
- "version": "1.0.0",
- "from": "assert-plus@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz"
- }
- }
+ "crypto-browserify": {
+ "version": "1.0.9",
+ "from": "crypto-browserify@1.0.9",
+ "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-1.0.9.tgz"
},
"debug": {
"version": "2.2.0",
"from": "debug@>=2.2.0 <3.0.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz"
},
- "decamelize": {
- "version": "1.2.0",
- "from": "decamelize@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
+ "decompress": {
+ "version": "4.0.0",
+ "from": "decompress@>=4.0.0 <5.0.0",
+ "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.0.0.tgz"
},
- "deep-eql": {
- "version": "0.1.3",
- "from": "deep-eql@>=0.1.3 <0.2.0",
- "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz",
- "dependencies": {
- "type-detect": {
- "version": "0.1.1",
- "from": "type-detect@0.1.1",
- "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz"
- }
- }
+ "decompress-tar": {
+ "version": "4.1.0",
+ "from": "decompress-tar@>=4.0.0 <5.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.0.tgz"
},
- "deep-is": {
- "version": "0.1.3",
- "from": "deep-is@>=0.1.3 <0.2.0",
- "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz"
+ "decompress-tarbz2": {
+ "version": "4.1.0",
+ "from": "decompress-tarbz2@>=4.0.0 <5.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.0.tgz"
},
- "del": {
- "version": "2.2.2",
- "from": "del@>=2.0.2 <3.0.0",
- "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz"
+ "decompress-targz": {
+ "version": "4.0.0",
+ "from": "decompress-targz@>=4.0.0 <5.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.0.0.tgz"
+ },
+ "decompress-unzip": {
+ "version": "4.0.1",
+ "from": "decompress-unzip@>=4.0.1 <5.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz"
+ },
+ "deep-extend": {
+ "version": "0.4.1",
+ "from": "deep-extend@>=0.4.0 <0.5.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz"
},
"delayed-stream": {
"version": "1.0.0",
"from": "delayed-stream@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
},
- "diff": {
- "version": "1.4.0",
- "from": "diff@1.4.0",
- "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz"
+ "download": {
+ "version": "5.0.2",
+ "from": "download@>=5.0.2 <6.0.0",
+ "resolved": "https://registry.npmjs.org/download/-/download-5.0.2.tgz"
},
- "doctrine": {
- "version": "1.3.0",
- "from": "doctrine@>=1.2.2 <2.0.0",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.3.0.tgz"
- },
- "ecc-jsbn": {
- "version": "0.1.1",
- "from": "ecc-jsbn@>=0.1.1 <0.2.0",
- "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz"
+ "duplexer3": {
+ "version": "0.1.4",
+ "from": "duplexer3@>=0.1.4 <0.2.0",
+ "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz"
},
"encoding": {
"version": "0.1.12",
@@ -392,170 +233,46 @@
"from": "end-of-stream@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.1.0.tgz"
},
- "error-ex": {
- "version": "1.3.0",
- "from": "error-ex@>=1.2.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.0.tgz"
- },
- "es5-ext": {
- "version": "0.10.12",
- "from": "es5-ext@>=0.10.11 <0.11.0",
- "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.12.tgz"
- },
- "es6-iterator": {
- "version": "2.0.0",
- "from": "es6-iterator@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.0.tgz"
- },
- "es6-map": {
- "version": "0.1.4",
- "from": "es6-map@>=0.1.3 <0.2.0",
- "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.4.tgz"
- },
- "es6-promise": {
- "version": "3.0.2",
- "from": "es6-promise@>=3.0.2 <3.1.0",
- "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz"
- },
- "es6-set": {
- "version": "0.1.4",
- "from": "es6-set@>=0.1.3 <0.2.0",
- "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.4.tgz"
- },
- "es6-symbol": {
- "version": "3.1.0",
- "from": "es6-symbol@>=3.1.0 <3.2.0",
- "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.0.tgz"
- },
- "es6-weak-map": {
- "version": "2.0.1",
- "from": "es6-weak-map@>=2.0.1 <3.0.0",
- "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.1.tgz"
- },
"escape-string-regexp": {
"version": "1.0.5",
"from": "escape-string-regexp@>=1.0.2 <2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz"
},
- "escodegen": {
- "version": "1.8.1",
- "from": "escodegen@>=1.8.0 <1.9.0",
- "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz",
- "dependencies": {
- "estraverse": {
- "version": "1.9.3",
- "from": "estraverse@>=1.9.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz"
- }
- }
- },
- "escope": {
- "version": "3.6.0",
- "from": "escope@>=3.6.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz"
- },
- "eslint-config-airbnb-base": {
- "version": "5.0.3",
- "from": "eslint-config-airbnb-base@>=5.0.2 <6.0.0",
- "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-5.0.3.tgz"
- },
- "eslint-import-resolver-node": {
- "version": "0.2.3",
- "from": "eslint-import-resolver-node@>=0.2.0 <0.3.0",
- "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.2.3.tgz"
- },
- "espree": {
- "version": "3.1.7",
- "from": "espree@>=3.1.6 <4.0.0",
- "resolved": "https://registry.npmjs.org/espree/-/espree-3.1.7.tgz"
- },
"esprima": {
"version": "2.7.3",
"from": "esprima@>=2.6.0 <3.0.0",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz"
},
- "esrecurse": {
- "version": "4.1.0",
- "from": "esrecurse@>=4.1.0 <5.0.0",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.1.0.tgz",
- "dependencies": {
- "estraverse": {
- "version": "4.1.1",
- "from": "estraverse@>=4.1.0 <4.2.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.1.1.tgz"
- }
- }
- },
- "estraverse": {
- "version": "4.2.0",
- "from": "estraverse@>=4.2.0 <5.0.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz"
- },
- "esutils": {
- "version": "2.0.2",
- "from": "esutils@>=2.0.2 <3.0.0",
- "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz"
- },
- "event-emitter": {
- "version": "0.3.4",
- "from": "event-emitter@>=0.3.4 <0.4.0",
- "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.4.tgz"
- },
- "exit-hook": {
- "version": "1.1.1",
- "from": "exit-hook@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz"
- },
"extend": {
"version": "3.0.0",
"from": "extend@>=3.0.0 <4.0.0",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz"
},
- "extsprintf": {
- "version": "1.0.2",
- "from": "extsprintf@1.0.2",
- "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz"
+ "fd-slicer": {
+ "version": "1.0.1",
+ "from": "fd-slicer@>=1.0.1 <1.1.0",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz"
},
- "fast-levenshtein": {
- "version": "1.1.4",
- "from": "fast-levenshtein@>=1.1.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz"
+ "file-type": {
+ "version": "3.8.0",
+ "from": "file-type@>=3.8.0 <4.0.0",
+ "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.8.0.tgz"
},
- "figures": {
- "version": "1.7.0",
- "from": "figures@>=1.3.5 <2.0.0",
- "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz"
+ "filename-reserved-regex": {
+ "version": "1.0.0",
+ "from": "filename-reserved-regex@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz"
},
- "file-entry-cache": {
- "version": "2.0.0",
- "from": "file-entry-cache@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz"
- },
- "find-up": {
- "version": "1.1.2",
- "from": "find-up@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz"
- },
- "flat-cache": {
+ "filenamify": {
"version": "1.2.1",
- "from": "flat-cache@>=1.2.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.2.1.tgz"
- },
- "forever-agent": {
- "version": "0.6.1",
- "from": "forever-agent@>=0.6.1 <0.7.0",
- "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz"
+ "from": "filenamify@>=1.2.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-1.2.1.tgz"
},
"form-data": {
"version": "1.0.0-rc3",
"from": "form-data@1.0.0-rc3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc3.tgz"
},
- "formatio": {
- "version": "1.1.1",
- "from": "formatio@1.1.1",
- "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz"
- },
"formidable": {
"version": "1.0.17",
"from": "formidable@>=1.0.14 <1.1.0",
@@ -571,47 +288,30 @@
"from": "fs.realpath@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
},
- "generate-function": {
- "version": "2.0.0",
- "from": "generate-function@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz"
+ "get-proxy": {
+ "version": "1.1.0",
+ "from": "get-proxy@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-1.1.0.tgz"
},
- "generate-object-property": {
- "version": "1.2.0",
- "from": "generate-object-property@>=1.1.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz"
+ "get-stdin": {
+ "version": "4.0.1",
+ "from": "get-stdin@>=4.0.1 <5.0.0",
+ "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz"
},
- "get-caller-file": {
- "version": "1.0.2",
- "from": "get-caller-file@>=1.0.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz"
- },
- "getpass": {
- "version": "0.1.6",
- "from": "getpass@>=0.1.1 <0.2.0",
- "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz",
- "dependencies": {
- "assert-plus": {
- "version": "1.0.0",
- "from": "assert-plus@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz"
- }
- }
+ "get-stream": {
+ "version": "2.3.1",
+ "from": "get-stream@>=2.2.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz"
},
"glob": {
- "version": "7.0.6",
- "from": "glob@>=7.0.0 <8.0.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz"
+ "version": "7.1.0",
+ "from": "glob@7.1.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.0.tgz"
},
- "globals": {
- "version": "9.9.0",
- "from": "globals@>=9.2.0 <10.0.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-9.9.0.tgz"
- },
- "globby": {
- "version": "5.0.0",
- "from": "globby@>=5.0.0 <6.0.0",
- "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz"
+ "got": {
+ "version": "6.5.0",
+ "from": "got@>=6.3.0 <7.0.0",
+ "resolved": "https://registry.npmjs.org/got/-/got-6.5.0.tgz"
},
"graceful-fs": {
"version": "4.1.6",
@@ -623,58 +323,11 @@
"from": "graceful-readlink@>=1.0.0",
"resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz"
},
- "growl": {
- "version": "1.9.2",
- "from": "growl@1.9.2",
- "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz"
- },
- "handlebars": {
- "version": "4.0.5",
- "from": "handlebars@>=4.0.1 <5.0.0",
- "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.5.tgz",
- "dependencies": {
- "source-map": {
- "version": "0.4.4",
- "from": "source-map@>=0.4.4 <0.5.0",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz"
- }
- }
- },
- "har-validator": {
- "version": "2.0.6",
- "from": "har-validator@>=2.0.2 <2.1.0",
- "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz"
- },
"has-ansi": {
"version": "2.0.0",
"from": "has-ansi@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz"
},
- "has-flag": {
- "version": "1.0.0",
- "from": "has-flag@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz"
- },
- "hawk": {
- "version": "3.1.3",
- "from": "hawk@>=3.1.0 <3.2.0",
- "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz"
- },
- "hoek": {
- "version": "2.16.3",
- "from": "hoek@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz"
- },
- "hosted-git-info": {
- "version": "2.1.5",
- "from": "hosted-git-info@>=2.1.4 <3.0.0",
- "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.1.5.tgz"
- },
- "http-signature": {
- "version": "1.1.1",
- "from": "http-signature@>=1.1.0 <1.2.0",
- "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz"
- },
"https-proxy-agent": {
"version": "1.0.0",
"from": "https-proxy-agent@>=1.0.0 <2.0.0",
@@ -685,20 +338,10 @@
"from": "iconv-lite@>=0.4.13 <0.5.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz"
},
- "ignore": {
- "version": "3.1.5",
- "from": "ignore@>=3.1.2 <4.0.0",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.1.5.tgz"
- },
- "immediate": {
- "version": "3.0.6",
- "from": "immediate@>=3.0.5 <3.1.0",
- "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz"
- },
- "imurmurhash": {
- "version": "0.1.4",
- "from": "imurmurhash@>=0.1.4 <0.2.0",
- "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz"
+ "ieee754": {
+ "version": "1.1.6",
+ "from": "ieee754@>=1.1.4 <2.0.0",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.6.tgz"
},
"inflight": {
"version": "1.0.5",
@@ -710,290 +353,85 @@
"from": "inherits@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
- "inquirer": {
- "version": "0.12.0",
- "from": "inquirer@>=0.12.0 <0.13.0",
- "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz"
+ "ini": {
+ "version": "1.3.4",
+ "from": "ini@>=1.3.0 <1.4.0",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz"
},
- "invert-kv": {
+ "is-absolute": {
+ "version": "0.1.7",
+ "from": "is-absolute@>=0.1.5 <0.2.0",
+ "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.1.7.tgz"
+ },
+ "is-natural-number": {
+ "version": "2.1.1",
+ "from": "is-natural-number@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-2.1.1.tgz"
+ },
+ "is-redirect": {
"version": "1.0.0",
- "from": "invert-kv@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz"
+ "from": "is-redirect@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz"
},
- "is-arrayish": {
- "version": "0.2.1",
- "from": "is-arrayish@>=0.2.1 <0.3.0",
- "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz"
+ "is-relative": {
+ "version": "0.1.3",
+ "from": "is-relative@>=0.1.0 <0.2.0",
+ "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.1.3.tgz"
},
- "is-buffer": {
- "version": "1.1.4",
- "from": "is-buffer@>=1.0.2 <2.0.0",
- "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.4.tgz"
- },
- "is-builtin-module": {
- "version": "1.0.0",
- "from": "is-builtin-module@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz"
- },
- "is-fullwidth-code-point": {
- "version": "1.0.0",
- "from": "is-fullwidth-code-point@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz"
- },
- "is-my-json-valid": {
- "version": "2.13.1",
- "from": "is-my-json-valid@>=2.12.4 <3.0.0",
- "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.13.1.tgz"
- },
- "is-path-cwd": {
- "version": "1.0.0",
- "from": "is-path-cwd@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz"
- },
- "is-path-in-cwd": {
- "version": "1.0.0",
- "from": "is-path-in-cwd@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz"
- },
- "is-path-inside": {
- "version": "1.0.0",
- "from": "is-path-inside@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz"
- },
- "is-property": {
- "version": "1.0.2",
- "from": "is-property@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz"
- },
- "is-resolvable": {
- "version": "1.0.0",
- "from": "is-resolvable@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz"
+ "is-retry-allowed": {
+ "version": "1.1.0",
+ "from": "is-retry-allowed@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz"
},
"is-stream": {
"version": "1.1.0",
"from": "is-stream@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz"
},
- "is-typedarray": {
- "version": "1.0.0",
- "from": "is-typedarray@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz"
- },
- "is-utf8": {
- "version": "0.2.1",
- "from": "is-utf8@>=0.2.0 <0.3.0",
- "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz"
- },
"isarray": {
"version": "1.0.0",
"from": "isarray@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
},
- "isexe": {
- "version": "1.1.2",
- "from": "isexe@>=1.1.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-1.1.2.tgz"
- },
- "isstream": {
- "version": "0.1.2",
- "from": "isstream@>=0.1.2 <0.2.0",
- "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz"
- },
"jmespath": {
"version": "0.15.0",
"from": "jmespath@0.15.0",
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz"
},
- "jodid25519": {
- "version": "1.0.2",
- "from": "jodid25519@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz"
- },
"js-yaml": {
"version": "3.6.1",
"from": "js-yaml@>=3.5.5 <4.0.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz"
},
- "jsbn": {
- "version": "0.1.0",
- "from": "jsbn@>=0.1.0 <0.2.0",
- "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz"
- },
"json-refs": {
"version": "2.1.6",
"from": "json-refs@>=2.1.5 <3.0.0",
"resolved": "https://registry.npmjs.org/json-refs/-/json-refs-2.1.6.tgz"
},
- "json-schema": {
- "version": "0.2.2",
- "from": "json-schema@0.2.2",
- "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz"
- },
- "json-stable-stringify": {
- "version": "1.0.1",
- "from": "json-stable-stringify@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz"
- },
- "json-stringify-safe": {
- "version": "5.0.1",
- "from": "json-stringify-safe@>=5.0.1 <5.1.0",
- "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz"
- },
- "json3": {
- "version": "3.3.2",
- "from": "json3@3.3.2",
- "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz"
- },
"jsonfile": {
"version": "2.3.1",
"from": "jsonfile@>=2.1.0 <3.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.3.1.tgz"
},
- "jsonify": {
- "version": "0.0.0",
- "from": "jsonify@>=0.0.0 <0.1.0",
- "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz"
- },
- "jsonpointer": {
- "version": "2.0.0",
- "from": "jsonpointer@2.0.0",
- "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz"
- },
- "jsprim": {
- "version": "1.3.0",
- "from": "jsprim@>=1.2.2 <2.0.0",
- "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.3.0.tgz"
- },
- "jsx-ast-utils": {
- "version": "1.3.1",
- "from": "jsx-ast-utils@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-1.3.1.tgz"
- },
- "kind-of": {
- "version": "3.0.4",
- "from": "kind-of@>=3.0.2 <4.0.0",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.0.4.tgz"
- },
"klaw": {
"version": "1.3.0",
"from": "klaw@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.0.tgz"
},
- "lazy-cache": {
- "version": "1.0.4",
- "from": "lazy-cache@>=1.0.3 <2.0.0",
- "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz"
- },
"lazystream": {
"version": "1.0.0",
"from": "lazystream@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz"
},
- "lcid": {
- "version": "1.0.0",
- "from": "lcid@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz"
- },
- "levn": {
- "version": "0.3.0",
- "from": "levn@>=0.3.0 <0.4.0",
- "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz"
- },
- "lie": {
- "version": "3.1.0",
- "from": "lie@>=3.1.0 <3.2.0",
- "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.0.tgz"
- },
- "load-json-file": {
- "version": "1.1.0",
- "from": "load-json-file@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz"
- },
"lodash": {
- "version": "4.15.0",
- "from": "lodash@>=4.13.1 <5.0.0",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.15.0.tgz"
+ "version": "4.16.4",
+ "from": "lodash@4.16.4",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.4.tgz"
},
- "lodash._baseassign": {
- "version": "3.2.0",
- "from": "lodash._baseassign@>=3.0.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz"
- },
- "lodash._basecopy": {
- "version": "3.0.1",
- "from": "lodash._basecopy@>=3.0.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz"
- },
- "lodash._basecreate": {
- "version": "3.0.3",
- "from": "lodash._basecreate@>=3.0.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz"
- },
- "lodash._getnative": {
- "version": "3.9.1",
- "from": "lodash._getnative@>=3.0.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz"
- },
- "lodash._isiterateecall": {
- "version": "3.0.9",
- "from": "lodash._isiterateecall@>=3.0.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz"
- },
- "lodash.assign": {
- "version": "4.2.0",
- "from": "lodash.assign@>=4.0.3 <5.0.0",
- "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz"
- },
- "lodash.cond": {
- "version": "4.5.2",
- "from": "lodash.cond@>=4.3.0 <5.0.0",
- "resolved": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz"
- },
- "lodash.create": {
- "version": "3.1.1",
- "from": "lodash.create@3.1.1",
- "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz"
- },
- "lodash.endswith": {
- "version": "4.2.1",
- "from": "lodash.endswith@>=4.0.1 <5.0.0",
- "resolved": "https://registry.npmjs.org/lodash.endswith/-/lodash.endswith-4.2.1.tgz"
- },
- "lodash.find": {
- "version": "4.6.0",
- "from": "lodash.find@>=4.3.0 <5.0.0",
- "resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz"
- },
- "lodash.findindex": {
- "version": "4.6.0",
- "from": "lodash.findindex@>=4.3.0 <5.0.0",
- "resolved": "https://registry.npmjs.org/lodash.findindex/-/lodash.findindex-4.6.0.tgz"
- },
- "lodash.isarguments": {
- "version": "3.1.0",
- "from": "lodash.isarguments@>=3.0.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz"
- },
- "lodash.isarray": {
- "version": "3.0.4",
- "from": "lodash.isarray@>=3.0.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz"
- },
- "lodash.keys": {
- "version": "3.1.2",
- "from": "lodash.keys@>=3.0.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz"
- },
- "lolex": {
- "version": "1.3.2",
- "from": "lolex@1.3.2",
- "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz"
- },
- "longest": {
- "version": "1.0.1",
- "from": "longest@>=1.0.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz"
+ "lowercase-keys": {
+ "version": "1.0.0",
+ "from": "lowercase-keys@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz"
},
"methods": {
"version": "1.1.2",
@@ -1038,65 +476,35 @@
}
},
"moment": {
- "version": "2.14.1",
- "from": "moment@>=2.13.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/moment/-/moment-2.14.1.tgz"
+ "version": "2.15.1",
+ "from": "moment@2.15.1",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.15.1.tgz"
},
"ms": {
"version": "0.7.1",
"from": "ms@0.7.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
},
- "mute-stream": {
- "version": "0.0.5",
- "from": "mute-stream@0.0.5",
- "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz"
- },
"native-promise-only": {
"version": "0.8.1",
"from": "native-promise-only@>=0.8.1 <0.9.0",
"resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz"
},
- "natural-compare": {
- "version": "1.4.0",
- "from": "natural-compare@>=1.4.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
- },
"node-fetch": {
- "version": "1.6.0",
- "from": "node-fetch@>=1.5.3 <2.0.0",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.6.0.tgz"
+ "version": "1.6.3",
+ "from": "node-fetch@1.6.3",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.6.3.tgz"
},
- "node-uuid": {
- "version": "1.4.7",
- "from": "node-uuid@>=1.4.2 <2.0.0",
- "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz"
- },
- "nopt": {
- "version": "3.0.6",
- "from": "nopt@>=3.0.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz"
- },
- "normalize-package-data": {
- "version": "2.3.5",
- "from": "normalize-package-data@>=2.3.2 <3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.3.5.tgz"
+ "node-status-codes": {
+ "version": "2.0.0",
+ "from": "node-status-codes@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/node-status-codes/-/node-status-codes-2.0.0.tgz"
},
"normalize-path": {
"version": "2.0.1",
"from": "normalize-path@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.0.1.tgz"
},
- "number-is-nan": {
- "version": "1.0.0",
- "from": "number-is-nan@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.0.tgz"
- },
- "oauth-sign": {
- "version": "0.8.2",
- "from": "oauth-sign@>=0.8.0 <0.9.0",
- "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz"
- },
"object-assign": {
"version": "4.1.0",
"from": "object-assign@>=4.0.1 <5.0.0",
@@ -1107,77 +515,20 @@
"from": "once@>=1.3.0 <2.0.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz"
},
- "onetime": {
- "version": "1.1.0",
- "from": "onetime@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz"
- },
- "optimist": {
- "version": "0.6.1",
- "from": "optimist@>=0.6.1 <0.7.0",
- "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
- "dependencies": {
- "minimist": {
- "version": "0.0.10",
- "from": "minimist@>=0.0.1 <0.1.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz"
- },
- "wordwrap": {
- "version": "0.0.3",
- "from": "wordwrap@>=0.0.2 <0.1.0",
- "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz"
- }
- }
- },
- "optionator": {
- "version": "0.8.1",
- "from": "optionator@>=0.8.1 <0.9.0",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.1.tgz"
- },
- "os-homedir": {
- "version": "1.0.1",
- "from": "os-homedir@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.1.tgz"
- },
- "os-locale": {
- "version": "1.4.0",
- "from": "os-locale@>=1.4.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz"
- },
- "pako": {
- "version": "1.0.3",
- "from": "pako@>=1.0.2 <1.1.0",
- "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.3.tgz"
- },
- "parse-json": {
- "version": "2.2.0",
- "from": "parse-json@>=2.2.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz"
- },
- "path-exists": {
- "version": "2.1.0",
- "from": "path-exists@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz"
- },
"path-is-absolute": {
"version": "1.0.0",
"from": "path-is-absolute@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz"
},
- "path-is-inside": {
- "version": "1.0.1",
- "from": "path-is-inside@>=1.0.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.1.tgz"
- },
"path-loader": {
"version": "1.0.1",
"from": "path-loader@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/path-loader/-/path-loader-1.0.1.tgz"
},
- "path-type": {
- "version": "1.1.0",
- "from": "path-type@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz"
+ "pend": {
+ "version": "1.2.0",
+ "from": "pend@>=1.2.0 <1.3.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz"
},
"pify": {
"version": "2.3.0",
@@ -1194,153 +545,73 @@
"from": "pinkie-promise@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz"
},
- "pkg-dir": {
- "version": "1.0.0",
- "from": "pkg-dir@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz"
- },
- "pkg-up": {
- "version": "1.0.0",
- "from": "pkg-up@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-1.0.0.tgz"
- },
- "pluralize": {
- "version": "1.2.1",
- "from": "pluralize@>=1.2.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz"
- },
- "prelude-ls": {
- "version": "1.1.2",
- "from": "prelude-ls@>=1.1.2 <1.2.0",
- "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz"
+ "prepend-http": {
+ "version": "1.0.4",
+ "from": "prepend-http@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz"
},
"process-nextick-args": {
"version": "1.0.7",
"from": "process-nextick-args@>=1.0.6 <1.1.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz"
},
- "progress": {
- "version": "1.1.8",
- "from": "progress@>=1.1.8 <2.0.0",
- "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz"
+ "punycode": {
+ "version": "1.3.2",
+ "from": "punycode@1.3.2",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz"
},
"qs": {
"version": "2.3.3",
"from": "qs@2.3.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz"
},
- "read-pkg": {
- "version": "1.1.0",
- "from": "read-pkg@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz"
+ "querystring": {
+ "version": "0.2.0",
+ "from": "querystring@0.2.0",
+ "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz"
},
- "read-pkg-up": {
- "version": "1.0.1",
- "from": "read-pkg-up@>=1.0.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz"
+ "rc": {
+ "version": "1.1.6",
+ "from": "rc@>=1.1.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.1.6.tgz"
},
"readable-stream": {
"version": "2.1.5",
"from": "readable-stream@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz"
},
- "readline2": {
- "version": "1.0.1",
- "from": "readline2@>=1.0.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz"
- },
"reduce-component": {
"version": "1.0.1",
"from": "reduce-component@1.0.1",
"resolved": "https://registry.npmjs.org/reduce-component/-/reduce-component-1.0.1.tgz"
},
- "repeat-string": {
- "version": "1.5.4",
- "from": "repeat-string@>=1.5.2 <2.0.0",
- "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.5.4.tgz"
- },
"replaceall": {
"version": "0.1.6",
"from": "replaceall@>=0.1.6 <0.2.0",
"resolved": "https://registry.npmjs.org/replaceall/-/replaceall-0.1.6.tgz"
},
- "request": {
- "version": "2.74.0",
- "from": "request@>=2.72.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/request/-/request-2.74.0.tgz",
- "dependencies": {
- "form-data": {
- "version": "1.0.0-rc4",
- "from": "form-data@>=1.0.0-rc4 <1.1.0",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz"
- },
- "qs": {
- "version": "6.2.1",
- "from": "qs@>=6.2.0 <6.3.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.1.tgz"
- }
- }
- },
- "require-directory": {
- "version": "2.1.1",
- "from": "require-directory@>=2.1.1 <3.0.0",
- "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz"
- },
- "require-main-filename": {
- "version": "1.0.1",
- "from": "require-main-filename@>=1.0.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz"
- },
- "require-uncached": {
- "version": "1.0.2",
- "from": "require-uncached@>=1.0.2 <2.0.0",
- "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.2.tgz"
- },
- "resolve": {
- "version": "1.1.7",
- "from": "resolve@>=1.1.6 <2.0.0",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz"
- },
- "resolve-from": {
- "version": "1.0.1",
- "from": "resolve-from@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz"
- },
- "restore-cursor": {
- "version": "1.0.1",
- "from": "restore-cursor@>=1.0.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz"
- },
- "right-align": {
- "version": "0.1.3",
- "from": "right-align@>=0.1.1 <0.2.0",
- "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz"
- },
"rimraf": {
"version": "2.5.4",
"from": "rimraf@>=2.2.8 <3.0.0",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz"
},
- "run-async": {
- "version": "0.1.0",
- "from": "run-async@>=0.1.0 <0.2.0",
- "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz"
- },
- "rx-lite": {
- "version": "3.1.2",
- "from": "rx-lite@>=3.1.2 <4.0.0",
- "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz"
- },
- "samsam": {
- "version": "1.1.2",
- "from": "samsam@1.1.2",
- "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz"
- },
"sax": {
"version": "1.1.5",
"from": "sax@1.1.5",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.1.5.tgz"
},
+ "seek-bzip": {
+ "version": "1.0.5",
+ "from": "seek-bzip@>=1.0.5 <2.0.0",
+ "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz",
+ "dependencies": {
+ "commander": {
+ "version": "2.8.1",
+ "from": "commander@>=2.8.1 <2.9.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz"
+ }
+ }
+ },
"semver": {
"version": "5.0.3",
"from": "semver@>=5.0.1 <5.1.0",
@@ -1351,11 +622,6 @@
"from": "semver-regex@latest",
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-1.0.0.tgz"
},
- "set-blocking": {
- "version": "2.0.0",
- "from": "set-blocking@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz"
- },
"shelljs": {
"version": "0.6.1",
"from": "shelljs@>=0.6.0 <0.7.0",
@@ -1366,93 +632,41 @@
"from": "slash@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz"
},
- "slice-ansi": {
- "version": "0.0.4",
- "from": "slice-ansi@0.0.4",
- "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz"
- },
- "sntp": {
- "version": "1.0.9",
- "from": "sntp@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz"
- },
- "source-map": {
- "version": "0.2.0",
- "from": "source-map@>=0.2.0 <0.3.0",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz"
- },
- "spdx-correct": {
- "version": "1.0.2",
- "from": "spdx-correct@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz"
- },
- "spdx-exceptions": {
- "version": "1.0.5",
- "from": "spdx-exceptions@>=1.0.4 <2.0.0",
- "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-1.0.5.tgz"
- },
- "spdx-expression-parse": {
- "version": "1.0.2",
- "from": "spdx-expression-parse@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.2.tgz"
- },
- "spdx-license-ids": {
- "version": "1.2.2",
- "from": "spdx-license-ids@>=1.0.2 <2.0.0",
- "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz"
- },
"sprintf-js": {
"version": "1.0.3",
"from": "sprintf-js@>=1.0.2 <1.1.0",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
},
- "sshpk": {
- "version": "1.9.2",
- "from": "sshpk@>=1.7.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.9.2.tgz",
- "dependencies": {
- "assert-plus": {
- "version": "1.0.0",
- "from": "assert-plus@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz"
- }
- }
- },
- "stack-trace": {
- "version": "0.0.9",
- "from": "stack-trace@>=0.0.0 <0.1.0",
- "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz"
- },
"string_decoder": {
"version": "0.10.31",
"from": "string_decoder@>=0.10.0 <0.11.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
},
- "string-width": {
- "version": "1.0.2",
- "from": "string-width@>=1.0.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz"
- },
- "stringstream": {
- "version": "0.0.5",
- "from": "stringstream@>=0.0.4 <0.1.0",
- "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz"
- },
"strip-ansi": {
"version": "3.0.1",
"from": "strip-ansi@>=3.0.0 <4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz"
},
- "strip-bom": {
- "version": "2.0.0",
- "from": "strip-bom@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz"
+ "strip-dirs": {
+ "version": "1.1.1",
+ "from": "strip-dirs@>=1.1.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-1.1.1.tgz"
},
"strip-json-comments": {
"version": "1.0.4",
"from": "strip-json-comments@>=1.0.1 <1.1.0",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz"
},
+ "strip-outer": {
+ "version": "1.0.0",
+ "from": "strip-outer@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.0.tgz"
+ },
+ "sum-up": {
+ "version": "1.0.3",
+ "from": "sum-up@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/sum-up/-/sum-up-1.0.3.tgz"
+ },
"superagent": {
"version": "1.8.4",
"from": "superagent@>=1.6.1 <2.0.0",
@@ -1475,132 +689,60 @@
"from": "supports-color@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz"
},
- "table": {
- "version": "3.7.8",
- "from": "table@>=3.7.8 <4.0.0",
- "resolved": "https://registry.npmjs.org/table/-/table-3.7.8.tgz"
- },
"tar-stream": {
"version": "1.5.2",
"from": "tar-stream@>=1.5.0 <2.0.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.2.tgz"
},
- "text-table": {
- "version": "0.2.0",
- "from": "text-table@>=0.2.0 <0.3.0",
- "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz"
- },
"through": {
"version": "2.3.8",
"from": "through@>=2.3.6 <3.0.0",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz"
},
- "tough-cookie": {
- "version": "2.3.1",
- "from": "tough-cookie@>=2.3.0 <2.4.0",
- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.1.tgz"
+ "timed-out": {
+ "version": "2.0.0",
+ "from": "timed-out@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz"
},
"traverse": {
"version": "0.6.6",
"from": "traverse@>=0.6.6 <0.7.0",
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz"
},
- "tryit": {
- "version": "1.0.2",
- "from": "tryit@>=1.0.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.2.tgz"
+ "trim-repeated": {
+ "version": "1.0.0",
+ "from": "trim-repeated@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz"
},
"tunnel-agent": {
"version": "0.4.3",
"from": "tunnel-agent@>=0.4.1 <0.5.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz"
},
- "tv4": {
- "version": "1.2.7",
- "from": "tv4@>=1.2.7 <2.0.0",
- "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.2.7.tgz"
+ "unbzip2-stream": {
+ "version": "1.0.10",
+ "from": "unbzip2-stream@>=1.0.9 <2.0.0",
+ "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.0.10.tgz"
},
- "tweetnacl": {
- "version": "0.13.3",
- "from": "tweetnacl@>=0.13.0 <0.14.0",
- "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.13.3.tgz"
- },
- "type-check": {
- "version": "0.3.2",
- "from": "type-check@>=0.3.2 <0.4.0",
- "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz"
- },
- "type-detect": {
- "version": "1.0.0",
- "from": "type-detect@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz"
- },
- "typedarray": {
- "version": "0.0.6",
- "from": "typedarray@>=0.0.5 <0.1.0",
- "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
- },
- "uglify-js": {
- "version": "2.7.3",
- "from": "uglify-js@>=2.6.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.7.3.tgz",
- "dependencies": {
- "async": {
- "version": "0.2.10",
- "from": "async@>=0.2.6 <0.3.0",
- "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz"
- },
- "camelcase": {
- "version": "1.2.1",
- "from": "camelcase@>=1.0.2 <2.0.0",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz"
- },
- "cliui": {
- "version": "2.1.0",
- "from": "cliui@>=2.1.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz"
- },
- "source-map": {
- "version": "0.5.6",
- "from": "source-map@>=0.5.1 <0.6.0",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz"
- },
- "window-size": {
- "version": "0.1.0",
- "from": "window-size@0.1.0",
- "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz"
- },
- "wordwrap": {
- "version": "0.0.2",
- "from": "wordwrap@0.0.2",
- "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz"
- },
- "yargs": {
- "version": "3.10.0",
- "from": "yargs@>=3.10.0 <3.11.0",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz"
- }
- }
- },
- "uglify-to-browserify": {
- "version": "1.0.2",
- "from": "uglify-to-browserify@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz"
+ "unzip-response": {
+ "version": "2.0.1",
+ "from": "unzip-response@>=2.0.1 <3.0.0",
+ "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz"
},
"uri-js": {
"version": "2.1.1",
"from": "uri-js@>=2.1.1 <3.0.0",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-2.1.1.tgz"
},
- "user-home": {
- "version": "2.0.0",
- "from": "user-home@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz"
- },
- "util": {
+ "url": {
"version": "0.10.3",
- "from": "util@>=0.10.3 <1.0.0",
- "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz"
+ "from": "url@0.10.3",
+ "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz"
+ },
+ "url-parse-lax": {
+ "version": "1.0.0",
+ "from": "url-parse-lax@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz"
},
"util-deprecate": {
"version": "1.0.2",
@@ -1608,55 +750,15 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
},
"uuid": {
- "version": "2.0.2",
- "from": "uuid@latest",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.2.tgz"
- },
- "validate-npm-package-license": {
- "version": "3.0.1",
- "from": "validate-npm-package-license@>=3.0.1 <4.0.0",
- "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz"
- },
- "verror": {
- "version": "1.3.6",
- "from": "verror@1.3.6",
- "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz"
- },
- "which": {
- "version": "1.2.10",
- "from": "which@>=1.1.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/which/-/which-1.2.10.tgz"
- },
- "which-module": {
- "version": "1.0.0",
- "from": "which-module@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz"
- },
- "window-size": {
- "version": "0.2.0",
- "from": "window-size@>=0.2.0 <0.3.0",
- "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz"
- },
- "wordwrap": {
- "version": "1.0.0",
- "from": "wordwrap@>=0.0.2",
- "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz"
- },
- "wrap-ansi": {
- "version": "2.0.0",
- "from": "wrap-ansi@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.0.0.tgz"
+ "version": "2.0.3",
+ "from": "uuid@2.0.3",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz"
},
"wrappy": {
"version": "1.0.2",
"from": "wrappy@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
},
- "write": {
- "version": "0.2.1",
- "from": "write@>=0.2.1 <0.3.0",
- "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz"
- },
"xml2js": {
"version": "0.4.15",
"from": "xml2js@0.4.15",
@@ -1674,30 +776,20 @@
}
}
},
- "xregexp": {
- "version": "3.1.1",
- "from": "xregexp@>=3.0.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-3.1.1.tgz"
- },
"xtend": {
"version": "4.0.1",
"from": "xtend@>=4.0.0 <5.0.0",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz"
},
- "y18n": {
- "version": "3.2.1",
- "from": "y18n@>=3.2.1 <4.0.0",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz"
+ "yauzl": {
+ "version": "2.6.0",
+ "from": "yauzl@>=2.4.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.6.0.tgz"
},
- "yargs": {
- "version": "4.8.1",
- "from": "yargs@>=4.7.0 <5.0.0",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-4.8.1.tgz"
- },
- "yargs-parser": {
- "version": "2.4.1",
- "from": "yargs-parser@>=2.4.1 <3.0.0",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz"
+ "zip-stream": {
+ "version": "1.1.0",
+ "from": "zip-stream@>=1.1.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.1.0.tgz"
}
}
}
diff --git a/package.json b/package.json
index bbd4eb23a..4d16e1cda 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "serverless",
- "version": "1.0.0-rc.2",
+ "version": "1.0.2",
"engines": {
"node": ">=4.0"
},
@@ -31,12 +31,20 @@
"internet of things",
"serverless.com"
],
+ "files": [
+ "bin",
+ "lib",
+ "package.json",
+ "npm-shrinkwrap.json",
+ "README.md",
+ "LICENSE.txt",
+ "CHANGELOG.md"
+ ],
"main": "lib/Serverless.js",
"bin": {
"serverless": "./bin/serverless",
"slss": "./bin/serverless",
- "sls": "./bin/serverless",
- "serverless-run-python-handler": "./bin/serverless-run-python-handler"
+ "sls": "./bin/serverless"
},
"scripts": {
"test": "istanbul cover node_modules/mocha/bin/_mocha tests/all -- -R spec --recursive",
@@ -44,7 +52,6 @@
"integration-test": "mocha tests/integration_test"
},
"devDependencies": {
- "all-contributors-cli": "^3.0.6",
"chai": "^3.5.0",
"coveralls": "^2.11.12",
"eslint": "^3.3.1",
@@ -58,6 +65,7 @@
"mocha": "^3.0.2",
"mocha-lcov-reporter": "^1.2.0",
"mock-require": "^1.3.0",
+ "proxyquire": "^1.7.10",
"sinon": "^1.17.5"
},
"dependencies": {
@@ -66,7 +74,9 @@
"aws-sdk": "^2.3.17",
"bluebird": "^3.4.0",
"chalk": "^1.1.1",
+ "download": "^5.0.2",
"fs-extra": "^0.26.7",
+ "glob": "^7.0.6",
"https-proxy-agent": "^1.0.0",
"js-yaml": "^3.6.1",
"json-refs": "^2.1.5",
diff --git a/tests/all.js b/tests/all.js
index 228e3c1ec..e15c40095 100644
--- a/tests/all.js
+++ b/tests/all.js
@@ -12,13 +12,14 @@ require('./classes/CLI');
// Core Plugins Tests
require('../lib/plugins/create/tests/create');
+require('../lib/plugins/install/tests/install');
require('../lib/plugins/deploy/tests/deploy');
require('../lib/plugins/info/tests/info');
require('../lib/plugins/invoke/tests/invoke');
require('../lib/plugins/logs/tests/logs');
require('../lib/plugins/remove/tests/remove');
require('../lib/plugins/package/tests/all');
-require('../lib/plugins/tracking/tests/tracking');
+require('../lib/plugins/slstats/tests/slstats');
// AWS Plugins Tests
require('../lib/plugins/aws/tests');
@@ -34,6 +35,7 @@ require('../lib/plugins/aws/deploy/compile/events/s3/tests');
require('../lib/plugins/aws/deploy/compile/events/schedule/tests');
require('../lib/plugins/aws/deploy/compile/events/apiGateway/tests/all');
require('../lib/plugins/aws/deploy/compile/events/sns/tests');
+require('../lib/plugins/aws/deploy/compile/events/stream/tests');
require('../lib/plugins/aws/deployFunction/tests/index');
// Other Tests
diff --git a/tests/classes/CLI.js b/tests/classes/CLI.js
index 6695353f7..e21325848 100644
--- a/tests/classes/CLI.js
+++ b/tests/classes/CLI.js
@@ -116,10 +116,11 @@ describe('CLI', () => {
};
}
}
- const pluginMock = new PluginMock();
- const plugins = [pluginMock];
+ serverless.pluginManager.addPlugin(PluginMock);
+
+ cli.setLoadedPlugins(serverless.pluginManager.getPlugins());
+ cli.setLoadedCommands(serverless.pluginManager.getCommands());
- cli.setLoadedPlugins(plugins);
const processedInput = cli.processInput();
const helpDisplayed = cli.displayHelp(processedInput);
@@ -180,10 +181,11 @@ describe('CLI', () => {
};
}
}
- const pluginMock = new PluginMock();
- const plugins = [pluginMock];
+ serverless.pluginManager.addPlugin(PluginMock);
+
+ cli.setLoadedPlugins(serverless.pluginManager.getPlugins());
+ cli.setLoadedCommands(serverless.pluginManager.getCommands());
- cli.setLoadedPlugins(plugins);
const processedInput = cli.processInput();
const helpDisplayed = cli.displayHelp(processedInput);
@@ -228,10 +230,11 @@ describe('CLI', () => {
};
}
}
- const pluginMock = new PluginMock();
- const plugins = [pluginMock];
+ serverless.pluginManager.addPlugin(PluginMock);
+
+ cli.setLoadedPlugins(serverless.pluginManager.getPlugins());
+ cli.setLoadedCommands(serverless.pluginManager.getCommands());
- cli.setLoadedPlugins(plugins);
const processedInput = cli.processInput();
const helpDisplayed = cli.displayHelp(processedInput);
diff --git a/tests/classes/PluginManager.js b/tests/classes/PluginManager.js
index d146f9331..bd51119fc 100644
--- a/tests/classes/PluginManager.js
+++ b/tests/classes/PluginManager.js
@@ -214,10 +214,6 @@ describe('PluginManager', () => {
expect(pluginManager.plugins.length).to.equal(0);
});
- it('should create an empty commandsList array', () => {
- expect(pluginManager.commandsList.length).to.equal(0);
- });
-
it('should create an empty commands object', () => {
expect(pluginManager.commands).to.deep.equal({});
});
@@ -254,81 +250,33 @@ describe('PluginManager', () => {
it('should convert shortcuts into options when a one level deep command matches', () => {
const cliOptionsMock = { r: 'eu-central-1', region: 'us-east-1' };
const cliCommandsMock = ['deploy']; // command with one level deepness
- const commandsMock = {
- deploy: {
- options: {
- region: {
- shortcut: 'r',
- },
+ const commandMock = {
+ options: {
+ region: {
+ shortcut: 'r',
},
},
};
pluginManager.setCliCommands(cliCommandsMock);
pluginManager.setCliOptions(cliOptionsMock);
- pluginManager.convertShortcutsIntoOptions(cliOptionsMock, commandsMock);
+ pluginManager.convertShortcutsIntoOptions(commandMock);
expect(pluginManager.cliOptions.region).to.equal(cliOptionsMock.r);
});
- it('should convert shortcuts into options when a two level deep command matches', () => {
- const cliOptionsMock = { f: 'function-1', function: 'function-2' };
- const cliCommandsMock = ['deploy', 'function']; // command with two level deepness
- const commandsMock = {
- deploy: {
- commands: {
- function: {
- options: {
- function: {
- shortcut: 'f',
- },
- },
- },
- },
- },
- };
- pluginManager.setCliCommands(cliCommandsMock);
- pluginManager.setCliOptions(cliOptionsMock);
-
- pluginManager.convertShortcutsIntoOptions(cliOptionsMock, commandsMock);
-
- expect(pluginManager.cliOptions.function).to.equal(cliOptionsMock.f);
- });
-
- it('should not convert shortcuts into options when the command does not match', () => {
- const cliOptionsMock = { r: 'eu-central-1', region: 'us-east-1' };
- const cliCommandsMock = ['foo'];
- const commandsMock = {
- deploy: {
- options: {
- region: {
- shortcut: 'r',
- },
- },
- },
- };
- pluginManager.setCliCommands(cliCommandsMock);
- pluginManager.setCliOptions(cliOptionsMock);
-
- pluginManager.convertShortcutsIntoOptions(cliOptionsMock, commandsMock);
-
- expect(pluginManager.cliOptions.region).to.equal(cliOptionsMock.region);
- });
-
it('should not convert shortcuts into options when the shortcut is not given', () => {
const cliOptionsMock = { r: 'eu-central-1', region: 'us-east-1' };
const cliCommandsMock = ['deploy'];
- const commandsMock = {
- deploy: {
- options: {
- region: {},
- },
+ const commandMock = {
+ options: {
+ region: {},
},
};
pluginManager.setCliCommands(cliCommandsMock);
pluginManager.setCliOptions(cliOptionsMock);
- pluginManager.convertShortcutsIntoOptions(cliOptionsMock, commandsMock);
+ pluginManager.convertShortcutsIntoOptions(commandMock);
expect(pluginManager.cliOptions.region).to.equal(cliOptionsMock.region);
});
@@ -344,7 +292,7 @@ describe('PluginManager', () => {
it('should load the plugin commands', () => {
pluginManager.addPlugin(SynchronousPluginMock);
- expect(pluginManager.commandsList[0]).to.have.property('deploy');
+ expect(pluginManager.commands).to.have.property('deploy');
});
});
@@ -438,19 +386,58 @@ describe('PluginManager', () => {
const synchronousPluginMockInstance = new SynchronousPluginMock();
pluginManager.loadCommands(synchronousPluginMockInstance);
- expect(pluginManager.commandsList[0]).to.have.property('deploy');
+ expect(pluginManager.commands).to.have.property('deploy');
+ });
+
+ it('should merge plugin commands', () => {
+ pluginManager.loadCommands({
+ commands: {
+ deploy: {
+ lifecycleEvents: [
+ 'one',
+ ],
+ options: {
+ foo: {},
+ },
+ },
+ },
+ });
+
+ pluginManager.loadCommands({
+ commands: {
+ deploy: {
+ lifecycleEvents: [
+ 'one',
+ 'two',
+ ],
+ options: {
+ bar: {},
+ },
+ commands: {
+ fn: {
+ },
+ },
+ },
+ },
+ });
+
+ expect(pluginManager.commands.deploy).to.have.property('options')
+ .that.has.all.keys('foo', 'bar');
+ expect(pluginManager.commands.deploy).to.have.property('lifecycleEvents')
+ .that.is.an('array')
+ .that.deep.equals(['one', 'two']);
+ expect(pluginManager.commands.deploy.commands).to.have.property('fn');
});
});
describe('#getEvents()', () => {
beforeEach(function () { // eslint-disable-line prefer-arrow-callback
- const synchronousPluginMockInstance = new SynchronousPluginMock();
- pluginManager.loadCommands(synchronousPluginMockInstance);
+ pluginManager.addPlugin(SynchronousPluginMock);
});
it('should get all the matching events for a root level command in the correct order', () => {
- const commandsArray = ['deploy'];
- const events = pluginManager.getEvents(commandsArray, pluginManager.commands);
+ const command = pluginManager.getCommand(['deploy']);
+ const events = pluginManager.getEvents(command);
expect(events[0]).to.equal('before:deploy:resources');
expect(events[1]).to.equal('deploy:resources');
@@ -461,8 +448,8 @@ describe('PluginManager', () => {
});
it('should get all the matching events for a nested level command in the correct order', () => {
- const commandsArray = ['deploy', 'onpremises'];
- const events = pluginManager.getEvents(commandsArray, pluginManager.commands);
+ const command = pluginManager.getCommand(['deploy', 'onpremises']);
+ const events = pluginManager.getEvents(command);
expect(events[0]).to.equal('before:deploy:onpremises:resources');
expect(events[1]).to.equal('deploy:onpremises:resources');
@@ -471,13 +458,6 @@ describe('PluginManager', () => {
expect(events[4]).to.equal('deploy:onpremises:functions');
expect(events[5]).to.equal('after:deploy:onpremises:functions');
});
-
- it('should return an empty events array when the command is not defined', () => {
- const commandsArray = ['foo'];
- const events = pluginManager.getEvents(commandsArray, pluginManager.commands);
-
- expect(events.length).to.equal(0);
- });
});
describe('#getPlugins()', () => {
@@ -500,53 +480,34 @@ describe('PluginManager', () => {
});
});
- describe('#validateCommands()', () => {
- it('should throw an error if a first level command is not found in the commands object', () => {
- pluginManager.commands = {
- foo: {},
- };
- const commandsArray = ['bar'];
-
- expect(() => { pluginManager.validateCommands(commandsArray); }).to.throw(Error);
- });
- });
-
describe('#validateOptions()', () => {
- it('should throw an error if a required option is not set in a plain commands object', () => {
+ it('should throw an error if a required option is not set', () => {
pluginManager.commands = {
foo: {
options: {
- bar: {
+ baz: {
+ shortcut: 'b',
+ required: true,
+ },
+ },
+ },
+ bar: {
+ options: {
+ baz: {
required: true,
},
},
},
};
- const commandsArray = ['foo'];
- expect(() => { pluginManager.validateOptions(commandsArray); }).to.throw(Error);
+ const foo = pluginManager.commands.foo;
+ const bar = pluginManager.commands.bar;
+
+ expect(() => { pluginManager.validateOptions(foo); }).to.throw(Error);
+ expect(() => { pluginManager.validateOptions(bar); }).to.throw(Error);
});
- it('should throw an error if a required option is not set in a nested commands object', () => {
- pluginManager.commands = {
- foo: {
- commands: {
- bar: {
- options: {
- baz: {
- required: true,
- },
- },
- },
- },
- },
- };
- const commandsArray = ['foo', 'bar'];
-
- expect(() => { pluginManager.validateOptions(commandsArray); }).to.throw(Error);
- });
-
- it('should throw an error if a customValidation is not set in a plain commands object', () => {
+ it('should throw an error if a customValidation is not met', () => {
pluginManager.setCliOptions({ bar: 'dev' });
pluginManager.commands = {
@@ -561,33 +522,9 @@ describe('PluginManager', () => {
},
},
};
- const commandsArray = ['foo'];
+ const command = pluginManager.commands.foo;
- expect(() => { pluginManager.validateOptions(commandsArray); }).to.throw(Error);
- });
-
- it('should throw an error if a customValidation is not set in a nested commands object', () => {
- pluginManager.setCliOptions({ baz: 100 });
-
- pluginManager.commands = {
- foo: {
- commands: {
- bar: {
- options: {
- baz: {
- customValidation: {
- regularExpression: /^[a-zA-z¥s]+$/,
- errorMessage: 'Custom Error Message',
- },
- },
- },
- },
- },
- },
- };
- const commandsArray = ['foo', 'bar'];
-
- expect(() => { pluginManager.validateOptions(commandsArray); }).to.throw(Error);
+ expect(() => { pluginManager.validateOptions(command); }).to.throw(Error);
});
it('should succeeds if a custom regex matches in a plain commands object', () => {
@@ -609,30 +546,6 @@ describe('PluginManager', () => {
expect(() => { pluginManager.validateOptions(commandsArray); }).to.not.throw(Error);
});
-
- it('should succeeds if a custom regex matches in a nested commands object', () => {
- pluginManager.setCliOptions({ baz: 'dev' });
-
- pluginManager.commands = {
- foo: {
- commands: {
- bar: {
- options: {
- baz: {
- customValidation: {
- regularExpression: /^[a-zA-z¥s]+$/,
- errorMessage: 'Custom Error Message',
- },
- },
- },
- },
- },
- },
- };
- const commandsArray = ['foo', 'bar'];
-
- expect(() => { pluginManager.validateOptions(commandsArray); }).to.not.throw(Error);
- });
});
describe('#run()', () => {
@@ -644,6 +557,22 @@ describe('PluginManager', () => {
expect(() => { pluginManager.run(commandsArray); }).to.throw(Error);
});
+ it('should throw an error when the given command has no hooks', () => {
+ class HooklessPlugin {
+ constructor() {
+ this.commands = {
+ foo: {},
+ };
+ }
+ }
+
+ pluginManager.addPlugin(HooklessPlugin);
+
+ const commandsArray = ['foo'];
+
+ expect(() => { pluginManager.run(commandsArray); }).to.throw(Error);
+ });
+
it('should run the hooks in the correct order', () => {
class CorrectHookOrderPluginMock {
constructor() {
@@ -732,7 +661,7 @@ describe('PluginManager', () => {
describe('when running a nested command', () => {
it('should run the nested command', () => {
const commandsArray = ['deploy', 'onpremises'];
- pluginManager.run(commandsArray)
+ return pluginManager.run(commandsArray)
.then(() => expect(pluginManager.plugins[0].deployedResources)
.to.equal(1));
});
@@ -750,14 +679,14 @@ describe('PluginManager', () => {
pluginManager.addPlugin(SynchronousPluginMock);
});
- it('should run only the providers plugins (if the provider is specified)', () => {
+ it('should load only the providers plugins (if the provider is specified)', () => {
const commandsArray = ['deploy'];
- pluginManager.run(commandsArray).then(() => {
+ return pluginManager.run(commandsArray).then(() => {
+ expect(pluginManager.plugins.length).to.equal(2);
expect(pluginManager.plugins[0].deployedFunctions).to.equal(1);
- expect(pluginManager.plugins[1].deployedFunctions).to.equal(0);
-
- // other, provider independent plugins should also be run
- expect(pluginManager.plugins[2].deployedFunctions).to.equal(1);
+ expect(pluginManager.plugins[0].provider).to.equal('provider1');
+ expect(pluginManager.plugins[1].deployedFunctions).to.equal(1);
+ expect(pluginManager.plugins[1].provider).to.equal(undefined);
});
});
});
diff --git a/tests/classes/Serverless.js b/tests/classes/Serverless.js
index 76392af86..c37ae03db 100644
--- a/tests/classes/Serverless.js
+++ b/tests/classes/Serverless.js
@@ -3,8 +3,6 @@
const expect = require('chai').expect;
const Serverless = require('../../lib/Serverless');
const semverRegex = require('semver-regex');
-const fs = require('fs');
-const fse = require('fs-extra');
const path = require('path');
const YAML = require('js-yaml');
@@ -151,7 +149,6 @@ describe('Serverless', () => {
google: {},
},
package: {
- include: ['include-me.js'],
exclude: ['exclude-me.js'],
artifact: 'some/path/foo.zip',
},
@@ -182,25 +179,6 @@ describe('Serverless', () => {
serverless.processedInput = { commands: [], options: {} };
});
- it('should track if tracking is enabled', (done) => {
- const tmpDirPath = testUtils.getTmpDirPath();
- fse.mkdirsSync(tmpDirPath);
-
- serverless.config.serverlessPath = tmpDirPath;
-
- serverless.run().then(() => done());
- });
-
- it('should not track if tracking is disabled', (done) => {
- const tmpDirPath = testUtils.getTmpDirPath();
- fse.mkdirsSync(tmpDirPath);
- fs.writeFileSync(path.join(tmpDirPath, 'do-not-track'), 'some-content');
-
- serverless.config.serverlessPath = tmpDirPath;
-
- serverless.run().then(() => done());
- });
-
it('should forward the entered command to the PluginManager class', () => {
serverless.processedInput.commands = ['someNotAvailableCommand'];
diff --git a/tests/classes/Service.js b/tests/classes/Service.js
index 520361f74..0c3d9fc20 100644
--- a/tests/classes/Service.js
+++ b/tests/classes/Service.js
@@ -53,7 +53,6 @@ describe('Service', () => {
google: {},
},
package: {
- include: ['include-me.js'],
exclude: ['exclude-me.js'],
artifact: 'some/path/foo.zip',
},
@@ -69,7 +68,6 @@ describe('Service', () => {
expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
expect(serviceInstance.resources.azure).to.deep.equal({});
expect(serviceInstance.resources.google).to.deep.equal({});
- expect(serviceInstance.package.include[0]).to.equal('include-me.js');
expect(serviceInstance.package.exclude[0]).to.equal('exclude-me.js');
expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
});
@@ -136,7 +134,6 @@ describe('Service', () => {
google: {},
},
package: {
- include: ['include-me.js'],
exclude: ['exclude-me.js'],
artifact: 'some/path/foo.zip',
},
@@ -158,8 +155,6 @@ describe('Service', () => {
expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
expect(serviceInstance.resources.azure).to.deep.equal({});
expect(serviceInstance.resources.google).to.deep.equal({});
- expect(serviceInstance.package.include.length).to.equal(1);
- expect(serviceInstance.package.include[0]).to.equal('include-me.js');
expect(serviceInstance.package.exclude.length).to.equal(1);
expect(serviceInstance.package.exclude[0]).to.equal('exclude-me.js');
expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
@@ -188,7 +183,6 @@ describe('Service', () => {
google: {},
},
package: {
- include: ['include-me.js'],
exclude: ['exclude-me.js'],
artifact: 'some/path/foo.zip',
},
diff --git a/tests/classes/Utils.js b/tests/classes/Utils.js
index afa2b766b..4fc73b85d 100644
--- a/tests/classes/Utils.js
+++ b/tests/classes/Utils.js
@@ -5,12 +5,28 @@ const os = require('os');
const expect = require('chai').expect;
const fse = require('fs-extra');
const fs = require('fs');
+const sinon = require('sinon');
+const BbPromise = require('bluebird');
+const proxyquire = require('proxyquire');
const Serverless = require('../../lib/Serverless');
const testUtils = require('../../tests/utils');
+const serverlessVersion = require('../../package.json').version;
-const serverless = new Serverless();
+const fetchStub = sinon.stub().returns(BbPromise.resolve());
+const Utils = proxyquire('../../lib/classes/Utils.js', {
+ 'node-fetch': fetchStub,
+});
describe('Utils', () => {
+ let utils;
+ let serverless;
+
+ beforeEach(() => {
+ serverless = new Serverless();
+ utils = new Utils(serverless);
+ serverless.init();
+ });
+
describe('#dirExistsSync()', () => {
describe('When reading a directory', () => {
it('should detect if a directory exists', () => {
@@ -259,60 +275,160 @@ describe('Utils', () => {
});
});
- describe('#track()', () => {
- let serverlessPath;
+ describe('#logStat()', () => {
+ let serverlessDirPath;
+ let homeDir;
beforeEach(() => {
serverless.init();
- // create a new tmpDir for the serverlessPath
+ // create a new tmpDir for the homeDir path
const tmpDirPath = testUtils.getTmpDirPath();
fse.mkdirsSync(tmpDirPath);
- serverlessPath = tmpDirPath;
- serverless.config.serverlessPath = tmpDirPath;
+ // save the homeDir so that we can reset this later on
+ homeDir = os.homedir();
+ process.env.HOME = tmpDirPath;
+ process.env.HOMEPATH = tmpDirPath;
+ process.env.USERPROFILE = tmpDirPath;
- // add some mock data to the serverless service object
- serverless.service.functions = {
- foo: {
- memorySize: 47,
- timeout: 11,
- events: [
- {
- http: 'GET foo',
- },
- ],
+ serverlessDirPath = path.join(os.homedir(), '.serverless');
+ });
+
+ it('should resolve if a file called stats-disabled is present', () => {
+ // create a stats-disabled file
+ serverless.utils.writeFileSync(
+ path.join(serverlessDirPath, 'stats-disabled'),
+ 'some content'
+ );
+
+ return utils.logStat(serverless).then(() => {
+ expect(fetchStub.calledOnce).to.equal(false);
+ });
+ });
+
+ it('should create a new file with a stats id if not found', () => {
+ const statsFilePath = path.join(serverlessDirPath, 'stats-enabled');
+
+ return serverless.utils.logStat(serverless).then(() => {
+ expect(fs.readFileSync(statsFilePath).toString().length).to.be.above(1);
+ });
+ });
+
+ it('should re-use an existing file which contains the stats id if found', () => {
+ const statsFilePath = path.join(serverlessDirPath, 'stats-enabled');
+ const statsId = 'some-id';
+
+ // create a new file with a stats id
+ fse.ensureFileSync(statsFilePath);
+ fs.writeFileSync(statsFilePath, statsId);
+
+ return serverless.utils.logStat(serverless).then(() => {
+ expect(fs.readFileSync(statsFilePath).toString()).to.be.equal(statsId);
+ });
+ });
+
+ it('should send the gathered information', () => {
+ serverless.service = {
+ service: 'new-service',
+ provider: {
+ name: 'aws',
+ runtime: 'nodejs4.3',
},
- bar: {
- events: [
- {
- http: 'GET foo',
- s3: 'someBucketName',
- },
- ],
+ defaults: {
+ stage: 'dev',
+ region: 'us-east-1',
+ variableSyntax: '\\${foo}',
+ },
+ plugins: [],
+ functions: {
+ functionOne: {
+ events: [
+ {
+ http: {
+ path: 'foo',
+ method: 'GET',
+ },
+ },
+ {
+ s3: 'my.bucket',
+ },
+ ],
+ },
+ functionTwo: {
+ memorySize: 16,
+ timeout: 200,
+ events: [
+ {
+ http: 'GET bar',
+ },
+ {
+ sns: 'my-topic-name',
+ },
+ ],
+ },
+ },
+ resources: {
+ Resources: {
+ foo: 'bar',
+ },
},
};
- });
- it('should create a new file with a tracking id if not found', () => {
- const trackingIdFilePath = path.join(serverlessPath, 'tracking-id');
+ return utils.logStat(serverless).then(() => {
+ expect(fetchStub.calledOnce).to.equal(true);
+ expect(fetchStub.args[0][0]).to.equal('https://api.segment.io/v1/track');
+ expect(fetchStub.args[0][1].method).to.equal('POST');
+ expect(fetchStub.args[0][1].timeout).to.equal('1000');
- return serverless.utils.track(serverless).then(() => {
- expect(fs.readFileSync(trackingIdFilePath).toString().length).to.be.above(1);
+ const parsedBody = JSON.parse(fetchStub.args[0][1].body);
+
+ expect(parsedBody.userId.length).to.be.at.least(1);
+ // command property
+ expect(parsedBody.properties.command
+ .isRunInService).to.equal(false); // false because CWD is not a service
+ // service property
+ expect(parsedBody.properties.service.numberOfCustomPlugins).to.equal(0);
+ expect(parsedBody.properties.service.hasCustomResourcesDefined).to.equal(true);
+ expect(parsedBody.properties.service.hasVariablesInCustomSectionDefined).to.equal(false);
+ expect(parsedBody.properties.service.hasCustomVariableSyntaxDefined).to.equal(true);
+ // functions property
+ expect(parsedBody.properties.functions.numberOfFunctions).to.equal(2);
+ expect(parsedBody.properties.functions.memorySizeAndTimeoutPerFunction[0]
+ .memorySize).to.equal(1024);
+ expect(parsedBody.properties.functions.memorySizeAndTimeoutPerFunction[0]
+ .timeout).to.equal(6);
+ expect(parsedBody.properties.functions.memorySizeAndTimeoutPerFunction[1]
+ .memorySize).to.equal(16);
+ expect(parsedBody.properties.functions.memorySizeAndTimeoutPerFunction[1]
+ .timeout).to.equal(200);
+ // events property
+ expect(parsedBody.properties.events.numberOfEvents).to.equal(3);
+ expect(parsedBody.properties.events.numberOfEventsPerType[0].name).to.equal('http');
+ expect(parsedBody.properties.events.numberOfEventsPerType[0].count).to.equal(2);
+ expect(parsedBody.properties.events.numberOfEventsPerType[1].name).to.equal('s3');
+ expect(parsedBody.properties.events.numberOfEventsPerType[1].count).to.equal(1);
+ expect(parsedBody.properties.events.numberOfEventsPerType[2].name).to.equal('sns');
+ expect(parsedBody.properties.events.numberOfEventsPerType[2].count).to.equal(1);
+ expect(parsedBody.properties.events.eventNamesPerFunction[0][0]).to.equal('http');
+ expect(parsedBody.properties.events.eventNamesPerFunction[0][1]).to.equal('s3');
+ expect(parsedBody.properties.events.eventNamesPerFunction[1][0]).to.equal('http');
+ expect(parsedBody.properties.events.eventNamesPerFunction[1][1]).to.equal('sns');
+ // general property
+ expect(parsedBody.properties.general.userId.length).to.be.at.least(1);
+ expect(parsedBody.properties.general.timestamp).to.match(/[0-9]+/);
+ expect(parsedBody.properties.general.timezone.length).to.be.at.least(1);
+ expect(parsedBody.properties.general.operatingSystem.length).to.be.at.least(1);
+ expect(parsedBody.properties.general.serverlessVersion).to.equal(serverlessVersion);
+ expect(parsedBody.properties.general.nodeJsVersion.length).to.be.at.least(1);
});
});
- it('should re-use an existing file which contains the tracking id if found', () => {
- const trackingIdFilePath = path.join(serverlessPath, 'tracking-id');
- const trackingId = 'some-tracking-id';
-
- // create a new file with a tracking id
- fse.ensureFileSync(trackingIdFilePath);
- fs.writeFileSync(trackingIdFilePath, trackingId);
-
- return serverless.utils.track(serverless).then(() => {
- expect(fs.readFileSync(trackingIdFilePath).toString()).to.be.equal(trackingId);
- });
+ afterEach(() => {
+ // recover the homeDir
+ process.env.HOME = homeDir;
+ process.env.HOMEPATH = homeDir;
+ process.env.USERPROFILE = homeDir;
});
});
});
diff --git a/tests/integration_test.js b/tests/integration_test.js
index 92a4be1fb..a83c8d27d 100644
--- a/tests/integration_test.js
+++ b/tests/integration_test.js
@@ -14,7 +14,7 @@ serverless.init();
const serverlessExec = path.join(serverless.config.serverlessPath, '..', 'bin', 'serverless');
const tmpDir = testUtils.getTmpDirPath();
-fse.mkdirSync(tmpDir);
+fse.mkdirsSync(tmpDir);
process.chdir(tmpDir);
const templateName = 'aws-nodejs';
@@ -47,7 +47,9 @@ describe('Service Lifecyle Integration Test', () => {
this.timeout(0);
const invoked = execSync(`${serverlessExec} invoke --function hello --noGreeting true`);
const result = JSON.parse(new Buffer(invoked, 'base64').toString());
- expect(result.message).to.be.equal('Go Serverless v1.0! Your function executed successfully!');
+ // parse it once again because the body is stringified to be LAMBDA-PROXY ready
+ const message = JSON.parse(result.body).message;
+ expect(message).to.be.equal('Go Serverless v1.0! Your function executed successfully!');
});
it('should deploy updated service to aws', function () {
diff --git a/tests/templates/integration-test-template b/tests/templates/integration-test-template
index 0ea20ed5f..0fec1dda1 100755
--- a/tests/templates/integration-test-template
+++ b/tests/templates/integration-test-template
@@ -31,10 +31,10 @@ else
fi
echo "Deploying Service"
-serverless deploy
+serverless deploy -v
echo "Invoking Service"
-serverless invoke --function hello --path event.json
+serverless invoke --function hello --path event.json -l
echo "Removing Service"
-serverless remove
+serverless remove -v
diff --git a/tests/templates/test_all_templates b/tests/templates/test_all_templates
index ce51a2638..d68c7268a 100755
--- a/tests/templates/test_all_templates
+++ b/tests/templates/test_all_templates
@@ -8,7 +8,7 @@ function integration-test {
$DIR/integration-test-template $@
}
-integration-test aws-java-gradle build
+integration-test aws-java-gradle ./gradlew build
integration-test aws-java-maven mvn package
integration-test aws-scala-sbt sbt assembly
integration-test aws-nodejs