remix-run / remix

Build Better Websites. Create modern, resilient user experiences with web fundamentals.
https://remix.run
MIT License
30.01k stars 2.53k forks source link

Vite config is evaluated twice in Remix dev mode #10023

Open kettanaito opened 1 month ago

kettanaito commented 1 month ago

Reproduction

This is reproducible on any latest Remix project (sorry for not providing an exact repo, there is literally nothing specific to the app).

  1. Open vite.config.ts.
  2. Put a console.log(1) in the root scope of the module.
  3. Run npm run dev.
  4. See 1 being printed twice.

System Info

System info is irrelevant.

Used Package Manager

pnpm

Expected Behavior

Vite, its configuration and plugins are executed once.

Actual Behavior

Remix executes everything Vite-related twice. This makes it impossible to write proper Vite plugins as every hook in a plugin will be called twice.

Here are the two calls traced to see their stack traces:

Trace: 1
    at file://app/vite.config.ts.timestamp-1727182481040-668186ca07abb.mjs:25:9
    at ModuleJob.run (node:internal/modules/esm/module_job:218:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:329:24)
    at async loadConfigFromBundledFile (file:///app/node_modules/.pnpm/vite@5.4.7_@types+node@20.16.5/node_modules/vite/dist/node/chunks/dep-DG6Lorbi.js:66633:15)
    at async loadConfigFromFile (file:///app/node_modules/.pnpm/vite@5.4.7_@types+node@20.16.5/node_modules/vite/dist/node/chunks/dep-DG6Lorbi.js:66474:24)
    at async resolveConfig (file:///app/node_modules/.pnpm/vite@5.4.7_@types+node@20.16.5/node_modules/vite/dist/node/chunks/dep-DG6Lorbi.js:66082:24)
    at async _createServer (file:///app/node_modules/.pnpm/vite@5.4.7_@types+node@20.16.5/node_modules/vite/dist/node/chunks/dep-DG6Lorbi.js:62700:18)
    at async dev (/app/node_modules/.pnpm/@remix-run+dev@2.12.0_@remix-run+react@2.12.0_@remix-run+serve@2.12.0_@types+node@20.16.5_typescript@5.6.2_vite@5.4.7/node_modules/@remix-run/dev/dist/vite/dev.js:39:16)
    at async Object.viteDev (/app/node_modules/.pnpm/@remix-run+dev@2.12.0_@remix-run+react@2.12.0_@remix-run+serve@2.12.0_@types+node@20.16.5_typescript@5.6.2_vite@5.4.7/node_modules/@remix-run/dev/dist/cli/commands.js:220:3)
    at async Object.run (/app/node_modules/.pnpm/@remix-run+dev@2.12.0_@remix-run+react@2.12.0_@remix-run+serve@2.12.0_@types+node@20.16.5_typescript@5.6.2_vite@5.4.7/node_modules/@remix-run/dev/dist/cli/run.js:271:7)

Trace: 1
    at file://app/vite.config.ts.timestamp-1727182481241-1ed74236d2d8e.mjs:25:9
    at ModuleJob.run (node:internal/modules/esm/module_job:218:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:329:24)
    at async loadConfigFromBundledFile (file:///app/node_modules/.pnpm/vite@5.4.7_@types+node@20.16.5/node_modules/vite/dist/node/chunks/dep-DG6Lorbi.js:66633:15)
    at async Module.loadConfigFromFile (file:///app/node_modules/.pnpm/vite@5.4.7_@types+node@20.16.5/node_modules/vite/dist/node/chunks/dep-DG6Lorbi.js:66474:24)
    at async configResolved (/app/node_modules/.pnpm/@remix-run+dev@2.12.0_@remix-run+react@2.12.0_@remix-run+serve@2.12.0_@types+node@20.16.5_typescript@5.6.2_vite@5.4.7/node_modules/@remix-run/dev/dist/vite/plugin.js:727:37)
    at async Promise.all (index 1)
    at async resolveConfig (file:///app/node_modules/.pnpm/vite@5.4.7_@types+node@20.16.5/node_modules/vite/dist/node/chunks/dep-DG6Lorbi.js:66346:3)
    at async _createServer (file:///app/node_modules/.pnpm/vite@5.4.7_@types+node@20.16.5/node_modules/vite/dist/node/chunks/dep-DG6Lorbi.js:62700:18)
    at async dev (/app/node_modules/.pnpm/@remix-run+dev@2.12.0_@remix-run+react@2.12.0_@remix-run+serve@2.12.0_@types+node@20.16.5_typescript@5.6.2_vite@5.4.7/node_modules/@remix-run/dev/dist/vite/dev.js:39:16)

The look quite identical to me. A bug, perhaps?

fmvilas commented 1 week ago

Seeing the same issue here. I'm trying to debounce calls but it doesn't work because state is not preserved between the two. At first, I thought each run was a different process but process.pid and process.ppid are the same, so not sure what's happening here. To illustrate the problem, I've put a variable in the root of my module like so:

let count = 0

And then just do a console.log(count) followed by a count = count + 1. I see the "0" twice, the "1" twice, and so on.

Seeing that pid and ppid are the same, the only idea that comes to my mind is that the two calls are using different Node.js module resolvers but honestly I have no idea of the internals of Remix or Vite 🤷

Hope that helps!

kettanaito commented 1 week ago

@fmvilas, I believe it may be a bit simpler. I think under the hood Remix may be using Vite CLI with the same Vite config twice. One for a dev server, the other for something else.

It would be really great for someone from the team to jump in on this. The issue makes it impossible to author Vite plugins for Remix apps (how other plugins work is also magic; perhaps they are broken too). cc @pcattori @brookslybrand

pcattori commented 1 week ago

Yes we are using a "child compiler" (aka a manually instantiated Vite dev server within the main Vite dev server) in order to do some processing. Currently we special case a couple internal plugins to be omitted from the "child compiler".

For example, to support MDX routes we need to be able to detect server-only export in MDX before Vite transpiles it to JS. That way any error messages about server-only exports can reference the original source code rather than the transpiled JS. To be honest, I think the whole "MDX as routes" approach I explored is a dead end and a better approach would be to enforce that routes are JS-syntax parsable (i.e. not MDX) and have a "collections" feature similar to Astro for content like MDX. Then a "blog post" route (written in TS/JS) could show items in the "blog" collection (written in MDX).

That said, our current approach shouldn't cause issues for stateless plugins. However, for stateful plugins this can certainly be an issue. Even so, I do wish that Vite had an API for programmatically applying transformations without needing to spin up an additional dev server.

To be honest, I've been unsatisfied with the trade-offs of the "child compiler" approach and am actively looking for ways to get off of it. Its too magical and brittle at the moment.