mrm-core

Utilities to make tasks for Mrm

Usage no npm install needed!

<script type="module">
  import mrmCore from 'https://cdn.skypack.dev/mrm-core';
</script>

README

mrm-core

npm

Utilities to write codemods for config files (JSON, YAML, INI, Markdown, etc.). Can be used to make tasks for Mrm.

Example

Add ESLint to your project:

const { json, lines, packageJson, install } = require('mrm-core');

module.exports = function(config) {
  const preset = config('preset', 'tamia');
  const packages = ['eslint', `eslint-config-${preset}`];

  // .eslintrc
  const eslintrc = json('.eslintrc');
  if (!eslintrc.get('extends').startsWith(preset)) {
    eslintrc.set('extends', preset).save();
  }

  // .eslintignore
  lines('.eslintignore')
    .add('node_modules')
    .save();

  // package.json
  const pkg = packageJson()
    .setScript('lint', 'eslint . --fix')
    .setScript('pretest', 'npm run line')
    .save();

  // Install dependencies
  install(packages);
};
module.exports.description = 'Adds ESLint with a custom preset';

Read more in the docs, and this task is already included by default.

You can find more examples in my dotfiles repository.

You don’t have to use mrm-core with mrm, you can run this tasks from your own code:

const get = require('lodash/get');
const addEslint = require('./tasks/eslint');
const config = {
  preset: 'airbnb'
};
const getConfig = (prop, defaultValue) =>
  get(config, prop, defaultValue);
addEslint(getConfig);

Installation

npm install mrm-core

API

Work with files

  • Do not overwrite original files, unless you want to.
  • All functions (except getters) can be chained.
  • save() will create file if it doesn’t exist or update it with new data.
  • save() will write file to disk only if the new content is different from the original file.
  • save() will try to keep formatting (indentation, end of file new line) of the original file or use style from EditorConfig.

JSON

API:

const { json } = require('mrm-core');
const file = json('file name', { default: 'values' });
file.exists(); // File exists?
file.get(); // Return everything
file.get('key.subkey', 'default value'); // Return value with given address
file.set('key.subkey', 'value'); // Set value by given address
file.set({ key: value }); // Replace JSON with given object
file.unset('key.subkey'); // Remove value by given address
file.merge({ key: value }); // Merge JSON with given object
file.save(); // Save file
file.delete(); // Delete file

Example:

json('.eslintrc')
  .merge({
    extends: 'eslint-config-recommended'
  })
  .save();

YAML

API:

const { yaml } = require('mrm-core');
const file = yaml(
  'file name', // File name
  { default: 'values' }, // Default value
  { version: '1.2' } // Options
);
file.exists(); // File exists?
file.get(); // Return everything
file.get('key.subkey', 'default value'); // Return value with given address
file.set('key.subkey', 'value'); // Set value by given address
file.set({ key: value }); // Replace JSON with given object
file.unset('key.subkey'); // Remove value by given address
file.merge({ key: value }); // Merge JSON with given object
file.save(); // Save file
file.delete(); // Delete file

Example:

yaml('.travis.yml')
  .set('language', 'node_js')
  .set('node_js', [4, 6])
  .save();

INI

API:

const { ini } = require('mrm-core');
const file = ini('file name', 'comment');
file.exists(); // File exists?
file.get(); // Return everything
file.get('section name'); // Return section value
file.set('section name', { key: value }); // Set section value
file.unset('section name'); // Remove section
file.save(); // Save file
file.save({ withSpaces: false }); // Disable spaces around =
file.delete(); // Delete file

Example:

const { ini } = require('mrm-core');
ini('.editorconfig', 'editorconfig.org')
  .set('_global', { root: true })
  .set('*', {
    indent_style: 'tab',
    end_of_line: 'lf'
  })
  .save();

Result:

# editorconfig.org
root = true

[*]
indent_style = tab
end_of_line = lf

New line separated text files

API:

const { lines } = require('mrm-core');
const file = lines('file name', ['default', 'values']);
file.exists(); // File exists?
file.get(); // Return everything
file.set(['line 1', 'line 2', 'line 3']); // Set file lines, overwrite existing
file.add('new'); // Add new line
file.add(['new', 'lines']); // Add multiple news lines
file.remove('new'); // Remove line
file.remove(['new', 'lines']); // Remove multiple lines
file.save(); // Save file
file.delete(); // Delete file

Example:

lines('.eslintignore')
  .add('node_modules')
  .save();

Markdown

Note: use template function to create Markdown files.

API:

const { markdown } = require('mrm-core');
const file = markdown('file name');
file.exists(); // File exists?
file.get(); // Return file content
// Add a badge at the beginning of the file (below header)
file.addBadge('image URL', 'link URL', 'alt text');
// Remove a badge when the predicate function returns true
file.removeBadge(({ imageUrl, linkUrl, altText }) =>
  imageUrl.startsWith('https://travis-ci.org')
);
file.save(); // Save file
file.delete(); // Delete file

Example:

const name = 'pizza';
markdown('Readme.md')
  .addBadge(
    `https://travis-ci.org/${config('github')}/${name}.svg`,
    `https://travis-ci.org/${config('github')}/${name}`,
    'Build Status'
  )
  .save();

Plain text templates

Templates use ECMAScript template literals syntax.

API:

const { template } = require('mrm-core');
const file = template('file name', 'template file name');
file.exists(); // File exists?
file.get(); // Return file content
file.apply({ key: 'value' }); // Replace template tags with given values
file.save(); // Save file
file.delete(); // Delete file

Example:

template('License.md', path.join(__dirname, 'License.md'))
  .apply(config(), {
    year: new Date().getFullYear()
  })
  .save();

Template:

The MIT License
===============

Copyright ${year} ${name} (${url}), contributors

Permission is hereby granted, free of charge, to any person obtaining...

Special files

package.json

API:

const { packageJson } = require('mrm-core');
const file = packageJson({ default: 'values' });
file.exists(); // File exists?
file.get(); // Return everything
file.getScript('test'); // Return script
file.getScript('test', 'eslint'); // Return a subcommand of a script
file.setScript('test', 'eslint --fix'); // Replace a script with a command: a -> b
file.appendScript('test', 'eslint --fix'); // Append command to a script: a -> a && b
file.prependScript('test', 'eslint --fix'); // Prepend a script with a command: a -> b && a
file.removeScript('test'); // Remove script
file.removeScript(/^mocha|ava$/); // Remove all scripts that match a regexp
file.removeScript('test', /b/); // Remove subcommands from a script: a && b -> a
file.save(); // Save file
file.delete(); // Delete file
// All methods of json() work too

Note: subcommand is a command between && in an npm script. For example, prettier --write '**/*.js' && eslint . --fix has two subcommands: prettier… and eslint….

Example:

packageJson()
  .appendScript('lint', 'eslint . --ext .js --fix')
  .save();

File system helpers

const { copyFiles, deleteFiles, makeDirs } = require('mrm-core');
copyFiles('source dir', 'file name'); // Copy file
copyFiles('source dir', ['file name 1', 'file name 2']); // Copy files
copyFiles('source dir', 'file name', { overwrite: false }); // Do not overwrite
deleteFiles('file name 1'); // Delete file or folder
deleteFiles(['file name 1', 'folder name 1']); // Delete files or folders
makeDirs('dir name'); // Create folder
makeDirs(['dir name 1', 'dir name 2']); // Create folders

Install and uninstall npm, Yarn, or pnpm packages

Installs npm package(s) and saves them to package.json if they aren’t installed yet or not satisfying range.

const { install } = require('mrm-core');
install('eslint'); // Install to devDependencies
install(['tamia', 'lodash'], { dev: false }); // Install to dependencies
install({ lodash: '^4.17.3' }); // Install particular version
install(['lodash'], {
  versions: { lodash: '^4.17.3', other: '1.0.0' }
}); // Install particular version
install(['github/repo']); // Install non-registry package without version

Note: Works with all semver ranges, like 1.2.3, ^1.2.0 or >=2.

Uninstalls npm package(s) and removes them from package.json:

const { uninstall } = require('mrm-core');
uninstall('eslint'); // Uninstall from devDependencies
uninstall(['tamia', 'lodash'], { dev: false }); // Uninstall from dependencies

To use Yarn pass yarn: true:

const { install, uninstall } = require('mrm-core');

uninstall(['eslint'], { yarn: true });
install(['standard'], { yarn: true });

With Yarn Berry, pass yarnBerry: true and for pnpm, pass pnpm: true.

Utilities

Infers style (indentation, new line at the end of file) from a source code or reads from the .editorconfig file.

const {
  inferStyle,
  getStyleForFile,
  getIndent,
  format
} = require('mrm-core');
inferStyle('for (;;) {\n  alert(1);\n}\n');
// => { insert_final_newline: true, indent_style: 'space', indent_size: 2 }
getStyleForFile('test.js');
// => { insert_final_newline: false, indent_style: 'tab', indent_size: 'tab' }
getIndent({ indent_style: 'space', indent_size: 2 });
// => '  '
format('alert(1)\n', { insert_final_newline: false });
// => 'alert(1)'
// Only insert_final_newline is supported

Get file extensions list from a command like eslint . --fix --ext .js,.jsx:

const { getExtsFromCommand } = require('mrm-core');
getExtsFromCommand(`eslint . --fix --ext .js,.jsx`, 'ext');
// => ['js', 'jsx']
getExtsFromCommand(`prettier --write '**/*.js'`);
// => ['js']

Custom error class: MrmError

Use this class to notify user about expected errors in your tasks. It will be printed without a stack trace and will abort task.

const { MrmError } = require('mrm-core');
if (!fs.existsSync('.travis.yml')) {
  throw new MrmError('Run travis task first');
}

Changelog

The changelog can be found on the Releases page.

Contributing

Everyone is welcome to contribute. Please take a moment to review the contributing guidelines.

Authors and license

Artem Sapegin and contributors.

MIT License, see the included License.md file.