remix icon indicating copy to clipboard operation
remix copied to clipboard

Inconsistent bundling of css assets between builds (Vite, Stylex)

Open predaytor opened this issue 1 year ago • 8 comments

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


Знімок екрана 2024-05-16 о 11 37 54 Знімок екрана 2024-05-16 о 11 40 38

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

predaytor avatar May 17 '24 09:05 predaytor

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.

Build Output

stewarthsoj avatar May 22 '24 15:05 stewarthsoj

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.

predaytor avatar Jun 10 '24 11:06 predaytor

update: Unfortunately, the problem persists. currently some routes load a single css file, others include duplicate.

predaytor avatar Jun 17 '24 11:06 predaytor

@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?

machour avatar Oct 17 '24 12:10 machour

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

machour avatar Oct 17 '24 12:10 machour

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 !

machour avatar Oct 17 '24 12:10 machour

Reproduce case on the latest remix version: https://github.com/machour/remix-issue-9451

machour avatar Oct 17 '24 13:10 machour

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.

silvenon avatar Oct 17 '24 16:10 silvenon

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 avatar Jan 14 '25 17:01 patdx

@patdx what are you using for CSS? Just regular (S)CSS, Tailwind or something else? Are you using Environment API by any chance?

silvenon avatar Jan 14 '25 18:01 silvenon

@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

patdx avatar Jan 15 '25 03:01 patdx

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,
  },
};

silvenon avatar Jan 15 '25 15:01 silvenon

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.

silvenon avatar Jan 15 '25 16:01 silvenon