π― The problems it solves β π‘ How it works β π Getting started β π List of Best Practices
βοΈ Tools & Configuration β βοΈ Conventions β ποΈ What's next
# first, <create your empty github repo>
git clone <YOUR REPO URL>
git remote add breakproof [email protected]:YotpoLtd/breakproof-base-monorepo.git
git pull breakproof main
pnpm --workspace-root generate repo initπ A pnpm monorepo template meant to be cloned and used for your projects.
The breakproof monorepo template comes with ready to be used tools already in it, where each tool lives in its own package, runs its own version of Node.js and provides strict default config to be consumed by your projects and the built-in optimized CI/CD
π Clone to create your own monorepo! β π Pull to update the tools & CI/CD!
Your clone π becomes a breakproof home π where you can develop your frontend projects without π₯ breaking your code or π° breaking-the-bank.
You start by importing your existing projects or generating new ones. β‘
Immediately those projects get a ton of working processes like
optimized CI/CD, meticulous code checks, automatic npm releases,
precise code-editor integration, etc. And because you cloned the repo,
you keep receiving updates for those processes simply via git pull.
You get:
-
ποΈ laser-focus on your projects β not on the tooling around them
-
π easier upgrades of your projects β due to full isolation between tools & projects (even different
versions)
-
π§Ή low-to-none repository maintenance β the breakproof repo will update tooling & processes around your projects, keep them optimized & following best practices, without you having to change your code
Note
All of that πΌ is implemented by making industry-standard tools π work together, NOT introducing "yet-another-abstraction" and NOT requiring specific project tech stack.
π οΈ You want to focus building your project, not waste time in upgrading the tooling around it or making different tools for code checks, testing, build & release behave well together or with your code editor, BUT at the same time you want to get the bug fixes and performance improvements from latest versions of all tools π€ (solution in next section).
Β
π¨βπ§ It's hard to create a well-optimized, precise & cost-effective CI/CD that doesn't break with scale and informs developers what is going to happen without noise. You want someone else to create & maintain 90% of CD/CD processes, without requiring your projects to have specific tech stack π€ (solution in next section).
Β
π You cannot regularly stop development until you upgrade everything everywhere, you want to do it piece by piece in isolation π€ (solution in next section).
Β
π You want to improve your codebase but cannot fix all problems at once. You need to be tolerant of existing problems but forbid new problematic code π€ (solution in next section).
Β
π₯΄ You are tired of learning new configuration formats with each new repo-management tool that comes out and then hitting its customization limits. You want to directly deal with the tools used but still rely on sensible default configuration you can extend π€ (solution in next section).
Β
π You want to use industry-standard best practices without having to design the entire process from scratch, BUT at the same time be free to swap individual tools without having to redo the entire system π€ (solution in next section).
Β
Β
-
The repo is a
pnpmworkspace β a.k.a. monorepo managed only usingpnpmwithout any abstraction on top of it. This, by itself, lets you isolate one project from another by making each a separate package. The breakproof repo goes a step further. -
When using the breakproof repo, your projects are isolated from the tools they use. This is achieved by installing the tools in a separate package instead of the same package as your project. In fact each different tool is installed in its own individual package. This allows us to leverage an ability of
pnpmto specify a differentnode.jsfor each package. Essentially you get an isolated pair of<tool> + <node.js>, e.g.eslint + node22and this lets you runeslint(usingnode22) from any place in the repo with apnpmcommand similar to:pnpm --filter=<PACKAGE NAME THAT INSTALLS TOOL IN IT> run <TOOL NAME>. For example:pnpm --filter="@repo/eslint-isolated-base" run eslint -
The breakproof repo comes with several tool packages already in it, each installs one industry-standard tool like
eslint, orTypeScript, orrollup, orjest, β¦( full list below β¬οΈ ). Additionally, each of those packages include detailed & strict base configuration for the specific tool that your projects can extend. This means your projects are in full control of the final tool configuration. If one package needs a tool, it calls a script from another package that installs that tool. That's it. π Each individual tool can be swapped with another as long as it does it's job. (role of each tool is listed below β¬οΈ -
On many occasions the base configuration is tweaked so it accounts for limitation of another tool, thus making them behave well together. Not all tools accept all their settings through a config file, some require CLI arguments. For those occasions, each package that contains an isolated tool defines a command in the "script" section of its
package.jsonto act as a shortcut. This shortcut can be executed from anywhere running:pnpm --filter=<PACKAGE NAME THAT INSTALLS TOOL IN IT> run <SHORTCUT NAME> -
The above πΌ means that suddenly you can upgrade one tool in complete isolation from the rest & your project, even if the newer version requires a newer
node.jsversion. This will not affect other packages. It also means your code can use whatevernode.jsversion you need while some useful tool is "stuck" in the past without dragging you down
Β
-
The CI/CD process is implemented as GitHub actions/workflows. Because it knows the repo uses
pnpm, we can target only a subset of the packages inside it when running tasks. For example, if a PR only changes your project, then the CI/CD will run test & linting checks only for your project. It will also run them in parallel and install only the dependencies that your project needs, which are cached, so subsequent runs with the same dependencies don't waste time re-downloading them. -
The only requirement from your projects to participate in the CI/CD is to define properties in the
"scripts"section of theirpackage.jsonwith specific names (a convention). You can run whatever command you like there, but probably you will run some of the isolated tools as shown above. For example, if you want your project tested in GitHub PRs, the project must define alint:github-prscript with value ofpnpm --filter='@repo/jest-base-isolated' run jestwhere@repo/jest-base-isolatedis just the name of the package that isolatesjest. The convention for the names of those scripts is as straightforward as possible, so you needbuildfor building,releasefornpmreleases, etc. -
Because the CI/CD is a GitHub action, it easily posts comments on your PRs with useful info about the upcoming releases when PR is merged, or summary of code problems found in the changed packages. The CI/CD will also run specific checks for repository conventions like making sure every
package.jsonexplicitly defines desirednode.jsversion to use. The breakproof repo even implements GitHub job summary which allows for a friendly way to overview the progress of the running workflow without the noise of going through logs. -
You can implement your custom CI/CD bits like running deployment as just another GitHub workflow, BUT with the benefit that your workflow can receive extra inputs β the list of packages that have been changed or the list of packages that will be automatically released to
npm. You can then use that information to only trigger logic in certain cases.
- To allow you to work on projects that have some code problems BUT at the
same time stop new problems from popping up, the breakproof repo has
created a few
node.jsscripts inspired byesplintthat basically "snapshots"/"remembers" what kind of problems each of your files have and how many occurrences per problem type there are. This allows you to ignore the existing ones but error out if the number of problems increase. This same ability of "remembering existing problems" is useful when tools are upgraded or their configuration changes. At this point they can become stricter or change the way they detect problems. To unblock the upgrade without investing immediately investing time you can snapshot the new results and continue with your day.
Β
Β
-
π Clone this repo. (and then push it to your own blank repo)
-
ποΈ Initialize your clone with your preferences & automatically configure your code editor:
pnpm --workspace-root generate repo init
-
π Generate new or import your existing projects.
Expand to see steps for setting up automatic publishing
You need to add some GitHub configuration for your repository:
-
Create a
bot/machineGitHub account that will be used ONLY for the automatic release process. Using email likebot@<your domain>.com. -
Set up GitHub repository
variables:
AUTORELEASE_BOT_NAMEβ the name to be used as commit author of the automatic version changes to your main branchAUTORELEASE_BOT_EMAILβ the email associated bot account you've created; to be used as commit author of the automatic version changes to your main branch
- Set up GitHub repository
secrets:
AUTORELEASE_BOT_TOKEN: a GitHub token with rights to push to repository to be used by the bot doing automatic releasesNPM_REGISTRY_USER: your npm registry user- [Optional]
NPM_REGISTRY_AUTH_TOKEN: your npm registry auth token, if you don't use a password instead - [Optional]
NPM_REGISTRY_PASSWORD: your npm registry password if you use this instead of auth token
- Set up GitHub Merge Queue:
-
Head to
<your github repo> -> Settings -> Rules -> Rulesets -> New ruleset -> Import a ruleset -
And upload
<repo root>/.github/branch-rulesets/Default Branch.json. This will:- Require a
Merge Queuefor your default branch. - Require
basicsjob from the main GitHub workflow to pass before PRs can be merged
The above is used to make sure that no PR is merged while the CI for automatic releases of
npmpackages is running. This CI job can push version bumps & changelogs to your default branch. Without the merge queue there is a risk of a race condition. - Require a
- Write the steps in
<repo root>/.github/actions/npm-loginthat will authenticate you in thenpmregistry.
- This action is empty by default and gets called on all places where packages are installed
- To authenticate you would need to configure some repository secrets, depending
on your auth approach. All the secrets will be accessible as environment
variables, and not through the GitHub
${{ secrets }}. This is done to overcome a limitation in GitHub reusable actions: they can't inherit secrets but do inherit environment variables.SECRET_NPM_REGISTRY_AUTH_TOKENβ for this to have a value you must create a secret in your repository settings calledNPM_REGISTRY_AUTH_TOKENSECRET_NPM_REGISTRY_USERβ for this to have a value you must create a secret in your repository settings calledNPM_REGISTRY_USERSECRET_NPM_REGISTRY_PASSWORDβ for this to have a value you must create a secret in your repository settings calledNPM_REGISTRY_PASSWORD
- If you are publishing to a private registry set this at the top of the
<repo root>/.npmrc.
Expand to see steps for setting up automatic application deployment
- Follow the first 3 steps from the instructions for
automatic
npmpublishing - If you use Slack and you want to get message to a channel of your choice, set
up a GitHub repository secret named
SLACK_WEBHOOK_URLwith a webhook generated from your slack admin - Open
<repo root>/.github/workflows/_repo-deploy-single-project-on-push.yamland copy theexamplejob there, after which customize the steps to match the specifics of your app deployment
Β
Β
| Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β badge | details |
|---|---|
| generate all required files for a new project via interactive CLI step-by-step wizard | |
| each new project comes with unit, e2e and integration tests enabled | |
| each new project comes with typescript support & build process enabled | |
| the typescript settings are fine-tuned based on the type of project you chose in the CLI wizard | |
| each new project comes with exceptionally extensive list of code checks, individually customizeable for each of your projects | |
| the code checks can apply possible autofixes to entire project or only to its files staged for commit | |
each new project has the ability for automatic release to npm & GitHub when merged in the default branch, following the conventional commit |
|
| each new project has its individual set of dependencies completely isolated from the dependencies of the other projects in the repo | |
each new project can choose a different node.js version to use, thus unlock innovation of individual projects |
|
all tools that do code checks, tests or release of your project, in fact run in their own isolated environment with their own nodejs version, which means you can upgrade them without having to immediately upgrade your project or vice versa |
|
automatically run some/all of the code checks & tests on each GitHub pull request, individually customizeable for each of your projects |
|
automatically run some/all of the code checks & tests before git commit, individually customizeable for each of your projects |
|
| detect which projects have been affected by the pull request changes or pending git commits, and run code checks only for them | |
| announce what is affected by the pull requests changes as a GitHub comment | |
| add meaningful summary of your GitHub workflows with the contextual info of what they are running (GitHub workflow markdown summary) | |
| ability to modify existing projects, so you can make them release-able or add code-checks, all done via a CLI step-by-step wizard | |
shared npm scripts listed in a single place and can be used by any of your packages to help you analyze problems with it |
|
| conventions to be followed by each package for more unified development experience | |
| instructions and importable configuration for your code editor to get the best developer experience possible when developing your projects | |
| instructions on how to configure your GitHub repository to prevent problems and work best with the breakproof base |
Β
Β
Nothing is perfect, so this list will inevitably change (see β¬οΈ "What's next?"). Here are the currently used tools:
Β
Β
- Avoid adding abstractions to the already overloaded frontend world
- People should be aware of what comes with their dependencies
- Each package must declare the
node.jsversion used for running thescriptsitspackage.json- mandatory eslint code check for this runs on git precommit & GitHub PR
- Packages are allowed to pick
node.jsversion from a white-list to avoid extreme fragmentation of versions- mandatory eslint code check for this runs on git precommit & GitHub PR
- Each package must have a script called
lint:precommitwhich would run as git hook before commits and execute preferred code checks- mandatory eslint code check for this runs on git precommit
- Each package must have a script called
lint:github-prwhich would run on each push to a GitHub PR and execute preferred code checks- mandatory eslint code check for this runs on GitHub PR
- Each package that requires a build step must have scripts called
buildanddevwhich are used to verify build passes and run before building other packages that depend on them- If the package is an app, the
devscript can also be namedserveto match popular conventions
- If the package is an app, the
- Each package that should be published to
npmregistry must have s script calledreleaseand whatever is executed must accept arguments compatible withrelease-itCLI - Following what each configuration extends and understanding why it's configured this way should be easy
- Writing configurations files should provide hints for available options
without having to jump to documentation every time
- Prefer
.tsconfig over.js,.cjs,.mjsor.jsonconfig. We get type checks + ability to comment. - Prefer
.mjsconfig with jsdoc over.js,.cjs, or.jsonconfig. We get type hints + ability to comment. - Prefer
.cjsor.jsconfig with jsdoc over.jsonconfig. We get type hints + ability to comment.
- Prefer
- Configurations can have different variations (e.g. "dev" vs "production" mode) and preferably they are defined in a single file without repeating different options for each mode
- When using
CLIcommands prefer--longerargument names over shorter versions like-l - Code editors are first-class runtime environments we need to care about
- postinstall script that guarantees code editors see
eslintandprettierbut CI doesn't do this extra step
- postinstall script that guarantees code editors see
- Build-caching should be thought as a separate layer that can be added later, not requirement from the start
Β
Β
The
GitHub issues of type Feature
can give you a picture of what kind of improvements we've been looking into.
Since the repository is done in a way that allows tools to be swapped there are some ideas, that we haven't yet created issues for. For example:
eslintmight in the future be swapped out with another tool likeoxclinter or perhaps whatever void0 are working on.rollupmight in the future be swapped out withrolldownwebpackis a build tool that served the FE community well. People can see it as "old" or "outdated", but it keeps on living, used bynext.jsor reborn asrspack, orturpopack. Currently, the sandbox applications in the breakproof repo usewebpack, but that could be swapped for any other build tool- so far
lint-stagedhas served us well, but we might just replace it with a simple.tsscript, since we are starting to tweak more from it, and we actually only use its config format and its ability to list staged files and then show their execution + some meta sugar in the terminal. cypress+ Cypress Cloud can be a great combo which has been a good friend in end-to-end and integration testing. But for a while we can see developers can are drawn to the more native way of writing tests withplaywright.- ... check out the GitHub issues or create one if you have an idea.
Β
Β
- conventional changelog allows a lot of prefixes, the repo only respects
fix,featand breaking changes.
There are 2 packages providing base config for eslint and prettier. One
(@repo/eslint-base-isolated) which depends on the other
(@yotpo-common/shared-linter-config).
We've done this because internally some projects in yotpo still live in their
own separate repos. @yotpo-common/shared-linter-config is used to share the
configuration with projects outside our breakproof repo, while
@repo/eslint-base-isolated extends it with some tweaks.
-
For better integration with JetBrains IDEs, there is a "fake"
<repo root>/package.json. It's intentionally a symlink to a non-existent path -
To work around a limitation in
pnpmfiltering, we introduce a new array section inpackage.jsoncalleddevtoolsDependencies. It should include names ofdevDependencieswhich do not affect the runtime or the build. So things like lint, tests, or local development helpers. -
In order to be strict and avoid packages accessing
<repo root>/node_modulesbut still let code-editors find installations of tools (e.g.prettier) we introducedcodeEditorDependenciesin rootpackage.json5that installs a few dependencies only for local development (NOT in CI) and DOES NOT SAVE them to the lock file. -
The preinstall script:
[ -n \"$(pwd | grep '/node_modules/')\" ] || echo $npm_config_user_agent | grep -q 'pnpm/' || (echo 'PLEASE USE PNPM, not NPM' && exit 1)
We want to disallow package managers other than
pnpmto be accidentally used in this repo. So we add this script to every packagepreinstallhook.Ideally it would be just
npx only-allow pnpmas described in pnpm docs, but:- There is a bug in
only-allowso we work around it. - If developer is only using
pnpmthey might not havenodeinstalled at all, so we need to check ifnpxexists (viawhich npx)
- There is a bug in