Skip to content
This repository has been archived by the owner on Jan 28, 2025. It is now read-only.

Serverless trace target support #405

Merged
merged 5 commits into from
May 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
}
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