Skip to content

Commit

Permalink
feat(vite): exclude modules within .server directories from client …
Browse files Browse the repository at this point in the history
…build
  • Loading branch information
pcattori committed Nov 27, 2023
1 parent fa908cd commit 487d453
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 61 deletions.
6 changes: 6 additions & 0 deletions .changeset/chatty-shrimps-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"integration-tests": minor
"@remix-run/dev": minor
---

Vite: exclude modules within `.server` directories from client build
2 changes: 0 additions & 2 deletions integration/helpers/vite-template/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
"paths": {
"~/*": ["./app/*"]
},

// Remix takes care of building everything in `remix build`.
"noEmit": true
}
}
7 changes: 3 additions & 4 deletions integration/helpers/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,18 +125,17 @@ 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,
env: {
...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"]);
Expand Down
132 changes: 79 additions & 53 deletions integration/vite-dot-server-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,75 +5,101 @@ 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";
`,
"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<typeof loader>();
return (
<>
<h2>Index</h2>
<p>{data}</p>
</>
);
}
`,
};
export default function() {
console.log(dotServerFile);
return <h1>Fail: Server file included in client</h1>
}
`,
});
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 <h1>Fail: Server directory included in client</h1>
}
`,
});
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<typeof loader>();
return (
<>
<h2>Index</h2>
<p>{data}</p>
</>
);
}
`,
});
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);
});
12 changes: 10 additions & 2 deletions packages/remix-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
},
},
{
Expand Down

0 comments on commit 487d453

Please sign in to comment.