Skip to content

Commit

Permalink
feat(lambda-at-edge): add serverless trace target support (serverless…
Browse files Browse the repository at this point in the history
  • Loading branch information
danielcondemarin authored and sclaughl committed Jul 16, 2020
1 parent fa4d3e3 commit eac52de
Show file tree
Hide file tree
Showing 21 changed files with 760 additions and 46 deletions.
1 change: 1 addition & 0 deletions packages/lambda-at-edge/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
!tests/fixtures/**
!tests/serverless-trace/**
dist/
531 changes: 507 additions & 24 deletions packages/lambda-at-edge/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/lambda-at-edge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"next-aws-cloudfront": "file:../cloudfront-lambda@edge-compat"
},
"dependencies": {
"@zeit/node-file-trace": "^0.5.1",
"execa": "^4.0.0",
"fs-extra": "^9.0.0",
"path-to-regexp": "^6.1.0"
Expand Down
120 changes: 107 additions & 13 deletions packages/lambda-at-edge/src/build.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import nodeFileTrace from "@zeit/node-file-trace";
import execa from "execa";
import fse from "fs-extra";
import { join } from "path";
Expand All @@ -17,28 +18,32 @@ export const DEFAULT_LAMBDA_CODE_DIR = "default-lambda";
export const API_LAMBDA_CODE_DIR = "api-lambda";

type BuildOptions = {
args: string[];
cwd: string;
env: NodeJS.ProcessEnv;
cmd: string;
args?: string[];
cwd?: string;
env?: NodeJS.ProcessEnv;
cmd?: string;
};

const defaultBuildOptions = {
args: [],
cwd: process.cwd(),
env: {},
cmd: "./node_modules/.bin/next"
};

class Builder {
nextConfigDir: string;
dotNextDirectory: string;
outputDir: string;
buildOptions: BuildOptions = {
args: [],
cwd: process.cwd(),
env: {},
cmd: "./node_modules/.bin/next"
};
buildOptions: BuildOptions = defaultBuildOptions;

constructor(
nextConfigDir: string,
outputDir: string,
buildOptions?: BuildOptions
) {
this.nextConfigDir = nextConfigDir;
this.dotNextDirectory = path.join(this.nextConfigDir, ".next");
this.outputDir = outputDir;
if (buildOptions) {
this.buildOptions = buildOptions;
Expand Down Expand Up @@ -100,10 +105,65 @@ class Builder {
return sortedPagesManifest;
}

buildDefaultLambda(
get isServerlessTraceTarget(): boolean {
try {
// eslint-disable-next-line
const nextConfig = require(`${path.resolve(
path.join(this.nextConfigDir, "next.config.js")
)}`);

if (nextConfig.target === "experimental-serverless-trace") {
return true;
}
} catch (err) {
// ignore error, it just means we can't use the experimental serverless trace
}

return false;
}

async buildDefaultLambda(
buildManifest: OriginRequestDefaultHandlerManifest
): Promise<void[]> {
let copyTraces: Promise<void>[] = [];

if (this.isServerlessTraceTarget) {
const ignoreAppAndDocumentPages = (page: string): boolean => {
const basename = path.basename(page);
return basename !== "_app.js" && basename !== "_document.js";
};

const allSsrPages = [
...Object.values(buildManifest.pages.ssr.nonDynamic),
...Object.values(buildManifest.pages.ssr.dynamic).map(
entry => entry.file
)
].filter(ignoreAppAndDocumentPages);

const ssrPages = Object.values(allSsrPages).map(pageFile =>
path.join(this.dotNextDirectory, "serverless", pageFile)
);

const { fileList, reasons } = await nodeFileTrace(ssrPages, {
base: path.resolve(this.nextConfigDir)
});

copyTraces = fileList
.filter(file => {
// exclude "initial" files from lambda artefact. These are just the pages themselves
// which are copied over separately
return !reasons[file] || reasons[file].type !== "initial";
})
.map((filePath: string) => {
return fse.copy(
path.join(path.resolve(this.nextConfigDir), filePath),
join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR, filePath)
);
});
}

return Promise.all([
...copyTraces,
fse.copy(
require.resolve("@sls-next/lambda-at-edge/dist/default-handler.js"),
join(this.outputDir, DEFAULT_LAMBDA_CODE_DIR, "index.js")
Expand Down Expand Up @@ -144,10 +204,41 @@ class Builder {
]);
}

buildApiLambda(
async buildApiLambda(
apiBuildManifest: OriginRequestApiHandlerManifest
): Promise<void[]> {
let copyTraces: Promise<void>[] = [];

if (this.isServerlessTraceTarget) {
const allApiPages = [
...Object.values(apiBuildManifest.apis.nonDynamic),
...Object.values(apiBuildManifest.apis.dynamic).map(entry => entry.file)
];

const apiPages = Object.values(allApiPages).map(pageFile =>
path.join(this.dotNextDirectory, "serverless", pageFile)
);

const { fileList, reasons } = await nodeFileTrace(apiPages, {
base: path.resolve(this.nextConfigDir)
});

copyTraces = fileList
.filter(file => {
// exclude "initial" files from lambda artefact. These are just the pages themselves
// which are copied over separately
return !reasons[file] || reasons[file].type !== "initial";
})
.map((filePath: string) => {
return fse.copy(
path.join(path.resolve(this.nextConfigDir), filePath),
join(this.outputDir, API_LAMBDA_CODE_DIR, filePath)
);
});
}

return Promise.all([
...copyTraces,
fse.copy(
require.resolve("@sls-next/lambda-at-edge/dist/api-handler.js"),
join(this.outputDir, API_LAMBDA_CODE_DIR, "index.js")
Expand Down Expand Up @@ -275,7 +366,10 @@ class Builder {
}

async build(): Promise<void> {
const { cmd, args, cwd, env } = this.buildOptions;
const { cmd, args, cwd, env } = Object.assign(
defaultBuildOptions,
this.buildOptions
);

// cleanup .next/ directory except for cache/ folder
await this.cleanupDotNext();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"/api/test-page": "pages/api/test-api-page.js",
"/api/sub/[dynamic-test-api-page]": "pages/api/sub/[dynamic-test-api-page].js",
"/test-page": "pages/test-page.js",
"/sub/[dynamic-test-page]": "pages/sub/[dynamic-test-page].js",
"/_app": "pages/_app.js",
"/_document": "pages/_document.js"
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const apiDependencyB = require("api-dependency-b");

module.exports = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const apiDependency = require("api-dependency-a");

module.exports = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// the following dependency is shared with other pages but should be copied over to artefact only once
const someDependency = require("dependency-a");

const anotherDependency = require("dependency-b");

module.exports = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const someDependency = require("dependency-a");

module.exports = {};
3 changes: 3 additions & 0 deletions packages/lambda-at-edge/tests/serverless-trace/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
target: "experimental-serverless-trace"
};

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import fse from "fs-extra";
import os from "os";
import path from "path";
import Builder from "../../../src/build";
import {
DEFAULT_LAMBDA_CODE_DIR,
API_LAMBDA_CODE_DIR
} from "../../../src/build";

describe("Serverless Trace", () => {
const fixturePath = path.join(__dirname, "../");
let outputDir: string;
let fseRemoveSpy: jest.SpyInstance;

beforeEach(async () => {
outputDir = os.tmpdir();

fseRemoveSpy = jest.spyOn(fse, "remove").mockImplementation(() => {
return;
});

const builder = new Builder(fixturePath, outputDir);

await builder.build();
});

afterEach(() => {
fseRemoveSpy.mockRestore();
});

it("copies api page dependencies to api lambda artefact", async () => {
const nodeModulesPath = path.join(
outputDir,
API_LAMBDA_CODE_DIR,
"node_modules"
);

const nodeModulesExists = await fse.pathExists(nodeModulesPath);
expect(nodeModulesExists).toBe(true);

const nodeModulesFiles = await fse.readdir(nodeModulesPath);
expect(nodeModulesFiles).toEqual(
expect.arrayContaining(["api-dependency-a", "api-dependency-b"])
);

const dependencyAPath = path.join(nodeModulesPath, "api-dependency-a");
const dependencyAFiles = await fse.readdir(dependencyAPath);
expect(dependencyAFiles).toEqual(expect.arrayContaining(["index.js"]));

const dependencyBPath = path.join(nodeModulesPath, "api-dependency-b");
const dependencyBFiles = await fse.readdir(dependencyBPath);
expect(dependencyBFiles).toEqual(expect.arrayContaining(["index.js"]));
});

it("copies ssr page dependencies to lambda artefact", async () => {
const nodeModulesPath = path.join(
outputDir,
DEFAULT_LAMBDA_CODE_DIR,
"node_modules"
);

const nodeModulesExists = await fse.pathExists(nodeModulesPath);
expect(nodeModulesExists).toBe(true);

const nodeModulesFiles = await fse.readdir(nodeModulesPath);
expect(nodeModulesFiles).toEqual(
expect.arrayContaining(["dependency-a", "dependency-b"])
);

const dependencyAPath = path.join(nodeModulesPath, "dependency-a");
const dependencyAFiles = await fse.readdir(dependencyAPath);
expect(dependencyAFiles).toEqual(
expect.arrayContaining(["index.js", "sub-dependency.js"])
);

const dependencyBPath = path.join(nodeModulesPath, "dependency-b");
const dependencyBFiles = await fse.readdir(dependencyBPath);
expect(dependencyBFiles).toEqual(expect.arrayContaining(["index.js"]));
});

it("does not copy any .next/ files into lambda artefact", async () => {
const nodeModulesPath = path.join(
outputDir,
DEFAULT_LAMBDA_CODE_DIR,
".next"
);

const dotNextDirExists = await fse.pathExists(nodeModulesPath);
expect(dotNextDirExists).toBe(false);
});
});
2 changes: 1 addition & 1 deletion packages/lambda-at-edge/tests/test-utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { remove } from "fs-extra";
import { OriginRequestEvent } from "../src/types";

export const cleanupDir = (dir): Promise<void> => {
export const cleanupDir = (dir: string): Promise<void> => {
return remove(dir);
};

Expand Down
21 changes: 16 additions & 5 deletions packages/serverless-plugin/lib/__tests__/rewritePageHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,26 @@ const getFactoryHandlerCode = require("../getFactoryHandlerCode");
const NextPage = require("../../classes/NextPage");
const logger = require("../../utils/logger");

jest.mock("fs");
jest.mock("../../utils/logger");
jest.mock("../getFactoryHandlerCode");

describe("rewritePageHandlers", () => {
describe("when compat layer is injected successfully", () => {
const pagesDir = "build/serverless/pages";
let rewritePageHandlersPromise;
let fsRenameSpy;
let fsWriteFileSpy;

beforeEach(() => {
fs.rename.mockImplementation((fileName, newFileName, cb) => cb(null, ""));
fs.writeFile.mockImplementation((filePath, data, cb) => {
cb(null, undefined);
});
fsRenameSpy = jest
.spyOn(fs, "rename")
.mockImplementation((fileName, newFileName, cb) => cb(null, ""));

fsWriteFileSpy = jest
.spyOn(fs, "writeFile")
.mockImplementation((filePath, data, cb) => {
cb(null, {});
});

getFactoryHandlerCode.mockReturnValue("module.exports.render={...}");

Expand All @@ -30,6 +36,11 @@ describe("rewritePageHandlers", () => {
return rewritePageHandlersPromise;
});

afterEach(() => {
fsRenameSpy.mockRestore();
fsWriteFileSpy.mockRestore();
});

it("should log", () => {
expect(logger.log).toBeCalledWith(
expect.stringContaining(
Expand Down
6 changes: 3 additions & 3 deletions packages/serverless-plugin/lib/rewritePageHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ const { promisify } = require("util");
const getFactoryHandlerCode = require("./getFactoryHandlerCode");
const logger = require("../utils/logger");

const writeFileAsync = promisify(fs.writeFile);
const renameAsync = promisify(fs.rename);

const processJsHandler = async (nextPage, customHandler) => {
const writeFileAsync = promisify(fs.writeFile);
const renameAsync = promisify(fs.rename);

const compatCodeContent = getFactoryHandlerCode(
nextPage.pagePath,
customHandler
Expand Down

0 comments on commit eac52de

Please sign in to comment.