UNPKG

@sls-next/core

Version:
307 lines (306 loc) 15.6 kB
"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 = void 0; const fs_extra_1 = __importDefault(require("fs-extra")); const path_1 = require("path"); const path_2 = __importDefault(require("path")); const redirector_1 = require("./lib/redirector"); const readDirectoryFiles_1 = __importDefault(require("./lib/readDirectoryFiles")); const filterOutDirectories_1 = __importDefault(require("./lib/filterOutDirectories")); const normalize_path_1 = __importDefault(require("normalize-path")); const createServerlessConfig_1 = __importDefault(require("./lib/createServerlessConfig")); const execa_1 = __importDefault(require("execa")); const _1 = require("./"); const pathToPosix_1 = __importDefault(require("./lib/pathToPosix")); exports.ASSETS_DIR = "assets"; const defaultBuildOptions = { nextConfigDir: "./", nextStaticDir: undefined, outputDir: ".serverless_nextjs", args: ["build"], cwd: process.cwd(), env: {}, cmd: "./node_modules/.bin/next", domainRedirects: {}, minifyHandlers: false, handler: undefined, authentication: undefined, baseDir: process.cwd(), cleanupDotNext: true, assetIgnorePatterns: [] }; /** * Core builder class that has common build functions for all platforms. */ class CoreBuilder { constructor(buildOptions) { var _a; this.buildOptions = defaultBuildOptions; if (buildOptions) { Object.assign(this.buildOptions, buildOptions); } this.nextConfigDir = path_2.default.resolve(this.buildOptions.nextConfigDir); this.nextStaticDir = path_2.default.resolve((_a = this.buildOptions.nextStaticDir) !== null && _a !== void 0 ? _a : this.buildOptions.nextConfigDir); this.dotNextDir = path_2.default.join(this.nextConfigDir, ".next"); this.serverlessDir = path_2.default.join(this.dotNextDir, "serverless"); this.outputDir = this.buildOptions.outputDir; } async build(debugMode) { await this.preBuild(); const { imageManifest, pageManifest } = await this.buildCore(debugMode); await this.buildPlatform({ imageManifest, pageManifest }, debugMode); } /** * Run prebuild steps which include cleaning up .next and emptying output directories. */ async preBuild() { await Promise.all([ this.cleanupDotNext(this.buildOptions.cleanupDotNext), fs_extra_1.default.emptyDir((0, path_1.join)(this.outputDir)) ]); } /** * Core build steps. Currently this runs the .next build and packages the assets since they are the same for all platforms. * @param debugMode */ async buildCore(debugMode) { var _a; const { cmd, args, cwd, env, assetIgnorePatterns } = Object.assign(defaultBuildOptions, this.buildOptions); const { restoreUserConfig } = await (0, createServerlessConfig_1.default)(cwd, path_2.default.join(this.nextConfigDir), false); try { const subprocess = (0, execa_1.default)(cmd, args, { cwd, env }); if (debugMode) { // @ts-ignore subprocess.stdout.pipe(process.stdout); } await subprocess; } finally { await restoreUserConfig(); } // eslint-disable-next-line @typescript-eslint/no-var-requires const routesManifest = require((0, path_1.join)(this.dotNextDir, "routes-manifest.json")); // eslint-disable-next-line @typescript-eslint/no-var-requires 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"), useV2Handler: true, ...this.buildOptions, domainRedirects: (_a = this.buildOptions.domainRedirects) !== null && _a !== void 0 ? _a : {} }; const { imageManifest, pageManifest } = await (0, _1.prepareBuildManifests)(options, await this.readNextConfig(), routesManifest, await this.readPagesManifest(), prerenderManifest, await this.readPublicFiles(assetIgnorePatterns)); // Copy any static assets to .serverless_nextjs/assets directory // This step is common to all platforms so it's in the core build step. await this.buildStaticAssets(pageManifest, routesManifest, assetIgnorePatterns); return { imageManifest, pageManifest }; } 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)) // normalization to unix paths needed for AWS .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); } /** * Check whether this .next/serverless/pages file is a JS file used for runtime rendering. * @param pageManifest * @param relativePageFile */ isSSRJSFile(pageManifest, relativePageFile) { if (path_2.default.extname(relativePageFile) === ".js") { const page = relativePageFile.startsWith("/") ? `pages${relativePageFile}` : `pages/${relativePageFile}`; if (page === "pages/_error.js" || Object.values(pageManifest.pages.ssr.nonDynamic).includes(page) || Object.values(pageManifest.pages.ssr.dynamic).includes(page)) { return true; } } return false; } /** * Process and copy RoutesManifest. * @param source * @param destination */ async processAndCopyRoutesManifest(source, destination) { // eslint-disable-next-line @typescript-eslint/no-var-requires const routesManifest = require(source); // Remove default trailing slash redirects as they are already handled without regex matching. routesManifest.redirects = routesManifest.redirects.filter((redirect) => { return !(0, redirector_1.isTrailingSlashRedirect)(redirect, routesManifest.basePath); }); await fs_extra_1.default.writeFile(destination, JSON.stringify(routesManifest)); } /** * Get filter function for files to be included in the default handler. */ getDefaultHandlerFileFilter(hasAPIRoutes, pageManifest) { return (file) => { const isNotPrerenderedHTMLPage = path_2.default.extname(file) !== ".html"; const isNotStaticPropsJSONFile = path_2.default.extname(file) !== ".json"; // If there are API routes, include all JS files. // If there are no API routes, include only JS files that used for SSR (including fallback). // We do this because if there are API routes, preview mode is possible which may use these JS files. // This is what Vercel does: https://github.com/vercel/next.js/discussions/15631#discussioncomment-44289 // TODO: possibly optimize bundle further for those apps using API routes. const isNotExcludedJSFile = hasAPIRoutes || path_2.default.extname(file) !== ".js" || this.isSSRJSFile(pageManifest, (0, pathToPosix_1.default)(path_2.default.relative(path_2.default.join(this.serverlessDir, "pages"), file)) // important: make sure to use posix path to generate forward-slash path across both posix/windows ); return (isNotPrerenderedHTMLPage && isNotStaticPropsJSONFile && isNotExcludedJSFile); }; } /** * Copy code chunks generated by Next.js. */ async copyChunks(handlerDir) { return (await fs_extra_1.default.pathExists((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, handlerDir, "chunks")) : Promise.resolve(); } /** * Copy additional JS files needed such as webpack-runtime.js (new in Next.js 12) */ async copyJSFiles(handlerDir) { await Promise.all([ (await fs_extra_1.default.pathExists((0, path_1.join)(this.serverlessDir, "webpack-api-runtime.js"))) ? fs_extra_1.default.copy((0, path_1.join)(this.serverlessDir, "webpack-api-runtime.js"), (0, path_1.join)(this.outputDir, handlerDir, "webpack-api-runtime.js")) : Promise.resolve(), (await fs_extra_1.default.pathExists((0, path_1.join)(this.serverlessDir, "webpack-runtime.js"))) ? fs_extra_1.default.copy((0, path_1.join)(this.serverlessDir, "webpack-runtime.js"), (0, path_1.join)(this.outputDir, handlerDir, "webpack-runtime.js")) : Promise.resolve() ]); } 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") { // Execute using phase based on: https://github.com/vercel/next.js/blob/8a489e24bcb6141ad706e1527b77f3ff38940b6d/packages/next/next-server/lib/constants.ts#L1-L4 normalisedNextConfig = nextConfig("phase-production-server", {}); } return normalisedNextConfig; } } /** * Build static assets such as client-side JS, public files, static pages, etc. * Note that the upload to S3 is done in a separate deploy step. */ async buildStaticAssets(pageManifest, routesManifest, ignorePatterns) { const buildId = pageManifest.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); } }; // Copy BUILD_ID file 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(pageManifest.pages.html.dynamic), ...Object.keys(pageManifest.pages.html.nonDynamic) ]; const ssgPaths = Object.keys(pageManifest.pages.ssg.nonDynamic); const fallbackFiles = Object.values(pageManifest.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); }); 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 // static dir ]); } 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" // avoid deleting the cache folder as that leads to slow next builds! ) .map((fileItem) => fs_extra_1.default.remove((0, path_1.join)(this.dotNextDir, fileItem)))); } } } exports.default = CoreBuilder;