Single SPA, Lerna, Typescript and shared utility modules

I’ve been playing with using Lerna to manage a monorepo for my single-spa project. It seems like a really good fit so far. One stumbling block I encountered was implementing a single-spa utility module (in this case a styleguide/component library).

A quick overview of the setup. First the lerna.json file:

1{
2  "packages": ["apps/*"],
3  "npmClient": "yarn",
4  "useWorkspaces": true,
5  "version": "0.1.0"
6}

Each of the micro frontends (MFEs) is in a subdirectory under apps. The package.json for each has a start command with a different port:

1"start": "webpack serve --port 8501"

The root package.json uses Lerna to start all the micro frontends in parallel:

1"start": "lerna run start --parallel"

So far so good, and much easier than checking out several different projects and remembering to run start on all of them.

Typescript woes

I created a new single-spa utility module in the apps directory, using the single-spa CLI and selecting util-module as the module type.

First I installed the util module as a dependency in the other micro frontends. The lerna add command “Adds package to each applicable package. Applicable are packages that are not package and are in scope”

1lerna add @myorg/my-util --ignore @myorg/root-config

However, when I imported anything from the util-module into any of the MFEs, typescript was not happy:

TS2307: Cannot find module '@myorg/my-util' or its corresponding type declarations.

I read a few suggestions about needing to specify a “main” in package.json for the util-module (I don’t want to depend on the built module) and generating type declaration files (I think the ts-config-single-spa config already does this). In the end the answer was to add a paths element to the compilerOptions in the tsconfig for each MFE

1"compilerOptions": {
2    "jsx": "react",
3    "baseUrl": ".",
4    "paths": {
5      "@myorg/my-util": ["../my-util/src/myorg-myutil"]
6    }
7  },

This made my IDE happy!

Configuring as a shared dependency

Because this is a shared utility module, I don’t want to bundle it with every MFE. So I added it to webpack externals for each MFE:

1return merge(defaultConfig, {
2    externals: {
3      "react": "react",
4      "react-dom": "react-dom",
5      "@myorg/my-util": "@myorg/my-util"
6    }
7  });

And added it to the import map in the root config. Note that there is no need to call registerApplication() for the util-module because it won’t be loaded independently.

1<% if (isLocal) { %>
2    <script type="systemjs-importmap">
3    {
4      "imports": {
5        "@myorg/mfe1": "//localhost:8501/myorg-mfe1.js",
6        "@myorg/mfe2": "//localhost:8502/myorg-mfe2.js",
7        "@myorg/my-util": "//localhost:8503/myorg-myutil.js"
8      }
9    }
10  </script>
11<% } %>

And that’s it – all working in a development environment. The next step will be to look at how to hook up to Lerna’s version management abilities so I can test and deploy micro frontends independently.

One thought on “Single SPA, Lerna, Typescript and shared utility modules

Leave a Reply

Your email address will not be published. Required fields are marked *