From 6ad886145bd35298accf04d43bd6ef69833567e2 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Tue, 19 Mar 2024 11:34:14 +1100 Subject: [PATCH] Migrate Vite build test to Vite test helpers (#9076) --- integration/vite-build-test.ts | 712 ++++++++++++++++----------------- 1 file changed, 345 insertions(+), 367 deletions(-) diff --git a/integration/vite-build-test.ts b/integration/vite-build-test.ts index 3fec1a9b55b..dd6c7681b63 100644 --- a/integration/vite-build-test.ts +++ b/integration/vite-build-test.ts @@ -1,402 +1,380 @@ import * as path from "node:path"; import { test, expect } from "@playwright/test"; -import shell from "shelljs"; +import getPort from "get-port"; import glob from "glob"; import { - createAppFixture, - createFixture, - js, -} from "./helpers/create-fixture.js"; -import type { Fixture, AppFixture } from "./helpers/create-fixture.js"; -import { PlaywrightFixture, selectHtml } from "./helpers/playwright-fixture.js"; - -test.describe("Vite build", () => { - let fixture: Fixture; - let appFixture: AppFixture; - - test.beforeAll(async () => { - fixture = await createFixture({ - compiler: "vite", - files: { - "remix.config.js": js` - throw new Error("Remix should not access remix.config.js when using Vite"); - export default {}; - `, - ".env": ` - ENV_VAR_FROM_DOTENV_FILE=true - `, - "vite.config.ts": js` - import { defineConfig } from "vite"; - import { vitePlugin as remix } from "@remix-run/dev"; - import mdx from "@mdx-js/rollup"; - - export default defineConfig({ - build: { - // force emitting asset files instead of inlined as data-url - assetsInlineLimit: 0, - }, - plugins: [ - mdx(), - remix(), - ], - }); - `, - "app/root.tsx": js` - import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; - - export default function Root() { - return ( - - - - - - -
-

Root

- -
- - - - ); - } - `, - "app/routes/_index.tsx": js` - import { useState, useEffect } from "react"; - import { json } from "@remix-run/node"; - - import { serverOnly1, serverOnly2 } from "../utils.server"; - - export const loader = () => { - return json({ serverOnly1 }) - } - - export const action = () => { - console.log(serverOnly2) - return null - } - - export default function() { - const [mounted, setMounted] = useState(false); - useEffect(() => { - setMounted(true); - }, []); - - return ( - <> -

Index

- {!mounted ?

Loading...

:

Mounted

} - - ); - } - `, - "app/utils.server.ts": js` - export const serverOnly1 = "SERVER_ONLY_1" - export const serverOnly2 = "SERVER_ONLY_2" - `, - "app/routes/resource.ts": js` - import { json } from "@remix-run/node"; - - import { serverOnly1, serverOnly2 } from "../utils.server"; - - export const loader = () => { - return json({ serverOnly1 }) - } - - export const action = () => { - console.log(serverOnly2) - return null - } - `, - "app/routes/mdx.mdx": js` - import { useEffect, useState } from "react"; - import { json } from "@remix-run/node"; - import { useLoaderData } from "@remix-run/react"; - - import { serverOnly1, serverOnly2 } from "../utils.server"; - - export const loader = () => { - return json({ - serverOnly1, - content: "MDX route content from loader", - }) - } - - export const action = () => { - console.log(serverOnly2) - return null - } - - export function MdxComponent() { - const [mounted, setMounted] = useState(false); - useEffect(() => { - setMounted(true); - }, []); - const { content } = useLoaderData(); - const text = content + (mounted - ? ": mounted" - : ": not mounted"); - return
{text}
- } - - ## MDX Route - - - `, - "app/routes/code-split1.tsx": js` - import { CodeSplitComponent } from "../code-split-component"; - - export default function CodeSplit1Route() { - return
; - } - `, - "app/routes/code-split2.tsx": js` - import { CodeSplitComponent } from "../code-split-component"; - - export default function CodeSplit2Route() { - return
; - } - `, - "app/code-split-component.tsx": js` - import classes from "./code-split.module.css"; - - export function CodeSplitComponent() { - return ok - } - `, - "app/code-split.module.css": js` - .test { - background-color: rgb(255, 170, 0); - } - `, - "app/routes/dotenv.tsx": js` - import { json } from "@remix-run/node"; - import { useLoaderData } from "@remix-run/react"; - - export const loader = () => { - return json({ - loaderContent: process.env.ENV_VAR_FROM_DOTENV_FILE ?? '.env file was NOT loaded, which is a good thing', - }) - } - - export default function DotenvRoute() { - const { loaderContent } = useLoaderData(); - - return
{loaderContent}
; - } - `, - - "app/assets/test.txt": "test", - "app/routes/ssr-only-assets.tsx": js` - import txtUrl from "../assets/test.txt?url"; - import { useLoaderData } from "@remix-run/react" - - export const loader: LoaderFunction = () => { - return { txtUrl }; - }; - - export default function SsrOnlyAssetsRoute() { - const loaderData = useLoaderData(); - return ( -
- txtUrl + createProject, + viteBuild, + viteRemixServe, + viteConfig, + grep, +} from "./helpers/vite.js"; + +let port: number; +let cwd: string; +let stop: () => void; + +const js = String.raw; + +test.beforeAll(async () => { + port = await getPort(); + cwd = await createProject({ + "remix.config.js": js` + throw new Error("Remix should not access remix.config.js when using Vite"); + export default {}; + `, + ".env": ` + ENV_VAR_FROM_DOTENV_FILE=true + `, + "vite.config.ts": js` + import { defineConfig } from "vite"; + import { vitePlugin as remix } from "@remix-run/dev"; + import mdx from "@mdx-js/rollup"; + + export default defineConfig({ + ${await viteConfig.server({ port })} + build: { + // force emitting asset files instead of inlined as data-url + assetsInlineLimit: 0, + }, + plugins: [ + mdx(), + remix(), + ], + }); + `, + "app/root.tsx": js` + import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; + + export default function Root() { + return ( + + + + + + +
+

Root

+
- ); - } - `, - - "app/assets/test.css": ".test{color:red}", - "app/routes/ssr-only-css-url-files.tsx": js` - import cssUrl from "../assets/test.css?url"; - import { useLoaderData } from "@remix-run/react" - - export const loader: LoaderFunction = () => { - return { cssUrl }; - }; - - export default function SsrOnlyCssUrlFilesRoute() { - const loaderData = useLoaderData(); - return ( -
- cssUrl -
- ); - } - `, - - "app/routes/ssr-code-split.tsx": js` - import { useLoaderData } from "@remix-run/react" - - export const loader: LoaderFunction = async () => { - const lib = await import("../ssr-code-split-lib"); - return lib.ssrCodeSplitTest(); - }; - - export default function SsrCodeSplitRoute() { - const loaderData = useLoaderData(); - return ( -
{loaderData}
- ); - } - `, - - "app/ssr-code-split-lib.ts": js` - export function ssrCodeSplitTest() { - return "ssrCodeSplitTest"; - } - `, - }, - }); - - appFixture = await createAppFixture(fixture); - }); - - test.afterAll(() => { - appFixture.close(); + + + + ); + } + `, + "app/routes/_index.tsx": js` + import { useState, useEffect } from "react"; + import { json } from "@remix-run/node"; + + import { serverOnly1, serverOnly2 } from "../utils.server"; + + export const loader = () => { + return json({ serverOnly1 }) + } + + export const action = () => { + console.log(serverOnly2) + return null + } + + export default function() { + const [mounted, setMounted] = useState(false); + useEffect(() => { + setMounted(true); + }, []); + + return ( + <> +

Index

+ {!mounted ?

Loading...

:

Mounted

} + + ); + } + `, + "app/utils.server.ts": js` + export const serverOnly1 = "SERVER_ONLY_1" + export const serverOnly2 = "SERVER_ONLY_2" + `, + "app/routes/resource.ts": js` + import { json } from "@remix-run/node"; + + import { serverOnly1, serverOnly2 } from "../utils.server"; + + export const loader = () => { + return json({ serverOnly1 }) + } + + export const action = () => { + console.log(serverOnly2) + return null + } + `, + "app/routes/mdx.mdx": js` + import { useEffect, useState } from "react"; + import { json } from "@remix-run/node"; + import { useLoaderData } from "@remix-run/react"; + + import { serverOnly1, serverOnly2 } from "../utils.server"; + + export const loader = () => { + return json({ + serverOnly1, + content: "MDX route content from loader", + }) + } + + export const action = () => { + console.log(serverOnly2) + return null + } + + export function MdxComponent() { + const [mounted, setMounted] = useState(false); + useEffect(() => { + setMounted(true); + }, []); + const { content } = useLoaderData(); + const text = content + (mounted + ? ": mounted" + : ": not mounted"); + return
{text}
+ } + + ## MDX Route + + + `, + "app/routes/code-split1.tsx": js` + import { CodeSplitComponent } from "../code-split-component"; + + export default function CodeSplit1Route() { + return
; + } + `, + "app/routes/code-split2.tsx": js` + import { CodeSplitComponent } from "../code-split-component"; + + export default function CodeSplit2Route() { + return
; + } + `, + "app/code-split-component.tsx": js` + import classes from "./code-split.module.css"; + + export function CodeSplitComponent() { + return ok + } + `, + "app/code-split.module.css": js` + .test { + background-color: rgb(255, 170, 0); + } + `, + "app/routes/dotenv.tsx": js` + import { json } from "@remix-run/node"; + import { useLoaderData } from "@remix-run/react"; + + export const loader = () => { + return json({ + loaderContent: process.env.ENV_VAR_FROM_DOTENV_FILE ?? '.env file was NOT loaded, which is a good thing', + }) + } + + export default function DotenvRoute() { + const { loaderContent } = useLoaderData(); + + return
{loaderContent}
; + } + `, + + "app/assets/test.txt": "test", + "app/routes/ssr-only-assets.tsx": js` + import txtUrl from "../assets/test.txt?url"; + import { useLoaderData } from "@remix-run/react" + + export const loader: LoaderFunction = () => { + return { txtUrl }; + }; + + export default function SsrOnlyAssetsRoute() { + const loaderData = useLoaderData(); + return ( +
+ txtUrl +
+ ); + } + `, + + "app/assets/test.css": ".test{color:red}", + "app/routes/ssr-only-css-url-files.tsx": js` + import cssUrl from "../assets/test.css?url"; + import { useLoaderData } from "@remix-run/react" + + export const loader: LoaderFunction = () => { + return { cssUrl }; + }; + + export default function SsrOnlyCssUrlFilesRoute() { + const loaderData = useLoaderData(); + return ( +
+ cssUrl +
+ ); + } + `, + + "app/routes/ssr-code-split.tsx": js` + import { useLoaderData } from "@remix-run/react" + + export const loader: LoaderFunction = async () => { + const lib = await import("../ssr-code-split-lib"); + return lib.ssrCodeSplitTest(); + }; + + export default function SsrCodeSplitRoute() { + const loaderData = useLoaderData(); + return ( +
{loaderData}
+ ); + } + `, + + "app/ssr-code-split-lib.ts": js` + export function ssrCodeSplitTest() { + return "ssrCodeSplitTest"; + } + `, }); - test("server code is removed from client build", async () => { - let clientBuildDir = path.join(fixture.projectDir, "build/client"); - - // detect client asset files - let assetFiles = glob.sync("**/*.@(js|jsx|ts|tsx)", { - cwd: clientBuildDir, - absolute: true, - }); + let { stderr, status } = viteBuild({ cwd }); + expect( + stderr + .toString() + // This can be removed when this issue is fixed: https://github.com/remix-run/remix/issues/9055 + .replace('Generated an empty chunk: "resource".', "") + .trim() + ).toBeFalsy(); + expect(status).toBe(0); + stop = await viteRemixServe({ cwd, port }); +}); +test.afterAll(() => stop()); - // grep for server-only values in client assets - let result = shell - .grep("-l", /SERVER_ONLY_1|SERVER_ONLY_2/, assetFiles) - .stdout.trim() - .split("\n") - .filter((line) => line.length > 0); +test("Vite / build / server code is removed from client build", async () => { + expect(grep(path.join(cwd, "build/client"), /SERVER_ONLY_1/).length).toBe(0); + expect(grep(path.join(cwd, "build/client"), /SERVER_ONLY_2/).length).toBe(0); +}); - expect(result).toHaveLength(0); - }); +test("Vite / build / renders matching MDX routes", async ({ page }) => { + let pageErrors: Error[] = []; + page.on("pageerror", (error) => pageErrors.push(error)); - test("server renders matching routes", async () => { - let res = await fixture.requestDocument("/"); - expect(res.status).toBe(200); - expect(selectHtml(await res.text(), "#content")).toBe(`
-

Root

-

Index

-

Loading...

-
`); + await page.goto(`http://localhost:${port}/mdx`, { + waitUntil: "networkidle", }); + await expect(page.locator("[data-mdx-route]")).toHaveText( + "MDX route content from loader: mounted" + ); + expect(pageErrors).toEqual([]); +}); - test("hydrates", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - expect(await page.locator("#content h2").textContent()).toBe("Index"); - expect(await page.locator("#content h3[data-mounted]").textContent()).toBe( - "Mounted" - ); - }); +test("Vite / build / emits SSR-only assets to the client assets directory", async ({ + page, +}) => { + let pageErrors: Error[] = []; + page.on("pageerror", (error) => pageErrors.push(error)); - test("server renders matching MDX routes", async ({ page }) => { - let res = await fixture.requestDocument("/mdx"); - expect(res.status).toBe(200); - expect(selectHtml(await res.text(), "[data-mdx-route]")).toBe( - `
MDX route content from loader: not mounted
` - ); + await page.goto(`http://localhost:${port}/ssr-only-assets`, { + waitUntil: "networkidle", }); - test("hydrates matching MDX routes", async ({ page }) => { - let pageErrors: unknown[] = []; - page.on("pageerror", (error) => pageErrors.push(error)); + await page.getByRole("link", { name: "txtUrl" }).click(); + await page.waitForURL("**/assets/test-*.txt"); + await expect(page.getByText("test")).toBeVisible(); + expect(pageErrors).toEqual([]); +}); - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/mdx"); - await expect(page.locator("[data-mdx-route]")).toContainText( - "MDX route content from loader: mounted" - ); +test("Vite / build /emits SSR-only .css?url files to the client assets directory", async ({ + page, +}) => { + let pageErrors: Error[] = []; + page.on("pageerror", (error) => pageErrors.push(error)); - expect(pageErrors).toEqual([]); + await page.goto(`http://localhost:${port}/ssr-only-css-url-files`, { + waitUntil: "networkidle", }); - test("emits SSR-only assets to the client assets directory", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/ssr-only-assets"); - - await page.getByRole("link", { name: "txtUrl" }).click(); - await page.waitForURL("**/assets/test-*.txt"); - await expect(page.getByText("test")).toBeVisible(); - }); + await page.getByRole("link", { name: "cssUrl" }).click(); + await page.waitForURL("**/assets/test-*.css"); + await expect(page.getByText(".test{")).toBeVisible(); + expect(pageErrors).toEqual([]); +}); - test("emits SSR-only .css?url files to the client assets directory", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/ssr-only-css-url-files"); +test("Vite / build / supports code-split JS from SSR build", async ({ + page, +}) => { + let pageErrors: Error[] = []; + page.on("pageerror", (error) => pageErrors.push(error)); - await page.getByRole("link", { name: "cssUrl" }).click(); - await page.waitForURL("**/assets/test-*.css"); - await expect(page.getByText(".test{")).toBeVisible(); + await page.goto(`http://localhost:${port}/ssr-code-split`, { + waitUntil: "networkidle", }); - test("supports code-split JS from SSR build", async ({ page }) => { - let pageErrors: unknown[] = []; - page.on("pageerror", (error) => pageErrors.push(error)); + await expect(page.locator("[data-ssr-code-split]")).toHaveText( + "ssrCodeSplitTest" + ); + expect(pageErrors).toEqual([]); +}); - let app = new PlaywrightFixture(appFixture, page); - await app.goto(`/ssr-code-split`); - expect(pageErrors).toEqual([]); +test("Vite / build / removes assets (other than code-split JS) and CSS files from SSR build", async () => { + let assetFiles = glob.sync("build/server/assets/**/*", { cwd }); + let [asset, ...rest] = assetFiles; + expect(rest).toEqual([]); // Provide more useful test output if this fails + expect(asset).toMatch(/ssr-code-split-lib-.*\.js/); +}); - await expect(page.locator("[data-ssr-code-split]")).toHaveText( - "ssrCodeSplitTest" - ); +test("Vite / build / supports code-split CSS", async ({ page }) => { + let pageErrors: unknown[] = []; + page.on("pageerror", (error) => pageErrors.push(error)); - expect(pageErrors).toEqual([]); + await page.goto(`http://localhost:${port}/code-split1`, { + waitUntil: "networkidle", }); - - test("removes assets (other than code-split JS) and CSS files from SSR build", async () => { - let assetFiles = glob.sync("*", { - cwd: path.join(fixture.projectDir, "build/server/assets"), - }); - let [asset, ...rest] = assetFiles; - expect(rest).toEqual([]); // Provide more useful test output if this fails - expect(asset).toMatch(/ssr-code-split-lib-.*\.js/); + expect( + await page + .locator("#code-split1 span") + .evaluate((e) => window.getComputedStyle(e).backgroundColor) + ).toBe("rgb(255, 170, 0)"); + + await page.goto(`http://localhost:${port}/code-split2`, { + waitUntil: "networkidle", }); + expect( + await page + .locator("#code-split2 span") + .evaluate((e) => window.getComputedStyle(e).backgroundColor) + ).toBe("rgb(255, 170, 0)"); - test("supports code-split css", async ({ page }) => { - let pageErrors: unknown[] = []; - page.on("pageerror", (error) => pageErrors.push(error)); - - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/code-split1"); - expect( - await page - .locator("#code-split1 span") - .evaluate((e) => window.getComputedStyle(e).backgroundColor) - ).toBe("rgb(255, 170, 0)"); - - await app.goto("/code-split2"); - expect( - await page - .locator("#code-split2 span") - .evaluate((e) => window.getComputedStyle(e).backgroundColor) - ).toBe("rgb(255, 170, 0)"); - - expect(pageErrors).toEqual([]); - }); + expect(pageErrors).toEqual([]); +}); - test("doesn't load .env file", async ({ page }) => { - let pageErrors: unknown[] = []; - page.on("pageerror", (error) => pageErrors.push(error)); +test("Vite / build / doesn't load .env file", async ({ page }) => { + let pageErrors: unknown[] = []; + page.on("pageerror", (error) => pageErrors.push(error)); - let app = new PlaywrightFixture(appFixture, page); - await app.goto(`/dotenv`); - expect(pageErrors).toEqual([]); + await page.goto(`http://localhost:${port}/dotenv`, { + waitUntil: "networkidle", + }); + expect(pageErrors).toEqual([]); - let loaderContent = page.locator("[data-dotenv-route-loader-content]"); - await expect(loaderContent).toHaveText( - ".env file was NOT loaded, which is a good thing" - ); + let loaderContent = page.locator("[data-dotenv-route-loader-content]"); + await expect(loaderContent).toHaveText( + ".env file was NOT loaded, which is a good thing" + ); - expect(pageErrors).toEqual([]); - }); + expect(pageErrors).toEqual([]); });