From 6523b5e84e79ccee691519d1cacc15fb8321823e Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 11 May 2019 14:43:08 +0100 Subject: [PATCH] API GW -> S3 proxy support & new routes configuration (#60) * feat: add support for new page routes and api gw static routes proxied to S3 --- __tests__/index.test.js | 151 +------ classes/NextPage.js | 39 +- classes/__tests__/NextPage.test.js | 393 +++++++++++------- examples/basic-next-serverless-app/README.md | 4 - .../assets/robots.txt | 2 + .../basic-next-serverless-app/package.json | 3 +- .../basic-next-serverless-app/serverless.yml | 19 +- index.js | 53 +-- .../serverless.yml | 16 +- .../serverless.yml | 14 +- integration/basic-app/serverless.yml | 14 +- .../addAssetsBucketForDeployment.test.js | 167 -------- lib/__tests__/addCustomStackResources.test.js | 269 ++++++++++++ lib/__tests__/addS3BucketToResources.test.js | 47 --- lib/__tests__/build.test.js | 163 ++++++-- lib/__tests__/getAssetsBucketName.test.js | 90 ++++ .../getNextPagesFromBuildDir.test.js | 63 ++- lib/__tests__/uploadStaticAssets.test.js | 6 +- lib/addAssetsBucketForDeployment.js | 55 --- lib/addCustomStackResources.js | 99 +++++ lib/addS3BucketToResources.js | 29 -- lib/build.js | 37 +- lib/getAssetsBucketName.js | 28 ++ lib/getNextPagesFromBuildDir.js | 12 +- package.json | 3 +- resources/api-gw-proxy.yml | 28 ++ resources.yml => resources/assets-bucket.yml | 0 utils/test/ServerlessPluginBuilder.js | 2 +- {lib => utils/yml}/cfSchema.js | 0 utils/yml/load.js | 14 + 30 files changed, 1104 insertions(+), 716 deletions(-) create mode 100644 examples/basic-next-serverless-app/assets/robots.txt delete mode 100644 lib/__tests__/addAssetsBucketForDeployment.test.js create mode 100644 lib/__tests__/addCustomStackResources.test.js delete mode 100644 lib/__tests__/addS3BucketToResources.test.js create mode 100644 lib/__tests__/getAssetsBucketName.test.js delete mode 100644 lib/addAssetsBucketForDeployment.js create mode 100644 lib/addCustomStackResources.js delete mode 100644 lib/addS3BucketToResources.js create mode 100644 lib/getAssetsBucketName.js create mode 100644 resources/api-gw-proxy.yml rename resources.yml => resources/assets-bucket.yml (100%) rename {lib => utils/yml}/cfSchema.js (100%) create mode 100644 utils/yml/load.js diff --git a/__tests__/index.test.js b/__tests__/index.test.js index 6bed4cca4b..2263a007f2 100644 --- a/__tests__/index.test.js +++ b/__tests__/index.test.js @@ -1,14 +1,7 @@ const ServerlessPluginBuilder = require("../utils/test/ServerlessPluginBuilder"); const displayStackOutput = require("../lib/displayStackOutput"); -const build = require("../lib/build"); -const NextPage = require("../classes/NextPage"); -const PluginBuildDir = require("../classes/PluginBuildDir"); -jest.mock("js-yaml"); -jest.mock("../lib/build"); -jest.mock("../lib/parseNextConfiguration"); jest.mock("../lib/displayStackOutput"); -jest.mock("../utils/logger"); describe("ServerlessNextJsPlugin", () => { let pluginBuilder; @@ -25,132 +18,20 @@ describe("ServerlessNextJsPlugin", () => { }); it.each` - hook | method - ${"before:offline:start"} | ${"buildNextPages"} - ${"before:package:initialize"} | ${"buildNextPages"} - ${"before:deploy:function:initialize"} | ${"buildNextPages"} - ${"before:package:createDeploymentArtifacts"} | ${"addAssetsBucketForDeployment"} - ${"after:aws:deploy:deploy:uploadArtifacts"} | ${"uploadStaticAssets"} - ${"after:aws:info:displayStackOutputs"} | ${"printStackOutput"} - ${"after:package:createDeploymentArtifacts"} | ${"removePluginBuildDir"} + hook | method + ${"before:offline:start"} | ${"build"} + ${"before:package:initialize"} | ${"build"} + ${"before:deploy:function:initialize"} | ${"build"} + ${"after:aws:deploy:deploy:uploadArtifacts"} | ${"uploadStaticAssets"} + ${"after:aws:info:displayStackOutputs"} | ${"printStackOutput"} + ${"after:package:createDeploymentArtifacts"} | ${"removePluginBuildDir"} + ${"before:aws:package:finalize:mergeCustomProviderResources"} | ${"addCustomStackResources"} `("should hook to $hook with method $method", ({ hook, method }) => { + expect(plugin[method]).toBeDefined(); expect(plugin.hooks[hook]).toEqual(plugin[method]); }); }); - describe("#buildNextPages", () => { - describe("packaging plugin build directory", () => { - const nextConfigDir = "/path/to/next-app"; - - beforeEach(() => { - build.mockResolvedValueOnce([]); - }); - - it("should include plugin build directory for packaging", () => { - expect.assertions(1); - - const plugin = pluginBuilder - .withNextCustomConfig({ nextConfigDir }) - .build(); - - return plugin.buildNextPages().then(() => { - expect(plugin.serverless.service.package.include).toContain( - `${nextConfigDir}/${PluginBuildDir.BUILD_DIR_NAME}/**` - ); - }); - }); - - it("should include plugin build directory for packaging when package include isn't defined", () => { - expect.assertions(1); - - const plugin = pluginBuilder - .withNextCustomConfig({ nextConfigDir }) - .build(); - - plugin.serverless.service.package.include = undefined; - - return plugin.buildNextPages().then(() => { - expect(plugin.serverless.service.package.include).toContain( - `${nextConfigDir}/${PluginBuildDir.BUILD_DIR_NAME}/**` - ); - }); - }); - }); - - it("should call build with pluginBuildDir and user provided pageConfig", () => { - expect.assertions(1); - - build.mockResolvedValueOnce([]); - const nextConfigDir = "/path/to/next"; - - const pageConfig = { - home: { - memory: "512" - } - }; - - const plugin = new ServerlessPluginBuilder() - .withNextCustomConfig({ - nextConfigDir: nextConfigDir, - pageConfig - }) - .build(); - - return plugin.buildNextPages().then(() => { - expect(build).toBeCalledWith( - new PluginBuildDir(nextConfigDir), - pageConfig, - undefined - ); - }); - }); - - it("should set the next functions in serverless", () => { - expect.assertions(1); - - const homePagePath = "/path/to/next/build/serverless/pages/home.js"; - const aboutPagePath = "/path/to/next/build/serverless/pages/about.js"; - - build.mockResolvedValueOnce([ - new NextPage(homePagePath), - new NextPage(aboutPagePath) - ]); - - const plugin = new ServerlessPluginBuilder().build(); - - return plugin.buildNextPages().then(() => { - expect(Object.keys(plugin.serverless.service.functions)).toEqual([ - "homePage", - "aboutPage" - ]); - }); - }); - - it("should call service.setFunctionNames", () => { - expect.assertions(1); - - const homePagePath = "/path/to/next/build/serverless/pages/home.js"; - const aboutPagePath = "/path/to/next/build/serverless/pages/about.js"; - - build.mockResolvedValueOnce([ - new NextPage(homePagePath), - new NextPage(aboutPagePath) - ]); - - const setFunctionNamesMock = jest.fn(); - - const plugin = new ServerlessPluginBuilder() - .withService({ - setFunctionNames: setFunctionNamesMock - }) - .build(); - - return plugin.buildNextPages().then(() => { - expect(setFunctionNamesMock).toBeCalled(); - }); - }); - }); - describe("#printStackOutput", () => { it("should call displayStackOutput with awsInfo", () => { const awsInfo = { @@ -183,4 +64,18 @@ describe("ServerlessNextJsPlugin", () => { }); }); }); + + describe("#getPluginConfigValue", () => { + it("uses default values when config key not provided", () => { + const plugin = pluginBuilder + .withPluginConfig({ + routes: undefined, + uploadBuildAssets: undefined + }) + .build(); + + expect(plugin.getPluginConfigValue("routes")).toEqual([]); + expect(plugin.getPluginConfigValue("uploadBuildAssets")).toEqual(true); + }); + }); }); diff --git a/classes/NextPage.js b/classes/NextPage.js index f46a6f67f8..fe7aaa5ad9 100644 --- a/classes/NextPage.js +++ b/classes/NextPage.js @@ -5,9 +5,10 @@ const toPosix = require("../utils/pathToPosix"); const PluginBuildDir = require("./PluginBuildDir"); class NextPage { - constructor(pagePath, serverlessFunctionOverrides) { + constructor(pagePath, { serverlessFunctionOverrides, routes } = {}) { this.pagePath = pagePath; this.serverlessFunctionOverrides = serverlessFunctionOverrides; + this.routes = routes; } get pageOriginalPath() { @@ -22,6 +23,22 @@ class NextPage { return path.dirname(this.pagePath); } + get pageId() { + const pathSegments = this.pagePath.split(path.sep); + + // strip out the parent build directory from path + // sls-next-build/foo/bar.js -> /foo/bar.js + const relativePathSegments = pathSegments.slice( + pathSegments.indexOf(PluginBuildDir.BUILD_DIR_NAME) + 1, + pathSegments.length + ); + + // remove extension + // foo/bar.js -> /foo/bar + const parsed = path.parse(relativePathSegments.join(path.posix.sep)); + return path.posix.join(parsed.dir, parsed.name); + } + get pageName() { return path.basename(this.pagePath, ".js"); } @@ -86,6 +103,15 @@ class NextPage { merge(configuration, this.serverlessFunctionOverrides); } + if (this.routes && this.routes.length > 0) { + configuration.events = []; + + this.routes.forEach(r => { + const httpEvent = this.getHttpEventForRoute(r); + configuration.events.push(httpEvent); + }); + } + const httpHeadEvents = this.getMatchingHttpHeadEvents( configuration.events.filter(e => e.http.method === "get") ); @@ -104,6 +130,17 @@ class NextPage { return headEvent; }); } + + getHttpEventForRoute(route) { + const httpEvent = { + http: { + method: "get", + ...route + } + }; + + return httpEvent; + } } module.exports = NextPage; diff --git a/classes/__tests__/NextPage.test.js b/classes/__tests__/NextPage.test.js index 831d4fa261..d4d249acdd 100644 --- a/classes/__tests__/NextPage.test.js +++ b/classes/__tests__/NextPage.test.js @@ -4,25 +4,195 @@ const PluginBuildDir = require("../PluginBuildDir"); describe("NextPage", () => { describe("#constructor", () => { - it("should set a pagePath", () => { + it("sets a pagePath", () => { const pagePath = `${PluginBuildDir.BUILD_DIR_NAME}/home.js`; - const page = new NextPage(pagePath); + const page = new NextPage(pagePath, { + serverlessFunctionOverrides: {}, + routes: [] + }); expect(page.pagePath).toEqual(pagePath); }); }); + describe("Simple page", () => { + const buildDir = PluginBuildDir.BUILD_DIR_NAME; + const pagePath = path.join(buildDir, "admin.js"); + let page; + + beforeEach(() => { + page = new NextPage(pagePath, { + serverlessFunctionOverrides: {}, + routes: [] + }); + }); + + it("returns pageCompatPath", () => { + expect(page.pageCompatPath).toEqual( + path.join(buildDir, "admin.compat.js") + ); + }); + + it("returns pageOriginalPath", () => { + expect(page.pageOriginalPath).toEqual( + path.join(buildDir, "admin.original.js") + ); + }); + + it("returns pageDir", () => { + expect(page.pageDir).toEqual(buildDir); + }); + + it("returns pageName", () => { + expect(page.pageName).toEqual("admin"); + }); + + it("returns pageHandler", () => { + expect(page.pageHandler).toEqual( + `${PluginBuildDir.BUILD_DIR_NAME}/admin.render` + ); + }); + + it("returns pageFunctionName", () => { + expect(page.functionName).toEqual("adminPage"); + }); + + it("returns pageId", () => { + expect(page.pageId).toEqual("admin"); + }); + + describe("#serverlessFunction", () => { + it("returns function name", () => { + const pageFunction = page.serverlessFunction; + expect(pageFunction.adminPage).toBeDefined(); + }); + + it("returns function handler", () => { + const { handler } = page.serverlessFunction.adminPage; + expect(handler).toEqual(`${buildDir}/admin.render`); + }); + + it("returns 2 http events", () => { + const { events } = page.serverlessFunction.adminPage; + expect(events).toHaveLength(2); + }); + + it("returns function http GET event", () => { + const { events } = page.serverlessFunction.adminPage; + + const httpEvent = events[0].http; + + expect(httpEvent.path).toEqual("admin"); + expect(httpEvent.method).toEqual("get"); + }); + + it("returns function http HEAD event", () => { + const { events } = page.serverlessFunction.adminPage; + + const httpEvent = events[1].http; + + expect(httpEvent.path).toEqual("admin"); + expect(httpEvent.method).toEqual("head"); + }); + + describe("When pageConfig override is provided", () => { + it("creates identical HEAD route for custom GET route", () => { + const serverlessFunctionOverrides = { + events: [ + { + http: { + path: "admin/{id}", + request: { + parameters: { + id: true + } + } + } + } + ] + }; + + const pageWithCustomConfig = new NextPage(pagePath, { + serverlessFunctionOverrides, + routes: [] + }); + + const { events } = pageWithCustomConfig.serverlessFunction.adminPage; + expect(events).toHaveLength(2); + + const httpGet = events[0].http; + const httpHead = events[1].http; + + expect(httpGet.method).toBe("get"); + expect(httpHead.method).toBe("head"); + + expect(httpGet.path).toBe("admin/{id}"); + expect(httpHead.path).toBe("admin/{id}"); + + expect(httpGet.request.parameters.id).toBe(true); + expect(httpHead.request.parameters.id).toBe(true); + }); + + it("overrides serverlessFunction with provided pageConfig", () => { + const serverlessFunctionOverrides = { foo: "bar" }; + + const pageWithCustomConfig = new NextPage(pagePath, { + serverlessFunctionOverrides, + routes: [] + }); + + expect(pageWithCustomConfig.serverlessFunction.adminPage.foo).toBe( + "bar" + ); + }); + + it("doesn't change handler with provided pageConfig", () => { + const serverlessFunctionOverrides = { handler: "invalid/handler" }; + + const pageWithCustomConfig = new NextPage(pagePath, { + serverlessFunctionOverrides, + routes: [] + }); + + expect( + pageWithCustomConfig.serverlessFunction.adminPage.handler + ).toBe(pageWithCustomConfig.pageHandler); + }); + + it("doesn't change runtime with provided pageConfig", () => { + const serverlessFunctionOverrides = { runtime: "python2.7" }; + + const pageWithCustomConfig = new NextPage(pagePath, { + serverlessFunctionOverrides, + routes: [] + }); + + expect( + pageWithCustomConfig.serverlessFunction.adminPage.runtime + ).toBe(undefined); + }); + }); + }); + }); + describe("When is the index page", () => { const buildDir = PluginBuildDir.BUILD_DIR_NAME; const pagePath = path.join(buildDir, "index.js"); let page; beforeEach(() => { - page = new NextPage(pagePath); + page = new NextPage(pagePath, { + serverlessFunctionOverrides: {}, + routes: [] + }); + }); + + it("returns pageId", () => { + expect(page.pageId).toEqual("index"); }); describe("#serverlessFunction", () => { - it("should have http GET event with path /", () => { + it("returns http GET event with path /", () => { const { events } = page.serverlessFunction.indexPage; const httpEvent = events[0].http; @@ -30,7 +200,7 @@ describe("NextPage", () => { expect(httpEvent.path).toEqual("/"); }); - it("should have http HEAD event with path /", () => { + it("returns http HEAD event with path /", () => { const { events } = page.serverlessFunction.indexPage; const httpEvent = events[1].http; @@ -46,7 +216,10 @@ describe("NextPage", () => { let page; beforeEach(() => { - page = new NextPage(pagePath); + page = new NextPage(pagePath, { + serverlessFunctionOverrides: {}, + routes: [] + }); }); describe("#serverlessFunction", () => { @@ -54,12 +227,12 @@ describe("NextPage", () => { expect(page.serverlessFunction.notFoundErrorPage).toBeDefined(); }); - it("should return two events", () => { + it("returns two events", () => { const { events } = page.serverlessFunction.notFoundErrorPage; expect(events).toHaveLength(2); }); - it("should return http event path /{proxy+} with GET method", () => { + it("returns http event path /{proxy+} with GET method", () => { const { events } = page.serverlessFunction.notFoundErrorPage; const httpGet = events[0].http; @@ -68,7 +241,7 @@ describe("NextPage", () => { expect(httpGet.method).toEqual("get"); }); - it("should return http event path /{proxy+} with HEAD method", () => { + it("returns http event path /{proxy+} with HEAD method", () => { const { events } = page.serverlessFunction.notFoundErrorPage; const httpHead = events[1].http; @@ -85,11 +258,18 @@ describe("NextPage", () => { let page; beforeEach(() => { - page = new NextPage(pagePath); + page = new NextPage(pagePath, { + serverlessFunctionOverrides: {}, + routes: [] + }); + }); + + it("returns pageId", () => { + expect(page.pageId).toEqual("categories/fridge/fridges"); }); describe("#serverlessFunction", () => { - it("should have URI path matching subdirectories", () => { + it("returns URI path matching subdirectories", () => { const { events } = page.serverlessFunction.fridgesPage; expect(events).toHaveLength(2); @@ -111,10 +291,13 @@ describe("NextPage", () => { let page; beforeEach(() => { - page = new NextPage(pagePath); + page = new NextPage(pagePath, { + serverlessFunctionOverrides: {}, + routes: [] + }); }); - it("should return posix pageHandler", () => { + it("returns posix pageHandler", () => { expect(page.pageHandler).toEqual( `${PluginBuildDir.BUILD_DIR_NAME}/admin.render` ); @@ -127,170 +310,64 @@ describe("NextPage", () => { let page; beforeEach(() => { - page = new NextPage(pagePath); + page = new NextPage(pagePath, { + serverlessFunctionOverrides: {}, + routes: [] + }); }); - it("should return pageHandler", () => { + it("returns pageHandler", () => { expect(page.pageHandler).toEqual( `app/${PluginBuildDir.BUILD_DIR_NAME}/admin.render` ); }); - it("should return pageRoute", () => { + it("returns pageRoute", () => { expect(page.pageRoute).toEqual("admin"); }); - }); - - describe("When a new instance is created", () => { - const buildDir = PluginBuildDir.BUILD_DIR_NAME; - const pagePath = `${buildDir}/admin.js`; - let page; - beforeEach(() => { - page = new NextPage(pagePath); - }); - - it("should have pageCompatPath", () => { - expect(page.pageCompatPath).toEqual( - path.join(buildDir, "admin.compat.js") - ); - }); - - it("should return pageOriginalPath", () => { - expect(page.pageOriginalPath).toEqual( - path.join(buildDir, "admin.original.js") - ); + it("returns pageId", () => { + expect(page.pageId).toEqual("admin"); }); + }); - it("should return pageDir", () => { - expect(page.pageDir).toEqual(buildDir); - }); + describe("When custom routes are provided", () => { + let pageWithCustomRoutes; - it("should return pageName", () => { - expect(page.pageName).toEqual("admin"); - }); - - it("should return pageHandler", () => { - expect(page.pageHandler).toEqual( - `${PluginBuildDir.BUILD_DIR_NAME}/admin.render` + beforeEach(() => { + pageWithCustomRoutes = new NextPage( + path.join(PluginBuildDir.BUILD_DIR_NAME, "foo.js"), + { + routes: [ + { + path: "/custom/path/to/foo" + }, + { + path: "/another/custom/path/to/foo" + } + ] + } ); }); - it("should return pageFunctionName", () => { - expect(page.functionName).toEqual("adminPage"); - }); - - describe("#serverlessFunction", () => { - it("should return function name", () => { - const pageFunction = page.serverlessFunction; - expect(pageFunction.adminPage).toBeDefined(); - }); - - it("should return function handler", () => { - const { handler } = page.serverlessFunction.adminPage; - expect(handler).toEqual(`${buildDir}/admin.render`); - }); + it("sets http GET and HEAD events for the route given", () => { + const { events } = pageWithCustomRoutes.serverlessFunction.fooPage; + expect(events).toHaveLength(4); - it("should return 2 http events", () => { - const { events } = page.serverlessFunction.adminPage; - expect(events).toHaveLength(2); - }); + const httpGetOne = events[0].http; + const httpGetTwo = events[1].http; + const httpHeadOne = events[2].http; + const httpHeadTwo = events[3].http; - it("should return function http GET event", () => { - const { events } = page.serverlessFunction.adminPage; - - const httpEvent = events[0].http; - - expect(httpEvent.path).toEqual("admin"); - expect(httpEvent.method).toEqual("get"); - }); - - it("should return function http HEAD event", () => { - const { events } = page.serverlessFunction.adminPage; - - const httpEvent = events[1].http; - - expect(httpEvent.path).toEqual("admin"); - expect(httpEvent.method).toEqual("head"); - }); + expect(httpGetOne.method).toBe("get"); + expect(httpHeadOne.method).toBe("head"); + expect(httpGetOne.path).toBe("/custom/path/to/foo"); + expect(httpHeadOne.path).toBe("/custom/path/to/foo"); - describe("When pageConfig override is provided", () => { - it("should create identical HEAD route for custom GET route", () => { - const serverlessFunctionOverrides = { - events: [ - { - http: { - path: "admin/{id}", - request: { - parameters: { - id: true - } - } - } - } - ] - }; - - const pageWithCustomConfig = new NextPage( - pagePath, - serverlessFunctionOverrides - ); - - const { events } = pageWithCustomConfig.serverlessFunction.adminPage; - expect(events).toHaveLength(2); - - const httpGet = events[0].http; - const httpHead = events[1].http; - - expect(httpGet.method).toBe("get"); - expect(httpHead.method).toBe("head"); - - expect(httpGet.path).toBe("admin/{id}"); - expect(httpHead.path).toBe("admin/{id}"); - - expect(httpGet.request.parameters.id).toBe(true); - expect(httpHead.request.parameters.id).toBe(true); - }); - - it("should override serverlessFunction with provided pageConfig", () => { - const serverlessFunctionOverrides = { foo: "bar" }; - - const pageWithCustomConfig = new NextPage( - pagePath, - serverlessFunctionOverrides - ); - - expect(pageWithCustomConfig.serverlessFunction.adminPage.foo).toBe( - "bar" - ); - }); - - it("should NOT change handler with provided pageConfig", () => { - const serverlessFunctionOverrides = { handler: "invalid/handler" }; - - const pageWithCustomConfig = new NextPage( - pagePath, - serverlessFunctionOverrides - ); - - expect( - pageWithCustomConfig.serverlessFunction.adminPage.handler - ).toBe(pageWithCustomConfig.pageHandler); - }); - - it("should NOT change runtime with provided pageConfig", () => { - const serverlessFunctionOverrides = { runtime: "python2.7" }; - - const pageWithCustomConfig = new NextPage( - pagePath, - serverlessFunctionOverrides - ); - - expect( - pageWithCustomConfig.serverlessFunction.adminPage.runtime - ).toBe(undefined); - }); - }); + expect(httpGetTwo.method).toBe("get"); + expect(httpHeadTwo.method).toBe("head"); + expect(httpGetTwo.path).toBe("/another/custom/path/to/foo"); + expect(httpHeadTwo.path).toBe("/another/custom/path/to/foo"); }); }); }); diff --git a/examples/basic-next-serverless-app/README.md b/examples/basic-next-serverless-app/README.md index e6a664f352..a6b2c931a9 100644 --- a/examples/basic-next-serverless-app/README.md +++ b/examples/basic-next-serverless-app/README.md @@ -28,10 +28,6 @@ module.exports = { Alternatively, remove `assetPrefix` and the bucket won't be provisioned. -#### Running locally - -`npx serverless offline` - #### Deploy `serverless deploy` diff --git a/examples/basic-next-serverless-app/assets/robots.txt b/examples/basic-next-serverless-app/assets/robots.txt new file mode 100644 index 0000000000..6f27bb66a3 --- /dev/null +++ b/examples/basic-next-serverless-app/assets/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: \ No newline at end of file diff --git a/examples/basic-next-serverless-app/package.json b/examples/basic-next-serverless-app/package.json index c2340cc819..cce2157eed 100644 --- a/examples/basic-next-serverless-app/package.json +++ b/examples/basic-next-serverless-app/package.json @@ -28,7 +28,6 @@ "devDependencies": { "eslint-plugin-react": "^7.12.4", "serverless": "^1.39.1", - "serverless-nextjs-plugin": "^1.2.0", - "serverless-offline": "^4.9.2" + "serverless-nextjs-plugin": "^1.2.0" } } diff --git a/examples/basic-next-serverless-app/serverless.yml b/examples/basic-next-serverless-app/serverless.yml index a018ccebf6..5622489501 100644 --- a/examples/basic-next-serverless-app/serverless.yml +++ b/examples/basic-next-serverless-app/serverless.yml @@ -15,15 +15,16 @@ plugins: custom: serverless-nextjs: nextConfigDir: ./ - pageConfig: - post: - events: - - http: - path: post/{slug} - request: - parameters: - paths: - slug: true + staticDir: ./assets + routes: + - src: ./assets/robots.txt + path: robots.txt + - src: post + path: post/{slug} + request: + parameters: + paths: + slug: true package: # exclude everything diff --git a/index.js b/index.js index 83ea0f9dbe..fbd539174a 100644 --- a/index.js +++ b/index.js @@ -1,12 +1,11 @@ "use strict"; -const path = require("path"); const displayStackOutput = require("./lib/displayStackOutput"); const parseNextConfiguration = require("./lib/parseNextConfiguration"); const build = require("./lib/build"); const PluginBuildDir = require("./classes/PluginBuildDir"); -const addAssetsBucketForDeployment = require("./lib/addAssetsBucketForDeployment"); const uploadStaticAssets = require("./lib/uploadStaticAssets"); +const addCustomStackResources = require("./lib/addCustomStackResources"); class ServerlessNextJsPlugin { constructor(serverless, options) { @@ -18,18 +17,18 @@ class ServerlessNextJsPlugin { this.providerRequest = this.provider.request.bind(this.provider); this.pluginBuildDir = new PluginBuildDir(this.nextConfigDir); - this.addAssetsBucketForDeployment = addAssetsBucketForDeployment.bind(this); + this.addCustomStackResources = addCustomStackResources.bind(this); this.uploadStaticAssets = uploadStaticAssets.bind(this); + this.build = build.bind(this); this.printStackOutput = this.printStackOutput.bind(this); - this.buildNextPages = this.buildNextPages.bind(this); this.removePluginBuildDir = this.removePluginBuildDir.bind(this); this.hooks = { - "before:offline:start": this.buildNextPages, - "before:package:initialize": this.buildNextPages, - "before:deploy:function:initialize": this.buildNextPages, - "before:package:createDeploymentArtifacts": this - .addAssetsBucketForDeployment, + "before:offline:start": this.build, + "before:package:initialize": this.build, + "before:deploy:function:initialize": this.build, + "before:aws:package:finalize:mergeCustomProviderResources": this + .addCustomStackResources, "after:package:createDeploymentArtifacts": this.removePluginBuildDir, "after:aws:deploy:deploy:uploadArtifacts": this.uploadStaticAssets, "after:aws:info:displayStackOutputs": this.printStackOutput @@ -45,36 +44,16 @@ class ServerlessNextJsPlugin { } getPluginConfigValue(param) { - return this.serverless.service.custom["serverless-nextjs"][param]; - } - - buildNextPages() { - const pluginBuildDir = this.pluginBuildDir; - const servicePackage = this.serverless.service.package; - - servicePackage.include = servicePackage.include || []; - servicePackage.include.push( - path.posix.join(pluginBuildDir.posixBuildDir, "**") - ); - - return build( - pluginBuildDir, - this.getPluginConfigValue("pageConfig"), - this.getPluginConfigValue("customHandler") - ).then(nextPages => this.setNextPages(nextPages)); - } - - setNextPages(nextPages) { - const service = this.serverless.service; - - this.nextPages = nextPages; + const defaults = { + routes: [], + uploadBuildAssets: true + }; - nextPages.forEach(page => { - const functionName = page.functionName; - service.functions[functionName] = page.serverlessFunction[functionName]; - }); + const userConfig = this.serverless.service.custom["serverless-nextjs"][ + param + ]; - this.serverless.service.setFunctionNames(); + return userConfig === undefined ? defaults[param] : userConfig; } printStackOutput() { diff --git a/integration/app-with-serverless-offline/serverless.yml b/integration/app-with-serverless-offline/serverless.yml index 76d79750fa..c0ba3d718e 100644 --- a/integration/app-with-serverless-offline/serverless.yml +++ b/integration/app-with-serverless-offline/serverless.yml @@ -11,15 +11,13 @@ plugins: custom: serverless-nextjs: nextConfigDir: ./ - pageConfig: - post: - events: - - http: - path: post/{slug} - request: - parameters: - paths: - slug: true + routes: + - src: post + path: post/{slug} + request: + parameters: + paths: + slug: true package: # exclude everything diff --git a/integration/basic-app-with-nested-next-config/serverless.yml b/integration/basic-app-with-nested-next-config/serverless.yml index dcf4231425..02595d7913 100644 --- a/integration/basic-app-with-nested-next-config/serverless.yml +++ b/integration/basic-app-with-nested-next-config/serverless.yml @@ -15,13 +15,13 @@ custom: pageConfig: post: memorySize: 2048 - events: - - http: - path: posts/{id} - request: - parameters: - paths: - id: true + routes: + - src: post + path: posts/{id} + request: + parameters: + paths: + id: true package: # exclude everything diff --git a/integration/basic-app/serverless.yml b/integration/basic-app/serverless.yml index 3e05732be7..5404dd07f3 100644 --- a/integration/basic-app/serverless.yml +++ b/integration/basic-app/serverless.yml @@ -15,13 +15,13 @@ custom: pageConfig: post: memorySize: 2048 - events: - - http: - path: posts/{id} - request: - parameters: - paths: - id: true + routes: + - src: post + path: posts/{id} + request: + parameters: + paths: + id: true package: # exclude everything diff --git a/lib/__tests__/addAssetsBucketForDeployment.test.js b/lib/__tests__/addAssetsBucketForDeployment.test.js deleted file mode 100644 index 44096b14d3..0000000000 --- a/lib/__tests__/addAssetsBucketForDeployment.test.js +++ /dev/null @@ -1,167 +0,0 @@ -const addS3BucketToResources = require("../addS3BucketToResources"); -const parseNextConfiguration = require("../parseNextConfiguration"); -const parsedNextConfigurationFactory = require("../../utils/test/parsedNextConfigurationFactory"); -const ServerlessPluginBuilder = require("../../utils/test/ServerlessPluginBuilder"); -const addAssetsBucketForDeployment = require("../addAssetsBucketForDeployment"); -const logger = require("../../utils/logger"); - -jest.mock("../addS3BucketToResources"); -jest.mock("../parseNextConfiguration"); -jest.mock("../../utils/logger"); - -describe("addAssetsBucketForDeployment", () => { - const mockCFWithBucket = { - Resources: { - NextStaticAssetsBucket: {} - } - }; - - let plugin; - - beforeEach(() => { - plugin = new ServerlessPluginBuilder().build(); - addS3BucketToResources.mockResolvedValue(mockCFWithBucket); - }); - - it("should error if staticDir provided but no bucket", () => { - expect.assertions(1); - - const pluginWithoutBucket = new ServerlessPluginBuilder() - .withNextCustomConfig({ - nextConfigDir: "./", - staticDir: "./static" - }) - .build(); - - parseNextConfiguration.mockReturnValueOnce( - parsedNextConfigurationFactory({ staticAssetsBucket: null }, null) - ); - - return addAssetsBucketForDeployment.call(pluginWithoutBucket).catch(err => { - expect(err.message).toContain("staticDir requires a bucket. See"); - }); - }); - - it("should not call addS3BucketToResources if a staticAssetsBucket is not available", () => { - expect.assertions(1); - parseNextConfiguration.mockReturnValueOnce( - parsedNextConfigurationFactory({}, null) - ); - - return addAssetsBucketForDeployment.call(plugin).then(() => { - expect(addS3BucketToResources).not.toBeCalled(); - }); - }); - - it("should call parseNextConfiguration with nextConfigDir", () => { - expect.assertions(1); - - const nextConfigDir = "./"; - - plugin = new ServerlessPluginBuilder() - .withNextCustomConfig({ - nextConfigDir - }) - .build(); - - parseNextConfiguration.mockReturnValueOnce( - parsedNextConfigurationFactory() - ); - - return addAssetsBucketForDeployment.call(plugin).then(() => { - expect(parseNextConfiguration).toBeCalledWith(nextConfigDir); - }); - }); - - it("should log when a bucket is going to be provisioned from parsed assetPrefix", () => { - expect.assertions(1); - - parseNextConfiguration.mockReturnValueOnce( - parsedNextConfigurationFactory() - ); - - return addAssetsBucketForDeployment.call(plugin).then(() => { - expect(logger.log).toBeCalledWith( - expect.stringContaining(`Found bucket "my-bucket"`) - ); - }); - }); - - it("should log when a bucket is going to be provisioned from plugin config", () => { - expect.assertions(1); - - parseNextConfiguration.mockReturnValueOnce( - parsedNextConfigurationFactory() - ); - - plugin = new ServerlessPluginBuilder() - .withNextCustomConfig({ - assetsBucketName: "my-assets" - }) - .build(); - - return addAssetsBucketForDeployment.call(plugin).then(() => { - expect(logger.log).toBeCalledWith( - expect.stringContaining(`Found bucket "my-assets"`) - ); - }); - }); - - it("should update coreCloudFormationTemplate with static assets bucket", () => { - expect.assertions(2); - - parseNextConfiguration.mockReturnValueOnce( - parsedNextConfigurationFactory() - ); - - const initialCoreCF = { - Resources: { bar: "baz" } - }; - - plugin = new ServerlessPluginBuilder() - .withService({ - provider: { - coreCloudFormationTemplate: initialCoreCF - } - }) - .build(); - - return addAssetsBucketForDeployment.call(plugin).then(() => { - const { coreCloudFormationTemplate } = plugin.serverless.service.provider; - - expect(addS3BucketToResources).toBeCalledWith("my-bucket", initialCoreCF); - expect(coreCloudFormationTemplate).toEqual(mockCFWithBucket); - }); - }); - - it("should update compiledCloudFormation with static assets bucket", () => { - expect.assertions(2); - - parseNextConfiguration.mockReturnValueOnce( - parsedNextConfigurationFactory() - ); - - const initialCompiledCF = { - Resources: { foo: "bar" } - }; - - const plugin = new ServerlessPluginBuilder() - .withService({ - provider: { - compiledCloudFormationTemplate: initialCompiledCF - } - }) - .build(); - - return addAssetsBucketForDeployment.call(plugin).then(() => { - const { - compiledCloudFormationTemplate - } = plugin.serverless.service.provider; - expect(addS3BucketToResources).toBeCalledWith( - "my-bucket", - initialCompiledCF - ); - expect(compiledCloudFormationTemplate).toEqual(mockCFWithBucket); - }); - }); -}); diff --git a/lib/__tests__/addCustomStackResources.test.js b/lib/__tests__/addCustomStackResources.test.js new file mode 100644 index 0000000000..e54c259197 --- /dev/null +++ b/lib/__tests__/addCustomStackResources.test.js @@ -0,0 +1,269 @@ +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 addCustomStackResources = require("../addCustomStackResources"); +const ServerlessPluginBuilder = require("../../utils/test/ServerlessPluginBuilder"); +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:... + `; + + let s3Resources; + let baseProxyResource; + + 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" + } + } + } + } + }; + + 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); + + getAssetsBucketName.mockReturnValueOnce(bucketName); + }); + + it("adds S3 bucket to resources", () => { + expect.assertions(3); + + const coreCfTemplate = { + Resources: { + foo: "bar" + } + }; + const s3ResourcesWithBucketName = clone(s3Resources); + s3ResourcesWithBucketName.Resources.NextStaticAssetsS3Bucket.Properties.BucketName = bucketName; + + const plugin = new ServerlessPluginBuilder().build(); + + plugin.serverless.service.provider.coreCloudFormationTemplate = clone( + coreCfTemplate + ); + + return addCustomStackResources.call(plugin).then(() => { + expect(logger.log).toBeCalledWith( + expect.stringContaining(`Found bucket "${bucketName}"`) + ); + expect( + plugin.serverless.service.resources.Resources.NextStaticAssetsS3Bucket + .Properties.BucketName + ).toEqual(bucketName); + expect( + plugin.serverless.service.provider.coreCloudFormationTemplate + ).toEqual(merge(coreCfTemplate, s3ResourcesWithBucketName)); + }); + }); + + it("adds single static proxy route to resources", () => { + expect.assertions(4); + + const plugin = new ServerlessPluginBuilder() + .withPluginConfig({ + staticDir: "./public", + routes: [ + { + src: "./public/robots.txt", + path: "robots.txt" + } + ] + }) + .build(); + + return addCustomStackResources.call(plugin).then(() => { + const resources = plugin.serverless.service.resources.Resources; + + const { RobotsProxyMethod, RobotsProxyResource } = 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` + ); + }); + }); + + it("adds static proxy route to resources when src filenames are same but different sub directories", () => { + expect.assertions(8); + + const plugin = new ServerlessPluginBuilder() + .withPluginConfig({ + staticDir: "./public", + routes: [ + { + src: "./public/foo/bar.js", + path: "foo/bar.js" + }, + { + src: "./public/bar.js", + path: "bar.js" + } + ] + }) + .build(); + + return addCustomStackResources.call(plugin).then(() => { + const { + FooBarProxyMethod, + FooBarProxyResource, + BarProxyMethod, + BarProxyResource + } = plugin.serverless.service.resources.Resources; + + expect(FooBarProxyMethod.Properties.Integration.Uri).toEqual( + `${bucketUrl}/public/foo/bar.js` + ); + expect(FooBarProxyMethod.Properties.ResourceId.Ref).toEqual( + "FooBarProxyResource" + ); + expect(FooBarProxyResource.Properties.PathPart).toEqual("foo/bar.js"); + expect(logger.log).toBeCalledWith( + `Proxying foo/bar.js -> ${bucketUrl}/public/foo/bar.js` + ); + + expect(BarProxyMethod.Properties.Integration.Uri).toEqual( + `${bucketUrl}/public/bar.js` + ); + expect(BarProxyMethod.Properties.ResourceId.Ref).toEqual( + "BarProxyResource" + ); + expect(BarProxyResource.Properties.PathPart).toEqual(`bar.js`); + expect(logger.log).toBeCalledWith( + `Proxying bar.js -> ${bucketUrl}/public/bar.js` + ); + }); + }); + + it("doesn't add static proxy route to resources if src isn't a sub path of staticDir", () => { + expect.assertions(1); + + const plugin = new ServerlessPluginBuilder() + .withPluginConfig({ + staticDir: "./public", + routes: [ + { + src: "assets/public/sw.js", + path: "proxied/sw.js" + }, + { + src: "static/sw.js", + path: "proxied/sw.js" + } + ] + }) + .build(); + + return addCustomStackResources.call(plugin).then(() => { + const resources = plugin.serverless.service.resources.Resources; + // should only contain bucket + expect(Object.keys(resources)).toEqual(["NextStaticAssetsS3Bucket"]); + }); + }); + + describe("When no bucket available", () => { + beforeEach(() => { + getAssetsBucketName.mockReset(); + getAssetsBucketName.mockReturnValue(null); + }); + + it("doesn't add S3 bucket to resources", () => { + expect.assertions(5); + + 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 + ).toEqual(undefined); + }); + }); + }); + + describe("When no staticDir given", () => { + it("doesn't add any static proxy routes", () => { + expect.assertions(1); + + const plugin = new ServerlessPluginBuilder() + .withPluginConfig({ + staticDir: undefined, + routes: [ + { + src: "static/sw.js", + path: "proxied/sw.js" + } + ] + }) + .build(); + + return addCustomStackResources.call(plugin).then(() => { + const resources = plugin.serverless.service.resources.Resources; + // should only contain bucket + expect(Object.keys(resources)).toEqual(["NextStaticAssetsS3Bucket"]); + }); + }); + }); +}); diff --git a/lib/__tests__/addS3BucketToResources.test.js b/lib/__tests__/addS3BucketToResources.test.js deleted file mode 100644 index ba80ccbb8b..0000000000 --- a/lib/__tests__/addS3BucketToResources.test.js +++ /dev/null @@ -1,47 +0,0 @@ -const fs = require("fs"); -const yaml = require("js-yaml"); -const addS3BucketToResources = require("../addS3BucketToResources"); - -jest.mock("fs"); -jest.mock("js-yaml"); - -describe("addS3BucketToResources", () => { - it("should merge S3 bucket resources for next static assets", () => { - expect.assertions(5); - - fs.readFile.mockImplementation((path, encoding, cb) => - cb(null, "Resources:...") - ); - const s3Resources = { - Resources: { - NextStaticAssetsS3Bucket: { - Properties: { - BucketName: "TO_BE_REPLACED" - } - } - } - }; - yaml.safeLoad.mockReturnValueOnce(s3Resources); - - const bucketName = "my-bucket"; - const baseCloudFormation = { - Resources: {} - }; - - return addS3BucketToResources(bucketName, baseCloudFormation).then(cf => { - expect(fs.readFile).toBeCalledWith( - expect.stringContaining("resources.yml"), - "utf-8", - expect.any(Function) - ); - - expect(yaml.safeLoad).toBeCalledWith("Resources:...", { - filename: expect.stringContaining("resources.yml") - }); - expect(cf.Resources).toHaveProperty("NextStaticAssetsS3Bucket"); - const bucketResource = cf.Resources.NextStaticAssetsS3Bucket; - expect(bucketResource).toHaveProperty("Properties"); - expect(bucketResource.Properties.BucketName).toEqual(bucketName); - }); - }); -}); diff --git a/lib/__tests__/build.test.js b/lib/__tests__/build.test.js index f18f4200e5..4933399205 100644 --- a/lib/__tests__/build.test.js +++ b/lib/__tests__/build.test.js @@ -10,11 +10,12 @@ const rewritePageHandlers = require("../rewritePageHandlers"); const PluginBuildDir = require("../../classes/PluginBuildDir"); const getNextPagesFromBuildDir = require("../getNextPagesFromBuildDir"); const NextPage = require("../../classes/NextPage"); +const ServerlessPluginBuilder = require("../../utils/test/ServerlessPluginBuilder"); +jest.mock("fs-extra"); jest.mock("next/dist/build"); jest.mock("../../utils/logger"); jest.mock("../copyBuildFiles"); -jest.mock("fs-extra"); jest.mock("../parseNextConfiguration"); jest.mock("../getNextPagesFromBuildDir"); jest.mock("../rewritePageHandlers"); @@ -23,31 +24,77 @@ describe("build", () => { beforeEach(() => { nextBuild.mockResolvedValueOnce(); copyBuildFiles.mockResolvedValueOnce(); + getNextPagesFromBuildDir.mockResolvedValue([]); }); - it("should log when it starts building", () => { + it("logs when it starts building", () => { expect.assertions(1); parseNextConfiguration.mockResolvedValueOnce( parsedNextConfigurationFactory() ); + + const plugin = new ServerlessPluginBuilder().build(); + + return build.call(plugin).then(() => { + expect(logger.log).toBeCalledWith("Started building next app ..."); + }); + }); + + it("includes plugin build directory for packaging", () => { + expect.assertions(1); + const nextConfigDir = "path/to/next-app"; - return build(new PluginBuildDir(nextConfigDir)).then(() => { - expect(logger.log).toBeCalledWith( - expect.stringContaining("building next app") + const parsedNextConfig = parsedNextConfigurationFactory(); + parseNextConfiguration.mockResolvedValueOnce(parsedNextConfig); + + const plugin = new ServerlessPluginBuilder() + .withPluginConfig({ nextConfigDir }) + .build(); + + return build.call(plugin).then(() => { + expect(plugin.serverless.service.package.include).toContain( + `${nextConfigDir}/${PluginBuildDir.BUILD_DIR_NAME}/**` ); }); }); - it("should copy build files", () => { + it("includes plugin build directory for packaging when package include isn't defined", () => { + expect.assertions(1); + + const nextConfigDir = "path/to/next-app"; + + const parsedNextConfig = parsedNextConfigurationFactory(); + parseNextConfiguration.mockResolvedValueOnce(parsedNextConfig); + + const plugin = new ServerlessPluginBuilder() + .withPluginConfig({ nextConfigDir }) + .build(); + + plugin.serverless.service.package.include = undefined; + + return build.call(plugin).then(() => { + expect(plugin.serverless.service.package.include).toContain( + `${nextConfigDir}/${PluginBuildDir.BUILD_DIR_NAME}/**` + ); + }); + }); + + it("copies build files", () => { expect.assertions(2); const parsedNextConfig = parsedNextConfigurationFactory(); parseNextConfiguration.mockResolvedValueOnce(parsedNextConfig); const nextConfigDir = "path/to/next-app"; - return build(new PluginBuildDir(nextConfigDir)).then(() => { + const plugin = new ServerlessPluginBuilder() + .withPluginConfig({ + nextConfigDir + }) + .build(); + + return build.call(plugin).then(() => { expect(parseNextConfiguration).toBeCalledWith(nextConfigDir); expect(nextBuild).toBeCalledWith( path.resolve(nextConfigDir), @@ -56,25 +103,31 @@ describe("build", () => { }); }); - it("should copy custom handler provided", () => { + it("copies custom handler provided", () => { expect.assertions(1); const parsedNextConfig = parsedNextConfigurationFactory(); parseNextConfiguration.mockResolvedValueOnce(parsedNextConfig); const nextConfigDir = "path/to/next-app"; - const pluginBuildDir = new PluginBuildDir(nextConfigDir); const customHandlerPath = "./path/to/handler.js"; - return build(pluginBuildDir, {}, customHandlerPath).then(() => { + const plugin = new ServerlessPluginBuilder() + .withPluginConfig({ + nextConfigDir, + customHandler: customHandlerPath + }) + .build(); + + return build.call(plugin).then(() => { expect(fse.copy).toBeCalledWith( path.resolve(nextConfigDir, customHandlerPath), - path.join(pluginBuildDir.buildDir, customHandlerPath) + path.join(plugin.pluginBuildDir.buildDir, customHandlerPath) ); }); }); - it('should override nextConfig target if is not "serverless" and log it', () => { + it('overrides nextConfig target if is not "serverless" and log it', () => { expect.assertions(2); const parsedConfig = parsedNextConfigurationFactory({ @@ -90,7 +143,13 @@ describe("build", () => { target: "serverless" }; - return build(new PluginBuildDir(nextConfigDir)).then(() => { + const plugin = new ServerlessPluginBuilder() + .withPluginConfig({ + nextConfigDir + }) + .build(); + + return build.call(plugin).then(() => { expect(logger.log).toBeCalledWith( expect.stringContaining('Target "server" found') ); @@ -101,7 +160,7 @@ describe("build", () => { }); }); - it("should rewrite the page handlers for each next page", () => { + it("rewrites the page handlers for each next page", () => { expect.assertions(2); const nextConfigDir = "path/to/next-app"; @@ -115,23 +174,57 @@ describe("build", () => { getNextPagesFromBuildDir.mockResolvedValueOnce(nextPages); const pageConfig = {}; + const routes = []; const customHandler = undefined; - return build( - new PluginBuildDir(nextConfigDir), - pageConfig, - customHandler - ).then(() => { + const plugin = new ServerlessPluginBuilder() + .withPluginConfig({ + pageConfig, + routes, + nextConfigDir + }) + .build(); + + return build.call(plugin).then(() => { expect(getNextPagesFromBuildDir).toBeCalledWith( new PluginBuildDir(nextConfigDir).buildDir, - pageConfig, - customHandler + { + pageConfig, + routes, + additionalExcludes: customHandler + } ); expect(rewritePageHandlers).toBeCalledWith(nextPages, undefined); }); }); - it("should return NextPage instances for each next page copied", () => { + it("sets the next page functions for deployment", () => { + expect.assertions(2); + + const parsedConfig = parsedNextConfigurationFactory(); + parseNextConfiguration.mockResolvedValueOnce(parsedConfig); + + const mockNextPages = [new NextPage("/foo/bar"), new NextPage("/foo/baz")]; + getNextPagesFromBuildDir.mockResolvedValueOnce(mockNextPages); + + const setFunctionNamesMock = jest.fn(); + + const plugin = new ServerlessPluginBuilder() + .withService({ + setFunctionNames: setFunctionNamesMock + }) + .build(); + + return build.call(plugin).then(() => { + expect(setFunctionNamesMock).toBeCalled(); + expect(Object.keys(plugin.serverless.service.functions)).toEqual([ + "barPage", + "bazPage" + ]); + }); + }); + + it("returns NextPage instances for each next page copied", () => { expect.assertions(2); const parsedConfig = parsedNextConfigurationFactory(); @@ -142,17 +235,23 @@ describe("build", () => { const nextConfigDir = "path/to/next-app"; const pageConfig = {}; + const routes = []; const customHandler = undefined; - return build(new PluginBuildDir(nextConfigDir), pageConfig).then( - nextPages => { - expect(getNextPagesFromBuildDir).toBeCalledWith( - new PluginBuildDir(nextConfigDir).buildDir, - pageConfig, - customHandler - ); - expect(nextPages).toEqual(mockNextPages); - } - ); + const plugin = new ServerlessPluginBuilder() + .withPluginConfig({ + pageConfig, + routes, + nextConfigDir + }) + .build(); + + return build.call(plugin).then(nextPages => { + expect(getNextPagesFromBuildDir).toBeCalledWith( + new PluginBuildDir(nextConfigDir).buildDir, + { pageConfig, routes, additionalExcludes: customHandler } + ); + expect(nextPages).toEqual(mockNextPages); + }); }); }); diff --git a/lib/__tests__/getAssetsBucketName.test.js b/lib/__tests__/getAssetsBucketName.test.js new file mode 100644 index 0000000000..995ca96d53 --- /dev/null +++ b/lib/__tests__/getAssetsBucketName.test.js @@ -0,0 +1,90 @@ +const parseNextConfiguration = require("../parseNextConfiguration"); +const parsedNextConfigurationFactory = require("../../utils/test/parsedNextConfigurationFactory"); +const ServerlessPluginBuilder = require("../../utils/test/ServerlessPluginBuilder"); +const getAssetsBucketName = require("../getAssetsBucketName"); + +jest.mock("../parseNextConfiguration"); +jest.mock("../../utils/logger"); + +describe("getAssetsBucketName", () => { + it("returns no bucket when there isn't one configured", () => { + expect.assertions(1); + + const bucketName = null; + + parseNextConfiguration.mockReturnValueOnce( + parsedNextConfigurationFactory({ distDir: ".next" }, bucketName) + ); + + const pluginWithoutBucket = new ServerlessPluginBuilder().build(); + + const result = getAssetsBucketName.call(pluginWithoutBucket); + + expect(result).toEqual(bucketName); + }); + + it("errors if staticDir provided but no bucket", () => { + expect.assertions(1); + + parseNextConfiguration.mockReturnValueOnce( + parsedNextConfigurationFactory({ staticAssetsBucket: null }, null) + ); + + const pluginWithoutBucket = new ServerlessPluginBuilder() + .withPluginConfig({ + nextConfigDir: "./", + staticDir: "./static" + }) + .build(); + + expect(() => getAssetsBucketName.call(pluginWithoutBucket)).toThrow( + "staticDir requires a bucket. See" + ); + }); + + it("returns bucket name parsed from next config", () => { + expect.assertions(2); + + const bucketName = "bucket-123"; + const nextConfigDir = "./"; + + parseNextConfiguration.mockReturnValueOnce( + parsedNextConfigurationFactory( + { + distDir: ".next" + }, + bucketName + ) + ); + + const plugin = new ServerlessPluginBuilder() + .withPluginConfig({ + nextConfigDir + }) + .build(); + + const result = getAssetsBucketName.call(plugin); + + expect(parseNextConfiguration).toBeCalledWith(nextConfigDir); + expect(result).toEqual(bucketName); + }); + + it("returns bucket from plugin config", () => { + expect.assertions(1); + + const bucketName = "my-assets"; + parseNextConfiguration.mockReturnValueOnce( + parsedNextConfigurationFactory() + ); + + plugin = new ServerlessPluginBuilder() + .withPluginConfig({ + assetsBucketName: bucketName + }) + .build(); + + const result = getAssetsBucketName.call(plugin); + + expect(result).toEqual(bucketName); + }); +}); diff --git a/lib/__tests__/getNextPagesFromBuildDir.test.js b/lib/__tests__/getNextPagesFromBuildDir.test.js index 964517849a..5b6b7b85a1 100644 --- a/lib/__tests__/getNextPagesFromBuildDir.test.js +++ b/lib/__tests__/getNextPagesFromBuildDir.test.js @@ -20,7 +20,7 @@ describe("getNextPagesFromBuildDir", () => { fs.lstatSync.mockReturnValue({ isDirectory: () => false }); }); - it("should return an empty array when there are no pages", () => { + it("returns an empty array when there are no pages", () => { expect.assertions(1); const buildDir = path.normalize(`path/to/${PluginBuildDir.BUILD_DIR_NAME}`); @@ -36,7 +36,7 @@ describe("getNextPagesFromBuildDir", () => { return getPagesPromise; }); - it("should return two next pages", () => { + it("returns two next pages", () => { expect.assertions(5); const buildDir = PluginBuildDir.BUILD_DIR_NAME; @@ -61,7 +61,7 @@ describe("getNextPagesFromBuildDir", () => { return promise; }); - it("should pass provided pageConfig to next pages", () => { + it("returns next pages with page function config. overridden", () => { expect.assertions(2); const indexPageConfigOverride = { foo: "bar" }; @@ -76,7 +76,7 @@ describe("getNextPagesFromBuildDir", () => { `/path/to/${PluginBuildDir.BUILD_DIR_NAME}` ); - const promise = getNextPagesFromBuildDir(buildDir, pageConfig).then( + const promise = getNextPagesFromBuildDir(buildDir, { pageConfig }).then( nextPages => { expect(nextPages[0].serverlessFunctionOverrides).toEqual( indexPageConfigOverride @@ -94,7 +94,46 @@ describe("getNextPagesFromBuildDir", () => { return promise; }); - it("should pass asterisk pageConfig to all pages", () => { + it("returns next pages with custom routes", () => { + expect.assertions(4); + + const routes = [ + { src: "index", path: "home" }, + { src: "foo", path: "custom/foo" }, + { src: "foo/bar", path: "one/bar" }, + { src: "foo/bar", path: "two/bar" }, + { src: "baz/bar", path: "three/bar" } + ]; + + const buildDir = path.normalize( + `/path/to/${PluginBuildDir.BUILD_DIR_NAME}` + ); + + const promise = getNextPagesFromBuildDir(buildDir, { + pageConfig: undefined, + routes + }).then(nextPages => { + const [indexPage, fooPage, fooBarPage, bazBarPage] = nextPages; + + expect(indexPage.routes).toEqual([{ path: "home" }]); + expect(fooPage.routes).toEqual([{ path: "custom/foo" }]); + expect(fooBarPage.routes).toEqual([ + { path: "one/bar" }, + { path: "two/bar" } + ]); + expect(bazBarPage.routes).toEqual([{ path: "three/bar" }]); + }); + + mockedStream.emit("data", { path: path.join(buildDir, "index.js") }); + mockedStream.emit("data", { path: path.join(buildDir, "foo.js") }); + mockedStream.emit("data", { path: path.join(buildDir, "foo/bar.js") }); + mockedStream.emit("data", { path: path.join(buildDir, "baz/bar.js") }); + mockedStream.emit("end"); + + return promise; + }); + + it("passes asterisk pageConfig to all pages", () => { expect.assertions(2); const asteriskPageConfigOverride = { foo: "bar" }; @@ -107,7 +146,7 @@ describe("getNextPagesFromBuildDir", () => { `/path/to/${PluginBuildDir.BUILD_DIR_NAME}` ); - const promise = getNextPagesFromBuildDir(buildDir, pageConfig).then( + const promise = getNextPagesFromBuildDir(buildDir, { pageConfig }).then( nextPages => { expect(nextPages[0].serverlessFunctionOverrides).toEqual( asteriskPageConfigOverride @@ -125,7 +164,7 @@ describe("getNextPagesFromBuildDir", () => { return promise; }); - it("should log pages found", () => { + it("logs pages found", () => { expect.assertions(1); const buildDir = path.normalize("/path/to/build"); @@ -140,7 +179,7 @@ describe("getNextPagesFromBuildDir", () => { return promise; }); - it("should skip _app and _document pages", () => { + it("skips _app and _document pages", () => { expect.assertions(2); const buildDir = path.normalize(`./${PluginBuildDir.BUILD_DIR_NAME}`); @@ -163,7 +202,7 @@ describe("getNextPagesFromBuildDir", () => { return promise; }); - it("should skip compatLayer file", () => { + it("skips compatLayer file", () => { expect.assertions(2); const buildDir = path.normalize( @@ -182,7 +221,7 @@ describe("getNextPagesFromBuildDir", () => { return promise; }); - it("should skip sourcemap files", () => { + it("skips sourcemap files", () => { expect.assertions(2); const buildDir = path.normalize( @@ -201,7 +240,7 @@ describe("getNextPagesFromBuildDir", () => { return promise; }); - it("should handle nested pages", () => { + it("handles nested pages", () => { expect.assertions(5); const buildDir = path.normalize(`./${PluginBuildDir.BUILD_DIR_NAME}`); @@ -230,7 +269,7 @@ describe("getNextPagesFromBuildDir", () => { return promise; }); - it("should skip page directories", () => { + it("skips page directories", () => { expect.assertions(1); const buildDir = path.normalize(`./${PluginBuildDir.BUILD_DIR_NAME}`); diff --git a/lib/__tests__/uploadStaticAssets.test.js b/lib/__tests__/uploadStaticAssets.test.js index 9b44d23803..574d979d77 100644 --- a/lib/__tests__/uploadStaticAssets.test.js +++ b/lib/__tests__/uploadStaticAssets.test.js @@ -60,7 +60,7 @@ describe("uploadStaticAssets", () => { ); const plugin = new ServerlessPluginBuilder() - .withNextCustomConfig({ + .withPluginConfig({ assetsBucketName: "custom-bucket" }) .build(); @@ -85,7 +85,7 @@ describe("uploadStaticAssets", () => { ); const plugin = new ServerlessPluginBuilder() - .withNextCustomConfig({ + .withPluginConfig({ staticDir: "/path/to/assets" }) .build(); @@ -106,7 +106,7 @@ describe("uploadStaticAssets", () => { ); const plugin = new ServerlessPluginBuilder() - .withNextCustomConfig({ + .withPluginConfig({ uploadBuildAssets: false, staticDir: "/path/to/assets" }) diff --git a/lib/addAssetsBucketForDeployment.js b/lib/addAssetsBucketForDeployment.js deleted file mode 100644 index 594f68ea27..0000000000 --- a/lib/addAssetsBucketForDeployment.js +++ /dev/null @@ -1,55 +0,0 @@ -const parseNextConfiguration = require("./parseNextConfiguration"); -const logger = require("../utils/logger"); -const addS3BucketToResources = require("./addS3BucketToResources"); - -const getCFTemplatesWithBucket = async function(bucketName) { - return Promise.all([ - addS3BucketToResources( - bucketName, - this.serverless.service.provider.compiledCloudFormationTemplate - ), - addS3BucketToResources( - bucketName, - this.serverless.service.provider.coreCloudFormationTemplate - ) - ]); -}; - -module.exports = async function() { - const nextConfigDir = this.getPluginConfigValue("nextConfigDir"); - const staticDir = this.getPluginConfigValue("staticDir"); - - let { staticAssetsBucket } = parseNextConfiguration(nextConfigDir); - - const bucketNameFromConfig = this.getPluginConfigValue("assetsBucketName"); - - if (bucketNameFromConfig) { - // bucket name provided via user config takes precendence - // over parsed value from assetPrefix - staticAssetsBucket = bucketNameFromConfig; - } - - if (!staticAssetsBucket) { - if (staticDir) { - return Promise.reject( - new Error( - "staticDir requires a bucket. See https://github.com/danielcondemarin/serverless-nextjs-plugin#hosting-static-assets" - ) - ); - } - - return Promise.resolve(); - } - - logger.log(`Found bucket "${staticAssetsBucket}"`); - - const [ - compiledCfWithBucket, - coreCfWithBucket - ] = await getCFTemplatesWithBucket.call(this, staticAssetsBucket); - - this.serverless.service.provider.compiledCloudFormationTemplate = compiledCfWithBucket; - this.serverless.service.provider.coreCloudFormationTemplate = coreCfWithBucket; - - return Promise.resolve(); -}; diff --git a/lib/addCustomStackResources.js b/lib/addCustomStackResources.js new file mode 100644 index 0000000000..7b6067052a --- /dev/null +++ b/lib/addCustomStackResources.js @@ -0,0 +1,99 @@ +const path = require("path"); +const merge = require("lodash.merge"); +const clone = require("lodash.clonedeep"); +const getAssetsBucketName = require("./getAssetsBucketName"); +const logger = require("../utils/logger"); +const loadYml = require("../utils/yml/load"); + +const capitaliseFirstLetter = str => str.charAt(0).toUpperCase() + str.slice(1); + +const isSubPath = (parentDir, subPath) => { + const relative = path.relative(parentDir, subPath); + return relative && !relative.startsWith("..") && !path.isAbsolute(relative); +}; + +// converts file path to a string which can be used in CF resource keys +// ./static/bar.js -> Bar +// ./static/foo/bar.js -> FooBar +const normaliseSrc = (staticDir, src) => + path + .relative(staticDir, src) + .split(path.sep) + .filter(s => s !== "." && s !== "..") + .map(capitaliseFirstLetter) + .join(""); + +const getStaticRouteProxyResources = async function(bucketName) { + const staticDir = this.getPluginConfigValue("staticDir"); + const routes = this.getPluginConfigValue("routes"); + + if (!staticDir) { + return {}; + } + + const baseResource = await loadYml( + path.join(__dirname, "../resources/api-gw-proxy.yml") + ); + const result = { + Resources: {} + }; + + routes + .filter(r => isSubPath(staticDir, r.src)) + .forEach(r => { + const { src, path: routePath } = r; + + const bucketUrl = `https://s3.amazonaws.com/${path.posix.join( + bucketName, + src + )}`; + + let resourceName = normaliseSrc(staticDir, src); + resourceName = path.parse(resourceName).name; + + const resource = clone(baseResource); + + resource.Resources.ProxyResource.Properties.PathPart = routePath; + resource.Resources.ProxyMethod.Properties.ResourceId.Ref = `${resourceName}ProxyResource`; + resource.Resources.ProxyMethod.Properties.Integration.Uri = bucketUrl; + + result.Resources[`${resourceName}ProxyResource`] = + resource.Resources.ProxyResource; + + result.Resources[`${resourceName}ProxyMethod`] = + resource.Resources.ProxyMethod; + + logger.log(`Proxying ${routePath} -> ${bucketUrl}`); + }); + + return result; +}; + +const addCustomStackResources = async function() { + const bucketName = getAssetsBucketName.call(this); + + if (bucketName === null) { + return Promise.resolve(); + } + + let resources = await loadYml( + path.join(__dirname, "../resources/assets-bucket.yml") + ); + + logger.log(`Found bucket "${bucketName}"`); + + resources.Resources.NextStaticAssetsS3Bucket.Properties.BucketName = bucketName; + + merge(this.serverless.service.provider.coreCloudFormationTemplate, resources); + + const proxyResources = await getStaticRouteProxyResources.call( + this, + bucketName + ); + + merge(resources, proxyResources); + + this.serverless.service.resources = resources; +}; + +module.exports = addCustomStackResources; diff --git a/lib/addS3BucketToResources.js b/lib/addS3BucketToResources.js deleted file mode 100644 index c854f0661e..0000000000 --- a/lib/addS3BucketToResources.js +++ /dev/null @@ -1,29 +0,0 @@ -const fs = require("fs"); -const { promisify } = require("util"); -const path = require("path"); -const yaml = require("js-yaml"); -const clone = require("lodash.clonedeep"); -const merge = require("lodash.merge"); -const cfSchema = require("./cfSchema"); - -const readFileAsync = promisify(fs.readFile); - -const addS3BucketToResources = async (bucketName, baseCf) => { - const cf = clone(baseCf); - - const filename = path.resolve(__dirname, "../resources.yml"); - const resourcesContent = await readFileAsync(filename, "utf-8"); - - const resources = yaml.safeLoad(resourcesContent, { - filename, - schema: cfSchema - }); - - merge(cf, resources); - - cf.Resources.NextStaticAssetsS3Bucket.Properties.BucketName = bucketName; - - return cf; -}; - -module.exports = addS3BucketToResources; diff --git a/lib/build.js b/lib/build.js index d5bfd4fdaa..26ca04b60b 100644 --- a/lib/build.js +++ b/lib/build.js @@ -18,10 +18,22 @@ const overrideTargetIfNotServerless = nextConfiguration => { } }; -module.exports = async (pluginBuildDir, pageConfig, customHandler) => { +module.exports = async function() { + const pluginBuildDir = this.pluginBuildDir; + const nextConfigDir = pluginBuildDir.nextConfigDir; + const pageConfig = this.getPluginConfigValue("pageConfig"); + const customHandler = this.getPluginConfigValue("customHandler"); + const routes = this.getPluginConfigValue("routes"); + logger.log("Started building next app ..."); - const nextConfigDir = pluginBuildDir.nextConfigDir; + const servicePackage = this.serverless.service.package; + + servicePackage.include = servicePackage.include || []; + servicePackage.include.push( + path.posix.join(pluginBuildDir.posixBuildDir, "**") + ); + const { nextConfiguration } = await parseNextConfiguration(nextConfigDir); overrideTargetIfNotServerless(nextConfiguration); @@ -39,13 +51,26 @@ module.exports = async (pluginBuildDir, pageConfig, customHandler) => { ); } - const nextPages = await getNextPagesFromBuildDir( - pluginBuildDir.buildDir, + const nextPages = await getNextPagesFromBuildDir(pluginBuildDir.buildDir, { pageConfig, - customHandler ? [path.basename(customHandler)] : undefined - ); + routes, + additionalExcludes: customHandler + ? [path.basename(customHandler)] + : undefined + }); await rewritePageHandlers(nextPages, customHandler); + const service = this.serverless.service; + + this.nextPages = nextPages; + + nextPages.forEach(page => { + const functionName = page.functionName; + service.functions[functionName] = page.serverlessFunction[functionName]; + }); + + this.serverless.service.setFunctionNames(); + return nextPages; }; diff --git a/lib/getAssetsBucketName.js b/lib/getAssetsBucketName.js new file mode 100644 index 0000000000..118e968df5 --- /dev/null +++ b/lib/getAssetsBucketName.js @@ -0,0 +1,28 @@ +const parseNextConfiguration = require("./parseNextConfiguration"); + +module.exports = function() { + const nextConfigDir = this.getPluginConfigValue("nextConfigDir"); + const staticDir = this.getPluginConfigValue("staticDir"); + + let { staticAssetsBucket } = parseNextConfiguration(nextConfigDir); + + const bucketNameFromConfig = this.getPluginConfigValue("assetsBucketName"); + + if (bucketNameFromConfig) { + // bucket name provided via user config takes precendence + // over parsed value from assetPrefix + staticAssetsBucket = bucketNameFromConfig; + } + + if (!staticAssetsBucket) { + if (staticDir) { + throw new Error( + "staticDir requires a bucket. See https://github.com/danielcondemarin/serverless-nextjs-plugin#hosting-static-assets" + ); + } + + return null; + } + + return staticAssetsBucket; +}; diff --git a/lib/getNextPagesFromBuildDir.js b/lib/getNextPagesFromBuildDir.js index 6601b36d8f..7d5d08a1c2 100644 --- a/lib/getNextPagesFromBuildDir.js +++ b/lib/getNextPagesFromBuildDir.js @@ -35,7 +35,9 @@ const getBuildFiles = buildDir => { }); }; -module.exports = async (buildDir, pageConfig = {}, additionalExcludes = []) => { +module.exports = async (buildDir, options = {}) => { + const { pageConfig = {}, additionalExcludes = [], routes = [] } = options; + const buildFiles = await getBuildFiles(buildDir); const [buildDirRoot] = buildDir.split(path.sep); const exclude = excludeBuildFiles.concat(additionalExcludes); @@ -54,6 +56,14 @@ module.exports = async (buildDir, pageConfig = {}, additionalExcludes = []) => { .filter(bf => !bf.endsWith(SOURCE_MAP_EXT)) .map(normalisedFilePath => { const nextPage = new NextPage(normalisedFilePath); + + nextPage.routes = routes + .filter(r => r.src === nextPage.pageId) + .map(r => { + const { src, ...routeParams } = r; + return routeParams; + }); + nextPage.serverlessFunctionOverrides = Object.assign( {}, pageConfig["*"], diff --git a/package.json b/package.json index 1ba8147f9e..43e56f8b9b 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "eslint-config-prettier": "^4.1.0", "eslint-plugin-prettier": "^3.0.1", "jest": "^24.1.0", + "jest-when": "^2.5.0", "next": "^8.0.4", "prettier": "^1.16.4", "react": "^16.8.6", @@ -60,7 +61,7 @@ ], "coverageDirectory": "/coverage/", "coveragePathIgnorePatterns": [ - "lib/cfSchema.js", + "/utils/yml/cfSchema.js", "/utils/test" ], "modulePathIgnorePatterns": [ diff --git a/resources/api-gw-proxy.yml b/resources/api-gw-proxy.yml new file mode 100644 index 0000000000..59e3784083 --- /dev/null +++ b/resources/api-gw-proxy.yml @@ -0,0 +1,28 @@ +Resources: + ProxyResource: + Type: AWS::ApiGateway::Resource + Properties: + ParentId: + Fn::GetAtt: + - ApiGatewayRestApi # serverless default Rest API logical ID + - RootResourceId + PathPart: TO_BE_REPLACED # the endpoint in your API that is set as proxy + RestApiId: + Ref: ApiGatewayRestApi + ProxyMethod: + Type: AWS::ApiGateway::Method + Properties: + AuthorizationType: NONE + ResourceId: + Ref: TO_BE_REPLACED + RestApiId: + Ref: ApiGatewayRestApi + HttpMethod: GET + MethodResponses: + - StatusCode: 200 + Integration: + IntegrationHttpMethod: ANY + Type: HTTP_PROXY + Uri: TO_BE_REPLACED + IntegrationResponses: + - StatusCode: 200 diff --git a/resources.yml b/resources/assets-bucket.yml similarity index 100% rename from resources.yml rename to resources/assets-bucket.yml diff --git a/utils/test/ServerlessPluginBuilder.js b/utils/test/ServerlessPluginBuilder.js index 6966b8ae2a..9b1e3d06ff 100644 --- a/utils/test/ServerlessPluginBuilder.js +++ b/utils/test/ServerlessPluginBuilder.js @@ -48,7 +48,7 @@ class ServerlessPluginBuilder { return this; } - withNextCustomConfig(config) { + withPluginConfig(config) { merge(this.serverless, { service: { custom: { diff --git a/lib/cfSchema.js b/utils/yml/cfSchema.js similarity index 100% rename from lib/cfSchema.js rename to utils/yml/cfSchema.js diff --git a/utils/yml/load.js b/utils/yml/load.js new file mode 100644 index 0000000000..9ef65de271 --- /dev/null +++ b/utils/yml/load.js @@ -0,0 +1,14 @@ +const fse = require("fs-extra"); +const yaml = require("js-yaml"); +const cfSchema = require("./cfSchema"); + +const load = async ymlFullPath => { + const ymlStr = await fse.readFile(ymlFullPath, "utf-8"); + + return yaml.safeLoad(ymlStr, { + ymlFullPath, + schema: cfSchema + }); +}; + +module.exports = load;