diff --git a/.depcheckrc.yml b/.depcheckrc.yml index f07a90b9a488..5d4a1561aa99 100644 --- a/.depcheckrc.yml +++ b/.depcheckrc.yml @@ -82,6 +82,7 @@ ignores: - 'path-browserify' # polyfill - 'nyc' # coverage - 'core-js-pure' # polyfills + - 'react-compiler-webpack' # build tool # babel - '@babel/plugin-transform-logical-assignment-operators' - 'babel-plugin-react-compiler' diff --git a/babel.config.js b/babel.config.js index e1e4e98d7fe1..3ad8419d8415 100644 --- a/babel.config.js +++ b/babel.config.js @@ -13,7 +13,7 @@ module.exports = function (api) { overrides: [ { test: new RegExp( - `^${path.join(__dirname, 'ui')}${slash}(?:components|contexts|hooks|layouts|pages)${slash}(?:.(?!\\.(?:test|stories|container)))+\\.(?:m?[jt]s|[jt]sx)$`, + `^${path.join(__dirname, 'ui')}${slash}(?:components|contexts|hooks|layouts|pages)${slash}(?!.*\\.(?:test|stories|container)\\.)(?:.*)\\.(?:m?[jt]s|[jt]sx)$`, 'u', ), plugins: [['babel-plugin-react-compiler', { target: '17' }]], diff --git a/development/webpack/test/cli.test.ts b/development/webpack/test/cli.test.ts index 0fa3ea421fe6..29023d1ab6f2 100644 --- a/development/webpack/test/cli.test.ts +++ b/development/webpack/test/cli.test.ts @@ -14,6 +14,8 @@ describe('./utils/cli.ts', () => { devtool: 'source-map', sentry: false, test: false, + reactCompilerVerbose: false, + reactCompilerDebug: 'none', zip: false, minify: false, browser: ['chrome'], diff --git a/development/webpack/utils/cli.ts b/development/webpack/utils/cli.ts index da0bddee71c2..9d56749ebc06 100644 --- a/development/webpack/utils/cli.ts +++ b/development/webpack/utils/cli.ts @@ -237,6 +237,23 @@ function getOptions( group: toOrange('Developer assistance:'), type: 'boolean', }, + reactCompilerVerbose: { + array: false, + default: false, + description: + 'Enables/disables React Compiler verbose mode and statistics', + group: toOrange('Developer assistance:'), + type: 'boolean', + }, + reactCompilerDebug: { + array: false, + choices: ['all', 'critical', 'none'] as const, + default: 'none', + description: + 'Sets React Compiler panic threshold that fails the build for all errors or critical errors only. If `none`, the build will not fail.', + group: toOrange('Developer assistance:'), + type: 'string', + }, ...prerequisites, zip: { @@ -394,6 +411,8 @@ LavaMoat debug: ${args.lavamoatDebug} Generate policy: ${args.generatePolicy} Snow: ${args.snow} Sentry: ${args.sentry} +React Compiler verbose: ${args.reactCompilerVerbose} +React Compiler debug: ${args.reactCompilerDebug} Manifest version: ${args.manifest_version} Release version: ${args.releaseVersion} Browsers: ${args.browser.join(', ')} diff --git a/development/webpack/utils/helpers.ts b/development/webpack/utils/helpers.ts index 2d36018f79b0..0107971cb762 100644 --- a/development/webpack/utils/helpers.ts +++ b/development/webpack/utils/helpers.ts @@ -53,6 +53,16 @@ export const TREZOR_MODULE_RE = new RegExp( 'u', ); +/** + * Regular expression to match React files in the top-level `ui/` directory + * Uses a platform-specific path separator: `/` on Unix-like systems and `\` on + * Windows. + */ +export const UI_DIR_RE = new RegExp( + `^${join(__dirname, '..', '..', '..', 'ui').replaceAll(sep, slash)}${slash}(?:components|contexts|hooks|layouts|pages)${slash}.*$`, + 'u', +); + /** * No Operation. A function that does nothing and returns nothing. * diff --git a/development/webpack/utils/loaders/reactCompilerLoader.ts b/development/webpack/utils/loaders/reactCompilerLoader.ts new file mode 100644 index 000000000000..9edd16594498 --- /dev/null +++ b/development/webpack/utils/loaders/reactCompilerLoader.ts @@ -0,0 +1,144 @@ +import { + type ReactCompilerLoaderOption, + defineReactCompilerLoaderOption, + reactCompilerLoader, +} from 'react-compiler-webpack'; +import type { Logger } from 'babel-plugin-react-compiler'; + +/** + * React Compiler logger that tracks compilation statistics + */ +class ReactCompilerLogger { + private compiledCount = 0; + + private skippedCount = 0; + + private errorCount = 0; + + private todoCount = 0; + + private compiledFiles: string[] = []; + + private skippedFiles: string[] = []; + + private errorFiles: string[] = []; + + private todoFiles: string[] = []; + + logEvent( + filename: string | null, + event: { kind: string; detail: { options: { category: string } } }, + ) { + if (filename === null) { + return; + } + const { options: errorDetails } = event.detail ?? {}; + switch (event.kind) { + case 'CompileSuccess': + this.compiledCount++; + this.compiledFiles.push(filename); + console.log(`āœ… Compiled: ${filename}`); + break; + case 'CompileSkip': + this.skippedCount++; + this.skippedFiles.push(filename); + break; + case 'CompileError': + // This error is thrown for syntax that is not yet supported by the React Compiler. + // We count these separately as "unsupported" errors, since there's no actionable fix we can apply. + if (errorDetails?.category === 'Todo') { + this.todoCount++; + this.todoFiles.push(filename); + break; + } + this.errorCount++; + this.errorFiles.push(filename); + console.error( + `āŒ React Compiler error in ${filename}: ${errorDetails ? JSON.stringify(errorDetails) : 'Unknown error'}`, + ); + break; + default: + break; + } + } + + getStats() { + return { + compiled: this.compiledCount, + skipped: this.skippedCount, + errors: this.errorCount, + unsupported: this.todoCount, + total: + this.compiledCount + + this.skippedCount + + this.errorCount + + this.todoCount, + compiledFiles: this.compiledFiles, + skippedFiles: this.skippedFiles, + errorFiles: this.errorFiles, + unsupportedFiles: this.todoFiles, + }; + } + + logSummary() { + const stats = this.getStats(); + console.log('\nšŸ“Š React Compiler Statistics:'); + console.log(` āœ… Compiled: ${stats.compiled} files`); + console.log(` ā­ļø Skipped: ${stats.skipped} files`); + console.log(` āŒ Errors: ${stats.errors} files`); + console.log(` šŸ” Unsupported: ${stats.unsupported} files`); + console.log(` šŸ“¦ Total processed: ${stats.total} files`); + } + + /** + * Reset all statistics. Should be called after each build in watch mode + * to prevent accumulation across rebuilds. + */ + reset() { + this.compiledCount = 0; + this.skippedCount = 0; + this.errorCount = 0; + this.todoCount = 0; + this.compiledFiles = []; + this.skippedFiles = []; + this.errorFiles = []; + this.todoFiles = []; + } +} + +const reactCompilerLogger = new ReactCompilerLogger(); + +/** + * Get the React Compiler logger singleton instance to access statistics. + */ +export function getReactCompilerLogger(): ReactCompilerLogger { + return reactCompilerLogger; +} + +/** + * Get the React Compiler loader. + * + * @param target - The target version of the React Compiler. + * @param verbose - Whether to enable verbose mode. + * @param debug - The debug level to use. + * - 'all': Fail build on and display debug information for all compilation errors. + * - 'critical': Fail build on and display debug information only for critical compilation errors. + * - 'none': Prevent build from failing. + * @returns The React Compiler loader object with the loader and configured options. + */ +export const getReactCompilerLoader = ( + target: ReactCompilerLoaderOption['target'], + verbose: boolean, + debug: 'all' | 'critical' | 'none', +) => { + const reactCompilerOptions = { + target, + logger: verbose ? (reactCompilerLogger as Logger) : undefined, + panicThreshold: debug === 'none' ? undefined : `${debug}_errors`, + } as const satisfies ReactCompilerLoaderOption; + + return { + loader: reactCompilerLoader, + options: defineReactCompilerLoaderOption(reactCompilerOptions), + }; +}; diff --git a/development/webpack/utils/plugins/ReactCompilerPlugin/index.ts b/development/webpack/utils/plugins/ReactCompilerPlugin/index.ts new file mode 100644 index 000000000000..231b0b595529 --- /dev/null +++ b/development/webpack/utils/plugins/ReactCompilerPlugin/index.ts @@ -0,0 +1,13 @@ +import type { Compiler } from 'webpack'; +import { getReactCompilerLogger } from '../../loaders/reactCompilerLoader'; + +export class ReactCompilerPlugin { + apply(compiler: Compiler): void { + compiler.hooks.afterEmit.tap(ReactCompilerPlugin.name, () => { + const logger = getReactCompilerLogger(); + logger.logSummary(); + // Reset statistics after logging to prevent accumulation in watch mode + logger.reset(); + }); + } +} diff --git a/development/webpack/webpack.config.ts b/development/webpack/webpack.config.ts index d7fc57cb0ea6..093cf8b0319e 100644 --- a/development/webpack/webpack.config.ts +++ b/development/webpack/webpack.config.ts @@ -28,11 +28,13 @@ import { __HMR_READY__, SNOW_MODULE_RE, TREZOR_MODULE_RE, + UI_DIR_RE, } from './utils/helpers'; import { transformManifest } from './utils/plugins/ManifestPlugin/helpers'; import { parseArgv, getDryRunMessage } from './utils/cli'; import { getCodeFenceLoader } from './utils/loaders/codeFenceLoader'; import { getSwcLoader } from './utils/loaders/swcLoader'; +import { getReactCompilerLoader } from './utils/loaders/reactCompilerLoader'; import { getVariables } from './utils/config'; import { ManifestPlugin } from './utils/plugins/ManifestPlugin'; import { getLatestCommit } from './utils/git'; @@ -211,6 +213,13 @@ if (args.progress) { const { ProgressPlugin } = require('webpack'); plugins.push(new ProgressPlugin()); } +if (args.reactCompilerVerbose) { + const { + ReactCompilerPlugin, + } = require('./utils/plugins/ReactCompilerPlugin'); + plugins.push(new ReactCompilerPlugin()); +} + // #endregion plugins const swcConfig = { args, browsersListQuery, isDevelopment }; @@ -218,6 +227,11 @@ const tsxLoader = getSwcLoader('typescript', true, safeVariables, swcConfig); const jsxLoader = getSwcLoader('ecmascript', true, safeVariables, swcConfig); const npmLoader = getSwcLoader('ecmascript', false, {}, swcConfig); const cjsLoader = getSwcLoader('ecmascript', false, {}, swcConfig, 'commonjs'); +const reactCompilerLoader = getReactCompilerLoader( + '17', + args.reactCompilerVerbose, + args.reactCompilerDebug, +); const config = { entry, @@ -320,6 +334,11 @@ const config = { dependency: 'url', type: 'asset/resource', }, + { + test: /^(?!.*\.(?:test|stories|container)\.)(?:.*)\.(?:m?[jt]s|[jt]sx)$/u, + include: UI_DIR_RE, + use: [reactCompilerLoader], + }, // own typescript, and own typescript with jsx { test: /\.(?:ts|mts|tsx)$/u, diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 497155b5ec35..efc163f3ff9f 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -291,12 +291,12 @@ "@babel/preset-env>@babel/helper-plugin-utils": true } }, - "@babel/preset-typescript>@babel/plugin-syntax-jsx": { + "react-compiler-webpack>@babel/plugin-syntax-jsx": { "packages": { "@babel/preset-env>@babel/helper-plugin-utils": true } }, - "@babel/preset-typescript>@babel/plugin-transform-typescript>@babel/plugin-syntax-typescript": { + "react-compiler-webpack>@babel/plugin-syntax-typescript": { "packages": { "@babel/preset-env>@babel/helper-plugin-utils": true } @@ -584,7 +584,7 @@ "@babel/preset-env>@babel/plugin-transform-classes>@babel/helper-annotate-as-pure": true, "@babel/core>@babel/helper-module-transforms>@babel/helper-module-imports": true, "@babel/preset-env>@babel/helper-plugin-utils": true, - "@babel/preset-typescript>@babel/plugin-syntax-jsx": true + "react-compiler-webpack>@babel/plugin-syntax-jsx": true } }, "@babel/preset-react>@babel/plugin-transform-react-pure-annotations": { @@ -650,7 +650,7 @@ "@babel/preset-env>@babel/plugin-transform-private-methods>@babel/helper-create-class-features-plugin": true, "@babel/preset-env>@babel/helper-plugin-utils": true, "@babel/preset-env>@babel/plugin-transform-for-of>@babel/helper-skip-transparent-expression-wrappers": true, - "@babel/preset-typescript>@babel/plugin-transform-typescript>@babel/plugin-syntax-typescript": true + "react-compiler-webpack>@babel/plugin-syntax-typescript": true } }, "@babel/preset-env>@babel/plugin-transform-unicode-escapes": { @@ -768,7 +768,7 @@ "packages": { "@babel/preset-env>@babel/helper-plugin-utils": true, "@babel/preset-env>@babel/helper-validator-option": true, - "@babel/preset-typescript>@babel/plugin-syntax-jsx": true, + "react-compiler-webpack>@babel/plugin-syntax-jsx": true, "@babel/preset-env>@babel/plugin-transform-modules-commonjs": true, "@babel/preset-typescript>@babel/plugin-transform-typescript": true } diff --git a/lavamoat/webpack/mv2/policy.json b/lavamoat/webpack/mv2/policy.json index ad6901a59f49..2437e1c94c17 100644 --- a/lavamoat/webpack/mv2/policy.json +++ b/lavamoat/webpack/mv2/policy.json @@ -4988,6 +4988,14 @@ "react": true } }, + "react-compiler-runtime": { + "globals": { + "console.error": true + }, + "packages": { + "react": true + } + }, "react-devtools-core": { "globals": { "CSSStyleRule": true, diff --git a/lavamoat/webpack/mv3/policy.json b/lavamoat/webpack/mv3/policy.json index d6217eb8ffba..0c5504fe8a11 100644 --- a/lavamoat/webpack/mv3/policy.json +++ b/lavamoat/webpack/mv3/policy.json @@ -3356,6 +3356,14 @@ "react": true } }, + "react-compiler-runtime": { + "globals": { + "console.error": true + }, + "packages": { + "react": true + } + }, "react-devtools-core": { "globals": { "CSSStyleRule": true, diff --git a/package.json b/package.json index 0797342bb815..5f8478df134c 100644 --- a/package.json +++ b/package.json @@ -684,6 +684,7 @@ "process": "^0.11.10", "pumpify": "^2.0.1", "randomcolor": "^0.5.4", + "react-compiler-webpack": "^1.0.0", "react-devtools": "^6.1.5", "react-devtools-core": "^6.1.5", "react-syntax-highlighter": "^15.5.0", diff --git a/yarn.lock b/yarn.lock index 8b2c643e9491..461f5be8fb7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -775,7 +775,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.22.5, @babel/plugin-syntax-jsx@npm:^7.25.9, @babel/plugin-syntax-jsx@npm:^7.7.2": +"@babel/plugin-syntax-jsx@npm:^7.22.5, @babel/plugin-syntax-jsx@npm:^7.25.9, @babel/plugin-syntax-jsx@npm:^7.27.1, @babel/plugin-syntax-jsx@npm:^7.7.2": version: 7.27.1 resolution: "@babel/plugin-syntax-jsx@npm:7.27.1" dependencies: @@ -863,14 +863,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-typescript@npm:^7.25.9, @babel/plugin-syntax-typescript@npm:^7.7.2": - version: 7.25.9 - resolution: "@babel/plugin-syntax-typescript@npm:7.25.9" +"@babel/plugin-syntax-typescript@npm:^7.25.9, @babel/plugin-syntax-typescript@npm:^7.27.1, @babel/plugin-syntax-typescript@npm:^7.7.2": + version: 7.27.1 + resolution: "@babel/plugin-syntax-typescript@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/0e9821e8ba7d660c36c919654e4144a70546942ae184e85b8102f2322451eae102cbfadbcadd52ce077a2b44b400ee52394c616feab7b5b9f791b910e933fd33 + checksum: 10/87836f7e32af624c2914c73cd6b9803cf324e07d43f61dbb973c6a86f75df725e12540d91fac7141c14b697aa9268fd064220998daced156e96ac3062d7afb41 languageName: node linkType: hard @@ -33796,6 +33796,7 @@ __metadata: react-beautiful-dnd: "npm:^13.1.1" react-chartjs-2: "npm:^5.2.0" react-compiler-runtime: "npm:^1.0.0" + react-compiler-webpack: "npm:^1.0.0" react-devtools: "npm:^6.1.5" react-devtools-core: "npm:^6.1.5" react-dom: "npm:^17.0.2" @@ -37943,6 +37944,19 @@ __metadata: languageName: node linkType: hard +"react-compiler-webpack@npm:^1.0.0": + version: 1.0.0 + resolution: "react-compiler-webpack@npm:1.0.0" + dependencies: + "@babel/core": "npm:^7.28.5" + "@babel/plugin-syntax-jsx": "npm:^7.27.1" + "@babel/plugin-syntax-typescript": "npm:^7.27.1" + peerDependencies: + babel-plugin-react-compiler: "*" + checksum: 10/00f07cbfa1568628ea593fbbb577645dac73baa682462ace681de610c9a4800039ddd33fbc751260fa1a36a114ddc4a6ffb48c9b84aab9298ae9b3db6ba0b7ed + languageName: node + linkType: hard + "react-devtools-core@npm:6.1.5, react-devtools-core@npm:^6.1.5": version: 6.1.5 resolution: "react-devtools-core@npm:6.1.5"