Skip to content

Instantly share code, notes, and snippets.

@gouldingken
Created December 1, 2022 21:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gouldingken/332abc5f95a9e59c685cb15461b698e1 to your computer and use it in GitHub Desktop.
Save gouldingken/332abc5f95a9e59c685cb15461b698e1 to your computer and use it in GitHub Desktop.
Workaround for pnpm global link bug

pnpm global link bug

This is a workaround for a pretty serious bug in pnpm that prevents you from using global links (or more accurately prevents you from installing anything once you use a global link).

For more on the bug - see here pnpm/pnpm#3584

Setup

Add the following Node.js files to a _dev folder at the root of your repo:

  • pnpm-post.js
  • pnpm-pre.js

Add these lines to package.json:

"postinstall": "node _dev/pnpm-post.js",
"installer": "node _dev/pnpm-pre.js && pnpm install",

Usage

At the root, run: pnpm run installer

instead of pnpm install

const fs = require('fs');
const path = require("path");
const runScript = (script) => {
const child_process = require('child_process');
child_process.execSync(script, {stdio: [0, 1, 2]});
};
const processDir = async (dir) => {
let files = await fs.promises.readdir(dir, {withFileTypes: true});
for (let f of files) {
let fullPath = path.join(dir, f.name);
if (f.isDirectory()) {
if (f.name === '.git' || f.name === '.idea' || f.name === '_dev' || f.name === 'node_modules') {
continue;
}
await processDir(fullPath);
} else {
if (f.name === 'package.json') {
console.log('FOUND package.json', fullPath);
const packageStr = await fs.promises.readFile(fullPath);
const packageData = JSON.parse(packageStr);
if (packageData.stashedGlobalDependencies) {
if (!packageData.dependencies) packageData.dependencies = {};
console.log('Setting dir', dir);
process.chdir(dir);
//I think it's ok to just run these and not wait for a result because this is a post-install
Object.keys(packageData.stashedGlobalDependencies).forEach((depKey) => {
console.log('Relinking:', depKey);
runScript(`pnpm link -g ${depKey}`);
packageData.dependencies[depKey] = packageData.stashedGlobalDependencies[depKey];
});
delete packageData.stashedGlobalDependencies;
//NOTE: this overwrites the update that was just written via pnpm link so version changes could happen...
//there may be a better approach that waits for that install to complete, then checks the version etc
await fs.promises.writeFile(fullPath, JSON.stringify(packageData, null, 2));
}
}
}
}
};
let baseDir = path.resolve(__dirname,'../');
console.log('RUNNING POST INSTALL...', baseDir);
processDir(baseDir);
const fs = require('fs');
const path = require('path');
console.log('RUNNING PREINSTALL');
const isIncluded = (f) => {
//NOTE write any custom inclusion rules for folders here so it doesn't have to scan EVERY folder
return true;
}
const saveJson = async (filePath, data) => {
await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2));
console.log('Saved ' + filePath);
}
const processNodeModules = async (dir, prefix = '') => {
// console.log('processNodeModules', dir);
let files = await fs.promises.readdir(dir, {withFileTypes: true});
const ans = [];
for (let f of files) {
if (f.isSymbolicLink()) {
let full_path = path.join(dir, f.name);
// console.log(full_path);
try {
const linkString = await fs.promises.readlink(full_path);
if (linkString.indexOf('.pnpm') < 0) {
console.log('global LINKED', dir, linkString, full_path);
ans.push({name: prefix + f.name, path: full_path});
}
} catch (err) {
console.error(err);
}
}
if (isIncluded(f)) {
let full_path = path.join(dir, f.name);
ans.push(...await processNodeModules(full_path, f.name + '/'));
}
}
return ans;
};
const isWorkspaceRef = (versionString) => {
return versionString.indexOf('*') > -1;
};
const stashGlobalDependencies = async (packageInfo) => {
const packageStr = await fs.promises.readFile(packageInfo.packageJson);
const packageData = JSON.parse(packageStr);
packageInfo.globalDependencies.forEach((dep, i) => {
let versionString = packageData.dependencies[dep.name];
if (versionString && !isWorkspaceRef(versionString)) {
if (!packageData.stashedGlobalDependencies) packageData.stashedGlobalDependencies = {};
packageData.stashedGlobalDependencies[dep.name] = versionString;
delete packageData.dependencies[dep.name];
}
});
await saveJson(packageInfo.packageJson, packageData);
};
const processDir = async (dir) => {
let files = await fs.promises.readdir(dir, {withFileTypes: true});
const packageInfo = {globalDependencies: []};
for (let f of files) {
let full_path = path.join(dir, f.name);
if (f.isDirectory()) {
if (f.name === '.git' || f.name === '.idea' || f.name === '_dev') {
continue;
}
if (f.name === 'node_modules') {
packageInfo.globalDependencies.push(...await processNodeModules(full_path));
} else {
await processDir(full_path);
}
} else {
if (f.name === 'package.json') {
// console.log('FOUND package.json', path.join(dir, f.name));
packageInfo.packageJson = path.join(dir, f.name);
}
}
}
if (packageInfo.globalDependencies.length > 0) {
// console.log('PACKAGE INFO: ', dir, JSON.stringify(packageInfo));
await stashGlobalDependencies(packageInfo);
}
};
const findLinks = async () => {
//find all package.json
//look through node_modules
//find all links that don't have a .pnpm or node_modules
//these are the global dependencies that should be deleted from the package.json
//we store these in a 'stashedGlobalDependencies' custom field on package.json
//the post install will run `pnpm install -g` on all these stashedGlobalDependencies
let baseDir = path.resolve(__dirname, '../');
console.log('PROCESSING...', baseDir);
await processDir(baseDir);
console.log('DONE PROCESSING ', baseDir);
process.exit();
};
findLinks();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment