From 2d3e1d8b81263c02c2e606739aac42403f33d538 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Mon, 27 Nov 2023 17:07:42 -0500 Subject: [PATCH] Vite: exclude modules within `.server` directories from client build (#8154) Co-authored-by: Mark Dalgleish --- .changeset/chatty-shrimps-sell.md | 5 + .../helpers/vite-template/tsconfig.json | 2 - integration/helpers/vite.ts | 20 +++- integration/package.json | 3 +- integration/vite-dot-server-test.ts | 105 ++++++++++++++++++ packages/remix-dev/vite/plugin.ts | 12 +- yarn.lock | 14 +++ 7 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 .changeset/chatty-shrimps-sell.md create mode 100644 integration/vite-dot-server-test.ts diff --git a/.changeset/chatty-shrimps-sell.md b/.changeset/chatty-shrimps-sell.md new file mode 100644 index 00000000000..6a6f7ba3664 --- /dev/null +++ b/.changeset/chatty-shrimps-sell.md @@ -0,0 +1,5 @@ +--- +"@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 cce5b28bf45..8dced0b42b4 100644 --- a/integration/helpers/vite.ts +++ b/integration/helpers/vite.ts @@ -1,4 +1,4 @@ -import { spawn, type ChildProcess } from "node:child_process"; +import { spawn, spawnSync, type ChildProcess } from "node:child_process"; import path from "node:path"; import type { Readable } from "node:stream"; import url from "node:url"; @@ -119,6 +119,24 @@ const createDev = return async () => await kill(proc.pid!); }; +export const viteBuild = (args: { cwd: string }) => { + let vite = resolveBin.sync("vite"); + let commands = [ + [vite, "build"], + [vite, "build", "--ssr"], + ]; + let results = []; + for (let command of commands) { + let result = spawnSync("node", command, { + cwd: args.cwd, + env: { + ...process.env, + }, + }); + results.push(result); + } + return results; +}; export const viteDev = createDev([resolveBin.sync("vite"), "dev"]); export const customDev = createDev(["./server.mjs"]); diff --git a/integration/package.json b/integration/package.json index 64c34581e02..99f6ff48453 100644 --- a/integration/package.json +++ b/integration/package.json @@ -34,6 +34,7 @@ "strip-indent": "^3.0.0", "tailwindcss": "^3.3.0", "type-fest": "^4.0.0", - "typescript": "^5.1.0" + "typescript": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" } } diff --git a/integration/vite-dot-server-test.ts b/integration/vite-dot-server-test.ts new file mode 100644 index 00000000000..537e5ec245a --- /dev/null +++ b/integration/vite-dot-server-test.ts @@ -0,0 +1,105 @@ +import * as path from "node:path"; +import { test, expect } from "@playwright/test"; +import shell from "shelljs"; +import glob from "glob"; + +import { createProject, viteBuild } from "./helpers/vite.js"; + +let files = { + "app/utils.server.ts": String.raw` + export const dotServerFile = "SERVER_ONLY_FILE"; + `, + "app/.server/utils.ts": String.raw` + export const dotServerDir = "SERVER_ONLY_DIR"; + `, +}; + +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() { + 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("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"; + + 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"; + + import { dotServerFile } from "../utils.server"; + import { dotServerDir } from "../.server/utils"; + + export const loader = () => { + let contents = fs.readFileSync("blah"); + let data = dotServerFile + dotServerDir + serverOnly + contents; + return json({ data }); + } + + export const action = () => { + console.log(dotServerFile, dotServerDir, serverOnly); + return null; + } + + export default function() { + let { data } = useLoaderData(); + return ( + <> +

Index

+

{data}

+ + ); + } + `, + }); + let client = viteBuild({ cwd })[0]; + expect(client.status).toBe(0); + + // 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, }; + } }, }, { diff --git a/yarn.lock b/yarn.lock index 682e1028180..592594a8e07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12553,6 +12553,11 @@ ts-interface-checker@^0.1.9: resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== +tsconfck@^2.1.0: + version "2.1.2" + resolved "https://registry.npmjs.org/tsconfck/-/tsconfck-2.1.2.tgz#f667035874fa41d908c1fe4d765345fcb1df6e35" + integrity sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg== + tsconfig-paths@^3.14.1: version "3.14.1" resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz" @@ -13122,6 +13127,15 @@ vite-node@^0.28.5: source-map-support "^0.5.21" vite "^3.0.0 || ^4.0.0" +vite-tsconfig-paths@^4.2.1: + version "4.2.1" + resolved "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.2.1.tgz#e53b89096b91d31a6d1e26f75999ea8c336a89ed" + integrity sha512-GNUI6ZgPqT3oervkvzU+qtys83+75N/OuDaQl7HmOqFTb0pjZsuARrRipsyJhJ3enqV8beI1xhGbToR4o78nSQ== + dependencies: + debug "^4.1.1" + globrex "^0.1.2" + tsconfck "^2.1.0" + "vite@^3.0.0 || ^4.0.0", vite@^4.1.4: version "4.4.10" resolved "https://registry.npmjs.org/vite/-/vite-4.4.10.tgz#3794639cc433f7cb33ad286930bf0378c86261c8"