rs-runner
Version:
RS is a CLI tool for quickly detecting package.json scripts, and running them.
270 lines (216 loc) • 8.72 kB
text/typescript
import * as fs from 'fs';
import * as os from 'os';
import path from 'path';
import { detectRunner, getPackageJsonScripts } from '../src/lib/pm';
import { getConfig, writeConfig } from '../src/lib/config';
import {
getGlobalScripts,
getDirectoryScripts,
addNewGlobalScript,
removeGlobalScript,
addNewDirectoryScript,
removeDirectoryScript,
} from '../src/lib/scripts';
import { Config } from '../src/types';
jest.mock('fs');
jest.mock('os');
jest.mock('../src/lib/output');
describe('integration tests', () => {
const mockHomedir = '/home/testuser';
const mockCwd = '/home/testuser/projects/myapp';
const configDir = path.join(mockHomedir, '.rs-runner');
const configPath = path.join(configDir, 'config.json');
let cwdSpy: jest.SpyInstance;
beforeEach(() => {
jest.clearAllMocks();
(os.homedir as jest.Mock).mockReturnValue(mockHomedir);
cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue(mockCwd);
});
afterEach(() => {
cwdSpy.mockRestore();
});
describe('multi-package-manager scenarios', () => {
it('should detect correct PM when multiple projects exist', () => {
// Simulate being in an npm project
(fs.existsSync as jest.Mock).mockImplementation(
(file) => file === 'package-lock.json',
);
expect(detectRunner()).toBe('npm');
// Switch to pnpm project
(fs.existsSync as jest.Mock).mockImplementation(
(file) => file === 'pnpm-lock.yaml',
);
expect(detectRunner()).toBe('pnpm');
});
it('should handle package.json scripts across different PM contexts', () => {
const npmProject = {
scripts: { start: 'node index.js', test: 'jest' },
};
(fs.existsSync as jest.Mock).mockReturnValue(true);
(fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(npmProject));
const scripts = getPackageJsonScripts();
expect(scripts).toEqual(npmProject.scripts);
});
it('should prioritize npm over yarn when both lock files exist', () => {
(fs.existsSync as jest.Mock).mockImplementation(
(file) => file === 'package-lock.json' || file === 'yarn.lock',
);
expect(detectRunner()).toBe('npm');
});
});
describe('directory script resolution', () => {
it('should isolate scripts between different directories', () => {
const project1Dir = '/home/testuser/projects/app1';
const project2Dir = '/home/testuser/projects/app2';
const fullConfig: Config = {
globalScripts: {},
directoryScripts: {
[project1Dir]: { dev: 'vite dev' },
[project2Dir]: { dev: 'next dev' },
},
};
(fs.existsSync as jest.Mock).mockReturnValue(true);
(fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(fullConfig));
// In project1
cwdSpy.mockReturnValue(project1Dir);
expect(getDirectoryScripts()).toEqual({ dev: 'vite dev' });
// In project2
cwdSpy.mockReturnValue(project2Dir);
expect(getDirectoryScripts()).toEqual({ dev: 'next dev' });
// In unknown directory
cwdSpy.mockReturnValue('/other/path');
expect(getDirectoryScripts()).toEqual({});
});
it('should maintain global scripts across directories', () => {
const fullConfig: Config = {
globalScripts: { lint: 'eslint .', format: 'prettier --write .' },
directoryScripts: {},
};
(fs.existsSync as jest.Mock).mockReturnValue(true);
(fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(fullConfig));
cwdSpy.mockReturnValue('/any/directory');
expect(getGlobalScripts()).toEqual(fullConfig.globalScripts);
cwdSpy.mockReturnValue('/another/path');
expect(getGlobalScripts()).toEqual(fullConfig.globalScripts);
});
});
describe('config file edge cases', () => {
it('should handle empty config file gracefully', () => {
(fs.existsSync as jest.Mock).mockReturnValue(true);
(fs.readFileSync as jest.Mock).mockReturnValue('{}');
expect(getConfig()).toBeNull();
});
it('should handle config with only globalScripts', () => {
const config = { globalScripts: { test: 'jest' } };
(fs.existsSync as jest.Mock).mockReturnValue(true);
(fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(config));
expect(getConfig()).toEqual(config);
expect(getGlobalScripts()).toEqual({ test: 'jest' });
});
it('should preserve other directories when adding to one', () => {
const existingConfig: Config = {
globalScripts: {},
directoryScripts: {
'/project/a': { build: 'npm run build' },
'/project/b': { test: 'npm test' },
},
};
cwdSpy.mockReturnValue('/project/c');
(fs.existsSync as jest.Mock).mockReturnValue(true);
(fs.readFileSync as jest.Mock)
.mockReturnValueOnce(JSON.stringify(existingConfig))
.mockReturnValueOnce(
JSON.stringify({
...existingConfig,
directoryScripts: {
...existingConfig.directoryScripts,
'/project/c': { dev: 'vite' },
},
}),
);
addNewDirectoryScript('dev', 'vite');
expect(fs.writeFileSync).toHaveBeenCalledWith(
configPath,
expect.stringContaining('/project/a'),
'utf8',
);
expect(fs.writeFileSync).toHaveBeenCalledWith(
configPath,
expect.stringContaining('/project/b'),
'utf8',
);
});
it('should handle concurrent global and directory script operations', () => {
let storedConfig: Config = {
globalScripts: { existing: 'command' },
directoryScripts: {},
};
(fs.existsSync as jest.Mock).mockReturnValue(true);
(fs.readFileSync as jest.Mock).mockImplementation(() =>
JSON.stringify(storedConfig),
);
(fs.writeFileSync as jest.Mock).mockImplementation((_, content) => {
storedConfig = JSON.parse(content as string);
});
// Add global script
addNewGlobalScript('lint', 'eslint .');
// Verify global script was added
expect(storedConfig.globalScripts).toHaveProperty('lint');
expect(storedConfig.globalScripts.existing).toBe('command');
// Add directory script
addNewDirectoryScript('dev', 'vite dev');
// Verify both are preserved
expect(storedConfig.globalScripts).toHaveProperty('lint');
expect(storedConfig.directoryScripts[mockCwd]).toHaveProperty('dev');
});
it('should handle removal of last script in directory', () => {
const config: Config = {
globalScripts: {},
directoryScripts: {
[mockCwd]: { onlyScript: 'echo test' },
},
};
(fs.existsSync as jest.Mock).mockReturnValue(true);
(fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(config));
removeDirectoryScript('onlyScript');
const writeCall = (fs.writeFileSync as jest.Mock).mock.calls[0];
const writtenConfig = JSON.parse(writeCall[1]);
expect(writtenConfig.directoryScripts[mockCwd]).toBeUndefined();
});
it('should handle special characters in script commands', () => {
const config: Config = {
globalScripts: {},
directoryScripts: {},
};
(fs.existsSync as jest.Mock).mockReturnValue(true);
(fs.readFileSync as jest.Mock)
.mockReturnValueOnce(JSON.stringify(config))
.mockReturnValueOnce(
JSON.stringify({
...config,
globalScripts: {
complex: 'echo "hello world" && npm run test -- --coverage',
},
}),
);
addNewGlobalScript(
'complex',
'echo "hello world" && npm run test -- --coverage',
);
const writeCall = (fs.writeFileSync as jest.Mock).mock.calls[0];
const writtenConfig = JSON.parse(writeCall[1]);
expect(writtenConfig.globalScripts.complex).toBe(
'echo "hello world" && npm run test -- --coverage',
);
});
it('should handle config directory creation on first use', () => {
(fs.existsSync as jest.Mock).mockImplementation((p) => {
if (p === configDir) return false;
if (p === configPath) return false;
return true;
});
getConfig();
expect(fs.mkdirSync).toHaveBeenCalledWith(configDir, { recursive: true });
});
});
});