This repository showcases using a monorepo to house a design system and the various packages one could contain. It is built for React and uses a number of different tools to create and maintain packages:
- 🏎 Turborepo — High-performance build system for monorepos
- 👷 Vite — ES Module focused build tool and bundler
- 🧪 Cypress — Browser based test runner
- 📖 Storybook — UI component environment powered by Vite
- 🔍 Syncpack — Ensures consistent dependencies within monorepos
- 🔐 Commitlint — Checks commits follow conventional commits format
- 📋 Changesets — Managing versioning, publishing and changelogs
- 🛠 GitHub Actions — Running workflows in continuous integration
Turborepo is a high-performance build system for JavaScript and TypeScript codebases. It was designed after the workflows used by massive software engineering organizations to ship code at scale. Turborepo abstracts the complex configuration needed for monorepos and provides fast, incremental builds with zero-configuration remote caching.
Using Turborepo simplifes managing your design system monorepo, as you can have a single lint, build, test, and release process for all packages. Learn more about how monorepos improve your development workflow.
Turborepo is configured in CI/CD to only test packages that have changes detected in their workspaces, and, workspaces that depend on said package. For example, if Package A depends on Package B, and Package B changes, then turborepo will ensure both packages have their tests run.
Highlights of benefits from using this monorepo are:
- Turborepo caching so commands don't re-run unless changed detected.
- Internal packages to house common configuration for tools like ESLint, Prettier, TypeScript.
- Ensures consistent package versioning via syncpack.
- Type-checking method within each package that turborepo can cache.
- Shared Storybook (
apps/docs
) across all React related packages.
Each application and package added to the monorepo will operate within its own workspace (specified within the top-level package file). Workspaces maintain their own package files but the entire monorepo has a single top-level lock file. All of a projects dependencies are installed together at the top-level which gives Yarn the ability to de-dupe and optimize them.
Each workspace has a symlink created for it within the top-level node_modules
folder which is what allows packages to depend on each other and have up to date code without requiring publishing. This is a better mechanism than yarn link
because it only affects the current project tree and not the whole file systems module system.
When importing from other workspaces, you can import the built version of the workspace by importing from its root where your tooling might pick the package files main
, module
or exports
fields. Alternatively you could also directly import the workspaces source files. For example, the <Button />
story imports the components source file from @acme/core/src
.
yarn build
- Build all applications and packagesyarn clean
- Clean all.turbo
,node_modules
anddist
foldersyarn lint
- Lint all relevant packagesyarn lint:packages
- Validate package dependencies are in syncyarn test
- Test all relevant packagesyarn types:check
- Type-check all relevant packages
This Turborepo includes the following packages and applications:
apps/docs
: Component documentation site with Storybookpackages/config-eslint
: Internal package for ESLint shared configurationpackages/config-pretter
: Internal package for Prettier shared configurationpackages/config-tsconfig
: Internal package for TypeScript shared configurationpackages/tds-core
: External package for current React componentspackages/tds-deprecated
: External package for deprecated React componentspackages/tds-utils
: External package for shared React utilities
Each application and package is a TypeScript project. Yarn Workspaces enables us to "hoist" dependencies that are shared between packages to the root package file. This means smaller node_modules
folders and a better local dev experience. They also have common commands such as build
, clean
, lint
and test
where appropriate.
To make the core library code work across all browsers, we need to compile the raw TypeScript and React code to plain JavaScript. We can accomplish this with vite
, which uses esbuild
to greatly improve performance. Running yarn build
from the root of the Turborepo will run the build
command defined in each package file. Turborepo runs each build
in parallel and caches & hashes the output to speed up future builds.
Relevant monorepo packages use Cypress for browser based component testing. The yarn test
command will run these commands in parallel, caching and hashing the output to speed up future runs. When this command is run locally there are no further considerations, however, in CI the turbo command will try to run these in parallel. Each process will try to insantiate xvfb
which causes issues. The CI script instantiates this before running the turbo command.
Storybook provides us with an interactive UI playground for our components. This allows us to preview our components in the browser and instantly see changes when developing locally. This example preconfigures Storybook to:
- Use Vite to bundle stories instantly (in milliseconds)
- Automatically find any stories inside the
stories/
folder - Support using module path aliases like
@anthonyhastings/tds-core
for imports - Write MDX for component documentation pages
yarn add-changeset
- Generate a changeset fileyarn version-packages
- Update versions, changelogs and dependencies of packages.yarn release
- Publishes changes to package registry and creates git tags.
The monorepo uses Changesets to manage versions, create changelogs, and publish to the package registry. You'll need to create an NPM_TOKEN
and GITHUB_TOKEN
and add it to your GitHub repository settings to enable access to the package registry.
To generate your changelog, run yarn add-changeset
locally:
- Which packages would you like to include? – This shows which packages and changed and which have remained the same. By default, no packages are included. Press
space
to select the packages you want to include in thechangeset
. - Which packages should have a major bump? – Press
space
to select the packages you want to bump versions for. - If doing the first major version, confirm you want to release.
- Write a summary for the changes.
- Confirm the changeset looks as expected.
- A new Markdown file will be created in the
changeset
folder with the summary and a list of the packages included.
These changeset files should be part of your PR and committed into the trunk branch, ready for future release.
When you merge your PR into the trunk branch, the GitHub Action will create a PR with all of the package versions updated and changelogs updated. If more PRs get merged with more changesets then the PR opened by the GitHub Action will be updated.
Merging this PR will, along with updating all of the files it changed, make the GitHub Action trigger it's release cycle where it attempts to publish each package not marked as private
within the workspaces package file.
Whenever you want to test a package in a consuming application before publishing a proper version for the wider public, we can create a snapshot release. These are special versions that take the form of 0.0.0-BRANCH_NAME-TIMESTAMP
e.g. 0.0.0-testing-snapshot-releases-20230226224821
. The branch name is also used as a dist-tag which points at the snapshot version e.g. "@anthonyhastings/tds-utils": "testing-snapshot-releases"
. The separate dist-tag ensures we don't touch the latest
dist-tag which is the tag used to determine what version of a package gets installed whenever someone installs it via yarn install PACKAGE_NAME
.
To create a snapshot release we can use the manually dispatched 'Semantic Release' workflow. The sequence of operations is as follows:
- Create a feature branch (e.g.
testing-snapshot-releases
) which contains changes to packages along with changeset files. - Manually dispatch the 'Snapshot Release' workflow and target it to run against the feature branch.
- The workflow will run
changeset version
with the--snapshot
argument to update changelog and package files using the version number0.0.0-testing-snapshot-releases-TIMESTAMP
. - The
prerelease
command will build the project which ensures build files will be present for the upcoming publish. - The
changeset publish
command runs with the--tag
argument set to the feature branch name and the--no-git-tag
argument. Changesets will publish the version to the registry without creating / pushing a source control tag.