Monorepors - P.2 - How?
Hi, LinkedIn!
This is the third part of my article about monorepos. Hope, you've read parts one and two of this series. This time we will build the first package - the types/db-client.
Initiate new project
First things first. Navigate to types/db-client folder with the command line and init the new package.
npm init -y
Now, let's open the pacakge.json file and make some modifications
Change the name property to reflect that this packages belongs to the global monorepo project.
You can define the description to reflect the purpose of this package.
The bundledDependecies property is just a hint, that tells NPM explicitly which code (or which package names) this library exposes and shares locally in that exact monorepository or when publishing.
You can omit it, but I always recommend to provide NPM, Node, TypeScript with all hints possible to save on configuration costs of the project.
Add the tsconfig.json file to the project
Let's add the tsconfig.json file with the basic config.
I believe that tsconfig.json files should be kept as easy as possible. So, don't clutter them with the additional tweaks, unless you are absolutely required to do so. The bare minimum config I recommend is shown above.
The strict property should be always set to true. That helps a lot to avoid some rookie errors. You can read more about that flag here. As a summary - that flag enables all available strict checks in one shot.
For libraries I recommend to set the target option to the ES2015. More on that property is here and here. In short this will warn you, when trying to use the newer ES syntax in the older target environment. However, this property does not provide any code poly-fills (with this exception though), it just warns you! You can read more on that in the thread on stackoverflow.
Module resolution and Module options are all about development time only. Unless required, I recommend both options to be set to NodeNext always.
The outDir points to a folder, where the generated code should be placed.
The include property tells the tsc to lookup code only in that particular folder(s) and/or file(s). There is an alternative - you can provide the rootDir option for the same purpose. However, there is a number of cases, when you will need to use the include property instead of the plain rootDir. So, I consider the include option to be more robust way of selecting sources.
Before we continue, let's install the typescript, the @types/node packages at the root of the repository.
npm i typescript@latest @type/node -D
Add some code for deploy
Let's create the src and dist folders in the root of types/db-client and add some code to deploy.
Here we create two files - the User.ts and the main.ts. The User is a DB entity as you might guess and the main is just a container that exports all definitions in one place.
领英推荐
One quick note here: always use full names of imported files, including extension. Doing so will save a lot of your time and effort when combining things for bundling and transpilation.
The User exposes the interface and the class. The problem here is that when built, the definition of the interface will be wiped out by tsc, leaving a pure js code. But that is not what we need. The interface definition should go to a d.ts file and the class - to the "real" js file.
Setup code split and deploy
The ts code won't run in a browser environment as is - it needs to be converted to a regular js code. For this purpose you can use any bundling tool of your choice - WebPack, Rollup, Parcel, Gulp, Vite, esbuild, Grunt, etc...
DISCLAIMER: I prefer using the Vite because it is fast enough (not the fastest though) and easy to configure.
Navigate to the root of the repository with the command line and install the vite, the vite-dts-plugin and the rimraf packages.
npm i vite@latest vite-plugin-dts rimraf -D
Then we need to create the vite.config.ts with the following contents in the root of the types/db-client folder just next to the tsconfig.json and the package.json.
The build lib property orders Vite to switch to the library mode. That means that it won't look up for html files (that is a normal Vite's flow) but will take the entry file (or files, if an array of entries is passed) and convert it to the desired output format. In our case it is a 'es' and 'cjs' for ES and CommonJS formats respectively.
A by-product of the transpilation will be the main.d.ts file that is built thanks to the dts plugin. In short that plugin takes all d.ts files found in the dist folder after the build is done, combines those into a one big file, named as the main file is, and then removes them.
Now, we need to add the build command to the scripts section:
The rimraf call is used to clear the dist folder in one shot. The vite build performs the build of the library. Let's execute the build command and see what will be generated as a result.
The main.cjs - is a CommonJS file for old require syntax.
The main.js - is a ES module for modern import syntax.
The main.d.ts - contains all available declarations to be used by TypeScript when library's code is shared across the other packages. Without this file typescript will have no idea what library's code do and which objects and methods it exposes.
Now, we need to declare all that information in the package.json file.
Exposing library's code
Lets explore what has changed in the package.json file.
First is the type property that tells Node and NPM that this library contains the modern ES syntax modules.
The main property points to the CommonJS file for old require syntax (for NodeJS).
The module property is a counterpart of the main property and should point to the ES code.
The exports property is actually an interesting one. Using that gives a hint to Node and NPM what is exported from the package. I will use it superpowers when deploying UI components library. You can read more on that property in NPM documentation.
Also, we need to define the types property to let TypeScript know how to deal with the libraries code.
The last property - the files - defines where source files are placed. Again, that is a hint, used by NPM when installing the library as a dependency.
Now, having that setup, we can use the library's code in other packages of the monorepo we are building. In the next part of this article we will create the services/db-client package that will use the types/db-client.