Search code examples
node.jsnpmyarnpkgyarnpkg-v2

Finding all installed versions of an npm package using the yarn API


My goal is to prevent out-of-order deployment of a back-end and an app. All of the software is written in JavaScript. The back-end interface is wrapped up in a API client npm package, which the app imports. The back-end server version and client package versions are related. The package and server versions are related. In general, the server needs to be at a higher version number than the package for the client to be safely deployed. A function in the package can also test if the server is compatible.

I have run into some complications with this approach as our app has grown. Now we have domain libraries that depend on the client library. The app depends on more than one domain library. Each of these depend on the API client package. Therefore the app possibly has two different versions of the API client package linked.

I tried to simplify the picture using peerDependencies. The domain libraries use a soft peer dependency on the API client. But this just sort of moves the version-checking problem. Now I have to be careful that the app's singular API client dependency version is compatible with the domain libraries. This isn't foolproof, since peer dependency mismatch is a warning (not an error). We have lots of these warnings, and it's hard to check for our specific API client.

I thought about going back to hard non-peer dependencies on the API client. Instead of ensuring a single version of the API client, the pre-deployment check would enumerate all of the installed versions of the API client and check each against the backend version.

This also runs into complications. We use yarn, so I don't think that I can simply grep the package.json files under node_modules because of PnP. I don't think that the node packages API helps enumerate installed packages.

This brings me to the main question: How can I use the yarn API to list all installed versions of a package?

I tried something like this:

const { Configuration, Project, Locator } = require("@yarnpkg/core");

async function getInstalledVersions(packageName) {
  const cwd = process.cwd();
  const configuration = await Configuration.find(cwd, null);
  const { project } = await Project.find(configuration, cwd);

  // Ensure the project's dependencies are up-to-date
  await project.restoreInstallState();

  const versions = [];

  for (const workspace of project.workspaces) {
    for (const dependencyType of ["dependencies", "devDependencies"]) {
      for (const descriptor of workspace.manifest[dependencyType].values()) {
        if (descriptor.name === packageName) {
          const pkg = project.storedPackages.get(Locator.locatorHash(descriptor));
          if (pkg) versions.push(pkg.version);
        }
      }
    }
  }
  return versions;
}

However, the Configuration.find fails with "UsageError: Unrecognized or legacy configuration settings found" if the .yarnrc.yml loads any plugins.

Is there an easier or more portable way of listing installed packages? I'm looking for programatic solutions that do not spawn a new "unnecessary" process (for example, exec("yarn info --recursive --json PackageName").


Solution

  • The yarn.lock (yarn v2+) file is yaml. Reading dependencies directly from it is much simpler than going through the yarn API.

    const yaml = require("js-yaml");
    const fs = require("fs");
    
    async function getInstalledPackageVersions(packageName) {
      const yarnLockYaml = await fs.promises.readFile("yarn.lock", "utf8");
      const yarnLock = yaml.load(yarnLockYaml);
      return Object.entries(yarnLock)
        .filter(([, v]) => v.resolution?.startsWith(`${packageName}@`))
        .map(([, v]) => v.version);
    }