Migrating a v1 CJS Remix project to Remix Vite (ESM)

If you're not living under a rock you're probably aware of the fact that Remix has become a Vite plugin, that also might be the reason you're reading this article. As much as that is awesome the fact that v1 used to be CJS and had no Vite in sight might be a bit of a hurdle for a lot to overcome and move to Vite. I'm here to help you understand everything you need to know to move your project over and give you some useful hints and tips to get you there.

My aim is that by the end of this article, you have your project working on Vite and you've fully migrated over. I might not cover all the edge cases but I should cover enough to get you 90% of the way there in the worst case. Alright, with that out of the way let's do it!

Also, if you don't know about it, Remix has an official guide as well, but I've written down challenges I had to resolve while moving a 100k LOC codebase to Vite and how you can tackle that as well. In any case you should go over the original article first:

https://remix.run/docs/en/main/future/vite#migrating

Package.json

Alright, the first thing you need to do is change is add "type": "module" to your package. This tells the bundler, among other things that your project is actually using ESM modules instead of CJS ones.

After you've added that make sure you install the latest remix version, at the time of writing the latest version is 2.4.0 so that might not be the case for you. After all of this is done you're pretty much done with the package.json

Tsconfig.json

After you're done with the package.json the next step is changing your tsconfig.json The most important two fields in the file are module and moduleResolution. Change your tsconfig.json to have the following:

"compilerOptions": {
    ...
    "moduleResolution": "Bundler", 
    "module": "ESNext", 
}

This will effectively move you over from CJS to ESM with the change we did to the package.json. After these two things are done we can move onto bigger things.

Vite configuration

First install the following dependencies:

npm i -D vite vite-tsconfig-paths

The next step is adding a vite.config.ts file and adding the following into it:

import { unstable_vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
// Only if you're using v1 convention still
import { createRoutesFromFolders } from "@remix-run/v1-route-convention";
// only if you're using mdx
import mdx from "@mdx-js/rollup";
// only if you want to make your experience with Remix even better
import { remixDevTools } from "remix-development-tools/vite";

export default defineConfig({ 
  plugins: [
    remixDevTools(),
    remix({
      ignoredRouteFiles: ["**/.*"],
      routes(defineRoutes) {
        return createRoutesFromFolders(defineRoutes, {
          ignoredFilePatterns: ["**/.*"]
        }) as any;
      }
    }),
    mdx(),
    tsconfigPaths()
  ]
});

What this does is allow the following behavior from v1 to work:

  • v1 route convention

  • mdx if you used it

  • alias resolving (~)

  • Remix.run working on Vite

You can omit and add whatever you feel is missing here. Make sure you also removed the remix.config.js file from your project as that is not needed anymore.

Alright, this concludes the Vite settings for now. Onto imports!

Imports and exports

The biggest difference in CJS and ESM is the export and import syntax. You shouldn't need to worry about npm packages but if you have the following in your project you'll need to change them to the ESM variant:

  • const pkg = require("pkg") => import pkg from "pkg"

  • const myImport = require("./file") => const myImport = await import("./file")

  • module.exports = {} => export default {}

This is usually the case with something like tailwind.config.js where you might have done module.exports.

After you've converted all of these, and hopefully you didn't do many of them you can move onto the next step.

Problematic dependencies

There are some dependencies out there that do not support CJS/ESM or just simply have trouble working with ESM. What you can do in this case is the following in your vite.config.ts :

export default defineConfig({
  ssr: {
    noExternal: ["remix-i18next", ... your dependencies that don't work...]
  }, 
});

I've found that this works well with packages that have issues with your move to ESM. If this doesn't work you can look into optimizeDeps in Vite and see if that can help, Vite usually handles CJS/ESM interop behind the scenes by prebundling dependencies with esbuild and making sure they work in both scenarios but if the package still has problems you can try the method from above.

Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.

Got this error? Don't be afraid, the fix is not that hard! This usually means there is a resolution issue with a 3rd party library that you're using where the default import is not resolved properly.

This is easily fixable by doing the following:

import  ReactPlayer, { type ReactPlayerProps } from "react-player";

const Player =
  typeof ReactPlayer.default !== "undefined"
    ? ReactPlayer.default
    : ReactPlayer;

export default Player as React.ComponentType<ReactPlayerProps>;

CSS

For this I won't go too deep because the Remix team already covers this topic in depth here:

https://remix.run/docs/en/main/future/vite#migrating

Fast refresh issues

If you're having issues with fast refresh make sure you're exporting constant named exports from your routes. What I've found that breaks fast refresh is the following:

  • barrelling your exports

  • Not having a constant name (the component name changes on every reload, this usually happens with arrow functions so consider using named function exports from your routes if you aren't already)

  • exporting stuff from routes that are not a part of Remix route exports ( Remix takes care of doing fast refresh and handling exports they expect but other exports can break fast refresh)

Custom scripts with ESM and Typescript

If you're using custom scripts to seed your database, run some queries, or do some code generation they might stop working after this change. If you were using ts-node to compile your scripts before what I've found that works is adding the following to my package.json:

"run:scripts": "node --no-warnings --experimental-specifier-resolution=node --loader ./loader.js",

Then adding the loader.js on the root:

import { resolve as resolveTs } from "ts-node/esm";
import * as tsConfigPaths from "tsconfig-paths";
import { pathToFileURL } from "url";

const { absoluteBaseUrl, paths } = tsConfigPaths.loadConfig();
const matchPath = tsConfigPaths.createMatchPath(absoluteBaseUrl, paths);

export function resolve(specifier, ctx, defaultResolve) {
  const match = matchPath(specifier);
  return match
    ? resolveTs(pathToFileURL(`${match}`).href, ctx, defaultResolve)
    : resolveTs(specifier, ctx, defaultResolve);
}

export { load, transformSource } from "ts-node/esm";

And then running the scripts with that, eg:

 "icons:create": "npm run run:scripts scripts/icons.ts"

Server bundle on the client

If you get the following error:

Uncaught SyntaxError: The requested module 'Y' does not provide an export named 'X'

This means your server code was leaking into the client bundle and you need to find the export X in module Y and make sure you refactor it in a way that it's not a server-only export, or this was a mistake. These cases are really specific and you need to figure them out on your own but the important take-away is that it basically means you imported something from ".server.ts" files and it ended up on the client.

Thank you

If you've reached the end you're a champ! I hope you liked my article.

If you liked the article consider sharing it with others, or tagging me on Twitter with your thoughts, would love to hear them!

If you wish to support me follow me on Twitter here:

twitter.com/AlemTuzlak59192

or if you want to follow my work you can do so on GitHub:

github.com/AlemTuzlak

And you can also sign up for the newsletter to get notified whenever I publish something new!