Skip to main content

ESLint example

In this guide, you'll learn how to integrate ESLint into moon.

Begin by installing eslint and any plugins in your root. We suggest using the same version across the entire repository.

yarn add --dev eslint eslint-config-moon

Setup

Since linting is a universal workflow, add a lint task to .moon/tasks/node.yml with the following parameters.

.moon/tasks/node.yml
tasks:
lint:
command:
- 'eslint'
# Support other extensions
- '--ext'
- '.js,.jsx,.ts,.tsx'
# Always fix and run extra checks
- '--fix'
- '--report-unused-disable-directives'
# Dont fail if a project has nothing to lint
- '--no-error-on-unmatched-pattern'
# Do fail if we encounter a fatal error
- '--exit-on-fatal-error'
# Only 1 ignore file is supported, so use the root
- '--ignore-path'
- '@in(4)'
# Run in current dir
- '.'
inputs:
# Source and test files
- 'src/**/*'
- 'tests/**/*'
# Other config files
- '*.config.*'
# Project configs, any format, any depth
- '**/.eslintrc.*'
# Root configs, any format
- '/.eslintignore'
- '/.eslintrc.*'

Projects can extend this task and provide additional parameters if need be, for example.

<project>/moon.yml
tasks:
lint:
args:
# Enable caching for this project
- '--cache'

TypeScript integration

If you're using the @typescript-eslint packages, and want to enable type-safety based lint rules, we suggest something similar to the official monorepo configuration.

Create a tsconfig.eslint.json in your repository root, extend your shared compiler options (we use tsconfig.options.json), and include all your project files.

tsconfig.eslint.json
{
"extends": "./tsconfig.options.json",
"compilerOptions": {
"emitDeclarationOnly": false,
"noEmit": true
},
"include": ["apps/**/*", "packages/**/*"]
}

Append the following inputs to your lint task.

.moon/tasks/node.yml
tasks:
lint:
# ...
inputs:
# TypeScript support
- 'types/**/*'
- 'tsconfig.json'
- '/tsconfig.eslint.json'
- '/tsconfig.options.json'

And lastly, add parserOptions to your root-level config.

Configuration

Root-level

The root-level ESLint config is required, as ESLint traverses upwards from each file to find configurations, and this denotes the stopping point. It's also used to define rules for the entire repository.

.eslintrc.js
module.exports = {
root: true, // Required!
extends: ['moon'],
rules: {
'no-console': 'error',
},

// TypeScript support
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.eslint.json',
tsconfigRootDir: __dirname,
},
};

The .eslintignore file must also be defined at the root, as only 1 ignore file can exist in a repository. We ensure this ignore file is used by passing --ignore-path above.

.eslintignore
node_modules/
*.min.js
*.map
*.snap

Project-level

A project-level ESLint config can be utilized by creating a .eslintrc.<json|js|cjs|yml> in the project root. This is optional, but necessary when defining rules and ignore patterns unique to the project.

<project>/.eslintrc.js
module.exports = {
// Patterns to ignore (alongside the root .eslintignore)
ignorePatterns: ['build', 'lib'],
// Project specific rules
rules: {
'no-console': 'off',
},
};

The extends setting should not extend the root-level config, as ESLint will automatically merge configs while traversing upwards!

Sharing

To share configuration across projects, you have 3 options:

  • Define settings in the root-level config. This only applies to the parent repository.
  • Create and publish an eslint-config or eslint-plugin npm package. This can be used in any repository.
  • A combination of 1 and 2.

For options 2 and 3, if you're utilizing package workspaces, create a local package with the following content.

packages/eslint-config-company/index.js
module.exports = {
extends: ['airbnb'],
};

Within your root-level ESLint config, you can extend this package to inherit the settings.

.eslintrc.js
module.exports = {
extends: 'eslint-config-company',
};

When using this approach, the package must be built and symlinked into node_modules before the linter will run correctly. Take this into account when going down this path!

FAQ

How to lint a single file or folder?

Unfortunately, this isn't currently possible, as the eslint binary itself requires a file or folder path to operate on, and in the task above we pass . (current directory). If this was not passed, then nothing would be linted.

This has the unintended side-effect of not being able to filter down lintable targets by passing arbitrary file paths. This is something we hope to resolve in the future.

To work around this limitation, you can create another lint task.

Should we use overrides?

Projects should define their own rules using an ESLint config in their project root. However, if you want to avoid touching many ESLint configs (think migrations), then overrides in the root are a viable option. Otherwise, we highly encourage project-level configs.

.eslintrc.js
module.exports = {
// ...
overrides: [
// Only apply to apps "foo" and "bar", but not others
{
files: ['apps/foo/**/*', 'apps/bar/**/*'],
rules: {
'no-magic-numbers': 'off',
},
},
],
};