Skip to content

Commit

Permalink
Support of EsLint with CRA4 (#219)
Browse files Browse the repository at this point in the history
* added the possibility to load the eslint config from the plugin instead of the loader since it changed in react-script 4

* first draft of a strategy pattern to support different eslint-loaders

* finish implementing the strategy pattern

* removed console log and added comment for context

* add plugins.js and cleanup

* fix a logic error

* rename the plugin file to webpack plugins

* Instead of parsing react-script version, we look at both place instead

* extract the matcher

* fix ci

* remove the code that supported the eslint-loader

* rename the loaderOptions to pluginOptions

* cleanup

* Since CRACO now only support the latest CRA version, we delete old code that was backward compatible

* Craco is no longer backward compatible

* remove migration path from release notes

* review fixes

* rename function

* rename variables and function
  • Loading branch information
alexasselin008 committed Dec 8, 2020
1 parent 161aee8 commit 1e4f912
Show file tree
Hide file tree
Showing 12 changed files with 19,876 additions and 6,829 deletions.
13,048 changes: 13,018 additions & 30 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 12 additions & 3 deletions packages/craco/README.md
Expand Up @@ -8,7 +8,7 @@ All you have to do is create your app using [create-react-app](https://github.co

## Support

- Create React App (CRA) 3.*
- Create React App (CRA) 4.*
- Yarn
- Yarn Workspace
- NPM
Expand All @@ -33,6 +33,7 @@ All you have to do is create your app using [create-react-app](https://github.co
- [Recipes](https://github.com/sharegate/craco/tree/master/recipes) – Short recipes for common use cases.
- [Available Plugins](https://github.com/sharegate/craco#community-maintained-plugins) - Plugins maintained by the community.
- [Develop a Plugin](#develop-a-plugin) - How to develop a plugin for CRACO.
- [Backward Compatibility](#backward-compatibility)
- [Debugging](#debugging)
- [License](#license)

Expand Down Expand Up @@ -149,8 +150,8 @@ module.exports = {
mode: "extends" /* (default value) */ || "file",
configure: { /* Any eslint configuration options: https://eslint.org/docs/user-guide/configuring */ },
configure: (eslintConfig, { env, paths }) => { return eslintConfig; },
loaderOptions: { /* Any eslint-loader configuration options: https://github.com/webpack-contrib/eslint-loader. */ },
loaderOptions: (eslintOptions, { env, paths }) => { return eslintOptions; }
pluginOptions: { /* Any eslint plugin configuration options: https://github.com/webpack-contrib/eslint-webpack-plugin#options. */ },
pluginOptions: (eslintOptions, { env, paths }) => { return eslintOptions; }
},
babel: {
presets: [],
Expand Down Expand Up @@ -935,6 +936,14 @@ Options:
> Only `message` is required.
## Backward Compatibility
CRACO is not meant to be backward compatible with older versions of react-scripts. This package will only support the latest version. If your project uses an old react-scripts version, refer to the following table to select the appropriate CRACO version.
| react-scripts Version |CRACO Version|
| --------------------- | -----------:|
| react-scripts < 4.0.0 | 5.8.0 |
## Debugging
### Verbose Logging
Expand Down
17 changes: 17 additions & 0 deletions packages/craco/lib/cra.js
@@ -1,9 +1,11 @@
const path = require("path");
const semver = require("semver");

const { log } = require("./logger");
const { projectRoot } = require("./paths");

let envLoaded = false;
const CRA_LATEST_SUPPORTED_MAJOR_VERSION = "4.0.0";

/************ Common *******************/

Expand Down Expand Up @@ -36,6 +38,20 @@ function overrideModule(modulePath, newModule) {
log(`Overrided require cache for module: ${modulePath}`);
}

function resolvePackageJson(cracoConfig) {
return require.resolve(path.join(cracoConfig.reactScriptsVersion, "package.json"), { paths: [projectRoot] });
}

function getReactScriptVersion(cracoConfig) {
const reactScriptPackageJsonPath = resolvePackageJson(cracoConfig);
const { version } = require(reactScriptPackageJsonPath);

return {
version,
isSupported: semver.gte(version, CRA_LATEST_SUPPORTED_MAJOR_VERSION)
};
}

/************ Paths *******************/

let _resolvedCraPaths = null;
Expand Down Expand Up @@ -232,6 +248,7 @@ module.exports = {
loadJestConfigProvider,
overrideJestConfigProvider,
getCraPaths,
getReactScriptVersion,
start,
build,
test
Expand Down
10 changes: 1 addition & 9 deletions packages/craco/lib/features/jest/merge-jest-config.js
Expand Up @@ -5,8 +5,6 @@ const { log } = require("../../logger");
const { applyJestConfigPlugins } = require("../plugins");
const { projectRoot } = require("../../paths");

const BABEL_TRANSFORM_ENTRY_KEY_BEFORE_2_1_0 = "^.+\\.(js|jsx)$";
const BABEL_TRANSFORM_ENTRY_KEY_BEFORE_4_0_0 = "^.+\\.(js|jsx|ts|tsx)$";
const BABEL_TRANSFORM_ENTRY_KEY = "^.+\\.(js|jsx|mjs|cjs|ts|tsx)$";

function overrideBabelTransform(jestConfig, cracoConfig, transformKey) {
Expand All @@ -30,14 +28,8 @@ function configureBabel(jestConfig, cracoConfig) {
if (isArray(presets) || isArray(plugins)) {
if (jestConfig.transform[BABEL_TRANSFORM_ENTRY_KEY]) {
overrideBabelTransform(jestConfig, cracoConfig, BABEL_TRANSFORM_ENTRY_KEY);
} else if (jestConfig.transform[BABEL_TRANSFORM_ENTRY_KEY_BEFORE_4_0_0]) {
overrideBabelTransform(jestConfig, cracoConfig, BABEL_TRANSFORM_ENTRY_KEY_BEFORE_4_0_0);
} else if (jestConfig.transform[BABEL_TRANSFORM_ENTRY_KEY_BEFORE_2_1_0]) {
overrideBabelTransform(jestConfig, cracoConfig, BABEL_TRANSFORM_ENTRY_KEY_BEFORE_2_1_0);
} else {
throw new Error(
`craco: Cannot find Jest transform entry for Babel ${BABEL_TRANSFORM_ENTRY_KEY}, ${BABEL_TRANSFORM_ENTRY_KEY_BEFORE_4_0_0} or ${BABEL_TRANSFORM_ENTRY_KEY_BEFORE_2_1_0}.`
);
throw new Error(`craco: Cannot find Jest transform entry for Babel ${BABEL_TRANSFORM_ENTRY_KEY}.`);
}
}
}
Expand Down
70 changes: 34 additions & 36 deletions packages/craco/lib/features/webpack/eslint.js
@@ -1,14 +1,14 @@
const { getLoader, removeLoaders, loaderByName } = require("../../loaders");
const { log, logError } = require("../../logger");
const { isFunction, deepMergeWithArray } = require("../../utils");
const { getPlugin, removePlugins, pluginByName } = require("../../webpack-plugins");

const ESLINT_MODES = {
extends: "extends",
file: "file"
};

function disableEslint(webpackConfig) {
const { hasRemovedAny } = removeLoaders(webpackConfig, loaderByName("eslint-loader"));
const { hasRemovedAny } = removePlugins(webpackConfig, pluginByName("ESLintWebpackPlugin"));

if (hasRemovedAny) {
log("Disabled ESLint.");
Expand All @@ -17,28 +17,28 @@ function disableEslint(webpackConfig) {
}
}

function extendsEslintConfig(loader, eslintConfig, context) {
function extendsEslintConfig(plugin, eslintConfig, context) {
const { configure } = eslintConfig;

if (configure) {
if (isFunction(configure)) {
if (loader.options) {
loader.options.baseConfig = configure(loader.options.baseConfig || {}, context);
if (plugin.options) {
plugin.options.baseConfig = configure(plugin.options.baseConfig || {}, context);
} else {
loader.options = {
plugin.options = {
baseConfig: configure({}, context)
};
}

if (!loader.options.baseConfig) {
if (!plugin.options.baseConfig) {
throw new Error("craco: 'eslint.configure' function didn't return a config object.");
}
} else {
// TODO: ensure is otherwise a plain object, if not, log an error.
if (loader.options) {
loader.options.baseConfig = deepMergeWithArray({}, loader.options.baseConfig || {}, configure);
if (plugin.options) {
plugin.options.baseConfig = deepMergeWithArray({}, plugin.options.baseConfig || {}, configure);
} else {
loader.options = {
plugin.options = {
baseConfig: configure
};
}
Expand All @@ -48,74 +48,72 @@ function extendsEslintConfig(loader, eslintConfig, context) {
}
}

function useEslintConfigFile(loader) {
if (loader.options) {
loader.options.useEslintrc = true;
delete loader.options.baseConfig;
function useEslintConfigFile(plugin) {
if (plugin.options) {
plugin.options.useEslintrc = true;
delete plugin.options.baseConfig;
} else {
loader.options = {
plugin.options = {
useEslintrc: true
};
}

log("Overrided ESLint config to use a config file.");
}

function enableEslintIgnoreFile(loader) {
if (loader.options) {
loader.options.ignore = true;
function enableEslintIgnoreFile(plugin) {
if (plugin.options) {
plugin.options.ignore = true;
} else {
loader.options = {
plugin.options = {
ignore: true
};
}

log("Overrided ESLint config to enable an ignore file.");
}

function applyLoaderOptions(loader, loaderOptions, context) {
if (isFunction(loaderOptions)) {
loader.options = loaderOptions(loader.options || {}, context);
function applyPluginOptions(plugin, pluginOptions, context) {
if (isFunction(pluginOptions)) {
plugin.options = pluginOptions(plugin.options || {}, context);

if (!loader.options) {
throw new Error("craco: 'eslint.loaderOptions' function didn't return a loader config object.");
if (!plugin.options) {
throw new Error("craco: 'eslint.pluginOptions' function didn't return a config object.");
}
} else {
// TODO: ensure is otherwise a plain object, if not, log an error.
loader.options = deepMergeWithArray(loader.options || {}, loaderOptions);
plugin.options = deepMergeWithArray(plugin.options || {}, pluginOptions);
}

log("Applied ESLint loader options.");
log("Applied ESLint plugin options.");
}

function overrideEsLint(cracoConfig, webpackConfig, context) {
if (cracoConfig.eslint) {
const { isFound, match } = getLoader(webpackConfig, loaderByName("eslint-loader"));

const { isFound, match } = getPlugin(webpackConfig, pluginByName("ESLintWebpackPlugin"));
if (!isFound) {
logError("Cannot find ESLint loader (eslint-loader).");

logError("Cannot find ESLint plugin (ESLintWebpackPlugin).");
return webpackConfig;
}

const { enable, mode, loaderOptions } = cracoConfig.eslint;
const { enable, mode, pluginOptions } = cracoConfig.eslint;

if (enable === false) {
disableEslint(webpackConfig);

return webpackConfig;
}

enableEslintIgnoreFile(match.loader);
enableEslintIgnoreFile(match);

if (mode === ESLINT_MODES.file) {
useEslintConfigFile(match.loader);
useEslintConfigFile(match);
} else {
extendsEslintConfig(match.loader, cracoConfig.eslint, context);
extendsEslintConfig(match, cracoConfig.eslint, context);
}

if (loaderOptions) {
applyLoaderOptions(match.loader, loaderOptions);
if (pluginOptions) {
applyPluginOptions(match, pluginOptions);
}
}

Expand Down
19 changes: 19 additions & 0 deletions packages/craco/lib/validate-cra-version.js
@@ -0,0 +1,19 @@
const { getReactScriptVersion } = require("../lib/cra");

function validateCraVersion(cracoConfig) {
const { isSupported, version } = getReactScriptVersion(cracoConfig);
if (!isSupported) {
throw new Error(
`Your current version of react-scripts(${version}) is not supported by this version of CRACO. Please try updating react-scripts to the latest version:\n\n` +
` $ yarn upgrade react-scripts\n\n` +
"Or:\n\n" +
` $ npm update react-scripts\n\n` +
`If that doesn't work or if you can't, refer to the following table to choose the right version of CRACO.\n` +
"https://github.com/gsoft-inc/craco/blob/master/packages/craco/README.md#backward-compatibility"
);
}
}

module.exports = {
validateCraVersion
};
31 changes: 31 additions & 0 deletions packages/craco/lib/webpack-plugins.js
@@ -0,0 +1,31 @@
function pluginByName(targetPluginName) {
return plugin => {
return plugin.constructor.name === targetPluginName;
};
}

function getPlugin(webpackConfig, matcher) {
const matchingPlugin = webpackConfig.plugins.find(matcher);

return {
isFound: matchingPlugin !== undefined,
match: matchingPlugin
};
}

function removePlugins(webpackConfig, matcher) {
const prevCount = webpackConfig.plugins.length;
webpackConfig.plugins = webpackConfig.plugins.filter(x => !matcher(x));
const removedPluginsCount = prevCount - webpackConfig.plugins.length;

return {
hasRemovedAny: removedPluginsCount > 0,
removedCount: removedPluginsCount
};
}

module.exports = {
getPlugin,
pluginByName,
removePlugins
};
7 changes: 4 additions & 3 deletions packages/craco/package.json
Expand Up @@ -33,15 +33,16 @@
"craco": "./bin/craco.js"
},
"peerDependencies": {
"react-scripts": "*"
"react-scripts": "^4.0.0"
},
"devDependencies": {
"react-scripts": "3.*"
"react-scripts": "4.*"
},
"dependencies": {
"cross-spawn": "^7.0.0",
"lodash": "^4.17.15",
"webpack-merge": "^4.2.2"
"webpack-merge": "^4.2.2",
"semver": "^7.3.2"
},
"version": "5.9.0",
"gitHead": "9c9c5e5ee1f79a17af4809945573bfdbdcc912be"
Expand Down
3 changes: 3 additions & 0 deletions packages/craco/scripts/build.js
Expand Up @@ -9,6 +9,7 @@ const { log } = require("../lib/logger");
const { getCraPaths, build } = require("../lib/cra");
const { loadCracoConfigAsync } = require("../lib/config");
const { overrideWebpackProd } = require("../lib/features/webpack/override");
const { validateCraVersion } = require("../lib/validate-cra-version");

log("Override started with arguments: ", process.argv);
log("For environment: ", process.env.NODE_ENV);
Expand All @@ -18,6 +19,8 @@ const context = {
};

loadCracoConfigAsync(context).then(cracoConfig => {
validateCraVersion(cracoConfig);

context.paths = getCraPaths(cracoConfig);

overrideWebpackProd(cracoConfig, context);
Expand Down
3 changes: 3 additions & 0 deletions packages/craco/scripts/start.js
Expand Up @@ -10,6 +10,7 @@ const { getCraPaths, start } = require("../lib/cra");
const { loadCracoConfigAsync } = require("../lib/config");
const { overrideWebpackDev } = require("../lib/features/webpack/override");
const { overrideDevServer } = require("../lib/features/dev-server/override");
const { validateCraVersion } = require("../lib/validate-cra-version");

log("Override started with arguments: ", process.argv);
log("For environment: ", process.env.NODE_ENV);
Expand All @@ -19,6 +20,8 @@ const context = {
};

loadCracoConfigAsync(context).then(cracoConfig => {
validateCraVersion(cracoConfig);

context.paths = getCraPaths(cracoConfig);

overrideWebpackDev(cracoConfig, context);
Expand Down
3 changes: 3 additions & 0 deletions packages/craco/scripts/test.js
Expand Up @@ -9,6 +9,7 @@ const { log } = require("../lib/logger");
const { getCraPaths, test } = require("../lib/cra");
const { overrideJest } = require("../lib/features/jest/override");
const { loadCracoConfigAsync } = require("../lib/config");
const { validateCraVersion } = require("../lib/validate-cra-version");

log("Override started with arguments: ", process.argv);
log("For environment: ", process.env.NODE_ENV);
Expand All @@ -18,6 +19,8 @@ const context = {
};

loadCracoConfigAsync(context).then(cracoConfig => {
validateCraVersion(cracoConfig);

context.paths = getCraPaths(cracoConfig);

overrideJest(cracoConfig, context);
Expand Down

0 comments on commit 1e4f912

Please sign in to comment.