diff --git a/.changeset/fix-monorepo-vitest.md b/.changeset/fix-monorepo-vitest.md new file mode 100644 index 00000000..5805bcbb --- /dev/null +++ b/.changeset/fix-monorepo-vitest.md @@ -0,0 +1,6 @@ +--- +"varlock": patch +"@varlock/vite-integration": patch +--- + +Fix Vitest workspace projects in monorepos: when running Vitest from the monorepo root using the `projects` config, varlock now correctly resolves `.env.schema` and `.env` files from each child package's directory instead of only looking in the monorepo root. diff --git a/packages/integrations/vite/src/index.ts b/packages/integrations/vite/src/index.ts index 72f43b29..a044005f 100644 --- a/packages/integrations/vite/src/index.ts +++ b/packages/integrations/vite/src/index.ts @@ -54,11 +54,12 @@ function resetStaticReplacements() { let loadCount = 0; -function reloadConfig() { - debug('loading config - count =', ++loadCount); +function reloadConfig(cwd?: string) { + debug('loading config - count =', ++loadCount, cwd ? `(cwd: ${cwd})` : ''); try { const execResult = execSyncVarlock('load --format json-full --compact', { env: originalProcessEnv, + ...(cwd && { cwd }), }); process.env.__VARLOCK_ENV = execResult; varlockLoadedEnv = JSON.parse(process.env.__VARLOCK_ENV) as SerializedEnvGraph; @@ -140,11 +141,28 @@ See https://varlock.dev/integrations/vite/ for more details. isDevCommand = env.command === 'serve'; - // this gets re-triggered after .env file updates - // TODO: be smarter about only reloading if the env files changed? - if (isFirstLoad) { + // Determine the project root for the current Vite/Vitest project. + // In monorepo setups with Vitest workspace projects, config.root + // points to the child package directory rather than the monorepo root + // where process.cwd() points. We need to reload varlock from the + // correct directory so it can find .env.schema and .env files. + const projectRoot = config.root ? path.resolve(config.root) : undefined; + const rootDiffersFromCwd = !!(projectRoot && path.relative(projectRoot, process.cwd()) !== ''); + + if (rootDiffersFromCwd) { + // Always reload with the correct project root when it differs from + // cwd. This handles monorepo Vitest workspace setups where each child + // project has its own env files — even if the monorepo root also has + // env files (which would cause the initial module-level load to + // succeed with the wrong project's config). + reloadConfig(projectRoot); + } else if (isFirstLoad) { isFirstLoad = false; + // Roots match — the module-level reloadConfig() already loaded from + // the correct directory, no need to reload. } else if (isDevCommand) { + // Dev mode re-trigger (e.g., after .env file updates) + // TODO: be smarter about only reloading if the env files changed? reloadConfig(); } diff --git a/packages/varlock/src/lib/exec-sync-varlock.ts b/packages/varlock/src/lib/exec-sync-varlock.ts index d364bb5b..cfd2f155 100644 --- a/packages/varlock/src/lib/exec-sync-varlock.ts +++ b/packages/varlock/src/lib/exec-sync-varlock.ts @@ -62,6 +62,7 @@ export function execSyncVarlock( try { const result = execSync(`varlock ${command}`, { ...opts?.env && { env: opts.env }, + ...opts?.cwd && { cwd: opts.cwd }, stdio: 'pipe', }); return result.toString(); @@ -73,11 +74,13 @@ export function execSyncVarlock( // if varlock was not found, it either means it is not installed // or we must find the path to node_modules/.bin ourselves. - // Search from callerDir first (if provided), then from process.cwd(). + // Search from cwd (if provided), callerDir, then process.cwd(). // This handles monorepo setups where cwd may be an unrelated workspace // root while varlock is only installed in a sub-package - the callerDir // supplied by auto-load.ts points inside that sub-package's node_modules. + const cwdStr = opts?.cwd ? String(opts.cwd) : undefined; const searchDirs = [ + ...(cwdStr ? [cwdStr] : []), ...(opts?.callerDir ? [opts.callerDir] : []), process.cwd(), ];