Published on

Using Lerna with Create React App Babel and a Server

Authors

In a previous post I spoke about how I managed to get Lerna to play nicely with Create React App. In that same post, I also mentioned how I could not work out how to get normal server-side Node to use shared modules in the mono repo.

Today I finally managed to work it out and have created a sample repo that demonstrates how. This sample repo has one very simple shared repo that has a reusable React component Heading.jsx and some shared constants stored in common.js.

Heaing.jsx is a super simple React component:

import React from 'react'

function Heading({ level = '1', title }) {
  return React.createElement(`h${level}`, {}, title)
}

export default Heading

While const.js contains a few constants we want to share between our server and ui:

export const FOO = 'FOO'
export const BAR = 'BAR'

The Heading component and constants are used by our cra-1 repo's App.js as below:

import React from 'react'
import logo from './logo.svg'
import './App.css'
import { FOO, BAR } from '@yourproject/common/src/const'
import Heading from '@yourproject/common/src/Heading'

function App() {
  return <Heading title={`Hello, World!!!! ${FOO} - ${BAR}`} />
}

export default App

The constants are used by our server in the users route:

import express from 'express'
import { FOO } from '@yourproject/common/src/const'

const router = express.Router()

/* GET users listing. */
router.get('/', function (req, res, next) {
  res.send(`respond with a resource: [${FOO}]`)
})

module.exports = router

Two Key Problems to Resolve

There were 2 main issues that I ran into which I finally solved:

  1. In Babel 7 how do you configure it and your monorepo?
  2. How do you access files from the common mono repo module from server-side node repos?

In Babel 7 how do you configure it and your monorepo

The first problem took a bunch of digging and trial and error to get to but the eventual project structure looked as follows:

packages/
                -> cra-1/
                            -> README.md
                            -> package-lock.json
                            -> package.json
                            -> public/
                              ...
                            -> src/
                                                  -> App.css
                                                  -> App.js
                                                  ...
                                                  -> serviceWorker.js
                -> common/
                            -> README.md
                            -> __tests__/
                              ...
                            -> package-lock.json
                            -> package.json
                            -> src/
                                    -> Heading.jsx
                                    -> const.js
                                    -> lib/
                                                    -> common.js
                -> server/
                            -> bin/
                            ...
                            -> package-lock.json
                            -> package.json
                            ...
                            -> registerbabel7.js
                            -> src/
                                    -> app.js
                                    -> routes/
                                              -> index.js
                                              -> users.js
                                    -> views/
                                              -> error.hbs
                                              -> index.hbs
                                              -> layout.hbs
babel.config.js
lerna.json
package-lock.json
package.json

The first thing to do is install the required babel dependencies in the root package.json as well as all sub-modules:

npm install --save-dev @babel/cli @babel/core @babel/node @babel/plugin-proposal-class-properties @babel/preset-env

We now need to define our babel.config.js in the root of the monorepo as follows:

module.exports = function (api) {
  console.log('-- root babel.config.js')
  api.cache(true)
  return {
    presets: ['@babel/preset-env', '@babel/preset-react'],
    plugins: ['@babel/plugin-proposal-class-properties'],
  }
}

This babel file is a Babel 7 feature. There is a console.log in it so that we can confirm that our sub-modules are using the root babel config file.

We do this by configuring any submodule calls to babel to use the --root-mode upward flag to indicate to babel that it must not look in the directory where it is being run from for the Babel config file but rather look up the project structure until it finds a babel config.

How do you access files from the common mono repo module from server-side node repos

This was not an issue at all in CRA, I had to change nothing there. The server side, on the other hand, proved to be really tricky. After much googling and digging through Github issues I came upon a solution. The solution involves updating any usages of babel-node and babel in your package.json scripts section to include the flags:

  • --root-mode upward
  • --ignore 'node_modules'

The scripts section of my server package.json is below:

"scripts": {
    "start": "nodemon --exec 'babel-node --ignore 'node_modules' --root-mode upward ./src/app.js'",
    "build": "babel src --out-dir dist --root-mode upward --ignore **/*.story.js,**/*.spec.js",
    "debug": "DEBUG=server:* npm start",
    "some:script": "babel-node --root-mode upward --ignore 'node_modules' --presets @babel/env ./src/scripts/script1.js",
    "test": "mocha --require registerbabel7.js 'src/**/**spec.js'"
  },

Checking It All Works

I updated the root package.json scripts section to use Lerna to manage the whole process from the root of the project. These scripts are below:

  "scripts": {
    "bootstrap": "lerna clean -y && lerna bootstrap --hoist",
    "build": "lerna exec --parallel -- babel --root-mode upward src -d lib --ignore **/*.story.js,**/*.spec.js",
    "start": "lerna bootstrap && lerna run start --stream"
  },

When the monorepo is first cloned we run:

## as you usually would
npm install

## run the bootstrap script to get lerna to bootstrap the monorepo
npm run bootstrap

Both the ui and the server can be run together using lerna by running the start script:

npm run start

Which outputs:

> root@ start ~/lerna-create-react-app-babel7
> lerna bootstrap && lerna run start --stream

lerna notice cli v3.14.1
lerna info Bootstrapping 3 packages
lerna info Symlinking packages and binaries
lerna success Bootstrapped 3 packages
lerna notice cli v3.14.1
lerna info Executing command in 2 packages: "npm run start"
server: > [email protected] start ~/lerna-create-react-app-babel7/packages/server
server: > nodemon --exec 'babel-node --ignore 'node_modules' --root-mode upward ./src/app.js'
cra-1: > [email protected] start ~/lerna-create-react-app-babel7/packages/cra-1
cra-1: > react-scripts start
server: [nodemon] 1.19.1
server: [nodemon] to restart at any time, enter `rs`
server: [nodemon] watching: *.*
server: [nodemon] starting `babel-node --ignore node_modules --root-mode upward ./src/app.js`
server: -- root babel.config.js
server: Server started on [http://localhost:3002]
cra-1: Starting the development server...

The ui renders correctly:

hello-world-using-common-heading-component-and-shared-consts

The /users route on the server also returns the correct output:

users-route-using-shared-const

As mentioned at the beginning of this post a sample working repo of what was described above can be found here.