Skip to content

Commit 1a4b0cf

Browse files
Copilotphilmillmantheoephraimclaude
authored
Fix varlock binary resolution for monorepo sub-packages (#547)
* Initial plan * Fix monorepo binary resolution in varlock/auto-load - Extract findVarlockBin() helper that walks up without premature throw when node_modules/.bin exists but lacks varlock (monorepo scenario) - Add callerDir option to execSyncVarlock for additional search start point - auto-load.ts passes import.meta.dirname as callerDir so binary search starts from inside the varlock package itself in node_modules Agent-Logs-Url: https://github.com/dmno-dev/varlock/sessions/b32f0fb8-9a46-4a26-9f47-8ec85420b88c Co-authored-by: philmillman <3722211+philmillman@users.noreply.github.com> * Fix import.meta.dirname fallback and Windows binary name - Add fallback to `new URL('.', import.meta.url).pathname` for older Node versions where `import.meta.dirname` is undefined - Use `varlock.exe` on Windows when searching node_modules/.bin Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: philmillman <3722211+philmillman@users.noreply.github.com> Co-authored-by: Theo Ephraim <theo@dmno.dev> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0c27ed1 commit 1a4b0cf

3 files changed

Lines changed: 66 additions & 20 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"varlock": patch
3+
---
4+
5+
Fix binary resolution in monorepos when `cwd` differs from the package root.
6+
7+
When importing `varlock/auto-load` (e.g. from a `playwright.config.ts` in a monorepo sub-package), VS Code and similar tools may set `process.cwd()` to the workspace root rather than the sub-package directory. This caused `execSyncVarlock` to search for the `varlock` binary starting at the workspace root and fail to find it when it was only installed in a sub-package's `node_modules/.bin`.
8+
9+
Two fixes are applied:
10+
11+
1. `execSyncVarlock` now accepts a `callerDir` option. When provided, the binary search walks up from `callerDir` before falling back to `process.cwd()`. `auto-load.ts` passes `import.meta.dirname` so the search always starts from inside the varlock package itself, which is already in the correct sub-package's `node_modules`.
12+
13+
2. The walk-up logic no longer throws immediately when it finds a `node_modules/.bin` directory that does not contain varlock. It now continues walking up, allowing the search to find varlock installed at a higher or lower level of a monorepo.

packages/varlock/src/auto-load.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import { patchGlobalResponse } from './runtime/patch-response';
1414
const execResult = execSyncVarlock('load --format json-full --compact', {
1515
exitOnError: true,
1616
showLogsOnError: true,
17+
// Pass the directory of this module so that in monorepos the binary search
18+
// starts from inside the varlock package (e.g. apps/web/node_modules/varlock)
19+
// rather than from process.cwd(), which may be an unrelated workspace root.
20+
callerDir: import.meta.dirname ?? new URL('.', import.meta.url).pathname,
1721
});
1822
process.env.__VARLOCK_ENV = execResult;
1923

packages/varlock/src/lib/exec-sync-varlock.ts

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,30 @@ const platform = os.platform();
1111
const isWindows = platform.match(/^win/i);
1212

1313

14+
/**
15+
* Walk up the directory tree from startDir looking for a node_modules/.bin/varlock binary.
16+
* Returns the full path to the binary if found, or null if not found.
17+
*/
18+
function findVarlockBin(startDir: string): string | null {
19+
let currentDir = startDir;
20+
while (currentDir) {
21+
const possibleBinPath = path.join(currentDir, 'node_modules', '.bin');
22+
if (fs.existsSync(possibleBinPath)) {
23+
const possibleVarlockPath = path.join(possibleBinPath, isWindows ? 'varlock.exe' : 'varlock');
24+
if (fs.existsSync(possibleVarlockPath)) {
25+
return possibleVarlockPath;
26+
}
27+
// Found a .bin directory but varlock is not in it - keep walking up.
28+
// In a monorepo the root node_modules/.bin may exist without varlock,
29+
// which is installed only in a sub-package.
30+
}
31+
const parentDir = path.dirname(currentDir);
32+
if (parentDir === currentDir) break;
33+
currentDir = parentDir;
34+
}
35+
return null;
36+
}
37+
1438
/**
1539
* small helper to call execSync and call the varlock cli
1640
*
@@ -22,6 +46,13 @@ export function execSyncVarlock(
2246
opts?: (Parameters<typeof execSyncType>[1] & {
2347
exitOnError?: boolean,
2448
showLogsOnError?: boolean,
49+
/**
50+
* Additional directory to start searching for the varlock binary from.
51+
* Searched before process.cwd(). Pass `import.meta.dirname` from the
52+
* call-site so that in monorepos the binary installed next to the
53+
* importing package is found even when cwd is an unrelated workspace root.
54+
*/
55+
callerDir?: string,
2556
}),
2657
) {
2758
try {
@@ -41,27 +72,25 @@ export function execSyncVarlock(
4172
}
4273

4374
// if varlock was not found, it either means it is not installed
44-
// or we must find the path to node_modules/.bin ourselves
45-
// so we'll walk up the directory tree looking for it
46-
let currentDir = process.cwd();
47-
while (currentDir) {
48-
const possibleBinPath = path.join(currentDir, 'node_modules', '.bin');
49-
if (fs.existsSync(possibleBinPath)) {
50-
const possibleVarlockPath = path.join(possibleBinPath, 'varlock');
51-
if (fs.existsSync(possibleVarlockPath)) {
52-
const result = execFileSync(possibleVarlockPath, command.split(' '), {
53-
...opts,
54-
stdio: 'pipe',
55-
});
56-
return result.toString();
57-
} else {
58-
throw new Error('Unable to find varlock executable');
59-
}
75+
// or we must find the path to node_modules/.bin ourselves.
76+
// Search from callerDir first (if provided), then from process.cwd().
77+
// This handles monorepo setups where cwd may be an unrelated workspace
78+
// root while varlock is only installed in a sub-package - the callerDir
79+
// supplied by auto-load.ts points inside that sub-package's node_modules.
80+
const searchDirs = [
81+
...(opts?.callerDir ? [opts.callerDir] : []),
82+
process.cwd(),
83+
];
84+
85+
for (const startDir of searchDirs) {
86+
const varlockPath = findVarlockBin(startDir);
87+
if (varlockPath) {
88+
const result = execFileSync(varlockPath, command.split(' '), {
89+
...opts,
90+
stdio: 'pipe',
91+
});
92+
return result.toString();
6093
}
61-
// when we reach the root, it will stop
62-
const parentDir = path.dirname(currentDir);
63-
if (parentDir === currentDir) break;
64-
currentDir = path.dirname(currentDir);
6594
}
6695
throw new Error('Unable to find varlock executable');
6796
} catch (err) {

0 commit comments

Comments
 (0)