Cover photo by Marc Sendra Martorell on Unsplash
Resources
- https://github.com/tolu/test-vite-monorepo (example repo)
- https://vitejs.dev/ (documentation)
- workspaces-run to run same task in all packages from root (might not need after npm 7.7.0)
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
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
andcra
dependencies -
lerna
deps and configs -
node-sass
, it's deprecated and we've had issues withnode-gyp
, we'll replace this with the officialsass
-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/*" ]
}
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 thatimport { 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()],
})
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 }
>;
}
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
Escape return value of some functions
@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:
@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
:
// 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 intsconfig.json
must be set toesnext
,es2020
orsystem
.
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"
}
}
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;
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
:
// 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 😍🙌
Top comments (3)
Did you find a nice alternative to Storybook?
Sorry for the late reply, but no I never did but I wasn't really looking either 😌 storybook works just fine as is.
Looks like vite is supported now too for Storybook - storybook.js.org/blog/storybook-fo... !