Skip to content
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ff3e99a
Install `react-compiler-webpack` as devDep
MajorLift Nov 21, 2025
27301e3
Add to depcheck ignore list
MajorLift Nov 21, 2025
11907d8
Update LavaMoat policies
MajorLift Nov 21, 2025
95bc187
Set up webpack config with react compiler loader
MajorLift Nov 21, 2025
6037088
Bump `react-compiler-webpack` to `^1.0.0`
MajorLift Nov 21, 2025
7f6626d
Use regex instead of array iteration to filter paths for react compil…
MajorLift Nov 21, 2025
50fa7b3
Fix `UI_DIR_RE` to only match top-level `ui/` dir
MajorLift Nov 21, 2025
13a52f8
Make regex groups non-capturing
MajorLift Nov 21, 2025
943035d
Remove redundant node_modules exclude
MajorLift Nov 21, 2025
29b7626
Define `reactCompilerLoader`, `ReactCompilerLogger`
MajorLift Nov 21, 2025
87b31a7
Add `reactCompilerVerbose` cli argument
MajorLift Nov 21, 2025
bfa3091
Set up loader, logger in webpack config
MajorLift Nov 21, 2025
5fe6afd
Update webpack-cli test mock
MajorLift Nov 21, 2025
6a0635c
Stricter regex for `ui/`
MajorLift Nov 23, 2025
0e67907
Add comments
MajorLift Nov 23, 2025
4777391
Define `--reactCompilerDebug` webpack build argument that sets `panic…
MajorLift Nov 23, 2025
83d86b9
Less ambiguous regex
MajorLift Nov 23, 2025
6f381b1
Fix regex
MajorLift Nov 24, 2025
38c2fb6
Set debug mode option default to 'none'
MajorLift Nov 24, 2025
e5e61db
Add entries to dry run message
MajorLift Nov 24, 2025
e785706
Group new CLI arguments with other developer assistance entries in `g…
MajorLift Nov 27, 2025
1444569
Define `ReactCompilerPlugin`
MajorLift Nov 28, 2025
b560c3e
Reset react compiler stats to prevent accumulation in watch mode
MajorLift Dec 3, 2025
986c815
Dedupe lockfile
MajorLift Dec 3, 2025
52857d7
Update LavaMoat policies
metamaskbot Dec 3, 2025
e7c11bf
Fix test regex for react compiler loader in both webpack, babel config
MajorLift Dec 3, 2025
0b3aa55
Set `panicThreshold` to `undefined` when `debug` set to `none`
MajorLift Dec 3, 2025
00b19d0
Merge branch 'main' into jongsun/build/enable-react-compiler-webpack
MajorLift Dec 3, 2025
fdba65b
Merge branch 'main' into jongsun/build/enable-react-compiler-webpack
MajorLift Dec 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .depcheckrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 2 additions & 0 deletions development/webpack/test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
19 changes: 19 additions & 0 deletions development/webpack/utils/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
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: {
Expand Down Expand Up @@ -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(', ')}
Expand Down
10 changes: 10 additions & 0 deletions development/webpack/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: UI_DIR_RE path construction misses app directory

The UI_DIR_RE regex resolves to /path/to/metamask-extension/ui/..., but webpack serves files from /path/to/metamask-extension/app/ui/.... The path construction in join(__dirname, '../../../') should be join(__dirname, '../../../../app/') to correctly match the webpack context where files are actually located.

Fix in Cursor Fix in Web

Copy link
Contributor Author

@MajorLift MajorLift Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not true in our project. app/ui/ does not exist.


/**
* No Operation. A function that does nothing and returns nothing.
*
Expand Down
144 changes: 144 additions & 0 deletions development/webpack/utils/loaders/reactCompilerLoader.ts
Original file line number Diff line number Diff line change
@@ -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 = [];
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Logger statistics accumulate indefinitely in watch mode

The ReactCompilerLogger is a module-level singleton that accumulates statistics across builds without any reset mechanism. In watch mode, the counts (compiledCount, errorCount, etc.) and file arrays (compiledFiles, errorFiles, etc.) continue growing with each rebuild. The logSummary called by ReactCompilerPlugin after each emit displays cumulative totals rather than per-build statistics, making the verbose output misleading. The file arrays can also contain duplicates when the same file is recompiled, and grow unboundedly during long watch sessions.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor Author

@MajorLift MajorLift Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed: b560c3e


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' ? debug : `${debug}_errors`,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Invalid panicThreshold value when debug is none

When debug is 'none', the code sets panicThreshold: 'none', but 'none' is not a valid panicThreshold value for React Compiler. According to React Compiler documentation, valid values are 'all_errors', 'critical_errors', or undefined (to disable panicking). The intended behavior of "prevent build from failing" requires setting panicThreshold to undefined, not the string 'none'. This may cause undefined behavior or runtime errors when React Compiler processes the unrecognized value.

Fix in Cursor Fix in Web

Copy link
Contributor Author

@MajorLift MajorLift Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: 0b3aa55

  • 'none' is accepted and triggers the correct behavior. It's also present in the type definition. But switching to undefined just to be safe, since missing properties fallback to default values.
  • undefined (i.e. optional) being the default value for panicThreshold is why I initially had 'all', 'critical', and undefined as the options for this flag, but 'none' as a default value is more explicit and consistent.

} as const satisfies ReactCompilerLoaderOption;

return {
loader: reactCompilerLoader,
options: defineReactCompilerLoaderOption(reactCompilerOptions),
};
};
13 changes: 13 additions & 0 deletions development/webpack/utils/plugins/ReactCompilerPlugin/index.ts
Original file line number Diff line number Diff line change
@@ -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();
});
}
}
19 changes: 19 additions & 0 deletions development/webpack/webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -211,13 +213,25 @@ 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 };
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,
Expand Down Expand Up @@ -320,6 +334,11 @@ const config = {
dependency: 'url',
type: 'asset/resource',
},
{
test: /(?:.(?!\.(?:test|stories|container)))+\.(?:m?[jt]s|[jt]sx)$/u,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Regex fails to exclude test/stories/container files

The regex /(?:.(?!\.(?:test|stories|container)))+\.(?:m?[jt]s|[jt]sx)$/u intended to exclude test, stories, and container files actually matches them. Since the regex lacks a ^ anchor, it can start matching from any position. For a file like Button.test.tsx, the regex matches starting at the . before test because at that position, what follows (test.tsx) doesn't start with \.test (it starts with t). This allows .test to match the first part and .tsx to match the second, resulting in the full match .test.tsx. Test and story files will be processed by the React Compiler loader when they should be excluded.

Fix in Cursor Fix in Web

Copy link
Contributor Author

@MajorLift MajorLift Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

include: UI_DIR_RE,
use: [reactCompilerLoader],
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: React Compiler loader missing required loaders

The React Compiler loader rule only applies reactCompilerLoader without the preceding tsxLoader and codeFenceLoader that are necessary to process TypeScript and JSX syntax. According to the PR discussion, reactCompilerLoader must be applied after codeFenceLoader and swcLoader to avoid errors about missing directives and source maps. The current configuration will fail to properly transform files in the UI directory.

Fix in Cursor Fix in Web

Copy link
Contributor Author

@MajorLift MajorLift Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reactCompilerLoader is applied last.

  • The code-fence directives and input-source-map errors appear if reactCompilerLoader is placed between swcLoader and codeFenceLoader in the use chains, or if the react compiler rule is placed below the own-javascript/typescript rules.
  • The UI files in the build are being correctly transformed.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: React Compiler loader not chained after SWC transforms

The React Compiler loader rule applies reactCompilerLoader in isolation without the preceding tsxLoader (SWC) and codeFenceLoader transformations. According to the PR discussion, reactCompilerLoader must be applied last, after codeFenceLoader and swcLoader. The current configuration applies it independently, which will cause the React Compiler to receive untransformed TypeScript/JSX code, resulting in transformation errors about missing directives and source maps.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed here: #38007 (comment)

// own typescript, and own typescript with jsx
{
test: /\.(?:ts|mts|tsx)$/u,
Expand Down
10 changes: 5 additions & 5 deletions lavamoat/build-system/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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
}
Expand Down
8 changes: 8 additions & 0 deletions lavamoat/webpack/mv2/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -4976,6 +4976,14 @@
"react": true
}
},
"react-compiler-runtime": {
"globals": {
"console.error": true
},
"packages": {
"react": true
}
},
"react-devtools-core": {
"globals": {
"CSSStyleRule": true,
Expand Down
8 changes: 8 additions & 0 deletions lavamoat/webpack/mv3/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -3356,6 +3356,14 @@
"react": true
}
},
"react-compiler-runtime": {
"globals": {
"console.error": true
},
"packages": {
"react": true
}
},
"react-devtools-core": {
"globals": {
"CSSStyleRule": true,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,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",
Expand Down
Loading
Loading