Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/binary-version-mismatch-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"varlock": patch
---

Add version mismatch detection between standalone binary and local node_modules install

When running the standalone binary (installed via homebrew/curl), varlock now checks if a different version is installed in the project's node_modules. If a version mismatch is detected, a warning is displayed suggesting users update the binary or use the locally installed version instead. This helps prevent confusing errors caused by running mismatched versions.
10 changes: 10 additions & 0 deletions packages/varlock/src/cli/cli-executable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { fmt } from './helpers/pretty-format';
import { trackCommand, trackInstall } from './helpers/telemetry';
import { InvalidEnvError } from './helpers/error-checks';
import { checkBunVersion } from '../lib/check-bun-version';
import { checkLocalVersionMismatch } from '../lib/check-local-version';
import packageJson from '../../package.json';

// we'll import just the spec from each, so the implementations can be lazy loaded
Expand Down Expand Up @@ -85,6 +86,15 @@ subCommands.set('typegen', buildLazyCommand(typegenCommandSpec, async () => awai
await trackCommand('version');
}

// warn if standalone binary version differs from local node_modules install
// skip for --version/--help since those are quick informational commands
if (__VARLOCK_SEA_BUILD__ && args[0] !== '--version' && args[0] !== '--help') {
const versionMismatchWarning = checkLocalVersionMismatch(packageJson.version);
if (versionMismatchWarning) {
console.warn(`\n⚠️ ${versionMismatchWarning}\n`);
}
}

await cli(args, {
// main command - triggered if you just run `varlock` with no args
run: () => {
Expand Down
41 changes: 41 additions & 0 deletions packages/varlock/src/lib/check-local-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import path from 'node:path';
import fs from 'node:fs';

/**
* When running as a standalone binary (SEA build), checks if varlock is also
* installed in a local node_modules directory. If found and the versions differ,
* returns a warning message to alert the user about the mismatch.
*
* This helps prevent confusing errors when users have both a standalone binary
* (e.g. installed via homebrew/curl) and a project-level npm install with
* different versions.
*/
export function checkLocalVersionMismatch(currentVersion: string): string | undefined {
// Walk up from cwd looking for node_modules/varlock/package.json
let currentDir = process.cwd();
while (true) {
const localPkgJsonPath = path.join(currentDir, 'node_modules', 'varlock', 'package.json');
if (fs.existsSync(localPkgJsonPath)) {
try {
const localPkgJson = JSON.parse(fs.readFileSync(localPkgJsonPath, 'utf-8'));
const localVersion = localPkgJson.version;
if (localVersion && localVersion !== currentVersion) {
return 'Varlock version mismatch detected!\n'
+ ` Standalone binary version: ${currentVersion}\n`
+ ` Local installed version: ${localVersion}\n`
+ 'You are running the standalone binary, but a different version of varlock is installed in this project\'s node_modules.\n'
+ 'This can cause unexpected errors. Please update your standalone binary or use the locally installed version instead\n'
+ '(e.g. via npx varlock, pnpm exec varlock, or bunx varlock).';
}
} catch {
// If we can't read/parse the package.json, just skip the check
}
// Found node_modules/varlock - stop walking regardless of outcome
break;
}
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) break;
currentDir = parentDir;
}
return undefined;
}
124 changes: 124 additions & 0 deletions packages/varlock/src/lib/test/check-local-version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import {
describe, it, expect, afterEach,
} from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { checkLocalVersionMismatch } from '../check-local-version';

describe('checkLocalVersionMismatch', () => {
const tmpDirs: Array<string> = [];

function createTempProject(localVarlockVersion?: string): string {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'varlock-test-'));
tmpDirs.push(tmpDir);

if (localVarlockVersion) {
const varlockDir = path.join(tmpDir, 'node_modules', 'varlock');
fs.mkdirSync(varlockDir, { recursive: true });
fs.writeFileSync(
path.join(varlockDir, 'package.json'),
JSON.stringify({ name: 'varlock', version: localVarlockVersion }),
);
}

return tmpDir;
}

function withCwd(dir: string, fn: () => void) {
const originalCwd = process.cwd();
process.chdir(dir);
try {
fn();
} finally {
process.chdir(originalCwd);
}
}

afterEach(() => {
for (const dir of tmpDirs) {
fs.rmSync(dir, { recursive: true, force: true });
}
tmpDirs.length = 0;
});

it('should return undefined when no node_modules/varlock exists', () => {
const tmpDir = createTempProject();
withCwd(tmpDir, () => {
expect(checkLocalVersionMismatch('1.0.0')).toBeUndefined();
});
});

it('should return undefined when versions match', () => {
const tmpDir = createTempProject('1.0.0');
withCwd(tmpDir, () => {
expect(checkLocalVersionMismatch('1.0.0')).toBeUndefined();
});
});

it('should return a warning when versions differ', () => {
const tmpDir = createTempProject('0.6.3');
withCwd(tmpDir, () => {
const result = checkLocalVersionMismatch('0.4.0');
expect(result).toBeDefined();
expect(result).toContain('0.4.0');
expect(result).toContain('0.6.3');
expect(result).toContain('mismatch');
});
});

it('should include the standalone binary version in the warning', () => {
const tmpDir = createTempProject('2.0.0');
withCwd(tmpDir, () => {
const result = checkLocalVersionMismatch('1.0.0');
expect(result).toContain('Standalone binary version: 1.0.0');
});
});

it('should include the local installed version in the warning', () => {
const tmpDir = createTempProject('2.0.0');
withCwd(tmpDir, () => {
const result = checkLocalVersionMismatch('1.0.0');
expect(result).toContain('Local installed version: 2.0.0');
});
});

it('should suggest using locally installed version', () => {
const tmpDir = createTempProject('2.0.0');
withCwd(tmpDir, () => {
const result = checkLocalVersionMismatch('1.0.0');
expect(result).toContain('npx varlock');
});
});

it('should find node_modules in parent directory', () => {
const tmpDir = createTempProject('2.0.0');
const subDir = path.join(tmpDir, 'src', 'app');
fs.mkdirSync(subDir, { recursive: true });
withCwd(subDir, () => {
const result = checkLocalVersionMismatch('1.0.0');
expect(result).toBeDefined();
expect(result).toContain('2.0.0');
});
});

it('should handle malformed package.json gracefully', () => {
const tmpDir = createTempProject();
const varlockDir = path.join(tmpDir, 'node_modules', 'varlock');
fs.mkdirSync(varlockDir, { recursive: true });
fs.writeFileSync(path.join(varlockDir, 'package.json'), 'not valid json');
withCwd(tmpDir, () => {
expect(checkLocalVersionMismatch('1.0.0')).toBeUndefined();
});
});

it('should handle package.json without version field', () => {
const tmpDir = createTempProject();
const varlockDir = path.join(tmpDir, 'node_modules', 'varlock');
fs.mkdirSync(varlockDir, { recursive: true });
fs.writeFileSync(path.join(varlockDir, 'package.json'), JSON.stringify({ name: 'varlock' }));
withCwd(tmpDir, () => {
expect(checkLocalVersionMismatch('1.0.0')).toBeUndefined();
});
});
});
Loading