@serverless-stack/nextjs-lambda
Version:
Provides handlers that can be used in CloudFront Lambda@Edge to deploy next.js applications to the edge
419 lines (418 loc) • 24.7 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSETS_DIR = exports.REGENERATION_LAMBDA_CODE_DIR = exports.IMAGE_LAMBDA_CODE_DIR = exports.API_LAMBDA_CODE_DIR = exports.DEFAULT_LAMBDA_CODE_DIR = void 0;
const nft_1 = require("@vercel/nft");
const execa_1 = __importDefault(require("execa"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const path_1 = require("path");
const path_2 = __importDefault(require("path"));
const pathToPosix_1 = __importDefault(require("./lib/pathToPosix"));
const normalizeNodeModules_1 = __importDefault(require("./lib/normalizeNodeModules"));
const createServerlessConfig_1 = __importDefault(require("./lib/createServerlessConfig"));
const redirector_1 = require("./routing/redirector");
const readDirectoryFiles_1 = __importDefault(require("./lib/readDirectoryFiles"));
const filterOutDirectories_1 = __importDefault(require("./lib/filterOutDirectories"));
const nextjs_core_1 = require("@serverless-stack/nextjs-core");
const next_i18next_1 = require("./build/third-party/next-i18next");
const normalize_path_1 = __importDefault(require("normalize-path"));
exports.DEFAULT_LAMBDA_CODE_DIR = "default-lambda";
exports.API_LAMBDA_CODE_DIR = "api-lambda";
exports.IMAGE_LAMBDA_CODE_DIR = "image-lambda";
exports.REGENERATION_LAMBDA_CODE_DIR = "regeneration-lambda";
exports.ASSETS_DIR = "assets";
const defaultBuildOptions = {
args: [],
cwd: process.cwd(),
env: {},
cmd: "./node_modules/.bin/next",
useServerlessTraceTarget: false,
logLambdaExecutionTimes: false,
domainRedirects: {},
minifyHandlers: false,
enableHTTPCompression: true,
authentication: undefined,
resolve: undefined,
baseDir: process.cwd(),
cleanupDotNext: true,
assetIgnorePatterns: [],
regenerationQueueName: undefined
};
class Builder {
constructor(nextConfigDir, outputDir, buildOptions, nextStaticDir) {
this.buildOptions = defaultBuildOptions;
this.nextConfigDir = path_2.default.resolve(nextConfigDir);
this.nextStaticDir = path_2.default.resolve(nextStaticDir !== null && nextStaticDir !== void 0 ? nextStaticDir : nextConfigDir);
this.dotNextDir = path_2.default.join(this.nextConfigDir, ".next");
this.serverlessDir = path_2.default.join(this.dotNextDir, "serverless");
this.outputDir = outputDir;
if (buildOptions) {
this.buildOptions = buildOptions;
}
}
async readPublicFiles(assetIgnorePatterns) {
const dirExists = await fs_extra_1.default.pathExists((0, path_1.join)(this.nextConfigDir, "public"));
if (dirExists) {
const files = await (0, readDirectoryFiles_1.default)((0, path_1.join)(this.nextConfigDir, "public"), assetIgnorePatterns);
return files
.map((e) => (0, normalize_path_1.default)(e.path))
.map((path) => path.replace((0, normalize_path_1.default)(this.nextConfigDir), ""))
.map((path) => path.replace("/public/", ""));
}
else {
return [];
}
}
async readPagesManifest() {
const path = (0, path_1.join)(this.serverlessDir, "pages-manifest.json");
const hasServerlessPageManifest = await fs_extra_1.default.pathExists(path);
if (!hasServerlessPageManifest) {
return Promise.reject("pages-manifest not found. Check if `next.config.js` target is set to 'serverless'");
}
return await fs_extra_1.default.readJSON(path);
}
copyLambdaHandlerDependencies(fileList, reasons, handlerDirectory, base) {
return fileList
.filter((file) => {
if (file.endsWith(".ts") || file.endsWith(".tsx")) {
return false;
}
return ((!reasons[file] || reasons[file].type !== "initial") &&
file !== "package.json");
})
.map((filePath) => {
const resolvedFilePath = path_2.default.resolve((0, path_1.join)(base, filePath));
const dst = (0, normalizeNodeModules_1.default)(path_2.default.relative(this.serverlessDir, resolvedFilePath));
if (resolvedFilePath !== (0, path_1.join)(this.outputDir, handlerDirectory, dst)) {
return fs_extra_1.default.copy(resolvedFilePath, (0, path_1.join)(this.outputDir, handlerDirectory, dst));
}
else {
return Promise.resolve();
}
});
}
isSSRJSFile(buildManifest, relativePageFile) {
if (path_2.default.extname(relativePageFile) === ".js") {
const page = relativePageFile.startsWith("/")
? `pages${relativePageFile}`
: `pages/${relativePageFile}`;
if (page === "pages/_error.js" ||
Object.values(buildManifest.pages.ssr.nonDynamic).includes(page) ||
Object.values(buildManifest.pages.ssr.dynamic).includes(page)) {
return true;
}
}
return false;
}
async processAndCopyRoutesManifest(source, destination) {
const routesManifest = require(source);
routesManifest.redirects = routesManifest.redirects.filter((redirect) => {
return !(0, redirector_1.isTrailingSlashRedirect)(redirect, routesManifest.basePath);
});
await fs_extra_1.default.writeFile(destination, JSON.stringify(routesManifest));
}
async processAndCopyHandler(handlerType, destination, shouldMinify) {
const source = path_2.default.dirname(require.resolve(` -stack/nextjs-lambda/dist/${handlerType}/${shouldMinify ? "minified" : "standard"}`));
await fs_extra_1.default.copy(source, destination);
}
async copyTraces(buildManifest, destination) {
let copyTraces = [];
if (this.buildOptions.useServerlessTraceTarget) {
const ignoreAppAndDocumentPages = (page) => {
const basename = path_2.default.basename(page);
return basename !== "_app.js" && basename !== "_document.js";
};
const allSsrPages = [
...Object.values(buildManifest.pages.ssr.nonDynamic),
...Object.values(buildManifest.pages.ssr.dynamic)
].filter(ignoreAppAndDocumentPages);
const ssrPages = Object.values(allSsrPages).map((pageFile) => path_2.default.join(this.serverlessDir, pageFile));
const base = this.buildOptions.baseDir || process.cwd();
const { fileList, reasons } = await (0, nft_1.nodeFileTrace)(ssrPages, {
base,
resolve: this.buildOptions.resolve
});
copyTraces = this.copyLambdaHandlerDependencies(fileList, reasons, destination, base);
}
await Promise.all(copyTraces);
}
async buildDefaultLambda(buildManifest) {
var _a;
const hasAPIRoutes = await fs_extra_1.default.pathExists((0, path_1.join)(this.serverlessDir, "pages/api"));
return Promise.all([
this.copyTraces(buildManifest, exports.DEFAULT_LAMBDA_CODE_DIR),
this.processAndCopyHandler("default-handler", (0, path_1.join)(this.outputDir, exports.DEFAULT_LAMBDA_CODE_DIR), !!this.buildOptions.minifyHandlers),
((_a = this.buildOptions) === null || _a === void 0 ? void 0 : _a.handler)
? fs_extra_1.default.copy((0, path_1.join)(this.nextConfigDir, this.buildOptions.handler), (0, path_1.join)(this.outputDir, exports.DEFAULT_LAMBDA_CODE_DIR, this.buildOptions.handler))
: Promise.resolve(),
fs_extra_1.default.writeJson((0, path_1.join)(this.outputDir, exports.DEFAULT_LAMBDA_CODE_DIR, "manifest.json"), buildManifest),
fs_extra_1.default.copy((0, path_1.join)(this.serverlessDir, "pages"), (0, path_1.join)(this.outputDir, exports.DEFAULT_LAMBDA_CODE_DIR, "pages"), {
filter: (file) => {
const isNotPrerenderedHTMLPage = path_2.default.extname(file) !== ".html";
const isNotStaticPropsJSONFile = path_2.default.extname(file) !== ".json";
const isNotApiPage = (0, pathToPosix_1.default)(file).indexOf("pages/api") === -1;
const isNotExcludedJSFile = hasAPIRoutes ||
path_2.default.extname(file) !== ".js" ||
this.isSSRJSFile(buildManifest, (0, pathToPosix_1.default)(path_2.default.relative(path_2.default.join(this.serverlessDir, "pages"), file)));
return (isNotApiPage &&
isNotPrerenderedHTMLPage &&
isNotStaticPropsJSONFile &&
isNotExcludedJSFile);
}
}),
this.copyChunks(exports.DEFAULT_LAMBDA_CODE_DIR),
fs_extra_1.default.copy((0, path_1.join)(this.dotNextDir, "prerender-manifest.json"), (0, path_1.join)(this.outputDir, exports.DEFAULT_LAMBDA_CODE_DIR, "prerender-manifest.json")),
this.processAndCopyRoutesManifest((0, path_1.join)(this.dotNextDir, "routes-manifest.json"), (0, path_1.join)(this.outputDir, exports.DEFAULT_LAMBDA_CODE_DIR, "routes-manifest.json")),
this.runThirdPartyIntegrations((0, path_1.join)(this.outputDir, exports.DEFAULT_LAMBDA_CODE_DIR), (0, path_1.join)(this.outputDir, exports.REGENERATION_LAMBDA_CODE_DIR))
]);
}
async buildApiLambda(apiBuildManifest) {
var _a;
let copyTraces = [];
if (this.buildOptions.useServerlessTraceTarget) {
const allApiPages = [
...Object.values(apiBuildManifest.apis.nonDynamic),
...Object.values(apiBuildManifest.apis.dynamic).map((entry) => entry.file)
];
const apiPages = Object.values(allApiPages).map((pageFile) => path_2.default.join(this.serverlessDir, pageFile));
const base = this.buildOptions.baseDir || process.cwd();
const { fileList, reasons } = await (0, nft_1.nodeFileTrace)(apiPages, {
base,
resolve: this.buildOptions.resolve
});
copyTraces = this.copyLambdaHandlerDependencies(fileList, reasons, exports.API_LAMBDA_CODE_DIR, base);
}
return Promise.all([
...copyTraces,
this.processAndCopyHandler("api-handler", (0, path_1.join)(this.outputDir, exports.API_LAMBDA_CODE_DIR), !!this.buildOptions.minifyHandlers),
((_a = this.buildOptions) === null || _a === void 0 ? void 0 : _a.handler)
? fs_extra_1.default.copy((0, path_1.join)(this.nextConfigDir, this.buildOptions.handler), (0, path_1.join)(this.outputDir, exports.API_LAMBDA_CODE_DIR, this.buildOptions.handler))
: Promise.resolve(),
fs_extra_1.default.copy((0, path_1.join)(this.serverlessDir, "pages/api"), (0, path_1.join)(this.outputDir, exports.API_LAMBDA_CODE_DIR, "pages/api")),
this.copyChunks(exports.API_LAMBDA_CODE_DIR),
fs_extra_1.default.writeJson((0, path_1.join)(this.outputDir, exports.API_LAMBDA_CODE_DIR, "manifest.json"), apiBuildManifest),
this.processAndCopyRoutesManifest((0, path_1.join)(this.dotNextDir, "routes-manifest.json"), (0, path_1.join)(this.outputDir, exports.API_LAMBDA_CODE_DIR, "routes-manifest.json"))
]);
}
async buildRegenerationHandler(buildManifest) {
await Promise.all([
this.copyTraces(buildManifest, exports.REGENERATION_LAMBDA_CODE_DIR),
fs_extra_1.default.writeJson((0, path_1.join)(this.outputDir, exports.REGENERATION_LAMBDA_CODE_DIR, "manifest.json"), buildManifest),
this.processAndCopyHandler("regeneration-handler", (0, path_1.join)(this.outputDir, exports.REGENERATION_LAMBDA_CODE_DIR), !!this.buildOptions.minifyHandlers),
this.copyChunks(exports.REGENERATION_LAMBDA_CODE_DIR),
fs_extra_1.default.copy((0, path_1.join)(this.serverlessDir, "pages"), (0, path_1.join)(this.outputDir, exports.REGENERATION_LAMBDA_CODE_DIR, "pages"), {
filter: (file) => {
const isNotPrerenderedHTMLPage = path_2.default.extname(file) !== ".html";
const isNotStaticPropsJSONFile = path_2.default.extname(file) !== ".json";
const isNotApiPage = (0, pathToPosix_1.default)(file).indexOf("pages/api") === -1;
return (isNotPrerenderedHTMLPage &&
isNotStaticPropsJSONFile &&
isNotApiPage);
}
})
]);
}
copyChunks(buildDir) {
return !this.buildOptions.useServerlessTraceTarget &&
fs_extra_1.default.existsSync((0, path_1.join)(this.serverlessDir, "chunks"))
? fs_extra_1.default.copy((0, path_1.join)(this.serverlessDir, "chunks"), (0, path_1.join)(this.outputDir, buildDir, "chunks"))
: Promise.resolve();
}
async buildImageLambda(buildManifest) {
var _a;
await Promise.all([
this.processAndCopyHandler("image-handler", (0, path_1.join)(this.outputDir, exports.IMAGE_LAMBDA_CODE_DIR), !!this.buildOptions.minifyHandlers),
((_a = this.buildOptions) === null || _a === void 0 ? void 0 : _a.handler)
? fs_extra_1.default.copy((0, path_1.join)(this.nextConfigDir, this.buildOptions.handler), (0, path_1.join)(this.outputDir, exports.IMAGE_LAMBDA_CODE_DIR, this.buildOptions.handler))
: Promise.resolve(),
fs_extra_1.default.writeJson((0, path_1.join)(this.outputDir, exports.IMAGE_LAMBDA_CODE_DIR, "manifest.json"), buildManifest),
this.processAndCopyRoutesManifest((0, path_1.join)(this.dotNextDir, "routes-manifest.json"), (0, path_1.join)(this.outputDir, exports.IMAGE_LAMBDA_CODE_DIR, "routes-manifest.json")),
fs_extra_1.default.copy((0, path_1.join)(path_2.default.dirname(require.resolve("@serverless-stack/nextjs-lambda/package.json")), "dist", "sharp_node_modules"), (0, path_1.join)(this.outputDir, exports.IMAGE_LAMBDA_CODE_DIR, "node_modules")),
fs_extra_1.default.copy((0, path_1.join)(this.dotNextDir, "images-manifest.json"), (0, path_1.join)(this.outputDir, exports.IMAGE_LAMBDA_CODE_DIR, "images-manifest.json"))
]);
}
async readNextConfig() {
const nextConfigPath = path_2.default.join(this.nextConfigDir, "next.config.js");
if (await fs_extra_1.default.pathExists(nextConfigPath)) {
const nextConfig = await require(nextConfigPath);
let normalisedNextConfig;
if (typeof nextConfig === "object") {
normalisedNextConfig = nextConfig;
}
else if (typeof nextConfig === "function") {
normalisedNextConfig = nextConfig("phase-production-server", {});
}
return normalisedNextConfig;
}
}
async buildStaticAssets(defaultBuildManifest, routesManifest, ignorePatterns) {
const buildId = defaultBuildManifest.buildId;
const basePath = routesManifest.basePath;
const nextConfigDir = this.nextConfigDir;
const nextStaticDir = this.nextStaticDir;
const dotNextDirectory = path_2.default.join(this.nextConfigDir, ".next");
const assetOutputDirectory = path_2.default.join(this.outputDir, exports.ASSETS_DIR);
const normalizedBasePath = basePath ? basePath.slice(1) : "";
const withBasePath = (key) => path_2.default.join(normalizedBasePath, key);
const copyIfExists = async (source, destination) => {
if (await fs_extra_1.default.pathExists(source)) {
await fs_extra_1.default.copy(source, destination);
}
};
const copyBuildId = copyIfExists(path_2.default.join(dotNextDirectory, "BUILD_ID"), path_2.default.join(assetOutputDirectory, withBasePath("BUILD_ID")));
const buildStaticFiles = await (0, readDirectoryFiles_1.default)(path_2.default.join(dotNextDirectory, "static"), ignorePatterns);
const staticFileAssets = buildStaticFiles
.filter(filterOutDirectories_1.default)
.map((fileItem) => {
const source = fileItem.path;
const destination = path_2.default.join(assetOutputDirectory, withBasePath(path_2.default
.relative(path_2.default.resolve(nextConfigDir), source)
.replace(/^.next/, "_next")));
return copyIfExists(source, destination);
});
const htmlPaths = [
...Object.keys(defaultBuildManifest.pages.html.dynamic),
...Object.keys(defaultBuildManifest.pages.html.nonDynamic)
];
const ssgPaths = Object.keys(defaultBuildManifest.pages.ssg.nonDynamic);
const fallbackFiles = Object.values(defaultBuildManifest.pages.ssg.dynamic)
.map(({ fallback }) => fallback)
.filter((fallback) => fallback);
const htmlFiles = [...htmlPaths, ...ssgPaths].map((path) => {
return path.endsWith("/") ? `${path}index.html` : `${path}.html`;
});
const jsonFiles = ssgPaths.map((path) => {
return path.endsWith("/") ? `${path}index.json` : `${path}.json`;
});
const htmlAssets = [...htmlFiles, ...fallbackFiles].map((file) => {
const source = path_2.default.join(dotNextDirectory, `serverless/pages${file}`);
const destination = path_2.default.join(assetOutputDirectory, withBasePath(`static-pages/${buildId}${file}`));
return copyIfExists(source, destination);
});
const jsonAssets = jsonFiles.map((file) => {
const source = path_2.default.join(dotNextDirectory, `serverless/pages${file}`);
const destination = path_2.default.join(assetOutputDirectory, withBasePath(`_next/data/${buildId}${file}`));
return copyIfExists(source, destination);
});
if (await fs_extra_1.default.pathExists(path_2.default.join(nextStaticDir, "public", "static"))) {
throw new Error("You cannot have assets in the directory [public/static] as they conflict with the static/* CloudFront cache behavior. Please move these assets into another directory.");
}
const buildPublicOrStaticDirectory = async (directory) => {
const directoryPath = path_2.default.join(nextStaticDir, directory);
if (!(await fs_extra_1.default.pathExists(directoryPath))) {
return Promise.resolve([]);
}
const files = await (0, readDirectoryFiles_1.default)(directoryPath, ignorePatterns);
return files.filter(filterOutDirectories_1.default).map((fileItem) => {
const source = fileItem.path;
const destination = path_2.default.join(assetOutputDirectory, withBasePath(path_2.default.relative(path_2.default.resolve(nextStaticDir), fileItem.path)));
return fs_extra_1.default.copy(source, destination);
});
};
const [publicDirAssets, staticDirAssets] = await Promise.all([
buildPublicOrStaticDirectory("public"),
buildPublicOrStaticDirectory("static")
]);
return Promise.all([
copyBuildId,
...staticFileAssets,
...htmlAssets,
...jsonAssets,
...publicDirAssets,
...staticDirAssets
]);
}
async cleanupDotNext(shouldClean) {
if (!shouldClean) {
return;
}
const exists = await fs_extra_1.default.pathExists(this.dotNextDir);
if (exists) {
const fileItems = await fs_extra_1.default.readdir(this.dotNextDir);
await Promise.all(fileItems
.filter((fileItem) => fileItem !== "cache")
.map((fileItem) => fs_extra_1.default.remove((0, path_1.join)(this.dotNextDir, fileItem))));
}
}
async build(debugMode) {
var _a, _b;
const { cmd, args, cwd, env, useServerlessTraceTarget, cleanupDotNext, assetIgnorePatterns } = Object.assign(defaultBuildOptions, this.buildOptions);
await Promise.all([
this.cleanupDotNext(cleanupDotNext),
fs_extra_1.default.emptyDir((0, path_1.join)(this.outputDir, exports.DEFAULT_LAMBDA_CODE_DIR)),
fs_extra_1.default.emptyDir((0, path_1.join)(this.outputDir, exports.API_LAMBDA_CODE_DIR)),
fs_extra_1.default.emptyDir((0, path_1.join)(this.outputDir, exports.IMAGE_LAMBDA_CODE_DIR)),
fs_extra_1.default.emptyDir((0, path_1.join)(this.outputDir, exports.REGENERATION_LAMBDA_CODE_DIR)),
fs_extra_1.default.emptyDir((0, path_1.join)(this.outputDir, exports.ASSETS_DIR))
]);
const { restoreUserConfig } = await (0, createServerlessConfig_1.default)(cwd, path_2.default.join(this.nextConfigDir), useServerlessTraceTarget);
try {
const subprocess = (0, execa_1.default)(cmd, args, {
cwd,
env
});
if (debugMode) {
subprocess.stdout.pipe(process.stdout);
subprocess.stderr.pipe(process.stderr);
}
await subprocess;
}
finally {
await restoreUserConfig();
}
const routesManifest = require((0, path_1.join)(this.dotNextDir, "routes-manifest.json"));
const prerenderManifest = require((0, path_1.join)(this.dotNextDir, "prerender-manifest.json"));
const options = {
buildId: await fs_extra_1.default.readFile(path_2.default.join(this.dotNextDir, "BUILD_ID"), "utf-8"),
...this.buildOptions,
domainRedirects: (_a = this.buildOptions.domainRedirects) !== null && _a !== void 0 ? _a : {}
};
const { apiManifest, imageManifest, pageManifest } = await (0, nextjs_core_1.prepareBuildManifests)(options, await this.readNextConfig(), routesManifest, await this.readPagesManifest(), prerenderManifest, await this.readPublicFiles(assetIgnorePatterns));
const { enableHTTPCompression, logLambdaExecutionTimes, regenerationQueueName } = this.buildOptions;
const apiBuildManifest = {
...apiManifest,
enableHTTPCompression
};
const defaultBuildManifest = {
...pageManifest,
enableHTTPCompression,
logLambdaExecutionTimes,
regenerationQueueName
};
const imageBuildManifest = {
...imageManifest,
enableHTTPCompression
};
await this.buildDefaultLambda(defaultBuildManifest);
await this.buildRegenerationHandler(defaultBuildManifest);
const hasAPIPages = Object.keys(apiBuildManifest.apis.nonDynamic).length > 0 ||
Object.keys(apiBuildManifest.apis.dynamic).length > 0;
if (hasAPIPages) {
await this.buildApiLambda(apiBuildManifest);
}
const hasImagesManifest = fs_extra_1.default.existsSync((0, path_1.join)(this.dotNextDir, "images-manifest.json"));
const imagesManifest = hasImagesManifest
? await fs_extra_1.default.readJSON((0, path_1.join)(this.dotNextDir, "images-manifest.json"))
: null;
const imageLoader = (_b = imagesManifest === null || imagesManifest === void 0 ? void 0 : imagesManifest.images) === null || _b === void 0 ? void 0 : _b.loader;
const isDefaultLoader = !imageLoader || imageLoader === "default";
const hasImageOptimizer = hasImagesManifest && isDefaultLoader;
const exportMarker = fs_extra_1.default.existsSync((0, path_1.join)(this.dotNextDir, "export-marker.json"))
? await fs_extra_1.default.readJSON(path_2.default.join(this.dotNextDir, "export-marker.json"))
: {};
const isNextImageImported = exportMarker.isNextImageImported !== false;
if (hasImageOptimizer && isNextImageImported) {
await this.buildImageLambda(imageBuildManifest);
}
await this.buildStaticAssets(defaultBuildManifest, routesManifest, assetIgnorePatterns);
}
async runThirdPartyIntegrations(defaultLambdaDir, regenerationLambdaDir) {
await Promise.all([
new next_i18next_1.NextI18nextIntegration(this.nextConfigDir, defaultLambdaDir).execute(),
new next_i18next_1.NextI18nextIntegration(this.nextConfigDir, regenerationLambdaDir).execute()
]);
}
}
exports.default = Builder;