A typical web project may consume hundreds of NPM packages, each developed independently by a random stranger somewhere on the internet. Each project has its own Git repository, is conceptually self-contained, and tries to maintain an API contract conforming to SemVer. When there is a bug in one of these packages, a pull request is merged, the version is bumped, and a new release is published. The new release then begins a slow propagation across the internet, as each downstream project upgrades and reacts to any incompatibilities that may arise in a delightful symphony of distributed collaboration.
At the root of this massive dependency tree is your client-side web application. Unlike everything else, it is one huge monolith. The bigger it grows, the more you begin to think "This is stupid -- we should break this thing up into 30 small NPM packages that can be reused and versioned independently." One heroic Saturday night, you create 30 Git repos and refactor everything.
Eventually people start complaining. Your team isn't 30 strangers making frivolous little "left-pad" utilities. They are application developers creating a mission-critical product with messy business logic. The components interact in complex ways, and every time you change one package, you seem to be breaking other packages. It feels wrong to be cloning and building 30 different Git repositories every day, when there's only 10 people on your team. Publishing is getting tedious. Running "npm link" is a minefield. This is no way to work!
And so, you consolidate all your NPM packages into one central Git repository, and write a script that runs "npm install", "npm link", and "gulp" 30 times, in the right order. This is way better! In the past, when Bob made a big change to a core library and then left for a backpacking trip across Europe, it could take a week for Alice to upgrade to the new version and realize that something was broken. Even though Bob caused the trouble, his victims unfairly had to shoulder the cost of debugging it. Having a unified build means that Bob cannot even merge his PR (let alone publish a new release) without passing all the unit tests for every downstream project. Catching problems early makes everyone more efficient. Having a central repository forces library owners pay attention to the source code and PRs that consume their APIs; no more "out of sight, out of mind."
There is just one problem... Builds are slowwwww. If "npm install" takes 1 minute (on a good day), then 30 installs take 30 minutes. Building 30 small projects is slower than building one big project. Other details like managing shrinkwrap and publishing can be tricky.
Rush formalizes this model and makes it quick. It works completely within the conventional NPM system: Each package will still have its own Gulpfile. Each package can still run "npm install" without Rush if desired. You are always free to move your projects around between Git repositories without any changes to package.json.
But when you use Rush, you get some big improvements:
-
Save time by installing all dependencies for all packages via a single "npm install" invocation
-
Rush automatically generates a shrinkwrap file for the entire repository. NPM shrinkwrap is the only way to avoid maddening problems of "what are you talking about, it works on my PC!"
-
All projects are automatically hooked up with "npm link" (using local symlinks so multiple Git folders won't get cross-linked)
-
A dependency solver uses package.json to automatically determine the build order.
-
Since each project has its own Gulpfile, Rush can spawn multiple NodeJS processes in parallel, making the build go significantly faster. (No matter how many promises you write, your Gulpfile is still fundamentally single-threaded.)
-
Use a single command to run "npm publish" for many packages
-
Git-based incremental builds, so you only rebuild a project if a source file in that project folder has changed.
-
Support for cyclic dependencies: For example, suppose that my-gulp-task depends on my-library, but my-library's Gulpfile has a devDependency on my-gulp-task. Rush can install the last published version of these packages for the Gulpfile, while still creating local links for other projects outside the cyclic dependency.
-
Support for enforcing certain Git policies, such as enforcing that Git committer email addresses conform to a well-defined pattern.
At any time, you can see the --help flag to find command-line usage information.
-
Run "npm install -g @microsoft/rush". To confirm that it's working, run "rush -h" which prints the version number and usage information.
-
From anywhere in your git working folder, run "rush install". This will install NPM modules in Rush's "Common" folder.
NOTE: If you are troubleshooting build issues, try "rush install --full-clean" instead.
-
From anywhere in your Git working folder, run "rush link". This creates symlinks so that all the projects will reuse the packages from "common/node_modules" (rather than having to run "npm install" in each project folder). It will also link projects to the folders for their local dependencies, so that you don't need to do "npm publish" to test your changes.
NOTE: The "rush.json" config file specifies how this linking is performed.
IMPORTANT: DO NOT run "npm install" inside project folders that have been linked by the Rush tool. If you want to do that, you need to "rush unlink" first.
-
Do your initial build by running "rush rebuild" . This will recurse through each project folder and run "gulp clean", "gulp", and "gulp test", and then give you a report of anything that failed to build.
NOTE: To suppress verbose output, use "rush rebuild -q".
The above steps are only necessary when you need to do a clean full build (e.g. after pulling changes that affected common/package.json). Otherwise, you can run "gulp" in individual project folders as usual. Your daily workflow will look like this:
-
Pull the latest changes from git.
-
If something changed in the common folder, then you may need to update your NPM:
C:\MyRepo> rush install
C:\MyRepo> rush link
C:\MyRepo> rush rebuild -q
-
Debug a project:
C:\MyRepo> cd my-project
C:\MyRepo\my-project> gulp serve
Once a project has been added to the rush.json file, several commands must be defined in the
project's package.json file, under the scripts section. Every project must define a clean
script. Additionally, every project must define either a test or build command. By default,
Rush will default to using the test command if both are defined, but will fall back to build
if test is missing.
The defined commands should either reference something on the PATH or should be an absolute path.
If the command is defined simply as gulp, the version of gulp from the common folder's node_modules/.bin
folder will be used.
An example configuration is below:
{
"name": "my-project",
"version": "1.0.0",
"scripts": {
"clean": "gulp clean",
"build": "gulp",
"test": "/usr/bin/testcommand --compile --things"
}
}If you need to add new dependencies to your package.json, you will need to regenerate the files in the common folder. Use these commands:
C:\MyRepo> rush generate
C:\MyRepo> rush link
This will change various generated files in common folder. You shuld include these changes in your Pull Request.
The "rush generate" command takes a long time. To speed up debugging, you can use "rush generate --lazy" instead; however you must run the full "rush generate" before submitting your PR.
To publish all NPM projects in your repository, run "rush publish". You can select a subset of projects using the "--include" option followed by a glob pattern. You can use the "--registry" option to specify a custom NPM registry, e.g. if you are testing with Verdaccio.
Currently you need to manually create an "rush.json" configuration file at the root of your repository, which specifies the list of projects to be built. You also need to set up your "common" folder, and add the appropriate files to Git. (We are working on an "rush init" command to simplify this.)
Suppose that your Rush repo has 30 different projects, and you want to keep track of what NPM packages people are using. When someone finds a new package and tries to add it to their project, you want to ask questions like: "Is this a good quality package?" "Are we already using a different library that does the same thing?" "Is the license allowed?" "How many other dependencies will this pull into our node_modules folder?" Rush can alert you when this happens.
In your rush.json file, add these optional fields:
"reviewCategories": [ "published", "internal", "experiment" ],
"packageReviewFile": "common/PackageDependencies.json",In this example, we defined three kinds of projects that we care about: Projects that we publish externally, projects kept internal to our company, and throwaway experiments. For each project in the repo, we will assign one of these categories as the "reviewCategory" field.
The PackageDependencies.json file contains the list of approved packages. This file should be added to Git. It might look like this:
{
"browserPackages": [
{
"name": "lodash",
"allowedCategories": [ "internal", "experiment" ]
}
],
"nonBrowserPackages": [
{
"name": "gulp",
"allowedCategories": [ "published", "internal", "experiment" ]
},
{
"name": "some-tool",
"allowedCategories": [ "experiment" ]
}
]
}Above, we specified that only our internal projects and experiments are allowed to use "lodash", whereas "gulp" is allowed everywhere. The "some-tool" library is being used by an experimental prototype, but should never be used in real projects.
Note that Rush distinguishes "browserPackages" from "nonBrowserPackages", since the approval criteria is generally different for these environments.
Now, suppose someone changes their package.json to add "lodash" to a project that was designated as "published". When they run "rush generate", Rush will automatically rewrite the PackageDependencies.json file, appending "published" to the "allowedCategories" for "lodash". In other words, it automatically broadens the rules so that they describe reality. When the pull request is created, reviewers will spot this diff and can ask appropriate questions. Since our criteria is based on generalized categories, the reviewers aren't hassled about every little package.json change; a PackageDependencies.json diff only appears for genuinely interesting changes.