Skip to content

Commit

Permalink
feat(eslint6): Add new plugin for eslint6
Browse files Browse the repository at this point in the history
  • Loading branch information
danez committed Sep 24, 2020
1 parent 06e6e45 commit e0856da
Show file tree
Hide file tree
Showing 6 changed files with 427 additions and 26 deletions.
42 changes: 42 additions & 0 deletions packages/spire-plugin-eslint6/README.md
@@ -0,0 +1,42 @@
# spire-plugin-eslint6

[ESLint](https://eslint.org/) version 6 plugin for
[Spire](https://github.com/researchgate/spire).

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Hooks](#hooks)
- [Options](#options)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Hooks

- `setup` Adds `lint` and prepares eslint arguments.
- `precommit` Adds eslint linter.
- `run` Runs eslint.

## Options

- Plugin `['spire-plugin-eslint', options]`

- `command` \<string\> Command name to run eslint on. Defaults to `lint`.
- `eslintConfig` \<string\> Default [eslint] configuration. Defaults to
[`./config.js`](./config.js).
- `autosetEslintConfig` \<boolean\> Decides if the plugin should automatically
create an `.eslintrc.js ` file. It will only create the file if there isn't
already a config present. Defaults to `true`.
- `allowCustomConfig` \<boolean\> Whether to allow user-provided config. If
this option is `false` and there's custom eslint config found it will throw
an error. Defaults to `true`.
- `eslintIgnore` \<string\> Path to default `.eslintignore`. Defaults to
`.gitignore`.
- `allowCustomIgnore` \<boolean\> Whether to allow user-provided
`.eslintignore`. If this option is `false` and there's custom ignore file
found it will throw an error. Defaults to `true`.
- `fileExtensions` \<string[]\> Extension of files eslint should scan.
Defaults to `['.js', '.jsx', '.mjs', '.ts', '.tsx']`.

- CLI `npx spire lint [args]`
- Passes all arguments as-is to eslint.
112 changes: 112 additions & 0 deletions packages/spire-plugin-eslint6/__tests__/index.spec.js
@@ -0,0 +1,112 @@
const { createFixture } = require('spire-test-utils');
const { stat, readFile } = require('fs-extra');
const { join } = require('path');

const configWithEslintPlugin = (options = {}) =>
JSON.stringify({
name: 'spire-plugin-eslint6-test',
spire: {
plugins: [[require.resolve('spire-plugin-eslint6'), options]],
},
});

describe('spire-plugin-eslint6', () => {
test('adds lint command', async () => {
const fixture = await createFixture({
'package.json': configWithEslintPlugin(),
});
await expect(fixture.run('spire', ['--help'])).resolves.toMatchObject({
stdout: /Commands:\s+spire lint/,
});
await fixture.clean();
});

test('adds eslint linter', async () => {
const fixture = await createFixture({
'package.json': configWithEslintPlugin(),
});
const { stdout } = await fixture.run('spire', [
'hook',
'precommit',
'--debug',
]);
expect(stdout).toMatch(/Using linters:/);
expect(stdout).toMatch(/\*\.\(js\|jsx\|mjs\|ts\|tsx\)/);
await fixture.clean();
});

test('passes custom arguments to eslint', async () => {
const fixture = await createFixture({
'package.json': configWithEslintPlugin(),
});
await expect(
fixture.run('spire', ['lint', '--version'])
).resolves.toMatchObject({
stdout: /v\d\.\d\.\d/,
});
await fixture.clean();
});

test('creates default eslint config for editors', async () => {
const fixture = await createFixture({
'package.json': configWithEslintPlugin(),
});
await fixture.run('spire', ['hook', 'postinstall']);
const eslintConfig = join(fixture.cwd, '.eslintrc.js');
expect(await stat(eslintConfig)).toBeTruthy();
expect(await readFile(eslintConfig, 'UTF-8')).toMatch(
/spire-plugin-eslint6\/config/
);
await fixture.clean();
});

test('prints deprecation warning for glob option', async () => {
const fixture = await createFixture({
'package.json': configWithEslintPlugin({ glob: 'abc' }),
});
await fixture.run('spire', ['hook', 'precommit']);

await expect(
fixture.run('spire', ['hook', 'precommit'])
).resolves.toMatchObject({
stdout: /The glob option is deprecated\. Use the option `fileExtensions` instead\./,
});
await fixture.clean();
});

test('creates custom eslint config for editors', async () => {
const fixture = await createFixture({
'node_modules/eslint6-config-cool-test/package.json': JSON.stringify({
name: 'eslint6-config-cool-test',
version: '1.0.0',
main: 'index.js',
}),
'node_modules/eslint6-config-cool-test/index.js': 'module.exports = {};',
'package.json': configWithEslintPlugin({
config: 'eslint6-config-cool-test',
}),
});
await fixture.run('spire', ['hook', 'postinstall']);
const eslintConfig = join(fixture.cwd, '.eslintrc.js');
expect(await stat(eslintConfig)).toBeTruthy();
expect(await readFile(eslintConfig, 'UTF-8')).toMatch(
/eslint6-config-cool-test/
);
await fixture.clean();
});

test('warns about custom eslint config not extending default config', async () => {
const fixture = await createFixture({
'package.json': configWithEslintPlugin(),
'.eslintrc.json': '',
});
await expect(
fixture.run('spire', ['hook', 'postinstall'])
).resolves.toMatchObject({
stdout: expect.stringMatching(
'Attempted to set ESLint config but it already exists. Please ensure existing config re-exports'
),
});
await fixture.clean();
});
});
19 changes: 19 additions & 0 deletions packages/spire-plugin-eslint6/config.js
@@ -0,0 +1,19 @@
module.exports = {
parserOptions: {
ecmaVersion: '2020',
sourceType: 'script',
},
extends: ['eslint:recommended', 'plugin:prettier/recommended'],
env: {
es6: true,
node: true,
},
overrides: [
{
files: ['**.spec.js', '**.test.js', '**/__tests__/**.js'],
env: {
jest: true,
},
},
],
};
124 changes: 124 additions & 0 deletions packages/spire-plugin-eslint6/index.js
@@ -0,0 +1,124 @@
const execa = require('execa');
const SpireError = require('spire/error');

const SUPPORTED_CONFIG_FILES = [
'.eslintrc',
'.eslintrc.js',
'.eslintrc.json',
'.eslintrc.yaml',
'.eslintrc.yml',
];

function eslint(
{ setState, getState, hasFile, readFile, writeFile, hasPackageProp },
{
command = 'lint',
config: defaultEslintConfig = 'spire-plugin-eslint6/config',
autosetEslintConfig = true,
allowCustomConfig = true,
eslintIgnore: defaultEslintIgnore = '.gitignore',
allowCustomIgnore = true,
fileExtensions = ['.js', '.jsx', '.mjs', '.ts', '.tsx'],
}
) {
async function hasCustomEslintConfig() {
for (const file of SUPPORTED_CONFIG_FILES) {
if (await hasFile(file)) {
return file;
}
}

return hasPackageProp('eslintConfig');
}
return {
name: 'spire-plugin-eslint6',
command,
description: 'lint files with ESLint',
async postinstall({ logger }) {
if (autosetEslintConfig) {
const hasCustomConfig = await hasCustomEslintConfig();
if (hasCustomConfig && typeof hasCustomConfig === 'string') {
const currentContent = await readFile(hasCustomConfig, 'UTF-8');
if (!currentContent.includes(defaultEslintConfig)) {
return logger.warn(
'Attempted to set ESLint config but it already exists. ' +
'Please ensure existing config re-exports `%s`.',
defaultEslintConfig
);
}
}
if (!hasCustomConfig) {
await writeFile(
'.eslintrc.js',
"'use strict';\n" +
'// This file was created by spire-plugin-eslint for editor support\n' +
`module.exports = require('${defaultEslintConfig}');\n`
);
}
}
},
async setup({ argv, resolve }) {
const hasCustomConfig =
argv.includes('--config') || (await hasCustomEslintConfig());
const eslintConfig =
allowCustomConfig && hasCustomConfig
? []
: ['--config', resolve(defaultEslintConfig)];
const hasCustomIgnore =
argv.includes('--ignore-path') ||
(await hasFile('.eslintignore')) ||
(await hasPackageProp('eslintIgnore'));
const eslintIgnore =
allowCustomIgnore && hasCustomIgnore
? []
: (await hasFile(defaultEslintIgnore))
? ['--ignore-path', defaultEslintIgnore]
: [];
const eslintExtensions = ['--ext', fileExtensions.join(',')];
setState({
eslintArgs: [...eslintConfig, ...eslintIgnore, ...eslintExtensions],
});
// Inform user about disallowed overrides
if (hasCustomConfig && !allowCustomConfig) {
throw new SpireError(
`Custom eslint config is not allowed, using ${defaultEslintConfig} instead`
);
}
if (hasCustomIgnore && !allowCustomIgnore) {
throw new SpireError(
`Custom eslint ignore is not allowed, using ${defaultEslintIgnore} instead`
);
}
},
async precommit() {
setState((prev) => ({
linters: [
...prev.linters,
{
[`*.(${fileExtensions.map((ext) => ext.substr(1)).join('|')})`]: [
'eslint',
...prev.eslintArgs,
'--fix',
],
},
],
}));
},
async run({ options, logger, cwd }) {
const { eslintArgs } = getState();
const [, ...userProvidedArgs] = options._;
const finalEslintArgs = [
...eslintArgs,
...(userProvidedArgs.length ? userProvidedArgs : ['.']),
];
logger.debug('Using eslint arguments: %s', finalEslintArgs.join(' '));
await execa('eslint', finalEslintArgs, {
cwd,
stdio: 'inherit',
preferLocal: true,
});
},
};
}

module.exports = eslint;
24 changes: 24 additions & 0 deletions packages/spire-plugin-eslint6/package.json
@@ -0,0 +1,24 @@
{
"name": "spire-plugin-eslint6",
"version": "3.1.0",
"description": "ESLint v6 plugin for Spire",
"main": "index.js",
"repository": "researchgate/spire",
"author": "Daniel Tschinder <daniel@tschinder.de>",
"license": "MIT",
"engines": {
"node": ">=10.18.0"
},
"dependencies": {
"eslint": "^6.0.0",
"eslint-config-prettier": "^6.0.0",
"eslint-plugin-prettier": "^3.1.0",
"execa": "^4.0.0"
},
"devDependencies": {
"spire-test-utils": "^3.0.0"
},
"peerDependencies": {
"spire": "^3.0.0"
}
}

0 comments on commit e0856da

Please sign in to comment.