Skip to content

pnpm GVS fs.allow detection misses monorepo-root .modules.yaml for nested workspace packages #22748

Description

@Hoffs

Describe the bug

The pnpm global-virtual-store fs.allow detection added in #22415 (shipped in [email protected]) does not fire for nested workspace packages in a pnpm monorepo.

In resolveServerOptions, Vite reads .modules.yaml from searchForPackageRoot(root):

const cwd = searchForPackageRoot(root);
const pnpmModulesYaml = path.join(cwd, "node_modules", ".modules.yaml");
try {
  const content = fs.readFileSync(pnpmModulesYaml, "utf-8");
  const virtualStoreDir = JSON.parse(content).virtualStoreDir;
  // ...push virtualStoreDir onto allowDirs
} catch {}

searchForPackageRoot returns the nearest dir containing a package.json. In a pnpm workspace the Vite project root is itself a workspace package, so cwd becomes that package (e.g. packages/app) — but .modules.yaml only exists at the monorepo root node_modules. The read ENOENTs into the empty catch {}, so virtualStoreDir is never added to server.fs.allow.

This only bites with enableGlobalVirtualStore: true, where deps symlink into a store outside the workspace (e.g. ~/.local/share/pnpm/store). Without GVS the virtual store sits under <repo>/node_modules/.pnpm, already covered by searchForWorkspaceRoot(root), so #22415 isn't needed there. With GVS it is needed and doesn't fire → the dev server returns 403 over /@fs for every dependency.

Suggested fix: resolve .modules.yaml from the workspace root rather than the package root — e.g. read it from searchForWorkspaceRoot(root), or walk up from cwd to the first ancestor whose node_modules/.modules.yaml exists, before giving up.

Reproduction

No hosted link — this is a filesystem / pnpm-store-location bug and isn't reproducible on StackBlitz / vite.new (WebContainers don't model a pnpm store outside the workspace). A complete, self-contained minimal repro is inlined under Steps to reproduce below; it's three small files and runs with pnpm install && pnpm --filter app repro. Happy to push it to a repo if that's preferred.

Steps to reproduce

Create this workspace (pnpm with enableGlobalVirtualStore: true, one nested package):

pnpm-workspace.yaml

enableGlobalVirtualStore: true
packages:
  - 'packages/*'

package.json

{ "name": "repro", "private": true, "version": "0.0.0" }

packages/app/package.json

{
  "name": "app",
  "private": true,
  "type": "module",
  "scripts": { "repro": "node repro.mjs" },
  "devDependencies": { "vite": "^8.1.0" }
}

packages/app/repro.mjs

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolveConfig, searchForWorkspaceRoot, version } from 'vite';

// searchForPackageRoot is not exported; inline it to show what
// resolveServerOptions uses as cwd for the .modules.yaml lookup.
function searchForPackageRoot(current, root = current) {
  if (fs.existsSync(path.join(current, 'package.json'))) return current;
  const dir = path.dirname(current);
  if (!dir || dir === current) return root;
  return searchForPackageRoot(dir, root);
}

const root = path.dirname(fileURLToPath(import.meta.url));
const config = await resolveConfig({ root, configFile: false, logLevel: 'silent' }, 'serve');
const viteReal = fs.realpathSync(path.join(root, 'node_modules', 'vite'));

console.log('vite version:                ', version);
console.log('searchForPackageRoot(root):  ', searchForPackageRoot(root), '(Vite reads .modules.yaml here)');
console.log('searchForWorkspaceRoot(root):', searchForWorkspaceRoot(root), '(where .modules.yaml lives)');
console.log('a dep resolves to:           ', viteReal);
console.log('resolved server.fs.allow:    ', config.server.fs.allow);

const covered = config.server.fs.allow.some((d) => viteReal.startsWith(d));
console.log('\n' + (covered ? 'PASS' : 'BUG: deps are OUTSIDE fs.allow -> dev server 403s over /@fs'));

Then:

pnpm install
pnpm --filter app repro

Observed output ([email protected], macOS, pnpm 11.7.0):

vite version:                 8.1.0
searchForPackageRoot(root):   /.../repro/packages/app (Vite reads .modules.yaml here)
searchForWorkspaceRoot(root): /.../repro (where .modules.yaml lives)
a dep resolves to:            /Users/me/.local/share/pnpm/store/v11/links/@/vite/8.1.0/.../node_modules/vite
resolved server.fs.allow:     [
  '/.../repro',
  '/Users/me/.local/share/pnpm/store/v11/links/@/vite/8.1.0/.../node_modules/vite/dist/client'
]

BUG: deps are OUTSIDE fs.allow -> dev server 403s over /@fs

The root .modules.yaml has virtualStoreDir: ../../../...Users/me/.local/share/pnpm/store/v11/links, but it's never read because Vite looks in packages/app/node_modules/.modules.yaml, which doesn't exist.

System Info

System:
  OS: macOS 26.5.1
  CPU: (11) arm64 Apple M3 Pro
Binaries:
  Node: 24.16.0
  pnpm: 11.7.0
npmPackages:
  vite: 8.1.0

Used Package Manager

pnpm

Validations

  • Follow our Code of Conduct
  • Read the Contributing Guidelines.
  • Read the docs.
  • Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
  • Make sure this is a Vite issue and not a framework-specific issue.
  • Check that this is a concrete bug.
  • The provided reproduction is a minimal reproducible example of the bug.

Metadata

Metadata

Assignees

No one assigned

    Labels

    has workaroundp2-edge-caseBug, but has workaround or limited in scope (priority)

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions