volta
volta copied to clipboard
Globally installed `prettier` npm package cannot use plugin packages
When installing npm packages globally (npm install -g
), volta seems to isolate each package into a sandbox, where it has its own node_modules
directory.
With prettier
, there's a number of plugins available, both official and unofficial, which are installed as separate npm packages. But thanks to the sandboxing, prettier can't find any plugins. Details about how it finds plugins are available here: https://prettier.io/docs/en/plugins.html
I'm not sure if there is an obvious solution other than maybe be able to install multiple packages into the same sandbox, but I've not found an obvious way to do that.
So far prettier
is the only tool I've run into where this is an issue, but I imagine there will be other npm packages as well.
As for prettier
specifically, I've managed to work around the issue with a very dirty hack where I kind of abuse the prettierrc.js
config file to have it scan through ~/.volta/tools/image/packages
for prettier plugins:
const homedir = require('os').homedir();
const fs = require('fs');
function voltaPrettierSearchDirs(voltaDir) {
const packagesDir = `${voltaDir}/tools/image/packages`;
let paths = []
let parents = [];
fs.readdirSync(packagesDir).forEach((item) => {
if (/^prettier-plugin-[^/]+$/.test(item)) {
paths.push(`${packagesDir}/${item}/lib`);
}
if (item == '@prettier') {
paths = paths.concat(
findDirs(`${packagesDir}/@prettier`, /^plugin-[^/]+$/, "lib")
);
} else if (/^@[^/]+$/.test(item)) {
parents.push(`${packagesDir}/${item}`);
}
return [];
})
parents.forEach((parent) => {
paths = paths.concat(findDirs(parent, /^prettier-plugin-[^/]+$/, "lib"));
})
return paths;
}
function findDirs(parent, pattern, suffix) {
return fs.readdirSync(parent).flatMap((item) => {
const fp = `${parent}/${item}`;
if (pattern.test(item) && fs.statSync(fp).isDirectory()) {
if (suffix) {
return `${fp}/${suffix}`;
}
return fp;
}
return [];
});
}
module.exports = {
pluginSearchDirs: voltaPrettierSearchDirs(`${homedir}/.volta`),
};
Hi @jimeh, great question! The sandbox vs interop issue is one we've put a fair amount of work and thought into. It looks like our solution is just slightly different enough that it doesn't work out of the box for prettier, however. What we do with global packages, to make them visible to one another, is we have a directory $VOLTA_HOME/tools/shared
, which contains links to all of the package sandboxes, as if it were a global node_modules
directory. We also add that to the NODE_PATH
environment variable so that Node module resolution can find them when we run a global binary.
In the case of prettier, if I'm understanding the docs correctly, you should be able to leverage that same directory directly as pluginSearchDirs
and not have to do manual traversing of the individual sandbox directories:
module.exports = {
pluginSearchDirs: [`${homedir}/.volta/tools/shared`]
};
Let me know if that works, if not we can look at what we can do to make the plugins more discoverable for prettier.
@charlespierce Thanks for information, I'm afraid it looks like your suggestion doesn't work with prettier specifically though. It seems the pluginSearchDirs
option needs directories which contain a node_modules
directory, despite the docs saying you can point it at a node_modules
directory.
This is the value that my hacky config sets it to locally:
[
'/Users/jimeh/.volta/tools/image/packages/@prettier/plugin-php/lib',
'/Users/jimeh/.volta/tools/image/packages/@prettier/plugin-ruby/lib',
'/Users/jimeh/.volta/tools/image/packages/@prettier/plugin-xml/lib',
'/Users/jimeh/.volta/tools/image/packages/prettier-plugin-erb/lib',
'/Users/jimeh/.volta/tools/image/packages/prettier-plugin-toml/lib'
]
And here's a bunch of values I tested with a PHP file, which all failed to find the PHP plugin:
-
['/Users/jimeh/.volta/tools/shared']
-
['/Users/jimeh/.volta/tools/image/packages/@prettier/plugin-php/lib/node_modules']
-
['/Users/jimeh/.volta/tools/shared/@prettier/plugin-php']
So this probably needs reporting to prettier devs as well/instead.
However, on the topic of Volta sandboxes, are there any documentation around how the sandboxing works? I might have been blind, but I found almost nothing about it before I opened this issue. Also, if it is kinda possible to merge sandboxes, could options be exposed to manage/configure that for specific global packages that need it?
I have a similar question, which feels relevant also because it relates to prettier. I'm using an editor integration that tries to require('prettier')
and I'd like it to be able to find the Volta-installed prettier when it runs under the default toolchain's node
. I notice that the NODE_PATH
seems set for custom binaries but not for invocations of node
itself. Is that an oversight or a deliberate choice?
@tilgovi That's actually a deliberate choice to attempt to match the behavior of Node itself with global packages. Outside of Volta's behavior, when running node
, calling require('package')
will fail when package
is an arbitrary package that was installed globally with e.g. npm i -g
. This is because AFAIK the global node_modules
directory is not searched by default, nor is it in the typical tree hierarchy that Node searches when running a script*. However, if you are running a tool that was itself installed globally, then it does work, because searching up from where the tool was installed will include the global node_modules
directory and all of the sibling global packages. So that's why we made the choice to add NODE_PATH
only when running a global binary.
- This was the case when we tested it as we were implementing the behavior, though it's been a while since then.
That's makes sense, thanks.
I also tested without Volta but I wasn't sure whether I had done something wrong.
https://prettier.io/blog/2023/07/05/3.0.0.html#plugin-search-feature-has-been-removed-14759
https://github.com/prettier/prettier/pull/14759
https://github.com/prettier/plugin-xml/issues/714