mrm
Version:
Codemods for your project config files
595 lines (525 loc) • 16.1 kB
JavaScript
// @ts-check
/* eslint-disable no-console */
const path = require('path');
const {
promiseFirst,
tryFile,
resolveUsingNpx,
getConfigFromFile,
getConfigFromCommandLine,
getConfig,
getTaskOptions,
runTask,
runAlias,
run,
getAllAliases,
getAllTasks,
getPackageName,
} = require('../index');
const configureInquirer = require('../../test/inquirer-mock');
const task1 = require('../../test/dir1/task1');
const task2 = require('../../test/dir1/task2');
const task3 = require('../../test/dir2/task3');
const task4 = require('../../test/dir2/task4');
const task5 = require('../../test/dir2/task5');
// interactive config tasks
const task6 = require('../../test/dir3/task6');
const task8 = require('../../test/dir5/task8');
const configFile = 'config.json';
const directories = [
path.resolve(__dirname, '../../test/dir1'),
path.resolve(__dirname, '../../test/dir2'),
path.resolve(__dirname, '../../test/dir3'),
path.resolve(__dirname, '../../test/dir4'),
];
const presetDir = [path.resolve(__dirname, '../../test/dir6')];
const options = {
pizza: 'salami',
};
const optionsWithAliases = {
aliases: {
alias1: ['task1', 'task3', 'task4'],
alias2: ['task4', 'task5'],
alias3: ['task1', 'alias2'],
},
};
const argv = {
_: [],
div: '~/pizza',
'config:foo': 42,
'config:bar': 'coffee',
};
const file = name => path.join(__dirname, '../../test', name);
describe('promiseFirst', () => {
it('should return the first resolving function', async () => {
const result = await promiseFirst([
() => Promise.reject(),
() => Promise.reject(),
() => Promise.resolve('pizza'),
() => Promise.reject(),
() => Promise.reject('cappuccino'),
]);
expect(result).toMatch('pizza');
});
it('should return reject if no resolving function is found', () => {
const result = promiseFirst([
() => Promise.reject(),
() => Promise.reject(),
() => Promise.reject(),
]);
return expect(result).rejects.toHaveProperty(
'message',
'None of the 3 thunks resolved.\n\n\n\n'
);
});
});
describe('tryFile', () => {
it('should return an absolute file path if the file exists', async () => {
let result;
result = await tryFile(directories, 'task1/index.js');
expect(result).toBe(file('dir1/task1/index.js'));
result = await tryFile(directories, 'task3/index.js');
expect(result).toBe(file('dir2/task3/index.js'));
});
it('should return undefined if the file doesn’t exist', () => {
const result = tryFile([], 'pizza');
// ideally we can use toThrowError but that works with >= jest@22
// https://github.com/facebook/jest/issues/5076
return expect(result).rejects.toHaveProperty(
'message',
'File “pizza” not found.'
);
});
});
describe('resolveUsingNpx', () => {
it('should install an npm module transparently', async () => {
const result = await resolveUsingNpx('yarnhook');
expect(result).toMatch(
/\.npm\/_npx\/\d*\/lib(64)?\/node_modules\/yarnhook\/index\.js$/
);
});
it('should throw if npm module is not found on the registry', () => {
const result = resolveUsingNpx('this-package-is-not-on-npm');
return expect(result).rejects.toHaveProperty(
'message',
`Install for this-package-is-not-on-npm failed with code 1`
);
});
});
describe('getPackageName', () => {
it('should resolve non-scoped task names', () => {
const result = getPackageName('task', 'pizza');
expect(result).toEqual('mrm-task-pizza');
});
it('should resolve non-scoped preset names', () => {
const result = getPackageName('preset', 'default');
expect(result).toEqual('mrm-preset-default');
});
it('should resolve scoped task names', () => {
const result = getPackageName('task', '@myorg/pizza');
expect(result).toEqual('@myorg/mrm-task-pizza');
});
it('should resolve scoped preset names', () => {
const result = getPackageName('preset', '@myorg/default');
expect(result).toEqual('@myorg/mrm-preset-default');
});
});
describe('getConfigFromFile', () => {
it('should return a config object', async () => {
const result = await getConfigFromFile(directories, configFile);
expect(result).toMatchObject(options);
});
it('should return an empty object when file not found', async () => {
const result = await getConfigFromFile(directories, 'pizza');
expect(result).toEqual({});
});
});
describe('getConfigFromCommandLine', () => {
it('should return a config object', () => {
const result = getConfigFromCommandLine(argv);
expect(result).toEqual({
foo: 42,
bar: 'coffee',
});
});
it('should return an empty object when no config options found', () => {
const result = getConfigFromCommandLine({
_: [],
div: '~/pizza',
});
expect(result).toEqual({});
});
});
describe('getConfig', () => {
it('should return a config object', async () => {
const result = await getConfig(directories, configFile, argv);
expect(result).toMatchObject({
pizza: 'salami',
foo: 42,
bar: 'coffee',
});
});
it('should return an empty object when file not found and no CLI options provided', async () => {
const result = await getConfig(directories, 'pizza', {});
expect(result).toEqual({});
});
it('CLI options should override options from config file', async () => {
const result = await getConfig(directories, configFile, {
'config:pizza': 'quattro formaggi',
});
expect(result).toMatchObject({
pizza: 'quattro formaggi',
});
});
});
describe('getTaskOptions', () => {
const third = task6.parameters['third-config'];
beforeEach(() => third.default.mockClear());
describe('interactive mode', () => {
it('should prompt values', async () => {
configureInquirer({
'first-config': 'first value',
'second-config': 'second value',
// 'third-config': keep default
});
const answers = await getTaskOptions(task6, true, {
'second-config': 'second value',
});
expect(answers).toEqual({
'first-config': 'first value',
'second-config': 'second value',
'third-config': 'eulav dnoces',
});
});
it('should respect parameters default values', async () => {
configureInquirer({});
const answers = await getTaskOptions(task6, true);
expect(answers).toEqual({
'first-config': '',
'second-config': 'default value',
'third-config': '',
});
});
it('should be possible to override parameters default values', async () => {
configureInquirer({});
const answers = await getTaskOptions(task6, true, {
'first-config': 'initial',
});
expect(answers).toEqual({
'first-config': 'initial',
'second-config': 'default value',
'third-config': '',
});
});
it('should use default values for static options and do not propmpt them', async () => {
configureInquirer({
'interactive-config': 'pizza',
'static-config': 'second value', // this value shouldn't be used
});
const answers = await getTaskOptions(task8, true);
expect(answers).toEqual({
'interactive-config': 'pizza',
'static-config': 'default value',
});
});
});
describe('non-interactive mode', () => {
it('should use default values for static options', async () => {
const answers = await getTaskOptions(task6, false);
expect(answers).toEqual({
'first-config': undefined,
'second-config': 'default value',
'third-config': undefined,
});
});
it('should NOT prompt when interactive mode is disabled', async () => {
configureInquirer({
'first-config': 'should not be used',
'second-config': 'should not be used',
'third-config': 'should not be used',
});
const answers = await getTaskOptions(task6, false);
expect(answers).toEqual({
'first-config': undefined,
'second-config': 'default value',
'third-config': undefined,
});
});
it('should validate options: valid', async () => {
const answers = await getTaskOptions(task8, false, {
'interactive-config': 'pizza',
});
expect(answers).toEqual({
'interactive-config': 'pizza',
'static-config': 'default value',
});
});
it('should validate options: invalid', async () => {
try {
await getTaskOptions(task8, false);
} catch (err) {
expect(err.message).toBe(
'Missing required config options: interactive-config.'
);
}
});
});
});
describe('runTask', () => {
beforeEach(() => {
task1.mockClear();
task4.mockClear();
task6.mockClear();
});
it('should run a module', () => {
return new Promise((resolve, reject) => {
runTask('task1', directories, {}, {})
.then(() => {
expect(task1).toHaveBeenCalledTimes(1);
resolve();
})
.catch(reject);
});
});
it('should pass a config function and a params object to a module', () => {
return new Promise((resolve, reject) => {
runTask('task1', directories, options, { interactive: false })
.then(() => {
expect(task1).toHaveBeenCalledWith(
expect.objectContaining({ pizza: 'salami' }),
{ interactive: false }
);
resolve();
})
.catch(reject);
});
});
it('should throw when module not found', () => {
const pizza = runTask('this-does-not-exist-on-npm', directories, {}, {});
// ideally we can use toThrowError but that works with >= jest@22
// https://github.com/facebook/jest/issues/5076
return expect(pizza).rejects.toHaveProperty(
'message',
'Task “this-does-not-exist-on-npm” not found.'
);
}, 20000);
it('should throw when task module is invalid', () => {
const result = runTask('task7', directories, {}, {});
// ideally we can use toThrowError but that works with >= jest@22
// https://github.com/facebook/jest/issues/5076
return expect(result).rejects.toHaveProperty(
'message',
'Cannot call task “task7”.'
);
});
it('should run an async module', () => {
return new Promise((resolve, reject) => {
runTask('task4', directories, {}, { stack: [] })
.then(() => {
expect(task4).toHaveBeenCalledTimes(1);
expect(task4.mock.calls[0][1].stack).toEqual(['Task 2.4']);
resolve();
})
.catch(reject);
});
});
it('should prompt interactive configs when interactive mode is on', async () => {
configureInquirer({
'first-config': 'value',
'second-config': 'other value',
});
await runTask('task6', directories, {}, { interactive: true });
expect(task6).toHaveBeenCalledTimes(1);
expect(task6).toHaveBeenCalledWith(
expect.objectContaining({
'first-config': 'value',
'second-config': 'other value',
}),
{ interactive: true }
);
});
it('should respect config defaults from task parameters when interactive mode is on', async () => {
configureInquirer({ 'first-config': 'value' });
await runTask('task6', directories, {}, { interactive: true });
expect(task6).toHaveBeenCalledTimes(1);
expect(task6).toHaveBeenCalledWith(
expect.objectContaining({
'first-config': 'value',
'second-config': 'default value',
}),
{ interactive: true }
);
});
it('should respect config defaults from task parameters when interactive mode is off', async () => {
await runTask('task6', directories, {}, { interactive: false });
expect(task6).toHaveBeenCalledTimes(1);
expect(task6).toHaveBeenCalledWith(
expect.objectContaining({
'first-config': undefined,
'second-config': 'default value',
}),
{ interactive: false }
);
});
it('should run normally when interactive mode is on but task has no interactive parameters', async () => {
await runTask('task3', directories, {}, { interactive: true });
expect(task3).toHaveBeenCalledTimes(1);
});
});
describe('runAlias', () => {
beforeEach(() => {
task1.mockClear();
task2.mockClear();
task3.mockClear();
task4.mockClear();
task5.mockClear();
});
it('should run all tasks defined in an alias', () => {
return new Promise((resolve, reject) => {
runAlias('alias1', directories, optionsWithAliases, { stack: [] })
.then(() => {
expect(task1).toHaveBeenCalledTimes(1);
expect(task2).toHaveBeenCalledTimes(0);
expect(task3).toHaveBeenCalledTimes(1);
expect(task4).toHaveBeenCalledTimes(1);
expect(task4.mock.calls[0][1].stack).toEqual(['Task 2.4']);
resolve();
})
.catch(reject);
});
});
it('should throw when alias not found', () => {
const pizza = runAlias('pizza', directories, optionsWithAliases, {});
return expect(pizza).rejects.toHaveProperty(
'message',
'Alias “pizza” not found.'
);
});
it('should run alias tasks in sequence', () => {
return new Promise((resolve, reject) => {
const stack = [];
runAlias(['alias2'], directories, optionsWithAliases, { stack })
.then(() => {
expect(task4).toHaveBeenCalledTimes(1);
expect(task5).toHaveBeenCalledTimes(1);
expect(stack).toEqual(['Task 2.4', 'Task 2.5']);
resolve();
})
.catch(reject);
});
});
it('should run alias when referencing another alias', () => {
return new Promise((resolve, reject) => {
runAlias('alias3', directories, optionsWithAliases, { stack: [] })
.then(() => {
expect(task1).toHaveBeenCalledTimes(1);
expect(task4).toHaveBeenCalledTimes(1);
expect(task5).toHaveBeenCalledTimes(1);
resolve();
})
.catch(reject);
});
});
});
describe('run', () => {
beforeEach(() => {
task1.mockClear();
task2.mockClear();
task3.mockClear();
task4.mockClear();
task5.mockClear();
});
it('should run a task', () => {
return new Promise((resolve, reject) => {
run('task1', directories, optionsWithAliases, {})
.then(() => {
expect(task1).toHaveBeenCalledTimes(1);
resolve();
})
.catch(reject);
});
});
it('should run all tasks defined in an alias', () => {
return new Promise((resolve, reject) => {
run('alias1', directories, optionsWithAliases, { stack: [] })
.then(() => {
expect(task1).toHaveBeenCalledTimes(1);
expect(task2).toHaveBeenCalledTimes(0);
expect(task3).toHaveBeenCalledTimes(1);
expect(task4).toHaveBeenCalledTimes(1);
expect(task4.mock.calls[0][1].stack).toEqual(['Task 2.4']);
resolve();
})
.catch(reject);
});
});
it('should run multiple tasks', () => {
return new Promise((resolve, reject) => {
run(['task1', 'task2', 'task4'], directories, optionsWithAliases, {
stack: [],
})
.then(() => {
expect(task1).toHaveBeenCalledTimes(1);
expect(task2).toHaveBeenCalledTimes(1);
expect(task4).toHaveBeenCalledTimes(1);
expect(task4.mock.calls[0][1].stack).toEqual(['Task 2.4']);
resolve();
})
.catch(reject);
});
});
it('should run multiple tasks in sequence', () => {
return new Promise((resolve, reject) => {
const stack = [];
run(['task4', 'task5'], directories, optionsWithAliases, { stack })
.then(() => {
expect(task4).toHaveBeenCalledTimes(1);
expect(task5).toHaveBeenCalledTimes(1);
expect(stack).toEqual(['Task 2.4', 'Task 2.5']);
resolve();
})
.catch(reject);
});
});
it('should run a task in a custom preset', () => {
return new Promise((resolve, reject) => {
run('task1', presetDir, optionsWithAliases, {})
.then(() => {
expect(task1).toHaveBeenCalledTimes(1);
resolve();
})
.catch(reject);
});
});
});
describe('getAllAliases', () => {
it('should return all aliases', () => {
const result = getAllAliases(optionsWithAliases);
expect(result).toEqual({
alias1: ['task1', 'task3', 'task4'],
alias2: ['task4', 'task5'],
alias3: ['task1', 'alias2'],
});
});
it('should return an empty object when no aliases defined', () => {
const result = getAllAliases(options);
expect(result).toEqual({});
});
});
describe('getAllTasks', () => {
it('should return all available tasks', () => {
const result = getAllTasks(directories, optionsWithAliases);
expect(result).toEqual({
alias1: ['task1', 'task3', 'task4'],
alias2: ['task4', 'task5'],
alias3: ['task1', 'alias2'],
task1: 'Taks 1.1',
task2: 'Taks 1.2',
task3: 'Taks 2.3',
task4: 'Taks 2.4',
task5: 'Taks 2.5',
task6: 'Taks 3.6',
task7: '',
});
});
});