From e5192823067d2055e48f66c0e33a544f9fa093ca Mon Sep 17 00:00:00 2001 From: Jordi Kloosterboer Date: Wed, 2 Sep 2026 10:17:36 -0600 Subject: [PATCH 1/5] Support ESLint flat config in dev-server lint plugin Bump eslint-webpack-plugin to ^5 and instantiate it flat-aware: apps with an eslint.config.{js,mjs,cjs} at their root get linted with that config directly (via FlatESLint on ESLint 8.57), while apps without one keep the legacy eslintrc baseConfig behavior unchanged. Co-Authored-By: Claude Fable 5 --- .../react-scripts/config/webpack.config.js | 37 +++++++++++++++++++ packages/react-scripts/package.json | 4 +- 2 files changed, 39 insertions(+), 2 deletions(-) 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..5542c01e2ce 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -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", From 423c721edeaf086309f9b968ef246019d3ed5494 Mon Sep 17 00:00:00 2001 From: Jordi Kloosterboer Date: Wed, 2 Sep 2026 10:23:07 -0600 Subject: [PATCH 2/5] Bump version to 8.17.0 and update changelog Co-Authored-By: Claude Fable 5 --- CHANGELOG-FRONTIER.md | 8 ++++++++ packages/react-scripts/package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG-FRONTIER.md b/CHANGELOG-FRONTIER.md index 938fc455397..662dd670745 100644 --- a/CHANGELOG-FRONTIER.md +++ b/CHANGELOG-FRONTIER.md @@ -1,3 +1,11 @@ +## 8.17.0 + +- Support ESLint flat config in the dev-server lint plugin; bump `eslint-webpack-plugin` from `^3.1.1` to `^5.0.3` and `eslint` from `^8.3.0` to `^8.57.1` + - Apps with an `eslint.config.{js,mjs,cjs}` at their root are now linted with that config directly. The plugin is instantiated with `configType: 'flat'` and `eslintPath: 'eslint/use-at-your-own-risk'`, which exposes `FlatESLint` on ESLint 8.57 (on ESLint 9+ the plugin uses `loadESLint()` and the same path keeps resolving the right class). `baseConfig`/`resolvePluginsRelativeTo` are dropped in this branch — `FlatESLint` rejects both — and the `react/react-in-jsx-scope` fallback for apps without the new JSX transform is passed as a flat-format `baseConfig` instead + - Apps without a flat config file keep the existing eslintrc behavior unchanged (baseConfig extending `@fs/eslint-config-frontier-react`), with an explicit `configType: 'eslintrc'` since eslint-webpack-plugin v5 defaults to `'flat'` + - `ESLINT_USE_FLAT_CONFIG=false` forces the legacy branch, mirroring the ESLint CLI escape hatch; `DISABLE_ESLINT_PLUGIN=true` still disables linting entirely + - `eslint@^8.57.1` is required because 8.57 is the first version whose `use-at-your-own-risk` entry exports `FlatESLint`/`LegacyESLint`, which the v5 plugin worker relies on + ## 8.16.3 - Migrate CI from Travis to GitHub Actions (`.github/workflows/ci.yml`); delete `.travis.yml` diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 5542c01e2ce..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": { From b6434ba73410178a2223854f1d07174565f11f78 Mon Sep 17 00:00:00 2001 From: Jordi Kloosterboer Date: Wed, 2 Sep 2026 10:24:25 -0600 Subject: [PATCH 3/5] Shorten 8.17.0 changelog entry Co-Authored-By: Claude Fable 5 --- CHANGELOG-FRONTIER.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG-FRONTIER.md b/CHANGELOG-FRONTIER.md index 662dd670745..827205d8f56 100644 --- a/CHANGELOG-FRONTIER.md +++ b/CHANGELOG-FRONTIER.md @@ -1,10 +1,8 @@ ## 8.17.0 -- Support ESLint flat config in the dev-server lint plugin; bump `eslint-webpack-plugin` from `^3.1.1` to `^5.0.3` and `eslint` from `^8.3.0` to `^8.57.1` - - Apps with an `eslint.config.{js,mjs,cjs}` at their root are now linted with that config directly. The plugin is instantiated with `configType: 'flat'` and `eslintPath: 'eslint/use-at-your-own-risk'`, which exposes `FlatESLint` on ESLint 8.57 (on ESLint 9+ the plugin uses `loadESLint()` and the same path keeps resolving the right class). `baseConfig`/`resolvePluginsRelativeTo` are dropped in this branch — `FlatESLint` rejects both — and the `react/react-in-jsx-scope` fallback for apps without the new JSX transform is passed as a flat-format `baseConfig` instead - - Apps without a flat config file keep the existing eslintrc behavior unchanged (baseConfig extending `@fs/eslint-config-frontier-react`), with an explicit `configType: 'eslintrc'` since eslint-webpack-plugin v5 defaults to `'flat'` - - `ESLINT_USE_FLAT_CONFIG=false` forces the legacy branch, mirroring the ESLint CLI escape hatch; `DISABLE_ESLINT_PLUGIN=true` still disables linting entirely - - `eslint@^8.57.1` is required because 8.57 is the first version whose `use-at-your-own-risk` entry exports `FlatESLint`/`LegacyESLint`, which the v5 plugin worker relies on +- 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 From 9780df21d6112cd8e9aa28943fe10070a6b2fe18 Mon Sep 17 00:00:00 2001 From: Jordi Kloosterboer Date: Wed, 2 Sep 2026 11:16:05 -0600 Subject: [PATCH 4/5] Pin webpack 5.110.1 in CI smoke-test app (temporary) webpack 5.110.2 breaks builds with "export 'css' was not found in '@linaria/core'". Pin via overrides in the scaffolded app only, per Frontier Core's announced workaround; remove once the Zion fix ships. Co-Authored-By: Claude Fable 5 --- publishPrReleaseAndCreateFreshCraTemplate.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/publishPrReleaseAndCreateFreshCraTemplate.js b/publishPrReleaseAndCreateFreshCraTemplate.js index dde78d06c61..5d9a8dd83b5 100644 --- a/publishPrReleaseAndCreateFreshCraTemplate.js +++ b/publishPrReleaseAndCreateFreshCraTemplate.js @@ -64,3 +64,14 @@ if (!fs.existsSync(scriptsBin)) { console.error('and is usually a transitive dependency of the template rather than of react-scripts.') process.exit(1) } + +// TEMPORARY (Sept 2026): webpack 5.110.2 breaks builds with "export 'css' (imported as 'css') +// was not found in '@linaria/core'". Frontier Core's announced workaround is pinning webpack +// 5.110.1 via overrides until the fix ships in Zion packages; remove this block once it does. +// https://familysearch.slack.com/archives/C04NU7BJ8/p1788365473434469 +alterPackageJsonFile(appDir, packageJson => { + packageJson.overrides = { ...packageJson.overrides, webpack: '5.110.1' } + console.log('TEMPORARY: pinning webpack to 5.110.1 in the scaffolded app (see comment above)') + return packageJson +}) +runExternalCommandSync('npm', ['install'], { cwd: appDir }) From a238c47d28d4d5f5cc257a491a93cc28bf1b7733 Mon Sep 17 00:00:00 2001 From: Jordi Kloosterboer Date: Thu, 3 Sep 2026 09:32:29 -0600 Subject: [PATCH 5/5] Remove temp fix for webpack issue --- publishPrReleaseAndCreateFreshCraTemplate.js | 80 ++++++++++---------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/publishPrReleaseAndCreateFreshCraTemplate.js b/publishPrReleaseAndCreateFreshCraTemplate.js index 5d9a8dd83b5..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,31 +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); } - -// TEMPORARY (Sept 2026): webpack 5.110.2 breaks builds with "export 'css' (imported as 'css') -// was not found in '@linaria/core'". Frontier Core's announced workaround is pinning webpack -// 5.110.1 via overrides until the fix ships in Zion packages; remove this block once it does. -// https://familysearch.slack.com/archives/C04NU7BJ8/p1788365473434469 -alterPackageJsonFile(appDir, packageJson => { - packageJson.overrides = { ...packageJson.overrides, webpack: '5.110.1' } - console.log('TEMPORARY: pinning webpack to 5.110.1 in the scaffolded app (see comment above)') - return packageJson -}) -runExternalCommandSync('npm', ['install'], { cwd: appDir })