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:

  "packages": ["apps/*"],
  "npmClient": "yarn",
  "useWorkspaces": true,
  "version": "0.1.0"

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:

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

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

    "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”

lerna 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

"compilerOptions": {
    "jsx": "react",
    "baseUrl": ".",
    "paths": {
      "@myorg/my-util": ["../my-util/src/myorg-myutil"]

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:

return merge(defaultConfig, {
    externals: {
      "react": "react",
      "react-dom": "react-dom",
      "@myorg/my-util": "@myorg/my-util"

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.

<% if (isLocal) { %>
    <script type="systemjs-importmap">
      "imports": {
        "@myorg/mfe1": "//localhost:8501/myorg-mfe1.js",
        "@myorg/mfe2": "//localhost:8502/myorg-mfe2.js",
        "@myorg/my-util": "//localhost:8503/myorg-myutil.js"
<% } %>

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.