JS Modules monorepo - A library and application repository where all necessary components and services required for production-ready full-stack applications are designed and implemented.
This monorepo is designed to primarily contain TypeScript code. However, packages can also be developed in JavaScript and Solidity.
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v<nvm version>/install.
sh | bash
nvm install 18
npm install -g pnpm nodemon ts-node -y
To install all packages' dependencies and bootstrap the monorepo, run the following command in the terminal, from the project's root directory:
pnpm reset:reset
To build all packages for production, run the following command in the terminal, from the project's root directory:
pnpm lerna:build
There are two ways to execute a package's package.json
script:
- Open a terminal, navigate to the package's root directory and execute
pnpm <script name>
- Open a terminal in the monorepo's root directory and execute
pnpm --filter <package name> <script name>
As stated in monorepo.tools, 'a monorepo is a single repository containing multiple distinct projects, with well-defined relationships.'
While there may be different interpretations for 'well-defined', the term
often implies not only separation and organization of code but also strict
encapsulation which is the case for this monorepo. Strict encapsulation
means that if package_1
consumes a type, variable or function from
package_2
, it must always import it by adding package_2
to the
dependencies of package_1
's package.json
.
Just to be clear, NO script should import anything from a different package using a relative path!
Building up on the concept of 'well-defined' relationships, defining the nature of packages is also important. In this monorepo, any given package can be in only one of the following two categories:
- Library: A package that exports one or more artifacts, which are consumed by other packages
- App: A package that builds into an application to be executed in a production runtime
No package should do both of these things!
The monorepo implements a hierarchical structure of package.json
files.
The root package.json
contains a set of scripts to bootstrap, lint, test
and build all packages. It also contains common dependencies and configs.
Each package's package.json
inherits from the package.json
s in its
parent directories and can include additional dependencies and/or
configurations required for the package's specific purpose.
There are several scripts in the root package.json
of the monorepo:
pnpm clean:main # Remove node_modules, build and coverage directories, as well as .husky and .ignore files
pnpm clean:tsbuildinfo # Remove .tsbuildinfo files
pnpm clean:graph # Remove dependency-graph files
pnpm clean:lock-files # Remove package manager lock files
pnpm clean:git-hooks # Disable husky git hooks
pnpm reset:clean # Run the clean:main & clean:tsbuildinfo scripts
pnpm reset:install # Create symbolic links to .gitignore for libraries that use .ignore files
pnpm reset:reset # Run the reset:clean & reset:install scripts
pnpm git-hooks:set # Configure and enable husky git hooks
pnpm git-hooks:reset # Run the git-hooks:clean & git-hooks:set scripts
pnpm test:test # Run all tests in the monorepo
pnpm test:watch # Automatically run tests when relevant code is changed
pnpm test:coverage # Run all tests in the monorepo and generate a coverage report
pnpm lint:check # Check code for linting errors
pnpm lint:fix # Check code for linting errors and automatically correct errors when possible
pnpm graph:dependency-map # Generate dependency graph files
pnpm graph:package-tree # Print package tree to the terminal
pnpm lerna:build # Run the build script of all packages
pnpm prepublish # Run the lint:fix, test:coverage & lerna:build scripts
All dependencies of the root package.json
are installed in all packages.
The monorepo's root package.json
contains configuration metadata for the
following libraries, which are shared by all packages:
- eslintConfig
- prettier
Each package can extend and/or override the root configuration in their own
package.json
.
The monorepo uses two separate TypeScript configuration files.
tsconfig.json
for developmenttsconfig.build.json
for production
Just like with the package.json
setup, there are the root tsconfig
s and
each package's tsconfig
s.
The root tsconfig.json
extends the root tsconfig.build.json` and adds
path aliases for all library packages whose artifacts, as explained above,
are consumed by other packages.
These path aliases allow
consumers of library artifacts to execute the raw TypeScript code, as they
would if the artifact were imported using a relative path. Without path
aliases, a library package must be compiled and published to the package
registry so that consumer packages can install it as a dependency in their
node_modules
directory and consume the library's artifacts from there. This
setup allows for the simultaneous development of different interrelated
packages, without breaking encapsulation, while avoiding the drag of having to
build and publish changes to library packages before said changes are
propagated to their consumers.
Unlike the root tsconfig.json
, a package's tsconfig.json
does not extend
the package's tsconfig.build.json
. Instead, a package's tsconfig.json
extends the root tsconfig.json
and its tsconfig.build.json
extends the
root tsconfig.build.json
.
Unit testing of JavaScript and TypeScript code is carried out using Jest.
The testing configuration is set up with a hierarchy of config files, similar
to the approach used for package.json
and tsconfig
described in the
sections above.
jest.config.ts
is the root config for jest, which contains the common config, shared by all packages, and enables testing for packages where there is ajest.config-project.ts
config in the package directory, which enables Jest to execute tests in the package and allows it to extend and/or override the root shared configuration for its purpose
The only .ignore
that is actively maintained is .gitignore
. All other
.ignore
s are symbolic links, generated during the execution of the
reset:install
script when the project is bootstrapped. Hence, no .ignore
file, aside from .gitignore
should be edited manually.
All packages in the monorepo are organized in a directory structure, within
the packages
directory, with two levels.
The first level is one of the following general categories:
api
: Library packages whose artifacts' main purpose is to support back-end/server features of packages in theapi
and/orapps
categoriesapps
: App packages and library packages that directly support said appscommon
: Library packages whose artifacts are consumed by packages in more than one ot the other categoriesweb
: Library packages whose artifacts' main purpose is to support front-end web features of packages in theweb
and/orapps
categories
The second level, inside the main categories, serves as a package subcategory and doesn't have to follow a strict nomenclature pattern.
The actual packages then go inside the subcategory directories and follow some basic guidelines for the sake of consistency:
- Package names are always prefixed as follows:
<category>-<subcategory>-<package name>
, e.g.api-nest-utils
,common-react-hooks
orweb-react-utils
. - The only place with
index
files are used is in each package'ssrc
directory. Everywhere else, files have meaningful names regardless of the fact that import statements are slightly more verbose. - All package artifacts are exposed in the package's
index.ts
mentioned in the previous point.
*IMPORTANT Note: NestJs always builds the app's code for production
before serving the API, regardless of whether the app is run in development or
production with the nest start
or nest build
, respectively. This means
that app packages which build NestJs servers
don't use the root tsconfig.json
path aliases. For this reason, when
changes are made in these packages' dependencies, the dependency packages
must be re-built as does the NestJs app package before said changes take
effect, even when the NestJs app is run with nest start
in dev mode.
I developed this project entirely in my free time and without monetary retribution. You are welcome and encouraged to use it free of charge but if it serves your purpose and you want to contribute to the project, any amount of donation is greatly appreciated!
Paypal | BTC | ETH |
---|---|---|
https://paypal.me/pools/c/8t2WvAATaG | bc1q7gq4crnt2t47nk9fnzc8vh488ekmns7l8ufj7z | 0x220E622eBF471F9b12203DC8E2107b5be1171AA8 |