import { join } from "path"; import fse from "fs-extra"; import execa from "execa"; import Builder, { ASSETS_DIR } from "../../src/build"; import { DEFAULT_LAMBDA_CODE_DIR, API_LAMBDA_CODE_DIR } from "../../src/build"; import { cleanupDir, removeNewLineChars } from "../test-utils"; import { OriginRequestDefaultHandlerManifest } from "../../src/types"; jest.mock("execa"); describe("Builder Tests (with locales)", () => { let fseRemoveSpy: jest.SpyInstance; let fseEmptyDirSpy: jest.SpyInstance; let defaultBuildManifest: OriginRequestDefaultHandlerManifest; const fixturePath = join(__dirname, "./simple-app-fixture-with-locales"); const outputDir = join(fixturePath, ".test_sls_next_output"); beforeEach(async () => { const mockExeca = execa as jest.Mock; mockExeca.mockResolvedValueOnce(); fseRemoveSpy = jest.spyOn(fse, "remove").mockImplementation(() => { return; }); fseEmptyDirSpy = jest.spyOn(fse, "emptyDir"); const builder = new Builder(fixturePath, outputDir); await builder.build(); defaultBuildManifest = await fse.readJSON( join(outputDir, `${DEFAULT_LAMBDA_CODE_DIR}/manifest.json`) ); }); afterEach(() => { fseEmptyDirSpy.mockRestore(); fseRemoveSpy.mockRestore(); return cleanupDir(outputDir); }); describe("Cleanup", () => { it(".next directory is emptied except for cache/ folder", () => { expect(fseRemoveSpy).toBeCalledWith( join(fixturePath, ".next/serverless") ); expect(fseRemoveSpy).toBeCalledWith( join(fixturePath, ".next/prerender-manifest.json") ); expect(fseRemoveSpy).not.toBeCalledWith(join(fixturePath, ".next/cache")); }); it("output directory is cleanup before building", () => { expect(fseEmptyDirSpy).toBeCalledWith( expect.stringContaining(join(".test_sls_next_output", "default-lambda")) ); expect(fseEmptyDirSpy).toBeCalledWith( expect.stringContaining(join(".test_sls_next_output", "api-lambda")) ); expect(fseEmptyDirSpy).toBeCalledWith( expect.stringContaining(join(".test_sls_next_output", "assets")) ); }); }); describe("Default Handler Manifest", () => { it("adds full manifest", () => { const { buildId, publicFiles, pages: { dynamic, ssr, ssg, html }, trailingSlash } = defaultBuildManifest; expect(removeNewLineChars(buildId)).toEqual("test-build-id"); expect(dynamic).toEqual([ { regex: "^\\/customers(?:\\/([^\\/#\\?]+?))[\\/#\\?]?$", route: "/customers/[customer]" }, { regex: "^\\/customers(?:\\/([^\\/#\\?]+?))\\/profile[\\/#\\?]?$", route: "/customers/[customer]/profile" }, { regex: "^\\/customers(?:\\/([^\\/#\\?]+?))(?:\\/([^\\/#\\?]+?))[\\/#\\?]?$", route: "/customers/[customer]/[post]" }, { regex: "^\\/customers(?:\\/((?:[^\\/#\\?]+?)(?:\\/(?:[^\\/#\\?]+?))*))?[\\/#\\?]?$", route: "/customers/[...catchAll]" }, { regex: "^\\/en\\/blog(?:\\/([^\\/#\\?]+?))[\\/#\\?]?$", route: "/en/blog/[post]" }, { regex: "^\\/en\\/customers(?:\\/([^\\/#\\?]+?))[\\/#\\?]?$", route: "/en/customers/[customer]" }, { regex: "^\\/en\\/customers(?:\\/([^\\/#\\?]+?))\\/profile[\\/#\\?]?$", route: "/en/customers/[customer]/profile" }, { regex: "^\\/en\\/customers(?:\\/([^\\/#\\?]+?))(?:\\/([^\\/#\\?]+?))[\\/#\\?]?$", route: "/en/customers/[customer]/[post]" }, { regex: "^\\/en\\/customers(?:\\/((?:[^\\/#\\?]+?)(?:\\/(?:[^\\/#\\?]+?))*))?[\\/#\\?]?$", route: "/en/customers/[...catchAll]" }, { regex: "^\\/en(?:\\/([^\\/#\\?]+?))[\\/#\\?]?$", route: "/en/[root]" }, { regex: "^\\/nl\\/blog(?:\\/([^\\/#\\?]+?))[\\/#\\?]?$", route: "/nl/blog/[post]" }, { regex: "^\\/nl\\/customers(?:\\/([^\\/#\\?]+?))[\\/#\\?]?$", route: "/nl/customers/[customer]" }, { regex: "^\\/nl\\/customers(?:\\/([^\\/#\\?]+?))\\/profile[\\/#\\?]?$", route: "/nl/customers/[customer]/profile" }, { regex: "^\\/nl\\/customers(?:\\/([^\\/#\\?]+?))(?:\\/([^\\/#\\?]+?))[\\/#\\?]?$", route: "/nl/customers/[customer]/[post]" }, { regex: "^\\/nl\\/customers(?:\\/((?:[^\\/#\\?]+?)(?:\\/(?:[^\\/#\\?]+?))*))?[\\/#\\?]?$", route: "/nl/customers/[...catchAll]" }, { regex: "^\\/nl(?:\\/([^\\/#\\?]+?))[\\/#\\?]?$", route: "/nl/[root]" }, { regex: "^(?:\\/([^\\/#\\?]+?))[\\/#\\?]?$", route: "/[root]" } ]); expect(ssr).toEqual({ dynamic: { "/[root]": "pages/[root].js", "/customers/[...catchAll]": "pages/customers/[...catchAll].js", "/customers/[customer]": "pages/customers/[customer].js", "/customers/[customer]/[post]": "pages/customers/[customer]/[post].js", "/customers/[customer]/profile": "pages/customers/[customer]/profile.js", "/en/[root]": "pages/[root].js", "/en/customers/[...catchAll]": "pages/customers/[...catchAll].js", "/en/customers/[customer]": "pages/customers/[customer].js", "/en/customers/[customer]/[post]": "pages/customers/[customer]/[post].js", "/en/customers/[customer]/profile": "pages/customers/[customer]/profile.js", "/nl/[root]": "pages/[root].js", "/nl/customers/[...catchAll]": "pages/customers/[...catchAll].js", "/nl/customers/[customer]": "pages/customers/[customer].js", "/nl/customers/[customer]/[post]": "pages/customers/[customer]/[post].js", "/nl/customers/[customer]/profile": "pages/customers/[customer]/profile.js" }, nonDynamic: { "/en/_app": "pages/_app.js", "/en/_document": "pages/_document.js", "/en/customers/new": "pages/customers/new.js", "/nl/_app": "pages/_app.js", "/nl/_document": "pages/_document.js", "/nl/customers/new": "pages/customers/new.js" } }); expect(html).toEqual({ nonDynamic: { "/en/404": "pages/en/404.html", "/en/terms": "pages/en/terms.html", "/en/about": "pages/en/about.html", "/nl/404": "pages/nl/404.html", "/nl/terms": "pages/nl/terms.html", "/nl/about": "pages/nl/about.html" }, dynamic: { "/en/blog/[post]": "pages/en/blog/[post].html", "/nl/blog/[post]": "pages/nl/blog/[post].html" } }); expect(ssg).toEqual({ nonDynamic: { "/en": { initialRevalidateSeconds: false, srcRoute: null }, "/en/contact": { initialRevalidateSeconds: false, srcRoute: null }, "/nl": { initialRevalidateSeconds: false, srcRoute: null }, "/nl/contact": { initialRevalidateSeconds: false, srcRoute: null } }, dynamic: {}, notFound: {} }); expect(publicFiles).toEqual({ "/favicon.ico": "favicon.ico", "/sub/image.png": "sub/image.png", "/sw.js": "sw.js" }); expect(trailingSlash).toBe(false); }); }); describe("API Handler", () => { it("has empty API handler directory", async () => { expect.assertions(1); const apiDir = await fse.readdir( join(outputDir, `${API_LAMBDA_CODE_DIR}`) ); expect(apiDir).toEqual([]); }); }); describe("Default Handler", () => { it("copies build files", async () => { expect.assertions(7); const files = await fse.readdir( join(outputDir, `${DEFAULT_LAMBDA_CODE_DIR}`) ); const pages = await fse.readdir( join(outputDir, `${DEFAULT_LAMBDA_CODE_DIR}/pages`) ); const customerPages = await fse.readdir( join(outputDir, `${DEFAULT_LAMBDA_CODE_DIR}/pages/customers`) ); const apiDirExists = await fse.pathExists( join(outputDir, `${DEFAULT_LAMBDA_CODE_DIR}/pages/api`) ); expect(files).toEqual( expect.arrayContaining([ "index.js", // there are more chunks but it should at least contain the entry point "manifest.json", "pages", "prerender-manifest.json", "routes-manifest.json" ]) ); // api pages should not be included in the default lambda expect(apiDirExists).toEqual(false); // HTML Prerendered pages or JSON static props files // should not be included in the default lambda expect(pages).not.toContain([ "blog.json", "nl.json", "contact.json", "en.json" ]); expect(pages).not.toContain([ "about.html", "terms.html", "contact.html", "en.html", "nl.html" ]); // JS files used only for prerendering at build time (contact.js, index.js) are not included since there are no API routes expect(pages).not.toContain(["contact.js", "index.js"]); // Default lambda has locale directories "en", "nl" but they are empty for now expect(pages).toEqual(["_error.js", "customers", "en", "nl"]); expect(customerPages).toEqual(["[...catchAll].js"]); }); }); describe("Assets", () => { it("copies locale-specific asset files", async () => { expect.assertions(9); // Root const nextDataFiles = await fse.readdir( join(outputDir, `${ASSETS_DIR}/_next/data/test-build-id`) ); expect(nextDataFiles).toEqual(["en", "en.json", "nl", "nl.json"]); // English const enNextDataFiles = await fse.readdir( join(outputDir, `${ASSETS_DIR}/_next/data/test-build-id/en`) ); expect(enNextDataFiles).toEqual(["contact.json"]); const enContactJson = await fse.readFile( join( outputDir, `${ASSETS_DIR}/_next/data/test-build-id/en/contact.json` ), "utf8" ); expect(enContactJson).toBe('"en"'); const enPageFiles = await fse.readdir( join(outputDir, `${ASSETS_DIR}/static-pages/test-build-id/en`) ); expect(enPageFiles).toEqual(["about.html", "contact.html", "terms.html"]); const enIndexHtml = await fse.readFile( join(outputDir, `${ASSETS_DIR}/static-pages/test-build-id/en.html`), "utf8" ); expect(enIndexHtml).toBe("en"); // Dutch const nlNextDataFiles = await fse.readdir( join(outputDir, `${ASSETS_DIR}/_next/data/test-build-id/nl`) ); expect(nlNextDataFiles).toEqual(["contact.json"]); const nlContactJson = await fse.readFile( join( outputDir, `${ASSETS_DIR}/_next/data/test-build-id/nl/contact.json` ), "utf8" ); expect(nlContactJson).toBe('"nl"'); const nlPageFiles = await fse.readdir( join(outputDir, `${ASSETS_DIR}/static-pages/test-build-id/nl`) ); expect(nlPageFiles).toEqual(["about.html", "contact.html", "terms.html"]); const nlIndexHtml = await fse.readFile( join( outputDir, `${ASSETS_DIR}/static-pages/test-build-id/nl/contact.html` ), "utf8" ); expect(nlIndexHtml).toBe("nl"); }); }); });