volta icon indicating copy to clipboard operation
volta copied to clipboard

Globally installed `prettier` npm package cannot use plugin packages

Open jimeh opened this issue 1 year ago • 2 comments

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`),
};

jimeh avatar Jul 27 '22 23:07 jimeh

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 avatar Aug 11 '22 19:08 charlespierce

@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?

jimeh avatar Aug 15 '22 01:08 jimeh

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 avatar Dec 03 '22 03:12 tilgovi

@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.

charlespierce avatar Dec 03 '22 20:12 charlespierce

That's makes sense, thanks.

I also tested without Volta but I wasn't sure whether I had done something wrong.

tilgovi avatar Dec 04 '22 00:12 tilgovi

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

jhult avatar Aug 31 '23 02:08 jhult