microsoft / rushstack

Monorepo for tools developed by the Rush Stack community
https://rushstack.io/
Other
5.93k stars 597 forks source link

Design Proposal: Lockfile Lint #4113

Open william2958 opened 1 year ago

william2958 commented 1 year ago

Summary

Currently in our monorepo's rush lockfile, there is no validation or verification of entries in the lockfile. This can lead to the introduction of potential issues such as side-by-side versions or dopplegangers as more developers work in the growing rush workspace.

The objective of this issue and the lockfile linter efforts is to create a configurable linter, similar to tslint or eslint in the sense that individual users of the linter can specify which rules they want to watch for and what sort of issues they want to be warned about. The linter should also be run in a C.I. environment to verify and validate the added entries to the lockfile when PRs are created, to ensure it does not violate any of the specified linting rules.

Some key features:

The usability of the linter will depend on two key factors:

Current progress is being developed in this PR: https://github.com/microsoft/rushstack/pull/4094

Linting Side-by-Side Clusters

Currently this is the main linting feature we are working on developing. It is a way of identifying and saving any side-by-side cluster nodes as well as connected projects. (Below is a handy list of vocabulary that we have been working on to help understand the feature.) This should allow monorepo maintainers to get a high-level overview of the highest priority side-by-side dependencies as well as any projects in the monorepo that are of the highest concern.

Cluster Nodes: A side-by-side dependency with no upstream side-by-side versions. In the following example below, heft@2 and heft@3.0.7 are cluster nodes because they have no other upstream dependencies that are side-by-side versions. However, eslint@8.35 and eslint@9 are not considered cluster nodes as they have an upstream side-by-side package (in this case the two versions of heft). We care only about the cluster node, because that is the determining factor that causes other dependencies to become side-by-side versions. Even if these side-by-side versions use the same dependency versions further down the tree, pnpm still has to install dopplegangers for those packages because the root dependency is different. For example, eslint@8.35 and eslint@9 cause dopplegangers (eslint-utils D1 and D2), even though the two versions of eslint may specify the same version of eslint-utils in the package.json. It also is likely to cause other side-by-side versions, such as things@3 and things@4.

Connected Cluster Nodes: A project in the rush workspace that has more than one cluster node dependencies somewhere in the tree. (denoted by allowedConnected in the linting output) In the following example, App7 has two cluster nodes in its dependency tree (thing@3 and thing@4). These situations present a more serious issue because these are the scenarios that greatly increase the project app's bundle size (bundling 2 versions of thing, as well as 2 versions of all their mismatched dependencies further down the chain.

image

From inside the linter itself, the data will be represented in a similar format to below. Please note that this would not be the final format that is saved in the repo to compare against any PRs, as it is too detailed and would likely cause a lot of churn. The final file used for comparison will be much simpler (see below)

"eslint": {
  "duplicateVersions": { 1.0.0, 2.0.0, 3.0.0 },
  "allowedConnected": {
    "project1": [
        ["project1","dependency1","1.0.0"], 
        ["project1","dependency2","2.0.0"], 
        ["project1","dependency3","3.0.0"]
    ]
  },
  "notConnected": {
    "project2": [
        ["project2","project1","dependency1","1.0.0"],
        ["project2","project1","dependency2","2.0.0"],
        ["project2","project1","dependency3","3.0.0"]
    ]
  }
}

To actually output a file that we can use to keep track of violations, this will be the approximate syntax:

"eslint": {
  "duplicateVersions": 3,
  "allowedConnected": ["project1"]
}

As this is much simpler of a format, it should prevent any churn in the file unless there is an issue that needs to be addressed (and thus require the monorepo maintainer to investigate & approve)

Trying out the Linter

To try out the lockfile linter in your monorepo, checkout the branch mentioned in the PR (https://github.com/microsoft/rushstack/pull/4094) and run the lockfile explorer application in your monorepo.

To run a development feature of rush, you may need to follow the following steps: https://rushjs.io/pages/contributing/#testing-rush-builds

Roadmapped rules

These are a set of additional linting rules and configurations that we have considered and will likely add as well in the future. We are also looking for any other linting rules that may be helpful to other monorepos (or that we have not considered!).

Version Sync Lint Ensure major versions of certain packages match other package versions. For example, ensuring that the major version of the webpack-merge package matches the webpack version.

Singleton Version Lint Ensure only one version of certain packages exist in the lockfile. For example, we may want to set that we only want to allow one version of tslib in the entire lockfile.

Licensing Lint It may be helpful to have a linting rule that prevents packages with certain licenses from entering the monorepo.

octogonz commented 1 year ago

For anyone who is interested, a prototype of this idea will be demoed at Rush Hour West tomorrow.