In a monorepo project, we may have dozens of lib modules, and lib modules that need to be published outside of monorepo must be packaged as js, with main/module pointing to the packaged js file so that everyone can use it.

For example, a simple lib module like the following

  • lib-a
    • src
    • README.md
    • package.json
    • tsconfig.json

The package.json may point directly to the source code when the original release is not needed

1
2
3
4
5
6
{
  "name": "lib-a",
  "main": "src/index.ts",
  "module": "src/index.ts",
  "types": "src/index.ts"
}

And when published, it needs to be modified to

1
2
3
4
5
6
{
  "name": "lib-a",
  "main": "dist/index.js",
  "module": "dist/index.esm.js",
  "types": "src/index.ts"
}

This leads to the need to add at least one setup script for someone pulling the project for the first time to perform the initialization actions for all modules in bulk. For example, the pnpm command might be pnpm --filter . run setup

1
2
3
4
5
{
  "scripts": {
    "setup": "npm run build"
  }
}

If there are only one or two modules, then it probably won’t take much time. But if there are dozens of modules (in our production project, there are about 37), even if the initial build of a module takes only a few seconds, it will cumulatively take several minutes. There are many current approaches

Since most of my web projects are based on vite, I considered creating a vite/rollup plugin to rewrite the module resolve, rewriting the imported modules to point directly to the source code instead of dist/index.js, even though this would increase the development time of each module, but on average each module depends on less than 10 other even though this adds time to the development of each module, the additional time is almost insignificant when averaging out the 10 or so other libs that each module depends on (mostly in a single nodejs process and compiled with esbuild).

Implementation

Before implementing it myself, I also retrieved existing plugins like @rollup/plugin-alias, but its configuration is static, for example my generation needs to configure @liuli-util/* all pointing to @liuli-util/*/src/index.ts, which needs to be configured separately for each module.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { defineConfig } from "rollup";
import alias from "@rollup/plugin-alias";

export default defineConfig({
  plugins: [
    alias({
      entries: [
        {
          find: "@liuli-util/async",
          replacement: "@liuli-util/async/src/index.ts",
        },
        {
          find: "@liuli-util/array",
          replacement: "@liuli-util/array/src/index.ts",
        },
        // 可能还有更多
      ],
    }),
  ],
});

And I wanted to focus on doing that, so I developed a separate plugin rollup-plugin-ts-alias

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { Plugin, ResolveIdResult } from "rollup";
import { pathExists } from "fs-extra";

export function tsAlias(
  includes: (string | RegExp)[],
  excludes: (string | RegExp)[] = []
): Plugin & { enforce: "pre" | "post" } {
  return {
    name: "rollup-plugin-ts-alias",
    enforce: "pre",
    async resolveId(source: string): Promise<ResolveIdResult> {
      excludes.push(/\/.*\//);
      const predicate = (item: string | RegExp) =>
        typeof item === "string" ? source.startsWith(item) : item.test(source);
      if (includes.some(predicate) && !excludes.some(predicate)) {
        let res: string;
        try {
          res = require.resolve(source + "/src/index.ts");
        } catch (e) {
          return null;
        }
        if (!(await pathExists(res))) {
          console.warn("path not exists: ", res);
          return null;
        }
        console.log("rewrite: ", res);
        return res;
      }
      return null;
    },
  };
}

Use

The plugin is published to npm @liuli-util/rollup-plugin-ts-alias

Install

1
pnpm i -D @liuli-util/rollup-plugin-ts-alias

Configuration

1
2
3
4
5
6
// vite.config.ts
import { tsAlias } from "@liuli-util/rollup-plugin-ts-alias";

export default defineConfig({
  plugins: [tsAlias(["@liuli-util/"])],
});

After that, the source code of the lib can be hot updated by modifying it directly in monorepo, without the need to start an additional terminal or add the setup command for all initialization. As you can see below, the dependent lib @liuli-util/react-router is already pointed to the source code

image