npm package: @poppinss/mrm-core
Description: Utilities to make tasks for Mrm
Author: poppinss,virk
Latest version: 1.0.0

<!-- Core utils -->

# mrm-core

[![npm](https://img.shields.io/npm/v/mrm-core.svg)](https://www.npmjs.com/package/mrm-core)

Utilities to write codemods for config files (JSON, YAML, INI, Markdown, etc.). Can be used to make tasks for [Mrm](https://github.com/sapegin/mrm).

## Example

Add ESLint to your project:

```js
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](https://github.com/sapegin/mrm/tree/master/packages/mrm-task-eslint), and this task is already included by default.

You can find more examples [in my dotfiles repository](https://github.com/sapegin/dotfiles/tree/master/mrm).

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

```js
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:

```js
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:

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

#### YAML

API:

```js
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:

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

#### INI

API:

```js
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:

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

Result:

```ini
# editorconfig.org
root = true

[*]
indent_style = tab
end_of_line = lf
```

#### New line separated text files

API:

```js
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:

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

#### Markdown

**Note:** use `template` function to create Markdown files.

API:

```js
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:

```js
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) syntax.

API:

```js
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:

```js
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:

```js
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:

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

### File system helpers

```js
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.

```js
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](https://semver.org/) ranges, like `1.2.3`, `^1.2.0` or `>=2`.

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

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

To use Yarn pass `yarn: true`:

```js
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.

```js
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`:

```js
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.

```js
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](https://github.com/sapegin/mrm/releases).

## Contributing

Everyone is welcome to contribute. Please take a moment to review the [contributing guidelines](../Contributing.md).

## Authors and license

[Artem Sapegin](http://sapegin.me) and [contributors](https://github.com/sapegin/mrm/graphs/contributors).

MIT License, see the included [License.md](License.md) file.