Skip to content

Commit

Permalink
fix(remix-dev/vite): tree-shake unused route exports (remix-run#8468)
Browse files Browse the repository at this point in the history
  • Loading branch information
markdalgleish authored Jan 11, 2024
1 parent 69ab6ac commit ee1f202
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/polite-weeks-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/dev": patch
---

Vite: Tree-shake unused route exports in the client build
37 changes: 37 additions & 0 deletions integration/vite-unused-route-exports-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as path from "node:path";
import { test, expect } from "@playwright/test";

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

test("Vite / dead-code elimination for unused route exports", async () => {
let cwd = await createProject({
"app/routes/custom-route-exports.tsx": String.raw`
const unusedMessage = "ROUTE_EXPORT_THAT_ISNT_USED";
const usedMessage = "ROUTE_EXPORT_THAT_IS_USED";
export const unusedRouteExport = unusedMessage;
export const usedRouteExport = usedMessage;
export default function CustomExportsRoute() {
return <h1>Custom route exports</h1>
}
`,
"app/routes/use-route-export.tsx": String.raw`
import { usedRouteExport } from "./custom-route-exports";
export default function CustomExportsRoute() {
return <h1>{usedRouteExport}</h1>
}
`,
});
let { status } = viteBuild({ cwd });
expect(status).toBe(0);

expect(
grep(path.join(cwd, "build/client"), /ROUTE_EXPORT_THAT_ISNT_USED/).length
).toBe(0);

expect(
grep(path.join(cwd, "build/client"), /ROUTE_EXPORT_THAT_IS_USED/).length
).toBeGreaterThanOrEqual(1);
});
77 changes: 57 additions & 20 deletions packages/remix-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,20 @@ const supportedRemixConfigKeys = [
type SupportedRemixConfigKey = typeof supportedRemixConfigKeys[number];
type SupportedRemixConfig = Pick<RemixUserConfig, SupportedRemixConfigKey>;

const SERVER_ONLY_EXPORTS = ["loader", "action", "headers"];
const SERVER_ONLY_ROUTE_EXPORTS = ["loader", "action", "headers"];
const CLIENT_ROUTE_EXPORTS = [
"clientAction",
"clientLoader",
"default",
"ErrorBoundary",
"handle",
"HydrateFallback",
"links",
"meta",
"shouldRevalidate",
];

const CLIENT_ROUTE_QUERY_STRING = "?client-route";

// We need to provide different JSDoc comments in some cases due to differences
// between the Remix config and the Vite plugin.
Expand Down Expand Up @@ -140,8 +153,6 @@ let remixReactProxyId = VirtualModule.id("remix-react-proxy");
let hmrRuntimeId = VirtualModule.id("hmr-runtime");
let injectHmrRuntimeId = VirtualModule.id("inject-hmr-runtime");

const isJsFile = (filePath: string) => /\.[cm]?[jt]sx?$/i.test(filePath);

const resolveRelativeRouteFilePath = (
route: ConfigRoute,
pluginConfig: ResolvedRemixVitePluginConfig
Expand Down Expand Up @@ -177,19 +188,19 @@ const resolveChunk = (
absoluteFilePath: string
) => {
let vite = importViteEsmSync();
let rootRelativeFilePath = path.relative(
pluginConfig.rootDirectory,
absoluteFilePath
let rootRelativeFilePath = vite.normalizePath(
path.relative(pluginConfig.rootDirectory, absoluteFilePath)
);
let manifestKey = vite.normalizePath(rootRelativeFilePath);
let entryChunk = viteManifest[manifestKey];
let entryChunk =
viteManifest[rootRelativeFilePath + CLIENT_ROUTE_QUERY_STRING] ??
viteManifest[rootRelativeFilePath];

if (!entryChunk) {
let knownManifestKeys = Object.keys(viteManifest)
.map((key) => '"' + key + '"')
.join(", ");
throw new Error(
`No manifest entry found for "${manifestKey}". Known manifest keys: ${knownManifestKeys}`
`No manifest entry found for "${rootRelativeFilePath}". Known manifest keys: ${knownManifestKeys}`
);
}

Expand All @@ -215,7 +226,7 @@ const resolveBuildAssetPaths = (
]);

return {
module: `${pluginConfig.publicPath}${entryChunk.file}`,
module: `${pluginConfig.publicPath}${entryChunk.file}${CLIENT_ROUTE_QUERY_STRING}`,
imports:
dedupe(chunks.flatMap((e) => e.imports ?? [])).map((imported) => {
return `${pluginConfig.publicPath}${viteManifest[imported].file}`;
Expand Down Expand Up @@ -296,7 +307,7 @@ const getRouteModuleExports = async (

let ssr = true;
let { pluginContainer, moduleGraph } = viteChildCompiler;
let routePath = path.join(pluginConfig.appDirectory, routeFile);
let routePath = path.resolve(pluginConfig.appDirectory, routeFile);
let url = resolveFileUrl(pluginConfig, routePath);

let resolveId = async () => {
Expand Down Expand Up @@ -576,9 +587,7 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
module: `${resolveFileUrl(
pluginConfig,
resolveRelativeRouteFilePath(route, pluginConfig)
)}${
isJsFile(route.file) ? "" : "?import" // Ensure the Vite dev server responds with a JS module
}`,
)}${CLIENT_ROUTE_QUERY_STRING}`,
hasAction: sourceExports.includes("action"),
hasLoader: sourceExports.includes("loader"),
hasClientAction: sourceExports.includes("clientAction"),
Expand Down Expand Up @@ -692,8 +701,12 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
preserveEntrySignatures: "exports-only",
input: [
pluginConfig.entryClientFilePath,
...Object.values(pluginConfig.routes).map((route) =>
path.resolve(pluginConfig.appDirectory, route.file)
...Object.values(pluginConfig.routes).map(
(route) =>
`${path.resolve(
pluginConfig.appDirectory,
route.file
)}${CLIENT_ROUTE_QUERY_STRING}`
),
],
},
Expand Down Expand Up @@ -796,10 +809,27 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
});
await viteChildCompiler.pluginContainer.buildStart({});
},
transform(code, id) {
async transform(code, id) {
if (isCssModulesFile(id)) {
cssModulesManifest[id] = code;
}

if (id.endsWith(CLIENT_ROUTE_QUERY_STRING)) {
invariant(cachedPluginConfig);
let routeModuleId = id.replace(CLIENT_ROUTE_QUERY_STRING, "");
let sourceExports = await getRouteModuleExports(
viteChildCompiler,
cachedPluginConfig,
routeModuleId
);

let routeFileName = path.basename(routeModuleId);
let clientExports = sourceExports
.filter((exportName) => CLIENT_ROUTE_EXPORTS.includes(exportName))
.join(", ");

return `export { ${clientExports} } from "./${routeFileName}";`;
}
},
buildStart() {
invariant(viteConfig);
Expand Down Expand Up @@ -1054,7 +1084,7 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
let isRoute = getRoute(pluginConfig, importer);

if (isRoute) {
let serverOnlyExports = SERVER_ONLY_EXPORTS.map(
let serverOnlyExports = SERVER_ONLY_ROUTE_EXPORTS.map(
(xport) => `\`${xport}\``
).join(", ");
throw Error(
Expand Down Expand Up @@ -1144,7 +1174,7 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
if (pluginConfig.isSpaMode) {
let serverOnlyExports = esModuleLexer(code)[1]
.map((exp) => exp.n)
.filter((exp) => SERVER_ONLY_EXPORTS.includes(exp));
.filter((exp) => SERVER_ONLY_ROUTE_EXPORTS.includes(exp));
if (serverOnlyExports.length > 0) {
let str = serverOnlyExports.map((e) => `\`${e}\``).join(", ");
let message =
Expand All @@ -1170,7 +1200,7 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
}

return {
code: removeExports(code, SERVER_ONLY_EXPORTS),
code: removeExports(code, SERVER_ONLY_ROUTE_EXPORTS),
map: null,
};
},
Expand Down Expand Up @@ -1286,6 +1316,13 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
let useFastRefresh = !ssr && (isJSX || code.includes(devRuntime));
if (!useFastRefresh) return;

if (id.endsWith(CLIENT_ROUTE_QUERY_STRING)) {
let pluginConfig =
cachedPluginConfig || (await resolvePluginConfig());

return { code: addRefreshWrapper(pluginConfig, code, id) };
}

let result = await babel.transformAsync(code, {
filename: id,
sourceFileName: filepath,
Expand Down

0 comments on commit ee1f202

Please sign in to comment.