mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
📦 Release qwen-code CLI as a Standalone Bundled Package (#866)
This commit is contained in:
258
packages/core/src/utils/ripgrepUtils.test.ts
Normal file
258
packages/core/src/utils/ripgrepUtils.test.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
|
||||
import {
|
||||
canUseRipgrep,
|
||||
ensureRipgrepPath,
|
||||
getRipgrepPath,
|
||||
} from './ripgrepUtils.js';
|
||||
import { fileExists } from './fileUtils.js';
|
||||
import path from 'node:path';
|
||||
|
||||
// Mock fileUtils
|
||||
vi.mock('./fileUtils.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('./fileUtils.js')>();
|
||||
return {
|
||||
...actual,
|
||||
fileExists: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('ripgrepUtils', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getRipgrepPath', () => {
|
||||
it('should return path with .exe extension on Windows', () => {
|
||||
const originalPlatform = process.platform;
|
||||
const originalArch = process.arch;
|
||||
|
||||
// Mock Windows x64
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
Object.defineProperty(process, 'arch', { value: 'x64' });
|
||||
|
||||
const rgPath = getRipgrepPath();
|
||||
|
||||
expect(rgPath).toContain('x64-win32');
|
||||
expect(rgPath).toContain('rg.exe');
|
||||
expect(rgPath).toContain(path.join('vendor', 'ripgrep'));
|
||||
|
||||
// Restore original values
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
Object.defineProperty(process, 'arch', { value: originalArch });
|
||||
});
|
||||
|
||||
it('should return path without .exe extension on macOS', () => {
|
||||
const originalPlatform = process.platform;
|
||||
const originalArch = process.arch;
|
||||
|
||||
// Mock macOS arm64
|
||||
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
||||
Object.defineProperty(process, 'arch', { value: 'arm64' });
|
||||
|
||||
const rgPath = getRipgrepPath();
|
||||
|
||||
expect(rgPath).toContain('arm64-darwin');
|
||||
expect(rgPath).toContain('rg');
|
||||
expect(rgPath).not.toContain('.exe');
|
||||
expect(rgPath).toContain(path.join('vendor', 'ripgrep'));
|
||||
|
||||
// Restore original values
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
Object.defineProperty(process, 'arch', { value: originalArch });
|
||||
});
|
||||
|
||||
it('should return path without .exe extension on Linux', () => {
|
||||
const originalPlatform = process.platform;
|
||||
const originalArch = process.arch;
|
||||
|
||||
// Mock Linux x64
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
Object.defineProperty(process, 'arch', { value: 'x64' });
|
||||
|
||||
const rgPath = getRipgrepPath();
|
||||
|
||||
expect(rgPath).toContain('x64-linux');
|
||||
expect(rgPath).toContain('rg');
|
||||
expect(rgPath).not.toContain('.exe');
|
||||
expect(rgPath).toContain(path.join('vendor', 'ripgrep'));
|
||||
|
||||
// Restore original values
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
Object.defineProperty(process, 'arch', { value: originalArch });
|
||||
});
|
||||
|
||||
it('should throw error for unsupported platform', () => {
|
||||
const originalPlatform = process.platform;
|
||||
const originalArch = process.arch;
|
||||
|
||||
// Mock unsupported platform
|
||||
Object.defineProperty(process, 'platform', { value: 'freebsd' });
|
||||
Object.defineProperty(process, 'arch', { value: 'x64' });
|
||||
|
||||
expect(() => getRipgrepPath()).toThrow('Unsupported platform: freebsd');
|
||||
|
||||
// Restore original values
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
Object.defineProperty(process, 'arch', { value: originalArch });
|
||||
});
|
||||
|
||||
it('should throw error for unsupported architecture', () => {
|
||||
const originalPlatform = process.platform;
|
||||
const originalArch = process.arch;
|
||||
|
||||
// Mock unsupported architecture
|
||||
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
||||
Object.defineProperty(process, 'arch', { value: 'ia32' });
|
||||
|
||||
expect(() => getRipgrepPath()).toThrow('Unsupported architecture: ia32');
|
||||
|
||||
// Restore original values
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
Object.defineProperty(process, 'arch', { value: originalArch });
|
||||
});
|
||||
|
||||
it('should handle all supported platform/arch combinations', () => {
|
||||
const originalPlatform = process.platform;
|
||||
const originalArch = process.arch;
|
||||
|
||||
const combinations: Array<{
|
||||
platform: string;
|
||||
arch: string;
|
||||
}> = [
|
||||
{ platform: 'darwin', arch: 'x64' },
|
||||
{ platform: 'darwin', arch: 'arm64' },
|
||||
{ platform: 'linux', arch: 'x64' },
|
||||
{ platform: 'linux', arch: 'arm64' },
|
||||
{ platform: 'win32', arch: 'x64' },
|
||||
];
|
||||
|
||||
combinations.forEach(({ platform, arch }) => {
|
||||
Object.defineProperty(process, 'platform', { value: platform });
|
||||
Object.defineProperty(process, 'arch', { value: arch });
|
||||
|
||||
const rgPath = getRipgrepPath();
|
||||
const binaryName = platform === 'win32' ? 'rg.exe' : 'rg';
|
||||
const expectedPathSegment = path.join(
|
||||
`${arch}-${platform}`,
|
||||
binaryName,
|
||||
);
|
||||
expect(rgPath).toContain(expectedPathSegment);
|
||||
});
|
||||
|
||||
// Restore original values
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
Object.defineProperty(process, 'arch', { value: originalArch });
|
||||
});
|
||||
});
|
||||
|
||||
describe('canUseRipgrep', () => {
|
||||
it('should return true if ripgrep binary exists', async () => {
|
||||
(fileExists as Mock).mockResolvedValue(true);
|
||||
|
||||
const result = await canUseRipgrep();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(fileExists).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should return false if ripgrep binary does not exist', async () => {
|
||||
(fileExists as Mock).mockResolvedValue(false);
|
||||
|
||||
const result = await canUseRipgrep();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(fileExists).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should return false if platform is unsupported', async () => {
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
// Mock unsupported platform
|
||||
Object.defineProperty(process, 'platform', { value: 'aix' });
|
||||
|
||||
const result = await canUseRipgrep();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(fileExists).not.toHaveBeenCalled();
|
||||
|
||||
// Restore original value
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
});
|
||||
|
||||
it('should return false if architecture is unsupported', async () => {
|
||||
const originalArch = process.arch;
|
||||
|
||||
// Mock unsupported architecture
|
||||
Object.defineProperty(process, 'arch', { value: 's390x' });
|
||||
|
||||
const result = await canUseRipgrep();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(fileExists).not.toHaveBeenCalled();
|
||||
|
||||
// Restore original value
|
||||
Object.defineProperty(process, 'arch', { value: originalArch });
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureRipgrepBinary', () => {
|
||||
it('should return ripgrep path if binary exists', async () => {
|
||||
(fileExists as Mock).mockResolvedValue(true);
|
||||
|
||||
const rgPath = await ensureRipgrepPath();
|
||||
|
||||
expect(rgPath).toBeDefined();
|
||||
expect(rgPath).toContain('rg');
|
||||
expect(fileExists).toHaveBeenCalledOnce();
|
||||
expect(fileExists).toHaveBeenCalledWith(rgPath);
|
||||
});
|
||||
|
||||
it('should throw error if binary does not exist', async () => {
|
||||
(fileExists as Mock).mockResolvedValue(false);
|
||||
|
||||
await expect(ensureRipgrepPath()).rejects.toThrow(
|
||||
/Ripgrep binary not found/,
|
||||
);
|
||||
await expect(ensureRipgrepPath()).rejects.toThrow(/Platform:/);
|
||||
await expect(ensureRipgrepPath()).rejects.toThrow(/Architecture:/);
|
||||
|
||||
expect(fileExists).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error with correct path information', async () => {
|
||||
(fileExists as Mock).mockResolvedValue(false);
|
||||
|
||||
try {
|
||||
await ensureRipgrepPath();
|
||||
// Should not reach here
|
||||
expect(true).toBe(false);
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
const errorMessage = (error as Error).message;
|
||||
expect(errorMessage).toContain('Ripgrep binary not found at');
|
||||
expect(errorMessage).toContain(process.platform);
|
||||
expect(errorMessage).toContain(process.arch);
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw error if platform is unsupported', async () => {
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
// Mock unsupported platform
|
||||
Object.defineProperty(process, 'platform', { value: 'openbsd' });
|
||||
|
||||
await expect(ensureRipgrepPath()).rejects.toThrow(
|
||||
'Unsupported platform: openbsd',
|
||||
);
|
||||
|
||||
// Restore original value
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
});
|
||||
});
|
||||
});
|
||||
114
packages/core/src/utils/ripgrepUtils.ts
Normal file
114
packages/core/src/utils/ripgrepUtils.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { fileExists } from './fileUtils.js';
|
||||
|
||||
// Get the directory of the current module
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
type Platform = 'darwin' | 'linux' | 'win32';
|
||||
type Architecture = 'x64' | 'arm64';
|
||||
|
||||
/**
|
||||
* Maps process.platform values to vendor directory names
|
||||
*/
|
||||
function getPlatformString(platform: string): Platform {
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
case 'linux':
|
||||
case 'win32':
|
||||
return platform;
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps process.arch values to vendor directory names
|
||||
*/
|
||||
function getArchitectureString(arch: string): Architecture {
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
case 'arm64':
|
||||
return arch;
|
||||
default:
|
||||
throw new Error(`Unsupported architecture: ${arch}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the bundled ripgrep binary for the current platform
|
||||
*/
|
||||
export function getRipgrepPath(): string {
|
||||
const platform = getPlatformString(process.platform);
|
||||
const arch = getArchitectureString(process.arch);
|
||||
|
||||
// Binary name includes .exe on Windows
|
||||
const binaryName = platform === 'win32' ? 'rg.exe' : 'rg';
|
||||
|
||||
// Path resolution:
|
||||
// When running from transpiled code: dist/src/utils/ripgrepUtils.js -> ../../../vendor/ripgrep/
|
||||
// When running from bundle: dist/index.js -> vendor/ripgrep/
|
||||
|
||||
// Detect if we're running from a bundle (single file)
|
||||
// In bundle, __filename will be something like /path/to/dist/index.js
|
||||
// In transpiled code, __filename will be /path/to/dist/src/utils/ripgrepUtils.js
|
||||
const isBundled = !__filename.includes(path.join('src', 'utils'));
|
||||
|
||||
const vendorPath = isBundled
|
||||
? path.join(
|
||||
__dirname,
|
||||
'vendor',
|
||||
'ripgrep',
|
||||
`${arch}-${platform}`,
|
||||
binaryName,
|
||||
)
|
||||
: path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'vendor',
|
||||
'ripgrep',
|
||||
`${arch}-${platform}`,
|
||||
binaryName,
|
||||
);
|
||||
|
||||
return vendorPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if ripgrep binary is available
|
||||
*/
|
||||
export async function canUseRipgrep(): Promise<boolean> {
|
||||
try {
|
||||
const rgPath = getRipgrepPath();
|
||||
return await fileExists(rgPath);
|
||||
} catch (_error) {
|
||||
// Unsupported platform/arch
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures ripgrep binary exists and returns its path
|
||||
* @throws Error if ripgrep binary is not available
|
||||
*/
|
||||
export async function ensureRipgrepPath(): Promise<string> {
|
||||
const rgPath = getRipgrepPath();
|
||||
|
||||
if (!(await fileExists(rgPath))) {
|
||||
throw new Error(
|
||||
`Ripgrep binary not found at ${rgPath}. ` +
|
||||
`Platform: ${process.platform}, Architecture: ${process.arch}`,
|
||||
);
|
||||
}
|
||||
|
||||
return rgPath;
|
||||
}
|
||||
Reference in New Issue
Block a user