diff --git a/CHANGELOG-FRONTIER.md b/CHANGELOG-FRONTIER.md index 938fc455397..827205d8f56 100644 --- a/CHANGELOG-FRONTIER.md +++ b/CHANGELOG-FRONTIER.md @@ -1,3 +1,9 @@ +## 8.17.0 + +- Support ESLint flat config in the dev-server lint plugin; bump `eslint-webpack-plugin` to `^5.0.3` and `eslint` to `^8.57.1` + - Apps with an `eslint.config.{js,mjs,cjs}` are linted with that config directly; apps without one keep the legacy eslintrc behavior unchanged + - `ESLINT_USE_FLAT_CONFIG=false` forces the legacy path + ## 8.16.3 - Migrate CI from Travis to GitHub Actions (`.github/workflows/ci.yml`); delete `.travis.yml` diff --git a/packages/react-scripts/config/webpack.config.js b/packages/react-scripts/config/webpack.config.js index 03c3eec5006..99db20d5c64 100644 --- a/packages/react-scripts/config/webpack.config.js +++ b/packages/react-scripts/config/webpack.config.js @@ -80,6 +80,13 @@ const babelRuntimeRegenerator = require.resolve('@babel/runtime/regenerator', { const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false' const disableESLintPlugin = process.env.DISABLE_ESLINT_PLUGIN === 'true' +// Apps with an ESLint flat config file get linted with it directly; apps without one keep the +// legacy eslintrc behavior (baseConfig extending @fs/eslint-config-frontier-react). +const useFlatEslintConfig = + process.env.ESLINT_USE_FLAT_CONFIG !== 'false' && + ['eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs'].some(configFile => + fs.existsSync(path.join(paths.appPath, configFile)) + ) const needsWci18nSupport = process.env.NEEDS_WCI18N_SUPPORT === 'true' const imageInlineSizeLimit = parseInt(process.env.IMAGE_INLINE_SIZE_LIMIT || '10000') @@ -878,11 +885,41 @@ module.exports = function (webpackEnv) { }, }), !disableESLintPlugin && + useFlatEslintConfig && + new ESLintPlugin({ + // Plugin options + extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'], + formatter: require.resolve('react-dev-utils/eslintFormatter'), + // 'use-at-your-own-risk' exposes FlatESLint on ESLint 8.57, which configType 'flat' + // needs; the app's eslint.config.* is auto-loaded from cwd. On ESLint 9+ the plugin + // uses loadESLint() and this path still resolves the right class. + eslintPath: require.resolve('eslint/use-at-your-own-risk'), + configType: 'flat', + failOnError: false, + context: paths.appSrc, + cache: true, + cacheLocation: path.resolve(paths.appNodeModules, '.cache/.eslintcache'), + // ESLint class options + cwd: paths.appPath, + baseConfig: [ + { + rules: { + ...(!hasJsxRuntime && { + 'react/react-in-jsx-scope': 'error', + }), + }, + }, + ], + }), + !disableESLintPlugin && + !useFlatEslintConfig && new ESLintPlugin({ // Plugin options extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'], formatter: require.resolve('react-dev-utils/eslintFormatter'), eslintPath: require.resolve('eslint'), + // eslint-webpack-plugin@5 defaults to 'flat'; keep legacy apps on eslintrc. + configType: 'eslintrc', failOnError: false, context: paths.appSrc, cache: true, diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 8534658cbec..2f20b859b90 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -1,6 +1,6 @@ { "name": "@fs/react-scripts", - "version": "8.16.3", + "version": "8.17.0", "upstreamVersion": "5.0.1", "description": "Configuration and scripts for Create React App.", "repository": { @@ -80,8 +80,8 @@ "filing-cabinet": "^5.2.0", "dotenv": "^10.0.0", "dotenv-expand": "^5.1.0", - "eslint": "^8.3.0", - "eslint-webpack-plugin": "^3.1.1", + "eslint": "^8.57.1", + "eslint-webpack-plugin": "^5.0.3", "file-loader": "^6.2.0", "fs-extra": "^10.0.0", "html-webpack-plugin": "^5.5.0", diff --git a/publishPrReleaseAndCreateFreshCraTemplate.js b/publishPrReleaseAndCreateFreshCraTemplate.js index dde78d06c61..a104d317774 100644 --- a/publishPrReleaseAndCreateFreshCraTemplate.js +++ b/publishPrReleaseAndCreateFreshCraTemplate.js @@ -1,40 +1,49 @@ -'use strict' +'use strict'; -const fs = require('fs') -const path = require('path') +const fs = require('fs'); +const path = require('path'); -const reactScriptPath = path.join(__dirname, 'packages/react-scripts') +const reactScriptPath = path.join(__dirname, 'packages/react-scripts'); const { alterPackageJsonFile, getCiPrereleaseVersion } = require(path.join( reactScriptPath, 'scripts/utils/frontierInit' -)) -const { runExternalCommandSync } = require(path.join(reactScriptPath, 'scripts/utils/osUtils')) +)); +const { runExternalCommandSync } = require(path.join( + reactScriptPath, + 'scripts/utils/osUtils' +)); -let originalVersion -let newVersion +let originalVersion; +let newVersion; alterPackageJsonFile(reactScriptPath, packageJson => { - originalVersion = packageJson.version - newVersion = getCiPrereleaseVersion(packageJson.version) - console.log(`setting @fs/react-scripts version to "${newVersion}" temporarily to get published`) - packageJson.version = newVersion - return packageJson -}) + originalVersion = packageJson.version; + newVersion = getCiPrereleaseVersion(packageJson.version); + console.log( + `setting @fs/react-scripts version to "${newVersion}" temporarily to get published` + ); + packageJson.version = newVersion; + return packageJson; +}); -runExternalCommandSync('npm', ['run', 'fs-publish', '--', '--allow-earlier-version'], { cwd: reactScriptPath }) +runExternalCommandSync( + 'npm', + ['run', 'fs-publish', '--', '--allow-earlier-version'], + { cwd: reactScriptPath } +); alterPackageJsonFile(reactScriptPath, packageJson => { - packageJson.version = originalVersion - console.log(`setting @fs/react-scripts version back to "${originalVersion}"`) - return packageJson -}) + packageJson.version = originalVersion; + console.log(`setting @fs/react-scripts version back to "${originalVersion}"`); + return packageJson; +}); -const tmpDir = `${process.env.HOME}/tmp` +const tmpDir = `${process.env.HOME}/tmp`; -const appDir = path.join(tmpDir, 'fresh-cra-template') +const appDir = path.join(tmpDir, 'fresh-cra-template'); -runExternalCommandSync('mkdir', ['-p', tmpDir]) +runExternalCommandSync('mkdir', ['-p', tmpDir]); runExternalCommandSync( 'npx', [ @@ -47,20 +56,24 @@ runExternalCommandSync( '@fs/cra-template', ], { cwd: tmpDir } -) +); // create-react-app does not fail when the template's dependency install fails: init.js logs // "`npm install ...` failed" and returns, so the process still exits 0. That is deliberate // upstream behavior — exiting non-zero makes create-react-app delete the app's package.json and // node_modules — so assert here instead. Without this, the real failure surfaces two steps later // as `react-scripts: not found`, which reads like a react-scripts packaging bug. -const scriptsBin = path.join(appDir, 'node_modules', '.bin', 'react-scripts') +const scriptsBin = path.join(appDir, 'node_modules', '.bin', 'react-scripts'); if (!fs.existsSync(scriptsBin)) { - console.error(`\nTemplate dependency install did not complete: ${scriptsBin} is missing.`) + console.error( + `\nTemplate dependency install did not complete: ${scriptsBin} is missing.` + ); console.error( 'Check the npm output above. A tarball blocked by jfrog curation shows up as `npm error code E403`,' - ) - console.error('and is usually a transitive dependency of the template rather than of react-scripts.') - process.exit(1) + ); + console.error( + 'and is usually a transitive dependency of the template rather than of react-scripts.' + ); + process.exit(1); }