Migrating a Create-React-App monorepo to Vite

I got several years developing React.js applications using creat-react-app as everyone did and never worried too much about tooling until it started to feel like a burden.
My first touch with Vite was with a friend of mine building a Vue frontend, but it wasn't until a few months ago I was presented with a coworker repo with React.js and it caught up with me the fast bundling and easy-to-start as with CRA.
So, after testing with a quick migration of a previous smaller project, I thought it was time to take this big monorepo to the next level.
Context
I had a two-years old monorepo built with CRA and managed with yarn workspaces. The repo structure was pretty simple, just a /packages directory, each one with a project (in this case, /admin and /app )and also a /lib directory which contains all shared components and utility files.
/
-- ./ github
-- / packages
---- / admin
------ / public
-------- index.html
------ / src
------ .env
------ config-overrides.js
------ package.json
---- / app
---- / lib
------ /components
------ /assets
------ /api
-- package.json
-- README.md
-- yarn.lock
Using customize-cra and react-app-rewired packages allowed me to customize the dev compiling process in my config-overrides.js.
Initial setup
The first thing I did was to install Vite and related packages using yarn in my root directory:
yarn add -W vite @vitejs/plugin-react
Second, create a vite.config.js in each project's root directory:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig(() => {
return {
build: {
outDir: 'build',
},
plugins: [react()],
};
});
This code is implementing Vite's React Plugin to define a building setup. I decided to keep build as my directory for bundled assets as its already defined in my Github actions.
Next, I needed to modify my index.html to match Vite requirements. I started by moving my index.html from /public of each one of my project's root and added a script tag with my JS entry point as my src attribute.
<html lang="en">
<head>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Admin</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<div class="loader-container">
<div class="loader">Loading...</div>
</div>
</div>
+ <script type="module" src="/src/index.jsx"></script>
</body>
</html>
I am accustomed to JSX files, and also have a small utility to rename all file name extensions so it wasn't a problem for me to rename all files from .js to .jsx.
Then I replaced every instance of react-app-rewired with Vite custom commands on my npm scripts for every project in the /projects directory. For example:

Custom configurations
At the first server start, I saw on my console a strange error I have never seen before: Uncaught SyntaxError: ambiguous indirect export: default. This time I forgot I had a bunch of SVG assets inside the lib directory.
Solution was simple: for each vite's config file I added support for SVG files with vite-plugin-svgr:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
+ import svgr from 'vite-plugin-svgr';
export default defineConfig(() => {
return {
build: {
outDir: 'build',
},
plugins: [
react(),
+ svgr({ svgrOptions: { icon: true } }),
],
};
});
Another important step during the migration was to replace all process.env in all sites of the project with import.meta.env, and as I was using environment variables on each project, also I had to replace REACT_APP with VITE. In the following example, my configs.js file inside the shared directory.

Also, in order to point static files served by Vite in /public directory, I removed all occurrences of %PUBLIC_URL% in index.html file.

Finally, removed all previous packages used before.
yarn remove -W react-scripts customize-cra react-app-rewired
Final thoughts
Migrating from create-react-app to Vite was a pretty straightforward process and there are plenty of guides out there, but with a monorepo I thought things were going to be different. Surprisingly it wasn't and the steps were as simple as in a normal repository. Here is the final structure of the repository. Note that there is no config-overrides.js as it is unnecessary now.
/
-- ./ github
-- / packages
---- / admin
------ / public
------ / src
------ .env
------ index.html
------ vite.config.js
------ package.json
---- / app
---- / lib
------ /components
------ /assets
------ /api
-- package.json
-- README.md
-- yarn.lock
If you found this post useful and would like to stay updated on my work, feel free to connect with me on any of my social profiles.
Share and comment your thoughts on it for more content like this. Thanks!


