From d26ca65ea7695fd5e4798da43717a0499c3c199f Mon Sep 17 00:00:00 2001 From: danielconde Date: Sat, 29 Jun 2019 16:27:45 +0100 Subject: [PATCH 1/7] test: burn mocks to the groundgit s --- packages/serverless-nextjs-plugin/index.js | 3 +- .../__tests__/addCustomStackResources.test.js | 194 ++++-------------- .../lib/addCustomStackResources.js | 36 ++-- .../resources/cloudfront.yml | 43 ++++ 4 files changed, 102 insertions(+), 174 deletions(-) create mode 100644 packages/serverless-nextjs-plugin/resources/cloudfront.yml diff --git a/packages/serverless-nextjs-plugin/index.js b/packages/serverless-nextjs-plugin/index.js index 04fde6c37f..834abbef1d 100644 --- a/packages/serverless-nextjs-plugin/index.js +++ b/packages/serverless-nextjs-plugin/index.js @@ -51,7 +51,8 @@ class ServerlessNextJsPlugin { const defaults = { routes: [], nextConfigDir: "./", - uploadBuildAssets: true + uploadBuildAssets: true, + cloudFront: false }; const userConfig = this.serverless.service.custom["serverless-nextjs"][ diff --git a/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js b/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js index f0381c58bb..b26ce66202 100644 --- a/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js +++ b/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js @@ -1,8 +1,6 @@ const { when } = require("jest-when"); -const yaml = require("js-yaml"); const fse = require("fs-extra"); const clone = require("lodash.clonedeep"); -const merge = require("lodash.merge"); const path = require("path"); const addCustomStackResources = require("../addCustomStackResources"); const ServerlessPluginBuilder = require("../../utils/test/ServerlessPluginBuilder"); @@ -10,158 +8,18 @@ const getAssetsBucketName = require("../getAssetsBucketName"); const logger = require("../../utils/logger"); jest.mock("../getAssetsBucketName"); -jest.mock("fs-extra"); -jest.mock("js-yaml"); jest.mock("../../utils/logger"); describe("addCustomStackResources", () => { const bucketName = "bucket-123"; const bucketUrl = `https://s3.amazonaws.com/${bucketName}`; - const s3ResourcesYmlString = ` - Resources: - NextStaticAssetsS3Bucket:... - `; - const proxyResourcesYmlString = ` - Resources: - ProxyResource:... - `; - const staticProxyResourcesYmlString = ` - resources: - Resources: - StaticAssetsProxyResource:... - `; - - const nextProxyResourcesYmlString = ` - resources: - Resources: - NextStaticAssetsProxyResource:... - `; - - let s3Resources; - let baseProxyResource; - let baseStaticProxyResource; - let baseNextProxyResource; - beforeEach(() => { - s3Resources = { - Resources: { - NextStaticAssetsS3Bucket: { - Properties: { - BucketName: "TO_BE_REPLACED" - } - } - } - }; - - baseProxyResource = { - Resources: { - ProxyResource: { - Properties: { - PathPart: "TO_BE_REPLACED" - } - }, - ProxyMethod: { - Properties: { - Integration: { - Uri: "TO_BE_REPLACED" - }, - ResourceId: { - Ref: "TO_BE_REPLACED" - } - } - } - } - }; - - baseStaticProxyResource = { - resources: { - Resources: { - StaticAssetsProxyParentResource: { - Properties: { - PathPart: "TO_BE_REPLACED" - } - }, - StaticAssetsProxyResource: { - Properties: { - PathPart: "TO_BE_REPLACED" - } - }, - StaticAssetsProxyMethod: { - Properties: { - Integration: { - Uri: "TO_BE_REPLACED" - }, - ResourceId: { - Ref: "TO_BE_REPLACED" - } - } - } - } - } - }; - - baseNextProxyResource = { - resources: { - Resources: { - NextStaticAssetsProxyParentResource: { - Properties: { - PathPart: "TO_BE_REPLACED" - } - }, - NextStaticAssetsProxyResource: { - Properties: { - PathPart: "TO_BE_REPLACED" - } - }, - NextStaticAssetsProxyMethod: { - Properties: { - Integration: { - Uri: "TO_BE_REPLACED" - }, - ResourceId: { - Ref: "TO_BE_REPLACED" - } - } - } - } - } - }; + fse.pathExists = jest.fn(); + fse.readdir = jest.fn(); fse.pathExists.mockResolvedValue(false); - when(fse.readFile) - .calledWith(expect.stringContaining("assets-bucket.yml"), "utf-8") - .mockResolvedValueOnce(s3ResourcesYmlString); - - when(yaml.safeLoad) - .calledWith(s3ResourcesYmlString, expect.any(Object)) - .mockReturnValueOnce(s3Resources); - - when(fse.readFile) - .calledWith(expect.stringContaining("api-gw-proxy.yml"), "utf-8") - .mockResolvedValueOnce(proxyResourcesYmlString); - - when(yaml.safeLoad) - .calledWith(proxyResourcesYmlString, expect.any(Object)) - .mockReturnValueOnce(baseProxyResource); - - when(fse.readFile) - .calledWith(expect.stringContaining("api-gw-next.yml"), "utf-8") - .mockResolvedValueOnce(nextProxyResourcesYmlString); - - when(yaml.safeLoad) - .calledWith(nextProxyResourcesYmlString, expect.any(Object)) - .mockReturnValueOnce(baseNextProxyResource); - - when(fse.readFile) - .calledWith(expect.stringContaining("api-gw-static.yml"), "utf-8") - .mockResolvedValueOnce(staticProxyResourcesYmlString); - - when(yaml.safeLoad) - .calledWith(staticProxyResourcesYmlString, expect.any(Object)) - .mockReturnValueOnce(baseStaticProxyResource); - getAssetsBucketName.mockReturnValueOnce(bucketName); }); @@ -170,11 +28,9 @@ describe("addCustomStackResources", () => { const coreCfTemplate = { Resources: { - foo: "bar" + existingResource: "existingValue" } }; - const s3ResourcesWithBucketName = clone(s3Resources); - s3ResourcesWithBucketName.Resources.NextStaticAssetsS3Bucket.Properties.BucketName = bucketName; const plugin = new ServerlessPluginBuilder().build(); @@ -186,13 +42,15 @@ describe("addCustomStackResources", () => { expect(logger.log).toBeCalledWith( expect.stringContaining(`Found bucket "${bucketName}"`) ); + const { service } = plugin.serverless; + const { NextStaticAssetsS3Bucket } = service.resources.Resources; + + expect(NextStaticAssetsS3Bucket.Properties.BucketName).toEqual( + bucketName + ); expect( - plugin.serverless.service.resources.Resources.NextStaticAssetsS3Bucket - .Properties.BucketName - ).toEqual(bucketName); - expect( - plugin.serverless.service.provider.coreCloudFormationTemplate - ).toEqual(merge(coreCfTemplate, s3ResourcesWithBucketName)); + service.provider.coreCloudFormationTemplate.Resources.existingResource + ).toEqual("existingValue"); }); }); @@ -332,6 +190,32 @@ describe("addCustomStackResources", () => { }); }); + describe.skip("when cloudfront is enabled", () => { + it("adds distribution", () => { + expect.assertions(1); + + const plugin = new ServerlessPluginBuilder() + .withPluginConfig({ + cloudFront: true + }) + .build(); + + const publicDir = path.join( + plugin.getPluginConfigValue("nextConfigDir"), + "public" + ); + + when(fse.pathExists) + .calledWith(publicDir) + .mockResolvedValue(false); + + return addCustomStackResources.call(plugin).then(() => { + const { Resources } = plugin.serverless.service.resources; + expect(Object.keys(Resources)).toHaveLength(2); // S3 bucket and CloudFront distribution + }); + }); + }); + describe("When no bucket available", () => { beforeEach(() => { getAssetsBucketName.mockReset(); @@ -339,14 +223,12 @@ describe("addCustomStackResources", () => { }); it("doesn't add S3 bucket to resources", () => { - expect.assertions(5); + expect.assertions(3); const plugin = new ServerlessPluginBuilder().build(); return addCustomStackResources.call(plugin).then(() => { expect(logger.log).not.toBeCalled(); - expect(fse.readFile).not.toBeCalled(); - expect(yaml.safeLoad).not.toBeCalled(); expect(plugin.serverless.service.resources).toEqual(undefined); expect( plugin.serverless.service.provider.coreCloudFormationTemplate diff --git a/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js b/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js index b8520b20b2..1381bc461c 100644 --- a/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js +++ b/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js @@ -150,19 +150,27 @@ const addCustomStackResources = async function() { const resourceConfiguration = { bucketName, bucketBaseUrl }; - const staticResources = await getStaticRouteProxyResources.call( - this, - resourceConfiguration - ); - const nextResources = await getNextRouteProxyResources.call( - this, - resourceConfiguration - ); - const publicResources = await getPublicRouteProxyResources.call( - this, - resourceConfiguration + let assetsBucketResource = await loadYml( + path.join(__dirname, "../resources/assets-bucket.yml") ); + assetsBucketResource.Resources.NextStaticAssetsS3Bucket.Properties.BucketName = bucketName; + + const cloudFront = this.getPluginConfigValue("cloudFront"); + + // if (cloudFront) { + + // return; + // } + + // api gateway -> S3 proxying + + const [staticResources, nextResources, publicResources] = await Promise.all([ + getStaticRouteProxyResources.call(this, resourceConfiguration), + getNextRouteProxyResources.call(this, resourceConfiguration), + getPublicRouteProxyResources.call(this, resourceConfiguration) + ]); + const proxyResources = { Resources: { ...staticResources, @@ -171,12 +179,6 @@ const addCustomStackResources = async function() { } }; - let assetsBucketResource = await loadYml( - path.join(__dirname, "../resources/assets-bucket.yml") - ); - - assetsBucketResource.Resources.NextStaticAssetsS3Bucket.Properties.BucketName = bucketName; - this.serverless.service.resources = this.serverless.service.resources || { Resources: {} }; diff --git a/packages/serverless-nextjs-plugin/resources/cloudfront.yml b/packages/serverless-nextjs-plugin/resources/cloudfront.yml new file mode 100644 index 0000000000..6aa3c2e18f --- /dev/null +++ b/packages/serverless-nextjs-plugin/resources/cloudfront.yml @@ -0,0 +1,43 @@ +AWSTemplateFormatVersion: "2010-09-09" +Resources: + myDistribution: + Type: AWS::CloudFront::Distribution + Properties: + DistributionConfig: + Origins: + - DomainName: danielcondemarinbucket.s3.amazonaws.com + Id: S3StaticOrigin + S3OriginConfig: + OriginAccessIdentity: "" + - DomainName: nbw1qo4fgh.execute-api.us-east-1.amazonaws.com + Id: ApiGatewayOrigin + OriginPath: /dev + CustomOriginConfig: + HTTPSPort: 443 + OriginProtocolPolicy: https-only + Enabled: "true" + DefaultCacheBehavior: + AllowedMethods: + - GET + - HEAD + - OPTIONS + TargetOriginId: ApiGatewayOrigin + ForwardedValues: + QueryString: "true" + Cookies: + Forward: all + ViewerProtocolPolicy: allow-all + CacheBehaviors: + - AllowedMethods: + - GET + - HEAD + - OPTIONS + TargetOriginId: S3StaticOrigin + ForwardedValues: + QueryString: "false" + Cookies: + Forward: none + ViewerProtocolPolicy: allow-all + MinTTL: "50" + PathPattern: static/ + PriceClass: PriceClass_All From 1831b70ba0d31eafa7dbb684c847bd9928688e2c Mon Sep 17 00:00:00 2001 From: danielconde Date: Sat, 29 Jun 2019 20:54:45 +0100 Subject: [PATCH 2/7] remove https from origin domain name and few other fixes --- .../__tests__/addCustomStackResources.test.js | 372 +++++++++++------- .../lib/addCustomStackResources.js | 113 ++++-- .../lib/checkForChanges.js | 1 + .../resources/cloudfront.yml | 15 +- 4 files changed, 322 insertions(+), 179 deletions(-) diff --git a/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js b/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js index b26ce66202..7a20558572 100644 --- a/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js +++ b/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js @@ -11,189 +11,194 @@ jest.mock("../getAssetsBucketName"); jest.mock("../../utils/logger"); describe("addCustomStackResources", () => { - const bucketName = "bucket-123"; - const bucketUrl = `https://s3.amazonaws.com/${bucketName}`; - beforeEach(() => { fse.pathExists = jest.fn(); fse.readdir = jest.fn(); fse.pathExists.mockResolvedValue(false); - - getAssetsBucketName.mockReturnValueOnce(bucketName); }); - it("adds S3 bucket to resources", () => { - expect.assertions(3); + describe("When cloudfront is disabled and S3 bucket is configured", () => { + const bucketName = "bucket-123"; + const bucketUrl = `https://s3.amazonaws.com/${bucketName}`; - const coreCfTemplate = { - Resources: { - existingResource: "existingValue" - } - }; + beforeEach(() => { + getAssetsBucketName.mockReturnValueOnce(bucketName); + }); - const plugin = new ServerlessPluginBuilder().build(); + it("adds S3 bucket to resources", () => { + expect.assertions(3); - plugin.serverless.service.provider.coreCloudFormationTemplate = clone( - coreCfTemplate - ); + const coreCfTemplate = { + Resources: { + existingResource: "existingValue" + } + }; - return addCustomStackResources.call(plugin).then(() => { - expect(logger.log).toBeCalledWith( - expect.stringContaining(`Found bucket "${bucketName}"`) - ); - const { service } = plugin.serverless; - const { NextStaticAssetsS3Bucket } = service.resources.Resources; + const plugin = new ServerlessPluginBuilder().build(); - expect(NextStaticAssetsS3Bucket.Properties.BucketName).toEqual( - bucketName + plugin.serverless.service.provider.coreCloudFormationTemplate = clone( + coreCfTemplate ); - expect( - service.provider.coreCloudFormationTemplate.Resources.existingResource - ).toEqual("existingValue"); + + return addCustomStackResources.call(plugin).then(() => { + expect(logger.log).toBeCalledWith( + expect.stringContaining(`Found bucket "${bucketName}"`) + ); + const { service } = plugin.serverless; + const { NextStaticAssetsS3Bucket } = service.resources.Resources; + + expect(NextStaticAssetsS3Bucket.Properties.BucketName).toEqual( + bucketName + ); + expect( + service.provider.coreCloudFormationTemplate.Resources.existingResource + ).toEqual("existingValue"); + }); }); - }); - it("adds proxy routes for static directory", () => { - expect.assertions(2); + it("adds proxy routes for static directory", () => { + expect.assertions(2); - const plugin = new ServerlessPluginBuilder().build(); + const plugin = new ServerlessPluginBuilder().build(); - const staticDir = path.join( - plugin.getPluginConfigValue("nextConfigDir"), - "static" - ); + const staticDir = path.join( + plugin.getPluginConfigValue("nextConfigDir"), + "static" + ); - when(fse.pathExists) - .calledWith(staticDir) - .mockResolvedValue(true); + when(fse.pathExists) + .calledWith(staticDir) + .mockResolvedValue(true); - return addCustomStackResources.call(plugin).then(() => { - const resources = plugin.serverless.service.resources.Resources; - expect(Object.keys(resources)).toEqual( - expect.arrayContaining([ - "StaticAssetsProxyParentResource", - "StaticAssetsProxyResource", - "StaticAssetsProxyMethod" - ]) - ); - expect( - resources.StaticAssetsProxyMethod.Properties.Integration.Uri - ).toEqual("https://s3.amazonaws.com/bucket-123/static/{proxy}"); + return addCustomStackResources.call(plugin).then(() => { + const resources = plugin.serverless.service.resources.Resources; + expect(Object.keys(resources)).toEqual( + expect.arrayContaining([ + "StaticAssetsProxyParentResource", + "StaticAssetsProxyResource", + "StaticAssetsProxyMethod" + ]) + ); + expect( + resources.StaticAssetsProxyMethod.Properties.Integration.Uri + ).toEqual("https://s3.amazonaws.com/bucket-123/static/{proxy}"); + }); }); - }); - it("adds proxy routes for nextjs assets", () => { - expect.assertions(2); + it("adds proxy routes for nextjs assets", () => { + expect.assertions(2); - const plugin = new ServerlessPluginBuilder().build(); + const plugin = new ServerlessPluginBuilder().build(); - return addCustomStackResources.call(plugin).then(() => { - const resources = plugin.serverless.service.resources.Resources; - expect(Object.keys(resources)).toEqual( - expect.arrayContaining([ - "NextStaticAssetsProxyParentResource", - "NextStaticAssetsProxyResource", - "NextStaticAssetsProxyMethod" - ]) - ); - expect( - resources.NextStaticAssetsProxyMethod.Properties.Integration.Uri - ).toEqual("https://s3.amazonaws.com/bucket-123/_next/{proxy}"); + return addCustomStackResources.call(plugin).then(() => { + const resources = plugin.serverless.service.resources.Resources; + expect(Object.keys(resources)).toEqual( + expect.arrayContaining([ + "NextStaticAssetsProxyParentResource", + "NextStaticAssetsProxyResource", + "NextStaticAssetsProxyMethod" + ]) + ); + expect( + resources.NextStaticAssetsProxyMethod.Properties.Integration.Uri + ).toEqual("https://s3.amazonaws.com/bucket-123/_next/{proxy}"); + }); }); - }); - - it("adds proxy route to each file in the public folder", () => { - expect.assertions(8); - const plugin = new ServerlessPluginBuilder().build(); - const publicDir = path.join( - plugin.getPluginConfigValue("nextConfigDir"), - "public" - ); + it("adds proxy route to each file in the public folder", () => { + expect.assertions(8); - when(fse.pathExists) - .calledWith(publicDir) - .mockResolvedValue(true); + const plugin = new ServerlessPluginBuilder().build(); + const publicDir = path.join( + plugin.getPluginConfigValue("nextConfigDir"), + "public" + ); - when(fse.readdir) - .calledWith(publicDir) - .mockResolvedValue(["robots.txt", "manifest.json"]); + when(fse.pathExists) + .calledWith(publicDir) + .mockResolvedValue(true); - return addCustomStackResources.call(plugin).then(() => { - const { - RobotsProxyMethod, - RobotsProxyResource, - ManifestProxyMethod, - ManifestProxyResource - } = plugin.serverless.service.resources.Resources; - - expect(RobotsProxyMethod.Properties.Integration.Uri).toEqual( - `${bucketUrl}/public/robots.txt` - ); - expect(RobotsProxyMethod.Properties.ResourceId.Ref).toEqual( - "RobotsProxyResource" - ); - expect(RobotsProxyResource.Properties.PathPart).toEqual("robots.txt"); - expect(logger.log).toBeCalledWith( - `Proxying robots.txt -> ${bucketUrl}/public/robots.txt` - ); + when(fse.readdir) + .calledWith(publicDir) + .mockResolvedValue(["robots.txt", "manifest.json"]); - expect(ManifestProxyMethod.Properties.Integration.Uri).toEqual( - `${bucketUrl}/public/manifest.json` - ); - expect(ManifestProxyMethod.Properties.ResourceId.Ref).toEqual( - "ManifestProxyResource" - ); - expect(ManifestProxyResource.Properties.PathPart).toEqual( - `manifest.json` - ); - expect(logger.log).toBeCalledWith( - `Proxying manifest.json -> ${bucketUrl}/public/manifest.json` - ); + return addCustomStackResources.call(plugin).then(() => { + const { + RobotsProxyMethod, + RobotsProxyResource, + ManifestProxyMethod, + ManifestProxyResource + } = plugin.serverless.service.resources.Resources; + + expect(RobotsProxyMethod.Properties.Integration.Uri).toEqual( + `${bucketUrl}/public/robots.txt` + ); + expect(RobotsProxyMethod.Properties.ResourceId.Ref).toEqual( + "RobotsProxyResource" + ); + expect(RobotsProxyResource.Properties.PathPart).toEqual("robots.txt"); + expect(logger.log).toBeCalledWith( + `Proxying robots.txt -> ${bucketUrl}/public/robots.txt` + ); + + expect(ManifestProxyMethod.Properties.Integration.Uri).toEqual( + `${bucketUrl}/public/manifest.json` + ); + expect(ManifestProxyMethod.Properties.ResourceId.Ref).toEqual( + "ManifestProxyResource" + ); + expect(ManifestProxyResource.Properties.PathPart).toEqual( + `manifest.json` + ); + expect(logger.log).toBeCalledWith( + `Proxying manifest.json -> ${bucketUrl}/public/manifest.json` + ); + }); }); - }); - - it("adds proxy route to resources with correct bucket url for the region", () => { - expect.assertions(2); - const euWestRegion = "eu-west-1"; - const bucketUrlIreland = `https://s3-${euWestRegion}.amazonaws.com/${bucketName}`; - const getRegion = jest.fn().mockReturnValueOnce(euWestRegion); + it("adds proxy route to resources with correct bucket url for the region", () => { + expect.assertions(2); - const plugin = new ServerlessPluginBuilder().build(); + const euWestRegion = "eu-west-1"; + const bucketUrlIreland = `https://s3-${euWestRegion}.amazonaws.com/${bucketName}`; + const getRegion = jest.fn().mockReturnValueOnce(euWestRegion); - const publicDir = path.join( - plugin.getPluginConfigValue("nextConfigDir"), - "public" - ); + const plugin = new ServerlessPluginBuilder().build(); - when(fse.pathExists) - .calledWith(publicDir) - .mockResolvedValue(true); + const publicDir = path.join( + plugin.getPluginConfigValue("nextConfigDir"), + "public" + ); - when(fse.readdir) - .calledWith(publicDir) - .mockResolvedValue(["robots.txt"]); + when(fse.pathExists) + .calledWith(publicDir) + .mockResolvedValue(true); - plugin.provider.getRegion = getRegion; + when(fse.readdir) + .calledWith(publicDir) + .mockResolvedValue(["robots.txt"]); - return addCustomStackResources.call(plugin).then(() => { - const { - RobotsProxyMethod - } = plugin.serverless.service.resources.Resources; + plugin.provider.getRegion = getRegion; - expect(getRegion).toBeCalled(); - expect(RobotsProxyMethod.Properties.Integration.Uri).toEqual( - `${bucketUrlIreland}/public/robots.txt` - ); + return addCustomStackResources.call(plugin).then(() => { + const { + RobotsProxyMethod + } = plugin.serverless.service.resources.Resources; + + expect(getRegion).toBeCalled(); + expect(RobotsProxyMethod.Properties.Integration.Uri).toEqual( + `${bucketUrlIreland}/public/robots.txt` + ); + }); }); }); - describe.skip("when cloudfront is enabled", () => { - it("adds distribution", () => { - expect.assertions(1); + describe("when cloudfront is enabled and S3 bucket is configured", () => { + let assetsBucketName = "foo.bar"; + let resources; + beforeEach(() => { const plugin = new ServerlessPluginBuilder() .withPluginConfig({ cloudFront: true @@ -205,24 +210,101 @@ describe("addCustomStackResources", () => { "public" ); + getAssetsBucketName.mockReturnValue(assetsBucketName); + when(fse.pathExists) .calledWith(publicDir) .mockResolvedValue(false); return addCustomStackResources.call(plugin).then(() => { - const { Resources } = plugin.serverless.service.resources; - expect(Object.keys(Resources)).toHaveLength(2); // S3 bucket and CloudFront distribution + resources = plugin.serverless.service.resources; + }); + }); + + it("adds distribution", () => { + const { Resources } = resources; + expect(Object.keys(Resources)).toHaveLength(2); // S3 bucket and CloudFront distribution + const { NextjsCloudFront } = Resources; + expect(NextjsCloudFront).toBeDefined(); + }); + + it("sets up S3 origin for /static directory", () => { + const { + Resources: { NextjsCloudFront } + } = resources; + + const staticOrigin = NextjsCloudFront.Properties.DistributionConfig.Origins.find( + o => o.Id === "S3StaticOrigin" + ); + + expect(staticOrigin.DomainName).toEqual( + `${assetsBucketName}.s3.amazonaws.com` + ); + }); + + it("sets up S3 origin for /public directory", () => { + const { + Resources: { NextjsCloudFront } + } = resources; + + const publicOrigin = NextjsCloudFront.Properties.DistributionConfig.Origins.find( + o => o.Id === "S3PublicOrigin" + ); + + expect(publicOrigin.DomainName).toEqual( + `${assetsBucketName}.s3.amazonaws.com` + ); + }); + + describe("when public folder exists", () => { + it("adds cache behaviours for public files", () => { + expect.assertions(4); + + const plugin = new ServerlessPluginBuilder() + .withPluginConfig({ + cloudFront: true + }) + .build(); + + const publicDir = path.join( + plugin.getPluginConfigValue("nextConfigDir"), + "public" + ); + + when(fse.pathExists) + .calledWith(publicDir) + .mockResolvedValue(true); + + when(fse.readdir) + .calledWith(publicDir) + .mockResolvedValue(["robots.txt", "manifest.json"]); + + return addCustomStackResources.call(plugin).then(() => { + const { + NextjsCloudFront + } = plugin.serverless.service.resources.Resources; + + expect(NextjsCloudFront).toBeDefined(); + + const { + CacheBehaviors + } = NextjsCloudFront.Properties.DistributionConfig; + + expect(CacheBehaviors).toHaveLength(3); // behavior for /static origin and 2 other behaviours for robots and manifest + expect(CacheBehaviors[1].PathPattern).toEqual("robots.txt"); + expect(CacheBehaviors[2].PathPattern).toEqual("manifest.json"); + }); }); }); }); - describe("When no bucket available", () => { + describe("When no bucket is configured", () => { beforeEach(() => { getAssetsBucketName.mockReset(); getAssetsBucketName.mockReturnValue(null); }); - it("doesn't add S3 bucket to resources", () => { + it("doesn't add any custom resources", () => { expect.assertions(3); const plugin = new ServerlessPluginBuilder().build(); diff --git a/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js b/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js index 1381bc461c..ebfafebab8 100644 --- a/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js +++ b/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js @@ -6,6 +6,16 @@ const logger = require("../utils/logger"); const loadYml = require("../utils/yml/load"); const fse = require("fs-extra"); +const dirInfo = async dir => { + const exists = await fse.pathExists(dir); + + if (!exists) { + return [false, []]; + } + + return [true, await fse.readdir(dir)]; +}; + const capitaliseFirstLetter = str => str.charAt(0).toUpperCase() + str.slice(1); // removes non-alphanumeric characters to adhere to AWS naming requirements @@ -21,6 +31,18 @@ const normaliseFilePathForCloudFrontResourceKey = filePath => .map(capitaliseFirstLetter) .join(""); +const cacheBehaviour = fileName => ({ + AllowedMethods: ["GET", "HEAD", "OPTIONS"], + TargetOriginId: "S3PublicOrigin", + ForwardedValues: { + QueryString: "false", + Cookies: { Forward: "none" } + }, + ViewerProtocolPolicy: "allow-all", + MinTTL: "50", + PathPattern: path.basename(fileName) +}); + const getNextRouteProxyResources = async function({ bucketBaseUrl, bucketName @@ -30,8 +52,6 @@ const getNextRouteProxyResources = async function({ path.join(__dirname, "../resources/api-gw-next.yml") ); - let result = {}; - const bucketUrl = `${bucketBaseUrl}/${path.posix.join( bucketName, nextDir, @@ -40,14 +60,14 @@ const getNextRouteProxyResources = async function({ let resource = clone(baseResource); - resource.resources.Resources.NextStaticAssetsProxyParentResource.Properties.PathPart = nextDir; - resource.resources.Resources.NextStaticAssetsProxyMethod.Properties.Integration.Uri = bucketUrl; + const { Resources } = resource.resources; - result = resource.resources.Resources; + Resources.NextStaticAssetsProxyParentResource.Properties.PathPart = nextDir; + Resources.NextStaticAssetsProxyMethod.Properties.Integration.Uri = bucketUrl; logger.log(`Proxying NextJS assets -> ${bucketUrl}`); - return result; + return Resources; }; const getStaticRouteProxyResources = async function({ @@ -55,8 +75,9 @@ const getStaticRouteProxyResources = async function({ bucketName }) { const staticDir = path.join(this.nextConfigDir, "static"); + const [staticDirExists] = await dirInfo(staticDir); - if (!(await fse.pathExists(staticDir))) { + if (!staticDirExists) { return {}; } @@ -64,37 +85,35 @@ const getStaticRouteProxyResources = async function({ path.join(__dirname, "../resources/api-gw-static.yml") ); - let result = {}; - const bucketUrl = `${bucketBaseUrl}/${path.posix.join( bucketName, "static", "{proxy}" )}`; - let resource = clone(baseResource); - resource.resources.Resources.StaticAssetsProxyParentResource.Properties.PathPart = staticDir; - resource.resources.Resources.StaticAssetsProxyMethod.Properties.Integration.Uri = bucketUrl; + let resource = clone(baseResource); + const { Resources } = resource.resources; - result = resource.resources.Resources; + Resources.StaticAssetsProxyParentResource.Properties.PathPart = "static"; + Resources.StaticAssetsProxyMethod.Properties.Integration.Uri = bucketUrl; logger.log(`Proxying static files -> ${bucketUrl}`); - return result; + return Resources; }; const getPublicRouteProxyResources = async function({ bucketBaseUrl, bucketName }) { - const publicDir = path.join(this.nextConfigDir, "public"); + const [publicDirExists, publicDirFiles] = await dirInfo( + path.join(this.nextConfigDir, "public") + ); - if (!(await fse.pathExists(publicDir))) { + if (!publicDirExists) { return {}; } - const publicFiles = await fse.readdir(publicDir); - const baseResource = await loadYml( path.join(__dirname, "../resources/api-gw-proxy.yml") ); @@ -103,7 +122,7 @@ const getPublicRouteProxyResources = async function({ Resources: {} }; - publicFiles.forEach(file => { + publicDirFiles.forEach(file => { const bucketUrl = `${bucketBaseUrl}/${path.posix.join( bucketName, "public", @@ -156,12 +175,53 @@ const addCustomStackResources = async function() { assetsBucketResource.Resources.NextStaticAssetsS3Bucket.Properties.BucketName = bucketName; + this.serverless.service.resources = this.serverless.service.resources || { + Resources: {} + }; + + merge( + this.serverless.service.provider.coreCloudFormationTemplate, + assetsBucketResource + ); + const cloudFront = this.getPluginConfigValue("cloudFront"); - // if (cloudFront) { + if (cloudFront) { + let cloudFrontResource = await loadYml( + path.join(__dirname, "../resources/cloudfront.yml") + ); + + const { + DistributionConfig + } = cloudFrontResource.Resources.NextjsCloudFront.Properties; + + const findOrigin = originId => + DistributionConfig.Origins.find(o => o.Id === originId); + + const publicOrigin = findOrigin("S3PublicOrigin"); + const staticOrigin = findOrigin("S3StaticOrigin"); + + publicOrigin.DomainName = `${bucketName}.s3.amazonaws.com`; + staticOrigin.DomainName = `${bucketName}.s3.amazonaws.com`; - // return; - // } + const [publicDirExists, publicDirFiles] = await dirInfo( + path.join(this.nextConfigDir, "public") + ); + + if (publicDirExists) { + publicDirFiles.forEach(f => { + DistributionConfig.CacheBehaviors.push(cacheBehaviour(f)); + }); + } + + merge( + this.serverless.service.resources, + assetsBucketResource, + cloudFrontResource + ); + + return; + } // api gateway -> S3 proxying @@ -179,15 +239,6 @@ const addCustomStackResources = async function() { } }; - this.serverless.service.resources = this.serverless.service.resources || { - Resources: {} - }; - - merge( - this.serverless.service.provider.coreCloudFormationTemplate, - assetsBucketResource - ); - merge( this.serverless.service.resources, assetsBucketResource, diff --git a/packages/serverless-nextjs-plugin/lib/checkForChanges.js b/packages/serverless-nextjs-plugin/lib/checkForChanges.js index 39b0b085f9..e68109c58c 100644 --- a/packages/serverless-nextjs-plugin/lib/checkForChanges.js +++ b/packages/serverless-nextjs-plugin/lib/checkForChanges.js @@ -2,6 +2,7 @@ const getAssetsBucketName = require("./getAssetsBucketName"); module.exports = function() { const bucketName = getAssetsBucketName.call(this); + return this.provider .request("S3", "listObjectsV2", { Bucket: bucketName, diff --git a/packages/serverless-nextjs-plugin/resources/cloudfront.yml b/packages/serverless-nextjs-plugin/resources/cloudfront.yml index 6aa3c2e18f..e631c5a8dc 100644 --- a/packages/serverless-nextjs-plugin/resources/cloudfront.yml +++ b/packages/serverless-nextjs-plugin/resources/cloudfront.yml @@ -1,15 +1,24 @@ AWSTemplateFormatVersion: "2010-09-09" Resources: - myDistribution: + NextjsCloudFront: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: Origins: - - DomainName: danielcondemarinbucket.s3.amazonaws.com + - DomainName: TO_BE_REPLACED Id: S3StaticOrigin S3OriginConfig: OriginAccessIdentity: "" - - DomainName: nbw1qo4fgh.execute-api.us-east-1.amazonaws.com + - DomainName: TO_BE_REPLACED + Id: S3PublicOrigin + OriginPath: /public + S3OriginConfig: + OriginAccessIdentity: "" + - DomainName: + Fn::Join: + - "" + - - Ref: ApiGatewayRestApi + - ".execute-api.us-east-1.amazonaws.com" Id: ApiGatewayOrigin OriginPath: /dev CustomOriginConfig: From 65c93b5db89d67e092437f5317630e77f416a6dc Mon Sep 17 00:00:00 2001 From: danielconde Date: Sat, 29 Jun 2019 22:28:04 +0100 Subject: [PATCH 3/7] handle custom region and stage for api gateway origin --- .../lib/__tests__/addCustomStackResources.test.js | 15 +++++++++++++++ .../lib/addCustomStackResources.js | 8 ++++++++ .../resources/cloudfront.yml | 4 ++-- .../utils/test/ServerlessPluginBuilder.js | 6 +++++- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js b/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js index 7a20558572..7530ecd91c 100644 --- a/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js +++ b/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js @@ -256,6 +256,21 @@ describe("addCustomStackResources", () => { ); }); + it("sets up Api Gateway origin", () => { + const { + Resources: { NextjsCloudFront } + } = resources; + + const apiGatewayOrigin = NextjsCloudFront.Properties.DistributionConfig.Origins.find( + o => o.Id === "ApiGatewayOrigin" + ); + + expect(apiGatewayOrigin.OriginPath).toEqual("/test"); + expect(apiGatewayOrigin.DomainName["Fn::Join"][1][1]).toEqual( + ".execute-api.us-east-1.amazonaws.com" + ); + }); + describe("when public folder exists", () => { it("adds cache behaviours for public files", () => { expect.assertions(4); diff --git a/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js b/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js index ebfafebab8..be049cef3a 100644 --- a/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js +++ b/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js @@ -153,6 +153,8 @@ const getPublicRouteProxyResources = async function({ const addCustomStackResources = async function() { const region = this.provider.getRegion(); + const stage = this.provider.getStage(); + const bucketName = getAssetsBucketName.call(this); if (bucketName === null) { @@ -198,6 +200,12 @@ const addCustomStackResources = async function() { const findOrigin = originId => DistributionConfig.Origins.find(o => o.Id === originId); + const apiGatewayOrigin = findOrigin("ApiGatewayOrigin"); + apiGatewayOrigin.OriginPath = `/${stage}`; + apiGatewayOrigin.DomainName[ + "Fn::Join" + ][1][1] = `.execute-api.${region}.amazonaws.com`; + const publicOrigin = findOrigin("S3PublicOrigin"); const staticOrigin = findOrigin("S3StaticOrigin"); diff --git a/packages/serverless-nextjs-plugin/resources/cloudfront.yml b/packages/serverless-nextjs-plugin/resources/cloudfront.yml index e631c5a8dc..7e718fcbfc 100644 --- a/packages/serverless-nextjs-plugin/resources/cloudfront.yml +++ b/packages/serverless-nextjs-plugin/resources/cloudfront.yml @@ -18,9 +18,9 @@ Resources: Fn::Join: - "" - - Ref: ApiGatewayRestApi - - ".execute-api.us-east-1.amazonaws.com" + - TO_BE_REPLACED Id: ApiGatewayOrigin - OriginPath: /dev + OriginPath: TO_BE_REPLACED CustomOriginConfig: HTTPSPort: 443 OriginProtocolPolicy: https-only diff --git a/packages/serverless-nextjs-plugin/utils/test/ServerlessPluginBuilder.js b/packages/serverless-nextjs-plugin/utils/test/ServerlessPluginBuilder.js index cf329e8c67..63fe7e3094 100644 --- a/packages/serverless-nextjs-plugin/utils/test/ServerlessPluginBuilder.js +++ b/packages/serverless-nextjs-plugin/utils/test/ServerlessPluginBuilder.js @@ -9,7 +9,11 @@ class ServerlessPluginBuilder { }, getPlugins: () => {}, getProvider: () => { - return { request: () => {}, getRegion: () => "us-east-1" }; + return { + request: () => {}, + getRegion: () => "us-east-1", + getStage: () => "test" + }; }, pluginManager: { run: () => {} From 17190f2bb49e1c582eb1d0378a781fa1b1ae7d5b Mon Sep 17 00:00:00 2001 From: danielconde Date: Sun, 30 Jun 2019 08:58:08 +0100 Subject: [PATCH 4/7] wee refactor --- .../__tests__/addCustomStackResources.test.js | 18 ++++++++++++------ .../lib/addCustomStackResources.js | 6 ++++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js b/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js index 7530ecd91c..58d221c483 100644 --- a/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js +++ b/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js @@ -195,6 +195,9 @@ describe("addCustomStackResources", () => { }); describe("when cloudfront is enabled and S3 bucket is configured", () => { + const findOrigin = (distribution, originId) => + distribution.Origins.find(o => o.Id === originId); + let assetsBucketName = "foo.bar"; let resources; @@ -233,8 +236,9 @@ describe("addCustomStackResources", () => { Resources: { NextjsCloudFront } } = resources; - const staticOrigin = NextjsCloudFront.Properties.DistributionConfig.Origins.find( - o => o.Id === "S3StaticOrigin" + const staticOrigin = findOrigin( + NextjsCloudFront.Properties.DistributionConfig, + "S3StaticOrigin" ); expect(staticOrigin.DomainName).toEqual( @@ -247,8 +251,9 @@ describe("addCustomStackResources", () => { Resources: { NextjsCloudFront } } = resources; - const publicOrigin = NextjsCloudFront.Properties.DistributionConfig.Origins.find( - o => o.Id === "S3PublicOrigin" + const publicOrigin = findOrigin( + NextjsCloudFront.Properties.DistributionConfig, + "S3PublicOrigin" ); expect(publicOrigin.DomainName).toEqual( @@ -261,8 +266,9 @@ describe("addCustomStackResources", () => { Resources: { NextjsCloudFront } } = resources; - const apiGatewayOrigin = NextjsCloudFront.Properties.DistributionConfig.Origins.find( - o => o.Id === "ApiGatewayOrigin" + const apiGatewayOrigin = findOrigin( + NextjsCloudFront.Properties.DistributionConfig, + "ApiGatewayOrigin" ); expect(apiGatewayOrigin.OriginPath).toEqual("/test"); diff --git a/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js b/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js index be049cef3a..3972f230b9 100644 --- a/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js +++ b/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js @@ -209,8 +209,10 @@ const addCustomStackResources = async function() { const publicOrigin = findOrigin("S3PublicOrigin"); const staticOrigin = findOrigin("S3StaticOrigin"); - publicOrigin.DomainName = `${bucketName}.s3.amazonaws.com`; - staticOrigin.DomainName = `${bucketName}.s3.amazonaws.com`; + const bucketDomainName = `${bucketName}.s3.amazonaws.com`; + + publicOrigin.DomainName = bucketDomainName; + staticOrigin.DomainName = bucketDomainName; const [publicDirExists, publicDirFiles] = await dirInfo( path.join(this.nextConfigDir, "public") From 6b500ccbc34abd17921f771b99f3af2255397cca Mon Sep 17 00:00:00 2001 From: danielconde Date: Sun, 30 Jun 2019 16:34:45 +0100 Subject: [PATCH 5/7] fix behaviors --- README.md | 20 ++++++++- packages/serverless-nextjs-plugin/README.md | 44 ++++++++++--------- .../__tests__/addCustomStackResources.test.js | 10 ++--- .../lib/addCustomStackResources.js | 4 +- .../resources/cloudfront.yml | 24 +++++++--- 5 files changed, 67 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 676b53daca..50577e96c5 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ The plugin targets [Next 8 serverless mode](https://nextjs.org/blog/next-8/#serv - [Motivation](#motivation) - [Getting Started](#getting-started) - [Hosting static assets](#hosting-static-assets) +- [Serving static assets](#serving-static-assets) - [Deploying](#deploying) - [Deploying a single page](#deploying-a-single-page) - [Overriding page configuration](#overriding-page-configuration) @@ -116,8 +117,6 @@ custom: assetsBucketName: "your-bucket-name" ``` -With this approach you could have a CloudFront distribution in front of the bucket and use a custom domain in the assetPrefix. - ## Serving static assets Static files can be served by [following the NextJs convention](https://github.com/zeit/next.js/#static-file-serving-eg-images) of using a `static` and `public` folder. @@ -136,6 +135,23 @@ To serve static files from the root directory you can add a folder called public Note that for this to work, an S3 bucket needs to be provisioned as per [hosting-static-assets](#hosting-static-assets). +**For production deployments, enabling CloudFront is recommended:** + +```yml +# serverless.yml +plugins: + - serverless-nextjs-plugin + +custom: + serverless-nextjs: + assetsBucketName: "your-bucket-name" + cloudFront: true +``` + +By doing this, a CloudFront distribution will be created in front of your next application to serve any static assets from S3 and the pages from Api Gateway. + +Note that deploying the stack for the first time will take considerably longer, as CloudFront takes time propagating the changes, typically 10 - 20mins. + ## Deploying `serverless deploy` diff --git a/packages/serverless-nextjs-plugin/README.md b/packages/serverless-nextjs-plugin/README.md index 9582f2dc1c..88413553de 100644 --- a/packages/serverless-nextjs-plugin/README.md +++ b/packages/serverless-nextjs-plugin/README.md @@ -10,13 +10,14 @@ A [serverless framework](https://serverless.com/) plugin to deploy nextjs apps. The plugin targets [Next 8 serverless mode](https://nextjs.org/blog/next-8/#serverless-nextjs) -![demo](../../demo.gif) +![demo](./demo.gif) ## Contents - [Motivation](#motivation) - [Getting Started](#getting-started) - [Hosting static assets](#hosting-static-assets) +- [Serving static assets](#serving-static-assets) - [Deploying](#deploying) - [Deploying a single page](#deploying-a-single-page) - [Overriding page configuration](#overriding-page-configuration) @@ -24,8 +25,8 @@ The plugin targets [Next 8 serverless mode](https://nextjs.org/blog/next-8/#serv - [Custom error page](#custom-error-page) - [Custom lambda handler](#custom-lambda-handler) - [All plugin configuration options](#all-plugin-configuration-options) -- [Caveats](#caveats) - [Examples](#examples) +- [Caveats](#caveats) - [Contributing](#contributing) ## Motivation @@ -118,18 +119,23 @@ custom: With this approach you could have a CloudFront distribution in front of the bucket and use a custom domain in the assetPrefix. -If you need the static assets available in the main domain of your application, you can use the `routes` configuration to proxy API Gateway requests to S3. For example, to host `/robots.txt`: +## Serving static assets -```yml -custom: - serverless-nextjs: - staticDir: ./assets - routes: - - src: ./assets/robots.txt - path: robots.txt +Static files can be served by [following the NextJs convention](https://github.com/zeit/next.js/#static-file-serving-eg-images) of using a `static` and `public` folder. + +From your code you can then reference those files with a `/static` URL: + +``` +function MyImage() { + return my image +} + +export default MyImage ``` -Note that for this to work, an S3 bucket needs to be provisioned by using the `assetsBucketName` plugin config or `assetPrefix` in `next.config.js`. +To serve static files from the root directory you can add a folder called public and reference those files from the root, e.g: /robots.txt. + +Note that for this to work, an S3 bucket needs to be provisioned as per [hosting-static-assets](#hosting-static-assets). ## Deploying @@ -312,15 +318,13 @@ module.exports = page => { ## All plugin configuration options -| Plugin config key | Default Value | Description | -| ----------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| nextConfigDir | ./ | Path to parent directory of `next.config.js`. | -| assetsBucketName | \ | Creates an S3 bucket with the name provided. The bucket will be used for uploading next static assets. | -| staticDir | \ | Directory with static assets to be uploaded to S3, typically a directory named `static`, but it can be any other name. Requires a bucket provided via the `assetPrefix` described above or the `assetsBucketName` plugin config. | -| routes | [] | Array of custom routes for the next pages or static assets. | -| customHandler | \ | Path to your own lambda handler. | -| uploadBuildAssets | true | In the unlikely event that you only want to upload `static` or `public` dirs, set this to `false`. | -| | +| Plugin config key | Default Value | Description | +| ----------------- | ------------- | ------------------------------------------------------------------------------------------------------ | +| nextConfigDir | ./ | Path to parent directory of `next.config.js`. | +| assetsBucketName | \ | Creates an S3 bucket with the name provided. The bucket will be used for uploading next static assets. | +| routes | [] | Array of custom routes for the next pages. | +| customHandler | \ | Path to your own lambda handler. | +| uploadBuildAssets | true | In the unlikely event that you only want to upload `static` or `public` dirs, set this to `false`. | ## Caveats diff --git a/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js b/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js index 58d221c483..9c521f33ca 100644 --- a/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js +++ b/packages/serverless-nextjs-plugin/lib/__tests__/addCustomStackResources.test.js @@ -231,14 +231,14 @@ describe("addCustomStackResources", () => { expect(NextjsCloudFront).toBeDefined(); }); - it("sets up S3 origin for /static directory", () => { + it("sets up S3 origin", () => { const { Resources: { NextjsCloudFront } } = resources; const staticOrigin = findOrigin( NextjsCloudFront.Properties.DistributionConfig, - "S3StaticOrigin" + "S3Origin" ); expect(staticOrigin.DomainName).toEqual( @@ -311,9 +311,9 @@ describe("addCustomStackResources", () => { CacheBehaviors } = NextjsCloudFront.Properties.DistributionConfig; - expect(CacheBehaviors).toHaveLength(3); // behavior for /static origin and 2 other behaviours for robots and manifest - expect(CacheBehaviors[1].PathPattern).toEqual("robots.txt"); - expect(CacheBehaviors[2].PathPattern).toEqual("manifest.json"); + expect(CacheBehaviors).toHaveLength(4); // behavior for static/*, _next/* origins and 2 other behaviours for robots and manifest + expect(CacheBehaviors[2].PathPattern).toEqual("robots.txt"); + expect(CacheBehaviors[3].PathPattern).toEqual("manifest.json"); }); }); }); diff --git a/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js b/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js index 3972f230b9..8632c7a284 100644 --- a/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js +++ b/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js @@ -38,7 +38,7 @@ const cacheBehaviour = fileName => ({ QueryString: "false", Cookies: { Forward: "none" } }, - ViewerProtocolPolicy: "allow-all", + ViewerProtocolPolicy: "https-only", MinTTL: "50", PathPattern: path.basename(fileName) }); @@ -207,7 +207,7 @@ const addCustomStackResources = async function() { ][1][1] = `.execute-api.${region}.amazonaws.com`; const publicOrigin = findOrigin("S3PublicOrigin"); - const staticOrigin = findOrigin("S3StaticOrigin"); + const staticOrigin = findOrigin("S3Origin"); const bucketDomainName = `${bucketName}.s3.amazonaws.com`; diff --git a/packages/serverless-nextjs-plugin/resources/cloudfront.yml b/packages/serverless-nextjs-plugin/resources/cloudfront.yml index 7e718fcbfc..4eb1cf9d69 100644 --- a/packages/serverless-nextjs-plugin/resources/cloudfront.yml +++ b/packages/serverless-nextjs-plugin/resources/cloudfront.yml @@ -6,7 +6,7 @@ Resources: DistributionConfig: Origins: - DomainName: TO_BE_REPLACED - Id: S3StaticOrigin + Id: S3Origin S3OriginConfig: OriginAccessIdentity: "" - DomainName: TO_BE_REPLACED @@ -35,18 +35,30 @@ Resources: QueryString: "true" Cookies: Forward: all - ViewerProtocolPolicy: allow-all + ViewerProtocolPolicy: https-only CacheBehaviors: - AllowedMethods: - GET - HEAD - OPTIONS - TargetOriginId: S3StaticOrigin + TargetOriginId: S3Origin ForwardedValues: QueryString: "false" Cookies: Forward: none - ViewerProtocolPolicy: allow-all + ViewerProtocolPolicy: https-only MinTTL: "50" - PathPattern: static/ - PriceClass: PriceClass_All + PathPattern: static/* + - AllowedMethods: + - GET + - HEAD + - OPTIONS + TargetOriginId: S3Origin + ForwardedValues: + QueryString: "false" + Cookies: + Forward: none + ViewerProtocolPolicy: https-only + MinTTL: "50" + PathPattern: _next/* + PriceClass: PriceClass_100 From e83ff8692cffc92a938faf5d6f9bc517e6ed4c86 Mon Sep 17 00:00:00 2001 From: danielconde Date: Sun, 30 Jun 2019 19:23:34 +0100 Subject: [PATCH 6/7] add compression --- .../serverless-nextjs-plugin/lib/addCustomStackResources.js | 1 + packages/serverless-nextjs-plugin/resources/cloudfront.yml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js b/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js index 8632c7a284..458788a7c3 100644 --- a/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js +++ b/packages/serverless-nextjs-plugin/lib/addCustomStackResources.js @@ -34,6 +34,7 @@ const normaliseFilePathForCloudFrontResourceKey = filePath => const cacheBehaviour = fileName => ({ AllowedMethods: ["GET", "HEAD", "OPTIONS"], TargetOriginId: "S3PublicOrigin", + Compress: true, ForwardedValues: { QueryString: "false", Cookies: { Forward: "none" } diff --git a/packages/serverless-nextjs-plugin/resources/cloudfront.yml b/packages/serverless-nextjs-plugin/resources/cloudfront.yml index 4eb1cf9d69..065830c3ee 100644 --- a/packages/serverless-nextjs-plugin/resources/cloudfront.yml +++ b/packages/serverless-nextjs-plugin/resources/cloudfront.yml @@ -31,6 +31,7 @@ Resources: - HEAD - OPTIONS TargetOriginId: ApiGatewayOrigin + Compress: "true" ForwardedValues: QueryString: "true" Cookies: @@ -42,6 +43,7 @@ Resources: - HEAD - OPTIONS TargetOriginId: S3Origin + Compress: "true" ForwardedValues: QueryString: "false" Cookies: @@ -54,6 +56,7 @@ Resources: - HEAD - OPTIONS TargetOriginId: S3Origin + Compress: "true" ForwardedValues: QueryString: "false" Cookies: From 31597c3aaf4f0acd590f3cc3327a6cf494811788 Mon Sep 17 00:00:00 2001 From: danielconde Date: Sun, 30 Jun 2019 19:29:26 +0100 Subject: [PATCH 7/7] update docs --- packages/serverless-nextjs-plugin/README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/serverless-nextjs-plugin/README.md b/packages/serverless-nextjs-plugin/README.md index 88413553de..50577e96c5 100644 --- a/packages/serverless-nextjs-plugin/README.md +++ b/packages/serverless-nextjs-plugin/README.md @@ -117,8 +117,6 @@ custom: assetsBucketName: "your-bucket-name" ``` -With this approach you could have a CloudFront distribution in front of the bucket and use a custom domain in the assetPrefix. - ## Serving static assets Static files can be served by [following the NextJs convention](https://github.com/zeit/next.js/#static-file-serving-eg-images) of using a `static` and `public` folder. @@ -137,6 +135,23 @@ To serve static files from the root directory you can add a folder called public Note that for this to work, an S3 bucket needs to be provisioned as per [hosting-static-assets](#hosting-static-assets). +**For production deployments, enabling CloudFront is recommended:** + +```yml +# serverless.yml +plugins: + - serverless-nextjs-plugin + +custom: + serverless-nextjs: + assetsBucketName: "your-bucket-name" + cloudFront: true +``` + +By doing this, a CloudFront distribution will be created in front of your next application to serve any static assets from S3 and the pages from Api Gateway. + +Note that deploying the stack for the first time will take considerably longer, as CloudFront takes time propagating the changes, typically 10 - 20mins. + ## Deploying `serverless deploy`