eslint-plugin-zillow
Version:
Zillow's ESLint config bundled into a "zero-conf" plugin
213 lines (183 loc) • 6.6 kB
JavaScript
;
const fs = require('fs');
const path = require('path');
const writeFile = require('util').promisify(fs.writeFile);
/* eslint-disable import/no-extraneous-dependencies */
const { ESLint } = require('eslint');
const ConfigValidator = require('@eslint/eslintrc/lib/shared/config-validator');
const BuiltInEnvironments = require('@eslint/eslintrc/conf/environments');
/* eslint-enable import/no-extraneous-dependencies */
const { getPluginEnvironments } = require('./plugins');
const PLUGIN_ENVIRONMENTS = new Map(Object.entries(getPluginEnvironments()));
async function main() {
const jestTask = renderConfig(
'jest',
{
extends: ['zillow/jest'],
rules: {
// plugin-specific rules that hamper effective testing
'@typescript-eslint/no-invalid-void-type': ['off'],
'react/prop-types': ['off'],
},
},
[
// prettier-ignore
'**/*{-,.}test.[jt]s?(x)',
'**/*.stories.[jt]s?(x)',
'**/__tests__/**/*.[jt]s?(x)',
'**/__mocks__/**/*.[jt]s?(x)',
'**/test/**/*.[jt]s?(x)',
]
);
const mochaTask = renderConfig(
'mocha',
{
extends: ['zillow/mocha'],
rules: {
// mocha does fancy things with test case scope,
// and this conflicts with mocha/no-mocha-arrow
'prefer-arrow-callback': 'off',
'func-names': 'off',
'react/prop-types': ['off'],
},
},
[
// prettier-ignore
'**/*-test.[jt]s?(x)',
'**/test/**/*.[jt]s?(x)',
]
);
const recommendedTask = renderConfig('recommended', {
extends: ['zillow'],
parser: 'babel-eslint',
rules: {
// TODO: re-enable when https://github.com/yannickcr/eslint-plugin-react/commit/2b0d70c is released
'react/prop-types': ['off'],
},
});
const typescriptTask = renderConfig(
'typescript',
{
extends: ['zillow-typescript'],
parser: '@typescript-eslint/parser',
},
['**/*.ts?(x)']
);
/* istanbul ignore next (catch doesn't need coverage) */
try {
await Promise.all([jestTask, mochaTask, recommendedTask, typescriptTask]);
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
process.exitCode = 1;
}
}
/**
* Render specified ESLint config to JSON file
* @param {string} name
* @param {{ extends: string[], rules?: { [k: string]: any }}} config
* @param {string[]} [overrides] List of override file globs
*/
async function renderConfig(name, config, overrides) {
const validator = new ConfigValidator();
const computedConfig = await getComputedConfig(config);
const wrappedConfig = wrapInPlugin(computedConfig, overrides);
const targetPath = path.resolve(__dirname, `./configs/${name}.json`);
validator.validate(
wrappedConfig,
config.extends[0],
() => {},
// (slightly less) horrible cheese to avoid exploding
envName => PLUGIN_ENVIRONMENTS.get(envName.replace('zillow/', ''))
);
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'test') {
// eslint-disable-next-line no-console
console.log(`writing ${path.relative('.', targetPath)}`);
}
await writeFile(targetPath, JSON.stringify(wrappedConfig, null, 2));
}
/**
* Generate ESLint config object from specified baseConfig
* @param {{ extends: string[], rules?: { [k: string]: any }}} baseConfig
*/
async function getComputedConfig(baseConfig) {
const engine = new ESLint({
useEslintrc: false,
allowInlineConfig: false,
baseConfig,
});
const computed = await engine.calculateConfigForFile('index.js');
// remove unnecessary fields
delete computed.filePath;
delete computed.baseDirectory;
// un-resolve parser (re-resolved during re-export)
if (computed.parser && baseConfig.parser && computed.parser !== baseConfig.parser) {
computed.parser = baseConfig.parser;
}
return computed;
}
function wrapInPlugin(config, files) {
// if files passed, this whole block will be moved to overrides[0]
const pluginConfig = {
files,
// We expose a config already computed from the whole extends chain, so no extends here.
...config,
extends: [],
// Plugins appear to come from this plugin, so it's the only one externally visible.
plugins: ['zillow'],
// The rules from third-party plugins need to be prefixed so they reference our namespace.
rules: prefixRuleConfigs('zillow', config.rules),
};
// if non-builtin envs are passed, make sure they're properly prefixed
if (pluginConfig.env) {
const ourEnv = {};
// eslint-disable-next-line no-restricted-syntax
for (const envName of Object.keys(pluginConfig.env)) {
if (BuiltInEnvironments.has(envName)) {
// pass through builtin environments
ourEnv[envName] = pluginConfig.env[envName];
} else {
// needs prefix to find our wrapper
ourEnv[`zillow/${envName}`] = pluginConfig.env[envName];
}
}
pluginConfig.env = ourEnv;
}
if (files) {
// https://eslint.org/docs/user-guide/configuring#configuration-based-on-glob-patterns
delete pluginConfig.extends;
delete pluginConfig.ignorePatterns;
delete pluginConfig.overrides;
delete pluginConfig.root;
if (pluginConfig.parserOptions && Object.keys(pluginConfig.parserOptions).length === 0) {
delete pluginConfig.parserOptions;
}
return { overrides: [pluginConfig] };
}
// "files" only valid in overrides
delete pluginConfig.files;
return pluginConfig;
}
/**
* Adds a prefix to rules that come from plugins.
*/
function prefixRuleConfigs(prefix, rules) {
return Object.keys(rules).reduce((acc, name) => {
// Plugin rules always have a slash in the name
if (name.indexOf('/') !== -1) {
acc[`${prefix}/${name}`] = rules[name];
} else {
acc[name] = rules[name];
}
return acc;
}, {});
}
/* istanbul ignore if */
if (require.main === module) {
// node lib/render-configs.js
main();
} else {
module.exports = main;
}