remix
remix copied to clipboard
Inconsistent bundling of css assets between builds (Vite, Stylex)
Reproduction
When using the ?url import, there seems to be a mismatch between the generated assets, resulting in two identical css files (with different hashes), with the mismatched included at the top of the <head/>. It's a vite-plugin-stylex-dev for StyleX css solution.
For some reason I cannot reproduce bug in the sandbox.
This happens occasionally between builds, so maybe it's due to some internal Remix caching. When this mismatch occurs, this line outputs:
✓ 1 resource moved from Remix server build to client resources.
build/client/assets/index-CiOyfy0p.css
Logs:
> remix vite:build
Using vars defined in .dev.vars
Using vars defined in .dev.vars
Using vars defined in .dev.vars
Using vars defined in .dev.vars
vite v5.2.11 building for production...
✓ 827 modules transformed.
Generated an empty chunk: "phone-textfield".
build/client/.vite/manifest.json 18.13 kB │ gzip: 1.81 kB
build/client/assets/sprite-BdV8AqDD.svg 30.02 kB │ gzip: 9.43 kB
build/client/assets/index-N3JczSMr.css 102.92 kB │ gzip: 21.23 kB
build/client/assets/phone-textfield-l0sNRNKZ.js 0.00 kB │ gzip: 0.02 kB
build/client/assets/_app.users.teachers-D0KjJBcV.js 0.13 kB │ gzip: 0.14 kB
build/client/assets/_app.users.removed-Bw8pfhzk.js 0.13 kB │ gzip: 0.14 kB
build/client/assets/_app.users.administrators-zltEt6Gl.js 0.13 kB │ gzip: 0.14 kB
build/client/assets/_app.users.applicants-zltEt6Gl.js 0.13 kB │ gzip: 0.14 kB
build/client/assets/recovery-DNcuDyGd.js 0.13 kB │ gzip: 0.14 kB
build/client/assets/toggle-button-BRlrV58r.js 0.29 kB │ gzip: 0.23 kB
build/client/assets/data-RBlXXuFm.js 0.36 kB │ gzip: 0.31 kB
build/client/assets/_app.groups-DRAlv3Dh.js 0.37 kB │ gzip: 0.29 kB
build/client/assets/_app.applications-DSix6XLc.js 0.37 kB │ gzip: 0.29 kB
build/client/assets/_app.dashboard-DGrcCHYH.js 0.37 kB │ gzip: 0.29 kB
build/client/assets/_app.profile-CDyQESv2.js 0.37 kB │ gzip: 0.29 kB
build/client/assets/_app.calendar-B-6OaHIp.js 0.37 kB │ gzip: 0.30 kB
build/client/assets/_app.learning-DN9SaJxK.js 0.37 kB │ gzip: 0.30 kB
build/client/assets/_app.report-DHEzYFMA.js 0.37 kB │ gzip: 0.30 kB
build/client/assets/_app.library-BOJ6LJAi.js 0.37 kB │ gzip: 0.30 kB
build/client/assets/_app.transactions-BwQdegkR.js 0.37 kB │ gzip: 0.30 kB
build/client/assets/_app.history-ocq8kroE.js 0.37 kB │ gzip: 0.30 kB
build/client/assets/_app.settings-CeF1owLH.js 0.37 kB │ gzip: 0.31 kB
build/client/assets/_app.messages-S8M7tn2Y.js 0.37 kB │ gzip: 0.30 kB
build/client/assets/icon-button--aXbSP48.js 0.41 kB │ gzip: 0.28 kB
build/client/assets/birthday-datefield-BqNzzUqC.js 0.42 kB │ gzip: 0.33 kB
build/client/assets/icon-D09kYBQN.js 0.43 kB │ gzip: 0.30 kB
build/client/assets/heading-C_rbyHuK.js 0.48 kB │ gzip: 0.34 kB
build/client/assets/header-C0nPIVLm.js 0.65 kB │ gzip: 0.49 kB
build/client/assets/container-B01piXsx.js 0.70 kB │ gzip: 0.42 kB
build/client/assets/logo-DuT4e1vb.js 0.75 kB │ gzip: 0.49 kB
build/client/assets/phone-textfield-C-jSd1_r.js 1.00 kB │ gzip: 0.59 kB
build/client/assets/email-textfield-CTVomzYq.js 1.42 kB │ gzip: 0.77 kB
build/client/assets/_app.users_.students._id-B3Yrzq6t.js 1.82 kB │ gzip: 0.95 kB
build/client/assets/_auth.signup-CArL0x5s.js 2.52 kB │ gzip: 1.18 kB
build/client/assets/_auth-DBv4tsY3.js 2.77 kB │ gzip: 1.44 kB
build/client/assets/_auth.login-ChIY2W4h.js 2.84 kB │ gzip: 1.37 kB
build/client/assets/stylex-BOjyk742.js 2.95 kB │ gzip: 1.35 kB
build/client/assets/route-D9NRnaPk.js 2.99 kB │ gzip: 1.23 kB
build/client/assets/_app.users_.students.new-DLn0FcZR.js 3.25 kB │ gzip: 1.65 kB
build/client/assets/root-Csrl-H1Q.js 3.39 kB │ gzip: 1.54 kB
build/client/assets/avatar-CWYAx4KT.js 3.42 kB │ gzip: 1.40 kB
build/client/assets/form-C70NaR-f.js 3.98 kB │ gzip: 1.55 kB
build/client/assets/checkbox-CoohEvgQ.js 4.08 kB │ gzip: 1.33 kB
build/client/assets/route-j0DL5Qgm.js 4.56 kB │ gzip: 2.21 kB
build/client/assets/input-qekBshbH.js 6.82 kB │ gzip: 2.00 kB
build/client/assets/password-textfield-Qbj1_dUL.js 8.38 kB │ gzip: 2.30 kB
build/client/assets/jsx-runtime-C7dT0ItP.js 9.95 kB │ gzip: 3.63 kB
build/client/assets/entry.client-VFfHxz5W.js 11.63 kB │ gzip: 4.09 kB
build/client/assets/route-COXK8fsl.js 21.87 kB │ gzip: 6.73 kB
build/client/assets/index-CQFX0XzH.js 36.81 kB │ gzip: 11.07 kB
build/client/assets/components-DxQqOcDi.js 276.88 kB │ gzip: 87.94 kB
build/client/assets/sprite-D7EuGFbZ.js 329.70 kB │ gzip: 91.42 kB
✓ built in 2.62s
Using vars defined in .dev.vars
Using vars defined in .dev.vars
vite v5.2.11 building SSR bundle for production...
"chain" is imported from external module "react-aria" but never used in "app/routes/_app.users.students/data.tsx", "app/components/fieldgroup.tsx", "app/components/link.tsx", "app/components/upload-file-field.tsx", "app/components/searchfield.tsx", "app/routes/_app/route.tsx", "app/components/avatar/avatar.tsx", "app/routes/_app.users/user-alert-dialog.tsx" and "app/routes/_app.users.students/route.tsx".
✓ 100 modules transformed.
build/server/.vite/manifest.json 0.51 kB
build/server/assets/sprite-BdV8AqDD.svg 30.02 kB
build/server/assets/index-CiOyfy0p.css 102.92 kB
build/server/index.js 204.82 kB
✓ 1 asset moved from Remix server build to client assets.
build/client/assets/index-CiOyfy0p.css
✓ built in 862ms
https://github.com/remix-run/remix/blob/2e65318b15df407b31b101362e03729e61ec5622/packages/remix-dev/vite/plugin.ts#L1441-L1454
Original issue: https://github.com/nonzzz/vite-plugin-stylex/issues/25
System Info
System:
OS: macOS 14.4
CPU: (8) arm64 Apple M1
Memory: 74.84 MB / 8.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 20.9.0 - /usr/local/bin/node
Yarn: 1.22.22 - /usr/local/bin/yarn
npm: 10.1.0 - /usr/local/bin/npm
pnpm: 9.1.0 - /usr/local/bin/pnpm
bun: 1.0.12 - ~/.bun/bin/bun
Browsers:
Chrome: 124.0.6367.208
Chrome Canary: 126.0.6478.1
Safari: 17.4
Safari Technology Preview: 17.4
npmPackages:
@remix-run/cloudflare: ^2.9.2 => 2.9.2
@remix-run/cloudflare-pages: ^2.9.2 => 2.9.2
@remix-run/dev: ^2.9.2 => 2.9.2
@remix-run/react: ^2.9.2 => 2.9.2
vite: ^5.2.11 => 5.2.11
Used Package Manager
pnpm
Expected Behavior
pnpm build should result in a single css file
Actual Behavior
pnpm build results in identical duplicate of the original css file and includes it in the output html
I'm not sure if these issues are related but I'm experiencing this too with Tailwind. I only have one CSS file in my Remix project, that being root.css. The build is outputting two CSS files, though slightly different in size.
it seems that after the release of [email protected] everything is fine, the duplicated css asset was generated in output (still odd), but not included in the application anymore.
update: Unfortunately, the problem persists. currently some routes load a single css file, others include duplicate.
@JoshStwrt I've experienced the same issue, and was able to fix it by changing from a ?url import to a side effect (import "~/app.css"). In this case Remix handle all this and there's no problem.
I'm trying to come up with a reproduce case for the team, but it seems like I'm unable to reproduce it outside my huge private project. Are you able to reproduce the bug?
Running a diff on both generated files (after reformatting with prettier), shows the following diffs:
.bg-gray-500\/20 {
- background-color: #6b728033;
+ background-color: rgba(107, 114, 128, 0.2);
}
.bg-green-50 {
--tw-bg-opacity: 1;
@@ -2048,7 +2048,7 @@
background-color: rgb(217 0 0 / var(--tw-bg-opacity));
}
.bg-ram\/20 {
- background-color: #d9000033;
+ background-color: rgba(217, 0, 0, 0.2);
}
.bg-red-50 {
--tw-bg-opacity: 1;
@@ -2059,7 +2059,7 @@
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
}
.bg-slate-900\/80 {
- background-color: #0f172acc;
+ background-color: rgba(15, 23, 42, 0.8);
}
something is inconsistent in the manner the tailwind css files are generated
Found the culprit, I'm using @vitejs/plugin-legacy to support older devices.
As soon as I remove it from vite, the problem goes away:
diff --git a/www/front/vite.config.js b/www/front/vite.config.js
index 71de5e117..47f56c9ba 100644
--- a/www/front/vite.config.js
+++ b/www/front/vite.config.js
@@ -1,6 +1,5 @@
import { vitePlugin as remix } from "@remix-run/dev";
import { installGlobals } from "@remix-run/node";
-import legacy from "@vitejs/plugin-legacy";
import { flatRoutes } from "remix-flat-routes";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
@@ -21,9 +20,6 @@ export default defineConfig({
},
routes: async defineRoutes => flatRoutes("routes", defineRoutes),
}),
- legacy({
- targets: ["defaults", "safari >= 12.1", "not IE 11"],
- }),
tsconfigPaths(),
],
});
There's an inconsistency somewhere between the client and server build !
Reproduce case on the latest remix version: https://github.com/machour/remix-issue-9451
Seems like @vitejs/plugin-legacy is creating another bundle that's intended to be inserted separately, but perhaps Remix doesn't have a way of handling that, so the server build collects the modern bundle, and the client build hydrates with the legacy bundle, or the other way around.
Maybe Remix's Vite plugin for collecting styles doesn't recognize that one styles are coming from one modern bundle and another from the legacy bundle because the import is named the same.
I'm yet to familiarize myself with FOUC-less Vite dev experience when SSR is involved, I just know that these Vite plugins for styles by frameworks follow a similar gimmick.
I hope that someone comes to the rescue 🤞 In the meantime I'll try to find out what more I can.
This is happening to me with React Router 7 (successor of Remix 2) too. After some investigation I realized that I am manually setting some build target and minify settings for the server/SSR build. I raised the target level of my server and turned off minification to help with debugging the server JS. I didn't realize that this also causes CSS referenced by the server build to get compiled to a different target level than the client build, causing the CSS contents to be slightly different in both builds, ultimate causing the filename hashes to also be different.
if I ensure that the minify settings and config.build.target (or config.build.cssTarget) settings are consistent between the client and server build configs, the issue seems to go away. But I feel a little uncomfortable with this solution. I think I will go back to the CSS side effect import strategy.
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
reactRouter(),
{
name: "ssr-config",
apply: "build",
enforce: "post",
config(config, env) {
if (env.isSsrBuild) {
return {
build: {
target: "esnext",
minify: false,
},
};
}
},
configResolved(config) {
console.log(
`config.build.cssTarget: ${JSON.stringify(config.build.cssTarget)}`
);
},
},
],
});
@patdx what are you using for CSS? Just regular (S)CSS, Tailwind or something else? Are you using Environment API by any chance?
@silvenon My main project is using Tailwind but I can reproduce this with regular CSS too. Not using Environment API. I prepared a minimum reproduction here: https://github.com/patdx/react-router-css-bundle-reproduction/blob/main/README.md
Yeah, in effort to make the CSS story simple Remix relies on the assumption that the CSS built in client and server build will always match. But the point of the Vite config is to edit it, so there should really be a warning on mismatch or even error.
Another hiccup which makes this tougher to work around is that Vite converts build.target: "modules" into:
["es2020", "edge88", "firefox78", "chrome87", "safari14"]
and doesn't make that array easy to reach at the moment where you need to, like the config() hook, in your case, or the root configuration itself, which is really unfortunate.
What would work in your case is using that array as build.cssTarget:
return {
build: {
target: "esnext",
// ensuring that server-side building produces the same CSS as client-side
// using simply "modules" won't work in this case, this array is what Vite resolves "modules" to
// https://github.com/vitejs/vite/blob/f2aed62d0bf1b66e870ee6b4aab80cd1702793ab/packages/vite/src/node/constants.ts#L67-L76
cssTarget: ["es2020", "edge88", "firefox78", "chrome87", "safari14"],
minify: false,
},
};
Actually there is a way to obtain it, via Vite's own resolveConfig:
async config(config, env) {
if (env.isSsrBuild) {
const resolvedConfig = await resolveConfig(
{
configFile: false, // avoid infinite loop
build: { target: "modules" }, // "modules" will get resolved to the array of targets
},
env.command,
);
return {
build: {
target: "esnext",
cssTarget: resolvedConfig.build.target,
minify: false,
},
};
}
},
Surprisingly enough, CSS files generated by SSR are still minified, despite build.minify. I don't know why.