DEV Community

Cover image for ViteJs - replacing create-react-app in a monorepo
Tobias Lundin
Tobias Lundin

Posted on • Updated on

ViteJs - replacing create-react-app in a monorepo

Cover photo by Marc Sendra Martorell on Unsplash

Resources

Premise

The aim is to reduce complexity (nr of deps etc) and increase inner-loop-speed in a monorepo at work using create-react-app (cra), lerna and craco by leveraging npm 7 workspaces and vite.

On weekdays I build a streaming-service webapp

Our original setup

We started out with something like this, a lerna project with 2 cra-apps (App1 & App2), a common-package for shared components/styles with Storybook setup and some general purpose tooling packages.
The (not ejected) cra-apps use craco for editing the webpack config with extended contexts (to be able to require packages from outside of root dir) and setting up require-aliases (for sass imports) etc.

apps/
├──App1/
│  App2/
│  common/
│  tooling/
├───eslint-cfg
│   prettier-cfg
package.json
readme.md
Enter fullscreen mode Exit fullscreen mode

This setup works well enough but we've noticed some pain points:

  • it's a hassle to update react-scripts and we don't really want to eject since then we have to manage 400 lines of webpack config by ourselves 😅
  • cra requires configuration to work with monorepo
  • we don't really publish anything so lerna seems a bit overkill
  • a cold start (git clean -fdx && npm i && npm start) clocks in at around 3+min (npm start is ~1min)

We can do better! And hopefullly ViteJs is the answer!

Next gen frontend tooling 🙌

Cleaning up 🧹

First things first, let's get rid of everything we shouldn't need.

  • craco scripts, plugins and inside npm scripts
  • craco and cra dependencies
  • lerna deps and configs
  • node-sass, it's deprecated and we've had issues with node-gyp, we'll replace this with the official sass-package instead

Let's make it new 🔮

Time to see what we can do with new tooling!

Setup npm@7 workspaces

Configure workspaces in root package.json like so:

{
 "worskpaces": [ "apps/*", "apps/tooling/*" ]
}
Enter fullscreen mode Exit fullscreen mode

A quick npm i in the root and we're done. That was easy!

Add vite and configure for react

Add dependencies

  • vite
  • @vitejs/plugin-react-refresh
  • vite-plugin-svgr

vite-plugin-svgr is for importing .svg files as components so that import { ReactComponent as SvgIcon } from 'some.svg' keeps working

to App1 & App2 and create a basic configuration file vite.config.ts in each app-folder.

// vite.config.ts
import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'
import svgr from 'vite-plugin-svgr'

export default defineConfig({
  plugins: [reactRefresh(), svgr()],
})
Enter fullscreen mode Exit fullscreen mode

Fix svg component import's

Since we're importing svg's as components we now get a type error (for import { ReactComponent as SvgLogo } from '...') that can be fixed by adding this file to the root each app that imports svg's (i.e. where vite-plugin-svgr is used)

// index.d.ts
declare module '*.svg' {
  import * as React from 'react';
  export const ReactComponent: React.FunctionComponent<
    React.SVGProps<SVGSVGElement> & { title?: string }
  >;
}
Enter fullscreen mode Exit fullscreen mode

Add sass-package

Basically all we needed was to npm i -D sass in our app's, but for 2 issues in our *.scss-files since the sass-package is stricter on some things:

Remove multiline @warn statements

- @warn 'bla,
-        di bla';
+ @warn 'bla, di bla
Enter fullscreen mode Exit fullscreen mode

Escape return value of some functions

@function pagePercentageMargins($percentage) {
-   @return (0.5vw * #{$percentage});
+   @return (#{(0.5 * $percentage)}vw);
}

Enter fullscreen mode Exit fullscreen mode

Other issues to solve

Using and resolving aliases from common-folder

To be able to split configuration between our 2 apps we used aliases (standard webpack resolve aliases) set in each app-config that we could use when resolving @imports from scss-files in the common-folder (different theme colors etc).

Aliases in the webpack-config (via a craco-plugin) are defined like so:

COMMON_COLORS: 'path/to/colors.scss'
Enter fullscreen mode Exit fullscreen mode

, and @imported using sass-loader by prepending a tilde sign:

@import '~COMMON_COLORS';
Enter fullscreen mode Exit fullscreen mode

With vite and sass, the tilde isn't needed and alises can easily be added to the config. Notice the hack for __dirname here since we went for a module-ts-file as config instead of a plain commonJs:

// vite.config.ts
import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'
import svgr from 'vite-plugin-svgr'

+import { dirname, resolve } from 'path';
+import { fileURLToPath } from 'url';

+const __dirname = dirname(fileURLToPath(import.meta.url));

export default defineConfig({
  plugins: [reactRefresh(), svgr()],
+  resolve: {
+    alias: {
+      'COMMON_COLORS': resolve(__dirname, 'src/styles/colors.scss'),
+    }
+  },
})
Enter fullscreen mode Exit fullscreen mode

To avoid ts-errors when using import.meta the "module"-property in tsconfig.json must be set to esnext, es2020 or system.

Provide .env parameters

In our cra/craco-setup some variables were provided via .env files and some set directly in the npm-script (making for long scripts 👀):

{
  "scripts": {
    "start": "cross-env CI=true REACT_APP_VERSION=$npm_package_version craco start"
  }
}
Enter fullscreen mode Exit fullscreen mode

The default in a cra-setup is that all env-variables that begin with REACT_APP get's injected via webpack's define-plugin so you can use them in your scripts like this

const version = process.env.REACT_APP_VERSION;
Enter fullscreen mode Exit fullscreen mode

In vite the default is that you use import.meta.env to get at variables. Only variables that begin with VITE_ are exposed and variables are automatically loaded via dot-env from .env-files.

Personally I dont really like long npm-scripts so I'd rather move the version and name we're using into the configuration.

In order to get that working, let's add a .env-file first:

VITE_CI=true
Enter fullscreen mode Exit fullscreen mode

Then we'll update our config to provide a global pkgJson variable that we can use "as-is" instead of via import.meta.env:

// vite.config.ts
import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'
import svgr from 'vite-plugin-svgr'
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
+import { name, version } from './package.json';

const __dirname = dirname(fileURLToPath(import.meta.url));

export default defineConfig({
  plugins: [reactRefresh(), svgr()],
  resolve: {
    alias: {
      'SASS_VARIABLES': resolve(__dirname, 'src/styles/common-variables.scss'),
    }
  },
+  define: {
+    pkgJson: { name, version }
+  }
})
Enter fullscreen mode Exit fullscreen mode

Those were (almost) all the steps needed for us to convert from cra to vite, greatly improve install / startup speed and reduce complexity in a world that already has too much of just that 😉

Results

🍰🎉🚀

vite v2.0.5 dev server running at:

> Local:    http://localhost:3000/
> Network:  http://172.25.231.128:3000/

ready in 729ms.
Enter fullscreen mode Exit fullscreen mode

The ~1 minute startup time went down to sub-second 😍🙌

Discussion (3)

Collapse
redstuff profile image
Sveinung Tord Røsaker

Did you find a nice alternative to Storybook?

Collapse
tolu profile image
Tobias Lundin Author

Sorry for the late reply, but no I never did but I wasn't really looking either 😌 storybook works just fine as is.

Collapse
jamesg1 profile image
James G

Looks like vite is supported now too for Storybook - storybook.js.org/blog/storybook-fo... !