DEV Community

loading...
Cover image for ViteJs - replacing create-react-app in a monorepo

ViteJs - replacing create-react-app in a monorepo

tolu profile image Tobias Lundin Updated on ・5 min read

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 (2)

pic
Editor guide
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.