Skip to content

Commit

Permalink
fix(vite): preserve names for exports from .client imports
Browse files Browse the repository at this point in the history
Co-authored-by: Hiroshi Ogawa <hi.ogawa.zz@gmail.com>
  • Loading branch information
pcattori and hi-ogawa committed Dec 2, 2023
1 parent 6ef76d6 commit 79b0b3b
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 20 deletions.
15 changes: 15 additions & 0 deletions .changeset/rude-keys-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@remix-run/dev": patch
---

Vite: Preserve names for exports from .client imports

Unlike `.server` modules, the main idea is not to prevent code from leaking into the server build
since the client build is already public. Rather, the goal is to isolate the SSR render from client-only code.
Routes need to import code from `.client` modules without compilation failing and then rely on runtime checks
to determine if the code is running on the server or client.

Replacing `.client` modules with empty modules would cause the build to fail as ESM named imports are statically analyzed.
So instead, we preserve the named export but replace each exported value with an empty object.
That way, the import is valid at build time and the standard runtime checks can be used to determine if then
code is running on the server or client.
16 changes: 16 additions & 0 deletions integration/helpers/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import resolveBin from "resolve-bin";
import stripIndent from "strip-indent";
import waitOn from "wait-on";
import getPort from "get-port";
import shell from "shelljs";
import glob from "glob";

const __dirname = url.fileURLToPath(new URL(".", import.meta.url));

Expand Down Expand Up @@ -249,3 +251,17 @@ export function createEditor(projectDir: string) {
await fs.writeFile(filepath, transform(contents), "utf8");
};
}

export function grep(cwd: string, pattern: RegExp): string[] {
let assetFiles = glob.sync("**/*.@(js|jsx|ts|tsx)", {
cwd,
absolute: true,
});

let lines = shell
.grep("-l", pattern, assetFiles)
.stdout.trim()
.split("\n")
.filter((line) => line.length > 0);
return lines;
}
48 changes: 48 additions & 0 deletions integration/vite-dot-client-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as path from "node:path";
import { test, expect } from "@playwright/test";

import { createProject, grep, viteBuild } from "./helpers/vite.js";

let files = {
"app/utils.client.ts": String.raw`
export const dotClientFile = "CLIENT_ONLY_FILE";
export default dotClientFile;
`,
"app/.client/utils.ts": String.raw`
export const dotClientDir = "CLIENT_ONLY_DIR";
export default dotClientDir;
`,
};

test("Vite / client code excluded from server bundle", async () => {
let cwd = await createProject({
...files,
"app/routes/dot-client-imports.tsx": String.raw`
import { dotClientFile } from "../utils.client";
import { dotClientDir } from "../.client/utils";
export default function() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return (
<>
<h2>Index</h2>
<p>{mounted ? dotClientFile + dotClientDir : ""}</p>
</>
);
}
`,
});
let [client, server] = viteBuild({ cwd });
expect(client.status).toBe(0);
expect(server.status).toBe(0);
let lines = grep(
path.join(cwd, "build/server"),
/CLIENT_ONLY_FILE|CLIENT_ONLY_DIR/
);
expect(lines).toHaveLength(0);
});
18 changes: 1 addition & 17 deletions integration/vite-dot-server-test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
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";
import { createProject, grep, viteBuild } from "./helpers/vite.js";

let files = {
"app/utils.server.ts": String.raw`
Expand Down Expand Up @@ -198,17 +196,3 @@ test("Vite / dead-code elimination for server exports", async () => {
);
expect(lines).toHaveLength(0);
});

function grep(cwd: string, pattern: RegExp): string[] {
let assetFiles = glob.sync("**/*.@(js|jsx|ts|tsx)", {
cwd,
absolute: true,
});

let lines = shell
.grep("-l", pattern, assetFiles)
.stdout.trim()
.split("\n")
.filter((line) => line.length > 0);
return lines;
}
13 changes: 10 additions & 3 deletions packages/remix-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -938,14 +938,21 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
},
{
name: "remix-empty-client-modules",
enforce: "pre",
async transform(_code, id, options) {
enforce: "post",
async transform(code, id, options) {
if (!options?.ssr) return;
let clientFileRE = /\.client(\.[cm]?[jt]sx?)?$/;
let clientDirRE = /\/\.client\//;
if (clientFileRE.test(id) || clientDirRE.test(id)) {
let exports = esModuleLexer(code)[1];
return {
code: "export {}",
code: exports
.map(({ n: name }) =>
name === "default"
? "export default {};"
: `export const ${name} = {};`
)
.join("\n"),
map: null,
};
}
Expand Down

0 comments on commit 79b0b3b

Please sign in to comment.