How to share React components in monorepo, by source?

Viewed 1020

I have a monorepo containing 3 different React projects:

- SHARED_COMPONENTS
  src
    Button.tsx
  package.json
- APP_A
  src
    main.ts
  package.json
- APP_B
  src
    main.ts
  package.json

How can I import the Button(.tsx) module from both main.ts files? E.g. I want to define my shared components inside the SHARED_COMPONENTS folder, and use these components inside APP_A and APP_B.

I do not want to compile the Button (and share it with tools like bit.dev or npm registry). I want to include the source file of Button.tsx, and let each individual build script (the ones for APP_A, and APP_B) compile/transpile the source of Button.tsx.

In other words, Button.tsx should become part of the source code of f.e. APP_A. I do not want to include it as a compiled component.

All projects (SHARED_COMPONENTS, APP_A, APP_B) are created with CRA. But if necessary the webpack config can be overridden, or we can eject and go manual.

I believe the main problem is that ../SHARED_COMPONENTS lays outside of the src directory for each individual APP project, and hence we cannot include the source code from shared components.

Are there solutions for this? Links to a guide or a thorough explanation would be much appreciated. I've tried searching extensively but the material I find often suggests packaging the shared components and share them with bit.dev/npm registry, which is what we don't want. We try this at the moment, and it's very cumbersome for the development cycle.

2 Answers

I have used craco to solve issues like this in the past. The general idea was to have craco modify the webpackConfig to include the directory of your SHARED_COMPONENTS in babel-loader's setup. You also need to add an alias for your SHARED_COMPONENTS so that you can import them easily. Here is what it might look like:

Follow the directions to install craco from its README: https://www.npmjs.com/package/@craco/craco#installation

Then install craco-babel-loader. This will help you add files outside of your /src directory for compilation despite CRA's default rules. npm install craco-babel-loader

Then go to your craco.config.js file and make the following changes

// APP_A/craco.config.js

const path = require("path");
const fs = require("fs");
const cracoBabelLoader = require("craco-babel-loader");

/**
 * CRA has a ModuleScopePlugin that prohibits imports outside of the /src
 * directory. If you have code outside this directory (e.g. components shared
 * between multiple projects) then this becomes an issue. This plugin takes
 * options: {path: "/asbolute-path", name: "alias-you-want"} NOTE: For this to
 * work, the directory that you point to must have a package.json file
 */
const enableImportOutsideSrcDir = {
  overrideWebpackConfig: function enableImportOutsideSrcDir({
    webpackConfig,
    pluginOptions,
    context: { paths, name },
  }) {
    const absolutePath = path.join(paths.appPath, pluginOptions.path);
    const moduleScopePlugin = webpackConfig.resolve.plugins.find(
      (plugin) => plugin.appSrcs && plugin.allowedFiles
    );

    if (moduleScopePlugin) {
      moduleScopePlugin.appSrcs.push(absolutePath);
    }

    webpackConfig.resolve.alias = Object.assign(webpackConfig.resolve.alias, {
      [pluginOptions.name]: absolutePath,
    });

    return webpackConfig;
  },
};

// Handle relative paths to sibling packages
const appDirectory = fs.realpathSync(process.cwd());
const resolvePackage = (relativePath) =>
  path.resolve(appDirectory, relativePath);

module.exports = {
  plugins: [
    {
      // enable compilation for the following directories outside of CRA's /src
      plugin: cracoBabelLoader,
      options: {
        includes: [resolvePackage("../SHARED_COMPONENTS")],
      },
    },
    {
      // now that we're compiling these files, enable importing them
      plugin: enableImportOutsideSrcDir,
      options: { path: "../SHARED_COMPONENTS", name: "sharedcomponents" },
    },
  ],
};

One useful thing to note is that SHARED_COMPONENTS must have its own package.json. This is true even though it has no dependencies and you're directly importing the source files. Otherwise you'll get errors when trying to run npm run start complaining that ModuleScopePlugin threw an error.

I had to repeat this setup for each project. So you'd install craco and the above craco.config.js for APP_A and APP_B.

I personally plan now to have my APP_A and APP_B together in one react app with a switch close to the root deciding on which one is enabled based on an environment variable. That's after trying to have a go at this same exact problem several times in the last two years.

CRA can't include files outside its project as you mentioned, and as I recall there are whole frameworks being developed by 3rd parties to have JS monorepos working, including React code sharing, but none seemed mainstream and mature enough for me to commit to.

So I will trade bundle size (not a concern for our small web apps) and just have one CRA app.

(I wanted to comment but I don't have enough reputation for it.)

Related