Skip to content

Instantly share code, notes, and snippets.

@mukaschultze
Last active August 8, 2022 13:12
Show Gist options
  • Save mukaschultze/3fe4be7fb7a0ae9f525b38a739bdda0d to your computer and use it in GitHub Desktop.
Save mukaschultze/3fe4be7fb7a0ae9f525b38a739bdda0d to your computer and use it in GitHub Desktop.
Pin yarn workspace versions
/** Yarn workspaces currently don't have a way of specifying that a workspace
* (i.e., package) should use the worktree (i.e., root package.json) version for
* a dependency. Some projects use the @* version descriptor (equivalent to
* latest version) to achieve this, although this approach works most of the
* time it can easily get the workspace and worktree versions out of sync when
* some of the packages are explicitly updated via package.json or when
* yarn.lock gets regenerated.
*
* The alternative to using the @* syntax is using the `file:` version
* descriptor, so the workspace uses the package from a given directory, in this
* case, the worktree node_modules. The problem with this is that yarn won't be
* able to hoist duplicate packages to the worktree node_modules, which is one
* of the main benefits of using yarn workspaces.
*
* To solve this, I've implemented this script that will update the yarn.lock to
* sync the versions that are using the @* syntax to the package.json versions,
* this will allow yarn to hoist the packages to the worktree node_modules while
* still maintaining sane versioning that doesn't break often.
*
* https://gist.github.com/mukaschultze/3fe4be7fb7a0ae9f525b38a739bdda0d
*/
const lockfile = require('@yarnpkg/lockfile');
const path = require('path');
const fs = require('fs');
const getAppRootPath = () => {
let cwd = fs.realpathSync(process.cwd()).replace('\\', '/');
while (!fs.existsSync(path.join(cwd, 'yarn.lock'))) {
const up = path.resolve(cwd, '../').replace('\\', '/');
if (up === cwd) {
throw new Error('No yarn.lock found for this project');
}
cwd = up;
}
return cwd;
};
const appRoot = getAppRootPath();
const lockfilePath = path.join(appRoot, 'yarn.lock');
const packageJsonPath = path.join(appRoot, 'package.json');
const lockFileText = fs.readFileSync(lockfilePath, 'utf8');
const lockFile = lockfile.parse(lockFileText).object;
const packageJsonText = fs.readFileSync(packageJsonPath, 'utf8');
const packageJson = JSON.parse(packageJsonText);
// find all packages that have @* version in the lockfile
const astheriskPackages = Object.keys(lockFile)
.filter((k) => k.endsWith('@*'))
.map((k) => k.slice(0, -2));
// from @* versions, filter those that have explicity versions on the
// package.json
const haveParentVersions = astheriskPackages.filter(
(package) => packageJson.dependencies[package]
);
// make a map from @* to the package json version
const parentVersionsMap = new Map(
haveParentVersions.map((package) => [
package,
packageJson.dependencies[package]
])
);
const actualPackageVersion = (package) => lockFile[`${package}@*`];
const targetPackageVersion = (package) =>
lockFile[`${package}@${parentVersionsMap.get(package)}`];
// filter the packages that have @* version in the lockfile that are different
// from those in the package.json
const needsUpdate = haveParentVersions.filter(
(package) =>
actualPackageVersion(package).version !==
targetPackageVersion(package).version
);
needsUpdate.forEach((package) => {
console.warn(
`Package ${package} is supposed to be ${
targetPackageVersion(package).version
} but is ${actualPackageVersion(package).version}`
);
lockFile[`${package}@*`] = targetPackageVersion(package);
});
const newLockFile = lockfile.stringify(lockFile);
fs.writeFileSync(lockfilePath, newLockFile);
if (needsUpdate.length > 0) {
console.log(`Updated ${needsUpdate.length} packages versions on lockfile`);
} else {
console.log(`No packages need updating`);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment