From 479af2ca11fddc321dec4fc7fa9956cc900c52a2 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Thu, 7 Jan 2021 14:29:31 +0100 Subject: [PATCH 1/7] feat(gatsby): add required eslint rules even if user has custom eslint config --- packages/gatsby/src/utils/eslint-config.ts | 57 ++++++++++++++++++-- packages/gatsby/src/utils/eslint/required.js | 7 +++ packages/gatsby/src/utils/webpack-utils.ts | 46 ++++++++++++++-- packages/gatsby/src/utils/webpack.config.js | 14 ++++- 4 files changed, 114 insertions(+), 10 deletions(-) create mode 100644 packages/gatsby/src/utils/eslint/required.js diff --git a/packages/gatsby/src/utils/eslint-config.ts b/packages/gatsby/src/utils/eslint-config.ts index c4c6397041842..b80d7734b7206 100644 --- a/packages/gatsby/src/utils/eslint-config.ts +++ b/packages/gatsby/src/utils/eslint-config.ts @@ -1,6 +1,53 @@ import { printSchema, GraphQLSchema } from "graphql" import { CLIEngine } from "eslint" +const eslintRulePaths = `${__dirname}/eslint-rules` +const eslintRequirePreset = require.resolve(`./eslint/required`) + +export const eslintRequiredConfig: CLIEngine.Options = { + rulePaths: [eslintRulePaths], + baseConfig: { + globals: { + graphql: true, + __PATH_PREFIX__: true, + __BASE_PATH__: true, // this will rarely, if ever, be used by consumers + }, + extends: [eslintRequirePreset], + }, +} + +export function mergeRequiredConfigIn( + existingOptions: CLIEngine.Options +): void { + // make sure rulePaths include our custom rules + if (existingOptions.rulePaths) { + if ( + Array.isArray(existingOptions.rulePaths) && + !existingOptions.rulePaths.includes(eslintRulePaths) + ) { + existingOptions.rulePaths.push(eslintRulePaths) + } + } else { + existingOptions.rulePaths = [eslintRulePaths] + } + + // make sure we extend required preset + if (!existingOptions.baseConfig) { + existingOptions.baseConfig = {} + } + + if (existingOptions.baseConfig.extends) { + if ( + Array.isArray(existingOptions.baseConfig.extends) && + !existingOptions.baseConfig.extends.includes(eslintRequirePreset) + ) { + existingOptions.baseConfig.extends.push(eslintRequirePreset) + } + } else { + existingOptions.baseConfig.extends = [eslintRequirePreset] + } +} + export const eslintConfig = ( schema: GraphQLSchema, usingJsxRuntime: boolean @@ -8,19 +55,19 @@ export const eslintConfig = ( return { useEslintrc: false, resolvePluginsRelativeTo: __dirname, - rulePaths: [`${__dirname}/eslint-rules`], + rulePaths: [eslintRulePaths], baseConfig: { globals: { graphql: true, __PATH_PREFIX__: true, __BASE_PATH__: true, // this will rarely, if ever, be used by consumers }, - extends: [require.resolve(`eslint-config-react-app`)], + extends: [ + require.resolve(`eslint-config-react-app`), + eslintRequirePreset, + ], plugins: [`graphql`], rules: { - // Custom ESLint rules from Gatsby - "no-anonymous-exports-page-templates": `warn`, - "limited-exports-page-templates": `warn`, // New versions of react use a special jsx runtime that remove the requirement // for having react in scope for jsx. Once the jsx runtime is backported to all // versions of react we can make this always be `off`. diff --git a/packages/gatsby/src/utils/eslint/required.js b/packages/gatsby/src/utils/eslint/required.js new file mode 100644 index 0000000000000..ab05990ed383a --- /dev/null +++ b/packages/gatsby/src/utils/eslint/required.js @@ -0,0 +1,7 @@ +module.exports = { + rules: { + // Custom ESLint rules from Gatsby + "no-anonymous-exports-page-templates": `warn`, + "limited-exports-page-templates": `warn`, + }, +} diff --git a/packages/gatsby/src/utils/webpack-utils.ts b/packages/gatsby/src/utils/webpack-utils.ts index 77c942504f122..c3f0c6fcbb557 100644 --- a/packages/gatsby/src/utils/webpack-utils.ts +++ b/packages/gatsby/src/utils/webpack-utils.ts @@ -1,5 +1,5 @@ import * as path from "path" -import { Loader, RuleSetRule, Plugin } from "webpack" +import { Loader, RuleSetRule, Plugin, Configuration } from "webpack" import { GraphQLSchema } from "graphql" import postcss from "postcss" import autoprefixer from "autoprefixer" @@ -20,7 +20,11 @@ import { import { builtinPlugins } from "./webpack-plugins" import { IProgram, Stage } from "../commands/types" -import { eslintConfig } from "./eslint-config" +import { + eslintConfig, + mergeRequiredConfigIn, + eslintRequiredConfig, +} from "./eslint-config" type LoaderResolver = (options?: T) => Loader @@ -124,6 +128,8 @@ interface IWebpackUtils { plugins: PluginUtils } +const vendorRegex = /(node_modules|bower_components)/ + /** * A factory method that produces an atoms namespace */ @@ -132,7 +138,6 @@ export const createWebpackUtils = ( program: IProgram ): IWebpackUtils => { const assetRelativeRoot = `static/` - const vendorRegex = /(node_modules|bower_components)/ const supportedBrowsers = getBrowsersList(program.directory) const PRODUCTION = !stage.includes(`develop`) @@ -749,3 +754,38 @@ export function reactHasJsxRuntime(): boolean { return false } + +export function ensureRequireEslintRules(config: Configuration): Configuration { + // for fast refresh we want to ensure that that there is eslint rule running + // because user might have added their own `eslint-loader` let's check if there is one + // and adjust it to add the rule or append new loader with required rule + const rule = config.module.rules.find(rule => { + if (typeof rule.loader === `string`) { + return rule.loader.includes(`eslint-loader`) + } + + return false + }) + + if (rule) { + if (typeof rule.options !== `string`) { + mergeRequiredConfigIn(rule.options) + } + } else { + config.module.rules.push({ + enforce: `pre`, + test: /\.jsx?$/, + exclude: (modulePath: string): boolean => + modulePath.includes(VIRTUAL_MODULES_BASE_PATH) || + vendorRegex.test(modulePath), + use: [ + { + loader: require.resolve(`eslint-loader`), + options: eslintRequiredConfig, + }, + ], + }) + } + + return config +} diff --git a/packages/gatsby/src/utils/webpack.config.js b/packages/gatsby/src/utils/webpack.config.js index 669d941995a28..9db7273c17770 100644 --- a/packages/gatsby/src/utils/webpack.config.js +++ b/packages/gatsby/src/utils/webpack.config.js @@ -15,7 +15,7 @@ const report = require(`gatsby-cli/lib/reporter`) import { withBasePath, withTrailingSlash } from "./path" import { getGatsbyDependents } from "./gatsby-dependents" const apiRunnerNode = require(`./api-runner-node`) -import { createWebpackUtils } from "./webpack-utils" +import { createWebpackUtils, ensureRequireEslintRules } from "./webpack-utils" import { hasLocalEslint } from "./local-eslint-config-finder" import { getAbsolutePathForVirtualModule } from "./gatsby-webpack-virtual-modules" @@ -732,5 +732,15 @@ module.exports = async ( parentSpan, }) - return getConfig() + let finalConfig = getConfig() + + if ( + stage === `develop` && + process.env.GATSBY_HOT_LOADER === `fast-refresh` && + hasLocalEslint(program.directory) + ) { + finalConfig = ensureRequireEslintRules(finalConfig) + } + + return finalConfig } From f5ad45a070346ccc8e380180c31b6b71a9ef02ac Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Mon, 11 Jan 2021 17:34:01 +0100 Subject: [PATCH 2/7] handle case when extends is single string, add some tests --- .../src/utils/__tests__/eslint-config.ts | 130 ++++++++++++++++++ packages/gatsby/src/utils/eslint-config.ts | 8 ++ 2 files changed, 138 insertions(+) create mode 100644 packages/gatsby/src/utils/__tests__/eslint-config.ts diff --git a/packages/gatsby/src/utils/__tests__/eslint-config.ts b/packages/gatsby/src/utils/__tests__/eslint-config.ts new file mode 100644 index 0000000000000..e9fdf61f41822 --- /dev/null +++ b/packages/gatsby/src/utils/__tests__/eslint-config.ts @@ -0,0 +1,130 @@ +import { mergeRequiredConfigIn } from "../eslint-config" +import { CLIEngine } from "eslint" +import * as path from "path" + +describe(`eslint-config`, () => { + describe(`mergeRequiredConfigIn`, () => { + it(`adds rulePaths and extends if those don't exist`, () => { + const conf: CLIEngine.Options = {} + + mergeRequiredConfigIn(conf) + + expect(conf?.baseConfig).toMatchInlineSnapshot(` + Object { + "extends": Array [ + "/packages/gatsby/src/utils/eslint/required.js", + ], + } + `) + + expect(conf.rulePaths).toMatchInlineSnapshot(` + Array [ + "/packages/gatsby/src/utils/eslint-rules", + ] + `) + }) + + it(`adds rulePath if rulePaths exist but don't contain required rules`, () => { + const conf: CLIEngine.Options = { + rulePaths: [`test`], + } + + mergeRequiredConfigIn(conf) + + expect(conf.rulePaths).toMatchInlineSnapshot(` + Array [ + "test", + "/packages/gatsby/src/utils/eslint-rules", + ] + `) + }) + + it(`doesn't add rulePath multiple times`, () => { + const conf: CLIEngine.Options = { + rulePaths: [path.resolve(__dirname, `../eslint-rules`), `test`], + } + + mergeRequiredConfigIn(conf) + + expect(conf.rulePaths).toMatchInlineSnapshot(` + Array [ + "/packages/gatsby/src/utils/eslint-rules", + "test", + ] + `) + }) + + it(`adds extend if extends exist (array) but don't contain required preset`, () => { + const conf: CLIEngine.Options = { + baseConfig: { + extends: [`ext1`], + }, + } + + mergeRequiredConfigIn(conf) + + expect(conf.baseConfig).toMatchInlineSnapshot(` + Object { + "extends": Array [ + "ext1", + "/packages/gatsby/src/utils/eslint/required.js", + ], + } + `) + }) + + it(`adds extend if extends exist (string) but don't contain required preset`, () => { + const conf: CLIEngine.Options = { + baseConfig: { + extends: `ext1`, + }, + } + + mergeRequiredConfigIn(conf) + + expect(conf.baseConfig).toMatchInlineSnapshot(` + Object { + "extends": Array [ + "ext1", + "/packages/gatsby/src/utils/eslint/required.js", + ], + } + `) + }) + + it(`doesn't add extend multiple times (extends is array)`, () => { + const conf: CLIEngine.Options = { + baseConfig: { + extends: [require.resolve(`../eslint/required`), `ext1`], + }, + } + + mergeRequiredConfigIn(conf) + + expect(conf.baseConfig).toMatchInlineSnapshot(` + Object { + "extends": Array [ + "/packages/gatsby/src/utils/eslint/required.js", + "ext1", + ], + } + `) + }) + + it(`doesn't add extend multiple times (extends is string)`, () => { + const conf: CLIEngine.Options = { + baseConfig: { + extends: require.resolve(`../eslint/required`), + }, + } + + mergeRequiredConfigIn(conf) + + expect(conf.baseConfig).toMatchInlineSnapshot(` + Object { + "extends": "/packages/gatsby/src/utils/eslint/required.js", + } + `) + }) + }) +}) diff --git a/packages/gatsby/src/utils/eslint-config.ts b/packages/gatsby/src/utils/eslint-config.ts index b80d7734b7206..c98bf5a3259e2 100644 --- a/packages/gatsby/src/utils/eslint-config.ts +++ b/packages/gatsby/src/utils/eslint-config.ts @@ -42,6 +42,14 @@ export function mergeRequiredConfigIn( !existingOptions.baseConfig.extends.includes(eslintRequirePreset) ) { existingOptions.baseConfig.extends.push(eslintRequirePreset) + } else if ( + typeof existingOptions.baseConfig.extends === `string` && + existingOptions.baseConfig.extends !== eslintRequirePreset + ) { + existingOptions.baseConfig.extends = [ + existingOptions.baseConfig.extends, + eslintRequirePreset, + ] } } else { existingOptions.baseConfig.extends = [eslintRequirePreset] From cd016ffb0808f629c738fefb746d4324791fed08 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Mon, 11 Jan 2021 19:14:59 +0100 Subject: [PATCH 3/7] some extra existance checking --- packages/gatsby/src/utils/webpack-utils.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/gatsby/src/utils/webpack-utils.ts b/packages/gatsby/src/utils/webpack-utils.ts index c3f0c6fcbb557..1f11e46230a70 100644 --- a/packages/gatsby/src/utils/webpack-utils.ts +++ b/packages/gatsby/src/utils/webpack-utils.ts @@ -756,10 +756,15 @@ export function reactHasJsxRuntime(): boolean { } export function ensureRequireEslintRules(config: Configuration): Configuration { + if (!config.module) { + config.module = { + rules: [], + } + } // for fast refresh we want to ensure that that there is eslint rule running // because user might have added their own `eslint-loader` let's check if there is one // and adjust it to add the rule or append new loader with required rule - const rule = config.module.rules.find(rule => { + const rule = config.module?.rules.find(rule => { if (typeof rule.loader === `string`) { return rule.loader.includes(`eslint-loader`) } @@ -769,6 +774,9 @@ export function ensureRequireEslintRules(config: Configuration): Configuration { if (rule) { if (typeof rule.options !== `string`) { + if (!rule.options) { + rule.options = {} + } mergeRequiredConfigIn(rule.options) } } else { From f477549ca5a241254aab003440f3358053ae8dee Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Tue, 12 Jan 2021 12:10:21 +0100 Subject: [PATCH 4/7] more exact checks for existance of eslint-loader rule --- packages/gatsby/src/utils/webpack-utils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/gatsby/src/utils/webpack-utils.ts b/packages/gatsby/src/utils/webpack-utils.ts index 1f11e46230a70..617a948c2000e 100644 --- a/packages/gatsby/src/utils/webpack-utils.ts +++ b/packages/gatsby/src/utils/webpack-utils.ts @@ -766,7 +766,11 @@ export function ensureRequireEslintRules(config: Configuration): Configuration { // and adjust it to add the rule or append new loader with required rule const rule = config.module?.rules.find(rule => { if (typeof rule.loader === `string`) { - return rule.loader.includes(`eslint-loader`) + return ( + rule.loader === `eslint-loader` || + rule.loader.endsWith(`eslint-loader/index.js`) || + rule.loader.endsWith(`eslint-loader/dist/cjs.js`) + ) } return false From 67d085b9c16072d7a0ab96860ea970eb725c344f Mon Sep 17 00:00:00 2001 From: LekoArts Date: Tue, 12 Jan 2021 17:10:25 +0100 Subject: [PATCH 5/7] let's see if slash fixes win32 + env var for rules --- packages/gatsby/src/utils/__tests__/eslint-config.ts | 3 ++- packages/gatsby/src/utils/eslint-config.ts | 3 ++- packages/gatsby/src/utils/eslint/required.js | 6 ++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/gatsby/src/utils/__tests__/eslint-config.ts b/packages/gatsby/src/utils/__tests__/eslint-config.ts index e9fdf61f41822..0e0778687ff0a 100644 --- a/packages/gatsby/src/utils/__tests__/eslint-config.ts +++ b/packages/gatsby/src/utils/__tests__/eslint-config.ts @@ -1,6 +1,7 @@ import { mergeRequiredConfigIn } from "../eslint-config" import { CLIEngine } from "eslint" import * as path from "path" +import { slash } from "gatsby-core-utils" describe(`eslint-config`, () => { describe(`mergeRequiredConfigIn`, () => { @@ -41,7 +42,7 @@ describe(`eslint-config`, () => { it(`doesn't add rulePath multiple times`, () => { const conf: CLIEngine.Options = { - rulePaths: [path.resolve(__dirname, `../eslint-rules`), `test`], + rulePaths: [slash(path.resolve(__dirname, `../eslint-rules`)), `test`], } mergeRequiredConfigIn(conf) diff --git a/packages/gatsby/src/utils/eslint-config.ts b/packages/gatsby/src/utils/eslint-config.ts index c98bf5a3259e2..c823bedbf69a5 100644 --- a/packages/gatsby/src/utils/eslint-config.ts +++ b/packages/gatsby/src/utils/eslint-config.ts @@ -1,7 +1,8 @@ import { printSchema, GraphQLSchema } from "graphql" import { CLIEngine } from "eslint" +import { slash } from "gatsby-core-utils" -const eslintRulePaths = `${__dirname}/eslint-rules` +const eslintRulePaths = slash(`${__dirname}/eslint-rules`) const eslintRequirePreset = require.resolve(`./eslint/required`) export const eslintRequiredConfig: CLIEngine.Options = { diff --git a/packages/gatsby/src/utils/eslint/required.js b/packages/gatsby/src/utils/eslint/required.js index ab05990ed383a..7ac22d57db856 100644 --- a/packages/gatsby/src/utils/eslint/required.js +++ b/packages/gatsby/src/utils/eslint/required.js @@ -1,7 +1,9 @@ module.exports = { rules: { // Custom ESLint rules from Gatsby - "no-anonymous-exports-page-templates": `warn`, - "limited-exports-page-templates": `warn`, + "no-anonymous-exports-page-templates": + process.env.GATSBY_HOT_LOADER === `fast-refresh` ? `warn` : `off`, + "limited-exports-page-templates": + process.env.GATSBY_HOT_LOADER === `fast-refresh` ? `warn` : `off`, }, } From 8fd1faf32b3a4ac853aea46af01eb560906cb5ad Mon Sep 17 00:00:00 2001 From: LekoArts Date: Tue, 12 Jan 2021 17:31:40 +0100 Subject: [PATCH 6/7] another try --- packages/gatsby/src/utils/__tests__/eslint-config.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/gatsby/src/utils/__tests__/eslint-config.ts b/packages/gatsby/src/utils/__tests__/eslint-config.ts index 0e0778687ff0a..e9fdf61f41822 100644 --- a/packages/gatsby/src/utils/__tests__/eslint-config.ts +++ b/packages/gatsby/src/utils/__tests__/eslint-config.ts @@ -1,7 +1,6 @@ import { mergeRequiredConfigIn } from "../eslint-config" import { CLIEngine } from "eslint" import * as path from "path" -import { slash } from "gatsby-core-utils" describe(`eslint-config`, () => { describe(`mergeRequiredConfigIn`, () => { @@ -42,7 +41,7 @@ describe(`eslint-config`, () => { it(`doesn't add rulePath multiple times`, () => { const conf: CLIEngine.Options = { - rulePaths: [slash(path.resolve(__dirname, `../eslint-rules`)), `test`], + rulePaths: [path.resolve(__dirname, `../eslint-rules`), `test`], } mergeRequiredConfigIn(conf) From 5c6bedca0fd58f85bef29d50c08a02b443b64299 Mon Sep 17 00:00:00 2001 From: LekoArts Date: Wed, 13 Jan 2021 15:33:40 +0100 Subject: [PATCH 7/7] don't use slash after all --- packages/gatsby/src/utils/eslint-config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/gatsby/src/utils/eslint-config.ts b/packages/gatsby/src/utils/eslint-config.ts index c823bedbf69a5..0064ab2dd07f3 100644 --- a/packages/gatsby/src/utils/eslint-config.ts +++ b/packages/gatsby/src/utils/eslint-config.ts @@ -1,8 +1,8 @@ import { printSchema, GraphQLSchema } from "graphql" import { CLIEngine } from "eslint" -import { slash } from "gatsby-core-utils" +import path from "path" -const eslintRulePaths = slash(`${__dirname}/eslint-rules`) +const eslintRulePaths = path.resolve(`${__dirname}/eslint-rules`) const eslintRequirePreset = require.resolve(`./eslint/required`) export const eslintRequiredConfig: CLIEngine.Options = {