start separating lambdas by functionality,

This commit is contained in:
Egor 2020-07-14 00:21:31 -07:00
parent 048904c000
commit 5db32796f6
28 changed files with 463 additions and 465 deletions

2
.d.ts vendored
View file

@ -56,7 +56,7 @@ type BaseDeploymentOptions = {
onPreDeploy?: () => Promise<void>; onPreDeploy?: () => Promise<void>;
onPostDeploy?: () => Promise<void>; onPostDeploy?: () => Promise<void>;
onShutdown?: () => Promise<void>; onShutdown?: () => Promise<void>;
build?: BuildOptions | boolean; buildOptions?: BuildOptions | boolean;
nextConfigDir?: string; nextConfigDir?: string;
domain?: string | string[]; domain?: string | string[];
}; };

216
README.md
View file

@ -6,16 +6,16 @@ Effortless deployment for Next.js apps 🚀
- [Getting Started](#Getting-Started) - [Getting Started](#Getting-Started)
- [Background](#Background) - [Background](#Background)
- [Configuration](#Configuration)
- [AWS](#AWS)
- [GitHub](#GitHub)
- [Advanced Configuration](#Advanced-Configuration)
- [CLI](#CLI) - [CLI](#CLI)
- [Environment](#Environment)
- [GitHub](#GitHub)
- [AWS](#AWS)
- [Configuration Options](#Configuration-Options) - [Configuration Options](#Configuration-Options)
- [Base Options](#Base-Options) - [Base Options](#Base-Options)
- [GitHub Options](#GitHub-Options) - [GitHub Options](#GitHub-Options)
- [AWS Options](#AWS-Options) - [AWS Options](#AWS-Options)
- [CI/CD](#CI/CD) - [Advanced Configuration](#Advanced-Configuration)
- [CI/CD](#CICD)
## Getting Started ## Getting Started
@ -36,7 +36,27 @@ Next Deploy was created to deploy web applications built using the wonderful [Ne
Next Deploy started as a fork of [serverless-next.js](https://github.com/serverless-nextjs/serverless-next.js) which itself is an orchestrator of various orphaned [serverless-components](https://github.com/serverless-components/). Next Deploy was created out of a need for a better, strongly typed codebase and an ability to provide more advanced functionality without the [influence of corporate backers](https://opencollective.com/goserverless#section-contributions). Next Deploy started as a fork of [serverless-next.js](https://github.com/serverless-nextjs/serverless-next.js) which itself is an orchestrator of various orphaned [serverless-components](https://github.com/serverless-components/). Next Deploy was created out of a need for a better, strongly typed codebase and an ability to provide more advanced functionality without the [influence of corporate backers](https://opencollective.com/goserverless#section-contributions).
## Configuration ## CLI
Next Deploy comes with a `next-deploy [argument]` CLI that you can run with `npx next-deploy` or `yarn next-deploy`.
There are currently 5 supported arguments:
- **Default** (default): Runs **Build** followed by **Deploy**.
- **Init**: Creates the base next-deploy.config.js configuration for your project.
- **Build**: Build the application for deployment.
- **Deploy**: Deploy the built application.
- **Remove**: Remove the deployed resources. Note that some resources (such as lambda@edge lambdas need to be cleaned up manually due to a timing constraint).
## Environment
### GitHub
No specific environment configuration is necessary. By default, your app will be built and exported to the `gh-pages` branch.
### AWS ### AWS
@ -47,86 +67,158 @@ AWS_ACCESS_KEY_ID=******
AWS_SECRET_ACCESS_KEY=****** AWS_SECRET_ACCESS_KEY=******
``` ```
If your account is restricted, [ensure that you have enough permissions to deploy](docs/aws-permissions.md). If your account is restricted, ensure that you have enough permissions to deploy.
You will need the following permissions:
### GitHub <details>
<summary>Click to view</summary>
No specific configuration is necessary. By default, your app will be built and exported to the `gh-pages` branch. ```
'acm:DescribeCertificate', // only for custom domains
### Advanced Configuration 'acm:ListCertificates', // only for custom domains
'acm:RequestCertificate', // only for custom domains
The deployment configuration is to be provided through `next-deploy.config.js`, which will be automatically created for you the first time you run `next-deploy`. 'cloudfront:CreateCloudFrontOriginAccessIdentity',
'cloudfront:CreateDistribution',
```javascript 'cloudfront:CreateInvalidation',
module.exports = { 'cloudfront:GetDistribution',
engine: 'aws', 'cloudfront:GetDistributionConfig',
debug: true, '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';
``` ```
A more advanced configuration that sets more [configurable options](#Configuration-Options): </details>
```javascript
module.exports = {
engine: 'aws',
onShutdown: () => console.log('⛔ Interrupted ⛔'),
onPostDeploy: () => console.log('🌟 Deployment Complete 🌟'),
debug: true,
bucketName: 'my-bucket-name',
description: 'My new lambda description.',
name: 'lambda-name',
domain: ['foobar', 'example.com'],
};
```
Environment variables may be substituted from `process.env` to allow for more flexibility that one would need for CI/CD.
## CLI
Next Deploy comes with a `next-deploy [argument]` CLI that you can run like `npx next-deploy` or `yarn next-deploy`.
There are currently 5 supported arguments:
**Default** (default): Runs **Build** followed by **Deploy**.
**Init**: Creates the base next-deploy.config.js configuration for your project.
**Build**: Build the application for deployment.
**Deploy**: Deploy the built application.
**Remove**: Remove, or at least attempt to remove, the deployed resources. Note that some resources (such as lambda@edge lambdas need to be cleaned up manually due to a timing constraint).
## Configuration Options ## Configuration Options
The next-deploy config varies by the provider (engine) that you're deploying to. All configuration options are optional and come with sensible defaults. The next-deploy config varies by the provider (engine) that you're deploying to. All configuration options are optional and come with sensible defaults.
The deployment configuration is to be provided through `next-deploy.config.js`, which will be automatically created for you the first time you run `next-deploy` or `next-deploy init`.
### Base Options ### Base Options
All engines support the basic options: All engines support the basic options:
| Name | Type | Default | Description | | Name | Type | Default | Description |
| :------------ | :---------------------- | :---------- | :------------------------------------------------------------------------------------------------------- | | ------------- | ------------------------------- | ------- | -------------------------------------------------------------------------------------------------------- |
| engine | `"aws"\|"github"` | `"aws"` | The platform to deploy to. | | engine | `"aws"\|"github"` | `aws` | The platform to deploy to. |
| debug | `boolean` | `false` | Print helpful messages to | | debug | `boolean` | `false` | Print helpful messages to |
| onPreDeploy | `() => Promise<void>` | `undefined` | A callback that gets called before the deployment. | | onPreDeploy | `() => Promise<void>` | `null` | A callback that gets called before the deployment. |
| onPostDeploy | `() => Promise<void>` | `undefined` | A callback that gets called after the deployment successfully finishes. | | onPostDeploy | `() => Promise<void>` | `null` | A callback that gets called after the deployment successfully finishes. |
| onShutdown | `() => Promise<void>` | `undefined` | A callback that gets called after the deployment is shutdown by a INT/QUIT/TERM signal like from ctrl+c. | | onShutdown | `() => Promise<void>` | `null` | A callback that gets called after the deployment is shutdown by a INT/QUIT/TERM signal like from ctrl+c. |
| build | `BuildOptions\|boolean` | `true` | Whether a new build should be run run or not. | | buildOptions | [`BuildOptions`](#BuildOptions) | `{}` | Build related options. |
| nextConfigDir | `string` | `./` | The directory holding the `next.config.js`. | | nextConfigDir | `string` | `./` | The directory holding the `next.config.js`. |
| domain | `string\|string[]` | `null` | The domain to deploy to . | | domain | `string\|string[]` | `null` | The domain to deploy to . |
#### BuildOptions
| Name | Type | Default | Description |
| ---- | ---------- | ------------------------ | ---------------------------------------------------- |
| cmd | `string` | `node_modules/.bin/next` | The build command. |
| args | `string[]` | `['build']` | A list of arguments to provide to the build command. |
| cwd | `string` | `./` | The current working directory. |
### Github Options ### Github Options
| Name | Type | Default | Description | | Name | Type | Default | Description |
| :------------- | :-------------------------------------------------------------- | :--------------------------------- | :--------------------------------------------------------------------------------------- | | -------------- | --------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| publishOptions | [`PublishOptions`](https://github.com/tschaub/gh-pages#options) | `{message: '...', dotfiles: true}` | The [git-hub page options](https://github.com/tschaub/gh-pages#options) to publish with. | | publishOptions | [`PublishOptions`](https://github.com/tschaub/gh-pages#options) | `{message: "Next Deployment Update", dotfiles: true}` | The [git-hub page options](https://github.com/tschaub/gh-pages#options) to publish with. |
### AWS Options ### AWS Options
TODO | Name | Type | Default | Description |
| -------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| bucketName | `string` | `*auto generated*` | Custom bucket name where static assets are stored. |
| bucketRegion | `string` | `us-east-1` | Region where you want to host your S3 bucket. |
| publicDirectoryCache | `boolean\|`[`PublicDirectoryCache`](#PublicDirectoryCache) | `true` | Customize the public/static directory asset caching policy. Assigning an object lets you customize the caching policy and the types of files being cached. Assigning false disables caching. |
| memory | `number` | `512` | The amount of memory that a lambda has access to. Increasing the lambda's memory also increases its CPU allocation. The value must be a multiple of 64 MB. |
| timeout | `number` | `10` | The amount of time that the lambda allows a function to run before stopping it. The maximum allowed value is 900 seconds. |
| name | `string` | `*auto generated*` | The name of the lambda function. |
| runtime | `string` | `nodejs12.x` | The identifier of the lambda's runtime. |
| description | `string` | <details>`"*lambda type* handler for the Next CloudFront distribution."`</details> | A description of the lambda. |
| policy | `string` | <details>`arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole`</details> | The arn policy of the lambda. |
| domainType | `"www"\|"apex"\|"both"` | `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. |
| cloudfront | [`CloudFront`](#CloudFront) | `{}` | Additional cloudfront options. |
## CI/CD #### PublicDirectoryCache
| Name | Type | Default | Description |
| ----- | -------- | ------------------------------------------------- | ---------------------------------------- |
| test | `string` | `/\.(gif|jpe?g|jp2|tiff|png|webp|bmp|svg|ico)$/i` | The test to apply the caching behaviour. |
| value | `string` | `public, max-age=31536000, must-revalidate` | The caching behavior. |
#### CloudFront
| Name | Type | Default | Description |
| ---------------------- | ------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ttl | `number` | `0` | The amount of time that you want objects to stay in CloudFront's cache before it forwards another request to determine whether the object has been updated. |
| smoothStreaming | `boolean` | `false` | Indicates whether you want to distribute media files in the Microsoft Smooth Streaming format. |
| viewerProtocolPolicy | `string` | `redirect-to-https` | The policy for viewers to access the content. |
| fieldLevelEncryptionId | `string` | `""` | The value of the ID for the field-level encryption configuration that you want to use. |
| forward | [`Forward`](#Forward) | `{}` | Determines the forwarding configuration |
| viewerCertificate | [`ViewerCertificate`](#ViewerCertificate) | `{}` | Determines the SSL/TLS configuration for communicating with viewers. |
| cookies | `string\|string[]` | `all` | Indicates which cookies should be forwarded. |
| queryString | `boolean` | `true` | Indicates whether the query string should be forwarded. |
| lambda@edge | [`LambdaAtEdgeConfig`](#LambdaAtEdgeConfig) | `TODO` | TODO |
#### Forward
| Name | Type | Default | Description |
| -------------------- | ---------- | ------- | ----------- |
| cookies | `string[]` | `TODO` | TODO |
| queryString | `string` | `TODO` | TODO |
| headers | `string[]` | `TODO` | TODO |
| queryStringCacheKeys | `string[]` | `TODO` | TODO |
#### ViewerCertificate
| Name | Type | Default | Description |
| ---------------------- | -------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ACMCertificateArn | `string` | `null` | If the SSL/TLS certificate is stored in ACM, provide the ARN of the ACM certificate. CloudFront only supports ACM certificates in the us-east-1. |
| SSLSupportMethod | `string` | `sni-only` | Specifies which viewers the distribution accepts HTTPS connections from. **sni-only** The distribution accepts HTTPS connections only from viewers that support server SNI (all modern browsers). **vip** The distribution accepts HTTPS connections from **all** (not recommended and results in additional monthly charges). |
| minimumProtocolVersion | `string` | `TLSv1.2_2018` | The security policy that you want to use for HTTPS connections with viewers. |
#### LambdaAtEdgeConfig
| Name | Type | Default | Description |
| ----------- | --------- | ------- | ----------- |
| arn | `string` | `null` | TODO |
| includeBody | `boolean` | `TODO` | TODO |
### Advanced Configuration
Environment variables may be substituted from `process.env` to allow for more flexibility.
### CI/CD
TODO TODO

View file

@ -1,6 +1,6 @@
{ {
"name": "next-deploy", "name": "next-deploy",
"version": "0.1.2", "version": "0.2.0-alpha-0",
"description": "Effortless deployment for Next.js apps 🚀", "description": "Effortless deployment for Next.js apps 🚀",
"author": "Nidratech Ltd. <egor@nidratech.com>", "author": "Nidratech Ltd. <egor@nidratech.com>",
"keywords": [ "keywords": [
@ -77,15 +77,15 @@
"@types/klaw": "^3.0.1", "@types/klaw": "^3.0.1",
"@types/klaw-sync": "^6.0.0", "@types/klaw-sync": "^6.0.0",
"@types/mime-types": "^2.1.0", "@types/mime-types": "^2.1.0",
"@types/node": "^14.0.22", "@types/node": "^14.0.23",
"@types/path-to-regexp": "^1.7.0", "@types/path-to-regexp": "^1.7.0",
"@types/ramda": "^0.27.11", "@types/ramda": "^0.27.11",
"@types/react": "^16.9.43", "@types/react": "^16.9.43",
"@types/react-dom": "^16.9.8", "@types/react-dom": "^16.9.8",
"@types/strip-ansi": "^5.2.1", "@types/strip-ansi": "^5.2.1",
"@types/webpack": "^4.41.21", "@types/webpack": "^4.41.21",
"@typescript-eslint/eslint-plugin": "^3.6.0", "@typescript-eslint/eslint-plugin": "^3.6.1",
"@typescript-eslint/parser": "^3.6.0", "@typescript-eslint/parser": "^3.6.1",
"eslint": "^7.4.0", "eslint": "^7.4.0",
"eslint-config-prettier": "^6.11.0", "eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.4", "eslint-plugin-prettier": "^3.1.4",
@ -103,8 +103,5 @@
}, },
"lint-staged": { "lint-staged": {
"*.{js,ts,md,yml}": "prettier --write" "*.{js,ts,md,yml}": "prettier --write"
},
"resolutions": {
"which": "^2.0.1"
} }
} }

View file

@ -1,3 +1 @@
const component = require('./dist/component.js'); module.exports = require('./dist/component.js').default;
module.exports = component.default;

View file

@ -84,7 +84,10 @@ class CloudFrontComponent extends Component {
region: this.state.region, region: this.state.region,
}); });
await deleteCloudFrontDistribution(cf, this.state.id); this.context.debug(
`Removing CloudFront distribution of ID ${this.state.id}. It could take a while.`
);
await deleteCloudFrontDistribution(cf, this.state.id, this.context.debug);
this.state = {}; this.state = {};
await this.save(); await this.save();

View file

@ -1,3 +1,5 @@
import { LambdaAtEdgeConfig, LambdaAtEdgeType } from '../../types';
const validLambdaTriggers = [ const validLambdaTriggers = [
'viewer-request', 'viewer-request',
'origin-request', 'origin-request',
@ -5,10 +7,31 @@ const validLambdaTriggers = [
'viewer-response', 'viewer-response',
]; ];
const triggersAllowedBody = ['viewer-request', 'origin-request'];
const makeCacheItem = (eventType: string, lambdaConfig: LambdaAtEdgeType) => {
let arn, includeBody;
if (typeof lambdaConfig === 'string') {
arn = lambdaConfig;
includeBody = triggersAllowedBody.includes(eventType);
} else {
({ arn, includeBody } = lambdaConfig);
if (includeBody && !triggersAllowedBody.includes(eventType)) {
throw new Error(`"includeBody" not allowed for ${eventType} lambda triggers.`);
}
}
return {
EventType: eventType,
LambdaFunctionARN: arn,
IncludeBody: includeBody,
};
};
// adds lambda@edge to cache behavior passed // adds lambda@edge to cache behavior passed
const addLambdaAtEdgeToCacheBehavior = ( const addLambdaAtEdgeToCacheBehavior = (
cacheBehavior: any, cacheBehavior: any,
lambdaAtEdgeConfig: Record<string, unknown> = {} lambdaAtEdgeConfig: LambdaAtEdgeConfig = {}
) => { ) => {
Object.keys(lambdaAtEdgeConfig).forEach((eventType) => { Object.keys(lambdaAtEdgeConfig).forEach((eventType) => {
if (!validLambdaTriggers.includes(eventType)) { if (!validLambdaTriggers.includes(eventType)) {
@ -17,13 +40,12 @@ const addLambdaAtEdgeToCacheBehavior = (
); );
} }
cacheBehavior.LambdaFunctionAssociations.Items.push(
makeCacheItem(eventType, lambdaAtEdgeConfig[eventType])
);
cacheBehavior.LambdaFunctionAssociations.Quantity = cacheBehavior.LambdaFunctionAssociations.Quantity =
cacheBehavior.LambdaFunctionAssociations.Quantity + 1; cacheBehavior.LambdaFunctionAssociations.Quantity + 1;
cacheBehavior.LambdaFunctionAssociations.Items.push({
EventType: eventType,
LambdaFunctionARN: lambdaAtEdgeConfig[eventType],
IncludeBody: true,
});
}); });
}; };

View file

@ -4,7 +4,7 @@ import addLambdaAtEdgeToCacheBehavior from './addLambdaAtEdgeToCacheBehavior';
import getForwardedValues from './getForwardedValues'; import getForwardedValues from './getForwardedValues';
import { PathPatternConfig } from '../../types'; import { PathPatternConfig } from '../../types';
const getDefaultCacheBehavior = (originId: string, defaults: Partial<PathPatternConfig> = {}) => { const getDefaultCacheBehavior = (originId: string, defaults: PathPatternConfig = {}) => {
const { const {
allowedHttpMethods = ['HEAD', 'GET'], allowedHttpMethods = ['HEAD', 'GET'],
forward = {}, forward = {},

View file

@ -12,10 +12,7 @@ const forwardDefaults = {
* @param defaults Default framework values (default cache behavior and custom cache behavior have different default values) * @param defaults Default framework values (default cache behavior and custom cache behavior have different default values)
* @returns Object * @returns Object
*/ */
export default function getForwardedValues( export default function getForwardedValues(config: Forward = {}, defaults?: PathPatternConfig) {
config: Forward = {},
defaults?: Partial<PathPatternConfig>
) {
const defaultValues = { ...forwardDefaults, ...defaults }; const defaultValues = { ...forwardDefaults, ...defaults };
const { const {
cookies, cookies,

View file

@ -5,11 +5,12 @@ import { Origin } from '../../types';
const getOriginConfig = (origin: string | Origin, { originAccessIdentityId = '' }) => { const getOriginConfig = (origin: string | Origin, { originAccessIdentityId = '' }) => {
const originUrl = typeof origin === 'string' ? origin : origin.url; const originUrl = typeof origin === 'string' ? origin : origin.url;
const protocolPolicy = typeof origin === 'string' ? null : origin.protocolPolicy;
const { hostname, pathname } = url.parse(originUrl); const { hostname, pathname } = url.parse(originUrl);
const originConfig: CloudFront.Origin = { const originConfig: CloudFront.Origin = {
Id: hostname as string, Id: `${hostname}${pathname}`.replace(/\/$/, ''),
DomainName: hostname as string, DomainName: hostname as string,
CustomHeaders: { CustomHeaders: {
Quantity: 0, Quantity: 0,
@ -18,10 +19,11 @@ const getOriginConfig = (origin: string | Origin, { originAccessIdentityId = ''
OriginPath: pathname === '/' ? '' : (pathname as string), OriginPath: pathname === '/' ? '' : (pathname as string),
}; };
if (originUrl.includes('s3')) { if (originUrl.includes('s3') && !originUrl.includes('s3-website')) {
const bucketName = (hostname as string).split('.')[0]; const bucketName = (hostname as string).split('.')[0];
originConfig.Id = bucketName;
originConfig.DomainName = `${bucketName}.s3.amazonaws.com`; originConfig.Id = pathname === '/' ? bucketName : `${bucketName}${pathname}`;
originConfig.DomainName = hostname as string;
originConfig.S3OriginConfig = { originConfig.S3OriginConfig = {
OriginAccessIdentity: originAccessIdentityId OriginAccessIdentity: originAccessIdentityId
? `origin-access-identity/cloudfront/${originAccessIdentityId}` ? `origin-access-identity/cloudfront/${originAccessIdentityId}`
@ -31,7 +33,7 @@ const getOriginConfig = (origin: string | Origin, { originAccessIdentityId = ''
originConfig.CustomOriginConfig = { originConfig.CustomOriginConfig = {
HTTPPort: 80, HTTPPort: 80,
HTTPSPort: 443, HTTPSPort: 443,
OriginProtocolPolicy: 'https-only', OriginProtocolPolicy: protocolPolicy || 'https-only',
OriginSslProtocols: { OriginSslProtocols: {
Quantity: 1, Quantity: 1,
Items: ['TLSv1.2'], Items: ['TLSv1.2'],

View file

@ -10,16 +10,20 @@ export { default as createInvalidation } from './createInvalidation';
const servePrivateContentEnabled = (inputs: CloudFrontInputs) => const servePrivateContentEnabled = (inputs: CloudFrontInputs) =>
inputs?.origins?.some((origin: string | Origin) => origin && (origin as Origin).private === true); inputs?.origins?.some((origin: string | Origin) => origin && (origin as Origin).private === true);
const unique = (value: string, index: number, self: string[]) => self.indexOf(value) === index;
const updateBucketsPolicies = async ( const updateBucketsPolicies = async (
s3: S3, s3: S3,
origins: CloudFront.Origins, origins: CloudFront.Origins,
s3CanonicalUserId: string s3CanonicalUserId: string
) => { ) => {
// update bucket policies with cloudfront access // update bucket policies with cloudfront access
const bucketNames = origins.Items.filter((origin) => origin.S3OriginConfig).map( const bucketNames = origins.Items.filter((origin) => origin.S3OriginConfig)
(origin) => origin.Id .map(
); (origin) =>
// remove path from the bucket name if origin had pathname
origin.Id.split('/')[0]
)
.filter(unique);
return Promise.all( return Promise.all(
bucketNames.map((bucketName: string) => bucketNames.map((bucketName: string) =>
@ -142,7 +146,11 @@ export const updateCloudFrontDistribution = async (
}; };
}; };
const disableCloudFrontDistribution = async (cf: CloudFront, distributionId: string) => { const disableCloudFrontDistribution = async (
cf: CloudFront,
distributionId: string,
debug: (message: string) => void
) => {
const distributionConfigResponse = await cf const distributionConfigResponse = await cf
.getDistributionConfig({ Id: distributionId }) .getDistributionConfig({ Id: distributionId })
.promise(); .promise();
@ -162,27 +170,24 @@ const disableCloudFrontDistribution = async (cf: CloudFront, distributionId: str
const res = await cf.updateDistribution(updateDistributionRequest).promise(); const res = await cf.updateDistribution(updateDistributionRequest).promise();
return { debug(`Waiting for the CloudFront distribution changes to be deployed.`);
id: res?.Distribution?.Id, await cf.waitFor('distributionDeployed', { Id: distributionId }).promise();
arn: res?.Distribution?.ARN,
url: `https://${res?.Distribution?.DomainName}`, return res;
};
}; };
export const deleteCloudFrontDistribution = async ( export const deleteCloudFrontDistribution = async (
cf: CloudFront, cf: CloudFront,
distributionId: string distributionId: string,
debug: (message: string) => void
): Promise<void> => { ): Promise<void> => {
try { let res = await cf.getDistributionConfig({ Id: distributionId }).promise();
const res = await cf.getDistributionConfig({ Id: distributionId }).promise();
if (res?.DistributionConfig?.Enabled) {
debug(`Disabling CloudFront distribution of ID ${distributionId} before removal.`);
res = await disableCloudFrontDistribution(cf, distributionId, debug);
}
const params = { Id: distributionId, IfMatch: res.ETag }; const params = { Id: distributionId, IfMatch: res.ETag };
await cf.deleteDistribution(params).promise(); await cf.deleteDistribution(params).promise();
} catch (e) {
if (e.code === 'DistributionNotDisabled') {
await disableCloudFrontDistribution(cf, distributionId);
} else {
throw e;
}
}
}; };

View file

@ -7,20 +7,43 @@ export type CloudFrontInputs = {
}; };
type PathPatternConfig = { type PathPatternConfig = {
allowedHttpMethods: string[]; allowedHttpMethods?: string[];
ttl: number; ttl?: number;
compress: boolean; compress?: boolean;
smoothStreaming: boolean; smoothStreaming?: boolean;
viewerProtocolPolicy: string; viewerProtocolPolicy?: string;
fieldLevelEncryptionId: string; fieldLevelEncryptionId?: string;
forward: Forward; forward?: Forward;
[path: string]: any; viewerCertificate?: ViewerCertificate;
cookies?: string;
queryString?: boolean;
'lambda@edge'?: LambdaAtEdgeConfig;
};
type ViewerCertificate = {
ACMCertificateArn: string;
SSLSupportMethod: string;
minimumProtocolVersion: string;
certificate: string;
certificateSource: string;
};
type LambdaAtEdgeConfig = {
[type: string]: LambdaAtEdgeType;
};
type LambdaAtEdgeType =
| string
| {
arn: string;
includeBody: boolean;
}; };
type Origin = { type Origin = {
url: string; url: string;
private?: boolean; private?: boolean;
pathPatterns?: Record<string, PathPatternConfig>; pathPatterns?: Record<string, PathPatternConfig>;
protocolPolicy?: string;
}; };
type Forward = { type Forward = {

View file

@ -1,3 +1 @@
const component = require('./dist/component.js'); module.exports = require('./dist/component.js').default;
module.exports = component.default;

View file

@ -1,34 +1,30 @@
import { Component } from '@serverless/core'; import { Component } from '@serverless/core';
import { pathExists, readJSON } from 'fs-extra'; import { readJSON } from 'fs-extra';
import { resolve, join } from 'path'; import { resolve, join } from 'path';
import Builder from '@next-deploy/aws-lambda-builder'; import Builder from '@next-deploy/aws-lambda-builder';
import AwsS3 from '@next-deploy/aws-s3'; import AwsS3 from '@next-deploy/aws-s3';
import AwsCloudFront from '@next-deploy/aws-cloudfront'; import AwsCloudFront from '@next-deploy/aws-cloudfront';
import { getDomains } from './utils'; import { OriginRequestHandlerManifest as BuildManifest } from '@next-deploy/aws-lambda-builder/types';
import {
OriginRequestDefaultHandlerManifest as BuildManifest,
OriginRequestApiHandlerManifest,
} from '@next-deploy/aws-lambda-builder/types';
import { PathPatternConfig } from '@next-deploy/aws-cloudfront/types'; import { PathPatternConfig } from '@next-deploy/aws-cloudfront/types';
import { Origin } from '@next-deploy/aws-cloudfront/types'; import { Origin } from '@next-deploy/aws-cloudfront/types';
import { getDomains } from './utils';
import { DeploymentResult, AwsComponentInputs, LambdaType, LambdaInput } from '../types'; import { DeploymentResult, AwsComponentInputs, LambdaType, LambdaInput } from '../types';
export const BUILD_DIR = '.next-deploy-build'; export const BUILD_DIR = '.next-deploy-build';
export const DEFAULT_LAMBDA_CODE_DIR = `${BUILD_DIR}/default-lambda`; export const REQUEST_LAMBDA_CODE_DIR = `${BUILD_DIR}/request-lambda`;
export const API_LAMBDA_CODE_DIR = `${BUILD_DIR}/api-lambda`;
class AwsComponent extends Component { class AwsComponent extends Component {
async default(inputs: AwsComponentInputs = {}): Promise<DeploymentResult> { async default(inputs: AwsComponentInputs = {}): Promise<DeploymentResult> {
if (inputs.build !== false) { if (inputs.buildOptions !== false) {
await this.build(inputs); await this.build(inputs);
} }
return this.deploy(inputs); return this.deploy(inputs);
} }
readDefaultBuildManifest(nextConfigPath: string): Promise<BuildManifest> { readRequestLambdaBuildManifest(nextConfigPath: string): Promise<BuildManifest> {
return readJSON(join(nextConfigPath, `${DEFAULT_LAMBDA_CODE_DIR}/manifest.json`)); return readJSON(join(nextConfigPath, `${REQUEST_LAMBDA_CODE_DIR}/manifest.json`));
} }
validatePathPatterns(pathPatterns: string[], buildManifest: BuildManifest): void { validatePathPatterns(pathPatterns: string[], buildManifest: BuildManifest): void {
@ -109,26 +105,22 @@ class AwsComponent extends Component {
} }
} }
async readApiBuildManifest(nextConfigPath: string): Promise<OriginRequestApiHandlerManifest> {
const path = join(nextConfigPath, `${API_LAMBDA_CODE_DIR}/manifest.json`);
return (await pathExists(path)) ? readJSON(path) : Promise.resolve(undefined);
}
async build(inputs: AwsComponentInputs = {}): Promise<void> { async build(inputs: AwsComponentInputs = {}): Promise<void> {
const nextConfigPath = inputs.nextConfigDir ? resolve(inputs.nextConfigDir) : process.cwd(); const nextConfigPath = inputs.nextConfigDir ? resolve(inputs.nextConfigDir) : process.cwd();
const buildCwd = const buildCwd =
typeof inputs.build === 'boolean' || typeof inputs.build === 'undefined' || !inputs.build.cwd typeof inputs.buildOptions === 'boolean' ||
typeof inputs.buildOptions === 'undefined' ||
!inputs.buildOptions.cwd
? nextConfigPath ? nextConfigPath
: resolve(inputs.build.cwd); : resolve(inputs.buildOptions.cwd);
const buildConfig: BuildOptions = { const buildConfig: BuildOptions = {
enabled: inputs.build enabled: inputs.buildOptions
? // @ts-ignore ? // @ts-ignore
inputs.build !== false && inputs.build.enabled !== false inputs.buildOptions !== false && inputs.buildOptions.enabled !== false
: true, : true,
cmd: 'node_modules/.bin/next', cmd: 'node_modules/.bin/next',
args: ['build'], args: ['build'],
...(typeof inputs.build === 'object' ? inputs.build : {}), ...(typeof inputs.buildOptions === 'object' ? inputs.buildOptions : {}),
cwd: buildCwd, cwd: buildCwd,
}; };
@ -148,24 +140,23 @@ class AwsComponent extends Component {
const nextStaticPath = inputs.nextStaticDir ? resolve(inputs.nextStaticDir) : nextConfigPath; const nextStaticPath = inputs.nextStaticDir ? resolve(inputs.nextStaticDir) : nextConfigPath;
const customCloudFrontConfig: Record<string, any> = inputs.cloudfront || {}; const customCloudFrontConfig: Record<string, any> = inputs.cloudfrontOptions || {};
const bucketRegion = inputs.bucketRegion || 'us-east-1';
const [defaultBuildManifest, apiBuildManifest] = await Promise.all([ const [defaultBuildManifest] = await Promise.all([
this.readDefaultBuildManifest(nextConfigPath), this.readRequestLambdaBuildManifest(nextConfigPath),
this.readApiBuildManifest(nextConfigPath),
]); ]);
const [bucket, cloudFront, defaultEdgeLambda, apiEdgeLambda] = await Promise.all([ const [bucket, cloudFront, requestEdgeLambda] = await Promise.all([
this.load('@next-deploy/aws-s3'), this.load('@next-deploy/aws-s3'),
this.load('@next-deploy/aws-cloudfront'), this.load('@next-deploy/aws-cloudfront'),
this.load('@next-deploy/aws-lambda', 'defaultEdgeLambda'), this.load('@next-deploy/aws-lambda', 'requestEdgeLambda'),
this.load('@next-deploy/aws-lambda', 'apiEdgeLambda'),
]); ]);
const bucketOutputs = await bucket({ const bucketOutputs = await bucket({
accelerated: true, accelerated: true,
name: inputs.bucketName, name: inputs.bucketName,
region: inputs.bucketRegion || 'us-east-1', region: bucketRegion,
}); });
await AwsS3.uploadStaticAssets({ await AwsS3.uploadStaticAssets({
@ -176,7 +167,7 @@ class AwsComponent extends Component {
publicDirectoryCache: inputs.publicDirectoryCache, publicDirectoryCache: inputs.publicDirectoryCache,
}); });
const bucketUrl = `http://${bucketOutputs.name}.s3.amazonaws.com`; const bucketUrl = `http://${bucketOutputs.name}.s3.${bucketRegion}.amazonaws.com`;
// If origin is relative path then prepend the bucketUrl // If origin is relative path then prepend the bucketUrl
// e.g. /path => http://bucket.s3.aws.com/path // e.g. /path => http://bucket.s3.aws.com/path
@ -196,10 +187,10 @@ class AwsComponent extends Component {
// parse origins from inputs // parse origins from inputs
let inputOrigins: any = []; let inputOrigins: any = [];
if (inputs.cloudfront && inputs.cloudfront.origins) { if (inputs.cloudfrontOptions && inputs.cloudfrontOptions.origins) {
const origins = inputs.cloudfront.origins as string[]; const origins = inputs.cloudfrontOptions.origins as string[];
inputOrigins = origins.map(expandRelativeUrls); inputOrigins = origins.map(expandRelativeUrls);
delete inputs.cloudfront.origins; delete inputs.cloudfrontOptions.origins;
} }
const cloudFrontOrigins = [ const cloudFrontOrigins = [
@ -228,13 +219,8 @@ class AwsComponent extends Component {
...inputOrigins, ...inputOrigins,
]; ];
const hasAPIPages =
apiBuildManifest &&
(Object.keys(apiBuildManifest.apis.nonDynamic).length > 0 ||
Object.keys(apiBuildManifest.apis.dynamic).length > 0);
const getLambdaInputValue = ( const getLambdaInputValue = (
inputKey: 'memory' | 'timeout' | 'name' | 'runtime', inputKey: 'memory' | 'timeout' | 'name' | 'runtime' | 'description',
lambdaType: LambdaType, lambdaType: LambdaType,
defaultValue: string | number | undefined defaultValue: string | number | undefined
): string | number | undefined => { ): string | number | undefined => {
@ -246,68 +232,39 @@ class AwsComponent extends Component {
if (!inputValue) { if (!inputValue) {
return defaultValue; return defaultValue;
} }
return inputValue[lambdaType] || defaultValue; return inputValue[lambdaType] || defaultValue;
}; };
if (hasAPIPages) {
const apiEdgeLambdaInput: LambdaInput = {
description: inputs.description
? `${inputs.description} (API)`
: 'API Lambda@Edge for Next CloudFront distribution',
handler: 'index.handler',
code: join(nextConfigPath, API_LAMBDA_CODE_DIR),
role: {
service: ['lambda.amazonaws.com', 'edgelambda.amazonaws.com'],
policy: {
arn:
inputs.policy || 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',
},
},
memory: getLambdaInputValue('memory', 'apiLambda', 512) as number,
timeout: getLambdaInputValue('timeout', 'apiLambda', 10) as number,
runtime: getLambdaInputValue('runtime', 'apiLambda', 'nodejs12.x') as string,
name: getLambdaInputValue('name', 'apiLambda', undefined) as string | undefined,
};
const apiEdgeLambdaOutputs = await apiEdgeLambda(apiEdgeLambdaInput);
const apiEdgeLambdaPublishOutputs = await apiEdgeLambda.publishVersion();
cloudFrontOrigins[0].pathPatterns['api/*'] = {
ttl: 0,
allowedHttpMethods: ['HEAD', 'DELETE', 'POST', 'GET', 'OPTIONS', 'PUT', 'PATCH'],
// lambda@edge key is last and therefore cannot be overridden
'lambda@edge': {
'origin-request': `${apiEdgeLambdaOutputs.arn}:${apiEdgeLambdaPublishOutputs.version}`,
},
};
}
const defaultEdgeLambdaInput: LambdaInput = { const defaultEdgeLambdaInput: LambdaInput = {
description: inputs.description || 'Default Lambda@Edge for Next CloudFront distribution',
handler: 'index.handler', handler: 'index.handler',
code: join(nextConfigPath, DEFAULT_LAMBDA_CODE_DIR), code: join(nextConfigPath, REQUEST_LAMBDA_CODE_DIR),
role: { role: {
service: ['lambda.amazonaws.com', 'edgelambda.amazonaws.com'], service: ['lambda.amazonaws.com', 'edgelambda.amazonaws.com'],
policy: { policy: {
arn: inputs.policy || 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', arn: inputs.policy || 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',
}, },
}, },
memory: getLambdaInputValue('memory', 'defaultLambda', 512) as number, memory: getLambdaInputValue('memory', 'requestLambda', 512) as number,
timeout: getLambdaInputValue('timeout', 'defaultLambda', 10) as number, timeout: getLambdaInputValue('timeout', 'requestLambda', 10) as number,
runtime: getLambdaInputValue('runtime', 'defaultLambda', 'nodejs12.x') as string, runtime: getLambdaInputValue('runtime', 'requestLambda', 'nodejs12.x') as string,
name: getLambdaInputValue('name', 'defaultLambda', undefined) as string | undefined, name: getLambdaInputValue('name', 'requestLambda', undefined) as string | undefined,
description: getLambdaInputValue(
'description',
'requestLambda',
'Request handler for the Next CloudFront distribution.'
) as string,
}; };
const defaultEdgeLambdaOutputs = await defaultEdgeLambda(defaultEdgeLambdaInput); const requestEdgeLambdaOutputs = await requestEdgeLambda(defaultEdgeLambdaInput);
const defaultEdgeLambdaPublishOutputs = await defaultEdgeLambda.publishVersion(); const requestEdgeLambdaPublishOutputs = await requestEdgeLambda.publishVersion();
let defaultCloudfrontInputs = {} as Partial<PathPatternConfig>; let defaultCloudfrontInputs = {} as PathPatternConfig;
if (inputs.cloudfront && inputs.cloudfront.defaults) { if (inputs.cloudfrontOptions && inputs.cloudfrontOptions.defaults) {
defaultCloudfrontInputs = inputs.cloudfront.defaults; defaultCloudfrontInputs = inputs.cloudfrontOptions.defaults;
delete inputs.cloudfront.defaults; delete inputs.cloudfrontOptions.defaults;
} }
// validate that the custom config paths match generated paths in the manifest // validate that the custom config paths match generated paths in the manifest
@ -329,7 +286,7 @@ class AwsComponent extends Component {
// for everything but static/* and _next/* we want to ensure that they are pointing at our lambda // for everything but static/* and _next/* we want to ensure that they are pointing at our lambda
edgeConfig[ edgeConfig[
'origin-request' 'origin-request'
] = `${defaultEdgeLambdaOutputs.arn}:${defaultEdgeLambdaPublishOutputs.version}`; ] = `${requestEdgeLambdaOutputs.arn}:${requestEdgeLambdaPublishOutputs.version}`;
} }
cloudFrontOrigins[0].pathPatterns[path] = { cloudFrontOrigins[0].pathPatterns[path] = {
@ -349,9 +306,14 @@ class AwsComponent extends Component {
cloudFrontOrigins[0].pathPatterns['_next/data/*'] = { cloudFrontOrigins[0].pathPatterns['_next/data/*'] = {
ttl: 0, ttl: 0,
forward: {
headers: 'none',
cookies: 'none',
queryString: false,
},
allowedHttpMethods: ['HEAD', 'GET'], allowedHttpMethods: ['HEAD', 'GET'],
'lambda@edge': { 'lambda@edge': {
'origin-request': `${defaultEdgeLambdaOutputs.arn}:${defaultEdgeLambdaPublishOutputs.version}`, 'origin-request': `${requestEdgeLambdaOutputs.arn}:${requestEdgeLambdaPublishOutputs.version}`,
}, },
}; };
@ -372,10 +334,10 @@ class AwsComponent extends Component {
...defaultCloudfrontInputs.forward, ...defaultCloudfrontInputs.forward,
}, },
// everything after here cant be overridden // everything after here cant be overridden
allowedHttpMethods: ['HEAD', 'GET'], allowedHttpMethods: ['HEAD', 'DELETE', 'POST', 'GET', 'OPTIONS', 'PUT', 'PATCH'],
'lambda@edge': { 'lambda@edge': {
...defaultLambdaAtEdgeConfig, ...defaultLambdaAtEdgeConfig,
'origin-request': `${defaultEdgeLambdaOutputs.arn}:${defaultEdgeLambdaPublishOutputs.version}`, 'origin-request': `${requestEdgeLambdaOutputs.arn}:${requestEdgeLambdaPublishOutputs.version}`,
}, },
compress: true, compress: true,
}, },

View file

@ -7,17 +7,17 @@ type AwsComponentInputs = BaseDeploymentOptions & {
bucketName?: string; bucketName?: string;
bucketRegion?: string; bucketRegion?: string;
publicDirectoryCache?: PublicDirectoryCache; publicDirectoryCache?: PublicDirectoryCache;
memory?: number | { defaultLambda?: number; apiLambda?: number }; memory?: number | { [key in LambdaType]: number };
timeout?: number | { defaultLambda?: number; apiLambda?: number }; timeout?: number | { [key in LambdaType]: number };
name?: string | { defaultLambda?: string; apiLambda?: string }; name?: string | { [key in LambdaType]: string };
runtime?: string | { defaultLambda?: string; apiLambda?: string }; runtime?: string | { [key in LambdaType]: string };
description?: string; description?: string | { [key in LambdaType]: string };
policy?: string; policy?: string;
domainType?: DomainType; domainType?: DomainType;
cloudfront?: CloudFrontInputs; cloudfrontOptions?: CloudFrontInputs;
}; };
type LambdaType = 'defaultLambda' | 'apiLambda'; type LambdaType = 'requestLambda' | 'responseLambda';
type LambdaInput = { type LambdaInput = {
description: string; description: string;

View file

@ -1,3 +1 @@
const component = require('./dist/component.js'); module.exports = require('./dist/component.js').default;
module.exports = component.default;

View file

@ -365,7 +365,7 @@ export const addDomainToCloudfrontDistribution = async (
subdomain: SubDomain, subdomain: SubDomain,
certificateArn: string, certificateArn: string,
domainType: DomainType, domainType: DomainType,
defaultCloudfrontInputs: Partial<PathPatternConfig> { viewerCertificate }: PathPatternConfig
): Promise<{ ): Promise<{
id?: string; id?: string;
arn?: string; arn?: string;
@ -389,12 +389,12 @@ export const addDomainToCloudfrontDistribution = async (
Items: [subdomain.domain], Items: [subdomain.domain],
}, },
ViewerCertificate: { ViewerCertificate: {
ACMCertificateArn: certificateArn, ACMCertificateArn: viewerCertificate?.ACMCertificateArn || certificateArn,
SSLSupportMethod: 'sni-only', SSLSupportMethod: viewerCertificate?.SSLSupportMethod || 'sni-only',
MinimumProtocolVersion: DEFAULT_MINIMUM_PROTOCOL_VERSION, MinimumProtocolVersion:
Certificate: certificateArn, viewerCertificate?.minimumProtocolVersion || DEFAULT_MINIMUM_PROTOCOL_VERSION,
CertificateSource: 'acm', Certificate: viewerCertificate?.certificate || certificateArn,
...defaultCloudfrontInputs.viewerCertificate, CertificateSource: viewerCertificate?.certificateSource || 'acm',
}, },
}, },
}; };

View file

@ -5,7 +5,7 @@ type AwsDomainInputs = {
region?: string; region?: string;
privateZone?: boolean; privateZone?: boolean;
domainType?: DomainType; domainType?: DomainType;
defaultCloudfrontInputs?: Partial<PathPatternConfig>; defaultCloudfrontInputs?: PathPatternConfig;
subdomains: SubDomain[]; subdomains: SubDomain[];
}; };

View file

@ -1,55 +0,0 @@
// @ts-ignore
import manifest from './manifest.json';
import { CloudFrontResultResponse, CloudFrontRequest } from 'aws-lambda';
import lambdaAtEdgeCompat from './compat';
import { OriginRequestApiHandlerManifest, OriginRequestEvent } from '../types';
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 } = lambdaAtEdgeCompat(event.Records[0].cf);
page.default(req, res);
return responsePromise;
};

View file

@ -15,12 +15,11 @@ import { pathToRegexp } from 'path-to-regexp';
import getAllFiles from './lib/getAllFilesInDirectory'; import getAllFiles from './lib/getAllFilesInDirectory';
import { getSortedRoutes } from './lib/sortedRoutes'; import { getSortedRoutes } from './lib/sortedRoutes';
import { OriginRequestDefaultHandlerManifest, OriginRequestApiHandlerManifest } from '../types'; import { OriginRequestHandlerManifest } from '../types';
import expressifyDynamicRoute from './lib/expressifyDynamicRoute'; import expressifyDynamicRoute from './lib/expressifyDynamicRoute';
import createServerlessConfig from './lib/createServerlessConfig'; import createServerlessConfig from './lib/createServerlessConfig';
export const DEFAULT_LAMBDA_CODE_DIR = 'default-lambda'; export const REQUEST_LAMBDA_CODE_DIR = 'request-lambda';
export const API_LAMBDA_CODE_DIR = 'api-lambda';
const pathToPosix = (path: string): string => path.replace(/\\/g, '/'); const pathToPosix = (path: string): string => path.replace(/\\/g, '/');
const normalizeNodeModules = (path: string): string => path.substring(path.indexOf('node_modules')); const normalizeNodeModules = (path: string): string => path.substring(path.indexOf('node_modules'));
@ -49,6 +48,7 @@ class Builder {
this.dotNextDir = join(this.nextConfigDir, '.next'); this.dotNextDir = join(this.nextConfigDir, '.next');
this.serverlessDir = join(this.dotNextDir, 'serverless'); this.serverlessDir = join(this.dotNextDir, 'serverless');
this.outputDir = outputDir; this.outputDir = outputDir;
if (buildOptions) { if (buildOptions) {
this.buildOptions = buildOptions; this.buildOptions = buildOptions;
} }
@ -92,9 +92,9 @@ class Builder {
const sortedDynamicRoutedPages = getSortedRoutes(dynamicRoutedPages); const sortedDynamicRoutedPages = getSortedRoutes(dynamicRoutedPages);
const sortedPagesManifest = pagesManifestWithoutDynamicRoutes; const sortedPagesManifest = pagesManifestWithoutDynamicRoutes;
sortedDynamicRoutedPages.forEach((route) => { sortedDynamicRoutedPages.forEach(
sortedPagesManifest[route] = pagesManifest[route]; (route) => (sortedPagesManifest[route] = pagesManifest[route])
}); );
return sortedPagesManifest; return sortedPagesManifest;
} }
@ -118,18 +118,20 @@ class Builder {
); );
} }
async buildDefaultLambda(buildManifest: OriginRequestDefaultHandlerManifest): Promise<void[]> { async buildRequestLambda(buildManifest: OriginRequestHandlerManifest): Promise<void[]> {
const ignoreAppAndDocumentPages = (page: string): boolean => { const ignoreAppAndDocumentPages = (page: string): boolean => {
const pageBasename = basename(page); const pageBasename = basename(page);
return pageBasename !== '_app.js' && pageBasename !== '_document.js'; return pageBasename !== '_app.js' && pageBasename !== '_document.js';
}; };
const allSsrPages = [ const allServerProcessablePages = [
...Object.values(buildManifest.pages.ssr.nonDynamic), ...Object.values(buildManifest.pages.ssr.nonDynamic),
...Object.values(buildManifest.pages.ssr.dynamic).map((entry) => entry.file), ...Object.values(buildManifest.pages.ssr.dynamic).map((entry) => entry.file),
...Object.values(buildManifest.pages.apis.nonDynamic),
...Object.values(buildManifest.pages.apis.dynamic).map((entry) => entry.file),
].filter(ignoreAppAndDocumentPages); ].filter(ignoreAppAndDocumentPages);
const ssrPages = Object.values(allSsrPages).map((pageFile) => const ssrPages = Object.values(allServerProcessablePages).map((pageFile) =>
join(this.serverlessDir, pageFile) join(this.serverlessDir, pageFile)
); );
@ -140,81 +142,38 @@ class Builder {
const copyTraces = this.copyLambdaHandlerDependencies( const copyTraces = this.copyLambdaHandlerDependencies(
fileList, fileList,
reasons, reasons,
DEFAULT_LAMBDA_CODE_DIR REQUEST_LAMBDA_CODE_DIR
); );
return Promise.all([ return Promise.all([
...copyTraces, ...copyTraces,
copy( copy(
require.resolve('@next-deploy/aws-lambda-builder/dist/default-handler.js'), require.resolve('@next-deploy/aws-lambda-builder/dist/request-handler.js'),
join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR, 'index.js') join(this.outputDir, REQUEST_LAMBDA_CODE_DIR, 'index.js')
), ),
copy( copy(
require.resolve('@next-deploy/aws-lambda-builder/dist/compat.js'), require.resolve('@next-deploy/aws-lambda-builder/dist/compat.js'),
join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR, 'compat.js') join(this.outputDir, REQUEST_LAMBDA_CODE_DIR, 'compat.js')
), ),
writeJson(join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR, 'manifest.json'), buildManifest), writeJson(join(this.outputDir, REQUEST_LAMBDA_CODE_DIR, 'manifest.json'), buildManifest),
copy( copy(
join(this.serverlessDir, 'pages'), join(this.serverlessDir, 'pages'),
join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR, 'pages'), join(this.outputDir, REQUEST_LAMBDA_CODE_DIR, 'pages'),
{ {
filter: (file: string) => { filter: (file: string) => extname(file) !== '.html' && extname(file) !== '.json',
const isNotPrerenderedHTMLPage = extname(file) !== '.html';
const isNotStaticPropsJSONFile = extname(file) !== '.json';
const isNotApiPage = pathToPosix(file).indexOf('pages/api') === -1;
return isNotApiPage && isNotPrerenderedHTMLPage && isNotStaticPropsJSONFile;
},
} }
), ),
copy( copy(
join(this.dotNextDir, 'prerender-manifest.json'), join(this.dotNextDir, 'prerender-manifest.json'),
join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR, 'prerender-manifest.json') join(this.outputDir, REQUEST_LAMBDA_CODE_DIR, 'prerender-manifest.json')
), ),
]); ]);
} }
async buildApiLambda(apiBuildManifest: OriginRequestApiHandlerManifest): Promise<void[]> { async prepareBuildManifest(): Promise<OriginRequestHandlerManifest> {
const allApiPages = [
...Object.values(apiBuildManifest.apis.nonDynamic),
...Object.values(apiBuildManifest.apis.dynamic).map((entry) => entry.file),
];
const apiPages = Object.values(allApiPages).map((pageFile) =>
join(this.serverlessDir, pageFile)
);
const { fileList, reasons } = await nodeFileTrace(apiPages, {
base: process.cwd(),
});
const copyTraces = this.copyLambdaHandlerDependencies(fileList, reasons, API_LAMBDA_CODE_DIR);
return Promise.all([
...copyTraces,
copy(
require.resolve('@next-deploy/aws-lambda-builder/dist/api-handler.js'),
join(this.outputDir, API_LAMBDA_CODE_DIR, 'index.js')
),
copy(
require.resolve('@next-deploy/aws-lambda-builder/dist/compat.js'),
join(this.outputDir, API_LAMBDA_CODE_DIR, 'compat.js')
),
copy(
join(this.serverlessDir, 'pages/api'),
join(this.outputDir, API_LAMBDA_CODE_DIR, 'pages/api')
),
writeJson(join(this.outputDir, API_LAMBDA_CODE_DIR, 'manifest.json'), apiBuildManifest),
]);
}
async prepareBuildManifests(): Promise<{
defaultBuildManifest: OriginRequestDefaultHandlerManifest;
apiBuildManifest: OriginRequestApiHandlerManifest;
}> {
const pagesManifest = await this.readPagesManifest(); const pagesManifest = await this.readPagesManifest();
const buildId = await readFile(join(this.dotNextDir, 'BUILD_ID'), 'utf-8'); const buildId = await readFile(join(this.dotNextDir, 'BUILD_ID'), 'utf-8');
const defaultBuildManifest: OriginRequestDefaultHandlerManifest = { const defaultBuildManifest: OriginRequestHandlerManifest = {
buildId, buildId,
pages: { pages: {
ssr: { ssr: {
@ -225,19 +184,17 @@ class Builder {
dynamic: {}, dynamic: {},
nonDynamic: {}, nonDynamic: {},
}, },
},
publicFiles: {},
};
const apiBuildManifest: OriginRequestApiHandlerManifest = {
apis: { apis: {
dynamic: {}, dynamic: {},
nonDynamic: {}, nonDynamic: {},
}, },
},
publicFiles: {},
}; };
const ssrPages = defaultBuildManifest.pages.ssr; const ssrPages = defaultBuildManifest.pages.ssr;
const htmlPages = defaultBuildManifest.pages.html; const htmlPages = defaultBuildManifest.pages.html;
const apiPages = apiBuildManifest.apis; const apiPages = defaultBuildManifest.pages.apis;
const isHtmlPage = (path: string): boolean => path.endsWith('.html'); const isHtmlPage = (path: string): boolean => path.endsWith('.html');
const isApiPage = (path: string): boolean => path.startsWith('pages/api'); const isApiPage = (path: string): boolean => path.startsWith('pages/api');
@ -281,10 +238,7 @@ class Builder {
publicFiles.forEach((pf) => (defaultBuildManifest.publicFiles[`/${pf}`] = pf)); publicFiles.forEach((pf) => (defaultBuildManifest.publicFiles[`/${pf}`] = pf));
return { return defaultBuildManifest;
defaultBuildManifest,
apiBuildManifest,
};
} }
async cleanupDotNext(): Promise<void> { async cleanupDotNext(): Promise<void> {
@ -308,8 +262,7 @@ class Builder {
await this.cleanupDotNext(); await this.cleanupDotNext();
await emptyDir(join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR)); await emptyDir(join(this.outputDir, REQUEST_LAMBDA_CODE_DIR));
await emptyDir(join(this.outputDir, API_LAMBDA_CODE_DIR));
const { restoreUserConfig } = await createServerlessConfig(cwd, join(this.nextConfigDir)); const { restoreUserConfig } = await createServerlessConfig(cwd, join(this.nextConfigDir));
@ -337,17 +290,9 @@ class Builder {
await restoreUserConfig(); await restoreUserConfig();
} }
const { defaultBuildManifest, apiBuildManifest } = await this.prepareBuildManifests(); const defaultBuildManifest = await this.prepareBuildManifest();
await this.buildDefaultLambda(defaultBuildManifest); await this.buildRequestLambda(defaultBuildManifest);
const hasAPIPages =
Object.keys(apiBuildManifest.apis.nonDynamic).length > 0 ||
Object.keys(apiBuildManifest.apis.dynamic).length > 0;
if (hasAPIPages) {
await this.buildApiLambda(apiBuildManifest);
}
} }
} }

View file

@ -12,14 +12,14 @@ import {
} from 'aws-lambda'; } from 'aws-lambda';
import { PrerenderManifest as PrerenderManifestType } from 'next/dist/build/index'; import { PrerenderManifest as PrerenderManifestType } from 'next/dist/build/index';
import { OriginRequestEvent, OriginRequestDefaultHandlerManifest } from '../types'; import { OriginRequestEvent, OriginRequestHandlerManifest } from '../types';
const addS3HostHeader = (req: CloudFrontRequest, s3DomainName: string): void => { const addS3HostHeader = (req: CloudFrontRequest, s3DomainName: string): void => {
req.headers['host'] = [{ key: 'host', value: s3DomainName }]; req.headers['host'] = [{ key: 'host', value: s3DomainName }];
}; };
const isDataRequest = (uri: string): boolean => uri.startsWith('/_next/data'); const isDataRequest = (uri: string): boolean => uri.startsWith('/_next/data');
const isApiRequest = (uri: string): boolean => uri.startsWith('/api');
const normaliseUri = (uri: string): string => (uri === '/' ? '/index' : uri); const normaliseUri = (uri: string): string => (uri === '/' ? '/index' : uri);
const normaliseS3OriginDomain = (s3Origin: CloudFrontS3Origin): string => { const normaliseS3OriginDomain = (s3Origin: CloudFrontS3Origin): string => {
@ -38,14 +38,13 @@ const normaliseS3OriginDomain = (s3Origin: CloudFrontS3Origin): string => {
return s3Origin.domainName; return s3Origin.domainName;
}; };
const router = (manifest: OriginRequestDefaultHandlerManifest): ((uri: string) => string) => { const router = (manifest: OriginRequestHandlerManifest): ((uri: string) => string | null) => {
const { const {
pages: { ssr, html }, pages: { ssr, html, apis },
} = manifest; } = manifest;
const allDynamicRoutes = { ...ssr.dynamic, ...html.dynamic, ...apis.dynamic };
const allDynamicRoutes = { ...ssr.dynamic, ...html.dynamic }; return (uri: string): string | null => {
return (uri: string): string => {
let normalizedUri = uri; let normalizedUri = uri;
if (isDataRequest(uri)) { if (isDataRequest(uri)) {
@ -67,6 +66,10 @@ const router = (manifest: OriginRequestDefaultHandlerManifest): ((uri: string) =
} }
} }
if (isApiRequest(uri)) {
return null;
}
// only use the 404 page if the project exports it // only use the 404 page if the project exports it
if (html.nonDynamic['/404'] !== undefined) { if (html.nonDynamic['/404'] !== undefined) {
return 'pages/404.html'; return 'pages/404.html';
@ -81,7 +84,7 @@ export const handler = async (
): Promise<CloudFrontResultResponse | CloudFrontRequest> => { ): Promise<CloudFrontResultResponse | CloudFrontRequest> => {
const { request } = event.Records[0].cf; const { request } = event.Records[0].cf;
const uri = normaliseUri(request.uri); const uri = normaliseUri(request.uri);
const manifest: OriginRequestDefaultHandlerManifest = Manifest; const manifest: OriginRequestHandlerManifest = Manifest;
const prerenderManifest: PrerenderManifestType = PrerenderManifest; const prerenderManifest: PrerenderManifestType = PrerenderManifest;
const { pages, publicFiles } = manifest; const { pages, publicFiles } = manifest;
const isStaticPage = pages.html.nonDynamic[uri]; const isStaticPage = pages.html.nonDynamic[uri];
@ -108,20 +111,28 @@ export const handler = async (
const pagePath = router(manifest)(uri); const pagePath = router(manifest)(uri);
if (!pagePath) {
return {
status: '404',
};
}
if (pagePath.endsWith('.html')) { if (pagePath.endsWith('.html')) {
s3Origin.path = '/static-pages'; s3Origin.path = '/static-pages';
request.uri = pagePath.replace('pages', ''); request.uri = pagePath.replace('pages', '');
addS3HostHeader(request, normalizedS3DomainName); addS3HostHeader(request, normalizedS3DomainName);
return request; return request;
} }
// eslint-disable-next-line
const page = require(`./${pagePath}`); const page = require(`./${pagePath}`);
const { req, res, responsePromise } = lambdaAtEdgeCompat(event.Records[0].cf); const { req, res, responsePromise } = lambdaAtEdgeCompat(event.Records[0].cf);
if (isDataRequest(uri)) { if (isApiRequest(uri)) {
page.default(req, res);
} else if (isDataRequest(uri)) {
const { renderOpts } = await page.renderReqToHTML(req, res, 'passthrough'); const { renderOpts } = await page.renderReqToHTML(req, res, 'passthrough');
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(renderOpts.pageData)); res.end(JSON.stringify(renderOpts.pageData));
} else { } else {

View file

@ -12,16 +12,7 @@ type DynamicPageKeyValue = {
}; };
}; };
type OriginRequestApiHandlerManifest = { type OriginRequestHandlerManifest = {
apis: {
dynamic: DynamicPageKeyValue;
nonDynamic: {
[key: string]: string;
};
};
};
type OriginRequestDefaultHandlerManifest = {
buildId: string; buildId: string;
pages: { pages: {
ssr: { ssr: {
@ -31,10 +22,16 @@ type OriginRequestDefaultHandlerManifest = {
}; };
}; };
html: { html: {
dynamic: DynamicPageKeyValue;
nonDynamic: { nonDynamic: {
[path: string]: string; [path: string]: string;
}; };
};
apis: {
dynamic: DynamicPageKeyValue; dynamic: DynamicPageKeyValue;
nonDynamic: {
[key: string]: string;
};
}; };
}; };
publicFiles: { publicFiles: {

View file

@ -1,3 +1 @@
const component = require('./dist/component.js'); module.exports = require('./dist/component.js').default;
module.exports = component.default;

View file

@ -1,3 +1 @@
const component = require('./dist/component.js'); module.exports = require('./dist/component.js').default;
module.exports = component.default;

View file

@ -130,7 +130,7 @@ class Context {
return credentials; return credentials;
} }
close(reason: string, message?: string): void { close(reason: string, error?: Error): void {
// Skip if not active // Skip if not active
process.stdout.write(ansiEscapes.cursorShow); process.stdout.write(ansiEscapes.cursorShow);
if (!this.isStatusEngineActive()) { if (!this.isStatusEngineActive()) {
@ -138,7 +138,7 @@ class Context {
process.exit(0); process.exit(0);
} }
return this.statusEngineStop(reason, message); return this.statusEngineStop(reason, error);
} }
getRelativeVerticalCursorPosition(contentString: string): number { getRelativeVerticalCursorPosition(contentString: string): number {
@ -172,11 +172,12 @@ class Context {
return this.statusEngine(); return this.statusEngine();
} }
statusEngineStop(reason: string, message?: string): void { statusEngineStop(reason: string, error?: Error): void {
this.metrics.status.running = false; this.metrics.status.running = false;
let message = '';
if (reason === 'error' && message) { if (reason === 'error' && error) {
message = red(message); message = red(`${error.stack || error.message}`);
} else if (reason === 'cancel') { } else if (reason === 'cancel') {
message = red('Cancelled ❌'); message = red('Cancelled ❌');
} else if (reason === 'done') { } else if (reason === 'done') {
@ -325,7 +326,7 @@ class Context {
console.log(); console.log();
// Write Error // Write Error
console.log(`${red('error:')}`); console.log(`${red('Error:')}`);
console.log(` `, error); console.log(` `, error);

View file

@ -76,7 +76,6 @@ const deploy = async (deployConfigPath: string, methodName = 'default'): Promise
context.close('done'); context.close('done');
process.exit(0); process.exit(0);
} catch (e) { } catch (e) {
context.renderError(e);
context.close('error', e); context.close('error', e);
process.exit(1); process.exit(1);
} }

View file

@ -1,3 +1 @@
const component = require('./dist/component.js'); module.exports = require('./dist/component.js').default;
module.exports = component.default;

View file

@ -9,7 +9,7 @@ import { GithubInputs, DeploymentResult } from '../types';
class GithubComponent extends Component { class GithubComponent extends Component {
async default(inputs: GithubInputs = {}): Promise<DeploymentResult> { async default(inputs: GithubInputs = {}): Promise<DeploymentResult> {
if (inputs.build !== false) { if (inputs.buildOptions !== false) {
await this.build(inputs); await this.build(inputs);
} }
@ -19,17 +19,19 @@ class GithubComponent extends Component {
async build(inputs: GithubInputs = {}): Promise<void> { async build(inputs: GithubInputs = {}): Promise<void> {
const nextConfigPath = inputs.nextConfigDir ? resolve(inputs.nextConfigDir) : process.cwd(); const nextConfigPath = inputs.nextConfigDir ? resolve(inputs.nextConfigDir) : process.cwd();
const buildCwd = const buildCwd =
typeof inputs.build === 'boolean' || typeof inputs.build === 'undefined' || !inputs.build.cwd typeof inputs.buildOptions === 'boolean' ||
typeof inputs.buildOptions === 'undefined' ||
!inputs.buildOptions.cwd
? nextConfigPath ? nextConfigPath
: resolve(inputs.build.cwd); : resolve(inputs.buildOptions.cwd);
const buildConfig: BuildOptions = { const buildConfig: BuildOptions = {
enabled: inputs.build enabled: inputs.buildOptions
? // @ts-ignore ? // @ts-ignore
inputs.build !== false && inputs.build.enabled !== false inputs.buildOptions !== false && inputs.buildOptions.enabled !== false
: true, : true,
cmd: 'node_modules/.bin/next', cmd: 'node_modules/.bin/next',
args: ['build'], args: ['build'],
...(typeof inputs.build === 'object' ? inputs.build : {}), ...(typeof inputs.buildOptions === 'object' ? inputs.buildOptions : {}),
cwd: buildCwd, cwd: buildCwd,
}; };
@ -57,7 +59,7 @@ class GithubComponent extends Component {
const publishPromise = new Promise((resolve, reject) => { const publishPromise = new Promise((resolve, reject) => {
publish( publish(
'out', 'out',
{ message: 'Next Deployment Update', ...inputs.publishOptions, dotfiles: true }, { message: 'Next Deployment Update', dotfiles: true, ...inputs.publishOptions },
(err) => { (err) => {
if (err) { if (err) {
return reject(err); return reject(err);

View file

@ -2206,10 +2206,10 @@
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6"
integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=
"@types/node@*", "@types/node@>= 8", "@types/node@^14.0.22": "@types/node@*", "@types/node@>= 8", "@types/node@^14.0.23":
version "14.0.22" version "14.0.23"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.22.tgz#23ea4d88189cec7d58f9e6b66f786b215eb61bdc" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.23.tgz#676fa0883450ed9da0bb24156213636290892806"
integrity sha512-emeGcJvdiZ4Z3ohbmw93E/64jRzUHAItSHt8nF7M4TGgQTiWqFVGB8KNpLGFmUHmHLvjvBgFwVlqNcq+VuGv9g== integrity sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw==
"@types/normalize-package-data@^2.4.0": "@types/normalize-package-data@^2.4.0":
version "2.4.0" version "2.4.0"
@ -2305,52 +2305,52 @@
"@types/webpack-sources" "*" "@types/webpack-sources" "*"
source-map "^0.6.0" source-map "^0.6.0"
"@typescript-eslint/eslint-plugin@^3.6.0": "@typescript-eslint/eslint-plugin@^3.6.1":
version "3.6.0" version "3.6.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.6.0.tgz#ba2b6cae478b8fca3f2e58ff1313e4198eea2d8a" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.6.1.tgz#5ced8fd2087fbb83a76973dea4a0d39d9cb4a642"
integrity sha512-ubHlHVt1lsPQB/CZdEov9XuOFhNG9YRC//kuiS1cMQI6Bs1SsqKrEmZnpgRwthGR09/kEDtr9MywlqXyyYd8GA== integrity sha512-06lfjo76naNeOMDl+mWG9Fh/a0UHKLGhin+mGaIw72FUMbMGBkdi/FEJmgEDzh4eE73KIYzHWvOCYJ0ak7nrJQ==
dependencies: dependencies:
"@typescript-eslint/experimental-utils" "3.6.0" "@typescript-eslint/experimental-utils" "3.6.1"
debug "^4.1.1" debug "^4.1.1"
functional-red-black-tree "^1.0.1" functional-red-black-tree "^1.0.1"
regexpp "^3.0.0" regexpp "^3.0.0"
semver "^7.3.2" semver "^7.3.2"
tsutils "^3.17.1" tsutils "^3.17.1"
"@typescript-eslint/experimental-utils@3.6.0": "@typescript-eslint/experimental-utils@3.6.1":
version "3.6.0" version "3.6.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.6.0.tgz#0138152d66e3e53a6340f606793fb257bf2d76a1" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.6.1.tgz#b5a2738ebbceb3fa90c5b07d50bb1225403c4a54"
integrity sha512-4Vdf2hvYMUnTdkCNZu+yYlFtL2v+N2R7JOynIOkFbPjf9o9wQvRwRkzUdWlFd2YiiUwJLbuuLnl5civNg5ykOQ== integrity sha512-oS+hihzQE5M84ewXrTlVx7eTgc52eu+sVmG7ayLfOhyZmJ8Unvf3osyFQNADHP26yoThFfbxcibbO0d2FjnYhg==
dependencies: dependencies:
"@types/json-schema" "^7.0.3" "@types/json-schema" "^7.0.3"
"@typescript-eslint/types" "3.6.0" "@typescript-eslint/types" "3.6.1"
"@typescript-eslint/typescript-estree" "3.6.0" "@typescript-eslint/typescript-estree" "3.6.1"
eslint-scope "^5.0.0" eslint-scope "^5.0.0"
eslint-utils "^2.0.0" eslint-utils "^2.0.0"
"@typescript-eslint/parser@^3.6.0": "@typescript-eslint/parser@^3.6.1":
version "3.6.0" version "3.6.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.6.0.tgz#79b5232e1a2d06f1fc745942b690cd87aca7b60e" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.6.1.tgz#216e8adf4ee9c629f77c985476a2ea07fb80e1dc"
integrity sha512-taghDxuLhbDAD1U5Fk8vF+MnR0yiFE9Z3v2/bYScFb0N1I9SK8eKHkdJl1DAD48OGFDMFTeOTX0z7g0W6SYUXw== integrity sha512-SLihQU8RMe77YJ/jGTqOt0lMq7k3hlPVfp7v/cxMnXA9T0bQYoMDfTsNgHXpwSJM1Iq2aAJ8WqekxUwGv5F67Q==
dependencies: dependencies:
"@types/eslint-visitor-keys" "^1.0.0" "@types/eslint-visitor-keys" "^1.0.0"
"@typescript-eslint/experimental-utils" "3.6.0" "@typescript-eslint/experimental-utils" "3.6.1"
"@typescript-eslint/types" "3.6.0" "@typescript-eslint/types" "3.6.1"
"@typescript-eslint/typescript-estree" "3.6.0" "@typescript-eslint/typescript-estree" "3.6.1"
eslint-visitor-keys "^1.1.0" eslint-visitor-keys "^1.1.0"
"@typescript-eslint/types@3.6.0": "@typescript-eslint/types@3.6.1":
version "3.6.0" version "3.6.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.6.0.tgz#4bd6eee55d2f9d35a4b36c4804be1880bf68f7bc" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.6.1.tgz#87600fe79a1874235d3cc1cf5c7e1a12eea69eee"
integrity sha512-JwVj74ohUSt0ZPG+LZ7hb95fW8DFOqBuR6gE7qzq55KDI3BepqsCtHfBIoa0+Xi1AI7fq5nCu2VQL8z4eYftqg== integrity sha512-NPxd5yXG63gx57WDTW1rp0cF3XlNuuFFB5G+Kc48zZ+51ZnQn9yjDEsjTPQ+aWM+V+Z0I4kuTFKjKvgcT1F7xQ==
"@typescript-eslint/typescript-estree@3.6.0": "@typescript-eslint/typescript-estree@3.6.1":
version "3.6.0" version "3.6.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.6.0.tgz#9b4cab43f1192b64ff51530815b8919f166ce177" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.6.1.tgz#a5c91fcc5497cce7922ff86bc37d5e5891dcdefa"
integrity sha512-G57NDSABHjvob7zVV09ehWyD1K6/YUKjz5+AufObFyjNO4DVmKejj47MHjVHHlZZKgmpJD2yyH9lfCXHrPITFg== integrity sha512-G4XRe/ZbCZkL1fy09DPN3U0mR6SayIv1zSeBNquRFRk7CnVLgkC2ZPj8llEMJg5Y8dJ3T76SvTGtceytniaztQ==
dependencies: dependencies:
"@typescript-eslint/types" "3.6.0" "@typescript-eslint/types" "3.6.1"
"@typescript-eslint/visitor-keys" "3.6.0" "@typescript-eslint/visitor-keys" "3.6.1"
debug "^4.1.1" debug "^4.1.1"
glob "^7.1.6" glob "^7.1.6"
is-glob "^4.0.1" is-glob "^4.0.1"
@ -2358,10 +2358,10 @@
semver "^7.3.2" semver "^7.3.2"
tsutils "^3.17.1" tsutils "^3.17.1"
"@typescript-eslint/visitor-keys@3.6.0": "@typescript-eslint/visitor-keys@3.6.1":
version "3.6.0" version "3.6.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.6.0.tgz#44185eb0cc47651034faa95c5e2e8b314ecebb26" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.6.1.tgz#5c57a7772f4dd623cfeacc219303e7d46f963b37"
integrity sha512-p1izllL2Ubwunite0ITjubuMQRBGgjdVYwyG7lXPX8GbrA6qF0uwSRz9MnXZaHMxID4948gX0Ez8v9tUDi/KfQ== integrity sha512-qC8Olwz5ZyMTZrh4Wl3K4U6tfms0R/mzU4/5W3XeUZptVraGVmbptJbn6h2Ey6Rb3hOs3zWoAUebZk8t47KGiQ==
dependencies: dependencies:
eslint-visitor-keys "^1.1.0" eslint-visitor-keys "^1.1.0"
@ -4597,9 +4597,9 @@ ecc-jsbn@~0.1.1:
safer-buffer "^2.1.0" safer-buffer "^2.1.0"
electron-to-chromium@^1.3.413, electron-to-chromium@^1.3.488: electron-to-chromium@^1.3.413, electron-to-chromium@^1.3.488:
version "1.3.496" version "1.3.497"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.496.tgz#3f43d32930481d82ad3663d79658e7c59a58af0b" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.497.tgz#de00f2f2f44c258c4577fbfbd5124b94c18bfa44"
integrity sha512-TXY4mwoyowwi4Lsrq9vcTUYBThyc1b2hXaTZI13p8/FRhY2CTaq5lK+DVjhYkKiTLsKt569Xes+0J5JsVXFurQ== integrity sha512-sPdW5bUDZwiFtoonuZCUwRGzsZmKzcLM0bMVhp6SMCfUG+B3faENLx3cE+o+K0Jl+MPuNA9s9cScyFjOlixZpQ==
elliptic@^6.0.0, elliptic@^6.5.2: elliptic@^6.0.0, elliptic@^6.5.2:
version "6.5.3" version "6.5.3"
@ -8946,9 +8946,9 @@ regexpu-core@^4.7.0:
unicode-match-property-value-ecmascript "^1.2.0" unicode-match-property-value-ecmascript "^1.2.0"
registry-auth-token@^4.0.0: registry-auth-token@^4.0.0:
version "4.1.1" version "4.2.0"
resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.1.1.tgz#40a33be1e82539460f94328b0f7f0f84c16d9479" resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.0.tgz#1d37dffda72bbecd0f581e4715540213a65eb7da"
integrity sha512-9bKS7nTl9+/A1s7tnPeGrUpRcVY+LUh7bfFgzpndALdPfXQBfQV77rQVtqgUV3ti4vc/Ik81Ex8UJDWDQ12zQA== integrity sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w==
dependencies: dependencies:
rc "^1.2.8" rc "^1.2.8"
@ -10211,9 +10211,9 @@ ts-pnp@^1.1.6:
integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==
ts-toolbelt@^6.3.3: ts-toolbelt@^6.3.3:
version "6.12.1" version "6.12.2"
resolved "https://registry.yarnpkg.com/ts-toolbelt/-/ts-toolbelt-6.12.1.tgz#bfcfaf0e1cc7416bcfc412c9340233f57a1f86c4" resolved "https://registry.yarnpkg.com/ts-toolbelt/-/ts-toolbelt-6.12.2.tgz#d9007906d903b89d7bf210f43fbefe2a2bfd5734"
integrity sha512-Y4ZMdw+CzHnzFp7yp8axTDl6G78ypeS1XcaarLoXpnW0McYR7kDLndU2tk7nIEdM99yoHjaRSbL09ULZMtN5RA== integrity sha512-Z9VWXJ32UpLrjw5OqieJ944heNN5gkVm69VLvVf9GgrdxoxPiM4ughyYFip6pIDqRnrVuiegMTD48zxnOW5j/Q==
tslib@^1.8.1, tslib@^1.9.0: tslib@^1.8.1, tslib@^1.9.0:
version "1.13.0" version "1.13.0"
@ -10641,7 +10641,14 @@ which-pm-runs@^1.0.0:
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
which@^1.2.9, which@^1.3.1, which@^2.0.1: which@^1.2.9, which@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
dependencies:
isexe "^2.0.0"
which@^2.0.1:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==