All Articles

Configuring Create React App To Load External Repositories

configuring-cra-to-load-external-repositories-cover

Cover image: Bunbeg Beach, Co. Donegal, Ireland

When I started writing React applications all the codebase was under a single repository. No code sharing, no context separation.

As soon as I gain interested in exploring new solutions, I wanted to build a small dashboard at home as a playground to test new libraries, React hooks or integration with other frameworks like Vue.

Creating the Dashboard backbone was a straightforward operation: a few components for the scaffold, a basic authentication module, a Routes component with few switch cases and an apps folder to contain the different projects.

Coming from a Python/Django background, I wanted to organize my code in different repositories as I was doing with all my previous projects. Unfortunately this operation has not been as straightforward as expected.

In Python there are a couple of tools which I am familiar with and that helped me to manage this requirement: virtualenvwrapper is one of these. One of its functionalities (add2virtualenv) is the ability to link different repositories together under the same environment and still being able to modify them without any re-installation or deployment - another option would be pip install -e from the repository folder.

Unfortunately, it’s not the same with npm/yarn and create-react-app; they both allows to link but each repository must resolve it’s own dependencies and have them installed.

Project structure

The currenct project structure is based on the standard create-react-app example.

package.json
src/
    apps/
        ...
    libs/
        ...
    scaffold/
        ...
    App.js
    index.js

My idea was to share the code inside libs folder to all the applications and to keep each application in it’s own repository.

The first thing I tried was yarn link, but it didn’t worked well. This approach assumes that the code inside the application is already packaged with it’s own dependencies resolved.

Something was missing!

Avoid ejecting

The first thing I did was to eject to explore the configuration opportunities. This is actually a good way to test out configurations if you can revert (which is pretty easy if you are under a version controlled folder).

Ejecting (and keeping each config file) however is not my preferred solution because when you go manual for a long period and starts to customize a lot of things you can’t easily go back, and you need to maintain the dependencies one by one.

On the opposite side, create-react-app does not allow you to modify the configuration files in order to keep the project as generic and standard as possible.

Few solutions has been suggested on the web: switch to razzle.js, use next.js, rethink the project structure with lerna or fork create-react-app.

In order to keep the project simple I didn’t want to introduce next.js or razzle.js. I hope to reuse my dashboard code for other projects and using a framework might not be the best solution.

Fork

Of all solutions I opted for forking create-react-app repository, following this guide.

On react-scripts package, I added the following lines to config/webpack.config.js.

  • saving the content to a variable instead of returing the configuration directly:
// remove the return statement and save the content
-  return {
+  let config = {
     mode: isEnvProduction ? 'production' : isEnvDevelopment &&  'development',
  • checking if a file called customWebpack.config.js exists in the project root folder and, if it has a modify function, override the config with the function’s result:
     // our own hints via the FileSizeReporter
     performance: false,
   };
+  console.log('Checking for custom webpack config');
+  if (fs.existsSync('./customWebpack.config.js')) {
+    console.log('  -- custom config found!');
+    const customWebpackConfig = require(path.resolve(
+      __dirname,
+      '../../../../customWebpack.config'
+    ));
+    if (customWebpackConfig.modify) {
+      config = customWebpackConfig.modify(config, { webpackEnv });
+    }
+  }
+
+  return config;
};

This similar approach is used by razzle (in a much fancier way).

On my project then I had to do three things:

  • Adding a customWebpack.config.js file:
module.exports = {
    modify: (config, { webpackEnv }) => {

        // List of external repositories that have to be added
        // to the testers to being correctly processed       
        let externalRepositories = [];
        if (process.env.REACT_APP_EXTERNAL_REPOSITORIES) {
            externalRepositories = process.env.REACT_APP_EXTERNAL_REPOSITORIES.split(',');
        }

        // Set a list of repositories required for this project
        const projectRepositories = [ 
            'my-test-repo'           
        ];

        // Validate that all repositories have been set before starting
        projectRepositories.forEach(repo => {
        if (externalRepositories.filter(eRepo => eRepo.endsWith(repo)).length !== 1)
            throw new Error(`==> Repository ${repo} must be included in ` +
                `.env.local REACT_APP_EXTERNAL_REPOSITORIES variable`);
        });

        config.module.rules[2].oneOf.forEach((test, index) => {
        if (test.include) {
            config.module.rules[2].oneOf[index].include = [
            ...(Array.isArray(test.include) ? test.include : [test.include]),
            ...externalRepositories,
            ];
        }
        });
    }
    return config;
};
  • add the repositories to REACT_APP_EXTERNAL_REPOSITORIES in .env.local file:
REACT_APP_EXTERNAL_REPOSITORIES=~/repositories/my-test-repo
  • and finally created a link
ln -s ~/repositories/my-test-repo dashboard-ui/src/apps/

Final considerations

This approach is not a standard approach in JS/React applications/modules development, but allows me to have the advantages I have when I develop python applications:

  • every module is in it’s own repository: different repositories might have different visibility, permissions, follow different merging strategies and have it’s own wiki (or even different team);
  • changes are picked up immediately by CRA without extra compile steps;
  • the application is bundled from one single point (however, this is also a disadvantage in terms of CI/CD, but node_modules can be cached to speed up build operations).

There are also some disadvantages, but for my pipeline they are not a problem:

  • as mentioned, one change in a single repository requires the full application to be bundled again. A mixed approach can be adopted by adding the external repositories only in development leveraging webpackEnv variable, building single modules with their own pipeline;
  • setup for a new repository is not straightforward: we need to create a link and add the repository to an env variable (this might be automated as well), and we need to do this in order to build.

Can you see a simpler way to achieve this result? You think this is a terrible approach? Please share your opinion!