ViteJs - replacing create-react-app in a monorepo
Tags: vite, npm, javascript, webdev
Published: 2021-03-08 09:20:26

Cover photo by <a href="https://unsplash.com/@marcsm?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Marc Sendra Martorell</a> on <a href="https://unsplash.com/s/photos/speed?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>

**Resources**
- https://github.com/tolu/test-vite-monorepo (example repo)
- https://vitejs.dev/ (documentation)
- [workspaces-run](https://www.npmjs.com/package/workspaces-run) 
 to run same task in all packages from root (might not need after [npm 7.7.0][npm7changes])

## 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] (_cra_), [lerna][lerna] and [craco][craco] by leveraging [npm 7 workspaces][npmws] and [vite][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
```

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][vite] 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:

```json
{
 "worskpaces": [ "apps/*", "apps/tooling/*" ]
}
```

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.

```ts
// 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()],
})
```

#### 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)

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

### 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
```diff
- @warn 'bla,
-        di bla';
+ @warn 'bla, di bla
```

Escape return value of some functions
```diff
@function pagePercentageMargins($percentage) {
-   @return (0.5vw * #{$percentage});
+   @return (#{(0.5 * $percentage)}vw);
}

```

### 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'
```
, and `@imported` using `sass-loader` by prepending a tilde sign:
```scss
@import '~COMMON_COLORS';
```

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`:

```diff
// 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'),
+    }
+  },
})
```

> 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 👀):
```json
{
  "scripts": {
    "start": "cross-env CI=true REACT_APP_VERSION=$npm_package_version craco start"
  }
}
```

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
```ts
const version = process.env.REACT_APP_VERSION;
```

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
```
Then we'll update our config to provide a global `pkgJson` variable that we can use "as-is" instead of via `import.meta.env`:
```diff
// 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 }
+  }
})
```

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.
```

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

[cra]: https://github.com/facebook/create-react-app
[lerna]: https://github.com/lerna/lerna
[craco]: https://github.com/gsoft-inc/craco
[npmws]: https://docs.npmjs.com/cli/v7/using-npm/workspaces
[vite]: https://vitejs.dev/
[npm7changes]: https://github.com/npm/cli/releases/tag/v7.7.0