diff --git a/.changeset/chatty-shrimps-sell.md b/.changeset/chatty-shrimps-sell.md new file mode 100644 index 00000000000..4fb24c4d3f4 --- /dev/null +++ b/.changeset/chatty-shrimps-sell.md @@ -0,0 +1,6 @@ +--- +"integration-tests": minor +"@remix-run/dev": minor +--- + +Vite: exclude modules within `.server` directories from client build diff --git a/integration/helpers/vite-template/tsconfig.json b/integration/helpers/vite-template/tsconfig.json index 269c0cc0fce..ad5ae05598e 100644 --- a/integration/helpers/vite-template/tsconfig.json +++ b/integration/helpers/vite-template/tsconfig.json @@ -16,8 +16,6 @@ "paths": { "~/*": ["./app/*"] }, - - // Remix takes care of building everything in `remix build`. "noEmit": true } } diff --git a/integration/helpers/vite.ts b/integration/helpers/vite.ts index 926b4c4e257..8dced0b42b4 100644 --- a/integration/helpers/vite.ts +++ b/integration/helpers/vite.ts @@ -125,6 +125,7 @@ export const viteBuild = (args: { cwd: string }) => { [vite, "build"], [vite, "build", "--ssr"], ]; + let results = []; for (let command of commands) { let result = spawnSync("node", command, { cwd: args.cwd, @@ -132,11 +133,9 @@ export const viteBuild = (args: { cwd: string }) => { ...process.env, }, }); - if (result.error || result.status) { - console.error(result.stderr.toString("utf-8")); - throw result.error || new Error(`Build failed, check the output above`); - } + results.push(result); } + return results; }; export const viteDev = createDev([resolveBin.sync("vite"), "dev"]); export const customDev = createDev(["./server.mjs"]); diff --git a/integration/vite-dot-server-test.ts b/integration/vite-dot-server-test.ts index 86caa6b03ae..537e5ec245a 100644 --- a/integration/vite-dot-server-test.ts +++ b/integration/vite-dot-server-test.ts @@ -5,11 +5,6 @@ import glob from "glob"; import { createProject, viteBuild } from "./helpers/vite.js"; -// TODO: test that builds succeeds if server-only (.server file + dir) code is only used in actions/loaders/etc. -// TODO: test that build fails if there are .server named imports in client code -// TODO: test that dev succeeds if server-only (.server file + dir) code is only used in actions/loaders/etc. -// TODO: test that alias for .server works - let files = { "app/utils.server.ts": String.raw` export const dotServerFile = "SERVER_ONLY_FILE"; @@ -17,63 +12,94 @@ let files = { "app/.server/utils.ts": String.raw` export const dotServerDir = "SERVER_ONLY_DIR"; `, - "app/routes/_index.tsx": String.raw` - import { json } from "@remix-run/node"; - - import { dotServerFile } from "../utils.server"; - import { dotServerDir } from "../.server/utils"; - - export const loader = () => { - let serverOnly = dotServerFile + dotServerDir; - return json({ data: serverOnly }); - } +}; - export const action = () => { - let serverOnly = dotServerFile + dotServerDir; - console.log(serverOnly); - return null; - } +test("Vite / build / .server file in client fails with expected error", async () => { + let cwd = await createProject({ + ...files, + "app/routes/fail-server-file-in-client.tsx": String.raw` + import { dotServerFile } from "~/utils.server"; - export default function() { - let { data } = useLoaderData(); - return ( - <> -

Index

-

{data}

- - ); - } - `, -}; + export default function() { + console.log(dotServerFile); + return

Fail: Server file included in client

+ } + `, + }); + let client = viteBuild({ cwd })[0]; + let stderr = client.stderr.toString("utf8"); + expect(stderr).toMatch( + `"dotServerFile" is not exported by "app/utils.server.ts"` + ); +}); -test.describe(() => { - let cwd: string; +test("Vite / build / .server dir in client fails with expected error", async () => { + let cwd = await createProject({ + ...files, + "app/routes/fail-server-dir-in-client.tsx": String.raw` + import { dotServerDir } from "~/.server/utils"; - test.beforeAll(async () => { - cwd = await createProject(files); + export default function() { + console.log(dotServerDir); + return

Fail: Server directory included in client

+ } + `, }); + let client = viteBuild({ cwd })[0]; + let stderr = client.stderr.toString("utf8"); + expect(stderr).toMatch( + `"dotServerDir" is not exported by "app/.server/utils.ts"` + ); +}); + +test("Vite / build / dead-code elimination for server exports", async () => { + let cwd = await createProject({ + ...files, + "app/routes/remove-server-exports-and-dce.tsx": String.raw` + import fs from "node:fs"; + import { json } from "@remix-run/node"; + import { useLoaderData } from "@remix-run/react"; - test("Vite / .server / build", async () => { - viteBuild({ cwd }); - console.log({ cwd }); - let clientBuildDir = path.join(cwd, "build/client"); + import { dotServerFile } from "../utils.server"; + import { dotServerDir } from "../.server/utils"; - // detect client asset files - let assetFiles = glob.sync("**/*.@(js|jsx|ts|tsx)", { - cwd: clientBuildDir, - absolute: true, - }); + export const loader = () => { + let contents = fs.readFileSync("blah"); + let data = dotServerFile + dotServerDir + serverOnly + contents; + return json({ data }); + } - // grep for server-only values in client assets - let result = shell - .grep("-l", /SERVER_ONLY_FILE|SERVER_ONLY_DIR/, assetFiles) - .stdout.trim() - .split("\n") - .filter((line) => line.length > 0); + export const action = () => { + console.log(dotServerFile, dotServerDir, serverOnly); + return null; + } - console.log({ result }); + export default function() { + let { data } = useLoaderData(); + return ( + <> +

Index

+

{data}

+ + ); + } + `, + }); + let client = viteBuild({ cwd })[0]; + expect(client.status).toBe(0); - expect(result).toHaveLength(0); - expect(1).toBe("chewbacca"); + // detect client asset files + let assetFiles = glob.sync("**/*.@(js|jsx|ts|tsx)", { + cwd: path.join(cwd, "build/client"), + absolute: true, }); + + // grep for server-only values in client assets + let result = shell + .grep("-l", /SERVER_ONLY_FILE|SERVER_ONLY_DIR|node:fs/, assetFiles) + .stdout.trim() + .split("\n") + .filter((line) => line.length > 0); + + expect(result).toHaveLength(0); }); diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts index 67f48f5e4ef..fa1b001d15f 100644 --- a/packages/remix-dev/vite/plugin.ts +++ b/packages/remix-dev/vite/plugin.ts @@ -889,22 +889,30 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => { name: "remix-empty-server-modules", enforce: "pre", async transform(_code, id, options) { - if (!options?.ssr && /\.server(\.[cm]?[jt]sx?)?$/.test(id)) + if (options?.ssr) return; + let serverFileRE = /\.server(\.[cm]?[jt]sx?)?$/; + let serverDirRE = /\/\.server\//; + if (serverFileRE.test(id) || serverDirRE.test(id)) { return { code: "export default {}", map: null, }; + } }, }, { name: "remix-empty-client-modules", enforce: "pre", async transform(_code, id, options) { - if (options?.ssr && /\.client(\.[cm]?[jt]sx?)?$/.test(id)) + if (!options?.ssr) return; + let clientFileRE = /\.client(\.[cm]?[jt]sx?)?$/; + let clientDirRE = /\/\.client\//; + if (clientFileRE.test(id) || clientDirRE.test(id)) { return { code: "export default {}", map: null, }; + } }, }, {