Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(vite): preserve names for exports from .client imports #8200

Merged
merged 1 commit into from
Dec 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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