Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🚧 Add bundledLambdas option for bundling them with esbuild #2178

Closed
wants to merge 1 commit into from
Closed
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ The fourth cache behaviour handles next API requests `api/*`.
| build.cleanupDotNext | `boolean` | `true` | Whether to clean up `.next` directory before running the build step |
| build.assetIgnorePatterns | `string[]` | `[]` | Glob patterns to ignore when discovering files to copy from _next/static, public, static directories. |
| build.useV2Handler | `boolean` | `false` | **Experimental** Set this to true to use V2 handlers which starts to use genericized handlers. Note: this has the functionality of `separateApiLambda` and `disableOriginResponseHandler` so it should not be used together. Also, it is not completely optimized yet in terms of code size, but should still be performant. In the future, we will likely use this mode by default. |
| build.bundledLambdas | `boolean` | `false` | **Experimental** Set this to true to use [esbuild](https://esbuild.github.io) to minify and bundle your Lambdas into a single file. This greatly decreases the Lambda file size and speeds up cold-boot time. |
| cloudfront | `object` | `{}` | Inputs to be passed to [aws-cloudfront](https://github.com/serverless-components/aws-cloudfront) |
| certificateArn | `string` | `` | Specific certificate ARN to use for CloudFront distribution. Helpful if you have a wildcard SSL cert you wish to use. This currently works only in tandem with the`domain`input. Please check [custom CloudFront configuration](https://github.com/serverless-nextjs/serverless-next.js#custom-cloudfront-configuration) for how to specify`certificate`without needing to use the`domain`input (note that doing so will override any certificate due to the domain input). |
| domainType |`string` |`"both"` | Can be one of:`"apex"`- apex domain only, don't create a www subdomain.`"www"` - www domain only, don't create an apex subdomain.`"both"`- create both www and apex domains when either one is provided. |
Expand Down
5 changes: 3 additions & 2 deletions packages/libs/core/src/build/lib/readDirectoryFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import normalizePath from "normalize-path";

const readDirectoryFiles = async (
directory: string,
ignorePatterns: string[]
ignorePatterns: string[],
onlyFiles = true
): Promise<Array<Entry>> => {
const directoryExists = fse.pathExistsSync(directory);
if (!directoryExists) {
Expand All @@ -16,7 +17,7 @@ const readDirectoryFiles = async (
const normalizedDirectory = normalizePath(directory);

return await glob(path.posix.join(normalizedDirectory, "**", "*"), {
onlyFiles: true,
onlyFiles,
stats: true,
dot: true, // To allow matching dot files or directories
ignore: ignorePatterns
Expand Down
1 change: 1 addition & 0 deletions packages/libs/lambda-at-edge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"@sls-next/aws-common": "link:../aws-common",
"@sls-next/core": "link:../core",
"@vercel/nft": "0.17.0",
"esbuild": "0.14.2",
"execa": "5.1.1",
"fs-extra": "9.1.0",
"get-stream": "6.0.1",
Expand Down
78 changes: 65 additions & 13 deletions packages/libs/lambda-at-edge/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import filterOutDirectories from "@sls-next/core/dist/build/lib/filterOutDirecto
import { Job } from "@vercel/nft/out/node-file-trace";
import { prepareBuildManifests } from "@sls-next/core";
import { NextConfig } from "@sls-next/core";
import type { ServerlessComponentInputs } from "@sls-next/serverless-component/types";
import { NextI18nextIntegration } from "@sls-next/core/dist/build/third-party/next-i18next";
import normalizePath from "normalize-path";
import { bundleLambda } from "./lib/bundleLambda";

export const DEFAULT_LAMBDA_CODE_DIR = "default-lambda";
export const API_LAMBDA_CODE_DIR = "api-lambda";
Expand Down Expand Up @@ -52,6 +54,8 @@ type BuildOptions = {
separateApiLambda?: boolean;
disableOriginResponseHandler?: boolean;
useV2Handler?: boolean;
bundledLambdas?: boolean;
runtime?: ServerlessComponentInputs["runtime"];
};

const defaultBuildOptions = {
Expand All @@ -71,7 +75,8 @@ const defaultBuildOptions = {
assetIgnorePatterns: [],
regenerationQueueName: undefined,
separateApiLambda: true,
useV2Handler: false
useV2Handler: false,
bundledLambdas: false
};

class Builder {
Expand All @@ -98,6 +103,20 @@ class Builder {
}
}

getRuntime(handler: string) {
const { runtime } = this.buildOptions;
if (!runtime) {
return null;
}
if (typeof runtime === "string") {
return runtime;
}
if (handler in runtime) {
return runtime[handler as keyof typeof runtime] || null;
}
return null;
}

async readPublicFiles(assetIgnorePatterns: string[]): Promise<string[]> {
const dirExists = await fse.pathExists(join(this.nextConfigDir, "public"));
if (dirExists) {
Expand Down Expand Up @@ -285,12 +304,12 @@ class Builder {
apiBuildManifest: OriginRequestApiHandlerManifest,
separateApiLambda: boolean,
useV2Handler: boolean
): Promise<void[]> {
): Promise<void> {
const hasAPIRoutes = await fse.pathExists(
join(this.serverlessDir, "pages/api")
);

return Promise.all([
await Promise.all([
this.copyTraces(buildManifest, DEFAULT_LAMBDA_CODE_DIR),
this.processAndCopyHandler(
useV2Handler ? "default-handler-v2" : "default-handler",
Expand Down Expand Up @@ -363,11 +382,19 @@ class Builder {
join(this.outputDir, REGENERATION_LAMBDA_CODE_DIR)
)
]);

if (this.buildOptions.bundledLambdas) {
await bundleLambda(
this.outputDir,
DEFAULT_LAMBDA_CODE_DIR,
this.getRuntime("defaultLambda")
);
}
}

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

if (this.buildOptions.useServerlessTraceTarget) {
Expand Down Expand Up @@ -396,7 +423,7 @@ class Builder {
);
}

return Promise.all([
await Promise.all([
...copyTraces,
this.processAndCopyHandler(
"api-handler",
Expand Down Expand Up @@ -424,6 +451,14 @@ class Builder {
join(this.outputDir, API_LAMBDA_CODE_DIR, "routes-manifest.json")
)
]);

if (this.buildOptions.bundledLambdas) {
await bundleLambda(
this.outputDir,
API_LAMBDA_CODE_DIR,
this.getRuntime("apiLambda")
);
}
}

async buildRegenerationHandler(
Expand Down Expand Up @@ -461,6 +496,14 @@ class Builder {
}
)
]);

if (this.buildOptions.bundledLambdas) {
await bundleLambda(
this.outputDir,
REGENERATION_LAMBDA_CODE_DIR,
this.getRuntime("regenerationLambda")
);
}
}

/**
Expand Down Expand Up @@ -527,19 +570,28 @@ class Builder {
join(this.dotNextDir, "routes-manifest.json"),
join(this.outputDir, IMAGE_LAMBDA_CODE_DIR, "routes-manifest.json")
),
fse.copy(
join(
path.dirname(require.resolve("@sls-next/core/package.json")),
"dist",
"sharp_node_modules"
),
join(this.outputDir, IMAGE_LAMBDA_CODE_DIR, "node_modules")
),
fse.copy(
join(this.dotNextDir, "images-manifest.json"),
join(this.outputDir, IMAGE_LAMBDA_CODE_DIR, "images-manifest.json")
)
]);

if (this.buildOptions.bundledLambdas) {
await bundleLambda(
this.outputDir,
IMAGE_LAMBDA_CODE_DIR,
this.getRuntime("imageLambda")
);
}

await fse.copy(
join(
path.dirname(require.resolve("@sls-next/core/package.json")),
"dist",
"sharp_node_modules"
),
join(this.outputDir, IMAGE_LAMBDA_CODE_DIR, "node_modules")
);
}

async readNextConfig(): Promise<NextConfig | undefined> {
Expand Down
86 changes: 86 additions & 0 deletions packages/libs/lambda-at-edge/src/lib/bundleLambda.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Runtime as LambdaRuntime } from "@aws-cdk/aws-lambda/lib/runtime";
import readDirectoryFiles from "@sls-next/core/dist/build/lib/readDirectoryFiles";
import type { ServerlessComponentInputs } from "@sls-next/serverless-component/types";
import { build } from "esbuild";
import glob from "fast-glob";
import fse from "fs-extra";
import path from "path";

const TARGET_FILE = "bundle.js";

type Runtime = NonNullable<ServerlessComponentInputs["runtime"]>;

const getTarget = (input?: string): string => {
switch (input) {
case LambdaRuntime.NODEJS_14_X.name:
return "node14";
case LambdaRuntime.NODEJS_10_X.name:
return "node10";
case LambdaRuntime.NODEJS_12_X.name:
default:
return "node12";
}
};

export const bundleLambda = async (
outputDir: string,
handler: string,
runtime: Runtime | null
) => {
const target = path.join(outputDir, handler);
const index = path.join(target, "index.js");
const outfile = path.join(target, TARGET_FILE);

const pathExists = await fse.pathExists(index);
if (!pathExists) {
throw `Failed to bundle \`${handler}\`, file \`${index}\` not found...`;
}

try {
await build({
bundle: true,
entryPoints: [index],
external: ["sharp"],
format: "cjs",
legalComments: "none" /** Handler code is not distributed */,
minify: true,
outfile,
platform: "node",
target: getTarget(
runtime
? typeof runtime === "string"
? runtime
: runtime[handler as keyof typeof runtime]
: undefined
)
});
} catch (error) {
throw `Esbuild failed to bundle \`${handler}\`.`;
}

const outputFiles = await readDirectoryFiles(target, [
outfile,
"**/BUILD_ID"
]);

// Remove all output files after bundling
await Promise.all(
Array.from(new Set(outputFiles)).map((file) => fse.remove(file.path))
);

const emptyDirectories = await readDirectoryFiles(
target,
[outfile, "**/BUILD_ID"],
false
);

// Remove all empty leftover directories
await Promise.all(
emptyDirectories.map((dir) =>
fse
.remove(dir.path)
// eslint-disable-next-line @typescript-eslint/no-empty-function
.catch(() => {})
)
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { remove, pathExists } from "fs-extra";
import path from "path";
import { getNextBinary } from "../../test-utils";
import os from "os";
import Builder from "../../../src/build";

jest.unmock("execa");

describe("Next.js 12 bundled lambdas", () => {
const nextBinary = getNextBinary();
const fixtureDir = path.join(__dirname, "./fixture");
let outputDir: string;

beforeAll(async () => {
outputDir = path.join(
os.tmpdir(),
new Date().getUTCMilliseconds().toString(),
"slsnext-test-build"
);

console.log("outputDir:", outputDir);

const builder = new Builder(fixtureDir, outputDir, {
cwd: fixtureDir,
cmd: nextBinary,
args: ["build"],
bundledLambdas: true
});

await builder.build();
});

afterAll(() => {
return Promise.all(
[".next"].map((file) => remove(path.join(fixtureDir, file)))
);
});

it("does not copy node_modules to default lambda artefact", async () => {
const exists = await pathExists(
path.join(outputDir, "default-lambda/node_modules")
);
expect(exists).toBe(false);
});

it("copies bundle.js to default lambda artefact", async () => {
const exists = await pathExists(
path.join(outputDir, "default-lambda/bundle.js")
);

expect(exists).toBe(true);
});
});
11 changes: 9 additions & 2 deletions packages/serverless-components/nextjs-component/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ class NextjsComponent extends Component {
? nextConfigPath
: resolve(inputs.build.baseDir);

const bundledLambdas =
typeof inputs.build !== "boolean" &&
typeof inputs.build !== "undefined" &&
!!inputs.build.bundledLambdas;

const buildConfig: BuildOptions = {
enabled: inputs.build
? // @ts-ignore
Expand All @@ -230,7 +235,8 @@ class NextjsComponent extends Component {
...(typeof inputs.build === "object" ? inputs.build : {}),
cwd: buildCwd,
baseDir: buildBaseDir, // @ts-ignore
cleanupDotNext: inputs.build?.cleanupDotNext ?? true
cleanupDotNext: inputs.build?.cleanupDotNext ?? true,
bundledLambdas
};

if (buildConfig.enabled) {
Expand Down Expand Up @@ -258,7 +264,8 @@ class NextjsComponent extends Component {
separateApiLambda: buildConfig.separateApiLambda ?? true,
disableOriginResponseHandler:
buildConfig.disableOriginResponseHandler ?? false,
useV2Handler: buildConfig.useV2Handler ?? false
useV2Handler: buildConfig.useV2Handler ?? false,
runtime: inputs.runtime ?? undefined
},
nextStaticPath
);
Expand Down
1 change: 1 addition & 0 deletions packages/serverless-components/nextjs-component/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export type BuildOptions = {
separateApiLambda?: boolean;
disableOriginResponseHandler?: boolean;
useV2Handler?: boolean;
bundledLambdas?: boolean;
};

export type LambdaType =
Expand Down
Loading