DEV Community

shripada
shripada

Posted on

Building a TypeScript library in a mono repo setup

TypeScript library

Creating a library is like assembling a toolbox of code snippets. These can be shared across multiple apps - whether they're backend-heavy, frontend-centric, or a blend of both. You might want to share a clever string processing script, a custom algorithm, or a unique data structure.

Libraries let you package and share these resources efficiently, reducing repetition and keeping your codebase streamlined. TypeScript is a popular language that encourages writing safe code with great developer experience. In this article, we'll delve into the creation and usage of a TypeScript library within a mono repository.

This setup hosts multiple apps, providing the convenience to develop and locally test libraries in concert with these apps.

Prerequisites

Create a mono repo using Turbo build system.

npx create-turbo@latest
Enter fullscreen mode Exit fullscreen mode
  • When prompted give the name to the monorepo. We shall give test-lib
  • Choose pnpm package manager as it has a very good support for linking to libs locally.
  • once monorepo is created open it in vs code:
cd test-lib
code .
Enter fullscreen mode Exit fullscreen mode

Creating the library using Vite

We shall use a modern tooling called Vite to create and setup our library.

  • fire up a terminal inside vs code.
  • cd into the packages folder inside monorepo and run:
cd packages
npm create vite@latest
Enter fullscreen mode Exit fullscreen mode
  • When it prompts for name, give test-lib
  • and choose Vanilla framework,
  • and TypeScript variant.
  • Once you press Enter, you should see the test-lib project created in packages folder.

Examine the vanilla test-lib package created

  • cd test-lib
  • Examine the folder structure
  • We have a basic venilla js project.
  • first, install dependencies. pnpm install
  • We can build this package using pnpm build, and it will create a built web site in /dist folder.
  • But we want a library and not the website. For that we need to configure the Vite to convert the package into a library

Convert the Vanilla project into a library

  • First delete files in the /src folder except vite-env.d.ts
  • Delete the public folder as well.
  • Create a file called math.ts with following content:
function add(a: number, b: number): number {
  return a + b;
}

export { add };

Enter fullscreen mode Exit fullscreen mode
  • Create another file called index.ts and add the following code:
export { add } from './math';
Enter fullscreen mode Exit fullscreen mode
  • Now we need to convert this into a library. Vite supports a library mode.
  • Create a vite.config.js file in the root of test-lib package
  • Copy the Vite config in the documentation above to our config file. Remove the rollup object from it.
  • make the lib object to look like so where we have given our library name as TestLib, and file name as test-lib:
    lib: {
      // Could also be a dictionary or array of multiple entry points
      entry: resolve(__dirname, 'lib/main.js'),
      name: 'TestLib',
      // the proper extensions will be added
      fileName: 'test-lib',
    },
Enter fullscreen mode Exit fullscreen mode
  • If you try building now, it will fail complaining the mentioned entry point in config is not found. Lets fix that.
entry: resolve(__dirname, 'src/index.ts'),
Enter fullscreen mode Exit fullscreen mode
  • Now if you build using pnpm build, you should see the build library in /dist folder. You will see ES6, and UMD modules.

Configuring the vite to generate ES6 and common js

We want the output in two formats, one in ES6 for all modern browsers and runtimes. CommoJS for node run time.

  • In vite.config.js, we need to add formats in lib section:
formats:['es', 'cjs']
Enter fullscreen mode Exit fullscreen mode
  • Build and check if you see both es6 and CommonJS modules getting generated in /dist folder.

Integrate the library in the apps/web next js project.

  • Fire up another terminal for web app
  • cd apps/web
  • Run the app in dev mode pnpm dev and then hit http://localhost:3000
  • You should see the page with some text and button
  • we want to install our lib as a dependency to this app.
  • Run npm install test-lib
  • You will notice pnpm will end up installing an existing package from npm registry with name test-lib. We don't want that. To ensure we don't conflict with npm registry entry, create a unique name using namespace such as @abccomp
  • Change the name in packages/test-lib/package.json to @abccomp/test-lib
  • fire up another terminal and cd into apps/web
  • now install the @abccomp/test-lib by running
pnpm install @abccomp/test-lib
Enter fullscreen mode Exit fullscreen mode
  • pnpm is clever and it will create a symlink to to our local lib module and you can see it in its output:
+ @abccomp/test-lib 0.0.0 <- ../../packages/test-lib
Enter fullscreen mode Exit fullscreen mode

Use the library

  • open the app/page.tsx
  • import add function from our library
import { add } from '@abccomp/test-lib';
Enter fullscreen mode Exit fullscreen mode

and in page.tsx, add this below the :

  <div>{add(10, 30)}</div>
Enter fullscreen mode Exit fullscreen mode
  • When you go back to browser and check, you will see an error saying module not found.
  • This is because, the package.json of our library is not giving enough information as to where to find the real module code in the package.
  • lets do that now by referring this recommended package.json for lib.
  • Copy the following code into package.json of test-lib
"
  files": ["dist"],
  "main": "./dist/my-lib.umd.cjs",
  "module": "./dist/my-lib.js",
  "exports": {
    ".": {
      "import": "./dist/my-lib.js",
      "require": "./dist/my-lib.umd.cjs"
    }
  }
Enter fullscreen mode Exit fullscreen mode
  • modify main to point to the common js, and module to es6 module paths generated in the dist folder. Also fix the export entries. Now the entries should look like:
 "files": [
    "dist"
  ],
  "main": "./dist/test-lib.cjs",
  "module": "./dist/test-lib.js",
  "exports": {
    ".": {
      "import": "./dist/test-lib.js",
      "require": "./dist/test-lib.cjs"
    }
  },
Enter fullscreen mode Exit fullscreen mode
  • Now go back in your browser and check the page, it should be displaying 35. Great! Library integration works successfully!.

Generating type declaration d.ts file for VSCode to help with type completion with our code.

You might observe that when we try to use our add function in the app, we don't get any code completion hints. This is because, VSCode is not able to locate the type declarations for our library. Generally type declarations are made available in the form of <module-name>.d.ts files right along with our generated modules.

Vite can help us to generate these via tsc using a plugin called vite-plugin-dts. Lets install that plugin

  • Install the vite-plugin-dts in test-lib package
    pnpm i vite-plugin-dts -D
Enter fullscreen mode Exit fullscreen mode
  • in vite.config.js, import the dts plugin import dts from 'vite-plugin-dts' and add this in the config: plugins:[dts()]
  • the config should look like so:
import { resolve } from 'path';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'TestLib',
      // the proper extensions will be added
      fileName: 'test-lib',
      formats: ['es', 'cjs'],
    },
  },
  plugins:[dts()]
});

Enter fullscreen mode Exit fullscreen mode
  • Now if we build the library using pnpm build we should see the d.ts files getting created inside /dist folder.
  • We are not done yet!. We need to add a types entry into package.json of test-lib as well. Add the following to the package.json of the test-lib:
"types": "./dist/index.d.ts"
Enter fullscreen mode Exit fullscreen mode

Now go back to the app, and observe that VSCode no longer warns on not finding types. And also if you will start seeing type hints when you type the function name add in the VSCode editor.

Integrating library into a node js project as ESM and also CommonJS package.

Our library can be used by even a backend code written in node. You can also integrate the library even from a JavaScript code. To experiment this, lets go ahead and create a node app inside /apps folder in our mono-repo. Run the following commands.

cd apps
mkdir node-app
cd node-app
pnpm init
touch index.js
touch index.cjs
pnpm install @abccomp/test-lib
Enter fullscreen mode Exit fullscreen mode

When we run the above commands, we should see package.json, and index.js, and index.cjs files created inside node-app folder.
If you inspect the package.json, you will see an entry:

  "type": "module"
Enter fullscreen mode Exit fullscreen mode

Which indicates, by default all js files are ESM modules. However we can tell a particular file is a common js module, by giving cjs extension.

Add the following code to index.js

import { add } from '@codecraft/test-lib';
console.log(add(40, 50));
Enter fullscreen mode Exit fullscreen mode

Add the following code to index.cjs file:

const { add } = require('@codecraft/test-lib');
console.log(add(19, 34));
Enter fullscreen mode Exit fullscreen mode

Observe how we use different ways of importing symbols in ESM, and CommonJS.

One nice thing about our library is it comes with type declarations, and even inside a JavaScript file, you will get nice code completion hints indicating each argument and type of it.

Test your node files and see both of them successfully use our library.

❯ node index.js
90
❯ node index.cjs
53
Enter fullscreen mode Exit fullscreen mode

unit testing typescript code using vitest

Before apps can integrate our libraries, it is important we test the code thoroughly. Vitest is a blazing fast unit test framework powered by Vite. we shall use this to write unit tests for our math module.

First, install vitest:

pnpm add -D vitest
Enter fullscreen mode Exit fullscreen mode

Create a test file called math.test.ts in the same src folder where math.ts is located. If you have already written jest tests in other projects, you already know how to write tests using vtest. Add the following code to math.test.ts

import { it, expect } from 'vitest';
import { add } from './math';

it('must add two integers correctly', () => {
  const actual = add(10, 25);
  const expected = 35;
  expect(actual).toEqual(expected);
});

Enter fullscreen mode Exit fullscreen mode

running tests

Add the following script to the package.json

test: vitest
Enter fullscreen mode Exit fullscreen mode

To run the tests, invoke

pnpm test
Enter fullscreen mode Exit fullscreen mode

you should see the console output that shows that tests are passing.

Debugging the code via tests.

Ability to debug our code in VSCode is vital to troubleshoot our code. Vite has very good out of the box support for enabling debugging. All you have to do is invoke the Javascript Debug Terminal in VSCode. Add some breakpoints in your test. In the command line of debug terminal, simply run

pnpm test
Enter fullscreen mode Exit fullscreen mode

You should see the breakpoint that you have set in your tests will become active with debugger pausing there.

Conclusion

In this article, we explored, how we can use a mono repo setup to create a TypeScript based library, and demonstrate how to integrate it in a next js app, and also a node application. In the process, we got to know how the tools like Turbo, pnpm, Vite and Vitest can facilitate a comfortable developer experience in building and testing the library.

Github source of the built repo

References:

Top comments (0)