next-deploy skeleton
This commit is contained in:
parent
23fbfdc85c
commit
4e71e620d1
235 changed files with 31121 additions and 2 deletions
24
.eslintrc.js
Normal file
24
.eslintrc.js
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ['plugin:prettier/recommended'],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2018,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
files: ['*.ts', '*.tsx'],
|
||||||
|
plugins: ['@typescript-eslint'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
||||||
|
'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
|
||||||
|
'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/ban-ts-ignore': 'off',
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
16
.github/stale.yml
vendored
Normal file
16
.github/stale.yml
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Number of days of inactivity before an issue becomes stale
|
||||||
|
daysUntilStale: 30
|
||||||
|
# Number of days of inactivity before a stale issue is closed
|
||||||
|
daysUntilClose: 7
|
||||||
|
# Issues with these labels will never be considered stale
|
||||||
|
exemptLabels:
|
||||||
|
- pinned
|
||||||
|
- security
|
||||||
|
# Label to use when marking an issue as stale
|
||||||
|
staleLabel: wontfix
|
||||||
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
|
markComment: >
|
||||||
|
This issue has been automatically marked as stale because it has not had
|
||||||
|
recent activity. It will be closed if no further activity occurs.
|
||||||
|
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||||
|
closeComment: true
|
||||||
24
.github/workflows/CI.yml
vendored
Normal file
24
.github/workflows/CI.yml
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: [ubuntu-latest]
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [10.x, 12.x, 14.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
- run: yarn --frozen-lockfile
|
||||||
|
- run: yarn test
|
||||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
node_modules
|
||||||
|
/coverage.data
|
||||||
|
/coverage/
|
||||||
|
.serverless
|
||||||
|
.next
|
||||||
|
.serverless_nextjs
|
||||||
|
dist
|
||||||
|
yarn-error.log
|
||||||
5
.prettierrc
Normal file
5
.prettierrc
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
||||||
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
- Demonstrating empathy and kindness toward other people
|
||||||
|
- Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
- Giving and gracefully accepting constructive feedback
|
||||||
|
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
- Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
- The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
- Public or private harassment
|
||||||
|
- Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
- Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
[INSERT CONTACT METHOD].
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
7
LICENSE
Normal file
7
LICENSE
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
Copyright 2020 Nidratech Ltd.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
407
README.md
407
README.md
|
|
@ -1,2 +1,405 @@
|
||||||
# next-deploy
|
# Serverless Nextjs Component
|
||||||
Deployment of Next.js apps.
|
|
||||||
|
A zero configuration Nextjs deployment.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [Motivation](#motivation)
|
||||||
|
- [Design principles](#design-principles)
|
||||||
|
- [Features](#features)
|
||||||
|
- [Getting started](#getting-started)
|
||||||
|
- [Lambda@Edge configuration](#lambda-at-edge-configuration)
|
||||||
|
- [Custom domain name](#custom-domain-name)
|
||||||
|
- [Custom CloudFront configuration](#custom-cloudfront-configuration)
|
||||||
|
- [Static pages caching](#static-pages-caching)
|
||||||
|
- [Public directory caching](#public-directory-caching)
|
||||||
|
- [AWS Permissions](#aws-permissions)
|
||||||
|
- [Inputs](#inputs)
|
||||||
|
- [FAQ](#faq)
|
||||||
|
|
||||||
|
### Motivation
|
||||||
|
|
||||||
|
Ez pz deployments for Next.js
|
||||||
|
|
||||||
|
### Design principles
|
||||||
|
|
||||||
|
1. Zero configuration by default
|
||||||
|
|
||||||
|
There is no configuration needed. You can extend defaults based on your application needs.
|
||||||
|
|
||||||
|
2. Feature parity with nextjs
|
||||||
|
|
||||||
|
Users of this component should be able to use nextjs development tooling, aka `next dev`. It is the component's job to deploy your application ensuring parity with all of next's features we know and love.
|
||||||
|
|
||||||
|
3. Fast deployments / no CloudFormation resource limits.
|
||||||
|
|
||||||
|
With a simplified architecture and no use of CloudFormation, there are no limits to how many pages you can have in your application, plus deployment times are very fast! with the exception of CloudFront propagation times of course.
|
||||||
|
|
||||||
|
### Getting started
|
||||||
|
|
||||||
|
Set your AWS credentials as environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AWS_ACCESS_KEY_ID=accesskey
|
||||||
|
AWS_SECRET_ACCESS_KEY=sshhh
|
||||||
|
```
|
||||||
|
|
||||||
|
And simply deploy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ serverless
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom domain name
|
||||||
|
|
||||||
|
In most cases you wouldn't want to use CloudFront's distribution domain to access your application. Instead, you can specify a custom domain name.
|
||||||
|
|
||||||
|
You can use any domain name but you must be using AWS Route53 for your DNS hosting. To migrate DNS records from an existing domain follow the instructions
|
||||||
|
[here](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/MigratingDNS.html). The requirements to use a custom domain name:
|
||||||
|
|
||||||
|
- Route53 must include a _hosted zone_ for your domain (e.g. `mydomain.com`) with a set of nameservers.
|
||||||
|
- You must update the nameservers listed with your domain name registrar (e.g. namecheap, godaddy, etc.) with those provided for your new _hosted zone_.
|
||||||
|
|
||||||
|
The serverless next.js component will automatically generate an SSL certificate and create a new record to point to your CloudFront distribution.
|
||||||
|
|
||||||
|
```yml
|
||||||
|
# serverless.yml
|
||||||
|
|
||||||
|
myNextApplication:
|
||||||
|
component: serverless-next.js
|
||||||
|
inputs:
|
||||||
|
domain: 'example.com' # sub-domain defaults to www
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also configure a `subdomain`:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
# serverless.yml
|
||||||
|
|
||||||
|
myNextApplication:
|
||||||
|
component: serverless-next.js
|
||||||
|
inputs:
|
||||||
|
domain: ['sub', 'example.com'] # [ sub-domain, domain ]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom CloudFront configuration
|
||||||
|
|
||||||
|
To specify your own CloudFront inputs, just add any [aws-cloudfront inputs](https://github.com/serverless-components/aws-cloudfront#3-configure) under `cloudfront`:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
# serverless.yml
|
||||||
|
|
||||||
|
myNextApplication:
|
||||||
|
component: serverless-next.js
|
||||||
|
inputs:
|
||||||
|
cloudfront:
|
||||||
|
# this is the default cache behaviour of the cloudfront distribution
|
||||||
|
# the origin-request edge lambda associated to this cache behaviour does the pages server side rendering
|
||||||
|
defaults:
|
||||||
|
forward:
|
||||||
|
headers:
|
||||||
|
[CloudFront-Is-Desktop-Viewer, CloudFront-Is-Mobile-Viewer, CloudFront-Is-Tablet-Viewer]
|
||||||
|
# this is the cache behaviour for next.js api pages
|
||||||
|
api:
|
||||||
|
ttl: 10
|
||||||
|
# you can set other cache behaviours like "defaults" above that can handle server side rendering
|
||||||
|
# but more specific for a subset of your next.js pages
|
||||||
|
/blog/*:
|
||||||
|
ttl: 1000
|
||||||
|
forward:
|
||||||
|
cookies: 'all'
|
||||||
|
queryString: false
|
||||||
|
/about:
|
||||||
|
ttl: 3000
|
||||||
|
# you can add custom origins to the cloudfront distribution
|
||||||
|
origins:
|
||||||
|
- url: /static
|
||||||
|
pathPatterns:
|
||||||
|
/wp-content/*:
|
||||||
|
ttl: 10
|
||||||
|
- url: https://old-static.com
|
||||||
|
pathPatterns:
|
||||||
|
/old-static/*:
|
||||||
|
ttl: 10
|
||||||
|
```
|
||||||
|
|
||||||
|
### Static pages caching
|
||||||
|
|
||||||
|
Statically rendered pages (i.e. HTML pages that are uploaded to S3) have the following Cache-Control set:
|
||||||
|
|
||||||
|
```
|
||||||
|
cache-control: public, max-age=0, s-maxage=2678400, must-revalidate
|
||||||
|
```
|
||||||
|
|
||||||
|
`s-maxage` allows Cloudfront to cache the pages at the edge locations for 31 days.
|
||||||
|
`max-age=0` in combination with `must-revalidate` ensure browsers never cache the static pages. This allows Cloudfront to be in full control of caching TTLs. On every deployment an invalidation`/*` is created to ensure users get fresh content.
|
||||||
|
|
||||||
|
### Public directory caching
|
||||||
|
|
||||||
|
By default, common image formats(`gif|jpe?g|jp2|tiff|png|webp|bmp|svg|ico`) under `/public` or `/static` folders
|
||||||
|
have a one-year `Cache-Control` policy applied(`public, max-age=31536000, must-revalidate`).
|
||||||
|
You may customize either the `Cache-Control` header `value` and the regex of which files to `test`, with `publicDirectoryCache`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
myNextApplication:
|
||||||
|
component: serverless-next.js
|
||||||
|
inputs:
|
||||||
|
publicDirectoryCache:
|
||||||
|
value: public, max-age=604800
|
||||||
|
test: /\.(gif|jpe?g|png|txt|xml)$/i
|
||||||
|
```
|
||||||
|
|
||||||
|
`value` must be a valid `Cache-Control` policy and `test` must be a valid `regex` of the types of files you wish to test.
|
||||||
|
If you don't want browsers to cache assets from the public directory, you can disable this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
myNextApplication:
|
||||||
|
component: serverless-next.js
|
||||||
|
inputs:
|
||||||
|
publicDirectoryCache: false
|
||||||
|
```
|
||||||
|
|
||||||
|
### AWS Permissions
|
||||||
|
|
||||||
|
By default the Lambda@Edge functions run using AWSLambdaBasicExecutionRole which only allows uploading logs to CloudWatch. If you need permissions beyond this, like for example access to DynamoDB or any other AWS resource you will need your own custom policy arn:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
# serverless.yml
|
||||||
|
|
||||||
|
myNextApplication:
|
||||||
|
component: serverless-next.js
|
||||||
|
inputs:
|
||||||
|
policy: 'arn:aws:iam::123456789012:policy/MyCustomPolicy'
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure you add CloudWatch log permissions to your custom policy.
|
||||||
|
|
||||||
|
The exhaustive list of AWS actions required for a deployment:
|
||||||
|
|
||||||
|
```
|
||||||
|
"acm:DescribeCertificate", // only for custom domains
|
||||||
|
"acm:ListCertificates", // only for custom domains
|
||||||
|
"acm:RequestCertificate", // only for custom domains
|
||||||
|
"cloudfront:CreateCloudFrontOriginAccessIdentity",
|
||||||
|
"cloudfront:CreateDistribution",
|
||||||
|
"cloudfront:CreateInvalidation",
|
||||||
|
"cloudfront:GetDistribution",
|
||||||
|
"cloudfront:GetDistributionConfig",
|
||||||
|
"cloudfront:ListCloudFrontOriginAccessIdentities",
|
||||||
|
"cloudfront:ListDistributions",
|
||||||
|
"cloudfront:ListDistributionsByLambdaFunction",
|
||||||
|
"cloudfront:ListDistributionsByWebACLId",
|
||||||
|
"cloudfront:ListFieldLevelEncryptionConfigs",
|
||||||
|
"cloudfront:ListFieldLevelEncryptionProfiles",
|
||||||
|
"cloudfront:ListInvalidations",
|
||||||
|
"cloudfront:ListPublicKeys",
|
||||||
|
"cloudfront:ListStreamingDistributions",
|
||||||
|
"cloudfront:UpdateDistribution",
|
||||||
|
"iam:AttachRolePolicy",
|
||||||
|
"iam:CreateRole",
|
||||||
|
"iam:CreateServiceLinkedRole",
|
||||||
|
"iam:GetRole",
|
||||||
|
"iam:PassRole",
|
||||||
|
"lambda:CreateFunction",
|
||||||
|
"lambda:EnableReplication",
|
||||||
|
"lambda:DeleteFunction", // only for custom domains
|
||||||
|
"lambda:GetFunction",
|
||||||
|
"lambda:GetFunctionConfiguration",
|
||||||
|
"lambda:PublishVersion",
|
||||||
|
"lambda:UpdateFunctionCode",
|
||||||
|
"lambda:UpdateFunctionConfiguration",
|
||||||
|
"route53:ChangeResourceRecordSets", // only for custom domains
|
||||||
|
"route53:ListHostedZonesByName",
|
||||||
|
"route53:ListResourceRecordSets", // only for custom domains
|
||||||
|
"s3:CreateBucket",
|
||||||
|
"s3:GetAccelerateConfiguration",
|
||||||
|
"s3:GetObject", // only if persisting state to S3 for CI/CD
|
||||||
|
"s3:HeadBucket",
|
||||||
|
"s3:ListBucket",
|
||||||
|
"s3:PutAccelerateConfiguration",
|
||||||
|
"s3:PutBucketPolicy",
|
||||||
|
"s3:PutObject"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lambda At Edge Configuration
|
||||||
|
|
||||||
|
Both **default** and **api** edge lambdas will be assigned 512mb of memory by default. This value can be altered by assigning a number to the `memory` input
|
||||||
|
|
||||||
|
```yml
|
||||||
|
# serverless.yml
|
||||||
|
|
||||||
|
myNextApplication:
|
||||||
|
component: serverless-next.js
|
||||||
|
inputs:
|
||||||
|
memory: 1024
|
||||||
|
```
|
||||||
|
|
||||||
|
Values for **default** and **api** lambdas can be separately defined by assigning `memory` to an object like so:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
# serverless.yml
|
||||||
|
|
||||||
|
myNextApplication:
|
||||||
|
component: serverless-next.js
|
||||||
|
inputs:
|
||||||
|
memory:
|
||||||
|
defaultLambda: 1024
|
||||||
|
apiLambda: 2048
|
||||||
|
```
|
||||||
|
|
||||||
|
The same pattern can be followed for specifying the Node.js runtime (nodejs12.x by default):
|
||||||
|
|
||||||
|
```yml
|
||||||
|
# serverless.yml
|
||||||
|
|
||||||
|
myNextApplication:
|
||||||
|
component: serverless-next.js
|
||||||
|
inputs:
|
||||||
|
runtime:
|
||||||
|
defaultLambda: 'nodejs10.x'
|
||||||
|
apiLambda: 'nodejs10.x'
|
||||||
|
```
|
||||||
|
|
||||||
|
Similarly, the timeout by default is 10 seconds. To customise you can:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
# serverless.yml
|
||||||
|
|
||||||
|
myNextApplication:
|
||||||
|
component: serverless-next.js
|
||||||
|
inputs:
|
||||||
|
timeout:
|
||||||
|
defaultLambda: 20
|
||||||
|
apiLambda: 15
|
||||||
|
```
|
||||||
|
|
||||||
|
Note the maximum timeout allowed for Lambda@Edge is 30 seconds. See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html
|
||||||
|
|
||||||
|
You can also set a custom name for **default** and **api** lambdas - if not the default is set by the [aws-lambda serverless component](https://github.com/serverless-components/aws-lambda) to the resource id:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
# serverless.yml
|
||||||
|
|
||||||
|
myNextApplication:
|
||||||
|
component: serverless-next.js
|
||||||
|
inputs:
|
||||||
|
name:
|
||||||
|
defaultLambda: fooDefaultLambda
|
||||||
|
apiLambda: fooApiLambda
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
|
||||||
|
| Name | Type | Default Value | Description |
|
||||||
|
| ------------- | ----------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| domain | `Array` | `null` | For example `['admin', 'portal.com']` |
|
||||||
|
| bucketName | `string` | `null` | Custom bucket name where static assets are stored. By default is autogenerated. |
|
||||||
|
| bucketRegion | `string` | `null` | Region where you want to host your s3 bucket. Make sure this is geographically closer to the majority of your end users to reduce latency when CloudFront proxies a request to S3. On first deployment, you may experience 307 temporary redirects if the configured region is not us-east-1. See https://aws.amazon.com/premiumsupport/knowledge-center/s3-http-307-response/ for more details. |
|
||||||
|
| nextConfigDir | `string` | `./` | Directory where your application `next.config.js` file is. This input is useful when the `serverless.yml` is not in the same directory as the next app. <br>**Note:** `nextConfigDir` should be set if `next.config.js` `distDir` is used |
|
||||||
|
| nextStaticDir | `string` | `./` | If your `static` or `public` directory is not a direct child of `nextConfigDir` this is needed |
|
||||||
|
| description | `string` | `*lambda-type*@Edge for Next CloudFront distribution` | The description that will be used for both lambdas. Note that "(API)" will be appended to the API lambda description. |
|
||||||
|
| policy | `string` | `arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole` | The arn policy that will be assigned to both lambdas. |
|
||||||
|
| runtime | `string\|object` | `nodejs12.x` | When assigned a value, both the default and api lambdas will be assigned the runtime defined in the value. When assigned to an object, values for the default and api lambdas can be separately defined | |
|
||||||
|
| memory | `number\|object` | `512` | When assigned a number, both the default and api lambdas will be assigned memory of that value. When assigned to an object, values for the default and api lambdas can be separately defined | |
|
||||||
|
| timeout | `number\|object` | `10` | Same as above |
|
||||||
|
| name | `string\|object` | / | When assigned a string, both the default and api lambdas will assigned name of that value. When assigned to an object, values for the default and api lambdas can be separately defined |
|
||||||
|
| build | `boolean\|object` | `true` | When true builds and deploys app, when false assume the app has been built and uses the `.next` `.serverless_nextjs` directories in `nextConfigDir` to deploy. If an object is passed `build` allows for overriding what script gets called and with what arguments. |
|
||||||
|
| build.cmd | `string` | `node_modules/.bin/next` | Build command |
|
||||||
|
| build.args | `Array\|string` | `['build']` | Arguments to pass to the build |
|
||||||
|
| build.cwd | `string` | `./` | Override the current working directory |
|
||||||
|
| build.enabled | `boolean` | `true` | Same as passing `build:false` but from within the config |
|
||||||
|
|
||||||
|
| cloudfront | `object` | `{}` | Inputs to be passed to [aws-cloudfront](https://github.com/serverless-components/aws-cloudfront) |
|
||||||
|
| domainType | `string` | `"both"` | Can be one of: `"apex"` - apex domain only, don't create a www subdomain. `"www"` - www domain only, don't create an apex subdomain.`"both"` - create both www and apex domains when either one is provided. |
|
||||||
|
| publicDirectoryCache | `boolean\|object` | `true` | Customize the `public`/`static` folder asset caching policy. Assigning an object with `value` and/or `test` lets you customize the caching policy and the types of files being cached. Assigning false disables caching |
|
||||||
|
| useServerlessTraceTarget | `boolean` | `false` | Use the experimental-serverless-trace target to build your next app. This is the same build target that Vercel Now uses. See this [RFC](https://github.com/vercel/next.js/pull/8246) for details. |
|
||||||
|
| verbose | `boolean` | `false` | Print verbose output to the console. |
|
||||||
|
|
||||||
|
Custom inputs can be configured like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
myNextApp:
|
||||||
|
component: serverless-next.js
|
||||||
|
inputs:
|
||||||
|
bucketName: my-bucket
|
||||||
|
```
|
||||||
|
|
||||||
|
### FAQ
|
||||||
|
|
||||||
|
#### My component doesn't deploy
|
||||||
|
|
||||||
|
Make sure your `serverless.yml` uses the `serverless-components` format. [serverless components](https://serverless.com/blog/what-are-serverless-components-how-use/) differ from the original serverless framework, even though they are both accessible via the same CLI.
|
||||||
|
|
||||||
|
✅ **Do**
|
||||||
|
|
||||||
|
```yml
|
||||||
|
# serverless.yml
|
||||||
|
myNextApp:
|
||||||
|
component: serverless-next.js
|
||||||
|
|
||||||
|
myTable:
|
||||||
|
component: serverless/aws-dynamodb
|
||||||
|
inputs:
|
||||||
|
name: Customers
|
||||||
|
# other components
|
||||||
|
```
|
||||||
|
|
||||||
|
❌ **Don't**
|
||||||
|
|
||||||
|
```yml
|
||||||
|
# serverless.yml
|
||||||
|
provider:
|
||||||
|
name: aws
|
||||||
|
runtime: nodejs10.x
|
||||||
|
region: eu-west-1
|
||||||
|
|
||||||
|
myNextApp:
|
||||||
|
component: serverless-next.js
|
||||||
|
|
||||||
|
Resources: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Note how the correct yaml doesn't declare a `provider`, `Resources`, etc.
|
||||||
|
|
||||||
|
For deploying, don't run `serverless deploy`. Simply run `serverless` and that deploys your components declared in the `serverless.yml` file.
|
||||||
|
|
||||||
|
For more information about serverless components go [here](https://serverless.com/blog/what-are-serverless-components-how-use/).
|
||||||
|
|
||||||
|
#### How do I interact with other AWS Services within my app?
|
||||||
|
|
||||||
|
See `examples/dynamodb-crud` for an example Todo application that interacts with DynamoDB.
|
||||||
|
|
||||||
|
#### [CI/CD] A new CloudFront distribution is created on every CI build. I wasn't expecting that
|
||||||
|
|
||||||
|
You need to commit your application state in source control. That is the files under the `.serverless` directory. Alternatively you could use S3 to store the `.serverless` files, see an example [here](https://gist.github.com/hadynz/b4e190e0ce10e5811cb462920a9c678f)
|
||||||
|
|
||||||
|
The serverless team is currently working on remote state storage so this won't be necessary in the future.
|
||||||
|
|
||||||
|
#### My lambda is deployed to `us-east-1`. How can I deploy it to another region?
|
||||||
|
|
||||||
|
Serverless next.js is _regionless_. By design, `serverless-next.js` applications will be deployed across the globe to every CloudFront edge location. The lambda might look like is only deployed to `us-east-1` but behind the scenes, it is replicated to every other region.
|
||||||
|
|
||||||
|
#### I require passing additional information into my build
|
||||||
|
|
||||||
|
See the sample below for an advanced `build` setup that includes passing additional arguments and environment variables to the build.
|
||||||
|
|
||||||
|
```yml
|
||||||
|
# serverless.yml
|
||||||
|
myDatabase:
|
||||||
|
component: MY_DATABASE_COMPNENT
|
||||||
|
myNextApp:
|
||||||
|
component: serverless-next.js
|
||||||
|
build:
|
||||||
|
args: ['build', 'custom/path/to/pages']
|
||||||
|
env:
|
||||||
|
DATABASE_URL: ${myDatabase.databaseUrl}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### I was expecting for automatic subdomain redirection when using the domainType: www/apex input
|
||||||
|
|
||||||
|
The redirection is not currently implemented, but there is a manual workaround outlined [here](https://simonecarletti.com/blog/2016/08/redirect-domain-https-amazon-cloudfront/#configuring-the-amazon-s3-static-site-with-redirect).
|
||||||
|
In summary, you will have to create a new S3 bucket and set it up with static website hosting to redirect requests to your supported subdomain type (ex. "www.example.com" or "example.com"). To be able to support HTTPS redirects, you'll need to set up a CloudFront distribution with the S3 redirect bucket as the origin. Finally, you'll need to create an "A" record in Route 53 with your newly created CloudFront distribution as the alias target.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Please see the [contributing](./CONTRIBUTING.md) guide.
|
||||||
|
|
|
||||||
4
babel.config.js
Normal file
4
babel.config.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
|
||||||
|
plugins: ['@babel/plugin-proposal-class-properties'],
|
||||||
|
};
|
||||||
18
jest.config.js
Normal file
18
jest.config.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
module.exports = {
|
||||||
|
clearMocks: true,
|
||||||
|
collectCoverage: true,
|
||||||
|
collectCoverageFrom: ['<rootDir>/packages/**/*.{js,ts}'],
|
||||||
|
moduleNameMapper: {
|
||||||
|
'fs-extra': '<rootDir>/node_modules/fs-extra',
|
||||||
|
},
|
||||||
|
coverageDirectory: '<rootDir>/coverage/',
|
||||||
|
coveragePathIgnorePatterns: [
|
||||||
|
'/.serverless_nextjs/',
|
||||||
|
'/fixtures/',
|
||||||
|
'/fixture/',
|
||||||
|
'/dist/',
|
||||||
|
'/tests/',
|
||||||
|
],
|
||||||
|
watchPathIgnorePatterns: ['/fixture/', '/fixtures/'],
|
||||||
|
testPathIgnorePatterns: ['/.next/', '/node_modules/', '/fixtures/', '/fixture/', '/examples/'],
|
||||||
|
};
|
||||||
11
lerna.json
Normal file
11
lerna.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"version": "independent",
|
||||||
|
"npmClient": "yarn",
|
||||||
|
"command": {
|
||||||
|
"publish": {
|
||||||
|
"ignoreChanges": ["*.md"],
|
||||||
|
"message": "chore(release): publish"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages": ["packages/*"]
|
||||||
|
}
|
||||||
80
package.json
Normal file
80
package.json
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
{
|
||||||
|
"name": "next-deploy",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Effortlessly deploy your Next.js application.",
|
||||||
|
"author": "Nidratech Ltd. <contact@nidratech.com>",
|
||||||
|
"main": "index.js",
|
||||||
|
"directories": {
|
||||||
|
"example": "examples"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"next",
|
||||||
|
"deploy",
|
||||||
|
"serverless",
|
||||||
|
"nextjs",
|
||||||
|
"lambda"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"prepare": "yarn packages-install",
|
||||||
|
"test": "jest --runInBand",
|
||||||
|
"packages-install": "lerna exec yarn",
|
||||||
|
"packages-build": "lerna run build",
|
||||||
|
"test:watch": "yarn test --watch --collect-coverage=false",
|
||||||
|
"publish": "lerna publish --conventional-commits",
|
||||||
|
"prerelease": "lerna publish --conventional-commits --conventional-prerelease",
|
||||||
|
"graduate": "lerna publish --conventional-commits --conventional-graduate",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"coveralls": "jest --runInBand --coverage && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/nidratech/next-deploy"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/nidratech/next-deploy/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/nidratech/next-deploy#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/preset-typescript": "^7.10.4",
|
||||||
|
"lambda-at-edge": "link:./packages/lambda-at-edge",
|
||||||
|
"next-aws-cloudfront": "link:./packages/lambda-at-edge-compat",
|
||||||
|
"@types/fs-extra": "^9.0.1",
|
||||||
|
"@types/jest": "^26.0.3",
|
||||||
|
"@types/react": "^16.9.41",
|
||||||
|
"@types/react-dom": "^16.9.8",
|
||||||
|
"@types/webpack": "^4.41.18",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^3.5.0",
|
||||||
|
"@typescript-eslint/parser": "^3.5.0",
|
||||||
|
"adm-zip": "^0.4.16",
|
||||||
|
"aws-sdk": "^2.709.0",
|
||||||
|
"coveralls": "^3.1.0",
|
||||||
|
"eslint": "^7.4.0",
|
||||||
|
"eslint-config-prettier": "^6.11.0",
|
||||||
|
"eslint-plugin-prettier": "^3.1.4",
|
||||||
|
"fs-extra": "^9.0.1",
|
||||||
|
"husky": "^4.2.5",
|
||||||
|
"jest": "^26.1.0",
|
||||||
|
"jest-when": "^2.7.2",
|
||||||
|
"lerna": "^3.22.1",
|
||||||
|
"lint-staged": "^10.2.11",
|
||||||
|
"next": "^9.4.4",
|
||||||
|
"prettier": "^2.0.5",
|
||||||
|
"react": "^16.13.1",
|
||||||
|
"react-dom": "^16.13.1",
|
||||||
|
"serverless": "^1.74.1",
|
||||||
|
"serverless-offline": "^6.4.0",
|
||||||
|
"typescript": "^3.9.6"
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "lint-staged"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,ts,md,yml}": "prettier --write"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"which": "^2.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
56
packages/aws-cloudfront/__mocks__/aws-sdk.mock.js
Normal file
56
packages/aws-cloudfront/__mocks__/aws-sdk.mock.js
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
const promisifyMock = (mockFn) => {
|
||||||
|
const promise = jest.fn();
|
||||||
|
mockFn.mockImplementation(() => ({
|
||||||
|
promise,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCreateDistribution = jest.fn();
|
||||||
|
const mockCreateDistributionPromise = promisifyMock(mockCreateDistribution);
|
||||||
|
|
||||||
|
const mockUpdateDistribution = jest.fn();
|
||||||
|
const mockUpdateDistributionPromise = promisifyMock(mockUpdateDistribution);
|
||||||
|
|
||||||
|
const mockGetDistributionConfig = jest.fn();
|
||||||
|
const mockGetDistributionConfigPromise = promisifyMock(mockGetDistributionConfig);
|
||||||
|
|
||||||
|
const mockDeleteDistribution = jest.fn();
|
||||||
|
const mockDeleteDistributionPromise = promisifyMock(mockDeleteDistribution);
|
||||||
|
|
||||||
|
const mockCreateCloudFrontOriginAccessIdentity = jest.fn();
|
||||||
|
const mockCreateCloudFrontOriginAccessIdentityPromise = promisifyMock(
|
||||||
|
mockCreateCloudFrontOriginAccessIdentity
|
||||||
|
);
|
||||||
|
|
||||||
|
const mockPutBucketPolicy = jest.fn();
|
||||||
|
const mockPutBucketPolicyPromise = promisifyMock(mockPutBucketPolicy);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mockCreateDistribution,
|
||||||
|
mockUpdateDistribution,
|
||||||
|
mockGetDistributionConfig,
|
||||||
|
mockDeleteDistribution,
|
||||||
|
mockCreateCloudFrontOriginAccessIdentity,
|
||||||
|
mockPutBucketPolicy,
|
||||||
|
|
||||||
|
mockPutBucketPolicyPromise,
|
||||||
|
mockCreateDistributionPromise,
|
||||||
|
mockUpdateDistributionPromise,
|
||||||
|
mockGetDistributionConfigPromise,
|
||||||
|
mockDeleteDistributionPromise,
|
||||||
|
mockCreateCloudFrontOriginAccessIdentityPromise,
|
||||||
|
|
||||||
|
CloudFront: jest.fn(() => ({
|
||||||
|
createDistribution: mockCreateDistribution,
|
||||||
|
updateDistribution: mockUpdateDistribution,
|
||||||
|
getDistributionConfig: mockGetDistributionConfig,
|
||||||
|
deleteDistribution: mockDeleteDistribution,
|
||||||
|
createCloudFrontOriginAccessIdentity: mockCreateCloudFrontOriginAccessIdentity,
|
||||||
|
})),
|
||||||
|
|
||||||
|
S3: jest.fn(() => ({
|
||||||
|
putBucketPolicy: mockPutBucketPolicy,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,255 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Input origin as a custom url creates distribution with custom behavior options 1`] = `
|
||||||
|
Object {
|
||||||
|
"DistributionConfig": Object {
|
||||||
|
"Aliases": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CacheBehaviors": Object {
|
||||||
|
"Items": Array [
|
||||||
|
Object {
|
||||||
|
"AllowedMethods": Object {
|
||||||
|
"CachedMethods": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"GET",
|
||||||
|
"HEAD",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Items": Array [
|
||||||
|
"GET",
|
||||||
|
"HEAD",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Compress": false,
|
||||||
|
"DefaultTTL": 0,
|
||||||
|
"FieldLevelEncryptionId": "321",
|
||||||
|
"ForwardedValues": Object {
|
||||||
|
"Cookies": Object {
|
||||||
|
"Forward": "whitelist",
|
||||||
|
"WhitelistedNames": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"auth-token",
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Headers": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"*",
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
"QueryString": true,
|
||||||
|
"QueryStringCacheKeys": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"LambdaFunctionAssociations": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"MaxTTL": 0,
|
||||||
|
"MinTTL": 0,
|
||||||
|
"PathPattern": "/sample/path",
|
||||||
|
"SmoothStreaming": false,
|
||||||
|
"TargetOriginId": "mycustomorigin.com",
|
||||||
|
"TrustedSigners": Object {
|
||||||
|
"Enabled": false,
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"ViewerProtocolPolicy": "redirect-to-https",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
"CallerReference": "1566599541192",
|
||||||
|
"Comment": "",
|
||||||
|
"DefaultCacheBehavior": Object {
|
||||||
|
"AllowedMethods": Object {
|
||||||
|
"CachedMethods": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Compress": false,
|
||||||
|
"DefaultTTL": 0,
|
||||||
|
"FieldLevelEncryptionId": "",
|
||||||
|
"ForwardedValues": Object {
|
||||||
|
"Cookies": Object {
|
||||||
|
"Forward": "none",
|
||||||
|
},
|
||||||
|
"Headers": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"QueryString": false,
|
||||||
|
"QueryStringCacheKeys": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"LambdaFunctionAssociations": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"MaxTTL": 31536000,
|
||||||
|
"MinTTL": 0,
|
||||||
|
"SmoothStreaming": false,
|
||||||
|
"TargetOriginId": "mycustomorigin.com",
|
||||||
|
"TrustedSigners": Object {
|
||||||
|
"Enabled": false,
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"ViewerProtocolPolicy": "redirect-to-https",
|
||||||
|
},
|
||||||
|
"Enabled": true,
|
||||||
|
"HttpVersion": "http2",
|
||||||
|
"Origins": Object {
|
||||||
|
"Items": Array [
|
||||||
|
Object {
|
||||||
|
"CustomHeaders": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CustomOriginConfig": Object {
|
||||||
|
"HTTPPort": 80,
|
||||||
|
"HTTPSPort": 443,
|
||||||
|
"OriginKeepaliveTimeout": 5,
|
||||||
|
"OriginProtocolPolicy": "https-only",
|
||||||
|
"OriginReadTimeout": 30,
|
||||||
|
"OriginSslProtocols": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"TLSv1.2",
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"DomainName": "mycustomorigin.com",
|
||||||
|
"Id": "mycustomorigin.com",
|
||||||
|
"OriginPath": "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
"PriceClass": "PriceClass_All",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Input origin as a custom url creates distribution with custom default behavior options 1`] = `
|
||||||
|
Object {
|
||||||
|
"DistributionConfig": Object {
|
||||||
|
"Aliases": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CacheBehaviors": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CallerReference": "1566599541192",
|
||||||
|
"Comment": "",
|
||||||
|
"DefaultCacheBehavior": Object {
|
||||||
|
"AllowedMethods": Object {
|
||||||
|
"CachedMethods": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Items": Array [
|
||||||
|
"GET",
|
||||||
|
"HEAD",
|
||||||
|
"OPTIONS",
|
||||||
|
"PUT",
|
||||||
|
"POST",
|
||||||
|
"PATCH",
|
||||||
|
"DELETE",
|
||||||
|
],
|
||||||
|
"Quantity": 7,
|
||||||
|
},
|
||||||
|
"Compress": true,
|
||||||
|
"DefaultTTL": 0,
|
||||||
|
"FieldLevelEncryptionId": "123",
|
||||||
|
"ForwardedValues": Object {
|
||||||
|
"Cookies": Object {
|
||||||
|
"Forward": "all",
|
||||||
|
},
|
||||||
|
"Headers": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"Accept",
|
||||||
|
"Accept-Language",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"QueryString": true,
|
||||||
|
"QueryStringCacheKeys": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"query",
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"LambdaFunctionAssociations": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"MaxTTL": 31536000,
|
||||||
|
"MinTTL": 0,
|
||||||
|
"SmoothStreaming": true,
|
||||||
|
"TargetOriginId": "mycustomorigin.com",
|
||||||
|
"TrustedSigners": Object {
|
||||||
|
"Enabled": false,
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"ViewerProtocolPolicy": "https-only",
|
||||||
|
},
|
||||||
|
"Enabled": true,
|
||||||
|
"HttpVersion": "http2",
|
||||||
|
"Origins": Object {
|
||||||
|
"Items": Array [
|
||||||
|
Object {
|
||||||
|
"CustomHeaders": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CustomOriginConfig": Object {
|
||||||
|
"HTTPPort": 80,
|
||||||
|
"HTTPSPort": 443,
|
||||||
|
"OriginKeepaliveTimeout": 5,
|
||||||
|
"OriginProtocolPolicy": "https-only",
|
||||||
|
"OriginReadTimeout": 30,
|
||||||
|
"OriginSslProtocols": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"TLSv1.2",
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"DomainName": "mycustomorigin.com",
|
||||||
|
"Id": "mycustomorigin.com",
|
||||||
|
"OriginPath": "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
"PriceClass": "PriceClass_All",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Input origin as a custom url creates distribution with custom url origin and sets defaults 1`] = `
|
||||||
|
Object {
|
||||||
|
"DistributionConfig": Object {
|
||||||
|
"Aliases": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CacheBehaviors": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CallerReference": "1566599541192",
|
||||||
|
"Comment": "",
|
||||||
|
"DefaultCacheBehavior": Object {
|
||||||
|
"AllowedMethods": Object {
|
||||||
|
"CachedMethods": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"DELETE",
|
||||||
|
"POST",
|
||||||
|
"GET",
|
||||||
|
"OPTIONS",
|
||||||
|
"PUT",
|
||||||
|
"PATCH",
|
||||||
|
],
|
||||||
|
"Quantity": 7,
|
||||||
|
},
|
||||||
|
"Compress": false,
|
||||||
|
"DefaultTTL": 10,
|
||||||
|
"FieldLevelEncryptionId": "",
|
||||||
|
"ForwardedValues": Object {
|
||||||
|
"Cookies": Object {
|
||||||
|
"Forward": "none",
|
||||||
|
},
|
||||||
|
"Headers": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"QueryString": false,
|
||||||
|
"QueryStringCacheKeys": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"LambdaFunctionAssociations": Object {
|
||||||
|
"Items": Array [
|
||||||
|
Object {
|
||||||
|
"EventType": "origin-request",
|
||||||
|
"IncludeBody": true,
|
||||||
|
"LambdaFunctionARN": "arn:aws:lambda:us-east-1:123:function:originRequestFunction",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
"MaxTTL": 31536000,
|
||||||
|
"MinTTL": 0,
|
||||||
|
"SmoothStreaming": false,
|
||||||
|
"TargetOriginId": "mycustomorigin.com",
|
||||||
|
"TrustedSigners": Object {
|
||||||
|
"Enabled": false,
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"ViewerProtocolPolicy": "redirect-to-https",
|
||||||
|
},
|
||||||
|
"Enabled": true,
|
||||||
|
"HttpVersion": "http2",
|
||||||
|
"Origins": Object {
|
||||||
|
"Items": Array [
|
||||||
|
Object {
|
||||||
|
"CustomHeaders": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CustomOriginConfig": Object {
|
||||||
|
"HTTPPort": 80,
|
||||||
|
"HTTPSPort": 443,
|
||||||
|
"OriginKeepaliveTimeout": 5,
|
||||||
|
"OriginProtocolPolicy": "https-only",
|
||||||
|
"OriginReadTimeout": 30,
|
||||||
|
"OriginSslProtocols": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"TLSv1.2",
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"DomainName": "mycustomorigin.com",
|
||||||
|
"Id": "mycustomorigin.com",
|
||||||
|
"OriginPath": "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
"PriceClass": "PriceClass_All",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Input origin as a custom url updates distribution 1`] = `
|
||||||
|
Object {
|
||||||
|
"DistributionConfig": Object {
|
||||||
|
"CacheBehaviors": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"Comment": "",
|
||||||
|
"DefaultCacheBehavior": Object {
|
||||||
|
"AllowedMethods": Object {
|
||||||
|
"CachedMethods": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Compress": false,
|
||||||
|
"DefaultTTL": 86400,
|
||||||
|
"FieldLevelEncryptionId": "",
|
||||||
|
"ForwardedValues": Object {
|
||||||
|
"Cookies": Object {
|
||||||
|
"Forward": "none",
|
||||||
|
},
|
||||||
|
"Headers": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"QueryString": false,
|
||||||
|
"QueryStringCacheKeys": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"LambdaFunctionAssociations": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"MaxTTL": 31536000,
|
||||||
|
"MinTTL": 0,
|
||||||
|
"SmoothStreaming": false,
|
||||||
|
"TargetOriginId": "mycustomoriginupdated.com",
|
||||||
|
"TrustedSigners": Object {
|
||||||
|
"Enabled": false,
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"ViewerProtocolPolicy": "redirect-to-https",
|
||||||
|
},
|
||||||
|
"Enabled": true,
|
||||||
|
"Origins": Object {
|
||||||
|
"Items": Array [
|
||||||
|
Object {
|
||||||
|
"CustomHeaders": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CustomOriginConfig": Object {
|
||||||
|
"HTTPPort": 80,
|
||||||
|
"HTTPSPort": 443,
|
||||||
|
"OriginKeepaliveTimeout": 5,
|
||||||
|
"OriginProtocolPolicy": "https-only",
|
||||||
|
"OriginReadTimeout": 30,
|
||||||
|
"OriginSslProtocols": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"TLSv1.2",
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"DomainName": "mycustomoriginupdated.com",
|
||||||
|
"Id": "mycustomoriginupdated.com",
|
||||||
|
"OriginPath": "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Id": "distribution123",
|
||||||
|
"IfMatch": "etag",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Input origin as a custom url creates distribution with lambda associations for each event type 1`] = `
|
||||||
|
Object {
|
||||||
|
"DistributionConfig": Object {
|
||||||
|
"Aliases": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CacheBehaviors": Object {
|
||||||
|
"Items": Array [
|
||||||
|
Object {
|
||||||
|
"AllowedMethods": Object {
|
||||||
|
"CachedMethods": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"GET",
|
||||||
|
"HEAD",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Items": Array [
|
||||||
|
"GET",
|
||||||
|
"HEAD",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Compress": true,
|
||||||
|
"DefaultTTL": 10,
|
||||||
|
"FieldLevelEncryptionId": "",
|
||||||
|
"ForwardedValues": Object {
|
||||||
|
"Cookies": Object {
|
||||||
|
"Forward": "all",
|
||||||
|
},
|
||||||
|
"Headers": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"QueryString": true,
|
||||||
|
"QueryStringCacheKeys": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"LambdaFunctionAssociations": Object {
|
||||||
|
"Items": Array [
|
||||||
|
Object {
|
||||||
|
"EventType": "viewer-request",
|
||||||
|
"IncludeBody": true,
|
||||||
|
"LambdaFunctionARN": "arn:aws:lambda:us-east-1:123:function:viewerRequestFunction",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"EventType": "origin-request",
|
||||||
|
"IncludeBody": true,
|
||||||
|
"LambdaFunctionARN": "arn:aws:lambda:us-east-1:123:function:originRequestFunction",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"EventType": "origin-response",
|
||||||
|
"IncludeBody": true,
|
||||||
|
"LambdaFunctionARN": "arn:aws:lambda:us-east-1:123:function:originResponseFunction",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"EventType": "viewer-response",
|
||||||
|
"IncludeBody": true,
|
||||||
|
"LambdaFunctionARN": "arn:aws:lambda:us-east-1:123:function:viewerResponseFunction",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Quantity": 4,
|
||||||
|
},
|
||||||
|
"MaxTTL": 10,
|
||||||
|
"MinTTL": 10,
|
||||||
|
"PathPattern": "/some/path",
|
||||||
|
"SmoothStreaming": false,
|
||||||
|
"TargetOriginId": "exampleorigin.com",
|
||||||
|
"TrustedSigners": Object {
|
||||||
|
"Enabled": false,
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"ViewerProtocolPolicy": "https-only",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
"CallerReference": "1566599541192",
|
||||||
|
"Comment": "",
|
||||||
|
"DefaultCacheBehavior": Object {
|
||||||
|
"AllowedMethods": Object {
|
||||||
|
"CachedMethods": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Compress": false,
|
||||||
|
"DefaultTTL": 86400,
|
||||||
|
"FieldLevelEncryptionId": "",
|
||||||
|
"ForwardedValues": Object {
|
||||||
|
"Cookies": Object {
|
||||||
|
"Forward": "none",
|
||||||
|
},
|
||||||
|
"Headers": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"QueryString": false,
|
||||||
|
"QueryStringCacheKeys": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"LambdaFunctionAssociations": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"MaxTTL": 31536000,
|
||||||
|
"MinTTL": 0,
|
||||||
|
"SmoothStreaming": false,
|
||||||
|
"TargetOriginId": "exampleorigin.com",
|
||||||
|
"TrustedSigners": Object {
|
||||||
|
"Enabled": false,
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"ViewerProtocolPolicy": "redirect-to-https",
|
||||||
|
},
|
||||||
|
"Enabled": true,
|
||||||
|
"HttpVersion": "http2",
|
||||||
|
"Origins": Object {
|
||||||
|
"Items": Array [
|
||||||
|
Object {
|
||||||
|
"CustomHeaders": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CustomOriginConfig": Object {
|
||||||
|
"HTTPPort": 80,
|
||||||
|
"HTTPSPort": 443,
|
||||||
|
"OriginKeepaliveTimeout": 5,
|
||||||
|
"OriginProtocolPolicy": "https-only",
|
||||||
|
"OriginReadTimeout": 30,
|
||||||
|
"OriginSslProtocols": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"TLSv1.2",
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"DomainName": "exampleorigin.com",
|
||||||
|
"Id": "exampleorigin.com",
|
||||||
|
"OriginPath": "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
"PriceClass": "PriceClass_All",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
@ -0,0 +1,281 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Input origin with path pattern creates distribution with custom url origin 1`] = `
|
||||||
|
Object {
|
||||||
|
"DistributionConfig": Object {
|
||||||
|
"Aliases": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CacheBehaviors": Object {
|
||||||
|
"Items": Array [
|
||||||
|
Object {
|
||||||
|
"AllowedMethods": Object {
|
||||||
|
"CachedMethods": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"GET",
|
||||||
|
"HEAD",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Items": Array [
|
||||||
|
"GET",
|
||||||
|
"HEAD",
|
||||||
|
"POST",
|
||||||
|
],
|
||||||
|
"Quantity": 3,
|
||||||
|
},
|
||||||
|
"Compress": true,
|
||||||
|
"DefaultTTL": 10,
|
||||||
|
"FieldLevelEncryptionId": "",
|
||||||
|
"ForwardedValues": Object {
|
||||||
|
"Cookies": Object {
|
||||||
|
"Forward": "all",
|
||||||
|
},
|
||||||
|
"Headers": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"QueryString": true,
|
||||||
|
"QueryStringCacheKeys": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"LambdaFunctionAssociations": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"MaxTTL": 10,
|
||||||
|
"MinTTL": 10,
|
||||||
|
"PathPattern": "/some/path",
|
||||||
|
"SmoothStreaming": false,
|
||||||
|
"TargetOriginId": "exampleorigin.com",
|
||||||
|
"TrustedSigners": Object {
|
||||||
|
"Enabled": false,
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"ViewerProtocolPolicy": "https-only",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
"CallerReference": "1566599541192",
|
||||||
|
"Comment": "",
|
||||||
|
"DefaultCacheBehavior": Object {
|
||||||
|
"AllowedMethods": Object {
|
||||||
|
"CachedMethods": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Compress": false,
|
||||||
|
"DefaultTTL": 86400,
|
||||||
|
"FieldLevelEncryptionId": "",
|
||||||
|
"ForwardedValues": Object {
|
||||||
|
"Cookies": Object {
|
||||||
|
"Forward": "none",
|
||||||
|
},
|
||||||
|
"Headers": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"QueryString": false,
|
||||||
|
"QueryStringCacheKeys": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"LambdaFunctionAssociations": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"MaxTTL": 31536000,
|
||||||
|
"MinTTL": 0,
|
||||||
|
"SmoothStreaming": false,
|
||||||
|
"TargetOriginId": "exampleorigin.com",
|
||||||
|
"TrustedSigners": Object {
|
||||||
|
"Enabled": false,
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"ViewerProtocolPolicy": "redirect-to-https",
|
||||||
|
},
|
||||||
|
"Enabled": true,
|
||||||
|
"HttpVersion": "http2",
|
||||||
|
"Origins": Object {
|
||||||
|
"Items": Array [
|
||||||
|
Object {
|
||||||
|
"CustomHeaders": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CustomOriginConfig": Object {
|
||||||
|
"HTTPPort": 80,
|
||||||
|
"HTTPSPort": 443,
|
||||||
|
"OriginKeepaliveTimeout": 5,
|
||||||
|
"OriginProtocolPolicy": "https-only",
|
||||||
|
"OriginReadTimeout": 30,
|
||||||
|
"OriginSslProtocols": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"TLSv1.2",
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"DomainName": "exampleorigin.com",
|
||||||
|
"Id": "exampleorigin.com",
|
||||||
|
"OriginPath": "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
"PriceClass": "PriceClass_All",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Input origin with path pattern updates distribution 1`] = `
|
||||||
|
Object {
|
||||||
|
"DistributionConfig": Object {
|
||||||
|
"CacheBehaviors": Object {
|
||||||
|
"Items": Array [
|
||||||
|
Object {
|
||||||
|
"AllowedMethods": Object {
|
||||||
|
"CachedMethods": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"GET",
|
||||||
|
"HEAD",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Items": Array [
|
||||||
|
"GET",
|
||||||
|
"HEAD",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Compress": true,
|
||||||
|
"DefaultTTL": 10,
|
||||||
|
"FieldLevelEncryptionId": "",
|
||||||
|
"ForwardedValues": Object {
|
||||||
|
"Cookies": Object {
|
||||||
|
"Forward": "all",
|
||||||
|
},
|
||||||
|
"Headers": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"QueryString": true,
|
||||||
|
"QueryStringCacheKeys": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"LambdaFunctionAssociations": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"MaxTTL": 10,
|
||||||
|
"MinTTL": 10,
|
||||||
|
"PathPattern": "/some/other/path",
|
||||||
|
"SmoothStreaming": false,
|
||||||
|
"TargetOriginId": "exampleorigin.com",
|
||||||
|
"TrustedSigners": Object {
|
||||||
|
"Enabled": false,
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"ViewerProtocolPolicy": "https-only",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
"Comment": "",
|
||||||
|
"DefaultCacheBehavior": Object {
|
||||||
|
"AllowedMethods": Object {
|
||||||
|
"CachedMethods": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Compress": false,
|
||||||
|
"DefaultTTL": 86400,
|
||||||
|
"FieldLevelEncryptionId": "",
|
||||||
|
"ForwardedValues": Object {
|
||||||
|
"Cookies": Object {
|
||||||
|
"Forward": "none",
|
||||||
|
},
|
||||||
|
"Headers": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"QueryString": false,
|
||||||
|
"QueryStringCacheKeys": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"LambdaFunctionAssociations": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"MaxTTL": 31536000,
|
||||||
|
"MinTTL": 0,
|
||||||
|
"SmoothStreaming": false,
|
||||||
|
"TargetOriginId": "exampleorigin.com",
|
||||||
|
"TrustedSigners": Object {
|
||||||
|
"Enabled": false,
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"ViewerProtocolPolicy": "redirect-to-https",
|
||||||
|
},
|
||||||
|
"Enabled": true,
|
||||||
|
"Origins": Object {
|
||||||
|
"Items": Array [
|
||||||
|
Object {
|
||||||
|
"CustomHeaders": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CustomOriginConfig": Object {
|
||||||
|
"HTTPPort": 80,
|
||||||
|
"HTTPSPort": 443,
|
||||||
|
"OriginKeepaliveTimeout": 5,
|
||||||
|
"OriginProtocolPolicy": "https-only",
|
||||||
|
"OriginReadTimeout": 30,
|
||||||
|
"OriginSslProtocols": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"TLSv1.2",
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"DomainName": "exampleorigin.com",
|
||||||
|
"Id": "exampleorigin.com",
|
||||||
|
"OriginPath": "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Id": "xyz",
|
||||||
|
"IfMatch": "etag",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
@ -0,0 +1,332 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`S3 origins When origin is an S3 URL only accessible via CloudFront creates distribution 1`] = `
|
||||||
|
Object {
|
||||||
|
"DistributionConfig": Object {
|
||||||
|
"Aliases": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CacheBehaviors": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CallerReference": "1566599541192",
|
||||||
|
"Comment": "",
|
||||||
|
"DefaultCacheBehavior": Object {
|
||||||
|
"AllowedMethods": Object {
|
||||||
|
"CachedMethods": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Compress": false,
|
||||||
|
"DefaultTTL": 86400,
|
||||||
|
"FieldLevelEncryptionId": "",
|
||||||
|
"ForwardedValues": Object {
|
||||||
|
"Cookies": Object {
|
||||||
|
"Forward": "none",
|
||||||
|
},
|
||||||
|
"Headers": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"QueryString": false,
|
||||||
|
"QueryStringCacheKeys": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"LambdaFunctionAssociations": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"MaxTTL": 31536000,
|
||||||
|
"MinTTL": 0,
|
||||||
|
"SmoothStreaming": false,
|
||||||
|
"TargetOriginId": "mybucket",
|
||||||
|
"TrustedSigners": Object {
|
||||||
|
"Enabled": false,
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"ViewerProtocolPolicy": "redirect-to-https",
|
||||||
|
},
|
||||||
|
"Enabled": true,
|
||||||
|
"HttpVersion": "http2",
|
||||||
|
"Origins": Object {
|
||||||
|
"Items": Array [
|
||||||
|
Object {
|
||||||
|
"CustomHeaders": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"DomainName": "mybucket.s3.amazonaws.com",
|
||||||
|
"Id": "mybucket",
|
||||||
|
"OriginPath": "",
|
||||||
|
"S3OriginConfig": Object {
|
||||||
|
"OriginAccessIdentity": "origin-access-identity/cloudfront/access-identity-xyz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
"PriceClass": "PriceClass_All",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`S3 origins When origin is an S3 URL only accessible via CloudFront updates distribution 1`] = `
|
||||||
|
Object {
|
||||||
|
"DistributionConfig": Object {
|
||||||
|
"Aliases": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CacheBehaviors": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CallerReference": "1566599541192",
|
||||||
|
"Comment": "",
|
||||||
|
"DefaultCacheBehavior": Object {
|
||||||
|
"AllowedMethods": Object {
|
||||||
|
"CachedMethods": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Compress": false,
|
||||||
|
"DefaultTTL": 86400,
|
||||||
|
"FieldLevelEncryptionId": "",
|
||||||
|
"ForwardedValues": Object {
|
||||||
|
"Cookies": Object {
|
||||||
|
"Forward": "none",
|
||||||
|
},
|
||||||
|
"Headers": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"QueryString": false,
|
||||||
|
"QueryStringCacheKeys": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"LambdaFunctionAssociations": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"MaxTTL": 31536000,
|
||||||
|
"MinTTL": 0,
|
||||||
|
"SmoothStreaming": false,
|
||||||
|
"TargetOriginId": "mybucket",
|
||||||
|
"TrustedSigners": Object {
|
||||||
|
"Enabled": false,
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"ViewerProtocolPolicy": "redirect-to-https",
|
||||||
|
},
|
||||||
|
"Enabled": true,
|
||||||
|
"HttpVersion": "http2",
|
||||||
|
"Origins": Object {
|
||||||
|
"Items": Array [
|
||||||
|
Object {
|
||||||
|
"CustomHeaders": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"DomainName": "mybucket.s3.amazonaws.com",
|
||||||
|
"Id": "mybucket",
|
||||||
|
"OriginPath": "",
|
||||||
|
"S3OriginConfig": Object {
|
||||||
|
"OriginAccessIdentity": "origin-access-identity/cloudfront/access-identity-xyz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
"PriceClass": "PriceClass_All",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`S3 origins When origin is an S3 bucket URL creates distribution 1`] = `
|
||||||
|
Object {
|
||||||
|
"DistributionConfig": Object {
|
||||||
|
"Aliases": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CacheBehaviors": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"CallerReference": "1566599541192",
|
||||||
|
"Comment": "",
|
||||||
|
"DefaultCacheBehavior": Object {
|
||||||
|
"AllowedMethods": Object {
|
||||||
|
"CachedMethods": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Compress": false,
|
||||||
|
"DefaultTTL": 86400,
|
||||||
|
"FieldLevelEncryptionId": "",
|
||||||
|
"ForwardedValues": Object {
|
||||||
|
"Cookies": Object {
|
||||||
|
"Forward": "none",
|
||||||
|
},
|
||||||
|
"Headers": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"QueryString": false,
|
||||||
|
"QueryStringCacheKeys": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"LambdaFunctionAssociations": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"MaxTTL": 31536000,
|
||||||
|
"MinTTL": 0,
|
||||||
|
"SmoothStreaming": false,
|
||||||
|
"TargetOriginId": "mybucket",
|
||||||
|
"TrustedSigners": Object {
|
||||||
|
"Enabled": false,
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"ViewerProtocolPolicy": "redirect-to-https",
|
||||||
|
},
|
||||||
|
"Enabled": true,
|
||||||
|
"HttpVersion": "http2",
|
||||||
|
"Origins": Object {
|
||||||
|
"Items": Array [
|
||||||
|
Object {
|
||||||
|
"CustomHeaders": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"DomainName": "mybucket.s3.amazonaws.com",
|
||||||
|
"Id": "mybucket",
|
||||||
|
"OriginPath": "",
|
||||||
|
"S3OriginConfig": Object {
|
||||||
|
"OriginAccessIdentity": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
"PriceClass": "PriceClass_All",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`S3 origins When origin is an S3 bucket URL updates distribution 1`] = `
|
||||||
|
Object {
|
||||||
|
"DistributionConfig": Object {
|
||||||
|
"CacheBehaviors": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"Comment": "",
|
||||||
|
"DefaultCacheBehavior": Object {
|
||||||
|
"AllowedMethods": Object {
|
||||||
|
"CachedMethods": Object {
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Items": Array [
|
||||||
|
"HEAD",
|
||||||
|
"GET",
|
||||||
|
],
|
||||||
|
"Quantity": 2,
|
||||||
|
},
|
||||||
|
"Compress": false,
|
||||||
|
"DefaultTTL": 86400,
|
||||||
|
"FieldLevelEncryptionId": "",
|
||||||
|
"ForwardedValues": Object {
|
||||||
|
"Cookies": Object {
|
||||||
|
"Forward": "none",
|
||||||
|
},
|
||||||
|
"Headers": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"QueryString": false,
|
||||||
|
"QueryStringCacheKeys": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"LambdaFunctionAssociations": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"MaxTTL": 31536000,
|
||||||
|
"MinTTL": 0,
|
||||||
|
"SmoothStreaming": false,
|
||||||
|
"TargetOriginId": "anotherbucket",
|
||||||
|
"TrustedSigners": Object {
|
||||||
|
"Enabled": false,
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"ViewerProtocolPolicy": "redirect-to-https",
|
||||||
|
},
|
||||||
|
"Enabled": true,
|
||||||
|
"Origins": Object {
|
||||||
|
"Items": Array [
|
||||||
|
Object {
|
||||||
|
"CustomHeaders": Object {
|
||||||
|
"Items": Array [],
|
||||||
|
"Quantity": 0,
|
||||||
|
},
|
||||||
|
"DomainName": "anotherbucket.s3.amazonaws.com",
|
||||||
|
"Id": "anotherbucket",
|
||||||
|
"OriginPath": "",
|
||||||
|
"S3OriginConfig": Object {
|
||||||
|
"OriginAccessIdentity": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Quantity": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Id": "distributionwithS3origin",
|
||||||
|
"IfMatch": "etag",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
const { createComponent } = require('../test-utils');
|
||||||
|
|
||||||
|
const { mockCreateDistribution, mockCreateDistributionPromise } = require('aws-sdk');
|
||||||
|
|
||||||
|
jest.mock('aws-sdk', () => require('../__mocks__/aws-sdk.mock'));
|
||||||
|
|
||||||
|
describe('Input origin as a custom url', () => {
|
||||||
|
let component;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockCreateDistributionPromise.mockResolvedValueOnce({
|
||||||
|
Distribution: {
|
||||||
|
Id: 'distribution123',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
component = await createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates distribution with custom default behavior options', async () => {
|
||||||
|
await component.default({
|
||||||
|
defaults: {
|
||||||
|
ttl: 0,
|
||||||
|
forward: {
|
||||||
|
headers: ['Accept', 'Accept-Language'],
|
||||||
|
cookies: 'all',
|
||||||
|
queryString: true,
|
||||||
|
queryStringCacheKeys: ['query'],
|
||||||
|
},
|
||||||
|
allowedHttpMethods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'POST', 'PATCH', 'DELETE'],
|
||||||
|
viewerProtocolPolicy: 'https-only',
|
||||||
|
smoothStreaming: true,
|
||||||
|
compress: true,
|
||||||
|
fieldLevelEncryptionId: '123',
|
||||||
|
},
|
||||||
|
origins: ['https://mycustomorigin.com'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCreateDistribution.mock.calls[0][0]).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates distribution with custom behavior options', async () => {
|
||||||
|
await component.default({
|
||||||
|
defaults: {
|
||||||
|
ttl: 0,
|
||||||
|
},
|
||||||
|
origins: [
|
||||||
|
{
|
||||||
|
url: 'https://mycustomorigin.com',
|
||||||
|
pathPatterns: {
|
||||||
|
'/sample/path': {
|
||||||
|
ttl: 0,
|
||||||
|
forward: {
|
||||||
|
headers: 'all',
|
||||||
|
cookies: ['auth-token'],
|
||||||
|
queryString: true,
|
||||||
|
},
|
||||||
|
allowedHttpMethods: ['GET', 'HEAD'],
|
||||||
|
viewerProtocolPolicy: 'redirect-to-https',
|
||||||
|
compress: false,
|
||||||
|
fieldLevelEncryptionId: '321',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCreateDistribution.mock.calls[0][0]).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
106
packages/aws-cloudfront/__tests__/custom-url-origin.test.js
Normal file
106
packages/aws-cloudfront/__tests__/custom-url-origin.test.js
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
const { createComponent, assertHasOrigin } = require('../test-utils');
|
||||||
|
|
||||||
|
const {
|
||||||
|
mockCreateDistribution,
|
||||||
|
mockUpdateDistribution,
|
||||||
|
mockCreateDistributionPromise,
|
||||||
|
mockGetDistributionConfigPromise,
|
||||||
|
mockUpdateDistributionPromise,
|
||||||
|
} = require('aws-sdk');
|
||||||
|
|
||||||
|
jest.mock('aws-sdk', () => require('../__mocks__/aws-sdk.mock'));
|
||||||
|
|
||||||
|
describe('Input origin as a custom url', () => {
|
||||||
|
let component;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockCreateDistributionPromise.mockResolvedValueOnce({
|
||||||
|
Distribution: {
|
||||||
|
Id: 'distribution123',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
component = await createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates distribution with custom url origin and sets defaults', async () => {
|
||||||
|
await component.default({
|
||||||
|
defaults: {
|
||||||
|
allowedHttpMethods: ['HEAD', 'DELETE', 'POST', 'GET', 'OPTIONS', 'PUT', 'PATCH'],
|
||||||
|
ttl: 10,
|
||||||
|
'lambda@edge': {
|
||||||
|
'origin-request': 'arn:aws:lambda:us-east-1:123:function:originRequestFunction',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
origins: ['https://mycustomorigin.com'],
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHasOrigin(mockCreateDistribution, {
|
||||||
|
Id: 'mycustomorigin.com',
|
||||||
|
DomainName: 'mycustomorigin.com',
|
||||||
|
CustomOriginConfig: {
|
||||||
|
HTTPPort: 80,
|
||||||
|
HTTPSPort: 443,
|
||||||
|
OriginProtocolPolicy: 'https-only',
|
||||||
|
OriginSslProtocols: {
|
||||||
|
Quantity: 1,
|
||||||
|
Items: ['TLSv1.2'],
|
||||||
|
},
|
||||||
|
OriginReadTimeout: 30,
|
||||||
|
OriginKeepaliveTimeout: 5,
|
||||||
|
},
|
||||||
|
CustomHeaders: {
|
||||||
|
Quantity: 0,
|
||||||
|
Items: [],
|
||||||
|
},
|
||||||
|
OriginPath: '',
|
||||||
|
});
|
||||||
|
expect(mockCreateDistribution.mock.calls[0][0]).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates distribution', async () => {
|
||||||
|
mockGetDistributionConfigPromise.mockResolvedValueOnce({
|
||||||
|
ETag: 'etag',
|
||||||
|
DistributionConfig: {
|
||||||
|
Origins: {
|
||||||
|
Items: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mockUpdateDistributionPromise.mockResolvedValueOnce({
|
||||||
|
Distribution: {
|
||||||
|
Id: 'xyz',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await component.default({
|
||||||
|
origins: ['https://mycustomorigin.com'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await component.default({
|
||||||
|
origins: ['https://mycustomoriginupdated.com'],
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHasOrigin(mockUpdateDistribution, {
|
||||||
|
Id: 'mycustomoriginupdated.com',
|
||||||
|
DomainName: 'mycustomoriginupdated.com',
|
||||||
|
CustomOriginConfig: {
|
||||||
|
HTTPPort: 80,
|
||||||
|
HTTPSPort: 443,
|
||||||
|
OriginProtocolPolicy: 'https-only',
|
||||||
|
OriginSslProtocols: {
|
||||||
|
Quantity: 1,
|
||||||
|
Items: ['TLSv1.2'],
|
||||||
|
},
|
||||||
|
OriginReadTimeout: 30,
|
||||||
|
OriginKeepaliveTimeout: 5,
|
||||||
|
},
|
||||||
|
CustomHeaders: {
|
||||||
|
Quantity: 0,
|
||||||
|
Items: [],
|
||||||
|
},
|
||||||
|
OriginPath: '',
|
||||||
|
});
|
||||||
|
expect(mockUpdateDistribution.mock.calls[0][0]).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
98
packages/aws-cloudfront/__tests__/general-options.test.js
Normal file
98
packages/aws-cloudfront/__tests__/general-options.test.js
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
const { createComponent } = require('../test-utils');
|
||||||
|
|
||||||
|
const {
|
||||||
|
mockCreateDistribution,
|
||||||
|
mockUpdateDistribution,
|
||||||
|
mockCreateDistributionPromise,
|
||||||
|
mockGetDistributionConfigPromise,
|
||||||
|
mockUpdateDistributionPromise,
|
||||||
|
} = require('aws-sdk');
|
||||||
|
|
||||||
|
jest.mock('aws-sdk', () => require('../__mocks__/aws-sdk.mock'));
|
||||||
|
|
||||||
|
describe('General options propagation', () => {
|
||||||
|
let component;
|
||||||
|
|
||||||
|
// sample origins
|
||||||
|
const origins = ['https://exampleorigin.com'];
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockCreateDistributionPromise.mockResolvedValueOnce({
|
||||||
|
Distribution: {
|
||||||
|
Id: 'distribution123',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mockGetDistributionConfigPromise.mockResolvedValueOnce({
|
||||||
|
ETag: 'etag',
|
||||||
|
DistributionConfig: {
|
||||||
|
Origins: {
|
||||||
|
Items: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mockUpdateDistributionPromise.mockResolvedValueOnce({
|
||||||
|
Distribution: {
|
||||||
|
Id: 'xyz',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
component = await createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create distribution with comment and update it', async () => {
|
||||||
|
await component.default({
|
||||||
|
comment: 'test comment',
|
||||||
|
origins,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCreateDistribution).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
DistributionConfig: expect.objectContaining({
|
||||||
|
Comment: 'test comment',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await component.default({
|
||||||
|
comment: 'updated comment',
|
||||||
|
origins,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockUpdateDistribution).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
DistributionConfig: expect.objectContaining({
|
||||||
|
Comment: 'updated comment',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create disabled distribution and update it', async () => {
|
||||||
|
await component.default({
|
||||||
|
enabled: false,
|
||||||
|
origins,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCreateDistribution).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
DistributionConfig: expect.objectContaining({
|
||||||
|
Enabled: false,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await component.default({
|
||||||
|
enabled: true,
|
||||||
|
origins,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockUpdateDistribution).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
DistributionConfig: expect.objectContaining({
|
||||||
|
Enabled: true,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
98
packages/aws-cloudfront/__tests__/lambda-at-edge.test.js
Normal file
98
packages/aws-cloudfront/__tests__/lambda-at-edge.test.js
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
const { createComponent, assertHasCacheBehavior } = require('../test-utils');
|
||||||
|
|
||||||
|
const { mockCreateDistribution, mockCreateDistributionPromise } = require('aws-sdk');
|
||||||
|
|
||||||
|
jest.mock('aws-sdk', () => require('../__mocks__/aws-sdk.mock'));
|
||||||
|
|
||||||
|
describe('Input origin as a custom url', () => {
|
||||||
|
let component;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockCreateDistributionPromise.mockResolvedValueOnce({
|
||||||
|
Distribution: {
|
||||||
|
Id: 'distribution123',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
component = await createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates distribution with lambda associations for each event type', async () => {
|
||||||
|
await component.default({
|
||||||
|
origins: [
|
||||||
|
{
|
||||||
|
url: 'https://exampleorigin.com',
|
||||||
|
pathPatterns: {
|
||||||
|
'/some/path': {
|
||||||
|
ttl: 10,
|
||||||
|
'lambda@edge': {
|
||||||
|
'viewer-request': 'arn:aws:lambda:us-east-1:123:function:viewerRequestFunction',
|
||||||
|
'origin-request': 'arn:aws:lambda:us-east-1:123:function:originRequestFunction',
|
||||||
|
'origin-response': 'arn:aws:lambda:us-east-1:123:function:originResponseFunction',
|
||||||
|
'viewer-response': 'arn:aws:lambda:us-east-1:123:function:viewerResponseFunction',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHasCacheBehavior(mockCreateDistribution, {
|
||||||
|
PathPattern: '/some/path',
|
||||||
|
LambdaFunctionAssociations: {
|
||||||
|
Quantity: 4,
|
||||||
|
Items: [
|
||||||
|
{
|
||||||
|
EventType: 'viewer-request',
|
||||||
|
LambdaFunctionARN: 'arn:aws:lambda:us-east-1:123:function:viewerRequestFunction',
|
||||||
|
IncludeBody: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EventType: 'origin-request',
|
||||||
|
LambdaFunctionARN: 'arn:aws:lambda:us-east-1:123:function:originRequestFunction',
|
||||||
|
IncludeBody: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EventType: 'origin-response',
|
||||||
|
LambdaFunctionARN: 'arn:aws:lambda:us-east-1:123:function:originResponseFunction',
|
||||||
|
IncludeBody: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EventType: 'viewer-response',
|
||||||
|
LambdaFunctionARN: 'arn:aws:lambda:us-east-1:123:function:viewerResponseFunction',
|
||||||
|
IncludeBody: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCreateDistribution.mock.calls[0][0]).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error when event type provided is not valid', async () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await component.default({
|
||||||
|
origins: [
|
||||||
|
{
|
||||||
|
url: 'https://exampleorigin.com',
|
||||||
|
pathPatterns: {
|
||||||
|
'/some/path': {
|
||||||
|
ttl: 10,
|
||||||
|
'lambda@edge': {
|
||||||
|
'invalid-eventtype':
|
||||||
|
'arn:aws:lambda:us-east-1:123:function:viewerRequestFunction',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).toEqual(
|
||||||
|
'"invalid-eventtype" is not a valid lambda trigger. See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-cloudfront-trigger-events.html for valid event types.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
const { createComponent, assertHasCacheBehavior, assertHasOrigin } = require('../test-utils');
|
||||||
|
|
||||||
|
const {
|
||||||
|
mockCreateDistribution,
|
||||||
|
mockUpdateDistribution,
|
||||||
|
mockCreateDistributionPromise,
|
||||||
|
mockGetDistributionConfigPromise,
|
||||||
|
mockUpdateDistributionPromise,
|
||||||
|
} = require('aws-sdk');
|
||||||
|
|
||||||
|
jest.mock('aws-sdk', () => require('../__mocks__/aws-sdk.mock'));
|
||||||
|
|
||||||
|
describe('Input origin with path pattern', () => {
|
||||||
|
let component;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockCreateDistributionPromise.mockResolvedValueOnce({
|
||||||
|
Distribution: {
|
||||||
|
Id: 'xyz',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
component = await createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates distribution with custom url origin', async () => {
|
||||||
|
await component.default({
|
||||||
|
origins: [
|
||||||
|
{
|
||||||
|
url: 'https://exampleorigin.com',
|
||||||
|
pathPatterns: {
|
||||||
|
'/some/path': {
|
||||||
|
ttl: 10,
|
||||||
|
allowedHttpMethods: ['GET', 'HEAD', 'POST'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHasOrigin(mockCreateDistribution, {
|
||||||
|
Id: 'exampleorigin.com',
|
||||||
|
DomainName: 'exampleorigin.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHasCacheBehavior(mockCreateDistribution, {
|
||||||
|
PathPattern: '/some/path',
|
||||||
|
MinTTL: 10,
|
||||||
|
TargetOriginId: 'exampleorigin.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCreateDistribution.mock.calls[0][0]).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates distribution', async () => {
|
||||||
|
mockGetDistributionConfigPromise.mockResolvedValueOnce({
|
||||||
|
ETag: 'etag',
|
||||||
|
DistributionConfig: {
|
||||||
|
Origins: {
|
||||||
|
Items: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mockUpdateDistributionPromise.mockResolvedValueOnce({
|
||||||
|
Distribution: {
|
||||||
|
Id: 'xyz',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await component.default({
|
||||||
|
origins: [
|
||||||
|
{
|
||||||
|
url: 'https://exampleorigin.com',
|
||||||
|
pathPatterns: {
|
||||||
|
'/some/path': {
|
||||||
|
ttl: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await component.default({
|
||||||
|
origins: [
|
||||||
|
{
|
||||||
|
url: 'https://exampleorigin.com',
|
||||||
|
pathPatterns: {
|
||||||
|
'/some/other/path': {
|
||||||
|
ttl: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHasOrigin(mockUpdateDistribution, {
|
||||||
|
Id: 'exampleorigin.com',
|
||||||
|
DomainName: 'exampleorigin.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHasCacheBehavior(mockUpdateDistribution, {
|
||||||
|
PathPattern: '/some/other/path',
|
||||||
|
MinTTL: 10,
|
||||||
|
TargetOriginId: 'exampleorigin.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockUpdateDistribution.mock.calls[0][0]).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
180
packages/aws-cloudfront/__tests__/s3-origin.test.js
Normal file
180
packages/aws-cloudfront/__tests__/s3-origin.test.js
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
const {
|
||||||
|
mockCreateDistribution,
|
||||||
|
mockUpdateDistribution,
|
||||||
|
mockCreateDistributionPromise,
|
||||||
|
mockGetDistributionConfigPromise,
|
||||||
|
mockUpdateDistributionPromise,
|
||||||
|
mockCreateCloudFrontOriginAccessIdentityPromise,
|
||||||
|
mockPutBucketPolicy,
|
||||||
|
} = require('aws-sdk');
|
||||||
|
|
||||||
|
const { createComponent, assertHasOrigin } = require('../test-utils');
|
||||||
|
|
||||||
|
jest.mock('aws-sdk', () => require('../__mocks__/aws-sdk.mock'));
|
||||||
|
|
||||||
|
describe('S3 origins', () => {
|
||||||
|
let component;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockCreateDistributionPromise.mockResolvedValueOnce({
|
||||||
|
Distribution: {
|
||||||
|
Id: 'distributionwithS3origin',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
component = await createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When origin is an S3 bucket URL', () => {
|
||||||
|
it('creates distribution', async () => {
|
||||||
|
await component.default({
|
||||||
|
origins: ['https://mybucket.s3.amazonaws.com'],
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHasOrigin(mockCreateDistribution, {
|
||||||
|
Id: 'mybucket',
|
||||||
|
DomainName: 'mybucket.s3.amazonaws.com',
|
||||||
|
S3OriginConfig: {
|
||||||
|
OriginAccessIdentity: '',
|
||||||
|
},
|
||||||
|
CustomHeaders: {
|
||||||
|
Quantity: 0,
|
||||||
|
Items: [],
|
||||||
|
},
|
||||||
|
OriginPath: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCreateDistribution.mock.calls[0][0]).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates distribution', async () => {
|
||||||
|
mockGetDistributionConfigPromise.mockResolvedValueOnce({
|
||||||
|
ETag: 'etag',
|
||||||
|
DistributionConfig: {
|
||||||
|
Origins: {
|
||||||
|
Items: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mockUpdateDistributionPromise.mockResolvedValueOnce({
|
||||||
|
Distribution: {
|
||||||
|
Id: 'distributionwithS3originupdated',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await component.default({
|
||||||
|
origins: ['https://mybucket.s3.amazonaws.com'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await component.default({
|
||||||
|
origins: ['https://anotherbucket.s3.amazonaws.com'],
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHasOrigin(mockUpdateDistribution, {
|
||||||
|
Id: 'anotherbucket',
|
||||||
|
DomainName: 'anotherbucket.s3.amazonaws.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockUpdateDistribution.mock.calls[0][0]).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When origin is an S3 URL only accessible via CloudFront', () => {
|
||||||
|
it('creates distribution', async () => {
|
||||||
|
mockCreateCloudFrontOriginAccessIdentityPromise.mockResolvedValueOnce({
|
||||||
|
CloudFrontOriginAccessIdentity: {
|
||||||
|
Id: 'access-identity-xyz',
|
||||||
|
S3CanonicalUserId: 's3-canonical-user-id-xyz',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await component.default({
|
||||||
|
origins: [
|
||||||
|
{
|
||||||
|
url: 'https://mybucket.s3.amazonaws.com',
|
||||||
|
private: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockPutBucketPolicy).toBeCalledWith({
|
||||||
|
Bucket: 'mybucket',
|
||||||
|
Policy: expect.stringContaining('"CanonicalUser":"s3-canonical-user-id-xyz"'),
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHasOrigin(mockCreateDistribution, {
|
||||||
|
Id: 'mybucket',
|
||||||
|
DomainName: 'mybucket.s3.amazonaws.com',
|
||||||
|
S3OriginConfig: {
|
||||||
|
OriginAccessIdentity: 'origin-access-identity/cloudfront/access-identity-xyz',
|
||||||
|
},
|
||||||
|
CustomHeaders: {
|
||||||
|
Quantity: 0,
|
||||||
|
Items: [],
|
||||||
|
},
|
||||||
|
OriginPath: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCreateDistribution.mock.calls[0][0]).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates distribution', async () => {
|
||||||
|
mockCreateCloudFrontOriginAccessIdentityPromise.mockResolvedValue({
|
||||||
|
CloudFrontOriginAccessIdentity: {
|
||||||
|
Id: 'access-identity-xyz',
|
||||||
|
S3CanonicalUserId: 's3-canonical-user-id-xyz',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mockGetDistributionConfigPromise.mockResolvedValueOnce({
|
||||||
|
ETag: 'etag',
|
||||||
|
DistributionConfig: {
|
||||||
|
Origins: {
|
||||||
|
Items: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mockUpdateDistributionPromise.mockResolvedValueOnce({
|
||||||
|
Distribution: {
|
||||||
|
Id: 'distributionwithS3originupdated',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await component.default({
|
||||||
|
origins: [
|
||||||
|
{
|
||||||
|
url: 'https://mybucket.s3.amazonaws.com',
|
||||||
|
private: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await component.default({
|
||||||
|
origins: [
|
||||||
|
{
|
||||||
|
url: 'https://anotherbucket.s3.amazonaws.com',
|
||||||
|
private: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockPutBucketPolicy).toBeCalledWith({
|
||||||
|
Bucket: 'anotherbucket',
|
||||||
|
Policy: expect.stringContaining('"CanonicalUser":"s3-canonical-user-id-xyz"'),
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHasOrigin(mockUpdateDistribution, {
|
||||||
|
Id: 'anotherbucket',
|
||||||
|
DomainName: 'anotherbucket.s3.amazonaws.com',
|
||||||
|
S3OriginConfig: {
|
||||||
|
OriginAccessIdentity: 'origin-access-identity/cloudfront/access-identity-xyz',
|
||||||
|
},
|
||||||
|
OriginPath: '',
|
||||||
|
CustomHeaders: { Items: [], Quantity: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCreateDistribution.mock.calls[0][0]).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
const validLambdaTriggers = [
|
||||||
|
'viewer-request',
|
||||||
|
'origin-request',
|
||||||
|
'origin-response',
|
||||||
|
'viewer-response',
|
||||||
|
];
|
||||||
|
|
||||||
|
// adds lambda@edge to cache behavior passed
|
||||||
|
module.exports = (cacheBehavior, lambdaAtEdgeConfig = {}) => {
|
||||||
|
Object.keys(lambdaAtEdgeConfig).forEach((eventType) => {
|
||||||
|
if (!validLambdaTriggers.includes(eventType)) {
|
||||||
|
throw new Error(
|
||||||
|
`"${eventType}" is not a valid lambda trigger. See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-cloudfront-trigger-events.html for valid event types.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheBehavior.LambdaFunctionAssociations.Quantity =
|
||||||
|
cacheBehavior.LambdaFunctionAssociations.Quantity + 1;
|
||||||
|
cacheBehavior.LambdaFunctionAssociations.Items.push({
|
||||||
|
EventType: eventType,
|
||||||
|
LambdaFunctionARN: lambdaAtEdgeConfig[eventType],
|
||||||
|
IncludeBody: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
68
packages/aws-cloudfront/lib/cacheBahaviorUtils.js
Normal file
68
packages/aws-cloudfront/lib/cacheBahaviorUtils.js
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
const forwardDefaults = {
|
||||||
|
cookies: 'none',
|
||||||
|
queryString: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param config User-defined config
|
||||||
|
* @param defaults Default framework values (default cache behavior and custom cache behavior have different default values)
|
||||||
|
* @returns Object
|
||||||
|
*/
|
||||||
|
function getForwardedValues(config = {}, defaults = {}) {
|
||||||
|
const defaultValues = { ...forwardDefaults, ...defaults };
|
||||||
|
const {
|
||||||
|
cookies,
|
||||||
|
queryString = defaultValues.queryString,
|
||||||
|
headers,
|
||||||
|
queryStringCacheKeys,
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
// Cookies
|
||||||
|
const forwardCookies = {
|
||||||
|
Forward: defaultValues.cookies,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof cookies === 'string') {
|
||||||
|
forwardCookies.Forward = cookies;
|
||||||
|
} else if (Array.isArray(cookies)) {
|
||||||
|
forwardCookies.Forward = 'whitelist';
|
||||||
|
forwardCookies.WhitelistedNames = {
|
||||||
|
Quantity: cookies.length,
|
||||||
|
Items: cookies,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
const forwardHeaders = {
|
||||||
|
Quantity: 0,
|
||||||
|
Items: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof headers === 'string' && headers === 'all') {
|
||||||
|
forwardHeaders.Quantity = 1;
|
||||||
|
forwardHeaders.Items = ['*'];
|
||||||
|
} else if (Array.isArray(headers)) {
|
||||||
|
forwardHeaders.Quantity = headers.length;
|
||||||
|
forwardHeaders.Items = headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryStringCacheKeys
|
||||||
|
const forwardQueryKeys = {
|
||||||
|
Quantity: 0,
|
||||||
|
Items: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Array.isArray(queryStringCacheKeys)) {
|
||||||
|
forwardQueryKeys.Quantity = queryStringCacheKeys.length;
|
||||||
|
forwardQueryKeys.Items = queryStringCacheKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
QueryString: queryString,
|
||||||
|
Cookies: forwardCookies,
|
||||||
|
Headers: forwardHeaders,
|
||||||
|
QueryStringCacheKeys: forwardQueryKeys,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getForwardedValues };
|
||||||
14
packages/aws-cloudfront/lib/createOriginAccessIdentity.js
Normal file
14
packages/aws-cloudfront/lib/createOriginAccessIdentity.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
module.exports = async (cf) => {
|
||||||
|
const {
|
||||||
|
CloudFrontOriginAccessIdentity: { Id, S3CanonicalUserId },
|
||||||
|
} = await cf
|
||||||
|
.createCloudFrontOriginAccessIdentity({
|
||||||
|
CloudFrontOriginAccessIdentityConfig: {
|
||||||
|
CallerReference: 'serverless-managed-cloudfront-access-identity',
|
||||||
|
Comment: 'CloudFront Origin Access Identity created to allow serving private S3 content',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.promise();
|
||||||
|
|
||||||
|
return { originAccessIdentityId: Id, s3CanonicalUserId: S3CanonicalUserId };
|
||||||
|
};
|
||||||
44
packages/aws-cloudfront/lib/getCacheBehavior.js
Normal file
44
packages/aws-cloudfront/lib/getCacheBehavior.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
const { getForwardedValues } = require('./cacheBahaviorUtils');
|
||||||
|
|
||||||
|
module.exports = (pathPattern, pathPatternConfig, originId) => {
|
||||||
|
const {
|
||||||
|
allowedHttpMethods = ['GET', 'HEAD'],
|
||||||
|
ttl,
|
||||||
|
compress = true,
|
||||||
|
smoothStreaming = false,
|
||||||
|
viewerProtocolPolicy = 'https-only',
|
||||||
|
fieldLevelEncryptionId = '',
|
||||||
|
} = pathPatternConfig;
|
||||||
|
|
||||||
|
return {
|
||||||
|
ForwardedValues: getForwardedValues(pathPatternConfig.forward, {
|
||||||
|
cookies: 'all',
|
||||||
|
queryString: true,
|
||||||
|
}),
|
||||||
|
MinTTL: ttl,
|
||||||
|
PathPattern: pathPattern,
|
||||||
|
TargetOriginId: originId,
|
||||||
|
TrustedSigners: {
|
||||||
|
Enabled: false,
|
||||||
|
Quantity: 0,
|
||||||
|
},
|
||||||
|
ViewerProtocolPolicy: viewerProtocolPolicy,
|
||||||
|
AllowedMethods: {
|
||||||
|
Quantity: allowedHttpMethods.length,
|
||||||
|
Items: allowedHttpMethods,
|
||||||
|
CachedMethods: {
|
||||||
|
Items: ['GET', 'HEAD'],
|
||||||
|
Quantity: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Compress: compress,
|
||||||
|
SmoothStreaming: smoothStreaming,
|
||||||
|
DefaultTTL: ttl,
|
||||||
|
MaxTTL: ttl,
|
||||||
|
FieldLevelEncryptionId: fieldLevelEncryptionId,
|
||||||
|
LambdaFunctionAssociations: {
|
||||||
|
Quantity: 0,
|
||||||
|
Items: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
47
packages/aws-cloudfront/lib/getDefaultCacheBehavior.js
Normal file
47
packages/aws-cloudfront/lib/getDefaultCacheBehavior.js
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
const addLambdaAtEdgeToCacheBehavior = require('./addLambdaAtEdgeToCacheBehavior');
|
||||||
|
const { getForwardedValues } = require('./cacheBahaviorUtils');
|
||||||
|
|
||||||
|
module.exports = (originId, defaults = {}) => {
|
||||||
|
const {
|
||||||
|
allowedHttpMethods = ['HEAD', 'GET'],
|
||||||
|
forward = {},
|
||||||
|
ttl = 86400,
|
||||||
|
compress = false,
|
||||||
|
smoothStreaming = false,
|
||||||
|
viewerProtocolPolicy = 'redirect-to-https',
|
||||||
|
fieldLevelEncryptionId = '',
|
||||||
|
} = defaults;
|
||||||
|
|
||||||
|
const defaultCacheBehavior = {
|
||||||
|
TargetOriginId: originId,
|
||||||
|
ForwardedValues: getForwardedValues(forward),
|
||||||
|
TrustedSigners: {
|
||||||
|
Enabled: false,
|
||||||
|
Quantity: 0,
|
||||||
|
Items: [],
|
||||||
|
},
|
||||||
|
ViewerProtocolPolicy: viewerProtocolPolicy,
|
||||||
|
MinTTL: 0,
|
||||||
|
AllowedMethods: {
|
||||||
|
Quantity: allowedHttpMethods.length,
|
||||||
|
Items: allowedHttpMethods,
|
||||||
|
CachedMethods: {
|
||||||
|
Quantity: 2,
|
||||||
|
Items: ['HEAD', 'GET'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SmoothStreaming: smoothStreaming,
|
||||||
|
DefaultTTL: ttl,
|
||||||
|
MaxTTL: 31536000,
|
||||||
|
Compress: compress,
|
||||||
|
LambdaFunctionAssociations: {
|
||||||
|
Quantity: 0,
|
||||||
|
Items: [],
|
||||||
|
},
|
||||||
|
FieldLevelEncryptionId: fieldLevelEncryptionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
addLambdaAtEdgeToCacheBehavior(defaultCacheBehavior, defaults['lambda@edge']);
|
||||||
|
|
||||||
|
return defaultCacheBehavior;
|
||||||
|
};
|
||||||
42
packages/aws-cloudfront/lib/getOriginConfig.js
Normal file
42
packages/aws-cloudfront/lib/getOriginConfig.js
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
const url = require('url');
|
||||||
|
|
||||||
|
module.exports = (origin, { originAccessIdentityId = '' }) => {
|
||||||
|
const originUrl = typeof origin === 'string' ? origin : origin.url;
|
||||||
|
|
||||||
|
const { hostname, pathname } = url.parse(originUrl);
|
||||||
|
|
||||||
|
const originConfig = {
|
||||||
|
Id: hostname,
|
||||||
|
DomainName: hostname,
|
||||||
|
CustomHeaders: {
|
||||||
|
Quantity: 0,
|
||||||
|
Items: [],
|
||||||
|
},
|
||||||
|
OriginPath: pathname === '/' ? '' : pathname,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (originUrl.includes('s3')) {
|
||||||
|
const bucketName = hostname.split('.')[0];
|
||||||
|
originConfig.Id = bucketName;
|
||||||
|
originConfig.DomainName = `${bucketName}.s3.amazonaws.com`;
|
||||||
|
originConfig.S3OriginConfig = {
|
||||||
|
OriginAccessIdentity: originAccessIdentityId
|
||||||
|
? `origin-access-identity/cloudfront/${originAccessIdentityId}`
|
||||||
|
: '',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
originConfig.CustomOriginConfig = {
|
||||||
|
HTTPPort: 80,
|
||||||
|
HTTPSPort: 443,
|
||||||
|
OriginProtocolPolicy: 'https-only',
|
||||||
|
OriginSslProtocols: {
|
||||||
|
Quantity: 1,
|
||||||
|
Items: ['TLSv1.2'],
|
||||||
|
},
|
||||||
|
OriginReadTimeout: 30,
|
||||||
|
OriginKeepaliveTimeout: 5,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return originConfig;
|
||||||
|
};
|
||||||
24
packages/aws-cloudfront/lib/grantCloudFrontBucketAccess.js
Normal file
24
packages/aws-cloudfront/lib/grantCloudFrontBucketAccess.js
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
module.exports = (s3, bucketName, s3CanonicalUserId) => {
|
||||||
|
const policy = `
|
||||||
|
{
|
||||||
|
"Version":"2012-10-17",
|
||||||
|
"Id":"PolicyForCloudFrontPrivateContent",
|
||||||
|
"Statement":[
|
||||||
|
{
|
||||||
|
"Sid":" Grant a CloudFront Origin Identity access to support private content",
|
||||||
|
"Effect":"Allow",
|
||||||
|
"Principal":{"CanonicalUser":"${s3CanonicalUserId}"},
|
||||||
|
"Action":"s3:GetObject",
|
||||||
|
"Resource":"arn:aws:s3:::${bucketName}/*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return s3
|
||||||
|
.putBucketPolicy({
|
||||||
|
Bucket: bucketName,
|
||||||
|
Policy: policy.replace(/(\r\n|\n|\r|\t)/gm, ''),
|
||||||
|
})
|
||||||
|
.promise();
|
||||||
|
};
|
||||||
178
packages/aws-cloudfront/lib/index.js
Normal file
178
packages/aws-cloudfront/lib/index.js
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
const parseInputOrigins = require('./parseInputOrigins');
|
||||||
|
const getDefaultCacheBehavior = require('./getDefaultCacheBehavior');
|
||||||
|
const createOriginAccessIdentity = require('./createOriginAccessIdentity');
|
||||||
|
const grantCloudFrontBucketAccess = require('./grantCloudFrontBucketAccess');
|
||||||
|
|
||||||
|
const servePrivateContentEnabled = (inputs) =>
|
||||||
|
inputs.origins.some((origin) => {
|
||||||
|
return origin && origin.private === true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateBucketsPolicies = async (s3, origins, s3CanonicalUserId) => {
|
||||||
|
// update bucket policies with cloudfront access
|
||||||
|
const bucketNames = origins.Items.filter((origin) => origin.S3OriginConfig).map(
|
||||||
|
(origin) => origin.Id
|
||||||
|
);
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
bucketNames.map((bucketName) => grantCloudFrontBucketAccess(s3, bucketName, s3CanonicalUserId))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createCloudFrontDistribution = async (cf, s3, inputs) => {
|
||||||
|
const params = {
|
||||||
|
DistributionConfig: {
|
||||||
|
CallerReference: String(Date.now()),
|
||||||
|
Comment: inputs.comment,
|
||||||
|
Aliases: {
|
||||||
|
Quantity: 0,
|
||||||
|
Items: [],
|
||||||
|
},
|
||||||
|
Origins: {
|
||||||
|
Quantity: 0,
|
||||||
|
Items: [],
|
||||||
|
},
|
||||||
|
PriceClass: 'PriceClass_All',
|
||||||
|
Enabled: inputs.enabled,
|
||||||
|
HttpVersion: 'http2',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const distributionConfig = params.DistributionConfig;
|
||||||
|
|
||||||
|
let originAccessIdentityId;
|
||||||
|
let s3CanonicalUserId;
|
||||||
|
|
||||||
|
if (servePrivateContentEnabled(inputs)) {
|
||||||
|
({ originAccessIdentityId, s3CanonicalUserId } = await createOriginAccessIdentity(cf));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Origins, CacheBehaviors } = parseInputOrigins(inputs.origins, {
|
||||||
|
originAccessIdentityId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (s3CanonicalUserId) {
|
||||||
|
await updateBucketsPolicies(s3, Origins, s3CanonicalUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
distributionConfig.Origins = Origins;
|
||||||
|
|
||||||
|
// set first origin declared as the default cache behavior
|
||||||
|
distributionConfig.DefaultCacheBehavior = getDefaultCacheBehavior(
|
||||||
|
Origins.Items[0].Id,
|
||||||
|
inputs.defaults
|
||||||
|
);
|
||||||
|
|
||||||
|
if (CacheBehaviors) {
|
||||||
|
distributionConfig.CacheBehaviors = CacheBehaviors;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await cf.createDistribution(params).promise();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: res.Distribution.Id,
|
||||||
|
arn: res.Distribution.ARN,
|
||||||
|
url: `https://${res.Distribution.DomainName}`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCloudFrontDistribution = async (cf, s3, distributionId, inputs) => {
|
||||||
|
// Update logic is a bit weird...
|
||||||
|
// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudFront.html#updateDistribution-property
|
||||||
|
|
||||||
|
// 1. we gotta get the config first...
|
||||||
|
// todo what if id does not exist?
|
||||||
|
const params = await cf.getDistributionConfig({ Id: distributionId }).promise();
|
||||||
|
|
||||||
|
// 2. then add this property
|
||||||
|
params.IfMatch = params.ETag;
|
||||||
|
|
||||||
|
// 3. then delete this property
|
||||||
|
delete params.ETag;
|
||||||
|
|
||||||
|
// 4. then set this property
|
||||||
|
params.Id = distributionId;
|
||||||
|
|
||||||
|
// 5. then make our changes
|
||||||
|
|
||||||
|
params.DistributionConfig.Enabled = inputs.enabled;
|
||||||
|
params.DistributionConfig.Comment = inputs.comment;
|
||||||
|
|
||||||
|
let s3CanonicalUserId;
|
||||||
|
let originAccessIdentityId;
|
||||||
|
|
||||||
|
if (servePrivateContentEnabled(inputs)) {
|
||||||
|
// presumably it's ok to call create origin access identity again
|
||||||
|
// aws api returns cached copy of what was previously created
|
||||||
|
// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudFront.html#createCloudFrontOriginAccessIdentity-property
|
||||||
|
({ originAccessIdentityId, s3CanonicalUserId } = await createOriginAccessIdentity(cf));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Origins, CacheBehaviors } = parseInputOrigins(inputs.origins, {
|
||||||
|
originAccessIdentityId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (s3CanonicalUserId) {
|
||||||
|
await updateBucketsPolicies(s3, Origins, s3CanonicalUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
params.DistributionConfig.DefaultCacheBehavior = getDefaultCacheBehavior(
|
||||||
|
Origins.Items[0].Id,
|
||||||
|
inputs.defaults
|
||||||
|
);
|
||||||
|
params.DistributionConfig.Origins = Origins;
|
||||||
|
|
||||||
|
if (CacheBehaviors) {
|
||||||
|
params.DistributionConfig.CacheBehaviors = CacheBehaviors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. then finally update!
|
||||||
|
const res = await cf.updateDistribution(params).promise();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: res.Distribution.Id,
|
||||||
|
arn: res.Distribution.ARN,
|
||||||
|
url: `https://${res.Distribution.DomainName}`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const disableCloudFrontDistribution = async (cf, distributionId) => {
|
||||||
|
const params = await cf.getDistributionConfig({ Id: distributionId }).promise();
|
||||||
|
|
||||||
|
params.IfMatch = params.ETag;
|
||||||
|
|
||||||
|
delete params.ETag;
|
||||||
|
|
||||||
|
params.Id = distributionId;
|
||||||
|
|
||||||
|
params.DistributionConfig.Enabled = false;
|
||||||
|
|
||||||
|
const res = await cf.updateDistribution(params).promise();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: res.Distribution.Id,
|
||||||
|
arn: res.Distribution.ARN,
|
||||||
|
url: `https://${res.Distribution.DomainName}`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteCloudFrontDistribution = async (cf, distributionId) => {
|
||||||
|
try {
|
||||||
|
const res = await cf.getDistributionConfig({ Id: distributionId }).promise();
|
||||||
|
|
||||||
|
const params = { Id: distributionId, IfMatch: res.ETag };
|
||||||
|
await cf.deleteDistribution(params).promise();
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === 'DistributionNotDisabled') {
|
||||||
|
await disableCloudFrontDistribution(cf, distributionId);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createCloudFrontDistribution,
|
||||||
|
updateCloudFrontDistribution,
|
||||||
|
deleteCloudFrontDistribution,
|
||||||
|
};
|
||||||
40
packages/aws-cloudfront/lib/parseInputOrigins.js
Normal file
40
packages/aws-cloudfront/lib/parseInputOrigins.js
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
const getOriginConfig = require('./getOriginConfig');
|
||||||
|
const getCacheBehavior = require('./getCacheBehavior');
|
||||||
|
const addLambdaAtEdgeToCacheBehavior = require('./addLambdaAtEdgeToCacheBehavior');
|
||||||
|
|
||||||
|
module.exports = (origins, options) => {
|
||||||
|
const distributionOrigins = {
|
||||||
|
Quantity: 0,
|
||||||
|
Items: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const distributionCacheBehaviors = {
|
||||||
|
Quantity: 0,
|
||||||
|
Items: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const origin of origins) {
|
||||||
|
const originConfig = getOriginConfig(origin, options);
|
||||||
|
|
||||||
|
distributionOrigins.Quantity = distributionOrigins.Quantity + 1;
|
||||||
|
distributionOrigins.Items.push(originConfig);
|
||||||
|
|
||||||
|
if (typeof origin === 'object') {
|
||||||
|
// add any cache behaviors
|
||||||
|
for (const pathPattern in origin.pathPatterns) {
|
||||||
|
const pathPatternConfig = origin.pathPatterns[pathPattern];
|
||||||
|
const cacheBehavior = getCacheBehavior(pathPattern, pathPatternConfig, originConfig.Id);
|
||||||
|
|
||||||
|
addLambdaAtEdgeToCacheBehavior(cacheBehavior, pathPatternConfig['lambda@edge']);
|
||||||
|
|
||||||
|
distributionCacheBehaviors.Quantity = distributionCacheBehaviors.Quantity + 1;
|
||||||
|
distributionCacheBehaviors.Items.push(cacheBehavior);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
Origins: distributionOrigins,
|
||||||
|
CacheBehaviors: distributionCacheBehaviors,
|
||||||
|
};
|
||||||
|
};
|
||||||
13
packages/aws-cloudfront/package.json
Normal file
13
packages/aws-cloudfront/package.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"name": "aws-cloudfront",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "./serverless.js",
|
||||||
|
"dependencies": {
|
||||||
|
"@serverless/core": "^1.1.2",
|
||||||
|
"ramda": "^0.27.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"fs-extra": "^9.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
85
packages/aws-cloudfront/serverless.js
Normal file
85
packages/aws-cloudfront/serverless.js
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
const aws = require('aws-sdk');
|
||||||
|
const { equals } = require('ramda');
|
||||||
|
const { Component } = require('@serverless/core');
|
||||||
|
const {
|
||||||
|
createCloudFrontDistribution,
|
||||||
|
updateCloudFrontDistribution,
|
||||||
|
deleteCloudFrontDistribution,
|
||||||
|
} = require('./lib');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Website
|
||||||
|
*/
|
||||||
|
class CloudFront extends Component {
|
||||||
|
async default(inputs = {}) {
|
||||||
|
this.context.status('Deploying');
|
||||||
|
|
||||||
|
inputs.region = inputs.region || 'us-east-1';
|
||||||
|
inputs.enabled = inputs.enabled === false ? false : true;
|
||||||
|
inputs.comment =
|
||||||
|
inputs.comment === null || inputs.comment === undefined ? '' : String(inputs.comment);
|
||||||
|
|
||||||
|
this.context.debug(
|
||||||
|
`Starting deployment of CloudFront distribution to the ${inputs.region} region.`
|
||||||
|
);
|
||||||
|
|
||||||
|
const cf = new aws.CloudFront({
|
||||||
|
credentials: this.context.credentials.aws,
|
||||||
|
region: inputs.region,
|
||||||
|
});
|
||||||
|
|
||||||
|
const s3 = new aws.S3({
|
||||||
|
credentials: this.context.credentials.aws,
|
||||||
|
region: inputs.region,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.state.id) {
|
||||||
|
if (
|
||||||
|
!equals(this.state.origins, inputs.origins) ||
|
||||||
|
!equals(this.state.defaults, inputs.defaults) ||
|
||||||
|
!equals(this.state.enabled, inputs.enabled) ||
|
||||||
|
!equals(this.state.comment, inputs.comment)
|
||||||
|
) {
|
||||||
|
this.context.debug(`Updating CloudFront distribution of ID ${this.state.id}.`);
|
||||||
|
this.state = await updateCloudFrontDistribution(cf, s3, this.state.id, inputs);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.context.debug(`Creating CloudFront distribution in the ${inputs.region} region.`);
|
||||||
|
this.state = await createCloudFrontDistribution(cf, s3, inputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.region = inputs.region;
|
||||||
|
this.state.enabled = inputs.enabled;
|
||||||
|
this.state.comment = inputs.comment;
|
||||||
|
this.state.origins = inputs.origins;
|
||||||
|
this.state.defaults = inputs.defaults;
|
||||||
|
await this.save();
|
||||||
|
|
||||||
|
this.context.debug(`CloudFront deployed successfully with URL: ${this.state.url}.`);
|
||||||
|
|
||||||
|
return this.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove() {
|
||||||
|
this.context.status(`Removing`);
|
||||||
|
|
||||||
|
if (!this.state.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cf = new aws.CloudFront({
|
||||||
|
credentials: this.context.credentials.aws,
|
||||||
|
region: this.state.region,
|
||||||
|
});
|
||||||
|
|
||||||
|
await deleteCloudFrontDistribution(cf, this.state.id);
|
||||||
|
|
||||||
|
this.state = {};
|
||||||
|
await this.save();
|
||||||
|
|
||||||
|
this.context.debug(`CloudFront distribution was successfully removed.`);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = CloudFront;
|
||||||
46
packages/aws-cloudfront/test-utils.js
Normal file
46
packages/aws-cloudfront/test-utils.js
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
const fse = require('fs-extra');
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
const CloudFrontComponent = require('./serverless');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createComponent: async () => {
|
||||||
|
// mock to prevent jest snapshots changing every time
|
||||||
|
Date.now = () => 1566599541192;
|
||||||
|
|
||||||
|
// create tmp folder to avoid state collisions between tests
|
||||||
|
const tmpFolder = await fse.mkdtemp(path.join(os.tmpdir(), 'test-'));
|
||||||
|
|
||||||
|
const component = new CloudFrontComponent('TestCloudFront', {
|
||||||
|
stateRoot: tmpFolder,
|
||||||
|
});
|
||||||
|
|
||||||
|
await component.init();
|
||||||
|
|
||||||
|
return component;
|
||||||
|
},
|
||||||
|
|
||||||
|
assertHasCacheBehavior: (spy, cacheBehavior) => {
|
||||||
|
expect(spy).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
DistributionConfig: expect.objectContaining({
|
||||||
|
CacheBehaviors: expect.objectContaining({
|
||||||
|
Items: [expect.objectContaining(cacheBehavior)],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
assertHasOrigin: (spy, origin) => {
|
||||||
|
expect(spy).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
DistributionConfig: expect.objectContaining({
|
||||||
|
Origins: expect.objectContaining({
|
||||||
|
Items: [expect.objectContaining(origin)],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
336
packages/aws-cloudfront/yarn.lock
Normal file
336
packages/aws-cloudfront/yarn.lock
Normal file
|
|
@ -0,0 +1,336 @@
|
||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@serverless/core@^1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@serverless/core/-/core-1.1.2.tgz#96a2ac428d81c0459474e77db6881ebdd820065d"
|
||||||
|
integrity sha512-PY7gH+7aQ+MltcUD7SRDuQODJ9Sav9HhFJsgOiyf8IVo7XVD6FxZIsSnpMI6paSkptOB7n+0Jz03gNlEkKetQQ==
|
||||||
|
dependencies:
|
||||||
|
fs-extra "^7.0.1"
|
||||||
|
js-yaml "^3.13.1"
|
||||||
|
package-json "^6.3.0"
|
||||||
|
ramda "^0.26.1"
|
||||||
|
semver "^6.1.1"
|
||||||
|
|
||||||
|
"@sindresorhus/is@^0.14.0":
|
||||||
|
version "0.14.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
|
||||||
|
integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==
|
||||||
|
|
||||||
|
"@szmarczak/http-timer@^1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421"
|
||||||
|
integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==
|
||||||
|
dependencies:
|
||||||
|
defer-to-connect "^1.0.1"
|
||||||
|
|
||||||
|
argparse@^1.0.7:
|
||||||
|
version "1.0.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
|
||||||
|
integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
|
||||||
|
dependencies:
|
||||||
|
sprintf-js "~1.0.2"
|
||||||
|
|
||||||
|
at-least-node@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
|
||||||
|
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
|
||||||
|
|
||||||
|
cacheable-request@^6.0.0:
|
||||||
|
version "6.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912"
|
||||||
|
integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==
|
||||||
|
dependencies:
|
||||||
|
clone-response "^1.0.2"
|
||||||
|
get-stream "^5.1.0"
|
||||||
|
http-cache-semantics "^4.0.0"
|
||||||
|
keyv "^3.0.0"
|
||||||
|
lowercase-keys "^2.0.0"
|
||||||
|
normalize-url "^4.1.0"
|
||||||
|
responselike "^1.0.2"
|
||||||
|
|
||||||
|
clone-response@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
|
||||||
|
integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=
|
||||||
|
dependencies:
|
||||||
|
mimic-response "^1.0.0"
|
||||||
|
|
||||||
|
decompress-response@^3.3.0:
|
||||||
|
version "3.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
|
||||||
|
integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=
|
||||||
|
dependencies:
|
||||||
|
mimic-response "^1.0.0"
|
||||||
|
|
||||||
|
deep-extend@^0.6.0:
|
||||||
|
version "0.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
|
||||||
|
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
|
||||||
|
|
||||||
|
defer-to-connect@^1.0.1:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591"
|
||||||
|
integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==
|
||||||
|
|
||||||
|
duplexer3@^0.1.4:
|
||||||
|
version "0.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
|
||||||
|
integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
|
||||||
|
|
||||||
|
end-of-stream@^1.1.0:
|
||||||
|
version "1.4.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
|
||||||
|
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
|
||||||
|
dependencies:
|
||||||
|
once "^1.4.0"
|
||||||
|
|
||||||
|
esprima@^4.0.0:
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
||||||
|
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
|
||||||
|
|
||||||
|
fs-extra@^7.0.1:
|
||||||
|
version "7.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
|
||||||
|
integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==
|
||||||
|
dependencies:
|
||||||
|
graceful-fs "^4.1.2"
|
||||||
|
jsonfile "^4.0.0"
|
||||||
|
universalify "^0.1.0"
|
||||||
|
|
||||||
|
fs-extra@^9.0.1:
|
||||||
|
version "9.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc"
|
||||||
|
integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==
|
||||||
|
dependencies:
|
||||||
|
at-least-node "^1.0.0"
|
||||||
|
graceful-fs "^4.2.0"
|
||||||
|
jsonfile "^6.0.1"
|
||||||
|
universalify "^1.0.0"
|
||||||
|
|
||||||
|
get-stream@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
|
||||||
|
integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
|
||||||
|
dependencies:
|
||||||
|
pump "^3.0.0"
|
||||||
|
|
||||||
|
get-stream@^5.1.0:
|
||||||
|
version "5.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9"
|
||||||
|
integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==
|
||||||
|
dependencies:
|
||||||
|
pump "^3.0.0"
|
||||||
|
|
||||||
|
got@^9.6.0:
|
||||||
|
version "9.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
|
||||||
|
integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==
|
||||||
|
dependencies:
|
||||||
|
"@sindresorhus/is" "^0.14.0"
|
||||||
|
"@szmarczak/http-timer" "^1.1.2"
|
||||||
|
cacheable-request "^6.0.0"
|
||||||
|
decompress-response "^3.3.0"
|
||||||
|
duplexer3 "^0.1.4"
|
||||||
|
get-stream "^4.1.0"
|
||||||
|
lowercase-keys "^1.0.1"
|
||||||
|
mimic-response "^1.0.1"
|
||||||
|
p-cancelable "^1.0.0"
|
||||||
|
to-readable-stream "^1.0.0"
|
||||||
|
url-parse-lax "^3.0.0"
|
||||||
|
|
||||||
|
graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0:
|
||||||
|
version "4.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
|
||||||
|
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
|
||||||
|
|
||||||
|
http-cache-semantics@^4.0.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
|
||||||
|
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
|
||||||
|
|
||||||
|
ini@~1.3.0:
|
||||||
|
version "1.3.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
||||||
|
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
|
||||||
|
|
||||||
|
js-yaml@^3.13.1:
|
||||||
|
version "3.14.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
|
||||||
|
integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==
|
||||||
|
dependencies:
|
||||||
|
argparse "^1.0.7"
|
||||||
|
esprima "^4.0.0"
|
||||||
|
|
||||||
|
json-buffer@3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
|
||||||
|
integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=
|
||||||
|
|
||||||
|
jsonfile@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
|
||||||
|
integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
|
||||||
|
optionalDependencies:
|
||||||
|
graceful-fs "^4.1.6"
|
||||||
|
|
||||||
|
jsonfile@^6.0.1:
|
||||||
|
version "6.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179"
|
||||||
|
integrity sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==
|
||||||
|
dependencies:
|
||||||
|
universalify "^1.0.0"
|
||||||
|
optionalDependencies:
|
||||||
|
graceful-fs "^4.1.6"
|
||||||
|
|
||||||
|
keyv@^3.0.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
|
||||||
|
integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==
|
||||||
|
dependencies:
|
||||||
|
json-buffer "3.0.0"
|
||||||
|
|
||||||
|
lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
|
||||||
|
integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
|
||||||
|
|
||||||
|
lowercase-keys@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
|
||||||
|
integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
|
||||||
|
|
||||||
|
mimic-response@^1.0.0, mimic-response@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
|
||||||
|
integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
|
||||||
|
|
||||||
|
minimist@^1.2.0:
|
||||||
|
version "1.2.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||||
|
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||||
|
|
||||||
|
normalize-url@^4.1.0:
|
||||||
|
version "4.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129"
|
||||||
|
integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==
|
||||||
|
|
||||||
|
once@^1.3.1, once@^1.4.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||||
|
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
|
||||||
|
dependencies:
|
||||||
|
wrappy "1"
|
||||||
|
|
||||||
|
p-cancelable@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
|
||||||
|
integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==
|
||||||
|
|
||||||
|
package-json@^6.3.0:
|
||||||
|
version "6.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0"
|
||||||
|
integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==
|
||||||
|
dependencies:
|
||||||
|
got "^9.6.0"
|
||||||
|
registry-auth-token "^4.0.0"
|
||||||
|
registry-url "^5.0.0"
|
||||||
|
semver "^6.2.0"
|
||||||
|
|
||||||
|
prepend-http@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||||
|
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
|
||||||
|
|
||||||
|
pump@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
|
||||||
|
integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
|
||||||
|
dependencies:
|
||||||
|
end-of-stream "^1.1.0"
|
||||||
|
once "^1.3.1"
|
||||||
|
|
||||||
|
ramda@^0.26.1:
|
||||||
|
version "0.26.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06"
|
||||||
|
integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==
|
||||||
|
|
||||||
|
ramda@^0.27.0:
|
||||||
|
version "0.27.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.0.tgz#915dc29865c0800bf3f69b8fd6c279898b59de43"
|
||||||
|
integrity sha512-pVzZdDpWwWqEVVLshWUHjNwuVP7SfcmPraYuqocJp1yo2U1R7P+5QAfDhdItkuoGqIBnBYrtPp7rEPqDn9HlZA==
|
||||||
|
|
||||||
|
rc@^1.2.8:
|
||||||
|
version "1.2.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
||||||
|
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
|
||||||
|
dependencies:
|
||||||
|
deep-extend "^0.6.0"
|
||||||
|
ini "~1.3.0"
|
||||||
|
minimist "^1.2.0"
|
||||||
|
strip-json-comments "~2.0.1"
|
||||||
|
|
||||||
|
registry-auth-token@^4.0.0:
|
||||||
|
version "4.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.1.1.tgz#40a33be1e82539460f94328b0f7f0f84c16d9479"
|
||||||
|
integrity sha512-9bKS7nTl9+/A1s7tnPeGrUpRcVY+LUh7bfFgzpndALdPfXQBfQV77rQVtqgUV3ti4vc/Ik81Ex8UJDWDQ12zQA==
|
||||||
|
dependencies:
|
||||||
|
rc "^1.2.8"
|
||||||
|
|
||||||
|
registry-url@^5.0.0:
|
||||||
|
version "5.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009"
|
||||||
|
integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==
|
||||||
|
dependencies:
|
||||||
|
rc "^1.2.8"
|
||||||
|
|
||||||
|
responselike@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7"
|
||||||
|
integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=
|
||||||
|
dependencies:
|
||||||
|
lowercase-keys "^1.0.0"
|
||||||
|
|
||||||
|
semver@^6.1.1, semver@^6.2.0:
|
||||||
|
version "6.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||||
|
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||||
|
|
||||||
|
sprintf-js@~1.0.2:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||||
|
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
|
||||||
|
|
||||||
|
strip-json-comments@~2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||||
|
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
||||||
|
|
||||||
|
to-readable-stream@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771"
|
||||||
|
integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==
|
||||||
|
|
||||||
|
universalify@^0.1.0:
|
||||||
|
version "0.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||||
|
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
||||||
|
|
||||||
|
universalify@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d"
|
||||||
|
integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==
|
||||||
|
|
||||||
|
url-parse-lax@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c"
|
||||||
|
integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=
|
||||||
|
dependencies:
|
||||||
|
prepend-http "^2.0.0"
|
||||||
|
|
||||||
|
wrappy@1:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||||
|
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
||||||
44
packages/aws-lambda/__mocks__/aws-sdk.mock.js
Normal file
44
packages/aws-lambda/__mocks__/aws-sdk.mock.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
const promisifyMock = (mockFn) => {
|
||||||
|
const promise = jest.fn();
|
||||||
|
mockFn.mockImplementation(() => ({
|
||||||
|
promise,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCreateFunction = jest.fn();
|
||||||
|
const mockCreateFunctionPromise = promisifyMock(mockCreateFunction);
|
||||||
|
|
||||||
|
const mockPublishVersion = jest.fn();
|
||||||
|
const mockPublishVersionPromise = promisifyMock(mockPublishVersion);
|
||||||
|
|
||||||
|
const mockGetFunctionConfiguration = jest.fn();
|
||||||
|
const mockGetFunctionConfigurationPromise = promisifyMock(mockGetFunctionConfiguration);
|
||||||
|
|
||||||
|
const mockUpdateFunctionCode = jest.fn();
|
||||||
|
const mockUpdateFunctionCodePromise = promisifyMock(mockUpdateFunctionCode);
|
||||||
|
|
||||||
|
const mockUpdateFunctionConfiguration = jest.fn();
|
||||||
|
const mockUpdateFunctionConfigurationPromise = promisifyMock(mockUpdateFunctionConfiguration);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mockCreateFunction,
|
||||||
|
mockCreateFunctionPromise,
|
||||||
|
mockPublishVersion,
|
||||||
|
mockPublishVersionPromise,
|
||||||
|
mockGetFunctionConfiguration,
|
||||||
|
mockGetFunctionConfigurationPromise,
|
||||||
|
mockUpdateFunctionCode,
|
||||||
|
mockUpdateFunctionCodePromise,
|
||||||
|
mockUpdateFunctionConfiguration,
|
||||||
|
mockUpdateFunctionConfigurationPromise,
|
||||||
|
|
||||||
|
Lambda: jest.fn(() => ({
|
||||||
|
createFunction: mockCreateFunction,
|
||||||
|
publishVersion: mockPublishVersion,
|
||||||
|
getFunctionConfiguration: mockGetFunctionConfiguration,
|
||||||
|
updateFunctionCode: mockUpdateFunctionCode,
|
||||||
|
updateFunctionConfiguration: mockUpdateFunctionConfiguration,
|
||||||
|
})),
|
||||||
|
};
|
||||||
106
packages/aws-lambda/__tests__/publishVersion.test.js
Normal file
106
packages/aws-lambda/__tests__/publishVersion.test.js
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
const { createComponent, createTmpDir } = require('../test-utils');
|
||||||
|
|
||||||
|
const {
|
||||||
|
mockCreateFunctionPromise,
|
||||||
|
mockPublishVersion,
|
||||||
|
mockPublishVersionPromise,
|
||||||
|
mockGetFunctionConfigurationPromise,
|
||||||
|
mockUpdateFunctionCodePromise,
|
||||||
|
mockUpdateFunctionConfigurationPromise,
|
||||||
|
} = require('aws-sdk');
|
||||||
|
|
||||||
|
jest.mock('aws-sdk', () => require('../__mocks__/aws-sdk.mock'));
|
||||||
|
|
||||||
|
const mockIamRole = jest.fn();
|
||||||
|
jest.mock('@serverless/aws-iam-role', () =>
|
||||||
|
jest.fn(() => {
|
||||||
|
const iamRole = mockIamRole;
|
||||||
|
iamRole.init = () => {};
|
||||||
|
iamRole.default = () => {};
|
||||||
|
iamRole.context = {};
|
||||||
|
return iamRole;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('publishVersion', () => {
|
||||||
|
let component;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockIamRole.mockResolvedValue({
|
||||||
|
arn: 'arn:aws:iam::123456789012:role/xyz',
|
||||||
|
});
|
||||||
|
mockCreateFunctionPromise.mockResolvedValueOnce({
|
||||||
|
FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:my-func',
|
||||||
|
CodeSha256: 'LQT0VA=',
|
||||||
|
});
|
||||||
|
|
||||||
|
component = await createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('publishes new version of lambda that was created', async () => {
|
||||||
|
mockGetFunctionConfigurationPromise.mockRejectedValueOnce({
|
||||||
|
code: 'ResourceNotFoundException',
|
||||||
|
});
|
||||||
|
mockPublishVersionPromise.mockResolvedValueOnce({
|
||||||
|
Version: 'v2',
|
||||||
|
});
|
||||||
|
const tmpFolder = await createTmpDir();
|
||||||
|
|
||||||
|
await component.default({
|
||||||
|
code: tmpFolder,
|
||||||
|
});
|
||||||
|
|
||||||
|
const versionResult = await component.publishVersion();
|
||||||
|
|
||||||
|
expect(mockPublishVersion).toBeCalledWith({
|
||||||
|
FunctionName: expect.any(String),
|
||||||
|
CodeSha256: 'LQT0VA=',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(versionResult).toEqual({
|
||||||
|
version: 'v2',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('publishes new version of lambda that was updated', async () => {
|
||||||
|
mockPublishVersionPromise.mockResolvedValue({
|
||||||
|
Version: 'v2',
|
||||||
|
});
|
||||||
|
mockGetFunctionConfigurationPromise.mockRejectedValueOnce({
|
||||||
|
code: 'ResourceNotFoundException',
|
||||||
|
});
|
||||||
|
mockGetFunctionConfigurationPromise.mockResolvedValueOnce({
|
||||||
|
FunctionName: 'my-func',
|
||||||
|
});
|
||||||
|
mockUpdateFunctionCodePromise.mockResolvedValueOnce({
|
||||||
|
FunctionName: 'my-func',
|
||||||
|
});
|
||||||
|
mockCreateFunctionPromise.mockResolvedValueOnce({
|
||||||
|
CodeSha256: 'LQT0VA=',
|
||||||
|
});
|
||||||
|
mockUpdateFunctionConfigurationPromise.mockResolvedValueOnce({
|
||||||
|
CodeSha256: 'XYZ0VA=',
|
||||||
|
});
|
||||||
|
|
||||||
|
const tmpFolder = await createTmpDir();
|
||||||
|
|
||||||
|
await component.default({
|
||||||
|
code: tmpFolder,
|
||||||
|
});
|
||||||
|
|
||||||
|
await component.default({
|
||||||
|
code: tmpFolder,
|
||||||
|
});
|
||||||
|
|
||||||
|
const versionResult = await component.publishVersion();
|
||||||
|
|
||||||
|
expect(mockPublishVersion).toBeCalledWith({
|
||||||
|
FunctionName: expect.any(String),
|
||||||
|
CodeSha256: 'XYZ0VA=', // compare against the hash received from the function update, *not* create
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(versionResult).toEqual({
|
||||||
|
version: 'v2',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
16
packages/aws-lambda/package.json
Normal file
16
packages/aws-lambda/package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "aws-lambda",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "serverless.js",
|
||||||
|
"dependencies": {
|
||||||
|
"@serverless/aws-iam-role": "^1.0.0",
|
||||||
|
"@serverless/aws-lambda-layer": "^1.0.0",
|
||||||
|
"@serverless/aws-s3": "^4.2.0",
|
||||||
|
"@serverless/core": "^1.1.2",
|
||||||
|
"archiver": "^4.0.1",
|
||||||
|
"fs-extra": "^9.0.1",
|
||||||
|
"globby": "^11.0.1",
|
||||||
|
"ramda": "^0.27.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
240
packages/aws-lambda/serverless.js
Normal file
240
packages/aws-lambda/serverless.js
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
const path = require('path');
|
||||||
|
const aws = require('aws-sdk');
|
||||||
|
const AwsSdkLambda = aws.Lambda;
|
||||||
|
const { mergeDeepRight, pick } = require('ramda');
|
||||||
|
const { Component, utils } = require('@serverless/core');
|
||||||
|
const {
|
||||||
|
createLambda,
|
||||||
|
updateLambdaCode,
|
||||||
|
updateLambdaConfig,
|
||||||
|
getLambda,
|
||||||
|
deleteLambda,
|
||||||
|
configChanged,
|
||||||
|
pack,
|
||||||
|
} = require('./utils');
|
||||||
|
|
||||||
|
const outputsList = [
|
||||||
|
'name',
|
||||||
|
'hash',
|
||||||
|
'description',
|
||||||
|
'memory',
|
||||||
|
'timeout',
|
||||||
|
'code',
|
||||||
|
'bucket',
|
||||||
|
'shims',
|
||||||
|
'handler',
|
||||||
|
'runtime',
|
||||||
|
'env',
|
||||||
|
'role',
|
||||||
|
'layer',
|
||||||
|
'arn',
|
||||||
|
'region',
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaults = {
|
||||||
|
description: 'AWS Lambda Component',
|
||||||
|
memory: 512,
|
||||||
|
timeout: 10,
|
||||||
|
code: process.cwd(),
|
||||||
|
bucket: undefined,
|
||||||
|
shims: [],
|
||||||
|
handler: 'handler.hello',
|
||||||
|
runtime: 'nodejs10.x',
|
||||||
|
env: {},
|
||||||
|
region: 'us-east-1',
|
||||||
|
};
|
||||||
|
|
||||||
|
class AwsLambda extends Component {
|
||||||
|
async default(inputs = {}) {
|
||||||
|
this.context.status(`Deploying`);
|
||||||
|
|
||||||
|
const config = mergeDeepRight(defaults, inputs);
|
||||||
|
|
||||||
|
config.name = inputs.name || this.state.name || this.context.resourceId();
|
||||||
|
|
||||||
|
this.context.debug(
|
||||||
|
`Starting deployment of lambda ${config.name} to the ${config.region} region.`
|
||||||
|
);
|
||||||
|
|
||||||
|
const lambda = new AwsSdkLambda({
|
||||||
|
region: config.region,
|
||||||
|
credentials: this.context.credentials.aws,
|
||||||
|
});
|
||||||
|
|
||||||
|
const awsIamRole = await this.load('@serverless/aws-iam-role');
|
||||||
|
|
||||||
|
// If no role exists, create a default role
|
||||||
|
let outputsAwsIamRole;
|
||||||
|
if (!config.role) {
|
||||||
|
this.context.debug(`No role provided for lambda ${config.name}.`);
|
||||||
|
|
||||||
|
outputsAwsIamRole = await awsIamRole({
|
||||||
|
service: 'lambda.amazonaws.com',
|
||||||
|
name: config.name,
|
||||||
|
policy: {
|
||||||
|
arn: 'arn:aws:iam::aws:policy/AdministratorAccess',
|
||||||
|
},
|
||||||
|
region: config.region,
|
||||||
|
});
|
||||||
|
config.role = { arn: outputsAwsIamRole.arn };
|
||||||
|
} else {
|
||||||
|
outputsAwsIamRole = await awsIamRole(config.role);
|
||||||
|
config.role = { arn: outputsAwsIamRole.arn };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
config.bucket &&
|
||||||
|
config.runtime === 'nodejs10.x' &&
|
||||||
|
(await utils.dirExists(path.join(config.code, 'node_modules')))
|
||||||
|
) {
|
||||||
|
this.context.debug(`Bucket ${config.bucket} is provided for lambda ${config.name}.`);
|
||||||
|
|
||||||
|
const layer = await this.load('@serverless/aws-lambda-layer');
|
||||||
|
|
||||||
|
const layerInputs = {
|
||||||
|
description: `${config.name} Dependencies Layer`,
|
||||||
|
code: path.join(config.code, 'node_modules'),
|
||||||
|
runtimes: ['nodejs10.x'],
|
||||||
|
prefix: 'nodejs/node_modules',
|
||||||
|
bucket: config.bucket,
|
||||||
|
region: config.region,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.context.status('Deploying Dependencies');
|
||||||
|
this.context.debug(`Packaging lambda code from ${config.code}.`);
|
||||||
|
this.context.debug(`Uploading dependencies as a layer for lambda ${config.name}.`);
|
||||||
|
|
||||||
|
const promises = [pack(config.code, config.shims, false), layer(layerInputs)];
|
||||||
|
const res = await Promise.all(promises);
|
||||||
|
config.zipPath = res[0];
|
||||||
|
config.layer = res[1];
|
||||||
|
} else {
|
||||||
|
this.context.status('Packaging');
|
||||||
|
this.context.debug(`Packaging lambda code from ${config.code}.`);
|
||||||
|
config.zipPath = await pack(config.code, config.shims);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.hash = await utils.hashFile(config.zipPath);
|
||||||
|
|
||||||
|
let deploymentBucket;
|
||||||
|
if (config.bucket) {
|
||||||
|
deploymentBucket = await this.load('@serverless/aws-s3');
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevLambda = await getLambda({ lambda, ...config });
|
||||||
|
|
||||||
|
if (!prevLambda) {
|
||||||
|
if (config.bucket) {
|
||||||
|
this.context.debug(`Uploading ${config.name} lambda package to bucket ${config.bucket}.`);
|
||||||
|
this.context.status(`Uploading`);
|
||||||
|
|
||||||
|
await deploymentBucket.upload({
|
||||||
|
name: config.bucket,
|
||||||
|
file: config.zipPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context.status(`Creating`);
|
||||||
|
this.context.debug(`Creating lambda ${config.name} in the ${config.region} region.`);
|
||||||
|
|
||||||
|
const createResult = await createLambda({ lambda, ...config });
|
||||||
|
config.arn = createResult.arn;
|
||||||
|
config.hash = createResult.hash;
|
||||||
|
} else {
|
||||||
|
config.arn = prevLambda.arn;
|
||||||
|
|
||||||
|
if (configChanged(prevLambda, config)) {
|
||||||
|
if (config.bucket && prevLambda.hash !== config.hash) {
|
||||||
|
this.context.status(`Uploading code`);
|
||||||
|
this.context.debug(`Uploading ${config.name} lambda code to bucket ${config.bucket}.`);
|
||||||
|
|
||||||
|
await deploymentBucket.upload({
|
||||||
|
name: config.bucket,
|
||||||
|
file: config.zipPath,
|
||||||
|
});
|
||||||
|
await updateLambdaCode({ lambda, ...config });
|
||||||
|
} else if (!config.bucket && prevLambda.hash !== config.hash) {
|
||||||
|
this.context.status(`Uploading code`);
|
||||||
|
this.context.debug(`Uploading ${config.name} lambda code.`);
|
||||||
|
await updateLambdaCode({ lambda, ...config });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context.status(`Updating`);
|
||||||
|
this.context.debug(`Updating ${config.name} lambda config.`);
|
||||||
|
|
||||||
|
const updateResult = await updateLambdaConfig({ lambda, ...config });
|
||||||
|
config.hash = updateResult.hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo we probably don't need this logic now that we auto generate names
|
||||||
|
if (this.state.name && this.state.name !== config.name) {
|
||||||
|
this.context.status(`Replacing`);
|
||||||
|
await deleteLambda({ lambda, name: this.state.name });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context.debug(
|
||||||
|
`Successfully deployed lambda ${config.name} in the ${config.region} region.`
|
||||||
|
);
|
||||||
|
|
||||||
|
const outputs = pick(outputsList, config);
|
||||||
|
|
||||||
|
this.state = outputs;
|
||||||
|
await this.save();
|
||||||
|
|
||||||
|
return outputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async publishVersion() {
|
||||||
|
const { name, region, hash } = this.state;
|
||||||
|
|
||||||
|
const lambda = new AwsSdkLambda({
|
||||||
|
region,
|
||||||
|
credentials: this.context.credentials.aws,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { Version } = await lambda
|
||||||
|
.publishVersion({
|
||||||
|
FunctionName: name,
|
||||||
|
CodeSha256: hash,
|
||||||
|
})
|
||||||
|
.promise();
|
||||||
|
|
||||||
|
return { version: Version };
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove() {
|
||||||
|
this.context.status(`Removing`);
|
||||||
|
|
||||||
|
if (!this.state.name) {
|
||||||
|
this.context.debug(`Aborting removal. Function name not found in state.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, region } = this.state;
|
||||||
|
|
||||||
|
const lambda = new AwsSdkLambda({
|
||||||
|
region,
|
||||||
|
credentials: this.context.credentials.aws,
|
||||||
|
});
|
||||||
|
|
||||||
|
const awsIamRole = await this.load('@serverless/aws-iam-role');
|
||||||
|
const layer = await this.load('@serverless/aws-lambda-layer');
|
||||||
|
|
||||||
|
await awsIamRole.remove();
|
||||||
|
await layer.remove();
|
||||||
|
|
||||||
|
this.context.debug(`Removing lambda ${name} from the ${region} region.`);
|
||||||
|
await deleteLambda({ lambda, name });
|
||||||
|
this.context.debug(`Successfully removed lambda ${name} from the ${region} region.`);
|
||||||
|
|
||||||
|
const outputs = pick(outputsList, this.state);
|
||||||
|
|
||||||
|
this.state = {};
|
||||||
|
await this.save();
|
||||||
|
|
||||||
|
return outputs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AwsLambda;
|
||||||
23
packages/aws-lambda/test-utils.js
Normal file
23
packages/aws-lambda/test-utils.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
const fse = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
const os = require('os');
|
||||||
|
const LambdaComponent = require('./serverless');
|
||||||
|
|
||||||
|
const createTmpDir = () => {
|
||||||
|
return fse.mkdtemp(path.join(os.tmpdir(), 'test-'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const createComponent = async () => {
|
||||||
|
// create tmp folder to avoid state collisions between tests
|
||||||
|
const tmpFolder = await createTmpDir();
|
||||||
|
|
||||||
|
const component = new LambdaComponent('TestLambda', {
|
||||||
|
stateRoot: tmpFolder,
|
||||||
|
});
|
||||||
|
|
||||||
|
await component.init();
|
||||||
|
|
||||||
|
return component;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { createComponent, createTmpDir };
|
||||||
262
packages/aws-lambda/utils.js
Normal file
262
packages/aws-lambda/utils.js
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
const { tmpdir } = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
const archiver = require('archiver');
|
||||||
|
const globby = require('globby');
|
||||||
|
const { contains, isNil, last, split, equals, not, pick } = require('ramda');
|
||||||
|
const { readFile, createReadStream, createWriteStream } = require('fs-extra');
|
||||||
|
const { utils } = require('@serverless/core');
|
||||||
|
|
||||||
|
const VALID_FORMATS = ['zip', 'tar'];
|
||||||
|
const isValidFormat = (format) => contains(format, VALID_FORMATS);
|
||||||
|
|
||||||
|
const packDir = async (inputDirPath, outputFilePath, include = [], exclude = [], prefix) => {
|
||||||
|
const format = last(split('.', outputFilePath));
|
||||||
|
|
||||||
|
if (!isValidFormat(format)) {
|
||||||
|
throw new Error('Please provide a valid format. Either a "zip" or a "tar"');
|
||||||
|
}
|
||||||
|
|
||||||
|
const patterns = ['**/*'];
|
||||||
|
|
||||||
|
if (!isNil(exclude)) {
|
||||||
|
exclude.forEach((excludedItem) => patterns.push(`!${excludedItem}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = (await globby(patterns, { cwd: inputDirPath, dot: true }))
|
||||||
|
.sort() // we must sort to ensure correct hash
|
||||||
|
.map((file) => ({
|
||||||
|
input: path.join(inputDirPath, file),
|
||||||
|
output: prefix ? path.join(prefix, file) : file,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const output = createWriteStream(outputFilePath);
|
||||||
|
const archive = archiver(format, {
|
||||||
|
zlib: { level: 9 },
|
||||||
|
});
|
||||||
|
|
||||||
|
output.on('open', () => {
|
||||||
|
archive.pipe(output);
|
||||||
|
|
||||||
|
// we must set the date to ensure correct hash
|
||||||
|
files.forEach((file) =>
|
||||||
|
archive.append(createReadStream(file.input), {
|
||||||
|
name: file.output,
|
||||||
|
date: new Date(0),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isNil(include)) {
|
||||||
|
include.forEach((file) => {
|
||||||
|
const stream = createReadStream(file);
|
||||||
|
archive.append(stream, {
|
||||||
|
name: path.basename(file),
|
||||||
|
date: new Date(0),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
archive.finalize();
|
||||||
|
});
|
||||||
|
|
||||||
|
archive.on('error', (err) => reject(err));
|
||||||
|
output.on('close', () => resolve(outputFilePath));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAccountId = async (aws) => {
|
||||||
|
const STS = new aws.STS();
|
||||||
|
const res = await STS.getCallerIdentity({}).promise();
|
||||||
|
return res.Account;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createLambda = async ({
|
||||||
|
lambda,
|
||||||
|
name,
|
||||||
|
handler,
|
||||||
|
memory,
|
||||||
|
timeout,
|
||||||
|
runtime,
|
||||||
|
env,
|
||||||
|
description,
|
||||||
|
zipPath,
|
||||||
|
bucket,
|
||||||
|
role,
|
||||||
|
layer,
|
||||||
|
}) => {
|
||||||
|
const params = {
|
||||||
|
FunctionName: name,
|
||||||
|
Code: {},
|
||||||
|
Description: description,
|
||||||
|
Handler: handler,
|
||||||
|
MemorySize: memory,
|
||||||
|
Publish: true,
|
||||||
|
Role: role.arn,
|
||||||
|
Runtime: runtime,
|
||||||
|
Timeout: timeout,
|
||||||
|
Environment: {
|
||||||
|
Variables: env,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (layer && layer.arn) {
|
||||||
|
params.Layers = [layer.arn];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bucket) {
|
||||||
|
params.Code.S3Bucket = bucket;
|
||||||
|
params.Code.S3Key = path.basename(zipPath);
|
||||||
|
} else {
|
||||||
|
params.Code.ZipFile = await readFile(zipPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await lambda.createFunction(params).promise();
|
||||||
|
|
||||||
|
return { arn: res.FunctionArn, hash: res.CodeSha256 };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLambdaConfig = async ({
|
||||||
|
lambda,
|
||||||
|
name,
|
||||||
|
handler,
|
||||||
|
memory,
|
||||||
|
timeout,
|
||||||
|
runtime,
|
||||||
|
env,
|
||||||
|
description,
|
||||||
|
role,
|
||||||
|
layer,
|
||||||
|
}) => {
|
||||||
|
const functionConfigParams = {
|
||||||
|
FunctionName: name,
|
||||||
|
Description: description,
|
||||||
|
Handler: handler,
|
||||||
|
MemorySize: memory,
|
||||||
|
Role: role.arn,
|
||||||
|
Runtime: runtime,
|
||||||
|
Timeout: timeout,
|
||||||
|
Environment: {
|
||||||
|
Variables: env,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (layer && layer.arn) {
|
||||||
|
functionConfigParams.Layers = [layer.arn];
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await lambda.updateFunctionConfiguration(functionConfigParams).promise();
|
||||||
|
|
||||||
|
return { arn: res.FunctionArn, hash: res.CodeSha256 };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLambdaCode = async ({ lambda, name, zipPath, bucket }) => {
|
||||||
|
const functionCodeParams = {
|
||||||
|
FunctionName: name,
|
||||||
|
Publish: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (bucket) {
|
||||||
|
functionCodeParams.S3Bucket = bucket;
|
||||||
|
functionCodeParams.S3Key = path.basename(zipPath);
|
||||||
|
} else {
|
||||||
|
functionCodeParams.ZipFile = await readFile(zipPath);
|
||||||
|
}
|
||||||
|
const res = await lambda.updateFunctionCode(functionCodeParams).promise();
|
||||||
|
|
||||||
|
return res.FunctionArn;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLambda = async ({ lambda, name }) => {
|
||||||
|
try {
|
||||||
|
const res = await lambda
|
||||||
|
.getFunctionConfiguration({
|
||||||
|
FunctionName: name,
|
||||||
|
})
|
||||||
|
.promise();
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: res.FunctionName,
|
||||||
|
description: res.Description,
|
||||||
|
timeout: res.Timeout,
|
||||||
|
runtime: res.Runtime,
|
||||||
|
role: {
|
||||||
|
arn: res.Role,
|
||||||
|
},
|
||||||
|
handler: res.Handler,
|
||||||
|
memory: res.MemorySize,
|
||||||
|
hash: res.CodeSha256,
|
||||||
|
env: res.Environment ? res.Environment.Variables : {},
|
||||||
|
arn: res.FunctionArn,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === 'ResourceNotFoundException') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteLambda = async ({ lambda, name }) => {
|
||||||
|
try {
|
||||||
|
const params = { FunctionName: name };
|
||||||
|
await lambda.deleteFunction(params).promise();
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== 'ResourceNotFoundException') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPolicy = async ({ name, region, accountId }) => {
|
||||||
|
return {
|
||||||
|
Version: '2012-10-17',
|
||||||
|
Statement: [
|
||||||
|
{
|
||||||
|
Action: ['logs:CreateLogStream'],
|
||||||
|
Resource: [`arn:aws:logs:${region}:${accountId}:log-group:/aws/lambda/${name}:*`],
|
||||||
|
Effect: 'Allow',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: ['logs:PutLogEvents'],
|
||||||
|
Resource: [`arn:aws:logs:${region}:${accountId}:log-group:/aws/lambda/${name}:*:*`],
|
||||||
|
Effect: 'Allow',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const configChanged = (prevLambda, lambda) => {
|
||||||
|
const keys = ['description', 'runtime', 'role', 'handler', 'memory', 'timeout', 'env', 'hash'];
|
||||||
|
const inputs = pick(keys, lambda);
|
||||||
|
inputs.role = { arn: inputs.role.arn }; // remove other inputs.role component outputs
|
||||||
|
const prevInputs = pick(keys, prevLambda);
|
||||||
|
return not(equals(inputs, prevInputs));
|
||||||
|
};
|
||||||
|
|
||||||
|
const pack = async (code, shims = [], packDeps = true) => {
|
||||||
|
if (utils.isArchivePath(code)) {
|
||||||
|
return path.resolve(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
let exclude = [];
|
||||||
|
|
||||||
|
if (!packDeps) {
|
||||||
|
exclude = ['node_modules/**'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputFilePath = path.join(tmpdir(), `${Math.random().toString(36).substring(6)}.zip`);
|
||||||
|
|
||||||
|
return packDir(code, outputFilePath, shims, exclude);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createLambda,
|
||||||
|
updateLambdaCode,
|
||||||
|
updateLambdaConfig,
|
||||||
|
getLambda,
|
||||||
|
deleteLambda,
|
||||||
|
getPolicy,
|
||||||
|
getAccountId,
|
||||||
|
configChanged,
|
||||||
|
pack,
|
||||||
|
};
|
||||||
1713
packages/aws-lambda/yarn.lock
Normal file
1713
packages/aws-lambda/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
14
packages/cloudfront/package.json
Normal file
14
packages/cloudfront/package.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"name": "cloudfront",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"prepare": "yarn build",
|
||||||
|
"build": "tsc -p tsconfig.build.json"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^3.9.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
packages/cloudfront/src/index.ts
Normal file
21
packages/cloudfront/src/index.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import AWS from 'aws-sdk';
|
||||||
|
import CloudFrontClientFactory, { Credentials } from './lib/cloudfront';
|
||||||
|
|
||||||
|
export type CreateInvalidationOptions = {
|
||||||
|
credentials: Credentials;
|
||||||
|
distributionId: string;
|
||||||
|
paths?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const createInvalidation = async (
|
||||||
|
options: CreateInvalidationOptions
|
||||||
|
): Promise<AWS.CloudFront.CreateInvalidationResult> => {
|
||||||
|
const { credentials, distributionId, paths } = options;
|
||||||
|
const cf = CloudFrontClientFactory({
|
||||||
|
credentials,
|
||||||
|
});
|
||||||
|
|
||||||
|
return cf.createInvalidation({ distributionId, paths });
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createInvalidation;
|
||||||
50
packages/cloudfront/src/lib/cloudfront.ts
Normal file
50
packages/cloudfront/src/lib/cloudfront.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import AWS from 'aws-sdk';
|
||||||
|
import { ALL_FILES_PATH } from './constants';
|
||||||
|
|
||||||
|
type CloudFrontClientFactoryOptions = {
|
||||||
|
credentials: Credentials;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CreateInvalidationOptions = {
|
||||||
|
distributionId: string;
|
||||||
|
callerReference?: string;
|
||||||
|
paths?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CloudFrontClient = {
|
||||||
|
createInvalidation: (
|
||||||
|
options: CreateInvalidationOptions
|
||||||
|
) => Promise<AWS.CloudFront.CreateInvalidationResult>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Credentials = {
|
||||||
|
accessKeyId: string;
|
||||||
|
secretAccessKey: string;
|
||||||
|
sessionToken?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ({ credentials }: CloudFrontClientFactoryOptions): CloudFrontClient => {
|
||||||
|
const cloudFront = new AWS.CloudFront({ credentials });
|
||||||
|
|
||||||
|
return {
|
||||||
|
createInvalidation: async (
|
||||||
|
options: CreateInvalidationOptions
|
||||||
|
): Promise<AWS.CloudFront.CreateInvalidationResult> => {
|
||||||
|
const timestamp = +new Date() + '';
|
||||||
|
const { distributionId, callerReference = timestamp, paths = [ALL_FILES_PATH] } = options;
|
||||||
|
|
||||||
|
return await cloudFront
|
||||||
|
.createInvalidation({
|
||||||
|
DistributionId: distributionId,
|
||||||
|
InvalidationBatch: {
|
||||||
|
CallerReference: callerReference,
|
||||||
|
Paths: {
|
||||||
|
Quantity: paths.length,
|
||||||
|
Items: paths,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.promise();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
1
packages/cloudfront/src/lib/constants.ts
Normal file
1
packages/cloudfront/src/lib/constants.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export const ALL_FILES_PATH = '/*';
|
||||||
21
packages/cloudfront/tests/aws-sdk.mock.ts
Normal file
21
packages/cloudfront/tests/aws-sdk.mock.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
declare module 'aws-sdk' {
|
||||||
|
const mockCreateInvalidation: jest.Mock;
|
||||||
|
const mockCreateInvalidationPromise: jest.Mock;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promisifyMock = (mockFn: jest.Mock): jest.Mock => {
|
||||||
|
const promise = jest.fn();
|
||||||
|
mockFn.mockReturnValue({ promise });
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockCreateInvalidation = jest.fn();
|
||||||
|
export const mockCreateInvalidationPromise = promisifyMock(mockCreateInvalidation);
|
||||||
|
|
||||||
|
const MockCloudFront = jest.fn(() => ({
|
||||||
|
createInvalidation: mockCreateInvalidation,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default {
|
||||||
|
CloudFront: MockCloudFront,
|
||||||
|
};
|
||||||
64
packages/cloudfront/tests/cache-invalidation.test.ts
Normal file
64
packages/cloudfront/tests/cache-invalidation.test.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import AWS, { mockCreateInvalidation } from 'aws-sdk';
|
||||||
|
import createInvalidation, { CreateInvalidationOptions } from '../src/index';
|
||||||
|
import { ALL_FILES_PATH } from '../src/lib/constants';
|
||||||
|
|
||||||
|
jest.mock('aws-sdk', () => require('./aws-sdk.mock'));
|
||||||
|
|
||||||
|
const invalidate = (
|
||||||
|
options: Partial<CreateInvalidationOptions> = {}
|
||||||
|
): Promise<AWS.CloudFront.CreateInvalidationResult> => {
|
||||||
|
return createInvalidation({
|
||||||
|
...options,
|
||||||
|
distributionId: 'fake-distribution-id',
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: 'fake-access-key',
|
||||||
|
secretAccessKey: 'fake-secret-key',
|
||||||
|
sessionToken: 'fake-session-token',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Cache invalidation tests', () => {
|
||||||
|
it('passes credentials to CloudFront client', async () => {
|
||||||
|
await invalidate();
|
||||||
|
|
||||||
|
expect(AWS.CloudFront).toBeCalledWith({
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: 'fake-access-key',
|
||||||
|
secretAccessKey: 'fake-secret-key',
|
||||||
|
sessionToken: 'fake-session-token',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invalidates CloudFront distribution', async () => {
|
||||||
|
await invalidate();
|
||||||
|
|
||||||
|
expect(mockCreateInvalidation).toBeCalledWith({
|
||||||
|
DistributionId: 'fake-distribution-id',
|
||||||
|
InvalidationBatch: {
|
||||||
|
CallerReference: expect.stringMatching(/^\d+$/),
|
||||||
|
Paths: {
|
||||||
|
Quantity: 1,
|
||||||
|
Items: [ALL_FILES_PATH],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invalidates specified paths', async () => {
|
||||||
|
const paths = ['/static/page1', '/static/page2'];
|
||||||
|
await invalidate({ paths });
|
||||||
|
|
||||||
|
expect(mockCreateInvalidation).toBeCalledWith({
|
||||||
|
DistributionId: 'fake-distribution-id',
|
||||||
|
InvalidationBatch: {
|
||||||
|
CallerReference: expect.stringMatching(/^\d+$/),
|
||||||
|
Paths: {
|
||||||
|
Quantity: 2,
|
||||||
|
Items: paths,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
8
packages/cloudfront/tsconfig.build.json
Normal file
8
packages/cloudfront/tsconfig.build.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"sourceMap": false,
|
||||||
|
"removeComments": true
|
||||||
|
},
|
||||||
|
"include": ["./src/"]
|
||||||
|
}
|
||||||
17
packages/cloudfront/tsconfig.json
Normal file
17
packages/cloudfront/tsconfig.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"declaration": true,
|
||||||
|
"target": "ES2015",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"allowJs": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules"],
|
||||||
|
"include": ["**/*.ts"]
|
||||||
|
}
|
||||||
8
packages/cloudfront/yarn.lock
Normal file
8
packages/cloudfront/yarn.lock
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
typescript@^3.9.6:
|
||||||
|
version "3.9.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a"
|
||||||
|
integrity sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw==
|
||||||
9
packages/domain/package.json
Normal file
9
packages/domain/package.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "domain",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "serverless.js",
|
||||||
|
"dependencies": {
|
||||||
|
"@serverless/core": "^1.1.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
186
packages/domain/serverless.js
Normal file
186
packages/domain/serverless.js
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
const { Component } = require('@serverless/core');
|
||||||
|
const {
|
||||||
|
getClients,
|
||||||
|
prepareSubdomains,
|
||||||
|
getDomainHostedZoneId,
|
||||||
|
describeCertificateByArn,
|
||||||
|
getCertificateArnByDomain,
|
||||||
|
createCertificate,
|
||||||
|
validateCertificate,
|
||||||
|
configureDnsForCloudFrontDistribution,
|
||||||
|
removeCloudFrontDomainDnsRecords,
|
||||||
|
addDomainToCloudfrontDistribution,
|
||||||
|
removeDomainFromCloudFrontDistribution,
|
||||||
|
} = require('./utils');
|
||||||
|
|
||||||
|
class Domain extends Component {
|
||||||
|
async default(inputs = {}) {
|
||||||
|
this.context.status('Deploying');
|
||||||
|
|
||||||
|
this.context.debug(`Starting Domain component deployment.`);
|
||||||
|
|
||||||
|
this.context.debug(`Validating inputs.`);
|
||||||
|
|
||||||
|
inputs.region = inputs.region || 'us-east-1';
|
||||||
|
inputs.privateZone = inputs.privateZone || false;
|
||||||
|
inputs.domainType = inputs.domainType || 'both';
|
||||||
|
inputs.defaultCloudfrontInputs = inputs.defaultCloudfrontInputs || {};
|
||||||
|
|
||||||
|
if (!inputs.domain) {
|
||||||
|
throw Error(`"domain" is a required input.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Check if domain has changed.
|
||||||
|
// On domain change, call remove for all previous state.
|
||||||
|
|
||||||
|
// Get AWS SDK Clients
|
||||||
|
const clients = getClients(this.context.credentials.aws, inputs.region);
|
||||||
|
|
||||||
|
this.context.debug(`Formatting domains and identifying cloud services being used.`);
|
||||||
|
const subdomains = prepareSubdomains(inputs);
|
||||||
|
this.state.region = inputs.region;
|
||||||
|
this.state.privateZone = JSON.parse(inputs.privateZone);
|
||||||
|
this.state.domain = inputs.domain;
|
||||||
|
this.state.subdomains = subdomains;
|
||||||
|
|
||||||
|
await this.save();
|
||||||
|
|
||||||
|
this.context.debug(`Getting the Hosted Zone ID for the domain ${inputs.domain}.`);
|
||||||
|
const domainHostedZoneId = await getDomainHostedZoneId(
|
||||||
|
clients.route53,
|
||||||
|
inputs.domain,
|
||||||
|
inputs.privateZone
|
||||||
|
);
|
||||||
|
|
||||||
|
this.context.debug(
|
||||||
|
`Searching for an AWS ACM Certificate based on the domain: ${inputs.domain}.`
|
||||||
|
);
|
||||||
|
let certificateArn = await getCertificateArnByDomain(clients.acm, inputs.domain);
|
||||||
|
if (!certificateArn) {
|
||||||
|
this.context.debug(
|
||||||
|
`No existing AWS ACM Certificates found for the domain: ${inputs.domain}.`
|
||||||
|
);
|
||||||
|
this.context.debug(`Creating a new AWS ACM Certificate for the domain: ${inputs.domain}.`);
|
||||||
|
certificateArn = await createCertificate(clients.acm, inputs.domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context.debug(`Checking the status of AWS ACM Certificate.`);
|
||||||
|
const certificate = await describeCertificateByArn(clients.acm, certificateArn);
|
||||||
|
|
||||||
|
if (certificate.Status === 'PENDING_VALIDATION') {
|
||||||
|
this.context.debug(`AWS ACM Certificate Validation Status is "PENDING_VALIDATION".`);
|
||||||
|
this.context.debug(`Validating AWS ACM Certificate via Route53 "DNS" method.`);
|
||||||
|
await validateCertificate(
|
||||||
|
clients.acm,
|
||||||
|
clients.route53,
|
||||||
|
certificate,
|
||||||
|
inputs.domain,
|
||||||
|
domainHostedZoneId
|
||||||
|
);
|
||||||
|
this.context.log(
|
||||||
|
'Your AWS ACM Certificate has been created and is being validated via DNS. This could take up to 30 minutes since it depends on DNS propagation. Continuing deployment, but you may have to wait for DNS propagation.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certificate.Status !== 'ISSUED' && certificate.Status !== 'PENDING_VALIDATION') {
|
||||||
|
// TODO: Should we auto-create a new one in this scenario?
|
||||||
|
throw new Error(
|
||||||
|
`Your AWS ACM Certificate for the domain "${inputs.domain}" has an unsupported status of: "${certificate.Status}". Please remove it manually and deploy again.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setting up domains for different services
|
||||||
|
for (const subdomain of subdomains) {
|
||||||
|
if (subdomain.type === 'awsS3Website') {
|
||||||
|
throw new Error(`Unsupported subdomain type ${awsS3Website}`);
|
||||||
|
} else if (subdomain.type === 'awsApiGateway') {
|
||||||
|
throw new Error(`Unsupported subdomain type ${awsS3Website}`);
|
||||||
|
} else if (subdomain.type === 'awsCloudFront') {
|
||||||
|
this.context.debug(
|
||||||
|
`Adding ${subdomain.domain} domain to CloudFront distribution with URL "${subdomain.url}"`
|
||||||
|
);
|
||||||
|
await addDomainToCloudfrontDistribution(
|
||||||
|
clients.cf,
|
||||||
|
subdomain,
|
||||||
|
certificate.CertificateArn,
|
||||||
|
inputs.domainType,
|
||||||
|
inputs.defaultCloudfrontInputs
|
||||||
|
);
|
||||||
|
|
||||||
|
this.context.debug(`Configuring DNS for distribution "${subdomain.url}".`);
|
||||||
|
await configureDnsForCloudFrontDistribution(
|
||||||
|
clients.route53,
|
||||||
|
subdomain,
|
||||||
|
domainHostedZoneId,
|
||||||
|
subdomain.url.replace('https://', ''),
|
||||||
|
inputs.domainType,
|
||||||
|
this
|
||||||
|
);
|
||||||
|
} else if (subdomain.type === 'awsAppSync') {
|
||||||
|
throw new Error(`Unsupported subdomain type ${awsS3Website}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputs = {};
|
||||||
|
let hasRoot = false;
|
||||||
|
outputs.domains = subdomains.map((subdomain) => {
|
||||||
|
if (subdomain.domain.startsWith('www')) {
|
||||||
|
hasRoot = true;
|
||||||
|
}
|
||||||
|
return `https://${subdomain.domain}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasRoot && inputs.domainType !== 'www') {
|
||||||
|
outputs.domains.unshift(`https://${inputs.domain.replace('www.', '')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove() {
|
||||||
|
this.context.status('Deploying');
|
||||||
|
|
||||||
|
if (!this.state.domain) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context.debug(`Starting Domain component removal.`);
|
||||||
|
|
||||||
|
// Get AWS SDK Clients
|
||||||
|
const clients = getClients(this.context.credentials.aws, this.state.region);
|
||||||
|
|
||||||
|
this.context.debug(`Getting the Hosted Zone ID for the domain ${this.state.domain}.`);
|
||||||
|
const domainHostedZoneId = await getDomainHostedZoneId(
|
||||||
|
clients.route53,
|
||||||
|
this.state.domain,
|
||||||
|
this.state.privateZone
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const subdomain in this.state.subdomains) {
|
||||||
|
const domainState = this.state.subdomains[subdomain];
|
||||||
|
if (domainState.type === 'awsS3Website') {
|
||||||
|
this.context.debug(`Unsupported subdomain type ${awsS3Website}`);
|
||||||
|
} else if (domainState.type === 'awsApiGateway') {
|
||||||
|
this.context.debug(`Unsupported subdomain type ${awsS3Website}`);
|
||||||
|
} else if (domainState.type === 'awsCloudFront') {
|
||||||
|
this.context.debug(`Removing domain ${domainState.domain} from CloudFront.`);
|
||||||
|
await removeDomainFromCloudFrontDistribution(clients.cf, domainState);
|
||||||
|
|
||||||
|
this.context.debug(`Removing CloudFront DNS records for domain ${domainState.domain}`);
|
||||||
|
await removeCloudFrontDomainDnsRecords(
|
||||||
|
clients.route53,
|
||||||
|
domainState.domain,
|
||||||
|
domainHostedZoneId,
|
||||||
|
domainState.url.replace('https://', '')
|
||||||
|
);
|
||||||
|
} else if (domainState.type === 'awsAppSync') {
|
||||||
|
this.context.debug(`Unsupported subdomain type ${awsS3Website}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.state = {};
|
||||||
|
await this.save();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Domain;
|
||||||
446
packages/domain/utils.js
Normal file
446
packages/domain/utils.js
Normal file
|
|
@ -0,0 +1,446 @@
|
||||||
|
const aws = require('aws-sdk');
|
||||||
|
const { utils } = require('@serverless/core');
|
||||||
|
|
||||||
|
const DEFAULT_MINIMUM_PROTOCOL_VERSION = 'TLSv1.2_2018';
|
||||||
|
const HOSTED_ZONE_ID = 'Z2FDTNDATAQYW2'; // this is a constant that you can get from here https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget.html
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Clients
|
||||||
|
* - Gets AWS SDK clients to use within this Component
|
||||||
|
*/
|
||||||
|
const getClients = (credentials, region = 'us-east-1') => {
|
||||||
|
const route53 = new aws.Route53({
|
||||||
|
credentials,
|
||||||
|
region,
|
||||||
|
});
|
||||||
|
|
||||||
|
const acm = new aws.ACM({
|
||||||
|
credentials,
|
||||||
|
region: 'us-east-1', // ACM must be in us-east-1
|
||||||
|
});
|
||||||
|
|
||||||
|
const cf = new aws.CloudFront({
|
||||||
|
credentials,
|
||||||
|
region,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
route53,
|
||||||
|
acm,
|
||||||
|
cf,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare Domains
|
||||||
|
* - Formats component domains & identifies cloud services they're using.
|
||||||
|
*/
|
||||||
|
const prepareSubdomains = (inputs) => {
|
||||||
|
const subdomains = [];
|
||||||
|
|
||||||
|
for (const subdomain in inputs.subdomains || {}) {
|
||||||
|
const domainObj = {};
|
||||||
|
|
||||||
|
domainObj.domain = `${subdomain}.${inputs.domain}`;
|
||||||
|
|
||||||
|
if (inputs.subdomains[subdomain].url.includes('cloudfront')) {
|
||||||
|
domainObj.distributionId = inputs.subdomains[subdomain].id;
|
||||||
|
domainObj.url = inputs.subdomains[subdomain].url;
|
||||||
|
domainObj.type = 'awsCloudFront';
|
||||||
|
}
|
||||||
|
|
||||||
|
subdomains.push(domainObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
return subdomains;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOutdatedDomains = (inputs, state) => {
|
||||||
|
if (inputs.domain !== state.domain) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const outdatedDomains = {
|
||||||
|
domain: state.domain,
|
||||||
|
subdomains: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const domain of state.subdomains) {
|
||||||
|
if (!inputs.subdomains[domain.domain]) {
|
||||||
|
outdatedDomains.push(domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return outdatedDomains;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Domain Hosted Zone ID
|
||||||
|
* - Every Domain on Route53 always has a Hosted Zone w/ 2 Record Sets.
|
||||||
|
* - These Record Sets are: "Name Servers (NS)" & "Start of Authority (SOA)"
|
||||||
|
* - These don't need to be created and SHOULD NOT be modified.
|
||||||
|
*/
|
||||||
|
const getDomainHostedZoneId = async (route53, domain, privateZone) => {
|
||||||
|
const hostedZonesRes = await route53.listHostedZonesByName().promise();
|
||||||
|
|
||||||
|
const hostedZone = hostedZonesRes.HostedZones.find(
|
||||||
|
// Name has a period at the end, so we're using includes rather than equals
|
||||||
|
(zone) => zone.Config.PrivateZone === privateZone && zone.Name.includes(domain)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hostedZone) {
|
||||||
|
throw Error(
|
||||||
|
`Domain ${domain} was not found in your AWS account. Please purchase it from Route53 first then try again.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hostedZone.Id.replace('/hostedzone/', ''); // hosted zone id is always prefixed with this :(
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describe Certificate By Arn
|
||||||
|
* - Describe an AWS ACM Certificate by its ARN
|
||||||
|
*/
|
||||||
|
const describeCertificateByArn = async (acm, certificateArn) => {
|
||||||
|
const certificate = await acm.describeCertificate({ CertificateArn: certificateArn }).promise();
|
||||||
|
return certificate && certificate.Certificate ? certificate.Certificate : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Certificate Arn By Domain
|
||||||
|
* - Gets an AWS ACM Certificate by a specified domain or return null
|
||||||
|
*/
|
||||||
|
const getCertificateArnByDomain = async (acm, domain) => {
|
||||||
|
const listRes = await acm.listCertificates().promise();
|
||||||
|
|
||||||
|
for (const certificate of listRes.CertificateSummaryList) {
|
||||||
|
if (certificate.DomainName === domain && certificate.CertificateArn) {
|
||||||
|
if (domain.startsWith('www.')) {
|
||||||
|
const nakedDomain = domain.replace('wwww.', '');
|
||||||
|
// check whether certificate support naked domain
|
||||||
|
const certDetail = await describeCertificateByArn(acm, certificate.CertificateArn);
|
||||||
|
const nakedDomainCert = certDetail.DomainValidationOptions.find(
|
||||||
|
({ DomainName }) => DomainName === nakedDomain
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!nakedDomainCert) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificate.CertificateArn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Certificate
|
||||||
|
* - Creates an AWS ACM Certificate for the specified domain
|
||||||
|
*/
|
||||||
|
const createCertificate = async (acm, domain) => {
|
||||||
|
const wildcardSubDomain = `*.${domain}`;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
DomainName: domain,
|
||||||
|
SubjectAlternativeNames: [domain, wildcardSubDomain],
|
||||||
|
ValidationMethod: 'DNS',
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await acm.requestCertificate(params).promise();
|
||||||
|
|
||||||
|
return res.CertificateArn;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate Certificate
|
||||||
|
* - Validate an AWS ACM Certificate via the "DNS" method
|
||||||
|
*/
|
||||||
|
const validateCertificate = async (acm, route53, certificate, domain, domainHostedZoneId) => {
|
||||||
|
let readinessCheckCount = 16;
|
||||||
|
let statusCheckCount = 16;
|
||||||
|
let validationResourceRecord;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check Readiness
|
||||||
|
* - Newly Created AWS ACM Certificates may not yet have the info needed to validate it
|
||||||
|
* - Specifically, the "ResourceRecord" object in the Domain Validation Options
|
||||||
|
* - Ensure this exists.
|
||||||
|
*/
|
||||||
|
const checkReadiness = async function () {
|
||||||
|
if (readinessCheckCount < 1) {
|
||||||
|
throw new Error(
|
||||||
|
'Your newly created AWS ACM Certificate is taking a while to initialize. Please try running this component again in a few minutes.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cert = await describeCertificateByArn(acm, certificate.CertificateArn);
|
||||||
|
|
||||||
|
// Find root domain validation option resource record
|
||||||
|
cert.DomainValidationOptions.forEach((option) => {
|
||||||
|
if (domain === option.DomainName) {
|
||||||
|
validationResourceRecord = option.ResourceRecord;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!validationResourceRecord) {
|
||||||
|
readinessCheckCount--;
|
||||||
|
await utils.sleep(5000);
|
||||||
|
return await checkReadiness();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await checkReadiness();
|
||||||
|
|
||||||
|
const checkRecordsParams = {
|
||||||
|
HostedZoneId: domainHostedZoneId,
|
||||||
|
MaxItems: '10',
|
||||||
|
StartRecordName: validationResourceRecord.Name,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the validation resource record sets already exist.
|
||||||
|
// This might be the case if the user is trying to deploy multiple times while validation is occurring.
|
||||||
|
const existingRecords = await route53.listResourceRecordSets(checkRecordsParams).promise();
|
||||||
|
|
||||||
|
if (!existingRecords.ResourceRecordSets.length) {
|
||||||
|
// Create CNAME record for DNS validation check
|
||||||
|
// NOTE: It can take 30 minutes or longer for DNS propagation so validation can complete, just continue on and don't wait for this...
|
||||||
|
const recordParams = {
|
||||||
|
HostedZoneId: domainHostedZoneId,
|
||||||
|
ChangeBatch: {
|
||||||
|
Changes: [
|
||||||
|
{
|
||||||
|
Action: 'UPSERT',
|
||||||
|
ResourceRecordSet: {
|
||||||
|
Name: validationResourceRecord.Name,
|
||||||
|
Type: validationResourceRecord.Type,
|
||||||
|
TTL: 300,
|
||||||
|
ResourceRecords: [
|
||||||
|
{
|
||||||
|
Value: validationResourceRecord.Value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await route53.changeResourceRecordSets(recordParams).promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check Validated Status
|
||||||
|
* - Newly Validated AWS ACM Certificates may not yet show up as valid
|
||||||
|
* - This gives them some time to update their status.
|
||||||
|
*/
|
||||||
|
const checkStatus = async function () {
|
||||||
|
if (statusCheckCount < 1) {
|
||||||
|
throw new Error(
|
||||||
|
'Your newly validated AWS ACM Certificate is taking a while to register as valid. Please try running this component again in a few minutes.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cert = await describeCertificateByArn(acm, certificate.CertificateArn);
|
||||||
|
|
||||||
|
if (cert.Status !== 'ISSUED') {
|
||||||
|
statusCheckCount--;
|
||||||
|
await utils.sleep(10000);
|
||||||
|
return await checkStatus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await checkStatus();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure DNS records for a distribution domain
|
||||||
|
*/
|
||||||
|
const configureDnsForCloudFrontDistribution = async (
|
||||||
|
route53,
|
||||||
|
subdomain,
|
||||||
|
domainHostedZoneId,
|
||||||
|
distributionUrl,
|
||||||
|
domainType,
|
||||||
|
that
|
||||||
|
) => {
|
||||||
|
const dnsRecordParams = {
|
||||||
|
HostedZoneId: domainHostedZoneId,
|
||||||
|
ChangeBatch: {
|
||||||
|
Changes: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// don't create www records for apex mode
|
||||||
|
if (!subdomain.domain.startsWith('www.') || domainType !== 'apex') {
|
||||||
|
dnsRecordParams.ChangeBatch.Changes.push({
|
||||||
|
Action: 'UPSERT',
|
||||||
|
ResourceRecordSet: {
|
||||||
|
Name: subdomain.domain,
|
||||||
|
Type: 'A',
|
||||||
|
AliasTarget: {
|
||||||
|
HostedZoneId: HOSTED_ZONE_ID,
|
||||||
|
DNSName: distributionUrl,
|
||||||
|
EvaluateTargetHealth: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't create apex records for www mode
|
||||||
|
if (subdomain.domain.startsWith('www.') && domainType !== 'www') {
|
||||||
|
dnsRecordParams.ChangeBatch.Changes.push({
|
||||||
|
Action: 'UPSERT',
|
||||||
|
ResourceRecordSet: {
|
||||||
|
Name: subdomain.domain.replace('www.', ''),
|
||||||
|
Type: 'A',
|
||||||
|
AliasTarget: {
|
||||||
|
HostedZoneId: HOSTED_ZONE_ID,
|
||||||
|
DNSName: distributionUrl,
|
||||||
|
EvaluateTargetHealth: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return route53.changeResourceRecordSets(dnsRecordParams).promise();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove AWS CloudFront Website DNS Records
|
||||||
|
*/
|
||||||
|
const removeCloudFrontDomainDnsRecords = async (
|
||||||
|
route53,
|
||||||
|
domain,
|
||||||
|
domainHostedZoneId,
|
||||||
|
distributionUrl
|
||||||
|
) => {
|
||||||
|
const params = {
|
||||||
|
HostedZoneId: domainHostedZoneId,
|
||||||
|
ChangeBatch: {
|
||||||
|
Changes: [
|
||||||
|
{
|
||||||
|
Action: 'DELETE',
|
||||||
|
ResourceRecordSet: {
|
||||||
|
Name: domain,
|
||||||
|
Type: 'A',
|
||||||
|
AliasTarget: {
|
||||||
|
HostedZoneId: HOSTED_ZONE_ID,
|
||||||
|
DNSName: distributionUrl,
|
||||||
|
EvaluateTargetHealth: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: should the CNAME records be removed too?
|
||||||
|
|
||||||
|
try {
|
||||||
|
await route53.changeResourceRecordSets(params).promise();
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code !== 'InvalidChangeBatch') {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addDomainToCloudfrontDistribution = async (
|
||||||
|
cf,
|
||||||
|
subdomain,
|
||||||
|
certificateArn,
|
||||||
|
domainType,
|
||||||
|
defaultCloudfrontInputs
|
||||||
|
) => {
|
||||||
|
// Update logic is a bit weird...
|
||||||
|
// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudFront.html#updateDistribution-property
|
||||||
|
|
||||||
|
// 1. we gotta get the config first...
|
||||||
|
const params = await cf.getDistributionConfig({ Id: subdomain.distributionId }).promise();
|
||||||
|
|
||||||
|
// 2. then add this property
|
||||||
|
params.IfMatch = params.ETag;
|
||||||
|
|
||||||
|
// 3. then delete this property
|
||||||
|
delete params.ETag;
|
||||||
|
|
||||||
|
// 4. then set this property
|
||||||
|
params.Id = subdomain.distributionId;
|
||||||
|
|
||||||
|
// 5. then make our changes
|
||||||
|
params.DistributionConfig.Aliases = {
|
||||||
|
Quantity: 1,
|
||||||
|
Items: [subdomain.domain],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (subdomain.domain.startsWith('www.')) {
|
||||||
|
if (domainType === 'apex') {
|
||||||
|
params.DistributionConfig.Aliases.Items = [`${subdomain.domain.replace('www.', '')}`];
|
||||||
|
} else if (domainType !== 'www') {
|
||||||
|
params.DistributionConfig.Aliases.Quantity = 2;
|
||||||
|
params.DistributionConfig.Aliases.Items.push(`${subdomain.domain.replace('www.', '')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params.DistributionConfig.ViewerCertificate = {
|
||||||
|
ACMCertificateArn: certificateArn,
|
||||||
|
SSLSupportMethod: 'sni-only',
|
||||||
|
MinimumProtocolVersion: DEFAULT_MINIMUM_PROTOCOL_VERSION,
|
||||||
|
Certificate: certificateArn,
|
||||||
|
CertificateSource: 'acm',
|
||||||
|
...defaultCloudfrontInputs.viewerCertificate,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 6. then finally update!
|
||||||
|
const res = await cf.updateDistribution(params).promise();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: res.Distribution.Id,
|
||||||
|
arn: res.Distribution.ARN,
|
||||||
|
url: res.Distribution.DomainName,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeDomainFromCloudFrontDistribution = async (cf, subdomain) => {
|
||||||
|
const params = await cf.getDistributionConfig({ Id: subdomain.distributionId }).promise();
|
||||||
|
|
||||||
|
params.IfMatch = params.ETag;
|
||||||
|
|
||||||
|
delete params.ETag;
|
||||||
|
|
||||||
|
params.Id = subdomain.distributionId;
|
||||||
|
|
||||||
|
params.DistributionConfig.Aliases = {
|
||||||
|
Quantity: 0,
|
||||||
|
Items: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
params.DistributionConfig.ViewerCertificate = {
|
||||||
|
SSLSupportMethod: 'sni-only',
|
||||||
|
MinimumProtocolVersion: DEFAULT_MINIMUM_PROTOCOL_VERSION,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await cf.updateDistribution(params).promise();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: res.Distribution.Id,
|
||||||
|
arn: res.Distribution.ARN,
|
||||||
|
url: res.Distribution.DomainName,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getClients,
|
||||||
|
prepareSubdomains,
|
||||||
|
getOutdatedDomains,
|
||||||
|
describeCertificateByArn,
|
||||||
|
getCertificateArnByDomain,
|
||||||
|
createCertificate,
|
||||||
|
validateCertificate,
|
||||||
|
getDomainHostedZoneId,
|
||||||
|
configureDnsForCloudFrontDistribution,
|
||||||
|
removeCloudFrontDomainDnsRecords,
|
||||||
|
addDomainToCloudfrontDistribution,
|
||||||
|
removeDomainFromCloudFrontDistribution,
|
||||||
|
};
|
||||||
302
packages/domain/yarn.lock
Normal file
302
packages/domain/yarn.lock
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@serverless/core@^1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@serverless/core/-/core-1.1.2.tgz#96a2ac428d81c0459474e77db6881ebdd820065d"
|
||||||
|
integrity sha512-PY7gH+7aQ+MltcUD7SRDuQODJ9Sav9HhFJsgOiyf8IVo7XVD6FxZIsSnpMI6paSkptOB7n+0Jz03gNlEkKetQQ==
|
||||||
|
dependencies:
|
||||||
|
fs-extra "^7.0.1"
|
||||||
|
js-yaml "^3.13.1"
|
||||||
|
package-json "^6.3.0"
|
||||||
|
ramda "^0.26.1"
|
||||||
|
semver "^6.1.1"
|
||||||
|
|
||||||
|
"@sindresorhus/is@^0.14.0":
|
||||||
|
version "0.14.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
|
||||||
|
integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==
|
||||||
|
|
||||||
|
"@szmarczak/http-timer@^1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421"
|
||||||
|
integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==
|
||||||
|
dependencies:
|
||||||
|
defer-to-connect "^1.0.1"
|
||||||
|
|
||||||
|
argparse@^1.0.7:
|
||||||
|
version "1.0.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
|
||||||
|
integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
|
||||||
|
dependencies:
|
||||||
|
sprintf-js "~1.0.2"
|
||||||
|
|
||||||
|
cacheable-request@^6.0.0:
|
||||||
|
version "6.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912"
|
||||||
|
integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==
|
||||||
|
dependencies:
|
||||||
|
clone-response "^1.0.2"
|
||||||
|
get-stream "^5.1.0"
|
||||||
|
http-cache-semantics "^4.0.0"
|
||||||
|
keyv "^3.0.0"
|
||||||
|
lowercase-keys "^2.0.0"
|
||||||
|
normalize-url "^4.1.0"
|
||||||
|
responselike "^1.0.2"
|
||||||
|
|
||||||
|
clone-response@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
|
||||||
|
integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=
|
||||||
|
dependencies:
|
||||||
|
mimic-response "^1.0.0"
|
||||||
|
|
||||||
|
decompress-response@^3.3.0:
|
||||||
|
version "3.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
|
||||||
|
integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=
|
||||||
|
dependencies:
|
||||||
|
mimic-response "^1.0.0"
|
||||||
|
|
||||||
|
deep-extend@^0.6.0:
|
||||||
|
version "0.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
|
||||||
|
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
|
||||||
|
|
||||||
|
defer-to-connect@^1.0.1:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591"
|
||||||
|
integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==
|
||||||
|
|
||||||
|
duplexer3@^0.1.4:
|
||||||
|
version "0.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
|
||||||
|
integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
|
||||||
|
|
||||||
|
end-of-stream@^1.1.0:
|
||||||
|
version "1.4.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
|
||||||
|
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
|
||||||
|
dependencies:
|
||||||
|
once "^1.4.0"
|
||||||
|
|
||||||
|
esprima@^4.0.0:
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
||||||
|
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
|
||||||
|
|
||||||
|
fs-extra@^7.0.1:
|
||||||
|
version "7.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
|
||||||
|
integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==
|
||||||
|
dependencies:
|
||||||
|
graceful-fs "^4.1.2"
|
||||||
|
jsonfile "^4.0.0"
|
||||||
|
universalify "^0.1.0"
|
||||||
|
|
||||||
|
get-stream@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
|
||||||
|
integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
|
||||||
|
dependencies:
|
||||||
|
pump "^3.0.0"
|
||||||
|
|
||||||
|
get-stream@^5.1.0:
|
||||||
|
version "5.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9"
|
||||||
|
integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==
|
||||||
|
dependencies:
|
||||||
|
pump "^3.0.0"
|
||||||
|
|
||||||
|
got@^9.6.0:
|
||||||
|
version "9.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
|
||||||
|
integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==
|
||||||
|
dependencies:
|
||||||
|
"@sindresorhus/is" "^0.14.0"
|
||||||
|
"@szmarczak/http-timer" "^1.1.2"
|
||||||
|
cacheable-request "^6.0.0"
|
||||||
|
decompress-response "^3.3.0"
|
||||||
|
duplexer3 "^0.1.4"
|
||||||
|
get-stream "^4.1.0"
|
||||||
|
lowercase-keys "^1.0.1"
|
||||||
|
mimic-response "^1.0.1"
|
||||||
|
p-cancelable "^1.0.0"
|
||||||
|
to-readable-stream "^1.0.0"
|
||||||
|
url-parse-lax "^3.0.0"
|
||||||
|
|
||||||
|
graceful-fs@^4.1.2, graceful-fs@^4.1.6:
|
||||||
|
version "4.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
|
||||||
|
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
|
||||||
|
|
||||||
|
http-cache-semantics@^4.0.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
|
||||||
|
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
|
||||||
|
|
||||||
|
ini@~1.3.0:
|
||||||
|
version "1.3.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
||||||
|
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
|
||||||
|
|
||||||
|
js-yaml@^3.13.1:
|
||||||
|
version "3.14.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
|
||||||
|
integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==
|
||||||
|
dependencies:
|
||||||
|
argparse "^1.0.7"
|
||||||
|
esprima "^4.0.0"
|
||||||
|
|
||||||
|
json-buffer@3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
|
||||||
|
integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=
|
||||||
|
|
||||||
|
jsonfile@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
|
||||||
|
integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
|
||||||
|
optionalDependencies:
|
||||||
|
graceful-fs "^4.1.6"
|
||||||
|
|
||||||
|
keyv@^3.0.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
|
||||||
|
integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==
|
||||||
|
dependencies:
|
||||||
|
json-buffer "3.0.0"
|
||||||
|
|
||||||
|
lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
|
||||||
|
integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
|
||||||
|
|
||||||
|
lowercase-keys@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
|
||||||
|
integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
|
||||||
|
|
||||||
|
mimic-response@^1.0.0, mimic-response@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
|
||||||
|
integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
|
||||||
|
|
||||||
|
minimist@^1.2.0:
|
||||||
|
version "1.2.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||||
|
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||||
|
|
||||||
|
normalize-url@^4.1.0:
|
||||||
|
version "4.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129"
|
||||||
|
integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==
|
||||||
|
|
||||||
|
once@^1.3.1, once@^1.4.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||||
|
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
|
||||||
|
dependencies:
|
||||||
|
wrappy "1"
|
||||||
|
|
||||||
|
p-cancelable@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
|
||||||
|
integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==
|
||||||
|
|
||||||
|
package-json@^6.3.0:
|
||||||
|
version "6.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0"
|
||||||
|
integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==
|
||||||
|
dependencies:
|
||||||
|
got "^9.6.0"
|
||||||
|
registry-auth-token "^4.0.0"
|
||||||
|
registry-url "^5.0.0"
|
||||||
|
semver "^6.2.0"
|
||||||
|
|
||||||
|
prepend-http@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||||
|
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
|
||||||
|
|
||||||
|
pump@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
|
||||||
|
integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
|
||||||
|
dependencies:
|
||||||
|
end-of-stream "^1.1.0"
|
||||||
|
once "^1.3.1"
|
||||||
|
|
||||||
|
ramda@^0.26.1:
|
||||||
|
version "0.26.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06"
|
||||||
|
integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==
|
||||||
|
|
||||||
|
rc@^1.2.8:
|
||||||
|
version "1.2.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
||||||
|
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
|
||||||
|
dependencies:
|
||||||
|
deep-extend "^0.6.0"
|
||||||
|
ini "~1.3.0"
|
||||||
|
minimist "^1.2.0"
|
||||||
|
strip-json-comments "~2.0.1"
|
||||||
|
|
||||||
|
registry-auth-token@^4.0.0:
|
||||||
|
version "4.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.1.1.tgz#40a33be1e82539460f94328b0f7f0f84c16d9479"
|
||||||
|
integrity sha512-9bKS7nTl9+/A1s7tnPeGrUpRcVY+LUh7bfFgzpndALdPfXQBfQV77rQVtqgUV3ti4vc/Ik81Ex8UJDWDQ12zQA==
|
||||||
|
dependencies:
|
||||||
|
rc "^1.2.8"
|
||||||
|
|
||||||
|
registry-url@^5.0.0:
|
||||||
|
version "5.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009"
|
||||||
|
integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==
|
||||||
|
dependencies:
|
||||||
|
rc "^1.2.8"
|
||||||
|
|
||||||
|
responselike@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7"
|
||||||
|
integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=
|
||||||
|
dependencies:
|
||||||
|
lowercase-keys "^1.0.0"
|
||||||
|
|
||||||
|
semver@^6.1.1, semver@^6.2.0:
|
||||||
|
version "6.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||||
|
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||||
|
|
||||||
|
sprintf-js@~1.0.2:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||||
|
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
|
||||||
|
|
||||||
|
strip-json-comments@~2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||||
|
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
||||||
|
|
||||||
|
to-readable-stream@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771"
|
||||||
|
integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==
|
||||||
|
|
||||||
|
universalify@^0.1.0:
|
||||||
|
version "0.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||||
|
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
||||||
|
|
||||||
|
url-parse-lax@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c"
|
||||||
|
integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=
|
||||||
|
dependencies:
|
||||||
|
prepend-http "^2.0.0"
|
||||||
|
|
||||||
|
wrappy@1:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||||
|
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
const create = require('../next-aws-cloudfront');
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
const { SPECIAL_NODE_HEADERS } = create;
|
||||||
|
|
||||||
|
describe('Request Tests', () => {
|
||||||
|
it('request url path', () => {
|
||||||
|
const { req } = create({
|
||||||
|
request: {
|
||||||
|
uri: '/',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(req.url).toEqual('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('querystring /?x=42', () => {
|
||||||
|
const { req } = create({
|
||||||
|
request: {
|
||||||
|
uri: '/',
|
||||||
|
querystring: 'x=42',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(req.url).toEqual('/?x=42');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('request method', () => {
|
||||||
|
const { req } = create({
|
||||||
|
request: {
|
||||||
|
uri: '',
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(req.method).toEqual('GET');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('request headers', () => {
|
||||||
|
const { req } = create({
|
||||||
|
request: {
|
||||||
|
uri: '',
|
||||||
|
headers: {
|
||||||
|
host: [
|
||||||
|
{
|
||||||
|
key: 'Host',
|
||||||
|
value: 'xyz.net',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'user-agent': [
|
||||||
|
{
|
||||||
|
key: 'User-Agent',
|
||||||
|
value: 'mozilla',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(req.headers['host']).toEqual('xyz.net');
|
||||||
|
expect(req.getHeader('host')).toEqual('xyz.net');
|
||||||
|
expect(req.headers['user-agent']).toEqual('mozilla');
|
||||||
|
expect(req.getHeader('user-agent')).toEqual('mozilla');
|
||||||
|
|
||||||
|
expect(req.getHeaders()).toEqual({
|
||||||
|
host: 'xyz.net',
|
||||||
|
'user-agent': 'mozilla',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(req.rawHeaders).toEqual(['Host', 'xyz.net', 'User-Agent', 'mozilla']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('duplicates of special request headers are discarded', () => {
|
||||||
|
SPECIAL_NODE_HEADERS.forEach((headerName) => {
|
||||||
|
// user-agent -> uSER-AGENT
|
||||||
|
const duplicateHeaderName = headerName.charAt(0) + headerName.substring(1).toUpperCase();
|
||||||
|
|
||||||
|
const { req } = create({
|
||||||
|
request: {
|
||||||
|
uri: '',
|
||||||
|
headers: {
|
||||||
|
[headerName]: [
|
||||||
|
{
|
||||||
|
key: headerName,
|
||||||
|
value: 'headerValue',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: duplicateHeaderName,
|
||||||
|
value: 'hEaderValue',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(req.headers[headerName]).toEqual('headerValue');
|
||||||
|
expect(req.getHeader(headerName)).toEqual('headerValue');
|
||||||
|
|
||||||
|
expect(req.getHeaders()).toEqual({
|
||||||
|
[headerName]: 'headerValue',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(req.rawHeaders).toEqual([
|
||||||
|
headerName,
|
||||||
|
'headerValue',
|
||||||
|
duplicateHeaderName,
|
||||||
|
'hEaderValue',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('text body', (done) => {
|
||||||
|
const { req } = create({
|
||||||
|
request: {
|
||||||
|
uri: '',
|
||||||
|
body: {
|
||||||
|
data: 'ok',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
req.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('end', () => {
|
||||||
|
expect(data).toEqual('ok');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('text base64 body', (done) => {
|
||||||
|
const { req } = create({
|
||||||
|
request: {
|
||||||
|
uri: '',
|
||||||
|
body: {
|
||||||
|
encoding: 'base64',
|
||||||
|
data: Buffer.from('ok').toString('base64'),
|
||||||
|
},
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
req.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('end', () => {
|
||||||
|
expect(data).toEqual('ok');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('text body with encoding', (done) => {
|
||||||
|
const { req } = create({
|
||||||
|
request: {
|
||||||
|
uri: '',
|
||||||
|
body: {
|
||||||
|
data: 'åäöß',
|
||||||
|
},
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
req.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('end', () => {
|
||||||
|
expect(data).toEqual('åäöß');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('connection', (done) => {
|
||||||
|
const { req } = create({
|
||||||
|
request: {
|
||||||
|
uri: '',
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(req.connection).toEqual({});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('request preserve http.IncomingMessage.prototype property', () => {
|
||||||
|
const exampleProperty = "I'm an example property";
|
||||||
|
http.IncomingMessage.prototype.exampleProperty = exampleProperty;
|
||||||
|
const { req } = create({
|
||||||
|
request: {
|
||||||
|
uri: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(typeof req.exampleProperty !== 'undefined').toEqual(true);
|
||||||
|
expect(req.exampleProperty).toEqual(exampleProperty);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('request preserve http.IncomingMessage.prototype function', () => {
|
||||||
|
const exampleFunction = function () {
|
||||||
|
return "I'm an example function.";
|
||||||
|
};
|
||||||
|
http.IncomingMessage.prototype.exampleFunction = exampleFunction;
|
||||||
|
const { req } = create({
|
||||||
|
request: {
|
||||||
|
uri: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(typeof req.exampleFunction === 'function').toEqual(true);
|
||||||
|
expect(req.exampleFunction()).toEqual(exampleFunction());
|
||||||
|
expect(req.exampleFunction.toString()).toEqual(exampleFunction.toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,373 @@
|
||||||
|
const zlib = require('zlib');
|
||||||
|
const create = require('../next-aws-cloudfront');
|
||||||
|
|
||||||
|
describe('Response Tests', () => {
|
||||||
|
it('statusCode writeHead 404', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const { responsePromise, res } = create({
|
||||||
|
request: {
|
||||||
|
uri: '/',
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end();
|
||||||
|
|
||||||
|
return responsePromise.then((response) => {
|
||||||
|
expect(response.status).toEqual(404);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('statusCode statusCode=200', () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
|
||||||
|
const { res, responsePromise } = create({
|
||||||
|
request: {
|
||||||
|
uri: '/',
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.statusCode = 200;
|
||||||
|
res.end();
|
||||||
|
|
||||||
|
return responsePromise.then((response) => {
|
||||||
|
expect(response.status).toEqual(200);
|
||||||
|
expect(response.statusDescription).toEqual('OK');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('statusCode statusCode=200 by default', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const { res, responsePromise } = create({
|
||||||
|
request: {
|
||||||
|
uri: '/',
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
|
||||||
|
return responsePromise.then((response) => {
|
||||||
|
expect(response.status).toEqual(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writeHead headers', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const { res, responsePromise } = create({
|
||||||
|
request: {
|
||||||
|
uri: '/',
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
'x-custom-1': '1',
|
||||||
|
'x-custom-2': '2',
|
||||||
|
});
|
||||||
|
res.end();
|
||||||
|
|
||||||
|
return responsePromise.then((response) => {
|
||||||
|
expect(response.headers).toEqual({
|
||||||
|
'x-custom-1': [
|
||||||
|
{
|
||||||
|
key: 'x-custom-1',
|
||||||
|
value: '1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'x-custom-2': [
|
||||||
|
{
|
||||||
|
key: 'x-custom-2',
|
||||||
|
value: '2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writeHead ignores special CloudFront Headers', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const { res, responsePromise } = create({
|
||||||
|
request: {
|
||||||
|
uri: '/',
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const cloudFrontReadOnlyHeaders = {
|
||||||
|
'Accept-Encoding': 'gzip',
|
||||||
|
'Content-Length': '1234',
|
||||||
|
'If-Modified-Since': 'Wed, 21 Oct 2015 07:28:00 GMT',
|
||||||
|
'If-None-Match': '*',
|
||||||
|
'If-Range': 'Wed, 21 Oct 2015 07:28:00 GMT',
|
||||||
|
'If-Unmodified-Since': 'Wed, 21 Oct 2015 07:28:00 GMT',
|
||||||
|
'Transfer-Encoding': 'compress',
|
||||||
|
Via: 'HTTP/1.1 GWA',
|
||||||
|
};
|
||||||
|
|
||||||
|
res.writeHead(200, cloudFrontReadOnlyHeaders);
|
||||||
|
res.end();
|
||||||
|
|
||||||
|
return responsePromise.then((response) => {
|
||||||
|
expect(response.headers).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setHeader (multiple headers with same name)', () => {
|
||||||
|
const { res, responsePromise } = create({
|
||||||
|
request: {
|
||||||
|
uri: '/',
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setHeader('set-cookie', ['1', '2']);
|
||||||
|
res.end();
|
||||||
|
|
||||||
|
return responsePromise.then((response) => {
|
||||||
|
expect(response.headers).toEqual({
|
||||||
|
'set-cookie': [
|
||||||
|
{
|
||||||
|
key: 'set-cookie',
|
||||||
|
value: '1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'set-cookie',
|
||||||
|
value: '2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setHeader', () => {
|
||||||
|
const { res, responsePromise } = create({
|
||||||
|
request: {
|
||||||
|
uri: '/',
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setHeader('x-custom-1', '1');
|
||||||
|
res.setHeader('x-custom-2', '2');
|
||||||
|
res.end();
|
||||||
|
|
||||||
|
return responsePromise.then((response) => {
|
||||||
|
expect(response.headers).toEqual({
|
||||||
|
'x-custom-1': [
|
||||||
|
{
|
||||||
|
key: 'x-custom-1',
|
||||||
|
value: '1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'x-custom-2': [
|
||||||
|
{
|
||||||
|
key: 'x-custom-2',
|
||||||
|
value: '2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setHeader ignores special CloudFront headers', () => {
|
||||||
|
const { res, responsePromise } = create({
|
||||||
|
request: {
|
||||||
|
uri: '/',
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setHeader('Content-Length', '123');
|
||||||
|
res.setHeader('x-custom-2', '2');
|
||||||
|
res.end();
|
||||||
|
|
||||||
|
return responsePromise.then((response) => {
|
||||||
|
expect(response.headers).toEqual({
|
||||||
|
'x-custom-2': [
|
||||||
|
{
|
||||||
|
key: 'x-custom-2',
|
||||||
|
value: '2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setHeader + removeHeader', () => {
|
||||||
|
const { res, responsePromise } = create({
|
||||||
|
request: {
|
||||||
|
uri: '/',
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setHeader('x-custom-1', '1');
|
||||||
|
res.setHeader('x-custom-2', '2');
|
||||||
|
res.removeHeader('x-custom-1');
|
||||||
|
res.end();
|
||||||
|
|
||||||
|
return responsePromise.then((response) => {
|
||||||
|
expect(response.headers).toEqual({
|
||||||
|
'x-custom-2': [
|
||||||
|
{
|
||||||
|
key: 'x-custom-2',
|
||||||
|
value: '2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getHeader/s', () => {
|
||||||
|
const { res } = create({
|
||||||
|
request: {
|
||||||
|
path: '/',
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res.setHeader('x-custom-1', '1');
|
||||||
|
res.setHeader('x-custom-2', '2');
|
||||||
|
expect(res.getHeader('x-custom-1')).toEqual('1');
|
||||||
|
expect(res.getHeaders()).toEqual({
|
||||||
|
'x-custom-1': '1',
|
||||||
|
'x-custom-2': '2',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hasHeader', () => {
|
||||||
|
const { res } = create({
|
||||||
|
request: {
|
||||||
|
path: '/',
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res.setHeader('x-custom-1', '1');
|
||||||
|
|
||||||
|
expect(res.hasHeader('x-custom-1')).toBe(true);
|
||||||
|
expect(res.hasHeader('x-custom-2')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('case insensitive headers', () => {
|
||||||
|
const { res } = create({
|
||||||
|
request: {
|
||||||
|
path: '/',
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res.setHeader('x-custom-1', '1');
|
||||||
|
res.setHeader('X-CUSTOM-2', '2');
|
||||||
|
res.setHeader('X-cUsToM-3', '3');
|
||||||
|
|
||||||
|
expect(res.getHeader('X-CUSTOM-1')).toEqual('1');
|
||||||
|
expect(res.getHeader('x-custom-2')).toEqual('2');
|
||||||
|
expect(res.getHeader('x-CuStOm-3')).toEqual('3');
|
||||||
|
|
||||||
|
expect(res.getHeaders()).toEqual({
|
||||||
|
'x-custom-1': '1',
|
||||||
|
'x-custom-2': '2',
|
||||||
|
'x-custom-3': '3',
|
||||||
|
});
|
||||||
|
|
||||||
|
res.removeHeader('X-CUSTOM-1');
|
||||||
|
res.removeHeader('x-custom-2');
|
||||||
|
res.removeHeader('x-CUSTom-3');
|
||||||
|
|
||||||
|
expect(res.getHeaders()).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`res.write('ok')`, () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
|
||||||
|
const { res, responsePromise } = create({
|
||||||
|
request: {
|
||||||
|
path: '/',
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.write('ok');
|
||||||
|
res.end();
|
||||||
|
|
||||||
|
return responsePromise.then((response) => {
|
||||||
|
expect(response.body).toEqual('b2s=');
|
||||||
|
expect(response.bodyEncoding).toEqual('base64');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`res.end('ok')`, () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const { res, responsePromise } = create({
|
||||||
|
request: {
|
||||||
|
path: '/',
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.end('ok');
|
||||||
|
|
||||||
|
return responsePromise.then((response) => {
|
||||||
|
expect(response.body).toEqual('b2s=');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`gzips`, () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
|
||||||
|
const gzipSpy = jest.spyOn(zlib, 'gzipSync');
|
||||||
|
gzipSpy.mockReturnValueOnce(Buffer.from('ok-gzipped'));
|
||||||
|
|
||||||
|
const { res, responsePromise } = create({
|
||||||
|
request: {
|
||||||
|
path: '/',
|
||||||
|
headers: {
|
||||||
|
'accept-encoding': [
|
||||||
|
{
|
||||||
|
key: 'Accept-Encoding',
|
||||||
|
value: 'gzip',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.end('ok');
|
||||||
|
|
||||||
|
gzipSpy.mockRestore();
|
||||||
|
|
||||||
|
return responsePromise.then((response) => {
|
||||||
|
expect(response.headers['content-encoding']).toEqual([
|
||||||
|
{ key: 'Content-Encoding', value: 'gzip' },
|
||||||
|
]);
|
||||||
|
expect(response.body).toEqual('b2stZ3ppcHBlZA=='); // "ok-gzipped" base64 encoded
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('response does not have a body if only statusCode is set', () => {
|
||||||
|
expect.assertions(4);
|
||||||
|
|
||||||
|
const { res, responsePromise } = create({
|
||||||
|
request: {
|
||||||
|
path: '/',
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.statusCode = 204;
|
||||||
|
res.end();
|
||||||
|
|
||||||
|
return responsePromise.then((response) => {
|
||||||
|
expect(response.body).not.toBeDefined();
|
||||||
|
expect(response.bodyEncoding).not.toBeDefined();
|
||||||
|
expect(response.status).toEqual(204);
|
||||||
|
expect(response.statusDescription).toEqual('No Content');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
12
packages/lambda-at-edge-compat/next-aws-cloudfront.d.ts
vendored
Normal file
12
packages/lambda-at-edge-compat/next-aws-cloudfront.d.ts
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { CloudFrontResultResponse, CloudFrontRequest } from 'aws-lambda';
|
||||||
|
import { IncomingMessage, ServerResponse } from 'http';
|
||||||
|
|
||||||
|
declare function lambdaAtEdgeCompat(event: {
|
||||||
|
request: CloudFrontRequest;
|
||||||
|
}): {
|
||||||
|
responsePromise: Promise<CloudFrontResultResponse>;
|
||||||
|
req: IncomingMessage;
|
||||||
|
res: ServerResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default lambdaAtEdgeCompat;
|
||||||
270
packages/lambda-at-edge-compat/next-aws-cloudfront.js
Normal file
270
packages/lambda-at-edge-compat/next-aws-cloudfront.js
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
const Stream = require('stream');
|
||||||
|
const zlib = require('zlib');
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
const specialNodeHeaders = [
|
||||||
|
'age',
|
||||||
|
'authorization',
|
||||||
|
'content-length',
|
||||||
|
'content-type',
|
||||||
|
'etag',
|
||||||
|
'expires',
|
||||||
|
'from',
|
||||||
|
'host',
|
||||||
|
'if-modified-since',
|
||||||
|
'if-unmodified-since',
|
||||||
|
'last-modified',
|
||||||
|
'location',
|
||||||
|
'max-forwards',
|
||||||
|
'proxy-authorization',
|
||||||
|
'referer',
|
||||||
|
'retry-after',
|
||||||
|
'user-agent',
|
||||||
|
];
|
||||||
|
|
||||||
|
const readOnlyCloudFrontHeaders = {
|
||||||
|
'accept-encoding': true,
|
||||||
|
'content-length': true,
|
||||||
|
'if-modified-since': true,
|
||||||
|
'if-none-match': true,
|
||||||
|
'if-range': true,
|
||||||
|
'if-unmodified-since': true,
|
||||||
|
'transfer-encoding': true,
|
||||||
|
via: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const HttpStatusCodes = {
|
||||||
|
202: 'Accepted',
|
||||||
|
502: 'Bad Gateway',
|
||||||
|
400: 'Bad Request',
|
||||||
|
409: 'Conflict',
|
||||||
|
100: 'Continue',
|
||||||
|
201: 'Created',
|
||||||
|
417: 'Expectation Failed',
|
||||||
|
424: 'Failed Dependency',
|
||||||
|
403: 'Forbidden',
|
||||||
|
504: 'Gateway Timeout',
|
||||||
|
410: 'Gone',
|
||||||
|
505: 'HTTP Version Not Supported',
|
||||||
|
418: "I'm a teapot",
|
||||||
|
419: 'Insufficient Space on Resource',
|
||||||
|
507: 'Insufficient Storage',
|
||||||
|
500: 'Server Error',
|
||||||
|
411: 'Length Required',
|
||||||
|
423: 'Locked',
|
||||||
|
420: 'Method Failure',
|
||||||
|
405: 'Method Not Allowed',
|
||||||
|
301: 'Moved Permanently',
|
||||||
|
302: 'Moved Temporarily',
|
||||||
|
207: 'Multi-Status',
|
||||||
|
300: 'Multiple Choices',
|
||||||
|
511: 'Network Authentication Required',
|
||||||
|
204: 'No Content',
|
||||||
|
203: 'Non Authoritative Information',
|
||||||
|
406: 'Not Acceptable',
|
||||||
|
404: 'Not Found',
|
||||||
|
501: 'Not Implemented',
|
||||||
|
304: 'Not Modified',
|
||||||
|
200: 'OK',
|
||||||
|
206: 'Partial Content',
|
||||||
|
402: 'Payment Required',
|
||||||
|
308: 'Permanent Redirect',
|
||||||
|
412: 'Precondition Failed',
|
||||||
|
428: 'Precondition Required',
|
||||||
|
102: 'Processing',
|
||||||
|
407: 'Proxy Authentication Required',
|
||||||
|
431: 'Request Header Fields Too Large',
|
||||||
|
408: 'Request Timeout',
|
||||||
|
413: 'Request Entity Too Large',
|
||||||
|
414: 'Request-URI Too Long',
|
||||||
|
416: 'Requested Range Not Satisfiable',
|
||||||
|
205: 'Reset Content',
|
||||||
|
303: 'See Other',
|
||||||
|
503: 'Service Unavailable',
|
||||||
|
101: 'Switching Protocols',
|
||||||
|
307: 'Temporary Redirect',
|
||||||
|
429: 'Too Many Requests',
|
||||||
|
401: 'Unauthorized',
|
||||||
|
422: 'Unprocessable Entity',
|
||||||
|
415: 'Unsupported Media Type',
|
||||||
|
305: 'Use Proxy',
|
||||||
|
};
|
||||||
|
|
||||||
|
const toCloudFrontHeaders = (headers) => {
|
||||||
|
const result = {};
|
||||||
|
|
||||||
|
Object.keys(headers).forEach((headerName) => {
|
||||||
|
const lowerCaseHeaderName = headerName.toLowerCase();
|
||||||
|
const headerValue = headers[headerName];
|
||||||
|
|
||||||
|
if (readOnlyCloudFrontHeaders[lowerCaseHeaderName]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result[lowerCaseHeaderName] = [];
|
||||||
|
|
||||||
|
if (headerValue instanceof Array) {
|
||||||
|
headerValue.forEach((val) => {
|
||||||
|
result[lowerCaseHeaderName].push({
|
||||||
|
key: headerName,
|
||||||
|
value: val.toString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result[lowerCaseHeaderName].push({
|
||||||
|
key: headerName,
|
||||||
|
value: headerValue.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isGzipSupported = (headers) => {
|
||||||
|
let gz = false;
|
||||||
|
const ae = headers['accept-encoding'];
|
||||||
|
if (ae) {
|
||||||
|
for (let i = 0; i < ae.length; i++) {
|
||||||
|
const { value } = ae[i];
|
||||||
|
const bits = value.split(',').map((x) => x.split(';')[0].trim());
|
||||||
|
if (bits.indexOf('gzip') !== -1) {
|
||||||
|
gz = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return gz;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = (event) => {
|
||||||
|
const { request: cfRequest } = event;
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
headers: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const newStream = new Stream.Readable();
|
||||||
|
const req = Object.assign(newStream, http.IncomingMessage.prototype);
|
||||||
|
req.url = cfRequest.uri;
|
||||||
|
req.method = cfRequest.method;
|
||||||
|
req.rawHeaders = [];
|
||||||
|
req.headers = {};
|
||||||
|
req.connection = {};
|
||||||
|
|
||||||
|
if (cfRequest.querystring) {
|
||||||
|
req.url = req.url + `?` + cfRequest.querystring;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = cfRequest.headers || {};
|
||||||
|
|
||||||
|
for (const lowercaseKey of Object.keys(headers)) {
|
||||||
|
const headerKeyValPairs = headers[lowercaseKey];
|
||||||
|
|
||||||
|
headerKeyValPairs.forEach((keyVal) => {
|
||||||
|
req.rawHeaders.push(keyVal.key);
|
||||||
|
req.rawHeaders.push(keyVal.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.headers[lowercaseKey] = headerKeyValPairs[0].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
req.getHeader = (name) => {
|
||||||
|
return req.headers[name.toLowerCase()];
|
||||||
|
};
|
||||||
|
|
||||||
|
req.getHeaders = () => {
|
||||||
|
return req.headers;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cfRequest.body && cfRequest.body.data) {
|
||||||
|
req.push(cfRequest.body.data, cfRequest.body.encoding ? 'base64' : undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.push(null);
|
||||||
|
|
||||||
|
const res = new Stream();
|
||||||
|
res.finished = false;
|
||||||
|
|
||||||
|
Object.defineProperty(res, 'statusCode', {
|
||||||
|
get() {
|
||||||
|
return response.status;
|
||||||
|
},
|
||||||
|
set(statusCode) {
|
||||||
|
response.status = statusCode;
|
||||||
|
response.statusDescription = HttpStatusCodes[statusCode];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.headers = {};
|
||||||
|
res.writeHead = (status, headers) => {
|
||||||
|
response.status = status;
|
||||||
|
|
||||||
|
if (headers) {
|
||||||
|
res.headers = Object.assign(res.headers, headers);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
res.write = (chunk) => {
|
||||||
|
if (!response.body) {
|
||||||
|
response.body = Buffer.from('');
|
||||||
|
}
|
||||||
|
|
||||||
|
response.body = Buffer.concat([
|
||||||
|
response.body,
|
||||||
|
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
let gz = isGzipSupported(headers);
|
||||||
|
|
||||||
|
const responsePromise = new Promise((resolve) => {
|
||||||
|
res.end = (text) => {
|
||||||
|
if (text) res.write(text);
|
||||||
|
|
||||||
|
if (!res.statusCode) {
|
||||||
|
res.statusCode = 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.finished = true;
|
||||||
|
|
||||||
|
if (response.body) {
|
||||||
|
response.bodyEncoding = 'base64';
|
||||||
|
response.body = gz
|
||||||
|
? zlib.gzipSync(response.body).toString('base64')
|
||||||
|
: Buffer.from(response.body).toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
response.headers = toCloudFrontHeaders(res.headers);
|
||||||
|
|
||||||
|
if (gz) {
|
||||||
|
response.headers['content-encoding'] = [{ key: 'Content-Encoding', value: 'gzip' }];
|
||||||
|
}
|
||||||
|
resolve(response);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setHeader = (name, value) => {
|
||||||
|
res.headers[name.toLowerCase()] = value;
|
||||||
|
};
|
||||||
|
res.removeHeader = (name) => {
|
||||||
|
delete res.headers[name.toLowerCase()];
|
||||||
|
};
|
||||||
|
res.getHeader = (name) => {
|
||||||
|
return res.headers[name.toLowerCase()];
|
||||||
|
};
|
||||||
|
res.getHeaders = () => {
|
||||||
|
return res.headers;
|
||||||
|
};
|
||||||
|
res.hasHeader = (name) => {
|
||||||
|
return !!res.getHeader(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
responsePromise,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
handler.SPECIAL_NODE_HEADERS = specialNodeHeaders;
|
||||||
|
|
||||||
|
module.exports = handler;
|
||||||
10
packages/lambda-at-edge-compat/package.json
Normal file
10
packages/lambda-at-edge-compat/package.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"name": "lambda-at-edge-compat",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "next-aws-cloudfront.js",
|
||||||
|
"types": "next-aws-cloudfront.d.ts",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/aws-lambda": "^8.10.57"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
packages/lambda-at-edge-compat/yarn.lock
Normal file
8
packages/lambda-at-edge-compat/yarn.lock
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@types/aws-lambda@^8.10.57":
|
||||||
|
version "8.10.57"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.57.tgz#39ca3bbe52c5b4e27dc83c2a3b5ed434834568b9"
|
||||||
|
integrity sha512-LMOA9bJLerYoe2KvzHugfaLTa0jUPWrqwxq5VUZ/ZuAMKLJm6oNdCio38vw8jWEIAkPR3P6mBIwnU1DPgelAKg==
|
||||||
1
packages/lambda-at-edge/__mocks__/execa.ts
Normal file
1
packages/lambda-at-edge/__mocks__/execa.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export default jest.fn();
|
||||||
26
packages/lambda-at-edge/package.json
Normal file
26
packages/lambda-at-edge/package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "lambda-at-edge",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"prepare": "yarn build",
|
||||||
|
"build": "tsc -p tsconfig.build.json"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/aws-lambda": "^8.10.57",
|
||||||
|
"@types/execa": "^2.0.0",
|
||||||
|
"@types/fs-extra": "^9.0.1",
|
||||||
|
"@types/node": "^14.0.14",
|
||||||
|
"@types/path-to-regexp": "^1.7.0",
|
||||||
|
"ts-loader": "^7.0.5",
|
||||||
|
"typescript": "^3.9.6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@zeit/node-file-trace": "^0.6.5",
|
||||||
|
"execa": "^4.0.2",
|
||||||
|
"fs-extra": "^9.0.1",
|
||||||
|
"path-to-regexp": "^6.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
55
packages/lambda-at-edge/src/api-handler.ts
Normal file
55
packages/lambda-at-edge/src/api-handler.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
// @ts-ignore
|
||||||
|
import manifest from './manifest.json';
|
||||||
|
import cloudFrontCompat from 'next-aws-cloudfront';
|
||||||
|
import { OriginRequestApiHandlerManifest, OriginRequestEvent } from '../types';
|
||||||
|
import { CloudFrontResultResponse, CloudFrontRequest } from 'aws-lambda';
|
||||||
|
|
||||||
|
const normaliseUri = (uri: string): string => (uri === '/' ? '/index' : uri);
|
||||||
|
|
||||||
|
const router = (manifest: OriginRequestApiHandlerManifest): ((path: string) => string | null) => {
|
||||||
|
const {
|
||||||
|
apis: { dynamic, nonDynamic },
|
||||||
|
} = manifest;
|
||||||
|
|
||||||
|
return (path: string): string | null => {
|
||||||
|
if (nonDynamic[path]) {
|
||||||
|
return nonDynamic[path];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const route in dynamic) {
|
||||||
|
const { file, regex } = dynamic[route];
|
||||||
|
|
||||||
|
const re = new RegExp(regex, 'i');
|
||||||
|
const pathMatchesRoute = re.test(path);
|
||||||
|
|
||||||
|
if (pathMatchesRoute) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handler = async (
|
||||||
|
event: OriginRequestEvent
|
||||||
|
): Promise<CloudFrontResultResponse | CloudFrontRequest> => {
|
||||||
|
const request = event.Records[0].cf.request;
|
||||||
|
const uri = normaliseUri(request.uri);
|
||||||
|
|
||||||
|
const pagePath = router(manifest)(uri);
|
||||||
|
|
||||||
|
if (!pagePath) {
|
||||||
|
return {
|
||||||
|
status: '404',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const page = require(`./${pagePath}`);
|
||||||
|
const { req, res, responsePromise } = cloudFrontCompat(event.Records[0].cf);
|
||||||
|
|
||||||
|
page.default(req, res);
|
||||||
|
|
||||||
|
return responsePromise;
|
||||||
|
};
|
||||||
132
packages/lambda-at-edge/src/default-handler.ts
Normal file
132
packages/lambda-at-edge/src/default-handler.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
// @ts-ignore
|
||||||
|
import PrerenderManifest from './prerender-manifest.json';
|
||||||
|
// @ts-ignore
|
||||||
|
import Manifest from './manifest.json';
|
||||||
|
import lambdaAtEdgeCompat from 'next-aws-cloudfront';
|
||||||
|
import {
|
||||||
|
CloudFrontRequest,
|
||||||
|
CloudFrontS3Origin,
|
||||||
|
CloudFrontOrigin,
|
||||||
|
CloudFrontResultResponse,
|
||||||
|
} from 'aws-lambda';
|
||||||
|
import {
|
||||||
|
OriginRequestEvent,
|
||||||
|
OriginRequestDefaultHandlerManifest,
|
||||||
|
PreRenderedManifest as PrerenderManifestType,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
const addS3HostHeader = (req: CloudFrontRequest, s3DomainName: string): void => {
|
||||||
|
req.headers['host'] = [{ key: 'host', value: s3DomainName }];
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDataRequest = (uri: string): boolean => uri.startsWith('/_next/data');
|
||||||
|
|
||||||
|
const normaliseUri = (uri: string): string => (uri === '/' ? '/index' : uri);
|
||||||
|
|
||||||
|
const normaliseS3OriginDomain = (s3Origin: CloudFrontS3Origin): string => {
|
||||||
|
if (s3Origin.region === 'us-east-1') {
|
||||||
|
return s3Origin.domainName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!s3Origin.domainName.includes(s3Origin.region)) {
|
||||||
|
const regionalEndpoint = s3Origin.domainName.replace(
|
||||||
|
's3.amazonaws.com',
|
||||||
|
`s3.${s3Origin.region}.amazonaws.com`
|
||||||
|
);
|
||||||
|
return regionalEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s3Origin.domainName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = (manifest: OriginRequestDefaultHandlerManifest): ((uri: string) => string) => {
|
||||||
|
const {
|
||||||
|
pages: { ssr, html },
|
||||||
|
} = manifest;
|
||||||
|
|
||||||
|
const allDynamicRoutes = { ...ssr.dynamic, ...html.dynamic };
|
||||||
|
|
||||||
|
return (uri: string): string => {
|
||||||
|
let normalizedUri = uri;
|
||||||
|
|
||||||
|
if (isDataRequest(uri)) {
|
||||||
|
normalizedUri = uri.replace(`/_next/data/${manifest.buildId}`, '').replace('.json', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ssr.nonDynamic[normalizedUri]) {
|
||||||
|
return ssr.nonDynamic[normalizedUri];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const route in allDynamicRoutes) {
|
||||||
|
const { file, regex } = allDynamicRoutes[route];
|
||||||
|
|
||||||
|
const re = new RegExp(regex, 'i');
|
||||||
|
const pathMatchesRoute = re.test(normalizedUri);
|
||||||
|
|
||||||
|
if (pathMatchesRoute) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// only use the 404 page if the project exports it
|
||||||
|
if (html.nonDynamic['/404'] !== undefined) {
|
||||||
|
return 'pages/404.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'pages/_error.js';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handler = async (
|
||||||
|
event: OriginRequestEvent
|
||||||
|
): Promise<CloudFrontResultResponse | CloudFrontRequest> => {
|
||||||
|
const request = event.Records[0].cf.request;
|
||||||
|
const uri = normaliseUri(request.uri);
|
||||||
|
const manifest: OriginRequestDefaultHandlerManifest = Manifest;
|
||||||
|
const prerenderManifest: PrerenderManifestType = PrerenderManifest;
|
||||||
|
const { pages, publicFiles } = manifest;
|
||||||
|
const isStaticPage = pages.html.nonDynamic[uri];
|
||||||
|
const isPublicFile = publicFiles[uri];
|
||||||
|
const isPrerenderedPage = prerenderManifest.routes[request.uri]; // prerendered pages are also static pages like "pages.html" above, but are defined in the prerender-manifest
|
||||||
|
const origin = request.origin as CloudFrontOrigin;
|
||||||
|
const s3Origin = origin.s3 as CloudFrontS3Origin;
|
||||||
|
const isHTMLPage = isStaticPage || isPrerenderedPage;
|
||||||
|
const normalizedS3DomainName = normaliseS3OriginDomain(s3Origin);
|
||||||
|
|
||||||
|
s3Origin.domainName = normalizedS3DomainName;
|
||||||
|
|
||||||
|
if (isHTMLPage || isPublicFile) {
|
||||||
|
s3Origin.path = isHTMLPage ? '/static-pages' : '/public';
|
||||||
|
|
||||||
|
if (isHTMLPage) {
|
||||||
|
addS3HostHeader(request, normalizedS3DomainName);
|
||||||
|
request.uri = `${uri}.html`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pagePath = router(manifest)(uri);
|
||||||
|
|
||||||
|
if (pagePath.endsWith('.html')) {
|
||||||
|
s3Origin.path = '/static-pages';
|
||||||
|
request.uri = pagePath.replace('pages', '');
|
||||||
|
addS3HostHeader(request, normalizedS3DomainName);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const page = require(`./${pagePath}`);
|
||||||
|
|
||||||
|
const { req, res, responsePromise } = lambdaAtEdgeCompat(event.Records[0].cf);
|
||||||
|
|
||||||
|
if (isDataRequest(uri)) {
|
||||||
|
const { renderOpts } = await page.renderReqToHTML(req, res, 'passthrough');
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.end(JSON.stringify(renderOpts.pageData));
|
||||||
|
} else {
|
||||||
|
page.render(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
return responsePromise;
|
||||||
|
};
|
||||||
353
packages/lambda-at-edge/src/index.ts
Normal file
353
packages/lambda-at-edge/src/index.ts
Normal file
|
|
@ -0,0 +1,353 @@
|
||||||
|
import nodeFileTrace, { NodeFileTraceReasons } from '@zeit/node-file-trace';
|
||||||
|
import execa from 'execa';
|
||||||
|
import fse from 'fs-extra';
|
||||||
|
import path, { join } from 'path';
|
||||||
|
|
||||||
|
import getAllFiles from './lib/getAllFilesInDirectory';
|
||||||
|
import { getSortedRoutes } from './lib/sortedRoutes';
|
||||||
|
import { OriginRequestDefaultHandlerManifest, OriginRequestApiHandlerManifest } from '../types';
|
||||||
|
import isDynamicRoute from './lib/isDynamicRoute';
|
||||||
|
import pathToPosix from './lib/pathToPosix';
|
||||||
|
import expressifyDynamicRoute from './lib/expressifyDynamicRoute';
|
||||||
|
import pathToRegexStr from './lib/pathToRegexStr';
|
||||||
|
import normalizeNodeModules from './lib/normalizeNodeModules';
|
||||||
|
import createServerlessConfig from './lib/createServerlessConfig';
|
||||||
|
|
||||||
|
export const DEFAULT_LAMBDA_CODE_DIR = 'default-lambda';
|
||||||
|
export const API_LAMBDA_CODE_DIR = 'api-lambda';
|
||||||
|
|
||||||
|
type BuildOptions = {
|
||||||
|
args?: string[];
|
||||||
|
cwd?: string;
|
||||||
|
cmd?: string;
|
||||||
|
useServerlessTraceTarget?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultBuildOptions = {
|
||||||
|
args: [],
|
||||||
|
cwd: process.cwd(),
|
||||||
|
cmd: './node_modules/.bin/next',
|
||||||
|
useServerlessTraceTarget: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Builder {
|
||||||
|
nextConfigDir: string;
|
||||||
|
dotNextDir: string;
|
||||||
|
serverlessDir: string;
|
||||||
|
outputDir: string;
|
||||||
|
buildOptions: BuildOptions = defaultBuildOptions;
|
||||||
|
|
||||||
|
constructor(nextConfigDir: string, outputDir: string, buildOptions?: BuildOptions) {
|
||||||
|
this.nextConfigDir = path.resolve(nextConfigDir);
|
||||||
|
this.dotNextDir = path.join(this.nextConfigDir, '.next');
|
||||||
|
this.serverlessDir = path.join(this.dotNextDir, 'serverless');
|
||||||
|
this.outputDir = outputDir;
|
||||||
|
if (buildOptions) {
|
||||||
|
this.buildOptions = buildOptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async readPublicFiles(): Promise<string[]> {
|
||||||
|
const dirExists = await fse.pathExists(join(this.nextConfigDir, 'public'));
|
||||||
|
if (dirExists) {
|
||||||
|
return getAllFiles(join(this.nextConfigDir, 'public'))
|
||||||
|
.map((e) => e.replace(this.nextConfigDir, ''))
|
||||||
|
.map((e) => e.split(path.sep).slice(2).join('/'));
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async readPagesManifest(): Promise<{ [key: string]: string }> {
|
||||||
|
const path = join(this.serverlessDir, 'pages-manifest.json');
|
||||||
|
const hasServerlessPageManifest = await fse.pathExists(path);
|
||||||
|
|
||||||
|
if (!hasServerlessPageManifest) {
|
||||||
|
return Promise.reject(
|
||||||
|
"pages-manifest not found. Check if `next.config.js` target is set to 'serverless'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pagesManifest = await fse.readJSON(path);
|
||||||
|
const pagesManifestWithoutDynamicRoutes = Object.keys(pagesManifest).reduce(
|
||||||
|
(acc: { [key: string]: string }, route: string) => {
|
||||||
|
if (isDynamicRoute(route)) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[route] = pagesManifest[route];
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const dynamicRoutedPages = Object.keys(pagesManifest).filter(isDynamicRoute);
|
||||||
|
const sortedDynamicRoutedPages = getSortedRoutes(dynamicRoutedPages);
|
||||||
|
const sortedPagesManifest = pagesManifestWithoutDynamicRoutes;
|
||||||
|
|
||||||
|
sortedDynamicRoutedPages.forEach((route) => {
|
||||||
|
sortedPagesManifest[route] = pagesManifest[route];
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortedPagesManifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
copyLambdaHandlerDependencies(
|
||||||
|
fileList: string[],
|
||||||
|
reasons: NodeFileTraceReasons,
|
||||||
|
handlerDirectory: string
|
||||||
|
): Promise<void>[] {
|
||||||
|
return fileList
|
||||||
|
.filter((file) => {
|
||||||
|
// exclude "initial" files from lambda artifact. These are just the pages themselves
|
||||||
|
// which are copied over separately
|
||||||
|
return !reasons[file] || reasons[file].type !== 'initial';
|
||||||
|
})
|
||||||
|
.map((filePath: string) => {
|
||||||
|
const resolvedFilePath = path.resolve(filePath);
|
||||||
|
const dst = normalizeNodeModules(path.relative(this.serverlessDir, resolvedFilePath));
|
||||||
|
|
||||||
|
return fse.copy(resolvedFilePath, join(this.outputDir, handlerDirectory, dst));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async buildDefaultLambda(buildManifest: OriginRequestDefaultHandlerManifest): Promise<void[]> {
|
||||||
|
let copyTraces: Promise<void>[] = [];
|
||||||
|
|
||||||
|
if (this.buildOptions.useServerlessTraceTarget) {
|
||||||
|
const ignoreAppAndDocumentPages = (page: string): boolean => {
|
||||||
|
const basename = path.basename(page);
|
||||||
|
return basename !== '_app.js' && basename !== '_document.js';
|
||||||
|
};
|
||||||
|
|
||||||
|
const allSsrPages = [
|
||||||
|
...Object.values(buildManifest.pages.ssr.nonDynamic),
|
||||||
|
...Object.values(buildManifest.pages.ssr.dynamic).map((entry) => entry.file),
|
||||||
|
].filter(ignoreAppAndDocumentPages);
|
||||||
|
|
||||||
|
const ssrPages = Object.values(allSsrPages).map((pageFile) =>
|
||||||
|
path.join(this.serverlessDir, pageFile)
|
||||||
|
);
|
||||||
|
|
||||||
|
const { fileList, reasons } = await nodeFileTrace(ssrPages, {
|
||||||
|
base: process.cwd(),
|
||||||
|
});
|
||||||
|
|
||||||
|
copyTraces = this.copyLambdaHandlerDependencies(fileList, reasons, DEFAULT_LAMBDA_CODE_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
...copyTraces,
|
||||||
|
fse.copy(
|
||||||
|
require.resolve('lambda-at-edge/dist/default-handler.js'),
|
||||||
|
join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR, 'index.js')
|
||||||
|
),
|
||||||
|
fse.writeJson(join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR, 'manifest.json'), buildManifest),
|
||||||
|
fse.copy(
|
||||||
|
require.resolve('next-aws-cloudfront'),
|
||||||
|
join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR, 'node_modules/next-aws-cloudfront/index.js')
|
||||||
|
),
|
||||||
|
fse.copy(
|
||||||
|
join(this.serverlessDir, 'pages'),
|
||||||
|
join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR, 'pages'),
|
||||||
|
{
|
||||||
|
filter: (file: string) => {
|
||||||
|
const isNotPrerenderedHTMLPage = path.extname(file) !== '.html';
|
||||||
|
const isNotStaticPropsJSONFile = path.extname(file) !== '.json';
|
||||||
|
const isNotApiPage = pathToPosix(file).indexOf('pages/api') === -1;
|
||||||
|
|
||||||
|
return isNotApiPage && isNotPrerenderedHTMLPage && isNotStaticPropsJSONFile;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
fse.copy(
|
||||||
|
join(this.dotNextDir, 'prerender-manifest.json'),
|
||||||
|
join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR, 'prerender-manifest.json')
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async buildApiLambda(apiBuildManifest: OriginRequestApiHandlerManifest): Promise<void[]> {
|
||||||
|
let copyTraces: Promise<void>[] = [];
|
||||||
|
|
||||||
|
if (this.buildOptions.useServerlessTraceTarget) {
|
||||||
|
const allApiPages = [
|
||||||
|
...Object.values(apiBuildManifest.apis.nonDynamic),
|
||||||
|
...Object.values(apiBuildManifest.apis.dynamic).map((entry) => entry.file),
|
||||||
|
];
|
||||||
|
|
||||||
|
const apiPages = Object.values(allApiPages).map((pageFile) =>
|
||||||
|
path.join(this.serverlessDir, pageFile)
|
||||||
|
);
|
||||||
|
|
||||||
|
const { fileList, reasons } = await nodeFileTrace(apiPages, {
|
||||||
|
base: process.cwd(),
|
||||||
|
});
|
||||||
|
|
||||||
|
copyTraces = this.copyLambdaHandlerDependencies(fileList, reasons, API_LAMBDA_CODE_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
...copyTraces,
|
||||||
|
fse.copy(
|
||||||
|
require.resolve('lambda-at-edge/dist/api-handler.js'),
|
||||||
|
join(this.outputDir, API_LAMBDA_CODE_DIR, 'index.js')
|
||||||
|
),
|
||||||
|
fse.copy(
|
||||||
|
require.resolve('next-aws-cloudfront'),
|
||||||
|
join(this.outputDir, API_LAMBDA_CODE_DIR, 'node_modules/next-aws-cloudfront/index.js')
|
||||||
|
),
|
||||||
|
fse.copy(
|
||||||
|
join(this.serverlessDir, 'pages/api'),
|
||||||
|
join(this.outputDir, API_LAMBDA_CODE_DIR, 'pages/api')
|
||||||
|
),
|
||||||
|
fse.writeJson(join(this.outputDir, API_LAMBDA_CODE_DIR, 'manifest.json'), apiBuildManifest),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async prepareBuildManifests(): Promise<{
|
||||||
|
defaultBuildManifest: OriginRequestDefaultHandlerManifest;
|
||||||
|
apiBuildManifest: OriginRequestApiHandlerManifest;
|
||||||
|
}> {
|
||||||
|
const pagesManifest = await this.readPagesManifest();
|
||||||
|
|
||||||
|
const buildId = await fse.readFile(path.join(this.dotNextDir, 'BUILD_ID'), 'utf-8');
|
||||||
|
const defaultBuildManifest: OriginRequestDefaultHandlerManifest = {
|
||||||
|
buildId,
|
||||||
|
pages: {
|
||||||
|
ssr: {
|
||||||
|
dynamic: {},
|
||||||
|
nonDynamic: {},
|
||||||
|
},
|
||||||
|
html: {
|
||||||
|
dynamic: {},
|
||||||
|
nonDynamic: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
publicFiles: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiBuildManifest: OriginRequestApiHandlerManifest = {
|
||||||
|
apis: {
|
||||||
|
dynamic: {},
|
||||||
|
nonDynamic: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ssrPages = defaultBuildManifest.pages.ssr;
|
||||||
|
const htmlPages = defaultBuildManifest.pages.html;
|
||||||
|
const apiPages = apiBuildManifest.apis;
|
||||||
|
|
||||||
|
const isHtmlPage = (path: string): boolean => path.endsWith('.html');
|
||||||
|
const isApiPage = (path: string): boolean => path.startsWith('pages/api');
|
||||||
|
|
||||||
|
Object.entries(pagesManifest).forEach(([route, pageFile]) => {
|
||||||
|
const dynamicRoute = isDynamicRoute(route);
|
||||||
|
const expressRoute = dynamicRoute ? expressifyDynamicRoute(route) : null;
|
||||||
|
|
||||||
|
if (isHtmlPage(pageFile)) {
|
||||||
|
if (dynamicRoute) {
|
||||||
|
const route = expressRoute as string;
|
||||||
|
htmlPages.dynamic[route] = {
|
||||||
|
file: pageFile,
|
||||||
|
regex: pathToRegexStr(route),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
htmlPages.nonDynamic[route] = pageFile;
|
||||||
|
}
|
||||||
|
} else if (isApiPage(pageFile)) {
|
||||||
|
if (dynamicRoute) {
|
||||||
|
const route = expressRoute as string;
|
||||||
|
apiPages.dynamic[route] = {
|
||||||
|
file: pageFile,
|
||||||
|
regex: pathToRegexStr(route),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
apiPages.nonDynamic[route] = pageFile;
|
||||||
|
}
|
||||||
|
} else if (dynamicRoute) {
|
||||||
|
const route = expressRoute as string;
|
||||||
|
ssrPages.dynamic[route] = {
|
||||||
|
file: pageFile,
|
||||||
|
regex: pathToRegexStr(route),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
ssrPages.nonDynamic[route] = pageFile;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const publicFiles = await this.readPublicFiles();
|
||||||
|
|
||||||
|
publicFiles.forEach((pf) => {
|
||||||
|
defaultBuildManifest.publicFiles['/' + pf] = pf;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
defaultBuildManifest,
|
||||||
|
apiBuildManifest,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanupDotNext(): Promise<void> {
|
||||||
|
const exists = await fse.pathExists(this.dotNextDir);
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
const fileItems = await fse.readdir(this.dotNextDir);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
fileItems
|
||||||
|
.filter(
|
||||||
|
(fileItem) => fileItem !== 'cache' // avoid deleting the cache folder as that would lead to slow builds!
|
||||||
|
)
|
||||||
|
.map((fileItem) => fse.remove(join(this.dotNextDir, fileItem)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async build(debugMode: boolean): Promise<void> {
|
||||||
|
const { cmd, args, cwd, useServerlessTraceTarget } = Object.assign(
|
||||||
|
defaultBuildOptions,
|
||||||
|
this.buildOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.cleanupDotNext();
|
||||||
|
|
||||||
|
await fse.emptyDir(join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR));
|
||||||
|
await fse.emptyDir(join(this.outputDir, API_LAMBDA_CODE_DIR));
|
||||||
|
|
||||||
|
const { restoreUserConfig } = await createServerlessConfig(
|
||||||
|
cwd,
|
||||||
|
path.join(this.nextConfigDir),
|
||||||
|
useServerlessTraceTarget
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const subprocess = execa(cmd, args, {
|
||||||
|
cwd,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (debugMode) {
|
||||||
|
// @ts-ignore
|
||||||
|
subprocess.stdout.pipe(process.stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
await subprocess;
|
||||||
|
} finally {
|
||||||
|
await restoreUserConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { defaultBuildManifest, apiBuildManifest } = await this.prepareBuildManifests();
|
||||||
|
|
||||||
|
await this.buildDefaultLambda(defaultBuildManifest);
|
||||||
|
|
||||||
|
const hasAPIPages =
|
||||||
|
Object.keys(apiBuildManifest.apis.nonDynamic).length > 0 ||
|
||||||
|
Object.keys(apiBuildManifest.apis.dynamic).length > 0;
|
||||||
|
|
||||||
|
if (hasAPIPages) {
|
||||||
|
await this.buildApiLambda(apiBuildManifest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Builder;
|
||||||
89
packages/lambda-at-edge/src/lib/createServerlessConfig.ts
Normal file
89
packages/lambda-at-edge/src/lib/createServerlessConfig.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
function getCustomData(importName: string, target: string): string {
|
||||||
|
return `
|
||||||
|
module.exports = function(...args) {
|
||||||
|
let original = require('./${importName}');
|
||||||
|
const finalConfig = {};
|
||||||
|
const target = { target: '${target}' };
|
||||||
|
if (typeof original === 'function' && original.constructor.name === 'AsyncFunction') {
|
||||||
|
// AsyncFunctions will become promises
|
||||||
|
original = original(...args);
|
||||||
|
}
|
||||||
|
if (original instanceof Promise) {
|
||||||
|
// Special case for promises, as it's currently not supported
|
||||||
|
// and will just error later on
|
||||||
|
return original
|
||||||
|
.then((orignalConfig) => Object.assign(finalConfig, orignalConfig))
|
||||||
|
.then((config) => Object.assign(config, target));
|
||||||
|
} else if (typeof original === 'function') {
|
||||||
|
Object.assign(finalConfig, original(...args));
|
||||||
|
} else if (typeof original === 'object') {
|
||||||
|
Object.assign(finalConfig, original);
|
||||||
|
}
|
||||||
|
Object.assign(finalConfig, target);
|
||||||
|
return finalConfig;
|
||||||
|
}
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultData(target: string): string {
|
||||||
|
return `module.exports = { target: '${target}' };`;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateServerlessConfigResult = {
|
||||||
|
restoreUserConfig: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function createServerlessConfig(
|
||||||
|
workPath: string,
|
||||||
|
entryPath: string,
|
||||||
|
useServerlessTraceTarget: boolean
|
||||||
|
): Promise<CreateServerlessConfigResult> {
|
||||||
|
const target = useServerlessTraceTarget ? 'experimental-serverless-trace' : 'serverless';
|
||||||
|
|
||||||
|
const primaryConfigPath = path.join(entryPath, 'next.config.js');
|
||||||
|
const secondaryConfigPath = path.join(workPath, 'next.config.js');
|
||||||
|
const backupConfigName = `next.config.original.${Date.now()}.js`;
|
||||||
|
|
||||||
|
const hasPrimaryConfig = fs.existsSync(primaryConfigPath);
|
||||||
|
const hasSecondaryConfig = fs.existsSync(secondaryConfigPath);
|
||||||
|
|
||||||
|
let configPath: string;
|
||||||
|
let backupConfigPath: string;
|
||||||
|
|
||||||
|
if (hasPrimaryConfig) {
|
||||||
|
// Prefer primary path
|
||||||
|
configPath = primaryConfigPath;
|
||||||
|
backupConfigPath = path.join(entryPath, backupConfigName);
|
||||||
|
} else if (hasSecondaryConfig) {
|
||||||
|
// Work with secondary path (some monorepo setups)
|
||||||
|
configPath = secondaryConfigPath;
|
||||||
|
backupConfigPath = path.join(workPath, backupConfigName);
|
||||||
|
} else {
|
||||||
|
// Default to primary path for creation
|
||||||
|
configPath = primaryConfigPath;
|
||||||
|
backupConfigPath = path.join(entryPath, backupConfigName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const configPathExists = fs.existsSync(configPath);
|
||||||
|
|
||||||
|
if (configPathExists) {
|
||||||
|
await fs.rename(configPath, backupConfigPath);
|
||||||
|
await fs.writeFile(configPath, getCustomData(backupConfigName, target));
|
||||||
|
} else {
|
||||||
|
await fs.writeFile(configPath, getDefaultData(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
restoreUserConfig: async (): Promise<void> => {
|
||||||
|
const needToRestoreUserConfig = configPathExists;
|
||||||
|
await fs.remove(configPath);
|
||||||
|
|
||||||
|
if (needToRestoreUserConfig) {
|
||||||
|
await fs.rename(backupConfigPath, configPath);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
12
packages/lambda-at-edge/src/lib/expressifyDynamicRoute.ts
Normal file
12
packages/lambda-at-edge/src/lib/expressifyDynamicRoute.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// converts a nextjs dynamic route /[param]/ -> /:param
|
||||||
|
// also handles catch all routes /[...param]/ -> /:param*
|
||||||
|
|
||||||
|
const expressifyDynamicRoute = (dynamicRoute: string): string => {
|
||||||
|
// replace any catch all group first
|
||||||
|
const expressified = dynamicRoute.replace(/\[\.\.\.(.*)]$/, ':$1*');
|
||||||
|
|
||||||
|
// now replace other dynamic route groups
|
||||||
|
return expressified.replace(/\[(.*?)]/g, ':$1');
|
||||||
|
};
|
||||||
|
|
||||||
|
export default expressifyDynamicRoute;
|
||||||
22
packages/lambda-at-edge/src/lib/getAllFilesInDirectory.ts
Normal file
22
packages/lambda-at-edge/src/lib/getAllFilesInDirectory.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const getAllFilesRecursively = (dirPath: string, arrayOfFiles: string[]): string[] => {
|
||||||
|
const files = fs.readdirSync(dirPath);
|
||||||
|
|
||||||
|
files.forEach(function (file) {
|
||||||
|
if (fs.statSync(dirPath + path.sep + file).isDirectory()) {
|
||||||
|
arrayOfFiles = getAllFilesRecursively(path.join(dirPath, file), arrayOfFiles);
|
||||||
|
} else {
|
||||||
|
arrayOfFiles.push(path.join(dirPath, file));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return arrayOfFiles;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAllFiles = (dirPath: string): string[] => {
|
||||||
|
return getAllFilesRecursively(dirPath, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getAllFiles;
|
||||||
6
packages/lambda-at-edge/src/lib/isDynamicRoute.ts
Normal file
6
packages/lambda-at-edge/src/lib/isDynamicRoute.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
const isDynamicRoute = (route: string): boolean => {
|
||||||
|
// Identify /[param]/ in route string
|
||||||
|
return /\/\[[^\/]+?\](?=\/|$)/.test(route);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default isDynamicRoute;
|
||||||
8
packages/lambda-at-edge/src/lib/normalizeNodeModules.ts
Normal file
8
packages/lambda-at-edge/src/lib/normalizeNodeModules.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
// removes parent paths of node_modules dir
|
||||||
|
// ../../node_modules/module/file.js -> node_modules/module/file.js
|
||||||
|
|
||||||
|
const normalizeNodeModules = (path: string): string => {
|
||||||
|
return path.substring(path.indexOf('node_modules'));
|
||||||
|
};
|
||||||
|
|
||||||
|
export default normalizeNodeModules;
|
||||||
2
packages/lambda-at-edge/src/lib/pathToPosix.ts
Normal file
2
packages/lambda-at-edge/src/lib/pathToPosix.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
const pathToPosix = (path: string): string => path.replace(/\\/g, '/');
|
||||||
|
export default pathToPosix;
|
||||||
6
packages/lambda-at-edge/src/lib/pathToRegexStr.ts
Normal file
6
packages/lambda-at-edge/src/lib/pathToRegexStr.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { pathToRegexp } from 'path-to-regexp';
|
||||||
|
|
||||||
|
export default (path: string): string =>
|
||||||
|
pathToRegexp(path)
|
||||||
|
.toString()
|
||||||
|
.replace(/\/(.*)\/\i/, '$1');
|
||||||
139
packages/lambda-at-edge/src/lib/sortedRoutes.ts
Normal file
139
packages/lambda-at-edge/src/lib/sortedRoutes.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
// copied as is from https://github.com/zeit/next.js/blob/canary/packages/next/next-server/lib/router/utils/sorted-routes.ts
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
class UrlNode {
|
||||||
|
placeholder: boolean = true;
|
||||||
|
children: Map<string, UrlNode> = new Map();
|
||||||
|
slugName: string | null = null;
|
||||||
|
restSlugName: string | null = null;
|
||||||
|
|
||||||
|
insert(urlPath: string): void {
|
||||||
|
this._insert(urlPath.split('/').filter(Boolean), [], false);
|
||||||
|
}
|
||||||
|
|
||||||
|
smoosh(): string[] {
|
||||||
|
return this._smoosh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _smoosh(prefix: string = '/'): string[] {
|
||||||
|
const childrenPaths = [...this.children.keys()].sort();
|
||||||
|
if (this.slugName !== null) {
|
||||||
|
childrenPaths.splice(childrenPaths.indexOf('[]'), 1);
|
||||||
|
}
|
||||||
|
if (this.restSlugName !== null) {
|
||||||
|
childrenPaths.splice(childrenPaths.indexOf('[...]'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes = childrenPaths
|
||||||
|
.map((c) => this.children.get(c)!._smoosh(`${prefix}${c}/`))
|
||||||
|
.reduce((prev, curr) => [...prev, ...curr], []);
|
||||||
|
|
||||||
|
if (this.slugName !== null) {
|
||||||
|
routes.push(...this.children.get('[]')!._smoosh(`${prefix}[${this.slugName}]/`));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.placeholder) {
|
||||||
|
routes.unshift(prefix === '/' ? '/' : prefix.slice(0, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.restSlugName !== null) {
|
||||||
|
routes.push(...this.children.get('[...]')!._smoosh(`${prefix}[...${this.restSlugName}]/`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _insert(urlPaths: string[], slugNames: string[], isCatchAll: boolean): void {
|
||||||
|
if (urlPaths.length === 0) {
|
||||||
|
this.placeholder = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCatchAll) {
|
||||||
|
throw new Error(`Catch-all must be the last part of the URL.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The next segment in the urlPaths list
|
||||||
|
let nextSegment = urlPaths[0];
|
||||||
|
|
||||||
|
// Check if the segment matches `[something]`
|
||||||
|
if (nextSegment.startsWith('[') && nextSegment.endsWith(']')) {
|
||||||
|
// Strip `[` and `]`, leaving only `something`
|
||||||
|
let segmentName = nextSegment.slice(1, -1);
|
||||||
|
if (segmentName.startsWith('...')) {
|
||||||
|
segmentName = segmentName.substring(3);
|
||||||
|
isCatchAll = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segmentName.startsWith('.')) {
|
||||||
|
throw new Error(`Segment names may not start with erroneous periods ('${segmentName}').`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSlug(previousSlug: string | null, nextSlug: string) {
|
||||||
|
if (previousSlug !== null) {
|
||||||
|
// If the specific segment already has a slug but the slug is not `something`
|
||||||
|
// This prevents collisions like:
|
||||||
|
// pages/[post]/index.js
|
||||||
|
// pages/[id]/index.js
|
||||||
|
// Because currently multiple dynamic params on the same segment level are not supported
|
||||||
|
if (previousSlug !== nextSlug) {
|
||||||
|
// TODO: This error seems to be confusing for users, needs an err.sh link, the description can be based on above comment.
|
||||||
|
throw new Error(
|
||||||
|
`You cannot use different slug names for the same dynamic path ('${previousSlug}' !== '${nextSlug}').`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slugNames.indexOf(nextSlug) !== -1) {
|
||||||
|
throw new Error(
|
||||||
|
`You cannot have the same slug name "${nextSlug}" repeat within a single dynamic path`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
slugNames.push(nextSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCatchAll) {
|
||||||
|
handleSlug(this.restSlugName, segmentName);
|
||||||
|
// slugName is kept as it can only be one particular slugName
|
||||||
|
this.restSlugName = segmentName;
|
||||||
|
// nextSegment is overwritten to [] so that it can later be sorted specifically
|
||||||
|
nextSegment = '[...]';
|
||||||
|
} else {
|
||||||
|
handleSlug(this.slugName, segmentName);
|
||||||
|
// slugName is kept as it can only be one particular slugName
|
||||||
|
this.slugName = segmentName;
|
||||||
|
// nextSegment is overwritten to [] so that it can later be sorted specifically
|
||||||
|
nextSegment = '[]';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this UrlNode doesn't have the nextSegment yet we create a new child UrlNode
|
||||||
|
if (!this.children.has(nextSegment)) {
|
||||||
|
this.children.set(nextSegment, new UrlNode());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.children.get(nextSegment)!._insert(urlPaths.slice(1), slugNames, isCatchAll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSortedRoutes(normalizedPages: string[]): string[] {
|
||||||
|
// First the UrlNode is created, and every UrlNode can have only 1 dynamic segment
|
||||||
|
// Eg you can't have pages/[post]/abc.js and pages/[hello]/something-else.js
|
||||||
|
// Only 1 dynamic segment per nesting level
|
||||||
|
|
||||||
|
// So in the case that is test/integration/dynamic-routing it'll be this:
|
||||||
|
// pages/[post]/comments.js
|
||||||
|
// pages/blog/[post]/comment/[id].js
|
||||||
|
// Both are fine because `pages/[post]` and `pages/blog` are on the same level
|
||||||
|
// So in this case `UrlNode` created here has `this.slugName === 'post'`
|
||||||
|
// And since your PR passed through `slugName` as an array basically it'd including it in too many possibilities
|
||||||
|
// Instead what has to be passed through is the upwards path's dynamic names
|
||||||
|
const root = new UrlNode();
|
||||||
|
|
||||||
|
// Here the `root` gets injected multiple paths, and insert will break them up into sublevels
|
||||||
|
normalizedPages.forEach((pagePath) => root.insert(pagePath));
|
||||||
|
// Smoosh will then sort those sublevels up to the point where you get the correct route definition priority
|
||||||
|
return root.smoosh();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"apis": {
|
||||||
|
"dynamic": {},
|
||||||
|
"nonDynamic": {
|
||||||
|
"/api/getCustomers": "pages/api/getCustomers.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { createCloudFrontEvent } from '../test-utils';
|
||||||
|
import { handler } from '../../src/api-handler';
|
||||||
|
import { CloudFrontResponseResult } from 'next-aws-cloudfront/node_modules/@types/aws-lambda';
|
||||||
|
|
||||||
|
jest.mock('../../src/manifest.json', () => require('./api-build-manifest.json'), {
|
||||||
|
virtual: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockPageRequire = (mockPagePath: string): void => {
|
||||||
|
jest.mock(
|
||||||
|
`../../src/${mockPagePath}`,
|
||||||
|
() => require(`../shared-fixtures/built-artifact/${mockPagePath}`),
|
||||||
|
{
|
||||||
|
virtual: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('API lambda handler', () => {
|
||||||
|
it('serves api request', async () => {
|
||||||
|
const event = createCloudFrontEvent({
|
||||||
|
uri: '/api/getCustomers',
|
||||||
|
host: 'mydistribution.cloudfront.net',
|
||||||
|
origin: {
|
||||||
|
s3: {
|
||||||
|
domainName: 'my-bucket.s3.amazonaws.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mockPageRequire('pages/api/getCustomers.js');
|
||||||
|
|
||||||
|
const response = (await handler(event)) as CloudFrontResponseResult;
|
||||||
|
|
||||||
|
const decodedBody = new Buffer(response.body, 'base64').toString('utf8');
|
||||||
|
|
||||||
|
expect(decodedBody).toEqual('pages/api/getCustomers');
|
||||||
|
expect(response.status).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 404 for not-found api routes', async () => {
|
||||||
|
const event = createCloudFrontEvent({
|
||||||
|
uri: '/foo/bar',
|
||||||
|
host: 'mydistribution.cloudfront.net',
|
||||||
|
origin: {
|
||||||
|
s3: {
|
||||||
|
domainName: 'my-bucket.s3.amazonaws.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mockPageRequire('pages/api/getCustomers.js');
|
||||||
|
|
||||||
|
const response = (await handler(event)) as CloudFrontResponseResult;
|
||||||
|
|
||||||
|
expect(response.status).toEqual('404');
|
||||||
|
});
|
||||||
|
});
|
||||||
200
packages/lambda-at-edge/tests/build/build.test.ts
Normal file
200
packages/lambda-at-edge/tests/build/build.test.ts
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
import { join } from 'path';
|
||||||
|
import fse from 'fs-extra';
|
||||||
|
import execa from 'execa';
|
||||||
|
import Builder from '../../src/build';
|
||||||
|
import { DEFAULT_LAMBDA_CODE_DIR, API_LAMBDA_CODE_DIR } from '../../src/build';
|
||||||
|
import { cleanupDir, removeNewLineChars } from '../test-utils';
|
||||||
|
import { OriginRequestDefaultHandlerManifest, OriginRequestApiHandlerManifest } from '../../types';
|
||||||
|
|
||||||
|
jest.mock('execa');
|
||||||
|
|
||||||
|
describe('Builder Tests', () => {
|
||||||
|
let fseRemoveSpy: jest.SpyInstance;
|
||||||
|
let fseEmptyDirSpy: jest.SpyInstance;
|
||||||
|
let defaultBuildManifest: OriginRequestDefaultHandlerManifest;
|
||||||
|
let apiBuildManifest: OriginRequestApiHandlerManifest;
|
||||||
|
|
||||||
|
const fixturePath = join(__dirname, './simple-app-fixture');
|
||||||
|
const outputDir = join(fixturePath, '.test_sls_next_output');
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const mockExeca = execa as jest.Mock;
|
||||||
|
mockExeca.mockResolvedValueOnce();
|
||||||
|
|
||||||
|
fseRemoveSpy = jest.spyOn(fse, 'remove').mockImplementation(() => {
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
fseEmptyDirSpy = jest.spyOn(fse, 'emptyDir');
|
||||||
|
|
||||||
|
const builder = new Builder(fixturePath, outputDir);
|
||||||
|
await builder.build();
|
||||||
|
|
||||||
|
defaultBuildManifest = await fse.readJSON(
|
||||||
|
join(outputDir, `${DEFAULT_LAMBDA_CODE_DIR}/manifest.json`)
|
||||||
|
);
|
||||||
|
|
||||||
|
apiBuildManifest = await fse.readJSON(join(outputDir, `${API_LAMBDA_CODE_DIR}/manifest.json`));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fseEmptyDirSpy.mockRestore();
|
||||||
|
fseRemoveSpy.mockRestore();
|
||||||
|
return cleanupDir(outputDir);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Cleanup', () => {
|
||||||
|
it('.next directory is emptied except for cache/ folder', () => {
|
||||||
|
expect(fseRemoveSpy).toBeCalledWith(join(fixturePath, '.next/serverless'));
|
||||||
|
expect(fseRemoveSpy).toBeCalledWith(join(fixturePath, '.next/prerender-manifest.json'));
|
||||||
|
expect(fseRemoveSpy).not.toBeCalledWith(join(fixturePath, '.next/cache'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('output directory is cleanup before building', () => {
|
||||||
|
expect(fseEmptyDirSpy).toBeCalledWith(
|
||||||
|
expect.stringContaining(join('.test_sls_next_output', 'default-lambda'))
|
||||||
|
);
|
||||||
|
expect(fseEmptyDirSpy).toBeCalledWith(
|
||||||
|
expect.stringContaining(join('.test_sls_next_output', 'api-lambda'))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Default Handler Manifest', () => {
|
||||||
|
it('adds full manifest', () => {
|
||||||
|
const {
|
||||||
|
buildId,
|
||||||
|
publicFiles,
|
||||||
|
pages: {
|
||||||
|
ssr: { dynamic, nonDynamic },
|
||||||
|
html,
|
||||||
|
},
|
||||||
|
} = defaultBuildManifest;
|
||||||
|
|
||||||
|
expect(removeNewLineChars(buildId)).toEqual('test-build-id');
|
||||||
|
expect(dynamic).toEqual({
|
||||||
|
'/:root': {
|
||||||
|
file: 'pages/[root].js',
|
||||||
|
regex: expect.any(String),
|
||||||
|
},
|
||||||
|
'/customers/:customer': {
|
||||||
|
file: 'pages/customers/[customer].js',
|
||||||
|
regex: expect.any(String),
|
||||||
|
},
|
||||||
|
'/customers/:customer/:post': {
|
||||||
|
file: 'pages/customers/[customer]/[post].js',
|
||||||
|
regex: expect.any(String),
|
||||||
|
},
|
||||||
|
'/customers/:customer/profile': {
|
||||||
|
file: 'pages/customers/[customer]/profile.js',
|
||||||
|
regex: expect.any(String),
|
||||||
|
},
|
||||||
|
'/customers/:catchAll*': {
|
||||||
|
file: 'pages/customers/[...catchAll].js',
|
||||||
|
regex: expect.any(String),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nonDynamic).toEqual({
|
||||||
|
'/customers/new': 'pages/customers/new.js',
|
||||||
|
'/': 'pages/index.js',
|
||||||
|
'/_app': 'pages/_app.js',
|
||||||
|
'/_document': 'pages/_document.js',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(html).toEqual({
|
||||||
|
nonDynamic: {
|
||||||
|
'/404': 'pages/404.html',
|
||||||
|
'/terms': 'pages/terms.html',
|
||||||
|
'/about': 'pages/about.html',
|
||||||
|
},
|
||||||
|
dynamic: {
|
||||||
|
'/blog/:post': {
|
||||||
|
file: 'pages/blog/[post].html',
|
||||||
|
regex: expect.any(String),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(publicFiles).toEqual({
|
||||||
|
'/favicon.ico': 'favicon.ico',
|
||||||
|
'/sub/image.png': 'sub/image.png',
|
||||||
|
'/sw.js': 'sw.js',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('API Handler Manifest', () => {
|
||||||
|
it('adds full api manifest', () => {
|
||||||
|
const {
|
||||||
|
apis: { dynamic, nonDynamic },
|
||||||
|
} = apiBuildManifest;
|
||||||
|
|
||||||
|
expect(nonDynamic).toEqual({
|
||||||
|
'/api/customers': 'pages/api/customers.js',
|
||||||
|
'/api/customers/new': 'pages/api/customers/new.js',
|
||||||
|
});
|
||||||
|
expect(dynamic).toEqual({
|
||||||
|
'/api/customers/:id': {
|
||||||
|
file: 'pages/api/customers/[id].js',
|
||||||
|
regex: expect.any(String),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Default Handler Artifact Files', () => {
|
||||||
|
it('copies build files', async () => {
|
||||||
|
expect.assertions(7);
|
||||||
|
|
||||||
|
const files = await fse.readdir(join(outputDir, `${DEFAULT_LAMBDA_CODE_DIR}`));
|
||||||
|
const pages = await fse.readdir(join(outputDir, `${DEFAULT_LAMBDA_CODE_DIR}/pages`));
|
||||||
|
const customerPages = await fse.readdir(
|
||||||
|
join(outputDir, `${DEFAULT_LAMBDA_CODE_DIR}/pages/customers`)
|
||||||
|
);
|
||||||
|
const apiDirExists = await fse.pathExists(
|
||||||
|
join(outputDir, `${DEFAULT_LAMBDA_CODE_DIR}/pages/api`)
|
||||||
|
);
|
||||||
|
const compatLayerIncluded = await fse.pathExists(
|
||||||
|
join(outputDir, `${DEFAULT_LAMBDA_CODE_DIR}/node_modules/next-aws-cloudfront/index.js`)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(files).toEqual([
|
||||||
|
'index.js',
|
||||||
|
'manifest.json',
|
||||||
|
'node_modules',
|
||||||
|
'pages',
|
||||||
|
'prerender-manifest.json',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(compatLayerIncluded).toEqual(true);
|
||||||
|
|
||||||
|
// api pages should not be included in the default lambda
|
||||||
|
expect(apiDirExists).toEqual(false);
|
||||||
|
|
||||||
|
// HTML Prerendered pages or JSON static props files
|
||||||
|
// should not be included in the default lambda
|
||||||
|
expect(pages).not.toContain(['blog.json']);
|
||||||
|
expect(pages).not.toContain(['about.html', 'terms.html']);
|
||||||
|
|
||||||
|
expect(pages).toEqual(['_error.js', 'blog.js', 'customers']);
|
||||||
|
expect(customerPages).toEqual(['[...catchAll].js', '[post].js']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('API Handler Artifact Files', () => {
|
||||||
|
it('copies build files', async () => {
|
||||||
|
expect.assertions(3);
|
||||||
|
|
||||||
|
const files = await fse.readdir(join(outputDir, `${API_LAMBDA_CODE_DIR}`));
|
||||||
|
const pages = await fse.readdir(join(outputDir, `${API_LAMBDA_CODE_DIR}/pages`));
|
||||||
|
|
||||||
|
const compatLayerIncluded = await fse.pathExists(
|
||||||
|
join(outputDir, `${API_LAMBDA_CODE_DIR}/node_modules/next-aws-cloudfront/index.js`)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(compatLayerIncluded).toEqual(true);
|
||||||
|
expect(files).toEqual(['index.js', 'manifest.json', 'node_modules', 'pages']);
|
||||||
|
expect(pages).toEqual(['api']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = { target: 'serverless' };
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
{
|
||||||
|
"buildId": "build-id2",
|
||||||
|
"pages": {
|
||||||
|
"ssr": {
|
||||||
|
"dynamic": {
|
||||||
|
"/:root": {
|
||||||
|
"file": "pages/[root].js",
|
||||||
|
"regex": "^/([^/]+?)(?:/)?$"
|
||||||
|
},
|
||||||
|
"/blog/:id": {
|
||||||
|
"file": "pages/blog/[id].js",
|
||||||
|
"regex": "^/blog/([^/]+?)(?:/)?$"
|
||||||
|
},
|
||||||
|
"/customers/:customer": {
|
||||||
|
"file": "pages/customers/[customer].js",
|
||||||
|
"regex": "^/customers/([^/]+?)(?:/)?$"
|
||||||
|
},
|
||||||
|
"/customers/:customer/profile": {
|
||||||
|
"file": "pages/customers/[customer]/profile.js",
|
||||||
|
"regex": "^/customers/([^/]+?)/profile(?:/)?$"
|
||||||
|
},
|
||||||
|
"/customers/:customer/:post": {
|
||||||
|
"file": "pages/customers/[customer]/[post].js",
|
||||||
|
"regex": "^/customers/([^/]+?)/([^/]+?)(?:/)?$"
|
||||||
|
},
|
||||||
|
"/customers/:catchAll*": {
|
||||||
|
"file": "pages/customers/[...catchAll].js",
|
||||||
|
"regex": "^/customers(?:/((?:[^/#?]+?)(?:/(?:[^/#?]+?))*))?[/#?]?$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nonDynamic": {
|
||||||
|
"/": "pages/index.js",
|
||||||
|
"/customers": "pages/customers/index.js",
|
||||||
|
"/customers/new": "pages/customers/new.js",
|
||||||
|
"/api/getCustomers": "pages/api/getCustomers.js",
|
||||||
|
"/_error": "pages/_error.js",
|
||||||
|
"/404": "pages/404.html"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"html": {
|
||||||
|
"nonDynamic": {
|
||||||
|
"/": "pages/index.html",
|
||||||
|
"/index": "pages/index.html",
|
||||||
|
"/terms": "pages/terms.html",
|
||||||
|
"/404": "pages/404.html"
|
||||||
|
},
|
||||||
|
"dynamic": {
|
||||||
|
"/users/:user": {
|
||||||
|
"file": "pages/users/[user].html",
|
||||||
|
"regex": "^/users/([^/]+?)(?:/)?$"
|
||||||
|
},
|
||||||
|
"/users/:user*": {
|
||||||
|
"file": "pages/users/[...user].html",
|
||||||
|
"regex": "^/users(?:/((?:[^/#?]+?)(?:/(?:[^/#?]+?))*))?[/#?]?$"
|
||||||
|
},
|
||||||
|
"/:username/:id": {
|
||||||
|
"file": "pages/[username]/[id].html",
|
||||||
|
"regex": "^/([^/]+?)/([^/]+?)(?:/)?$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"publicFiles": {
|
||||||
|
"/favicon.ico": "favicon.ico",
|
||||||
|
"/manifest.json": "manifest.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
{
|
||||||
|
"buildId": "build-id",
|
||||||
|
"pages": {
|
||||||
|
"ssr": {
|
||||||
|
"dynamic": {
|
||||||
|
"/:root": {
|
||||||
|
"file": "pages/[root].js",
|
||||||
|
"regex": "^/([^/]+?)(?:/)?$"
|
||||||
|
},
|
||||||
|
"/blog/:id": {
|
||||||
|
"file": "pages/blog/[id].js",
|
||||||
|
"regex": "^/blog/([^/]+?)(?:/)?$"
|
||||||
|
},
|
||||||
|
"/customers/:customer": {
|
||||||
|
"file": "pages/customers/[customer].js",
|
||||||
|
"regex": "^/customers/([^/]+?)(?:/)?$"
|
||||||
|
},
|
||||||
|
"/customers/:customer/profile": {
|
||||||
|
"file": "pages/customers/[customer]/profile.js",
|
||||||
|
"regex": "^/customers/([^/]+?)/profile(?:/)?$"
|
||||||
|
},
|
||||||
|
"/customers/:customer/:post": {
|
||||||
|
"file": "pages/customers/[customer]/[post].js",
|
||||||
|
"regex": "^/customers/([^/]+?)/([^/]+?)(?:/)?$"
|
||||||
|
},
|
||||||
|
"/customers/:catchAll*": {
|
||||||
|
"file": "pages/customers/[...catchAll].js",
|
||||||
|
"regex": "^/customers(?:/((?:[^/#?]+?)(?:/(?:[^/#?]+?))*))?[/#?]?$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nonDynamic": {
|
||||||
|
"/": "pages/index.js",
|
||||||
|
"/customers": "pages/customers/index.js",
|
||||||
|
"/customers/new": "pages/customers/new.js",
|
||||||
|
"/api/getCustomers": "pages/api/getCustomers.js",
|
||||||
|
"/_error": "pages/_error.js",
|
||||||
|
"/404": "pages/404.html"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"html": {
|
||||||
|
"nonDynamic": {
|
||||||
|
"/": "pages/index.html",
|
||||||
|
"/index": "pages/index.html",
|
||||||
|
"/terms": "pages/terms.html"
|
||||||
|
},
|
||||||
|
"dynamic": {
|
||||||
|
"/users/:user": {
|
||||||
|
"file": "pages/users/[user].html",
|
||||||
|
"regex": "^/users/([^/]+?)(?:/)?$"
|
||||||
|
},
|
||||||
|
"/users/:user*": {
|
||||||
|
"file": "pages/users/[...user].html",
|
||||||
|
"regex": "^/users(?:/((?:[^/#?]+?)(?:/(?:[^/#?]+?))*))?[/#?]?$"
|
||||||
|
},
|
||||||
|
"/:username/:id": {
|
||||||
|
"file": "pages/[username]/[id].html",
|
||||||
|
"regex": "^/([^/]+?)/([^/]+?)(?:/)?$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"publicFiles": {
|
||||||
|
"/favicon.ico": "favicon.ico",
|
||||||
|
"/manifest.json": "manifest.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { handler } from '../../src/default-handler';
|
||||||
|
import { createCloudFrontEvent } from '../test-utils';
|
||||||
|
import { CloudFrontResultResponse } from 'aws-lambda';
|
||||||
|
|
||||||
|
jest.mock('../../src/manifest.json', () => require('./default-build-manifest-with-404.json'), {
|
||||||
|
virtual: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('../../src/prerender-manifest.json', () => require('./prerender-manifest.json'), {
|
||||||
|
virtual: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Lambda@Edge', () => {
|
||||||
|
it("renders a static 404 page if request path can't be matched to any page / api routes and a 404.html was generated", async () => {
|
||||||
|
const event = createCloudFrontEvent({
|
||||||
|
uri: '/page/does/not/exist',
|
||||||
|
host: 'mydistribution.cloudfront.net',
|
||||||
|
origin: {
|
||||||
|
s3: {
|
||||||
|
domainName: 'my-bucket.s3.amazonaws.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = (await handler(event)) as CloudFrontResultResponse;
|
||||||
|
|
||||||
|
expect(response.uri).toEqual('/404.html');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
import { handler } from '../../src/default-handler';
|
||||||
|
import { createCloudFrontEvent } from '../test-utils';
|
||||||
|
import {
|
||||||
|
CloudFrontRequest,
|
||||||
|
CloudFrontResultResponse,
|
||||||
|
CloudFrontHeaders,
|
||||||
|
CloudFrontOrigin,
|
||||||
|
} from 'aws-lambda';
|
||||||
|
|
||||||
|
jest.mock('../../src/manifest.json', () => require('./default-build-manifest.json'), {
|
||||||
|
virtual: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('../../src/prerender-manifest.json', () => require('./prerender-manifest.json'), {
|
||||||
|
virtual: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockPageRequire = (mockPagePath: string): void => {
|
||||||
|
jest.mock(
|
||||||
|
`../../src/${mockPagePath}`,
|
||||||
|
() => require(`../shared-fixtures/built-artifact/${mockPagePath}`),
|
||||||
|
{
|
||||||
|
virtual: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Lambda@Edge', () => {
|
||||||
|
describe('Routing', () => {
|
||||||
|
describe('HTML pages routing', () => {
|
||||||
|
it.each`
|
||||||
|
path | expectedPage
|
||||||
|
${'/'} | ${'/index.html'}
|
||||||
|
${'/index'} | ${'/index.html'}
|
||||||
|
${'/terms'} | ${'/terms.html'}
|
||||||
|
${'/users/batman'} | ${'/users/[user].html'}
|
||||||
|
${'/users/test/catch/all'} | ${'/users/[...user].html'}
|
||||||
|
${'/john/123'} | ${'/[username]/[id].html'}
|
||||||
|
${'/tests/prerender-manifest/example-static-page'} | ${'/tests/prerender-manifest/example-static-page.html'}
|
||||||
|
`('serves page $expectedPage from S3 for path $path', async ({ path, expectedPage }) => {
|
||||||
|
const event = createCloudFrontEvent({
|
||||||
|
uri: path,
|
||||||
|
host: 'mydistribution.cloudfront.net',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handler(event);
|
||||||
|
|
||||||
|
const request = result as CloudFrontRequest;
|
||||||
|
|
||||||
|
expect(request.origin).toEqual({
|
||||||
|
s3: {
|
||||||
|
authMethod: 'origin-access-identity',
|
||||||
|
domainName: 'my-bucket.s3.amazonaws.com',
|
||||||
|
path: '/static-pages',
|
||||||
|
region: 'us-east-1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(request.uri).toEqual(expectedPage);
|
||||||
|
expect(request.headers.host[0].key).toEqual('host');
|
||||||
|
expect(request.headers.host[0].value).toEqual('my-bucket.s3.amazonaws.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Public files routing', () => {
|
||||||
|
it('serves public file from S3 /public folder', async () => {
|
||||||
|
const event = createCloudFrontEvent({
|
||||||
|
uri: '/manifest.json',
|
||||||
|
host: 'mydistribution.cloudfront.net',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handler(event);
|
||||||
|
|
||||||
|
const request = result as CloudFrontRequest;
|
||||||
|
|
||||||
|
expect(request.origin).toEqual({
|
||||||
|
s3: {
|
||||||
|
authMethod: 'origin-access-identity',
|
||||||
|
domainName: 'my-bucket.s3.amazonaws.com',
|
||||||
|
path: '/public',
|
||||||
|
region: 'us-east-1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(request.uri).toEqual('/manifest.json');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SSR pages routing', () => {
|
||||||
|
it.each`
|
||||||
|
path | expectedPage
|
||||||
|
${'/abc'} | ${'pages/[root].js'}
|
||||||
|
${'/blog/foo'} | ${'pages/blog/[id].js'}
|
||||||
|
${'/customers'} | ${'pages/customers/index.js'}
|
||||||
|
${'/customers/superman'} | ${'pages/customers/[customer].js'}
|
||||||
|
${'/customers/superman/howtofly'} | ${'pages/customers/[customer]/[post].js'}
|
||||||
|
${'/customers/superman/profile'} | ${'pages/customers/[customer]/profile.js'}
|
||||||
|
${'/customers/test/catch/all'} | ${'pages/customers/[...catchAll].js'}
|
||||||
|
`('renders page $expectedPage for path $path', async ({ path, expectedPage }) => {
|
||||||
|
const event = createCloudFrontEvent({
|
||||||
|
uri: path,
|
||||||
|
host: 'mydistribution.cloudfront.net',
|
||||||
|
});
|
||||||
|
|
||||||
|
mockPageRequire(expectedPage);
|
||||||
|
|
||||||
|
const response = await handler(event);
|
||||||
|
|
||||||
|
const cfResponse = response as CloudFrontResultResponse;
|
||||||
|
const decodedBody = new Buffer(cfResponse.body as string, 'base64').toString('utf8');
|
||||||
|
|
||||||
|
expect(decodedBody).toEqual(expectedPage);
|
||||||
|
expect(cfResponse.status).toEqual(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Data Requests', () => {
|
||||||
|
it.each`
|
||||||
|
path | expectedPage
|
||||||
|
${'/_next/data/build-id/customers.json'} | ${'pages/customers/index.js'}
|
||||||
|
${'/_next/data/build-id/customers/superman.json'} | ${'pages/customers/[customer].js'}
|
||||||
|
${'/_next/data/build-id/customers/superman/profile.json'} | ${'pages/customers/[customer]/profile.js'}
|
||||||
|
`('serves json data for path $path', async ({ path, expectedPage }) => {
|
||||||
|
const event = createCloudFrontEvent({
|
||||||
|
uri: path,
|
||||||
|
host: 'mydistribution.cloudfront.net',
|
||||||
|
});
|
||||||
|
|
||||||
|
mockPageRequire(expectedPage);
|
||||||
|
|
||||||
|
const result = await handler(event);
|
||||||
|
|
||||||
|
const response = result as CloudFrontResultResponse;
|
||||||
|
const decodedBody = new Buffer(response.body as string, 'base64').toString('utf8');
|
||||||
|
|
||||||
|
const headers = response.headers as CloudFrontHeaders;
|
||||||
|
expect(headers['content-type'][0].value).toEqual('application/json');
|
||||||
|
expect(JSON.parse(decodedBody)).toEqual({
|
||||||
|
page: expectedPage,
|
||||||
|
});
|
||||||
|
expect(response.status).toEqual(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses default s3 endpoint when bucket region is us-east-1', async () => {
|
||||||
|
const event = createCloudFrontEvent({
|
||||||
|
uri: '/terms',
|
||||||
|
host: 'mydistribution.cloudfront.net',
|
||||||
|
s3Region: 'us-east-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handler(event);
|
||||||
|
|
||||||
|
const request = result as CloudFrontRequest;
|
||||||
|
const origin = request.origin as CloudFrontOrigin;
|
||||||
|
|
||||||
|
expect(origin.s3).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
domainName: 'my-bucket.s3.amazonaws.com',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(request.headers.host[0].key).toEqual('host');
|
||||||
|
expect(request.headers.host[0].value).toEqual('my-bucket.s3.amazonaws.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses regional endpoint when bucket region is not us-east-1', async () => {
|
||||||
|
const event = createCloudFrontEvent({
|
||||||
|
uri: '/terms',
|
||||||
|
host: 'mydistribution.cloudfront.net',
|
||||||
|
s3DomainName: 'my-bucket.s3.amazonaws.com',
|
||||||
|
s3Region: 'eu-west-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handler(event);
|
||||||
|
|
||||||
|
const request = result as CloudFrontRequest;
|
||||||
|
const origin = request.origin as CloudFrontOrigin;
|
||||||
|
|
||||||
|
expect(origin).toEqual({
|
||||||
|
s3: {
|
||||||
|
authMethod: 'origin-access-identity',
|
||||||
|
domainName: 'my-bucket.s3.eu-west-1.amazonaws.com',
|
||||||
|
path: '/static-pages',
|
||||||
|
region: 'eu-west-1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(request.uri).toEqual('/terms.html');
|
||||||
|
expect(request.headers.host[0].key).toEqual('host');
|
||||||
|
expect(request.headers.host[0].value).toEqual('my-bucket.s3.eu-west-1.amazonaws.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders 404 page if request path can't be matched to any page / api routes", async () => {
|
||||||
|
const event = createCloudFrontEvent({
|
||||||
|
uri: '/page/does/not/exist',
|
||||||
|
host: 'mydistribution.cloudfront.net',
|
||||||
|
});
|
||||||
|
|
||||||
|
mockPageRequire('pages/_error.js');
|
||||||
|
|
||||||
|
const response = (await handler(event)) as CloudFrontResultResponse;
|
||||||
|
const body = response.body as string;
|
||||||
|
const decodedBody = new Buffer(body, 'base64').toString('utf8');
|
||||||
|
|
||||||
|
expect(decodedBody).toEqual('pages/_error.js');
|
||||||
|
expect(response.status).toEqual(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"routes": {
|
||||||
|
"/tests/prerender-manifest/example-static-page": {
|
||||||
|
"initialRevalidateSeconds": false,
|
||||||
|
"srcRoute": "/tests/prerender-manifest/[staticPageName]",
|
||||||
|
"dataRoute": "/_next/data/test-build-id/tests/prerender-manifest/example-static-page.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dynamicRoutes": {
|
||||||
|
"/tests/prerender-manifest/[staticPageName]": {
|
||||||
|
"routeRegex": "^/tests/prerender-manifest/(?:([^/]+?))/?$",
|
||||||
|
"dataRoute": "/_next/data/test-build-id/tests/prerender-manifest/[staticPageName].json",
|
||||||
|
"fallback": false,
|
||||||
|
"dataRouteRegex": "^/_next/data/test-build-id/tests/prerender-manifest/(?:([^/]+?)).json/?$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"previewModeId": "test-preview-mode-id",
|
||||||
|
"previewModeSigningKey": "test-preview-mode-signing-key",
|
||||||
|
"previewModeEncryptionKey": "test-preview-mode-enc-key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { join } from 'path';
|
||||||
|
import fse, { readJSON } from 'fs-extra';
|
||||||
|
import execa from 'execa';
|
||||||
|
import Builder from '../../src/build';
|
||||||
|
import { DEFAULT_LAMBDA_CODE_DIR, API_LAMBDA_CODE_DIR } from '../../src/build';
|
||||||
|
import { cleanupDir } from '../test-utils';
|
||||||
|
import { OriginRequestDefaultHandlerManifest, OriginRequestApiHandlerManifest } from '../../types';
|
||||||
|
|
||||||
|
jest.mock('execa');
|
||||||
|
|
||||||
|
describe('Dynamic Routes Precedence', () => {
|
||||||
|
let defaultBuildManifest: OriginRequestDefaultHandlerManifest;
|
||||||
|
let apiBuildManifest: OriginRequestApiHandlerManifest;
|
||||||
|
let fseRemoveSpy: jest.SpyInstance;
|
||||||
|
|
||||||
|
const fixturePath = join(__dirname, './fixture');
|
||||||
|
const outputDir = join(fixturePath, '.test_sls_next_output');
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const mockExeca = execa as jest.Mock;
|
||||||
|
mockExeca.mockResolvedValueOnce();
|
||||||
|
fseRemoveSpy = jest.spyOn(fse, 'remove').mockImplementation(() => {
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
const builder = new Builder(fixturePath, outputDir);
|
||||||
|
await builder.build();
|
||||||
|
|
||||||
|
defaultBuildManifest = await readJSON(
|
||||||
|
join(outputDir, `${DEFAULT_LAMBDA_CODE_DIR}/manifest.json`)
|
||||||
|
);
|
||||||
|
|
||||||
|
apiBuildManifest = await readJSON(join(outputDir, `${API_LAMBDA_CODE_DIR}/manifest.json`));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fseRemoveSpy.mockRestore();
|
||||||
|
return cleanupDir(outputDir);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds dynamic page routes to the manifest in correct order of precedence', async () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const {
|
||||||
|
pages: {
|
||||||
|
ssr: { dynamic },
|
||||||
|
},
|
||||||
|
} = defaultBuildManifest;
|
||||||
|
|
||||||
|
const routes = Object.keys(dynamic);
|
||||||
|
expect(routes).toEqual(['/customers/:customer', '/:blog/:id']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds dynamic api routes to the manifest in correct order of precedence', async () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const {
|
||||||
|
apis: { dynamic },
|
||||||
|
} = apiBuildManifest;
|
||||||
|
|
||||||
|
const routes = Object.keys(dynamic);
|
||||||
|
expect(routes).toEqual(['/api/customers/:customer', '/api/:blog/:id']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = { target: 'serverless' };
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export default () => 'Hello World!';
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { getNextBinary } from '../../test-utils';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
import Builder from '../../../src/build';
|
||||||
|
import { remove, pathExists } from 'fs-extra';
|
||||||
|
|
||||||
|
jest.unmock('execa');
|
||||||
|
|
||||||
|
jest.setTimeout(15000);
|
||||||
|
|
||||||
|
describe('No Next Config Build Test', () => {
|
||||||
|
const nextBinary = getNextBinary();
|
||||||
|
const fixtureDir = path.join(__dirname, './fixture');
|
||||||
|
let mockDateNow: jest.SpyInstance<number, []>;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const builder = new Builder(fixtureDir, os.tmpdir(), {
|
||||||
|
cwd: fixtureDir,
|
||||||
|
cmd: nextBinary,
|
||||||
|
args: ['build'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await builder.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
return Promise.all(
|
||||||
|
['.next', 'next.config.js', 'next.config.original.123.js'].map((file) =>
|
||||||
|
remove(path.join(fixtureDir, file))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockDateNow = jest.spyOn(Date, 'now').mockReturnValue(123);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockDateNow.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes temporary next.config.js created', async () => {
|
||||||
|
expect(await pathExists(path.join(fixtureDir, 'next.config.js'))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cleans up temporary next.config.original.x.js generated', async () => {
|
||||||
|
expect(await pathExists(path.join(fixtureDir, 'next.config.original.123.js'))).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
.next
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export default () => <span>Hello World</span>;
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default (req, res) => {
|
||||||
|
res.statusCode = 200;
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.end(JSON.stringify({ name: 'John Doe' }));
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
const DynamicComponent = dynamic(() => import('../components/hello'));
|
||||||
|
|
||||||
|
const Page = () => <DynamicComponent />;
|
||||||
|
|
||||||
|
Page.getInitialProps = () => {
|
||||||
|
// just forcing this page to be server side rendered
|
||||||
|
return {
|
||||||
|
foo: 'bar',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { remove, readdir, pathExists } from 'fs-extra';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
import Builder from '../../../src/build';
|
||||||
|
import { getNextBinary } from '../../test-utils';
|
||||||
|
|
||||||
|
jest.unmock('execa');
|
||||||
|
|
||||||
|
describe('Serverless Trace With Dynamic Import', () => {
|
||||||
|
const nextBinary = getNextBinary();
|
||||||
|
const fixtureDir = path.join(__dirname, './fixture');
|
||||||
|
let outputDir: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
outputDir = path.join(os.tmpdir(), 'slsnext-test-build');
|
||||||
|
const builder = new Builder(fixtureDir, outputDir, {
|
||||||
|
cwd: fixtureDir,
|
||||||
|
cmd: nextBinary,
|
||||||
|
args: ['build'],
|
||||||
|
useServerlessTraceTarget: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await builder.build();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
return Promise.all(['.next'].map((file) => remove(path.join(fixtureDir, file))));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('copies node_modules to default lambda artifact', async () => {
|
||||||
|
const nodeModules = await readdir(path.join(outputDir, 'default-lambda/node_modules'));
|
||||||
|
expect(nodeModules.length).toBeGreaterThan(5); // 5 is just an arbitrary number to ensure dependencies are being copied
|
||||||
|
});
|
||||||
|
|
||||||
|
it('copies node_modules to api lambda artifact', async () => {
|
||||||
|
const nodeModules = await readdir(path.join(outputDir, 'api-lambda/node_modules'));
|
||||||
|
expect(nodeModules).toEqual(['@sls-next', 'next']);
|
||||||
|
|
||||||
|
const slsNextNodeModules = await readdir(path.join(outputDir, 'api-lambda/node_modules'));
|
||||||
|
expect(slsNextNodeModules).toContain('next-aws-cloudfront');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('copies dynamic chunk to default lambda artifact', async () => {
|
||||||
|
const chunkFileName = (await readdir(path.join(fixtureDir, '.next/serverless'))).find(
|
||||||
|
(file) => {
|
||||||
|
return /^[\d]+\.+[\w,\s-]+\.(js)$/.test(file);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(chunkFileName).toBeDefined();
|
||||||
|
|
||||||
|
const chunkExistsInOutputBuild = await pathExists(
|
||||||
|
path.join(outputDir, 'default-lambda', chunkFileName as string)
|
||||||
|
);
|
||||||
|
expect(chunkExistsInOutputBuild).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = () => ({});
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export default () => 'Hello World!';
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue