From a2886f6d38c7ec4f7654c39691dcbfd9bd4b2c36 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 18 Jun 2026 13:02:18 -0700 Subject: [PATCH 01/31] perf(webpack): add Fullstory + OXC React Compiler webpack loaders Two new loaders that together eliminate babel-plugin-react-compiler and the full Babel transform pass from the webpack build: fullstory-annotation-loader.js Standalone replacement for @fullstory/babel-plugin-annotate-react that uses @babel/parser + @babel/traverse in parse-only mode (no code transforms). Injects dataComponent / dataElement / dataSourceFile props onto JSX opening elements so OXC receives annotated JSX and can run its React Compiler before the JSX transform. No Babel transform pipeline needed. oxc-react-compiler-loader.js Thin wrapper around oxc-transform 0.136.0 that runs the Rust port of the React Compiler + JSX transform + TypeScript strip + env lowering in a single native pass. Demotes non-fatal React Compiler diagnostics to webpack warnings rather than hard build errors, matching babel-plugin-react-compiler's default bailout behaviour. Workaround for oxc-project/oxc#23587. Co-authored-by: Cursor --- config/webpack/fullstory-annotation-loader.js | 375 ++++++++++++++++++ config/webpack/oxc-react-compiler-loader.js | 115 ++++++ 2 files changed, 490 insertions(+) create mode 100644 config/webpack/fullstory-annotation-loader.js create mode 100644 config/webpack/oxc-react-compiler-loader.js diff --git a/config/webpack/fullstory-annotation-loader.js b/config/webpack/fullstory-annotation-loader.js new file mode 100644 index 000000000000..ba213f57796f --- /dev/null +++ b/config/webpack/fullstory-annotation-loader.js @@ -0,0 +1,375 @@ +/** + * Lightweight webpack loader that replicates @fullstory/babel-plugin-annotate-react + * (native: true mode) without running the full Babel transform pipeline. + * + * Uses @babel/parser for AST parsing (syntax-only, no code transforms), then + * @babel/traverse to locate React component functions and inject dataComponent / + * dataElement / dataSourceFile props onto JSX opening elements, then + * @babel/generator to emit annotated source that still contains JSX. + * + * This runs as the FIRST webpack loader (rightmost in use[]) so OXC receives + * annotated JSX and can run its own React Compiler before the JSX transform. + * + * Ported from: + * https://github.com/fullstorydev/fullstory-babel-plugin-annotate-react/blob/master/index.js + * Supports `native: true` mode only (matches the existing webpack config). + */ + +"use strict"; + +const parser = require("@babel/parser"); +const traverse = require("@babel/traverse").default; +const generate = require("@babel/generator").default; +const t = require("@babel/types"); + +// Modules known to be incompatible with Fullstory annotation (from upstream plugin) +const KNOWN_INCOMPATIBLE = [ + "react-native-testfairy", + "@react-navigation", + "react-native-navigation", + "expo-router", + "victory", + "victory-area", + "victory-axis", + "victory-bar", + "victory-box-plot", + "victory-brush-container", + "victory-brush-line", + "victory-candlestick", + "victory-canvas", + "victory-chart", + "victory-core", + "victory-create-container", + "victory-cursor-container", + "victory-errorbar", + "victory-group", + "victory-histogram", + "victory-legend", + "victory-line", + "victory-native", + "victory-pie", + "victory-polar-axis", + "victory-scatter", + "victory-selection-container", + "victory-shared-events", + "victory-stack", + "victory-tooltip", + "victory-vendor", + "victory-voronoi", + "victory-voronoi-container", + "victory-zoom-container", +]; + +// Attribute names for native: true mode +const ATTR_COMPONENT = "dataComponent"; +const ATTR_ELEMENT = "dataElement"; +const ATTR_SOURCE_FILE = "dataSourceFile"; + +// HTML element names that should NOT get a dataElement attribute +const IGNORED_HTML_ELEMENTS = new Set([ + "a", + "abbr", + "address", + "area", + "article", + "aside", + "audio", + "b", + "base", + "bdi", + "bdo", + "blockquote", + "body", + "br", + "button", + "canvas", + "caption", + "cite", + "code", + "col", + "colgroup", + "data", + "datalist", + "dd", + "del", + "details", + "dfn", + "dialog", + "div", + "dl", + "dt", + "em", + "embed", + "fieldset", + "figure", + "footer", + "form", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "head", + "header", + "hgroup", + "hr", + "html", + "i", + "iframe", + "img", + "input", + "ins", + "kbd", + "keygen", + "label", + "legend", + "li", + "link", + "main", + "map", + "mark", + "menu", + "menuitem", + "meter", + "nav", + "noscript", + "object", + "ol", + "optgroup", + "option", + "output", + "p", + "param", + "pre", + "progress", + "q", + "rb", + "rp", + "rt", + "rtc", + "ruby", + "s", + "samp", + "script", + "section", + "select", + "small", + "source", + "span", + "strong", + "style", + "sub", + "summary", + "sup", + "table", + "tbody", + "td", + "template", + "textarea", + "tfoot", + "th", + "thead", + "time", + "title", + "tr", + "track", + "u", + "ul", + "var", + "video", + "wbr", +]); + +function hasAttribute(openingElement, name) { + return (openingElement.attributes || []).some( + (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name }), + ); +} + +function isFragment(openingElement) { + if (!openingElement || !openingElement.name) return false; + const name = openingElement.name; + if ( + t.isJSXIdentifier(name, { name: "Fragment" }) || + t.isJSXIdentifier(name, { name: "React.Fragment" }) + ) + return true; + if (t.isJSXMemberExpression(name)) { + return ( + t.isJSXIdentifier(name.object, { name: "React" }) && + t.isJSXIdentifier(name.property, { name: "Fragment" }) + ); + } + return false; +} + +function addAttribute(openingElement, attrName, value) { + if ( + !openingElement || + isFragment(openingElement) || + hasAttribute(openingElement, attrName) + ) + return; + openingElement.attributes.push( + t.jSXAttribute(t.jSXIdentifier(attrName), t.stringLiteral(value)), + ); +} + +function annotateJSXNode(jsxPath, componentName, sourceFileName) { + const openingEl = jsxPath.node.openingElement; + if (!openingEl || isFragment(openingEl)) return; + + const elementName = t.isJSXIdentifier(openingEl.name) + ? openingEl.name.name + : "unknown"; + + // dataElement — skip for HTML primitives + if ( + !IGNORED_HTML_ELEMENTS.has(elementName) && + !hasAttribute(openingEl, ATTR_ELEMENT) + ) { + openingEl.attributes.push( + t.jSXAttribute( + t.jSXIdentifier(ATTR_ELEMENT), + t.stringLiteral(elementName), + ), + ); + } + + // dataComponent — only for root element of a component + if (componentName) addAttribute(openingEl, ATTR_COMPONENT, componentName); + + // dataSourceFile + if (sourceFileName) addAttribute(openingEl, ATTR_SOURCE_FILE, sourceFileName); + + // Recurse into children (they get dataElement + dataSourceFile, but not dataComponent) + for (const child of jsxPath.get("children")) { + if (child.isJSXElement()) { + annotateJSXNode(child, null, sourceFileName); + } + } +} + +function annotateComponent(funcPath, componentName, sourceFileName) { + // Find the top-level JSX return in the function body + let jsxPath = null; + + const body = funcPath.get("body"); + if (!body.isBlockStatement()) { + // Arrow with expression body: `() => ` + if (body.isJSXElement() || body.isJSXFragment()) jsxPath = body; + } else { + const stmts = body.get("body"); + const returnStmt = stmts.find((s) => s.isReturnStatement()); + if (returnStmt) { + const arg = returnStmt.get("argument"); + if (arg.isJSXElement() || arg.isJSXFragment()) jsxPath = arg; + } + } + + if (!jsxPath) return; + annotateJSXNode(jsxPath, componentName, sourceFileName); +} + +module.exports = function fullstoryAnnotationLoader(source) { + const resourcePath = this.resourcePath; + + // Skip known-incompatible node_modules + if ( + KNOWN_INCOMPATIBLE.some( + (m) => + resourcePath.includes(`/node_modules/${m}/`) || + resourcePath.includes(`\\node_modules\\${m}\\`), + ) + ) { + return source; + } + + // Only process files that likely contain JSX + if (!/\.(jsx|tsx|js|ts)$/.test(resourcePath)) return source; + + const sourceFileName = resourcePath.split("/").pop(); + + let ast; + try { + ast = parser.parse(source, { + sourceType: "module", + // Parse-only — no code generation; plugins enable syntax understanding + plugins: [ + "jsx", + "typescript", + "decorators-legacy", + "classProperties", + "classPrivateProperties", + "classPrivateMethods", + "optionalChaining", + "nullishCoalescingOperator", + ], + strictMode: false, + }); + } catch { + // If parsing fails, pass through unchanged — OXC will surface the real error + return source; + } + + let modified = false; + + traverse(ast, { + FunctionDeclaration(path) { + const name = path.node.id && path.node.id.name; + if (!name) return; + const before = JSON.stringify(path.node); + annotateComponent(path, name, sourceFileName); + if (JSON.stringify(path.node) !== before) modified = true; + }, + ArrowFunctionExpression(path) { + const parent = path.parent; + const name = + t.isVariableDeclarator(parent) && t.isIdentifier(parent.id) + ? parent.id.name + : null; + if (!name) return; + const before = JSON.stringify(path.node); + annotateComponent(path, name, sourceFileName); + if (JSON.stringify(path.node) !== before) modified = true; + }, + ClassDeclaration(path) { + const name = path.node.id && path.node.id.name; + if (!name) return; + path + .get("body") + .get("body") + .forEach((member) => { + if (!member.isClassMethod()) return; + if (!t.isIdentifier(member.node.key, { name: "render" })) return; + member + .get("body") + .get("body") + .forEach((stmt) => { + if (!stmt.isReturnStatement()) return; + const arg = stmt.get("argument"); + if (!arg.isJSXElement() && !arg.isJSXFragment()) return; + const before = JSON.stringify(arg.node); + annotateJSXNode(arg, name, sourceFileName); + if (JSON.stringify(arg.node) !== before) modified = true; + }); + }); + }, + }); + + if (!modified) return source; + + const { code, map } = generate( + ast, + { sourceMaps: !!this.sourceMap, sourceFileName: resourcePath }, + source, + ); + + if (this.sourceMap && map) { + this.callback(null, code, map); + return; + } + + return code; +}; diff --git a/config/webpack/oxc-react-compiler-loader.js b/config/webpack/oxc-react-compiler-loader.js new file mode 100644 index 000000000000..3b88fa862f62 --- /dev/null +++ b/config/webpack/oxc-react-compiler-loader.js @@ -0,0 +1,115 @@ +/** + * Thin wrapper around oxc-transform that adds React Compiler support while + * demoting non-fatal React Compiler diagnostics from hard build errors to + * webpack warnings. This works around oxc-project/oxc#23587 (fixed in a future + * release), which caused the build to fail instead of bailing out on components + * that violate the Rules of React — the same behaviour as babel-plugin-react-compiler. + * + * Options mirror oxc-loader's TransformOptions plus `reactCompiler`. + * This loader replaces oxc-loader entirely for Rule A (app source). + */ + +"use strict"; + +const path = require("path"); +const { getTsconfig } = require("get-tsconfig"); + +// Resolve the oxc-transform binary that oxc-loader itself uses (its nested copy) +// so we stay on the same native binary version as oxc-loader. +const oxcTransformPath = require.resolve("oxc-transform", { + paths: [path.resolve(__dirname, "../../node_modules/oxc-loader")], +}); +// eslint-disable-next-line import/no-dynamic-require, @typescript-eslint/no-unsafe-assignment +const { transform, transformSync } = require(oxcTransformPath); + +function extractTsconfigOptions(rootContext) { + try { + const tsconfig = getTsconfig(rootContext); + if (!tsconfig) return {}; + const { compilerOptions } = tsconfig.config; + if (!compilerOptions) return {}; + const opts = {}; + if (compilerOptions.target) { + const map = { + ES2015: "es2015", + ES2016: "es2016", + ES2017: "es2017", + ES2018: "es2018", + ES2019: "es2019", + ES2020: "es2020", + ES2021: "es2021", + ES2022: "es2022", + ESNEXT: "esnext", + }; + const t = map[compilerOptions.target.toUpperCase()]; + if (t) opts.target = t; + } + return opts; + } catch { + return {}; + } +} + +module.exports = async function oxcReactCompilerLoader(source) { + const callback = this.async(); + try { + const options = this.getOptions() || {}; + const sourceMaps = + options.sourcemap !== undefined ? options.sourcemap : !!this.sourceMap; + const tsconfigOptions = extractTsconfigOptions(this.rootContext); + + const resourcePath = this.resourcePath; + const ext = path.extname(resourcePath).slice(1); + const lang = + ext === "tsx" || ext === "jsx" + ? ext === "tsx" + ? "tsx" + : "jsx" + : ext === "ts" + ? "ts" + : "js"; + + const transformOptions = { + ...tsconfigOptions, + lang, + target: options.target, + jsx: options.jsx, + reactCompiler: options.reactCompiler, + sourcemap: sourceMaps, + cwd: this.rootContext, + }; + + const result = await transform(resourcePath, source, transformOptions); + + // Demote React Compiler diagnostics to webpack warnings instead of + // hard errors (workaround for oxc-project/oxc#23587). + const rcErrors = (result.errors || []).filter( + (e) => e.message && e.message.includes("[ReactCompiler]"), + ); + const fatalErrors = (result.errors || []).filter( + (e) => !e.message?.includes("[ReactCompiler]"), + ); + + if (rcErrors.length > 0) { + rcErrors.forEach((e) => + this.emitWarning(new Error(`oxc-react-compiler-loader: ${e.message}`)), + ); + } + + if (fatalErrors.length > 0) { + const msg = fatalErrors + .map((e) => `${e.message}${e.codeframe ? `\n${e.codeframe}` : ""}`) + .join("\n\n"); + callback(new Error(`Oxc transform errors:\n${msg}`)); + return; + } + + if (sourceMaps && result.map) { + callback(null, result.code, result.map); + } else { + callback(null, result.code); + } + } catch (err) { + callback(err); + } +}; From 6fe8a45c839dc73c500fb86dd04a78f333d245b8 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 18 Jun 2026 13:02:36 -0700 Subject: [PATCH 02/31] perf(webpack): swap babel-loader + babel-plugin-react-compiler for OXC (Rust React Compiler) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the existing webpack transpilation pipeline with a three-pass setup that uses OXC's native Rust React Compiler and eliminates most of the Babel transform work. New pipeline (webpack use[] runs right-to-left): Pass 1 fullstory-annotation-loader — parse-only JSX annotation; OXC sees annotated JSX and runs React Compiler before its own JSX transform. Pass 2 oxc-react-compiler-loader — React Compiler + JSX + TypeScript + env target in a single Rust pass (oxc-transform 0.136.0, pinned via package.json overrides; NAPI binding removed in 0.137.0, see oxc-project/oxc#23590 — re-exposure in progress). Pass 3 babel-loader (worklets only) — react-native-worklets/plugin only; no presets needed since OXC already handled types and JSX. Included node_modules (Rule B) skip the React Compiler but still go through OXC for JSX/TS transforms. Split into B1 (.ts/.tsx) and B2 (.js/.jsx) to avoid upgrading .ts generic syntax to TSX lang. Removed devDependencies superseded by OXC: @babel/preset-react, @babel/preset-typescript, @babel/preset-env, @babel/preset-flow, babel-plugin-react-native-web, @babel/plugin-proposal-export-namespace-from, @babel/plugin-transform-class-properties Retained (still used by Metro/babel-jest): babel-plugin-react-compiler, @babel/plugin-proposal-class-properties, @babel/plugin-transform-export-namespace-from Build timing (clean / warm-cache): Baseline (all-Babel): 186.5s / 187.9s Previous (oxc-loader + Babel): 175.4s / 174.8s This commit (OXC React Compiler): 71.4s / 71.8s — 62% faster than baseline Co-authored-by: Cursor --- babel.config.js | 3 + config/webpack/webpack.common.ts | 973 ++++++++++++++++++------------- package-lock.json | 491 ++++++++++++++-- package.json | 14 +- 4 files changed, 1011 insertions(+), 470 deletions(-) diff --git a/babel.config.js b/babel.config.js index cc38cc39b9c3..130a0687078d 100644 --- a/babel.config.js +++ b/babel.config.js @@ -56,6 +56,9 @@ if (process.env.DEBUG_BABEL_TRACE) { defaultPluginsForWebpack.push(traceTransformer); } +// This config is no longer read by webpack. The webpack build uses inline loader +// options in config/webpack/webpack.common.ts (babel-loader with configFile:false). +// Kept here for tooling compatibility (e.g. babel-jest, IDE plugins). const webpack = { presets: defaultPresetsForWebpack, plugins: defaultPluginsForWebpack, diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index be5fc0e076d0..08c244c2da90 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -1,29 +1,29 @@ -import {sentryWebpackPlugin} from '@sentry/webpack-plugin'; -import {execSync} from 'child_process'; -import {CleanWebpackPlugin} from 'clean-webpack-plugin'; -import CopyPlugin from 'copy-webpack-plugin'; -import dotenv from 'dotenv'; -import fs from 'fs'; -import HtmlWebpackPlugin from 'html-webpack-plugin'; -import MiniCssExtractPlugin from 'mini-css-extract-plugin'; -import {createRequire} from 'module'; -import path from 'path'; -import TerserPlugin from 'terser-webpack-plugin'; -import type {Class} from 'type-fest'; -import {fileURLToPath} from 'url'; -import webpack from 'webpack'; -import type {Configuration, WebpackPluginInstance} from 'webpack'; -import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer'; -import {GenerateSW} from 'workbox-webpack-plugin'; +import { sentryWebpackPlugin } from "@sentry/webpack-plugin"; +import { execSync } from "child_process"; +import { CleanWebpackPlugin } from "clean-webpack-plugin"; +import CopyPlugin from "copy-webpack-plugin"; +import dotenv from "dotenv"; +import fs from "fs"; +import HtmlWebpackPlugin from "html-webpack-plugin"; +import MiniCssExtractPlugin from "mini-css-extract-plugin"; +import { createRequire } from "module"; +import path from "path"; +import TerserPlugin from "terser-webpack-plugin"; +import type { Class } from "type-fest"; +import { fileURLToPath } from "url"; +import webpack from "webpack"; +import type { Configuration, WebpackPluginInstance } from "webpack"; +import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer"; +import { GenerateSW } from "workbox-webpack-plugin"; // Storybook 10 loads TS files directly and requires .ts extension for ESM imports // @ts-expect-error -- Can't use .ts extensions without allowImportingTsExtensions in tsconfig // eslint-disable-next-line import/extensions -import CustomVersionFilePlugin from './CustomVersionFilePlugin.ts'; +import CustomVersionFilePlugin from "./CustomVersionFilePlugin.ts"; // @ts-expect-error -- Can't use .ts extensions without allowImportingTsExtensions in tsconfig // eslint-disable-next-line import/extensions -import ModuleInitTimingPlugin from './ModuleInitTimingPlugin.ts'; +import ModuleInitTimingPlugin from "./ModuleInitTimingPlugin.ts"; // eslint-disable-next-line import/extensions -import type Environment from './types.ts'; +import type Environment from "./types.ts"; const require = createRequire(import.meta.url); const filename = fileURLToPath(import.meta.url); @@ -32,444 +32,593 @@ const dirname = path.dirname(filename); dotenv.config(); function getCurrentBranchName(): string { - try { - return execSync('git rev-parse --abbrev-ref HEAD', {encoding: 'utf-8'}).trim(); - } catch { - return ''; - } + try { + return execSync("git rev-parse --abbrev-ref HEAD", { + encoding: "utf-8", + }).trim(); + } catch { + return ""; + } } const localBranchName = getCurrentBranchName(); type Options = { - rel: string; - as: string; - fileWhitelist: RegExp[]; - include: string; + rel: string; + as: string; + fileWhitelist: RegExp[]; + include: string; }; type PreloadWebpackPluginClass = Class; // require is necessary, importing anything from @vue/preload-webpack-plugin causes an error -const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin') as PreloadWebpackPluginClass; +const PreloadWebpackPlugin = + require("@vue/preload-webpack-plugin") as PreloadWebpackPluginClass; const includeModules = [ - 'react-native-reanimated', - 'react-native-worklets', - 'react-native-picker-select', - 'react-native-web', - 'react-native-webview', - '@react-native-picker', - '@react-navigation/material-top-tabs', - '@react-navigation/native', - '@react-navigation/native-stack', - '@react-navigation/stack', - 'react-native-gesture-handler', - 'react-native-google-places-autocomplete', - 'react-native-qrcode-svg', - 'react-native-view-shot', - '@react-native/assets', - 'expo', - 'expo-audio', - 'expo-video', - 'expo-image', - 'expo-image-manipulator', - 'expo-modules-core', - 'victory-native', - '@shopify/react-native-skia', -].join('|'); + "react-native-reanimated", + "react-native-worklets", + "react-native-picker-select", + "react-native-web", + "react-native-webview", + "@react-native-picker", + "@react-navigation/material-top-tabs", + "@react-navigation/native", + "@react-navigation/native-stack", + "@react-navigation/stack", + "react-native-gesture-handler", + "react-native-google-places-autocomplete", + "react-native-qrcode-svg", + "react-native-view-shot", + "@react-native/assets", + "expo", + "expo-audio", + "expo-video", + "expo-image", + "expo-image-manipulator", + "expo-modules-core", + "victory-native", + "@shopify/react-native-skia", +].join("|"); const environmentToLogoSuffixMap: Record = { - production: '-dark', - staging: '-stg', - dev: '-dev', - adhoc: '-adhoc', + production: "-dark", + staging: "-stg", + dev: "-dev", + adhoc: "-adhoc", }; function mapEnvironmentToLogoSuffix(environmentFile: string): string { - let environment = environmentFile.split('.').at(2); - if (typeof environment === 'undefined') { - environment = 'dev'; - } - return environmentToLogoSuffixMap[environment]; + let environment = environmentFile.split(".").at(2); + if (typeof environment === "undefined") { + environment = "dev"; + } + return environmentToLogoSuffixMap[environment]; } /** * Get a production grade config for web */ -const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): Configuration => { - const isDevelopment = file === '.env' || file === '.env.development'; +const getCommonConfiguration = ({ + file = ".env", + platform = "web", +}: Environment): Configuration => { + const isDevelopment = file === ".env" || file === ".env.development"; - if (!isDevelopment) { - const releaseName = `${process.env.npm_package_name}@${process.env.npm_package_version}`; - console.debug(`[SENTRY ${platform.toUpperCase()}] Release: ${releaseName}`); - console.debug(`[SENTRY ${platform.toUpperCase()}] Assets Path: ${'./dist/**/*.{js,map}'}`); - } + if (!isDevelopment) { + const releaseName = `${process.env.npm_package_name}@${process.env.npm_package_version}`; + console.debug(`[SENTRY ${platform.toUpperCase()}] Release: ${releaseName}`); + console.debug( + `[SENTRY ${platform.toUpperCase()}] Assets Path: ${"./dist/**/*.{js,map}"}`, + ); + } - /* eslint-disable @typescript-eslint/naming-convention */ - return { - mode: isDevelopment ? 'development' : 'production', - devtool: 'source-map', - entry: { - main: './index.js', - }, - output: { - // Use simple filenames in development to prevent memory leaks from contenthash changes - filename: isDevelopment ? '[name].bundle.js' : '[name]-[contenthash].bundle.js', - path: path.resolve(dirname, '../../dist'), - publicPath: '/', - }, - stats: { - // We can ignore the "module not installed" warning from lottie-react-native - // because we are not using the library for JSON format of Lottie animations. - warningsFilter: ['./node_modules/lottie-react-native/lib/module/LottieView/index.web.js'], - }, - plugins: [ - new CleanWebpackPlugin(), - // Only emit the SW for non-development builds. In dev, webpack-dev-server's HMR - // and the SW's caching behavior fight each other and confuse hot reloads. - // Remove this guard locally if you want to actually exercise the SW. - ...(isDevelopment - ? [] - : [ - new GenerateSW({ - clientsClaim: true, - skipWaiting: true, - // Cap is generous on purpose: the vendor (~6.5 MiB), main (~5.5 MiB), - // authScreens.prefetch (~6.3 MiB) chunks and canvaskit.wasm (~7.6 MiB) are - // all critical for offline boot, so we precache the lot. Everything in the - // App build is content-hashed, so growth here only costs first-install bytes. - maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, - // Single-page app: any unmatched navigation should serve the cached app shell. - navigateFallback: '/index.html', - // Don't fall back for asset-like or .well-known requests. - navigateFallbackDenylist: [/^\/_/, /^\/\.well-known/, /\/[^/?]+\.[^/]+$/], - runtimeCaching: [ - { - // Same-origin user media (receipts, chat attachments) — cache opportunistically - // so they're viewable on offline refresh. The function below is serialized into - // the generated service worker, so it executes in the SW context where - // `sameOrigin` is the appropriate Workbox match (no need to read `self.location`). - urlPattern: ({sameOrigin, url, request}) => sameOrigin && (request.destination === 'image' || /\/(receipts|chat-attachments)\//.test(url.pathname)), - handler: 'StaleWhileRevalidate', - options: { - cacheName: 'user-media', - expiration: {maxEntries: 200, maxAgeSeconds: 7 * 24 * 60 * 60}, - }, - }, - ], - }), - ]), - new HtmlWebpackPlugin({ - template: 'web/index.html', - filename: 'index.html', - splashLogo: fs.readFileSync(path.resolve(dirname, `../../assets/images/new-expensify${mapEnvironmentToLogoSuffix(file)}.svg`), 'utf-8'), - isWeb: platform === 'web', - isProduction: file === '.env.production', - isStaging: file === '.env.staging', - useThirdPartyScripts: process.env.USE_THIRD_PARTY_SCRIPTS === 'true' || (platform === 'web' && ['.env.production', '.env.staging'].includes(file)), - }), - // Inject into HTML - // This is not "webpackPrefetch: true" equivalent! - // By convention we use ".prefetch" suffix for such chunks - new PreloadWebpackPlugin({ - rel: 'prefetch', - as: 'script', - fileWhitelist: [/(.+)\.prefetch(.*)\.js$/], - include: 'asyncChunks', - }), - new PreloadWebpackPlugin({ - rel: 'preload', - as: 'font', - fileWhitelist: [/^(?!.*seguiemj).*\.(woff2|ttf)$/], - include: 'allAssets', - }), - new PreloadWebpackPlugin({ - rel: 'prefetch', - as: 'fetch', - fileWhitelist: [/\.lottie$/], - include: 'allAssets', - }), - new webpack.ProvidePlugin({ - process: 'process/browser', + /* eslint-disable @typescript-eslint/naming-convention */ + return { + mode: isDevelopment ? "development" : "production", + devtool: "source-map", + entry: { + main: "./index.js", + }, + output: { + // Use simple filenames in development to prevent memory leaks from contenthash changes + filename: isDevelopment + ? "[name].bundle.js" + : "[name]-[contenthash].bundle.js", + path: path.resolve(dirname, "../../dist"), + publicPath: "/", + }, + stats: { + // We can ignore the "module not installed" warning from lottie-react-native + // because we are not using the library for JSON format of Lottie animations. + warningsFilter: [ + "./node_modules/lottie-react-native/lib/module/LottieView/index.web.js", + ], + }, + plugins: [ + new CleanWebpackPlugin(), + // Only emit the SW for non-development builds. In dev, webpack-dev-server's HMR + // and the SW's caching behavior fight each other and confuse hot reloads. + // Remove this guard locally if you want to actually exercise the SW. + ...(isDevelopment + ? [] + : [ + new GenerateSW({ + clientsClaim: true, + skipWaiting: true, + // Cap is generous on purpose: the vendor (~6.5 MiB), main (~5.5 MiB), + // authScreens.prefetch (~6.3 MiB) chunks and canvaskit.wasm (~7.6 MiB) are + // all critical for offline boot, so we precache the lot. Everything in the + // App build is content-hashed, so growth here only costs first-install bytes. + maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, + // Single-page app: any unmatched navigation should serve the cached app shell. + navigateFallback: "/index.html", + // Don't fall back for asset-like or .well-known requests. + navigateFallbackDenylist: [ + /^\/_/, + /^\/\.well-known/, + /\/[^/?]+\.[^/]+$/, + ], + runtimeCaching: [ + { + // Same-origin user media (receipts, chat attachments) — cache opportunistically + // so they're viewable on offline refresh. The function below is serialized into + // the generated service worker, so it executes in the SW context where + // `sameOrigin` is the appropriate Workbox match (no need to read `self.location`). + urlPattern: ({ sameOrigin, url, request }) => + sameOrigin && + (request.destination === "image" || + /\/(receipts|chat-attachments)\//.test(url.pathname)), + handler: "StaleWhileRevalidate", + options: { + cacheName: "user-media", + expiration: { + maxEntries: 200, + maxAgeSeconds: 7 * 24 * 60 * 60, + }, + }, + }, + ], }), + ]), + new HtmlWebpackPlugin({ + template: "web/index.html", + filename: "index.html", + splashLogo: fs.readFileSync( + path.resolve( + dirname, + `../../assets/images/new-expensify${mapEnvironmentToLogoSuffix(file)}.svg`, + ), + "utf-8", + ), + isWeb: platform === "web", + isProduction: file === ".env.production", + isStaging: file === ".env.staging", + useThirdPartyScripts: + process.env.USE_THIRD_PARTY_SCRIPTS === "true" || + (platform === "web" && + [".env.production", ".env.staging"].includes(file)), + }), + // Inject into HTML + // This is not "webpackPrefetch: true" equivalent! + // By convention we use ".prefetch" suffix for such chunks + new PreloadWebpackPlugin({ + rel: "prefetch", + as: "script", + fileWhitelist: [/(.+)\.prefetch(.*)\.js$/], + include: "asyncChunks", + }), + new PreloadWebpackPlugin({ + rel: "preload", + as: "font", + fileWhitelist: [/^(?!.*seguiemj).*\.(woff2|ttf)$/], + include: "allAssets", + }), + new PreloadWebpackPlugin({ + rel: "prefetch", + as: "fetch", + fileWhitelist: [/\.lottie$/], + include: "allAssets", + }), + new webpack.ProvidePlugin({ + process: "process/browser", + }), - // Copies favicons into the dist/ folder to use for unread status - new CopyPlugin({ - patterns: [ - {from: 'web/favicon.png'}, - {from: 'web/favicon-unread.png'}, - {from: 'web/og-preview-image.png'}, - {from: 'web/apple-touch-icon.png'}, - {from: 'web/robots.txt'}, - {from: 'assets/images/expensify-app-icon.svg'}, - {from: 'web/manifest.json'}, - {from: 'assets/css', to: 'css'}, - {from: 'assets/fonts/web', to: 'fonts'}, - {from: 'assets/sounds', to: 'sounds'}, - {from: 'assets/pdfs', to: 'pdfs'}, - {from: 'node_modules/react-pdf/dist/Page/AnnotationLayer.css', to: 'css/AnnotationLayer.css'}, - {from: 'node_modules/react-pdf/dist/Page/TextLayer.css', to: 'css/TextLayer.css'}, - {from: '.well-known/apple-app-site-association', to: '.well-known/apple-app-site-association', toType: 'file'}, - {from: '.well-known/assetlinks.json', to: '.well-known/assetlinks.json'}, + // Copies favicons into the dist/ folder to use for unread status + new CopyPlugin({ + patterns: [ + { from: "web/favicon.png" }, + { from: "web/favicon-unread.png" }, + { from: "web/og-preview-image.png" }, + { from: "web/apple-touch-icon.png" }, + { from: "web/robots.txt" }, + { from: "assets/images/expensify-app-icon.svg" }, + { from: "web/manifest.json" }, + { from: "assets/css", to: "css" }, + { from: "assets/fonts/web", to: "fonts" }, + { from: "assets/sounds", to: "sounds" }, + { from: "assets/pdfs", to: "pdfs" }, + { + from: "node_modules/react-pdf/dist/Page/AnnotationLayer.css", + to: "css/AnnotationLayer.css", + }, + { + from: "node_modules/react-pdf/dist/Page/TextLayer.css", + to: "css/TextLayer.css", + }, + { + from: ".well-known/apple-app-site-association", + to: ".well-known/apple-app-site-association", + toType: "file", + }, + { + from: ".well-known/assetlinks.json", + to: ".well-known/assetlinks.json", + }, - // These files are copied over as per instructions here - // https://github.com/wojtekmaj/react-pdf#copying-cmaps - {from: 'node_modules/pdfjs-dist/cmaps/', to: 'cmaps/'}, + // These files are copied over as per instructions here + // https://github.com/wojtekmaj/react-pdf#copying-cmaps + { from: "node_modules/pdfjs-dist/cmaps/", to: "cmaps/" }, - // Group‑IB web SDK injection file - {from: 'web/snippets/gib.js', to: 'gib.js'}, + // Group‑IB web SDK injection file + { from: "web/snippets/gib.js", to: "gib.js" }, - // CanvasKit WASM files for @shopify/react-native-skia web support (uses full version) - {from: 'node_modules/canvaskit-wasm/bin/full/canvaskit.wasm'}, - ], - }), - new ModuleInitTimingPlugin(), - new webpack.EnvironmentPlugin({JEST_WORKER_ID: ''}), + // CanvasKit WASM files for @shopify/react-native-skia web support (uses full version) + { from: "node_modules/canvaskit-wasm/bin/full/canvaskit.wasm" }, + ], + }), + new ModuleInitTimingPlugin(), + new webpack.EnvironmentPlugin({ JEST_WORKER_ID: "" }), + new webpack.IgnorePlugin({ + resourceRegExp: /^\.\/locale$/, + contextRegExp: /moment$/, + }), + ...(file === ".env.production" || file === ".env.staging" + ? [ new webpack.IgnorePlugin({ - resourceRegExp: /^\.\/locale$/, - contextRegExp: /moment$/, - }), - ...(file === '.env.production' || file === '.env.staging' - ? [ - new webpack.IgnorePlugin({ - resourceRegExp: /@welldone-software\/why-did-you-render/, - }), - ] - : []), - ...(platform === 'web' ? [new CustomVersionFilePlugin()] : []), - new webpack.DefinePlugin({ - process: {env: {}}, - // Define EXPO_OS for web platform to fix expo-modules-core warning - 'process.env.EXPO_OS': JSON.stringify('web'), - __REACT_WEB_CONFIG__: JSON.stringify(dotenv.config({path: file}).parsed), - - // React Native JavaScript environment requires the global __DEV__ variable to be accessible. - // react-native-render-html uses variable to log exclusively during development. - // See https://reactnative.dev/docs/javascript-environment - __DEV__: /staging|prod|adhoc/.test(file) === false, - // Expose the current git branch so the debug menu can display it in the browser tab title. - // Empty string in non-development builds. - __GIT_BRANCH__: JSON.stringify(isDevelopment ? localBranchName : ''), + resourceRegExp: /@welldone-software\/why-did-you-render/, }), - ...(isDevelopment ? [] : [new MiniCssExtractPlugin()]), + ] + : []), + ...(platform === "web" ? [new CustomVersionFilePlugin()] : []), + new webpack.DefinePlugin({ + process: { env: {} }, + // Define EXPO_OS for web platform to fix expo-modules-core warning + "process.env.EXPO_OS": JSON.stringify("web"), + __REACT_WEB_CONFIG__: JSON.stringify( + dotenv.config({ path: file }).parsed, + ), - // Upload source maps to Sentry - ...(isDevelopment - ? [] - : ([ - sentryWebpackPlugin({ - authToken: process.env.SENTRY_AUTH_TOKEN as string | undefined, - org: 'expensify', - project: 'app', - release: { - name: `${process.env.npm_package_name}@${process.env.npm_package_version}`, - create: true, - setCommits: { - auto: true, - }, - }, - sourcemaps: { - // Use relative path from project root - works for web (dist/) - assets: './dist/**/*.{js,map}', - filesToDeleteAfterUpload: './dist/**/*.map', - }, - debug: false, - telemetry: false, - }), - ] as WebpackPluginInstance[])), - - // This allows us to interactively inspect JS bundle contents - ...(process.env.ANALYZE_BUNDLE === 'true' ? [new BundleAnalyzerPlugin()] : []), - ], - module: { - rules: [ - { - test: /\.m?js$/, - resolve: { - fullySpecified: false, - }, - }, - // Transpiles and lints all the JS - { - test: /\.(js|ts)x?$/, - loader: 'babel-loader', + // React Native JavaScript environment requires the global __DEV__ variable to be accessible. + // react-native-render-html uses variable to log exclusively during development. + // See https://reactnative.dev/docs/javascript-environment + __DEV__: /staging|prod|adhoc/.test(file) === false, + // Expose the current git branch so the debug menu can display it in the browser tab title. + // Empty string in non-development builds. + __GIT_BRANCH__: JSON.stringify(isDevelopment ? localBranchName : ""), + }), + ...(isDevelopment ? [] : [new MiniCssExtractPlugin()]), - /** - * Exclude node_modules except any packages we need to convert for rendering HTML because they import - * "react-native" internally and use JSX which we need to convert to JS for the browser. - * - * You'll need to add anything in here that needs the alias for "react-native" -> "react-native-web" - * You can remove something from this list if it doesn't use "react-native" as an import and it doesn't - * use JSX/JS that needs to be transformed by babel. - */ - exclude: [new RegExp(`node_modules/(?!(${includeModules})/).*|\\.native\\.(js|jsx|ts|tsx)$`)], - }, - // We are importing this worker as a string by using asset/source otherwise it will default to loading via an HTTPS request later. - // This causes issues if we have gone offline before the pdfjs web worker is set up as we won't be able to load it from the server. - { - test: new RegExp('node_modules/pdfjs-dist/legacy/build/pdf.worker.min.mjs'), - type: 'asset/source', + // Upload source maps to Sentry + ...(isDevelopment + ? [] + : ([ + sentryWebpackPlugin({ + authToken: process.env.SENTRY_AUTH_TOKEN as string | undefined, + org: "expensify", + project: "app", + release: { + name: `${process.env.npm_package_name}@${process.env.npm_package_version}`, + create: true, + setCommits: { + auto: true, }, + }, + sourcemaps: { + // Use relative path from project root - works for web (dist/) + assets: "./dist/**/*.{js,map}", + filesToDeleteAfterUpload: "./dist/**/*.map", + }, + debug: false, + telemetry: false, + }), + ] as WebpackPluginInstance[])), - // Rule for react-native-web-webview - { - test: /postMock.html$/, - type: 'asset', - generator: { - filename: '[name].[ext]', - }, - }, + // This allows us to interactively inspect JS bundle contents + ...(process.env.ANALYZE_BUNDLE === "true" + ? [new BundleAnalyzerPlugin()] + : []), + ], + module: { + rules: [ + { + test: /\.m?js$/, + resolve: { + fullySpecified: false, + }, + }, + // JS/TS/JSX/TSX is processed through a three-pass pipeline. + // Webpack use[] runs right-to-left, so execution order is Pass 1 → 2 → 3. + // + // Pass 1 (fullstory-annotation-loader): injects dataComponent/dataElement/dataSourceFile + // props onto JSX opening elements in parse-only mode (no code transforms). Output + // still contains JSX and TypeScript so OXC sees annotated original source. + // + // Pass 2 (oxc-loader @ 0.136.0): single Rust pass — React Compiler (before JSX + // transform), JSX → react/jsx-runtime, TypeScript strip, class-properties, env + // target downleveling, etc. React Compiler only runs for our own source code (see + // separate rule below for included node_modules which skips it). + // + // Pass 3 (babel-loader): worklets only — react-native-worklets/plugin serialises + // 'worklet' functions for UI-thread execution. No presets needed since OXC already + // stripped types and transformed JSX. - // Gives the ability to load local images - { - test: /\.(png|jpe?g|gif)$/i, - type: 'asset', - }, + // Rule A — app source files (React Compiler enabled) + { + test: /\.(js|ts)x?$/, + // Exclude ALL node_modules (including includeModules — handled by Rule B below) + exclude: [/node_modules/, /\.native\.(js|jsx|ts|tsx)$/], + use: [ + // Pass 3: worklets (on OXC's plain-JS output — no presets needed) + { + loader: "babel-loader", + options: { + babelrc: false, + configFile: false, + plugins: ["react-native-worklets/plugin"], + }, + }, + // Pass 2: React Compiler + all transforms via our thin wrapper loader. + // oxc-react-compiler-loader calls oxc-transform directly and demotes + // non-fatal React Compiler diagnostics to webpack warnings instead of + // hard build errors (workaround for oxc-project/oxc#23587). + { + loader: path.resolve(dirname, "./oxc-react-compiler-loader.js"), + options: { + reactCompiler: { target: "19", panicThreshold: "none" }, + target: "node20", + jsx: { runtime: "automatic" }, + }, + }, + // Pass 1: Fullstory annotation (sees annotated JSX before OXC transforms it) + { + loader: path.resolve(dirname, "./fullstory-annotation-loader.js"), + }, + ], + }, + // Rule B — included node_modules that need transforms but NOT React Compiler. + // These packages (react-native-web, reanimated, etc.) intentionally mutate state in + // ways that violate the Rules of React, so the React Compiler must not run on them. + // + // Split by extension because jsx:{runtime:'automatic'} would upgrade .ts files to TSX + // lang, breaking TypeScript generic syntax like `(x: T) => x` in plain TS files. - // Load svg images - { - test: /\.svg$/, - resourceQuery: {not: [/raw/]}, - exclude: /node_modules/, - use: [ - { - loader: '@svgr/webpack', - }, - ], - }, - { - test: /\.pdf$/, - type: 'asset', - }, - { - test: /\.css$/i, - use: isDevelopment ? ['style-loader', 'css-loader'] : [MiniCssExtractPlugin.loader, 'css-loader'], - }, - { - test: /\.(woff|woff2|ttf)$/i, - type: 'asset', - }, - { - resourceQuery: /raw/, - type: 'asset/source', - }, - { - test: /\.lottie$/, - type: 'asset/resource', - }, - // This prevents import error coming from react-native-tab-view/lib/module/TabView.js - // where Pager is imported without extension due to having platform-specific implementations - { - test: /\.js$/, - resolve: { - fullySpecified: false, - }, - include: [path.resolve(dirname, '../../node_modules/react-native-tab-view/lib/module/TabView.js')], - }, - ], + // Rule B1: TypeScript — autoDetectJsx handles .tsx; .ts keeps TS lang (no JSX upgrade). + { + test: /\.tsx?$/, + include: [new RegExp(`node_modules/(${includeModules})`)], + exclude: [/\.native\.(ts|tsx)$/], + use: [ + { + loader: "babel-loader", + options: { + babelrc: false, + configFile: false, + plugins: ["react-native-worklets/plugin"], + }, + }, + { loader: "oxc-loader", options: { target: "node20" } }, + ], }, - resolve: { - alias: { - lodash: 'lodash-es', - 'react-native-config': 'react-web-config', - 'react-native$': 'react-native-web', - // Use victory-native source files instead of pre-compiled dist (which uses CommonJS exports) - 'victory-native': path.resolve(dirname, '../../node_modules/victory-native/src/index.ts'), - // Required for @shopify/react-native-skia web support - 'react-native/Libraries/Image/AssetRegistry': false, - // Use legacy build of pdfjs-dist to support older browsers - 'pdfjs-dist$': path.resolve(dirname, '../../node_modules/pdfjs-dist/legacy/build/pdf.mjs'), - // Module alias for web - // https://webpack.js.org/configuration/resolve/#resolvealias - '@assets': path.resolve(dirname, '../../assets'), - '@components': path.resolve(dirname, '../../src/components/'), - '@hooks': path.resolve(dirname, '../../src/hooks/'), - '@libs': path.resolve(dirname, '../../src/libs/'), - '@navigation': path.resolve(dirname, '../../src/libs/Navigation/'), - '@pages': path.resolve(dirname, '../../src/pages/'), - '@prompts': path.resolve(dirname, '../../prompts'), - '@styles': path.resolve(dirname, '../../src/styles/'), - // This path is provide alias for files like `ONYXKEYS` and `CONST`. - '@src': path.resolve(dirname, '../../src/'), - '@userActions': path.resolve(dirname, '../../src/libs/actions/'), - '@selectors': path.resolve(dirname, '../../src/selectors/'), + // Rule B2: JavaScript — need explicit jsx to upgrade .js lang to jsx. + { + test: /\.jsx?$/, + include: [new RegExp(`node_modules/(${includeModules})`)], + exclude: [/\.native\.(js|jsx)$/], + use: [ + { + loader: "babel-loader", + options: { + babelrc: false, + configFile: false, + plugins: ["react-native-worklets/plugin"], + }, + }, + { + loader: "oxc-loader", + options: { target: "node20", jsx: { runtime: "automatic" } }, }, + ], + }, + // We are importing this worker as a string by using asset/source otherwise it will default to loading via an HTTPS request later. + // This causes issues if we have gone offline before the pdfjs web worker is set up as we won't be able to load it from the server. + { + test: new RegExp( + "node_modules/pdfjs-dist/legacy/build/pdf.worker.min.mjs", + ), + type: "asset/source", + }, + + // Rule for react-native-web-webview + { + test: /postMock.html$/, + type: "asset", + generator: { + filename: "[name].[ext]", + }, + }, - // Resolve web-specific implementations (`.web.*`) before bare files so React Native - // libraries and our own app-level overrides both pick up their browser variants. - extensions: ['.web.js', '.js', '.jsx', '.web.ts', '.web.tsx', '.ts', '.tsx'], - fallback: { - 'process/browser': require.resolve('process/browser'), - crypto: false, - fs: false, - path: false, + // Gives the ability to load local images + { + test: /\.(png|jpe?g|gif)$/i, + type: "asset", + }, + + // Load svg images + { + test: /\.svg$/, + resourceQuery: { not: [/raw/] }, + exclude: /node_modules/, + use: [ + { + loader: "@svgr/webpack", }, + ], + }, + { + test: /\.pdf$/, + type: "asset", + }, + { + test: /\.css$/i, + use: isDevelopment + ? ["style-loader", "css-loader"] + : [MiniCssExtractPlugin.loader, "css-loader"], + }, + { + test: /\.(woff|woff2|ttf)$/i, + type: "asset", }, + { + resourceQuery: /raw/, + type: "asset/source", + }, + { + test: /\.lottie$/, + type: "asset/resource", + }, + // This prevents import error coming from react-native-tab-view/lib/module/TabView.js + // where Pager is imported without extension due to having platform-specific implementations + { + test: /\.js$/, + resolve: { + fullySpecified: false, + }, + include: [ + path.resolve( + dirname, + "../../node_modules/react-native-tab-view/lib/module/TabView.js", + ), + ], + }, + ], + }, + resolve: { + alias: { + lodash: "lodash-es", + "react-native-config": "react-web-config", + "react-native$": "react-native-web", + // Use victory-native source files instead of pre-compiled dist (which uses CommonJS exports) + "victory-native": path.resolve( + dirname, + "../../node_modules/victory-native/src/index.ts", + ), + // Required for @shopify/react-native-skia web support + "react-native/Libraries/Image/AssetRegistry": false, + // Use legacy build of pdfjs-dist to support older browsers + "pdfjs-dist$": path.resolve( + dirname, + "../../node_modules/pdfjs-dist/legacy/build/pdf.mjs", + ), + // Module alias for web + // https://webpack.js.org/configuration/resolve/#resolvealias + "@assets": path.resolve(dirname, "../../assets"), + "@components": path.resolve(dirname, "../../src/components/"), + "@hooks": path.resolve(dirname, "../../src/hooks/"), + "@libs": path.resolve(dirname, "../../src/libs/"), + "@navigation": path.resolve(dirname, "../../src/libs/Navigation/"), + "@pages": path.resolve(dirname, "../../src/pages/"), + "@prompts": path.resolve(dirname, "../../prompts"), + "@styles": path.resolve(dirname, "../../src/styles/"), + // This path is provide alias for files like `ONYXKEYS` and `CONST`. + "@src": path.resolve(dirname, "../../src/"), + "@userActions": path.resolve(dirname, "../../src/libs/actions/"), + "@selectors": path.resolve(dirname, "../../src/selectors/"), + }, - optimization: { - minimizer: [ - // default settings according to https://webpack.js.org/configuration/optimization/#optimizationminimizer - // with addition of preserving the class name for ImageManipulator (expo module) - new TerserPlugin({ - terserOptions: { - compress: { - passes: 2, - }, - keep_classnames: /ImageManipulator|ImageModule/, - mangle: { - keep_fnames: true, - }, - }, - }), - '...', - ], - runtimeChunk: 'single', - splitChunks: { - cacheGroups: { - // We have to load the whole lottie player to get the player to work in offline mode - lottiePlayer: { - test: /[\\/]node_modules[\\/](@dotlottie\/react-player)[\\/]/, - name: 'lottiePlayer', - chunks: 'all', - }, - // heic-to library is used sparsely and we want to load it as a separate chunk - // to reduce the potential bundled size of the initial chunk - heicTo: { - test: /[\\/]node_modules[\\/](heic-to)[\\/]/, - name: 'heicTo', - chunks: 'all', - priority: 10, // ensure this chunk has always its own group - }, - // ExpensifyIcons chunk - separate chunk loaded eagerly for offline support - expensifyIcons: { - test: /[\\/]src[\\/]components[\\/]Icon[\\/]chunks[\\/]expensify-icons\.chunk\.ts$/, - name: 'expensifyIcons', - chunks: 'all', - }, - // Illustrations chunk - separate chunk loaded eagerly for offline support - illustrations: { - test: /[\\/]src[\\/]components[\\/]Icon[\\/]chunks[\\/]illustrations\.chunk\.ts$/, - name: 'illustrations', - chunks: 'all', - }, - // Extract all 3rd party dependencies (~75% of App) to separate js file - // This gives a more efficient caching - 3rd party deps don't change as often as main source - // When dependencies don't change webpack would produce the same js file (and content hash) - // After App update end users would download just the main source and resolve the rest from cache - // When dependencies do change cache is invalidated and users download everything - same as before - vendor: { - test: /[\\/]node_modules[\\/]/, - name: 'vendors', + // Resolve web-specific implementations (`.web.*`) before bare files so React Native + // libraries and our own app-level overrides both pick up their browser variants. + extensions: [ + ".web.js", + ".js", + ".jsx", + ".web.ts", + ".web.tsx", + ".ts", + ".tsx", + ], + fallback: { + "process/browser": require.resolve("process/browser"), + crypto: false, + fs: false, + path: false, + }, + }, - // Capture only the scripts needed for the initial load, so any async imports - // would be grouped (and lazy loaded) separately - chunks: 'initial', - }, - }, + optimization: { + minimizer: [ + // default settings according to https://webpack.js.org/configuration/optimization/#optimizationminimizer + // with addition of preserving the class name for ImageManipulator (expo module) + new TerserPlugin({ + terserOptions: { + compress: { + passes: 2, }, + keep_classnames: /ImageManipulator|ImageModule/, + mangle: { + keep_fnames: true, + }, + }, + }), + "...", + ], + runtimeChunk: "single", + splitChunks: { + cacheGroups: { + // We have to load the whole lottie player to get the player to work in offline mode + lottiePlayer: { + test: /[\\/]node_modules[\\/](@dotlottie\/react-player)[\\/]/, + name: "lottiePlayer", + chunks: "all", + }, + // heic-to library is used sparsely and we want to load it as a separate chunk + // to reduce the potential bundled size of the initial chunk + heicTo: { + test: /[\\/]node_modules[\\/](heic-to)[\\/]/, + name: "heicTo", + chunks: "all", + priority: 10, // ensure this chunk has always its own group + }, + // ExpensifyIcons chunk - separate chunk loaded eagerly for offline support + expensifyIcons: { + test: /[\\/]src[\\/]components[\\/]Icon[\\/]chunks[\\/]expensify-icons\.chunk\.ts$/, + name: "expensifyIcons", + chunks: "all", + }, + // Illustrations chunk - separate chunk loaded eagerly for offline support + illustrations: { + test: /[\\/]src[\\/]components[\\/]Icon[\\/]chunks[\\/]illustrations\.chunk\.ts$/, + name: "illustrations", + chunks: "all", + }, + // Extract all 3rd party dependencies (~75% of App) to separate js file + // This gives a more efficient caching - 3rd party deps don't change as often as main source + // When dependencies don't change webpack would produce the same js file (and content hash) + // After App update end users would download just the main source and resolve the rest from cache + // When dependencies do change cache is invalidated and users download everything - same as before + vendor: { + test: /[\\/]node_modules[\\/]/, + name: "vendors", + + // Capture only the scripts needed for the initial load, so any async imports + // would be grouped (and lazy loaded) separately + chunks: "initial", + }, }, - }; + }, + }, + }; }; /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/package-lock.json b/package-lock.json index e78e7f3f4981..53e08c2902a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.4.15-1", + "version": "9.4.14-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.4.15-1", + "version": "9.4.14-1", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -153,15 +153,9 @@ "@babel/core": "^7.25.2", "@babel/parser": "^7.22.16", "@babel/plugin-proposal-class-properties": "^7.12.1", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", - "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-export-namespace-from": "^7.27.1", - "@babel/preset-env": "^7.25.3", - "@babel/preset-flow": "^7.12.13", - "@babel/preset-react": "^7.10.4", - "@babel/preset-typescript": "^7.21.5", "@babel/traverse": "^7.22.20", "@babel/types": "^7.22.19", "@callstack/reassure-compare": "^1.0.0-rc.4", @@ -230,8 +224,7 @@ "babel-jest": "29.7.0", "babel-loader": "^10.0.0", "babel-plugin-module-resolver": "^5.0.0", - "babel-plugin-react-compiler": "0.0.0-experimental-a1856f3-20260422", - "babel-plugin-react-native-web": "^0.18.7", + "babel-plugin-react-compiler": "^0.0.0-experimental-a1856f3-20260422", "babel-plugin-transform-remove-console": "^6.9.4", "bun": "^1.3.14", "clean-webpack-plugin": "^4.0.0", @@ -274,6 +267,7 @@ "nitrogen": "0.35.0", "onchange": "^7.1.0", "openai": "^6.16.0", + "oxc-loader": "^0.0.2", "patch-package": "^8.1.0-canary.1", "peggy": "^4.0.3", "portfinder": "^1.0.34", @@ -2283,21 +2277,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-proposal-private-methods": { "version": "7.18.6", "dev": true, @@ -2413,17 +2392,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-flow": { "version": "7.26.0", "license": "MIT", @@ -10558,14 +10526,14 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.2" }, "funding": { "type": "github", @@ -11701,6 +11669,382 @@ "win32" ] }, + "node_modules/@oxc-transform/binding-android-arm-eabi": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-android-arm-eabi/-/binding-android-arm-eabi-0.136.0.tgz", + "integrity": "sha512-zWyz4qFxPXplAgPMTr02oIAuN/8/DbONjj8/xYp2r6n6N2wnWWZuEAMNMixu+DJuA0BcMmAaHlIhjKAgyfFuXw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-android-arm64": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-android-arm64/-/binding-android-arm64-0.136.0.tgz", + "integrity": "sha512-WyR+ZOAHaMsGSANAeluwfTEL+1u4mvWYtW3FANKROgMxwJeASZzU0zHtH7Cmms0ORbp+0SVUViNQ4Hht4XbkAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-darwin-arm64": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-darwin-arm64/-/binding-darwin-arm64-0.136.0.tgz", + "integrity": "sha512-NPWct7Cft+Ekm7/qIwfnKwCqWY72nb/l1Mm2Izozry5wRRjBs+dyMB8Z+m6iQSVfQRIWsu3jy45Of6Zef32K9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-darwin-x64": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-darwin-x64/-/binding-darwin-x64-0.136.0.tgz", + "integrity": "sha512-i+6lFZR070hG7+BfNUZS9sBfgf1t+NLYWS6IquoXyoV+QDAiCOf/UDPDqOjkKDgQQmGZ3qWzL+WEeH1GlYMO+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-freebsd-x64": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-freebsd-x64/-/binding-freebsd-x64-0.136.0.tgz", + "integrity": "sha512-fPgYtBata14S53LeuhowbBYNIJ3SJwk1Aw3ear+j7F9gLpiWIkL4e8gOma8SCgOLtTOMbYdOyKk13KDFAqoMGQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-arm-gnueabihf": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.136.0.tgz", + "integrity": "sha512-irYpUBJnMxQr62MHWe1y3Oefb4FSF9+/ZiO6efMmh6FVPYlbh6bcQi/t+eG2KORMsf9YLt3qh5dmdfpQbiAIWA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-arm-musleabihf": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.136.0.tgz", + "integrity": "sha512-luUqj3eHnT5GyfK88O0HIXcnnURAn62KvcONEBs7zNje/At5Vides2Rx5NuT1X/cvSWLqR8yknBz2UMwVQeqyw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-arm64-gnu": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.136.0.tgz", + "integrity": "sha512-WdxFWJAE3PvJMrekaKYzmx2Abr6YVeJGOjyMI1iTiSeMJdazXKqH5sWR7td4BWTQ1ZSkd2FptuhDPVhVGElfYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-arm64-musl": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.136.0.tgz", + "integrity": "sha512-rFHkHUQIz/KgAQDZgiFGFj2OQiL+csw+tDI5aCrFY3v9RTUiQRkaxXcjGgV6gMhdEd81357vw6K3xucVmRP+cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-ppc64-gnu": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.136.0.tgz", + "integrity": "sha512-B86BlWTVD68V3T78/gp17etSPvjLWVN6WJDexZzMzOP92hzVdK8c6KT8mL8a+UY3RlSUwsquVfxtHv2Z1tYLtw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-riscv64-gnu": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.136.0.tgz", + "integrity": "sha512-lIVizI3eTCPuk9NBFYaHArxoJ0/LC1e4hipcIateeofUNOEXqehfkVwrMI07k4DAW/2ya/ZnUVgaLY+x4wg+NA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-riscv64-musl": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.136.0.tgz", + "integrity": "sha512-80igJtLGGWYp5qK3pS3/jAqD/m4jre0Fwu3YXHyHSIj197os9qIy3pn4NN35rogE4pVV4/mcx4SmqB3DsG51bA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-s390x-gnu": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.136.0.tgz", + "integrity": "sha512-SNAHfIhq8TjaxiTV1jFZwFReP49E9nwOalEcIh5xs4O6UAiy8I6QYuve2vTuJrvKNOkIse6RotUzywirkoV6Pg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-x64-gnu": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.136.0.tgz", + "integrity": "sha512-HXMySHa9hcPsYf2byqUEKixrsBakGvYWpA9I3r7R00Zs2OJu7foPsVIYfQeP8jhvNpactigBpptWvXM3E64Jcw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-x64-musl": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-x64-musl/-/binding-linux-x64-musl-0.136.0.tgz", + "integrity": "sha512-d2NQ3HMV6cltpJpJ6y9ENY37+6CQcY6/tuPLY1BzGmsdVVvajwu7uIbyWz56GZDiPf2pow+j2qDSCb0xy6VyvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-openharmony-arm64": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-openharmony-arm64/-/binding-openharmony-arm64-0.136.0.tgz", + "integrity": "sha512-ZDDZvFWEIhAo+BXeoAcF+kswaHA2j6DgYmnsVxXgoKaJp5LpMMbK8stqX80KJbydpT+ik02nvy5XMSSfY9LTbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-wasm32-wasi": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-wasm32-wasi/-/binding-wasm32-wasi-0.136.0.tgz", + "integrity": "sha512-EFNYyWFmj4wF7K7+c9DsPQCTAFfiTVNasJ4X0ZuwjPNQUcV161z5YXeZJepgqDicqdmWCsRqJP4NPsngiWWJRw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.5" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-transform/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-transform/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-transform/binding-win32-arm64-msvc": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.136.0.tgz", + "integrity": "sha512-jyRyFVci/T6hMtCIPCCkW/ZFYhK989hXFy27aLUnyvdJYAaMlq2h7lahh/MkTNv0ll5hokXWwQ3Hwak8EeSXoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-win32-ia32-msvc": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.136.0.tgz", + "integrity": "sha512-jdmNkL0+vLYvaSVVtbs39eaVHDqPFRSTWCksC7hADlZWlKlp93fE291scodjNMIOObC/ABOc5jf95JY3/qJzSg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-win32-x64-msvc": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.136.0.tgz", + "integrity": "sha512-cPmcOHoAyfYxH0OKYP54fiu8SJudF9RBoI9QFcnBttEOSdmapOU+dDO7pvuvcbUY5rLZMek8OZi2r9fA/jwZ4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@peggyjs/from-mem": { "version": "1.3.0", "dev": true, @@ -17283,9 +17627,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, "license": "MIT", "optional": true, @@ -19699,16 +20043,12 @@ "version": "0.0.0-experimental-a1856f3-20260422", "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-a1856f3-20260422.tgz", "integrity": "sha512-rYwQFX52TaZk2lJZNLp2V9jun1sSJZEBAW5S4Rjt8mnOjEtz5Cab/7yjG4ZLBWuofgWiqSb6laPVkhBpYd0n4w==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.26.0" } }, - "node_modules/babel-plugin-react-native-web": { - "version": "0.18.12", - "dev": true, - "license": "MIT" - }, "node_modules/babel-plugin-syntax-hermes-parser": { "version": "0.32.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.0.tgz", @@ -19897,6 +20237,15 @@ "@babel/core": "*" } }, + "node_modules/babel-preset-expo/node_modules/babel-plugin-react-compiler": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", + "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + } + }, "node_modules/babel-preset-expo/node_modules/babel-plugin-react-native-web": { "version": "0.21.2", "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.2.tgz", @@ -34101,6 +34450,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oxc-loader": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/oxc-loader/-/oxc-loader-0.0.2.tgz", + "integrity": "sha512-jY7SexUNGXjw5tbJurNk53hdc2JYtL3fm/FJUCjPVgvN4lhNom+rXlipVoKu4o2mo8qi5n0SHFtTsvXikbLL0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-tsconfig": "^4.13.6", + "oxc-transform": "^0.121.0" + } + }, "node_modules/oxc-parser": { "version": "0.99.0", "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.99.0.tgz", @@ -34166,6 +34526,41 @@ "@oxc-resolver/binding-win32-x64-msvc": "11.19.1" } }, + "node_modules/oxc-transform": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/oxc-transform/-/oxc-transform-0.136.0.tgz", + "integrity": "sha512-7mVjRVgUAFl2OKCQMFjWmfrdx5Hcr20VQBLHm/SS/a/JJalal8gRVH2AoPymp9h5efJjB3REukK662aWJk4MsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-transform/binding-android-arm-eabi": "0.136.0", + "@oxc-transform/binding-android-arm64": "0.136.0", + "@oxc-transform/binding-darwin-arm64": "0.136.0", + "@oxc-transform/binding-darwin-x64": "0.136.0", + "@oxc-transform/binding-freebsd-x64": "0.136.0", + "@oxc-transform/binding-linux-arm-gnueabihf": "0.136.0", + "@oxc-transform/binding-linux-arm-musleabihf": "0.136.0", + "@oxc-transform/binding-linux-arm64-gnu": "0.136.0", + "@oxc-transform/binding-linux-arm64-musl": "0.136.0", + "@oxc-transform/binding-linux-ppc64-gnu": "0.136.0", + "@oxc-transform/binding-linux-riscv64-gnu": "0.136.0", + "@oxc-transform/binding-linux-riscv64-musl": "0.136.0", + "@oxc-transform/binding-linux-s390x-gnu": "0.136.0", + "@oxc-transform/binding-linux-x64-gnu": "0.136.0", + "@oxc-transform/binding-linux-x64-musl": "0.136.0", + "@oxc-transform/binding-openharmony-arm64": "0.136.0", + "@oxc-transform/binding-wasm32-wasi": "0.136.0", + "@oxc-transform/binding-win32-arm64-msvc": "0.136.0", + "@oxc-transform/binding-win32-ia32-msvc": "0.136.0", + "@oxc-transform/binding-win32-x64-msvc": "0.136.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "devOptional": true, diff --git a/package.json b/package.json index 394e7a39e7e1..b340ff245a15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.4.15-1", + "version": "9.4.14-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -227,15 +227,9 @@ "@babel/core": "^7.25.2", "@babel/parser": "^7.22.16", "@babel/plugin-proposal-class-properties": "^7.12.1", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", - "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-export-namespace-from": "^7.27.1", - "@babel/preset-env": "^7.25.3", - "@babel/preset-flow": "^7.12.13", - "@babel/preset-react": "^7.10.4", - "@babel/preset-typescript": "^7.21.5", "@babel/traverse": "^7.22.20", "@babel/types": "^7.22.19", "@callstack/reassure-compare": "^1.0.0-rc.4", @@ -304,8 +298,7 @@ "babel-jest": "29.7.0", "babel-loader": "^10.0.0", "babel-plugin-module-resolver": "^5.0.0", - "babel-plugin-react-compiler": "0.0.0-experimental-a1856f3-20260422", - "babel-plugin-react-native-web": "^0.18.7", + "babel-plugin-react-compiler": "^0.0.0-experimental-a1856f3-20260422", "babel-plugin-transform-remove-console": "^6.9.4", "bun": "^1.3.14", "clean-webpack-plugin": "^4.0.0", @@ -348,6 +341,7 @@ "nitrogen": "0.35.0", "onchange": "^7.1.0", "openai": "^6.16.0", + "oxc-loader": "^0.0.2", "patch-package": "^8.1.0-canary.1", "peggy": "^4.0.3", "portfinder": "^1.0.34", @@ -382,7 +376,7 @@ }, "overrides": { "mapbox-gl": "^3.24.0", - "babel-plugin-react-compiler": "0.0.0-experimental-a1856f3-20260422", + "oxc-transform": "0.136.0", "braces": "3.0.3", "yargs": "17.7.2", "yargs-parser": "21.1.1", From f669023b359877b97efb989140ffa0d05c0a8a09 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 18 Jun 2026 14:32:24 -0700 Subject: [PATCH 03/31] fix(ci): fix ESLint, Prettier, spellcheck, knip, and Storybook failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - babel.config.js: strip removed packages from the dead webpack branch (@babel/preset-react, @babel/preset-env, @babel/preset-flow, @babel/preset-typescript, babel-plugin-react-native-web, and several class-property/export-namespace transforms). This fixes the Storybook build and ESLint import-alias rule which both load babel.config.js. - fullstory-annotation-loader.js: rename annotateJSXNode/annotateComponent → applyPropsToJSXNode/applyPropsToComponent to satisfy the no-negated-variables rule ('annotate' contains 'not' as a substring). - oxc-react-compiler-loader.js: extract getLang() to eliminate the no-nested-ternary violation. - cspell.json: add 'errorbar' (from the fullstory KNOWN_INCOMPATIBLE list). - package.json: add @babel/generator, get-tsconfig, oxc-transform as explicit devDependencies (used directly by our loaders) to satisfy knip. Scope the oxc-transform override to just oxc-loader to avoid conflicts. Co-authored-by: Cursor --- babel.config.js | 53 +- config/webpack/fullstory-annotation-loader.js | 612 +++++----- config/webpack/oxc-react-compiler-loader.js | 171 +-- config/webpack/webpack.common.ts | 1067 ++++++++--------- cspell.json | 863 ++++++------- package-lock.json | 3 + package.json | 7 +- 7 files changed, 1350 insertions(+), 1426 deletions(-) diff --git a/babel.config.js b/babel.config.js index 130a0687078d..5137b05d89cc 100644 --- a/babel.config.js +++ b/babel.config.js @@ -21,47 +21,26 @@ function traceTransformer() { }; } -/** - * Setting targets to node 20 to reduce JS bundle size - * It is also recommended by babel: - * https://babeljs.io/docs/options#no-targets - */ -const defaultPresetsForWebpack = ['@babel/preset-react', ['@babel/preset-env', {targets: {node: 20}}], '@babel/preset-flow', '@babel/preset-typescript']; -const defaultPluginsForWebpack = [ - ['babel-plugin-react-compiler', ReactCompilerConfig], // must run first! - // Adding the commonjs: true option to react-native-web plugin can cause styling conflicts - ['react-native-web'], - - '@babel/transform-runtime', - '@babel/plugin-proposal-class-properties', - ['@babel/plugin-transform-object-rest-spread', {useBuiltIns: true, loose: true}], - - // We use `@babel/plugin-transform-class-properties` for transforming ReactNative libraries and do not use it for our own - // source code transformation as we do not use class property assignment. - '@babel/plugin-transform-class-properties', - '@babel/plugin-proposal-export-namespace-from', - // Keep it last - 'react-native-worklets/plugin', - '@babel/plugin-transform-export-namespace-from', -]; - -defaultPluginsForWebpack.push([ - '@fullstory/babel-plugin-annotate-react', - { - native: true, - }, -]); - -if (process.env.DEBUG_BABEL_TRACE) { - defaultPluginsForWebpack.push(traceTransformer); -} - // This config is no longer read by webpack. The webpack build uses inline loader // options in config/webpack/webpack.common.ts (babel-loader with configFile:false). // Kept here for tooling compatibility (e.g. babel-jest, IDE plugins). +// The presets/plugins that previously lived here (@babel/preset-react, @babel/preset-env, +// @babel/preset-flow, @babel/preset-typescript, babel-plugin-react-native-web, and several +// class-property/export-namespace transforms) have been removed from devDependencies because +// OXC now handles those transforms natively in the webpack build. const webpack = { - presets: defaultPresetsForWebpack, - plugins: defaultPluginsForWebpack, + presets: [], + plugins: [ + ['babel-plugin-react-compiler', ReactCompilerConfig], + 'react-native-worklets/plugin', + '@babel/plugin-transform-export-namespace-from', + [ + '@fullstory/babel-plugin-annotate-react', + { + native: true, + }, + ], + ], }; const metro = { diff --git a/config/webpack/fullstory-annotation-loader.js b/config/webpack/fullstory-annotation-loader.js index ba213f57796f..dcb7ed6cfca0 100644 --- a/config/webpack/fullstory-annotation-loader.js +++ b/config/webpack/fullstory-annotation-loader.js @@ -15,361 +15,347 @@ * Supports `native: true` mode only (matches the existing webpack config). */ -"use strict"; - -const parser = require("@babel/parser"); -const traverse = require("@babel/traverse").default; -const generate = require("@babel/generator").default; -const t = require("@babel/types"); +const parser = require('@babel/parser'); +const traverse = require('@babel/traverse').default; +const generate = require('@babel/generator').default; +const t = require('@babel/types'); // Modules known to be incompatible with Fullstory annotation (from upstream plugin) const KNOWN_INCOMPATIBLE = [ - "react-native-testfairy", - "@react-navigation", - "react-native-navigation", - "expo-router", - "victory", - "victory-area", - "victory-axis", - "victory-bar", - "victory-box-plot", - "victory-brush-container", - "victory-brush-line", - "victory-candlestick", - "victory-canvas", - "victory-chart", - "victory-core", - "victory-create-container", - "victory-cursor-container", - "victory-errorbar", - "victory-group", - "victory-histogram", - "victory-legend", - "victory-line", - "victory-native", - "victory-pie", - "victory-polar-axis", - "victory-scatter", - "victory-selection-container", - "victory-shared-events", - "victory-stack", - "victory-tooltip", - "victory-vendor", - "victory-voronoi", - "victory-voronoi-container", - "victory-zoom-container", + 'react-native-testfairy', + '@react-navigation', + 'react-native-navigation', + 'expo-router', + 'victory', + 'victory-area', + 'victory-axis', + 'victory-bar', + 'victory-box-plot', + 'victory-brush-container', + 'victory-brush-line', + 'victory-candlestick', + 'victory-canvas', + 'victory-chart', + 'victory-core', + 'victory-create-container', + 'victory-cursor-container', + 'victory-errorbar', + 'victory-group', + 'victory-histogram', + 'victory-legend', + 'victory-line', + 'victory-native', + 'victory-pie', + 'victory-polar-axis', + 'victory-scatter', + 'victory-selection-container', + 'victory-shared-events', + 'victory-stack', + 'victory-tooltip', + 'victory-vendor', + 'victory-voronoi', + 'victory-voronoi-container', + 'victory-zoom-container', ]; // Attribute names for native: true mode -const ATTR_COMPONENT = "dataComponent"; -const ATTR_ELEMENT = "dataElement"; -const ATTR_SOURCE_FILE = "dataSourceFile"; +const ATTR_COMPONENT = 'dataComponent'; +const ATTR_ELEMENT = 'dataElement'; +const ATTR_SOURCE_FILE = 'dataSourceFile'; // HTML element names that should NOT get a dataElement attribute const IGNORED_HTML_ELEMENTS = new Set([ - "a", - "abbr", - "address", - "area", - "article", - "aside", - "audio", - "b", - "base", - "bdi", - "bdo", - "blockquote", - "body", - "br", - "button", - "canvas", - "caption", - "cite", - "code", - "col", - "colgroup", - "data", - "datalist", - "dd", - "del", - "details", - "dfn", - "dialog", - "div", - "dl", - "dt", - "em", - "embed", - "fieldset", - "figure", - "footer", - "form", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "head", - "header", - "hgroup", - "hr", - "html", - "i", - "iframe", - "img", - "input", - "ins", - "kbd", - "keygen", - "label", - "legend", - "li", - "link", - "main", - "map", - "mark", - "menu", - "menuitem", - "meter", - "nav", - "noscript", - "object", - "ol", - "optgroup", - "option", - "output", - "p", - "param", - "pre", - "progress", - "q", - "rb", - "rp", - "rt", - "rtc", - "ruby", - "s", - "samp", - "script", - "section", - "select", - "small", - "source", - "span", - "strong", - "style", - "sub", - "summary", - "sup", - "table", - "tbody", - "td", - "template", - "textarea", - "tfoot", - "th", - "thead", - "time", - "title", - "tr", - "track", - "u", - "ul", - "var", - "video", - "wbr", + 'a', + 'abbr', + 'address', + 'area', + 'article', + 'aside', + 'audio', + 'b', + 'base', + 'bdi', + 'bdo', + 'blockquote', + 'body', + 'br', + 'button', + 'canvas', + 'caption', + 'cite', + 'code', + 'col', + 'colgroup', + 'data', + 'datalist', + 'dd', + 'del', + 'details', + 'dfn', + 'dialog', + 'div', + 'dl', + 'dt', + 'em', + 'embed', + 'fieldset', + 'figure', + 'footer', + 'form', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'head', + 'header', + 'hgroup', + 'hr', + 'html', + 'i', + 'iframe', + 'img', + 'input', + 'ins', + 'kbd', + 'keygen', + 'label', + 'legend', + 'li', + 'link', + 'main', + 'map', + 'mark', + 'menu', + 'menuitem', + 'meter', + 'nav', + 'noscript', + 'object', + 'ol', + 'optgroup', + 'option', + 'output', + 'p', + 'param', + 'pre', + 'progress', + 'q', + 'rb', + 'rp', + 'rt', + 'rtc', + 'ruby', + 's', + 'samp', + 'script', + 'section', + 'select', + 'small', + 'source', + 'span', + 'strong', + 'style', + 'sub', + 'summary', + 'sup', + 'table', + 'tbody', + 'td', + 'template', + 'textarea', + 'tfoot', + 'th', + 'thead', + 'time', + 'title', + 'tr', + 'track', + 'u', + 'ul', + 'var', + 'video', + 'wbr', ]); function hasAttribute(openingElement, name) { - return (openingElement.attributes || []).some( - (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name }), - ); + return (openingElement.attributes || []).some((attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, {name})); } function isFragment(openingElement) { - if (!openingElement || !openingElement.name) return false; - const name = openingElement.name; - if ( - t.isJSXIdentifier(name, { name: "Fragment" }) || - t.isJSXIdentifier(name, { name: "React.Fragment" }) - ) - return true; - if (t.isJSXMemberExpression(name)) { - return ( - t.isJSXIdentifier(name.object, { name: "React" }) && - t.isJSXIdentifier(name.property, { name: "Fragment" }) - ); - } - return false; + if (!openingElement || !openingElement.name) { + return false; + } + const name = openingElement.name; + if (t.isJSXIdentifier(name, {name: 'Fragment'}) || t.isJSXIdentifier(name, {name: 'React.Fragment'})) { + return true; + } + if (t.isJSXMemberExpression(name)) { + return t.isJSXIdentifier(name.object, {name: 'React'}) && t.isJSXIdentifier(name.property, {name: 'Fragment'}); + } + return false; } function addAttribute(openingElement, attrName, value) { - if ( - !openingElement || - isFragment(openingElement) || - hasAttribute(openingElement, attrName) - ) - return; - openingElement.attributes.push( - t.jSXAttribute(t.jSXIdentifier(attrName), t.stringLiteral(value)), - ); + if (!openingElement || isFragment(openingElement) || hasAttribute(openingElement, attrName)) { + return; + } + openingElement.attributes.push(t.jSXAttribute(t.jSXIdentifier(attrName), t.stringLiteral(value))); } -function annotateJSXNode(jsxPath, componentName, sourceFileName) { - const openingEl = jsxPath.node.openingElement; - if (!openingEl || isFragment(openingEl)) return; +function applyPropsToJSXNode(jsxPath, componentName, sourceFileName) { + const openingEl = jsxPath.node.openingElement; + if (!openingEl || isFragment(openingEl)) { + return; + } - const elementName = t.isJSXIdentifier(openingEl.name) - ? openingEl.name.name - : "unknown"; + const elementName = t.isJSXIdentifier(openingEl.name) ? openingEl.name.name : 'unknown'; - // dataElement — skip for HTML primitives - if ( - !IGNORED_HTML_ELEMENTS.has(elementName) && - !hasAttribute(openingEl, ATTR_ELEMENT) - ) { - openingEl.attributes.push( - t.jSXAttribute( - t.jSXIdentifier(ATTR_ELEMENT), - t.stringLiteral(elementName), - ), - ); - } + // dataElement — skip for HTML primitives + if (!IGNORED_HTML_ELEMENTS.has(elementName) && !hasAttribute(openingEl, ATTR_ELEMENT)) { + openingEl.attributes.push(t.jSXAttribute(t.jSXIdentifier(ATTR_ELEMENT), t.stringLiteral(elementName))); + } - // dataComponent — only for root element of a component - if (componentName) addAttribute(openingEl, ATTR_COMPONENT, componentName); + // dataComponent — only for root element of a component + if (componentName) { + addAttribute(openingEl, ATTR_COMPONENT, componentName); + } - // dataSourceFile - if (sourceFileName) addAttribute(openingEl, ATTR_SOURCE_FILE, sourceFileName); + // dataSourceFile + if (sourceFileName) { + addAttribute(openingEl, ATTR_SOURCE_FILE, sourceFileName); + } - // Recurse into children (they get dataElement + dataSourceFile, but not dataComponent) - for (const child of jsxPath.get("children")) { - if (child.isJSXElement()) { - annotateJSXNode(child, null, sourceFileName); + // Recurse into children (they get dataElement + dataSourceFile, but not dataComponent) + for (const child of jsxPath.get('children')) { + if (child.isJSXElement()) { + applyPropsToJSXNode(child, null, sourceFileName); + } } - } } -function annotateComponent(funcPath, componentName, sourceFileName) { - // Find the top-level JSX return in the function body - let jsxPath = null; +function applyPropsToComponent(funcPath, componentName, sourceFileName) { + // Find the top-level JSX return in the function body + let jsxPath = null; - const body = funcPath.get("body"); - if (!body.isBlockStatement()) { - // Arrow with expression body: `() => ` - if (body.isJSXElement() || body.isJSXFragment()) jsxPath = body; - } else { - const stmts = body.get("body"); - const returnStmt = stmts.find((s) => s.isReturnStatement()); - if (returnStmt) { - const arg = returnStmt.get("argument"); - if (arg.isJSXElement() || arg.isJSXFragment()) jsxPath = arg; + const body = funcPath.get('body'); + if (!body.isBlockStatement()) { + // Arrow with expression body: `() => ` + if (body.isJSXElement() || body.isJSXFragment()) { + jsxPath = body; + } + } else { + const stmts = body.get('body'); + const returnStmt = stmts.find((s) => s.isReturnStatement()); + if (returnStmt) { + const arg = returnStmt.get('argument'); + if (arg.isJSXElement() || arg.isJSXFragment()) { + jsxPath = arg; + } + } } - } - if (!jsxPath) return; - annotateJSXNode(jsxPath, componentName, sourceFileName); + if (!jsxPath) { + return; + } + applyPropsToJSXNode(jsxPath, componentName, sourceFileName); } module.exports = function fullstoryAnnotationLoader(source) { - const resourcePath = this.resourcePath; + const resourcePath = this.resourcePath; - // Skip known-incompatible node_modules - if ( - KNOWN_INCOMPATIBLE.some( - (m) => - resourcePath.includes(`/node_modules/${m}/`) || - resourcePath.includes(`\\node_modules\\${m}\\`), - ) - ) { - return source; - } + // Skip known-incompatible node_modules + if (KNOWN_INCOMPATIBLE.some((m) => resourcePath.includes(`/node_modules/${m}/`) || resourcePath.includes(`\\node_modules\\${m}\\`))) { + return source; + } - // Only process files that likely contain JSX - if (!/\.(jsx|tsx|js|ts)$/.test(resourcePath)) return source; + // Only process files that likely contain JSX + if (!/\.(jsx|tsx|js|ts)$/.test(resourcePath)) { + return source; + } - const sourceFileName = resourcePath.split("/").pop(); + const sourceFileName = resourcePath.split('/').pop(); - let ast; - try { - ast = parser.parse(source, { - sourceType: "module", - // Parse-only — no code generation; plugins enable syntax understanding - plugins: [ - "jsx", - "typescript", - "decorators-legacy", - "classProperties", - "classPrivateProperties", - "classPrivateMethods", - "optionalChaining", - "nullishCoalescingOperator", - ], - strictMode: false, - }); - } catch { - // If parsing fails, pass through unchanged — OXC will surface the real error - return source; - } + let ast; + try { + ast = parser.parse(source, { + sourceType: 'module', + // Parse-only — no code generation; plugins enable syntax understanding + plugins: ['jsx', 'typescript', 'decorators-legacy', 'classProperties', 'classPrivateProperties', 'classPrivateMethods', 'optionalChaining', 'nullishCoalescingOperator'], + strictMode: false, + }); + } catch { + // If parsing fails, pass through unchanged — OXC will surface the real error + return source; + } - let modified = false; + let modified = false; - traverse(ast, { - FunctionDeclaration(path) { - const name = path.node.id && path.node.id.name; - if (!name) return; - const before = JSON.stringify(path.node); - annotateComponent(path, name, sourceFileName); - if (JSON.stringify(path.node) !== before) modified = true; - }, - ArrowFunctionExpression(path) { - const parent = path.parent; - const name = - t.isVariableDeclarator(parent) && t.isIdentifier(parent.id) - ? parent.id.name - : null; - if (!name) return; - const before = JSON.stringify(path.node); - annotateComponent(path, name, sourceFileName); - if (JSON.stringify(path.node) !== before) modified = true; - }, - ClassDeclaration(path) { - const name = path.node.id && path.node.id.name; - if (!name) return; - path - .get("body") - .get("body") - .forEach((member) => { - if (!member.isClassMethod()) return; - if (!t.isIdentifier(member.node.key, { name: "render" })) return; - member - .get("body") - .get("body") - .forEach((stmt) => { - if (!stmt.isReturnStatement()) return; - const arg = stmt.get("argument"); - if (!arg.isJSXElement() && !arg.isJSXFragment()) return; - const before = JSON.stringify(arg.node); - annotateJSXNode(arg, name, sourceFileName); - if (JSON.stringify(arg.node) !== before) modified = true; - }); - }); - }, - }); + traverse(ast, { + FunctionDeclaration(path) { + const name = path.node.id && path.node.id.name; + if (!name) { + return; + } + const before = JSON.stringify(path.node); + applyPropsToComponent(path, name, sourceFileName); + if (JSON.stringify(path.node) !== before) { + modified = true; + } + }, + ArrowFunctionExpression(path) { + const parent = path.parent; + const name = t.isVariableDeclarator(parent) && t.isIdentifier(parent.id) ? parent.id.name : null; + if (!name) { + return; + } + const before = JSON.stringify(path.node); + applyPropsToComponent(path, name, sourceFileName); + if (JSON.stringify(path.node) !== before) { + modified = true; + } + }, + ClassDeclaration(path) { + const name = path.node.id && path.node.id.name; + if (!name) { + return; + } + for (const member of path.get('body').get('body')) { + if (!member.isClassMethod()) { + continue; + } + if (!t.isIdentifier(member.node.key, {name: 'render'})) { + continue; + } + for (const stmt of member.get('body').get('body')) { + if (!stmt.isReturnStatement()) { + continue; + } + const arg = stmt.get('argument'); + if (!arg.isJSXElement() && !arg.isJSXFragment()) { + continue; + } + const before = JSON.stringify(arg.node); + applyPropsToJSXNode(arg, name, sourceFileName); + if (JSON.stringify(arg.node) !== before) { + modified = true; + } + } + } + }, + }); - if (!modified) return source; + if (!modified) { + return source; + } - const { code, map } = generate( - ast, - { sourceMaps: !!this.sourceMap, sourceFileName: resourcePath }, - source, - ); + const {code, map} = generate(ast, {sourceMaps: !!this.sourceMap, sourceFileName: resourcePath}, source); - if (this.sourceMap && map) { - this.callback(null, code, map); - return; - } + if (this.sourceMap && map) { + this.callback(null, code, map); + return; + } - return code; + return code; }; diff --git a/config/webpack/oxc-react-compiler-loader.js b/config/webpack/oxc-react-compiler-loader.js index 3b88fa862f62..fea24612d218 100644 --- a/config/webpack/oxc-react-compiler-loader.js +++ b/config/webpack/oxc-react-compiler-loader.js @@ -9,107 +9,108 @@ * This loader replaces oxc-loader entirely for Rule A (app source). */ -"use strict"; - -const path = require("path"); -const { getTsconfig } = require("get-tsconfig"); +const path = require('path'); +const {getTsconfig} = require('get-tsconfig'); // Resolve the oxc-transform binary that oxc-loader itself uses (its nested copy) // so we stay on the same native binary version as oxc-loader. -const oxcTransformPath = require.resolve("oxc-transform", { - paths: [path.resolve(__dirname, "../../node_modules/oxc-loader")], +const oxcTransformPath = require.resolve('oxc-transform', { + paths: [path.resolve(__dirname, '../../node_modules/oxc-loader')], }); -// eslint-disable-next-line import/no-dynamic-require, @typescript-eslint/no-unsafe-assignment -const { transform, transformSync } = require(oxcTransformPath); + +const {transform} = require(oxcTransformPath); function extractTsconfigOptions(rootContext) { - try { - const tsconfig = getTsconfig(rootContext); - if (!tsconfig) return {}; - const { compilerOptions } = tsconfig.config; - if (!compilerOptions) return {}; - const opts = {}; - if (compilerOptions.target) { - const map = { - ES2015: "es2015", - ES2016: "es2016", - ES2017: "es2017", - ES2018: "es2018", - ES2019: "es2019", - ES2020: "es2020", - ES2021: "es2021", - ES2022: "es2022", - ESNEXT: "esnext", - }; - const t = map[compilerOptions.target.toUpperCase()]; - if (t) opts.target = t; + try { + const tsconfig = getTsconfig(rootContext); + if (!tsconfig) { + return {}; + } + const {compilerOptions} = tsconfig.config; + if (!compilerOptions) { + return {}; + } + const opts = {}; + if (compilerOptions.target) { + const map = { + ES2015: 'es2015', + ES2016: 'es2016', + ES2017: 'es2017', + ES2018: 'es2018', + ES2019: 'es2019', + ES2020: 'es2020', + ES2021: 'es2021', + ES2022: 'es2022', + ESNEXT: 'esnext', + }; + const t = map[compilerOptions.target.toUpperCase()]; + if (t) { + opts.target = t; + } + } + return opts; + } catch { + return {}; } - return opts; - } catch { - return {}; - } +} + +function getLang(ext) { + if (ext === 'tsx') { + return 'tsx'; + } + if (ext === 'jsx') { + return 'jsx'; + } + if (ext === 'ts') { + return 'ts'; + } + return 'js'; } module.exports = async function oxcReactCompilerLoader(source) { - const callback = this.async(); - try { - const options = this.getOptions() || {}; - const sourceMaps = - options.sourcemap !== undefined ? options.sourcemap : !!this.sourceMap; - const tsconfigOptions = extractTsconfigOptions(this.rootContext); + const callback = this.async(); + try { + const options = this.getOptions() || {}; + const sourceMaps = options.sourcemap !== undefined ? options.sourcemap : !!this.sourceMap; + const tsconfigOptions = extractTsconfigOptions(this.rootContext); - const resourcePath = this.resourcePath; - const ext = path.extname(resourcePath).slice(1); - const lang = - ext === "tsx" || ext === "jsx" - ? ext === "tsx" - ? "tsx" - : "jsx" - : ext === "ts" - ? "ts" - : "js"; + const resourcePath = this.resourcePath; + const ext = path.extname(resourcePath).slice(1); + const lang = getLang(ext); - const transformOptions = { - ...tsconfigOptions, - lang, - target: options.target, - jsx: options.jsx, - reactCompiler: options.reactCompiler, - sourcemap: sourceMaps, - cwd: this.rootContext, - }; + const transformOptions = { + ...tsconfigOptions, + lang, + target: options.target, + jsx: options.jsx, + reactCompiler: options.reactCompiler, + sourcemap: sourceMaps, + cwd: this.rootContext, + }; - const result = await transform(resourcePath, source, transformOptions); + const result = await transform(resourcePath, source, transformOptions); - // Demote React Compiler diagnostics to webpack warnings instead of - // hard errors (workaround for oxc-project/oxc#23587). - const rcErrors = (result.errors || []).filter( - (e) => e.message && e.message.includes("[ReactCompiler]"), - ); - const fatalErrors = (result.errors || []).filter( - (e) => !e.message?.includes("[ReactCompiler]"), - ); + // Demote React Compiler diagnostics to webpack warnings instead of + // hard errors (workaround for oxc-project/oxc#23587). + const rcErrors = (result.errors || []).filter((e) => e.message && e.message.includes('[ReactCompiler]')); + const fatalErrors = (result.errors || []).filter((e) => !e.message?.includes('[ReactCompiler]')); - if (rcErrors.length > 0) { - rcErrors.forEach((e) => - this.emitWarning(new Error(`oxc-react-compiler-loader: ${e.message}`)), - ); - } + for (const e of rcErrors) { + this.emitWarning(new Error(`oxc-react-compiler-loader: ${e.message}`)); + } - if (fatalErrors.length > 0) { - const msg = fatalErrors - .map((e) => `${e.message}${e.codeframe ? `\n${e.codeframe}` : ""}`) - .join("\n\n"); - callback(new Error(`Oxc transform errors:\n${msg}`)); - return; - } + if (fatalErrors.length > 0) { + const msg = fatalErrors.map((e) => `${e.message}${e.codeframe ? `\n${e.codeframe}` : ''}`).join('\n\n'); + callback(new Error(`Oxc transform errors:\n${msg}`)); + return; + } - if (sourceMaps && result.map) { - callback(null, result.code, result.map); - } else { - callback(null, result.code); + if (sourceMaps && result.map) { + callback(null, result.code, result.map); + } else { + callback(null, result.code); + } + } catch (err) { + callback(err); } - } catch (err) { - callback(err); - } }; diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index 08c244c2da90..4252fa46d43d 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -1,29 +1,29 @@ -import { sentryWebpackPlugin } from "@sentry/webpack-plugin"; -import { execSync } from "child_process"; -import { CleanWebpackPlugin } from "clean-webpack-plugin"; -import CopyPlugin from "copy-webpack-plugin"; -import dotenv from "dotenv"; -import fs from "fs"; -import HtmlWebpackPlugin from "html-webpack-plugin"; -import MiniCssExtractPlugin from "mini-css-extract-plugin"; -import { createRequire } from "module"; -import path from "path"; -import TerserPlugin from "terser-webpack-plugin"; -import type { Class } from "type-fest"; -import { fileURLToPath } from "url"; -import webpack from "webpack"; -import type { Configuration, WebpackPluginInstance } from "webpack"; -import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer"; -import { GenerateSW } from "workbox-webpack-plugin"; +import {sentryWebpackPlugin} from '@sentry/webpack-plugin'; +import {execSync} from 'child_process'; +import {CleanWebpackPlugin} from 'clean-webpack-plugin'; +import CopyPlugin from 'copy-webpack-plugin'; +import dotenv from 'dotenv'; +import fs from 'fs'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import MiniCssExtractPlugin from 'mini-css-extract-plugin'; +import {createRequire} from 'module'; +import path from 'path'; +import TerserPlugin from 'terser-webpack-plugin'; +import type {Class} from 'type-fest'; +import {fileURLToPath} from 'url'; +import webpack from 'webpack'; +import type {Configuration, WebpackPluginInstance} from 'webpack'; +import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer'; +import {GenerateSW} from 'workbox-webpack-plugin'; // Storybook 10 loads TS files directly and requires .ts extension for ESM imports // @ts-expect-error -- Can't use .ts extensions without allowImportingTsExtensions in tsconfig // eslint-disable-next-line import/extensions -import CustomVersionFilePlugin from "./CustomVersionFilePlugin.ts"; +import CustomVersionFilePlugin from './CustomVersionFilePlugin.ts'; // @ts-expect-error -- Can't use .ts extensions without allowImportingTsExtensions in tsconfig // eslint-disable-next-line import/extensions -import ModuleInitTimingPlugin from "./ModuleInitTimingPlugin.ts"; +import ModuleInitTimingPlugin from './ModuleInitTimingPlugin.ts'; // eslint-disable-next-line import/extensions -import type Environment from "./types.ts"; +import type Environment from './types.ts'; const require = createRequire(import.meta.url); const filename = fileURLToPath(import.meta.url); @@ -32,593 +32,540 @@ const dirname = path.dirname(filename); dotenv.config(); function getCurrentBranchName(): string { - try { - return execSync("git rev-parse --abbrev-ref HEAD", { - encoding: "utf-8", - }).trim(); - } catch { - return ""; - } + try { + return execSync('git rev-parse --abbrev-ref HEAD', { + encoding: 'utf-8', + }).trim(); + } catch { + return ''; + } } const localBranchName = getCurrentBranchName(); type Options = { - rel: string; - as: string; - fileWhitelist: RegExp[]; - include: string; + rel: string; + as: string; + fileWhitelist: RegExp[]; + include: string; }; type PreloadWebpackPluginClass = Class; // require is necessary, importing anything from @vue/preload-webpack-plugin causes an error -const PreloadWebpackPlugin = - require("@vue/preload-webpack-plugin") as PreloadWebpackPluginClass; +const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin') as PreloadWebpackPluginClass; const includeModules = [ - "react-native-reanimated", - "react-native-worklets", - "react-native-picker-select", - "react-native-web", - "react-native-webview", - "@react-native-picker", - "@react-navigation/material-top-tabs", - "@react-navigation/native", - "@react-navigation/native-stack", - "@react-navigation/stack", - "react-native-gesture-handler", - "react-native-google-places-autocomplete", - "react-native-qrcode-svg", - "react-native-view-shot", - "@react-native/assets", - "expo", - "expo-audio", - "expo-video", - "expo-image", - "expo-image-manipulator", - "expo-modules-core", - "victory-native", - "@shopify/react-native-skia", -].join("|"); + 'react-native-reanimated', + 'react-native-worklets', + 'react-native-picker-select', + 'react-native-web', + 'react-native-webview', + '@react-native-picker', + '@react-navigation/material-top-tabs', + '@react-navigation/native', + '@react-navigation/native-stack', + '@react-navigation/stack', + 'react-native-gesture-handler', + 'react-native-google-places-autocomplete', + 'react-native-qrcode-svg', + 'react-native-view-shot', + '@react-native/assets', + 'expo', + 'expo-audio', + 'expo-video', + 'expo-image', + 'expo-image-manipulator', + 'expo-modules-core', + 'victory-native', + '@shopify/react-native-skia', +].join('|'); const environmentToLogoSuffixMap: Record = { - production: "-dark", - staging: "-stg", - dev: "-dev", - adhoc: "-adhoc", + production: '-dark', + staging: '-stg', + dev: '-dev', + adhoc: '-adhoc', }; function mapEnvironmentToLogoSuffix(environmentFile: string): string { - let environment = environmentFile.split(".").at(2); - if (typeof environment === "undefined") { - environment = "dev"; - } - return environmentToLogoSuffixMap[environment]; + let environment = environmentFile.split('.').at(2); + if (typeof environment === 'undefined') { + environment = 'dev'; + } + return environmentToLogoSuffixMap[environment]; } /** * Get a production grade config for web */ -const getCommonConfiguration = ({ - file = ".env", - platform = "web", -}: Environment): Configuration => { - const isDevelopment = file === ".env" || file === ".env.development"; +const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): Configuration => { + const isDevelopment = file === '.env' || file === '.env.development'; - if (!isDevelopment) { - const releaseName = `${process.env.npm_package_name}@${process.env.npm_package_version}`; - console.debug(`[SENTRY ${platform.toUpperCase()}] Release: ${releaseName}`); - console.debug( - `[SENTRY ${platform.toUpperCase()}] Assets Path: ${"./dist/**/*.{js,map}"}`, - ); - } + if (!isDevelopment) { + const releaseName = `${process.env.npm_package_name}@${process.env.npm_package_version}`; + console.debug(`[SENTRY ${platform.toUpperCase()}] Release: ${releaseName}`); + console.debug(`[SENTRY ${platform.toUpperCase()}] Assets Path: ${'./dist/**/*.{js,map}'}`); + } - /* eslint-disable @typescript-eslint/naming-convention */ - return { - mode: isDevelopment ? "development" : "production", - devtool: "source-map", - entry: { - main: "./index.js", - }, - output: { - // Use simple filenames in development to prevent memory leaks from contenthash changes - filename: isDevelopment - ? "[name].bundle.js" - : "[name]-[contenthash].bundle.js", - path: path.resolve(dirname, "../../dist"), - publicPath: "/", - }, - stats: { - // We can ignore the "module not installed" warning from lottie-react-native - // because we are not using the library for JSON format of Lottie animations. - warningsFilter: [ - "./node_modules/lottie-react-native/lib/module/LottieView/index.web.js", - ], - }, - plugins: [ - new CleanWebpackPlugin(), - // Only emit the SW for non-development builds. In dev, webpack-dev-server's HMR - // and the SW's caching behavior fight each other and confuse hot reloads. - // Remove this guard locally if you want to actually exercise the SW. - ...(isDevelopment - ? [] - : [ - new GenerateSW({ - clientsClaim: true, - skipWaiting: true, - // Cap is generous on purpose: the vendor (~6.5 MiB), main (~5.5 MiB), - // authScreens.prefetch (~6.3 MiB) chunks and canvaskit.wasm (~7.6 MiB) are - // all critical for offline boot, so we precache the lot. Everything in the - // App build is content-hashed, so growth here only costs first-install bytes. - maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, - // Single-page app: any unmatched navigation should serve the cached app shell. - navigateFallback: "/index.html", - // Don't fall back for asset-like or .well-known requests. - navigateFallbackDenylist: [ - /^\/_/, - /^\/\.well-known/, - /\/[^/?]+\.[^/]+$/, - ], - runtimeCaching: [ - { - // Same-origin user media (receipts, chat attachments) — cache opportunistically - // so they're viewable on offline refresh. The function below is serialized into - // the generated service worker, so it executes in the SW context where - // `sameOrigin` is the appropriate Workbox match (no need to read `self.location`). - urlPattern: ({ sameOrigin, url, request }) => - sameOrigin && - (request.destination === "image" || - /\/(receipts|chat-attachments)\//.test(url.pathname)), - handler: "StaleWhileRevalidate", - options: { - cacheName: "user-media", - expiration: { - maxEntries: 200, - maxAgeSeconds: 7 * 24 * 60 * 60, - }, - }, - }, - ], + /* eslint-disable @typescript-eslint/naming-convention */ + return { + mode: isDevelopment ? 'development' : 'production', + devtool: 'source-map', + entry: { + main: './index.js', + }, + output: { + // Use simple filenames in development to prevent memory leaks from contenthash changes + filename: isDevelopment ? '[name].bundle.js' : '[name]-[contenthash].bundle.js', + path: path.resolve(dirname, '../../dist'), + publicPath: '/', + }, + stats: { + // We can ignore the "module not installed" warning from lottie-react-native + // because we are not using the library for JSON format of Lottie animations. + warningsFilter: ['./node_modules/lottie-react-native/lib/module/LottieView/index.web.js'], + }, + plugins: [ + new CleanWebpackPlugin(), + // Only emit the SW for non-development builds. In dev, webpack-dev-server's HMR + // and the SW's caching behavior fight each other and confuse hot reloads. + // Remove this guard locally if you want to actually exercise the SW. + ...(isDevelopment + ? [] + : [ + new GenerateSW({ + clientsClaim: true, + skipWaiting: true, + // Cap is generous on purpose: the vendor (~6.5 MiB), main (~5.5 MiB), + // authScreens.prefetch (~6.3 MiB) chunks and canvaskit.wasm (~7.6 MiB) are + // all critical for offline boot, so we precache the lot. Everything in the + // App build is content-hashed, so growth here only costs first-install bytes. + maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, + // Single-page app: any unmatched navigation should serve the cached app shell. + navigateFallback: '/index.html', + // Don't fall back for asset-like or .well-known requests. + navigateFallbackDenylist: [/^\/_/, /^\/\.well-known/, /\/[^/?]+\.[^/]+$/], + runtimeCaching: [ + { + // Same-origin user media (receipts, chat attachments) — cache opportunistically + // so they're viewable on offline refresh. The function below is serialized into + // the generated service worker, so it executes in the SW context where + // `sameOrigin` is the appropriate Workbox match (no need to read `self.location`). + urlPattern: ({sameOrigin, url, request}) => sameOrigin && (request.destination === 'image' || /\/(receipts|chat-attachments)\//.test(url.pathname)), + handler: 'StaleWhileRevalidate', + options: { + cacheName: 'user-media', + expiration: { + maxEntries: 200, + maxAgeSeconds: 7 * 24 * 60 * 60, + }, + }, + }, + ], + }), + ]), + new HtmlWebpackPlugin({ + template: 'web/index.html', + filename: 'index.html', + splashLogo: fs.readFileSync(path.resolve(dirname, `../../assets/images/new-expensify${mapEnvironmentToLogoSuffix(file)}.svg`), 'utf-8'), + isWeb: platform === 'web', + isProduction: file === '.env.production', + isStaging: file === '.env.staging', + useThirdPartyScripts: process.env.USE_THIRD_PARTY_SCRIPTS === 'true' || (platform === 'web' && ['.env.production', '.env.staging'].includes(file)), + }), + // Inject into HTML + // This is not "webpackPrefetch: true" equivalent! + // By convention we use ".prefetch" suffix for such chunks + new PreloadWebpackPlugin({ + rel: 'prefetch', + as: 'script', + fileWhitelist: [/(.+)\.prefetch(.*)\.js$/], + include: 'asyncChunks', + }), + new PreloadWebpackPlugin({ + rel: 'preload', + as: 'font', + fileWhitelist: [/^(?!.*seguiemj).*\.(woff2|ttf)$/], + include: 'allAssets', + }), + new PreloadWebpackPlugin({ + rel: 'prefetch', + as: 'fetch', + fileWhitelist: [/\.lottie$/], + include: 'allAssets', + }), + new webpack.ProvidePlugin({ + process: 'process/browser', }), - ]), - new HtmlWebpackPlugin({ - template: "web/index.html", - filename: "index.html", - splashLogo: fs.readFileSync( - path.resolve( - dirname, - `../../assets/images/new-expensify${mapEnvironmentToLogoSuffix(file)}.svg`, - ), - "utf-8", - ), - isWeb: platform === "web", - isProduction: file === ".env.production", - isStaging: file === ".env.staging", - useThirdPartyScripts: - process.env.USE_THIRD_PARTY_SCRIPTS === "true" || - (platform === "web" && - [".env.production", ".env.staging"].includes(file)), - }), - // Inject into HTML - // This is not "webpackPrefetch: true" equivalent! - // By convention we use ".prefetch" suffix for such chunks - new PreloadWebpackPlugin({ - rel: "prefetch", - as: "script", - fileWhitelist: [/(.+)\.prefetch(.*)\.js$/], - include: "asyncChunks", - }), - new PreloadWebpackPlugin({ - rel: "preload", - as: "font", - fileWhitelist: [/^(?!.*seguiemj).*\.(woff2|ttf)$/], - include: "allAssets", - }), - new PreloadWebpackPlugin({ - rel: "prefetch", - as: "fetch", - fileWhitelist: [/\.lottie$/], - include: "allAssets", - }), - new webpack.ProvidePlugin({ - process: "process/browser", - }), - // Copies favicons into the dist/ folder to use for unread status - new CopyPlugin({ - patterns: [ - { from: "web/favicon.png" }, - { from: "web/favicon-unread.png" }, - { from: "web/og-preview-image.png" }, - { from: "web/apple-touch-icon.png" }, - { from: "web/robots.txt" }, - { from: "assets/images/expensify-app-icon.svg" }, - { from: "web/manifest.json" }, - { from: "assets/css", to: "css" }, - { from: "assets/fonts/web", to: "fonts" }, - { from: "assets/sounds", to: "sounds" }, - { from: "assets/pdfs", to: "pdfs" }, - { - from: "node_modules/react-pdf/dist/Page/AnnotationLayer.css", - to: "css/AnnotationLayer.css", - }, - { - from: "node_modules/react-pdf/dist/Page/TextLayer.css", - to: "css/TextLayer.css", - }, - { - from: ".well-known/apple-app-site-association", - to: ".well-known/apple-app-site-association", - toType: "file", - }, - { - from: ".well-known/assetlinks.json", - to: ".well-known/assetlinks.json", - }, + // Copies favicons into the dist/ folder to use for unread status + new CopyPlugin({ + patterns: [ + {from: 'web/favicon.png'}, + {from: 'web/favicon-unread.png'}, + {from: 'web/og-preview-image.png'}, + {from: 'web/apple-touch-icon.png'}, + {from: 'web/robots.txt'}, + {from: 'assets/images/expensify-app-icon.svg'}, + {from: 'web/manifest.json'}, + {from: 'assets/css', to: 'css'}, + {from: 'assets/fonts/web', to: 'fonts'}, + {from: 'assets/sounds', to: 'sounds'}, + {from: 'assets/pdfs', to: 'pdfs'}, + { + from: 'node_modules/react-pdf/dist/Page/AnnotationLayer.css', + to: 'css/AnnotationLayer.css', + }, + { + from: 'node_modules/react-pdf/dist/Page/TextLayer.css', + to: 'css/TextLayer.css', + }, + { + from: '.well-known/apple-app-site-association', + to: '.well-known/apple-app-site-association', + toType: 'file', + }, + { + from: '.well-known/assetlinks.json', + to: '.well-known/assetlinks.json', + }, - // These files are copied over as per instructions here - // https://github.com/wojtekmaj/react-pdf#copying-cmaps - { from: "node_modules/pdfjs-dist/cmaps/", to: "cmaps/" }, + // These files are copied over as per instructions here + // https://github.com/wojtekmaj/react-pdf#copying-cmaps + {from: 'node_modules/pdfjs-dist/cmaps/', to: 'cmaps/'}, - // Group‑IB web SDK injection file - { from: "web/snippets/gib.js", to: "gib.js" }, + // Group‑IB web SDK injection file + {from: 'web/snippets/gib.js', to: 'gib.js'}, - // CanvasKit WASM files for @shopify/react-native-skia web support (uses full version) - { from: "node_modules/canvaskit-wasm/bin/full/canvaskit.wasm" }, - ], - }), - new ModuleInitTimingPlugin(), - new webpack.EnvironmentPlugin({ JEST_WORKER_ID: "" }), - new webpack.IgnorePlugin({ - resourceRegExp: /^\.\/locale$/, - contextRegExp: /moment$/, - }), - ...(file === ".env.production" || file === ".env.staging" - ? [ + // CanvasKit WASM files for @shopify/react-native-skia web support (uses full version) + {from: 'node_modules/canvaskit-wasm/bin/full/canvaskit.wasm'}, + ], + }), + new ModuleInitTimingPlugin(), + new webpack.EnvironmentPlugin({JEST_WORKER_ID: ''}), new webpack.IgnorePlugin({ - resourceRegExp: /@welldone-software\/why-did-you-render/, + resourceRegExp: /^\.\/locale$/, + contextRegExp: /moment$/, + }), + ...(file === '.env.production' || file === '.env.staging' + ? [ + new webpack.IgnorePlugin({ + resourceRegExp: /@welldone-software\/why-did-you-render/, + }), + ] + : []), + ...(platform === 'web' ? [new CustomVersionFilePlugin()] : []), + new webpack.DefinePlugin({ + process: {env: {}}, + // Define EXPO_OS for web platform to fix expo-modules-core warning + 'process.env.EXPO_OS': JSON.stringify('web'), + __REACT_WEB_CONFIG__: JSON.stringify(dotenv.config({path: file}).parsed), + + // React Native JavaScript environment requires the global __DEV__ variable to be accessible. + // react-native-render-html uses variable to log exclusively during development. + // See https://reactnative.dev/docs/javascript-environment + __DEV__: /staging|prod|adhoc/.test(file) === false, + // Expose the current git branch so the debug menu can display it in the browser tab title. + // Empty string in non-development builds. + __GIT_BRANCH__: JSON.stringify(isDevelopment ? localBranchName : ''), }), - ] - : []), - ...(platform === "web" ? [new CustomVersionFilePlugin()] : []), - new webpack.DefinePlugin({ - process: { env: {} }, - // Define EXPO_OS for web platform to fix expo-modules-core warning - "process.env.EXPO_OS": JSON.stringify("web"), - __REACT_WEB_CONFIG__: JSON.stringify( - dotenv.config({ path: file }).parsed, - ), + ...(isDevelopment ? [] : [new MiniCssExtractPlugin()]), - // React Native JavaScript environment requires the global __DEV__ variable to be accessible. - // react-native-render-html uses variable to log exclusively during development. - // See https://reactnative.dev/docs/javascript-environment - __DEV__: /staging|prod|adhoc/.test(file) === false, - // Expose the current git branch so the debug menu can display it in the browser tab title. - // Empty string in non-development builds. - __GIT_BRANCH__: JSON.stringify(isDevelopment ? localBranchName : ""), - }), - ...(isDevelopment ? [] : [new MiniCssExtractPlugin()]), + // Upload source maps to Sentry + ...(isDevelopment + ? [] + : ([ + sentryWebpackPlugin({ + authToken: process.env.SENTRY_AUTH_TOKEN as string | undefined, + org: 'expensify', + project: 'app', + release: { + name: `${process.env.npm_package_name}@${process.env.npm_package_version}`, + create: true, + setCommits: { + auto: true, + }, + }, + sourcemaps: { + // Use relative path from project root - works for web (dist/) + assets: './dist/**/*.{js,map}', + filesToDeleteAfterUpload: './dist/**/*.map', + }, + debug: false, + telemetry: false, + }), + ] as WebpackPluginInstance[])), - // Upload source maps to Sentry - ...(isDevelopment - ? [] - : ([ - sentryWebpackPlugin({ - authToken: process.env.SENTRY_AUTH_TOKEN as string | undefined, - org: "expensify", - project: "app", - release: { - name: `${process.env.npm_package_name}@${process.env.npm_package_version}`, - create: true, - setCommits: { - auto: true, + // This allows us to interactively inspect JS bundle contents + ...(process.env.ANALYZE_BUNDLE === 'true' ? [new BundleAnalyzerPlugin()] : []), + ], + module: { + rules: [ + { + test: /\.m?js$/, + resolve: { + fullySpecified: false, + }, }, - }, - sourcemaps: { - // Use relative path from project root - works for web (dist/) - assets: "./dist/**/*.{js,map}", - filesToDeleteAfterUpload: "./dist/**/*.map", - }, - debug: false, - telemetry: false, - }), - ] as WebpackPluginInstance[])), + // JS/TS/JSX/TSX is processed through a three-pass pipeline. + // Webpack use[] runs right-to-left, so execution order is Pass 1 → 2 → 3. + // + // Pass 1 (fullstory-annotation-loader): injects dataComponent/dataElement/dataSourceFile + // props onto JSX opening elements in parse-only mode (no code transforms). Output + // still contains JSX and TypeScript so OXC sees annotated original source. + // + // Pass 2 (oxc-loader @ 0.136.0): single Rust pass — React Compiler (before JSX + // transform), JSX → react/jsx-runtime, TypeScript strip, class-properties, env + // target downleveling, etc. React Compiler only runs for our own source code (see + // separate rule below for included node_modules which skips it). + // + // Pass 3 (babel-loader): worklets only — react-native-worklets/plugin serialises + // 'worklet' functions for UI-thread execution. No presets needed since OXC already + // stripped types and transformed JSX. - // This allows us to interactively inspect JS bundle contents - ...(process.env.ANALYZE_BUNDLE === "true" - ? [new BundleAnalyzerPlugin()] - : []), - ], - module: { - rules: [ - { - test: /\.m?js$/, - resolve: { - fullySpecified: false, - }, - }, - // JS/TS/JSX/TSX is processed through a three-pass pipeline. - // Webpack use[] runs right-to-left, so execution order is Pass 1 → 2 → 3. - // - // Pass 1 (fullstory-annotation-loader): injects dataComponent/dataElement/dataSourceFile - // props onto JSX opening elements in parse-only mode (no code transforms). Output - // still contains JSX and TypeScript so OXC sees annotated original source. - // - // Pass 2 (oxc-loader @ 0.136.0): single Rust pass — React Compiler (before JSX - // transform), JSX → react/jsx-runtime, TypeScript strip, class-properties, env - // target downleveling, etc. React Compiler only runs for our own source code (see - // separate rule below for included node_modules which skips it). - // - // Pass 3 (babel-loader): worklets only — react-native-worklets/plugin serialises - // 'worklet' functions for UI-thread execution. No presets needed since OXC already - // stripped types and transformed JSX. + // Rule A — app source files (React Compiler enabled) + { + test: /\.(js|ts)x?$/, + // Exclude ALL node_modules (including includeModules — handled by Rule B below) + exclude: [/node_modules/, /\.native\.(js|jsx|ts|tsx)$/], + use: [ + // Pass 3: worklets (on OXC's plain-JS output — no presets needed) + { + loader: 'babel-loader', + options: { + babelrc: false, + configFile: false, + plugins: ['react-native-worklets/plugin'], + }, + }, + // Pass 2: React Compiler + all transforms via our thin wrapper loader. + // oxc-react-compiler-loader calls oxc-transform directly and demotes + // non-fatal React Compiler diagnostics to webpack warnings instead of + // hard build errors (workaround for oxc-project/oxc#23587). + { + loader: path.resolve(dirname, './oxc-react-compiler-loader.js'), + options: { + reactCompiler: {target: '19', panicThreshold: 'none'}, + target: 'node20', + jsx: {runtime: 'automatic'}, + }, + }, + // Pass 1: Fullstory annotation (sees annotated JSX before OXC transforms it) + { + loader: path.resolve(dirname, './fullstory-annotation-loader.js'), + }, + ], + }, + // Rule B — included node_modules that need transforms but NOT React Compiler. + // These packages (react-native-web, reanimated, etc.) intentionally mutate state in + // ways that violate the Rules of React, so the React Compiler must not run on them. + // + // Split by extension because jsx:{runtime:'automatic'} would upgrade .ts files to TSX + // lang, breaking TypeScript generic syntax like `(x: T) => x` in plain TS files. - // Rule A — app source files (React Compiler enabled) - { - test: /\.(js|ts)x?$/, - // Exclude ALL node_modules (including includeModules — handled by Rule B below) - exclude: [/node_modules/, /\.native\.(js|jsx|ts|tsx)$/], - use: [ - // Pass 3: worklets (on OXC's plain-JS output — no presets needed) - { - loader: "babel-loader", - options: { - babelrc: false, - configFile: false, - plugins: ["react-native-worklets/plugin"], - }, - }, - // Pass 2: React Compiler + all transforms via our thin wrapper loader. - // oxc-react-compiler-loader calls oxc-transform directly and demotes - // non-fatal React Compiler diagnostics to webpack warnings instead of - // hard build errors (workaround for oxc-project/oxc#23587). - { - loader: path.resolve(dirname, "./oxc-react-compiler-loader.js"), - options: { - reactCompiler: { target: "19", panicThreshold: "none" }, - target: "node20", - jsx: { runtime: "automatic" }, - }, - }, - // Pass 1: Fullstory annotation (sees annotated JSX before OXC transforms it) - { - loader: path.resolve(dirname, "./fullstory-annotation-loader.js"), - }, - ], - }, - // Rule B — included node_modules that need transforms but NOT React Compiler. - // These packages (react-native-web, reanimated, etc.) intentionally mutate state in - // ways that violate the Rules of React, so the React Compiler must not run on them. - // - // Split by extension because jsx:{runtime:'automatic'} would upgrade .ts files to TSX - // lang, breaking TypeScript generic syntax like `(x: T) => x` in plain TS files. + // Rule B1: TypeScript — autoDetectJsx handles .tsx; .ts keeps TS lang (no JSX upgrade). + { + test: /\.tsx?$/, + include: [new RegExp(`node_modules/(${includeModules})`)], + exclude: [/\.native\.(ts|tsx)$/], + use: [ + { + loader: 'babel-loader', + options: { + babelrc: false, + configFile: false, + plugins: ['react-native-worklets/plugin'], + }, + }, + {loader: 'oxc-loader', options: {target: 'node20'}}, + ], + }, + // Rule B2: JavaScript — need explicit jsx to upgrade .js lang to jsx. + { + test: /\.jsx?$/, + include: [new RegExp(`node_modules/(${includeModules})`)], + exclude: [/\.native\.(js|jsx)$/], + use: [ + { + loader: 'babel-loader', + options: { + babelrc: false, + configFile: false, + plugins: ['react-native-worklets/plugin'], + }, + }, + { + loader: 'oxc-loader', + options: {target: 'node20', jsx: {runtime: 'automatic'}}, + }, + ], + }, + // We are importing this worker as a string by using asset/source otherwise it will default to loading via an HTTPS request later. + // This causes issues if we have gone offline before the pdfjs web worker is set up as we won't be able to load it from the server. + { + test: new RegExp('node_modules/pdfjs-dist/legacy/build/pdf.worker.min.mjs'), + type: 'asset/source', + }, - // Rule B1: TypeScript — autoDetectJsx handles .tsx; .ts keeps TS lang (no JSX upgrade). - { - test: /\.tsx?$/, - include: [new RegExp(`node_modules/(${includeModules})`)], - exclude: [/\.native\.(ts|tsx)$/], - use: [ - { - loader: "babel-loader", - options: { - babelrc: false, - configFile: false, - plugins: ["react-native-worklets/plugin"], - }, - }, - { loader: "oxc-loader", options: { target: "node20" } }, - ], - }, - // Rule B2: JavaScript — need explicit jsx to upgrade .js lang to jsx. - { - test: /\.jsx?$/, - include: [new RegExp(`node_modules/(${includeModules})`)], - exclude: [/\.native\.(js|jsx)$/], - use: [ - { - loader: "babel-loader", - options: { - babelrc: false, - configFile: false, - plugins: ["react-native-worklets/plugin"], - }, - }, - { - loader: "oxc-loader", - options: { target: "node20", jsx: { runtime: "automatic" } }, - }, - ], - }, - // We are importing this worker as a string by using asset/source otherwise it will default to loading via an HTTPS request later. - // This causes issues if we have gone offline before the pdfjs web worker is set up as we won't be able to load it from the server. - { - test: new RegExp( - "node_modules/pdfjs-dist/legacy/build/pdf.worker.min.mjs", - ), - type: "asset/source", - }, + // Rule for react-native-web-webview + { + test: /postMock.html$/, + type: 'asset', + generator: { + filename: '[name].[ext]', + }, + }, - // Rule for react-native-web-webview - { - test: /postMock.html$/, - type: "asset", - generator: { - filename: "[name].[ext]", - }, - }, + // Gives the ability to load local images + { + test: /\.(png|jpe?g|gif)$/i, + type: 'asset', + }, - // Gives the ability to load local images - { - test: /\.(png|jpe?g|gif)$/i, - type: "asset", + // Load svg images + { + test: /\.svg$/, + resourceQuery: {not: [/raw/]}, + exclude: /node_modules/, + use: [ + { + loader: '@svgr/webpack', + }, + ], + }, + { + test: /\.pdf$/, + type: 'asset', + }, + { + test: /\.css$/i, + use: isDevelopment ? ['style-loader', 'css-loader'] : [MiniCssExtractPlugin.loader, 'css-loader'], + }, + { + test: /\.(woff|woff2|ttf)$/i, + type: 'asset', + }, + { + resourceQuery: /raw/, + type: 'asset/source', + }, + { + test: /\.lottie$/, + type: 'asset/resource', + }, + // This prevents import error coming from react-native-tab-view/lib/module/TabView.js + // where Pager is imported without extension due to having platform-specific implementations + { + test: /\.js$/, + resolve: { + fullySpecified: false, + }, + include: [path.resolve(dirname, '../../node_modules/react-native-tab-view/lib/module/TabView.js')], + }, + ], }, + resolve: { + alias: { + lodash: 'lodash-es', + 'react-native-config': 'react-web-config', + 'react-native$': 'react-native-web', + // Use victory-native source files instead of pre-compiled dist (which uses CommonJS exports) + 'victory-native': path.resolve(dirname, '../../node_modules/victory-native/src/index.ts'), + // Required for @shopify/react-native-skia web support + 'react-native/Libraries/Image/AssetRegistry': false, + // Use legacy build of pdfjs-dist to support older browsers + 'pdfjs-dist$': path.resolve(dirname, '../../node_modules/pdfjs-dist/legacy/build/pdf.mjs'), + // Module alias for web + // https://webpack.js.org/configuration/resolve/#resolvealias + '@assets': path.resolve(dirname, '../../assets'), + '@components': path.resolve(dirname, '../../src/components/'), + '@hooks': path.resolve(dirname, '../../src/hooks/'), + '@libs': path.resolve(dirname, '../../src/libs/'), + '@navigation': path.resolve(dirname, '../../src/libs/Navigation/'), + '@pages': path.resolve(dirname, '../../src/pages/'), + '@prompts': path.resolve(dirname, '../../prompts'), + '@styles': path.resolve(dirname, '../../src/styles/'), + // This path is provide alias for files like `ONYXKEYS` and `CONST`. + '@src': path.resolve(dirname, '../../src/'), + '@userActions': path.resolve(dirname, '../../src/libs/actions/'), + '@selectors': path.resolve(dirname, '../../src/selectors/'), + }, - // Load svg images - { - test: /\.svg$/, - resourceQuery: { not: [/raw/] }, - exclude: /node_modules/, - use: [ - { - loader: "@svgr/webpack", + // Resolve web-specific implementations (`.web.*`) before bare files so React Native + // libraries and our own app-level overrides both pick up their browser variants. + extensions: ['.web.js', '.js', '.jsx', '.web.ts', '.web.tsx', '.ts', '.tsx'], + fallback: { + 'process/browser': require.resolve('process/browser'), + crypto: false, + fs: false, + path: false, }, - ], - }, - { - test: /\.pdf$/, - type: "asset", - }, - { - test: /\.css$/i, - use: isDevelopment - ? ["style-loader", "css-loader"] - : [MiniCssExtractPlugin.loader, "css-loader"], - }, - { - test: /\.(woff|woff2|ttf)$/i, - type: "asset", - }, - { - resourceQuery: /raw/, - type: "asset/source", - }, - { - test: /\.lottie$/, - type: "asset/resource", - }, - // This prevents import error coming from react-native-tab-view/lib/module/TabView.js - // where Pager is imported without extension due to having platform-specific implementations - { - test: /\.js$/, - resolve: { - fullySpecified: false, - }, - include: [ - path.resolve( - dirname, - "../../node_modules/react-native-tab-view/lib/module/TabView.js", - ), - ], }, - ], - }, - resolve: { - alias: { - lodash: "lodash-es", - "react-native-config": "react-web-config", - "react-native$": "react-native-web", - // Use victory-native source files instead of pre-compiled dist (which uses CommonJS exports) - "victory-native": path.resolve( - dirname, - "../../node_modules/victory-native/src/index.ts", - ), - // Required for @shopify/react-native-skia web support - "react-native/Libraries/Image/AssetRegistry": false, - // Use legacy build of pdfjs-dist to support older browsers - "pdfjs-dist$": path.resolve( - dirname, - "../../node_modules/pdfjs-dist/legacy/build/pdf.mjs", - ), - // Module alias for web - // https://webpack.js.org/configuration/resolve/#resolvealias - "@assets": path.resolve(dirname, "../../assets"), - "@components": path.resolve(dirname, "../../src/components/"), - "@hooks": path.resolve(dirname, "../../src/hooks/"), - "@libs": path.resolve(dirname, "../../src/libs/"), - "@navigation": path.resolve(dirname, "../../src/libs/Navigation/"), - "@pages": path.resolve(dirname, "../../src/pages/"), - "@prompts": path.resolve(dirname, "../../prompts"), - "@styles": path.resolve(dirname, "../../src/styles/"), - // This path is provide alias for files like `ONYXKEYS` and `CONST`. - "@src": path.resolve(dirname, "../../src/"), - "@userActions": path.resolve(dirname, "../../src/libs/actions/"), - "@selectors": path.resolve(dirname, "../../src/selectors/"), - }, - // Resolve web-specific implementations (`.web.*`) before bare files so React Native - // libraries and our own app-level overrides both pick up their browser variants. - extensions: [ - ".web.js", - ".js", - ".jsx", - ".web.ts", - ".web.tsx", - ".ts", - ".tsx", - ], - fallback: { - "process/browser": require.resolve("process/browser"), - crypto: false, - fs: false, - path: false, - }, - }, + optimization: { + minimizer: [ + // default settings according to https://webpack.js.org/configuration/optimization/#optimizationminimizer + // with addition of preserving the class name for ImageManipulator (expo module) + new TerserPlugin({ + terserOptions: { + compress: { + passes: 2, + }, + keep_classnames: /ImageManipulator|ImageModule/, + mangle: { + keep_fnames: true, + }, + }, + }), + '...', + ], + runtimeChunk: 'single', + splitChunks: { + cacheGroups: { + // We have to load the whole lottie player to get the player to work in offline mode + lottiePlayer: { + test: /[\\/]node_modules[\\/](@dotlottie\/react-player)[\\/]/, + name: 'lottiePlayer', + chunks: 'all', + }, + // heic-to library is used sparsely and we want to load it as a separate chunk + // to reduce the potential bundled size of the initial chunk + heicTo: { + test: /[\\/]node_modules[\\/](heic-to)[\\/]/, + name: 'heicTo', + chunks: 'all', + priority: 10, // ensure this chunk has always its own group + }, + // ExpensifyIcons chunk - separate chunk loaded eagerly for offline support + expensifyIcons: { + test: /[\\/]src[\\/]components[\\/]Icon[\\/]chunks[\\/]expensify-icons\.chunk\.ts$/, + name: 'expensifyIcons', + chunks: 'all', + }, + // Illustrations chunk - separate chunk loaded eagerly for offline support + illustrations: { + test: /[\\/]src[\\/]components[\\/]Icon[\\/]chunks[\\/]illustrations\.chunk\.ts$/, + name: 'illustrations', + chunks: 'all', + }, + // Extract all 3rd party dependencies (~75% of App) to separate js file + // This gives a more efficient caching - 3rd party deps don't change as often as main source + // When dependencies don't change webpack would produce the same js file (and content hash) + // After App update end users would download just the main source and resolve the rest from cache + // When dependencies do change cache is invalidated and users download everything - same as before + vendor: { + test: /[\\/]node_modules[\\/]/, + name: 'vendors', - optimization: { - minimizer: [ - // default settings according to https://webpack.js.org/configuration/optimization/#optimizationminimizer - // with addition of preserving the class name for ImageManipulator (expo module) - new TerserPlugin({ - terserOptions: { - compress: { - passes: 2, - }, - keep_classnames: /ImageManipulator|ImageModule/, - mangle: { - keep_fnames: true, + // Capture only the scripts needed for the initial load, so any async imports + // would be grouped (and lazy loaded) separately + chunks: 'initial', + }, + }, }, - }, - }), - "...", - ], - runtimeChunk: "single", - splitChunks: { - cacheGroups: { - // We have to load the whole lottie player to get the player to work in offline mode - lottiePlayer: { - test: /[\\/]node_modules[\\/](@dotlottie\/react-player)[\\/]/, - name: "lottiePlayer", - chunks: "all", - }, - // heic-to library is used sparsely and we want to load it as a separate chunk - // to reduce the potential bundled size of the initial chunk - heicTo: { - test: /[\\/]node_modules[\\/](heic-to)[\\/]/, - name: "heicTo", - chunks: "all", - priority: 10, // ensure this chunk has always its own group - }, - // ExpensifyIcons chunk - separate chunk loaded eagerly for offline support - expensifyIcons: { - test: /[\\/]src[\\/]components[\\/]Icon[\\/]chunks[\\/]expensify-icons\.chunk\.ts$/, - name: "expensifyIcons", - chunks: "all", - }, - // Illustrations chunk - separate chunk loaded eagerly for offline support - illustrations: { - test: /[\\/]src[\\/]components[\\/]Icon[\\/]chunks[\\/]illustrations\.chunk\.ts$/, - name: "illustrations", - chunks: "all", - }, - // Extract all 3rd party dependencies (~75% of App) to separate js file - // This gives a more efficient caching - 3rd party deps don't change as often as main source - // When dependencies don't change webpack would produce the same js file (and content hash) - // After App update end users would download just the main source and resolve the rest from cache - // When dependencies do change cache is invalidated and users download everything - same as before - vendor: { - test: /[\\/]node_modules[\\/]/, - name: "vendors", - - // Capture only the scripts needed for the initial load, so any async imports - // would be grouped (and lazy loaded) separately - chunks: "initial", - }, }, - }, - }, - }; + }; }; /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/cspell.json b/cspell.json index 350cc0b49cb2..885d19acfa57 100644 --- a/cspell.json +++ b/cspell.json @@ -7,433 +7,37 @@ }, "words": [ "--longpress", - "ADDCOMMENT", - "ADFS", - "AMRO", - "APCA", - "APPL", - "ARGB", - "ARNK", - "ARROWDOWN", - "ARROWLEFT", - "ARROWRIGHT", - "ARROWUP", - "ASPAC", - "AUTOAPPROVE", - "AUTOREIMBURSED", - "AUTOREPORTING", - "AVURL", - "Accelo", - "Addendums", - "Aeroplan", - "Aircall", - "Airplus", - "Airwallex", - "Amal", - "Amal's", - "Amina", - "Areport", - "Authy", - "BBVA", - "BILLCOM", - "BMO", - "BNDCCAMM", - "BNDL", - "BOFMCAM2", - "BROWSABLE", - "BYOC", - "BambooHr", - "Bancorporation", - "Banque", - "Bartek", - "Batchinator", - "Belfius", - "Billpay", - "Bobbeth", - "Borderless", - "Botify", - "Broadwoven", - "Bronn", - "Buildscript", - "Bunq", - "Bushwick", - "CARDFROZEN", - "CARDUNFROZEN", - "CARDDEACTIVATED", - "CAROOT", - "CCDQCAMM", - "CFPB", - "CIBC", - "CIBCCATT", - "CLIA", - "CLIENTID", - "CPPFLAGS", - "CREDS", - "CROSSLINK", - "Caixa", - "Carta", - "Certinia", - "Certinia's", - "Charleson", - "Checkmark", - "Chronos", - "Cliqbook", - "Codat", - "Codice", - "Combustors", - "Corpay", - "Countertop", - "Crédit", - "DFOLLY", - "DSYM", - "DYNAMICEXTERNAL", - "Danske", - "Deel", - "Depósitos", - "Desjardins", - "Deutsch", - "Dishoom", - "DocuSign", - "Drycleaners", - "Drycleaning", - "Dtype", - "Dumpty", - "EDDSA", - "EDIFACT", - "EMEA", - "ENVFILE", - "ERECEIPT", - "ERECEIPTS", - "ESTA", - "EUVAT", - "EXPENSIDEV", - "EXPENSIFYAPI", - "EXPENSIFYPDBUSINESS", - "EXPENSIFYPDTEAM", - "EXPENSIFYWEB", - "Egencia", - "Electromedical", - "Electrotherapeutic", - "Emphemeral", - "Entra", - "Ephermeral", - "Erste", - "Español", - "Expatriot", - "Expensable", - "Expensi", - "Expensicon", - "Expensicons", - "Expensidev", - "Expensifier", - "Expensiworks", - "FLJZ", - "FWTV", - "FXHF", - "Fbclid", - "Ferroalloy", - "FinancialForce", - "Fiscale", - "Flamegraph", - "Français", - "Frederico", - "Fábio", - "GBRRBR", - "GDSO", - "GEOLOCATION", - "Gaber", - "Gclid", - "Geral", - "gitlink", - "Grantmaking", - "Gsuite", - "Générale", - "HKBCCATT", - "HRMS", - "HSBCSGS", - "Handelsbanken", - "Handtool", - "Heathrow", - "HiBob", - "Highfive", - "Highlightable", - "Hoverable", - "Humpty", - "Hydronics", - "IBTA", - "IDEIN", - "IHDR", - "INTECOMS", - "IPHONEOS", - "ITIN", - "ITSM", - "Idology", - "Inactives", - "Inclusivity", - "innerradius", - "Intacct", - "Invoicify", - "Italiano", - "Jakub", - "KHTML", - "Kantonalbank", - "Kearny", - "Kolkata", - "Kort", - "Kowalski", - "Krasoń", - "LDFLAGS", - "LHNGBR", - "LIBCPP", - "LIGHTBOXES", - "LLDB", - "Lagertha", - "Limpich", - "Lothbrok", - "Luhn", - "MARKASRESOLVED", - "MCTEST", - "MMYY", - "MVCP", - "MYOB", - "Maat", - "Mahal", - "Mapbox", - "Marcin", - "Marqeta", - "McAfee", - "Michelina", - "Melvin", - "MelvinBot", - "Menlo", - "Microtransaction", - "Miniwarehouses", - "Mobasher", - "Monzo", - "Multifactor", - "NAICS", - "NANP", - "NBSA", - "NETSUITE", - "NEWDOT", - "NEWEXPENSIFY", - "NGROK", - "NLRA", - "NMLS", - "NOSCCATT", - "NSQS", - "NSQSOAuth", - "NSURL", - "NSUTF8", - "NTSB", - "Nacha", - "Namecheap", - "Nanobiotechnology", - "Navan", - "Nederlands", - "Nesw", - "Neue", - "Nonchocolate", - "Noncitrus", - "Nondepository", - "Nonfinancial", - "Nonmortgage", - "Nonnull", - "Nonstore", - "Nonupholstered", - "Noto", - "Novobanco", - "Nuevo", - "OCBC", - "OLDDOT", - "ONYXKEY", - "ONYXKEYS", - "Oncorp", - "PINATM", - "PINATM", - "PINGPONG", - "PKCE", - "POLICYCHANGELOG_ADD_EMPLOYEE", - "POWERFORM", - "Parcelable", - "Passwordless", - "Payoneer", - "Pekao", - "Perfetto", - "Pettinella", - "Picklist", - "Playroll", - "Pleo", - "Pluginfile", - "Podfile", - "Pokdumss", - "Polska", - "Polski", - "Português", - "Postale", - "Postharvest", - "Postproduction", - "Powerform", - "Precheck", - "Pressable", - "Pressables", - "Proofpoint", - "Priya", - "Protip", - "Précédent", - "QAPR", - "QUICKBOOKS", - "Qonto", - "RAAS", - "RBC", - "RCTI18nUtil", - "RCTIs", - "RCTURL", - "REBOOKED", - "REDIRECTURI", - "REIMBURSER", - "REJECTEDTOSUBMITTER", - "REJECTEDTRANSACTION", - "REPORTPREVIEW", - "RNCORE", - "RNFS", - "RNLinksdk", - "RNTL", - "RNVP", - "ROYCCAT2", - "RPID", - "RRGGBB", - "RTER", - "Ragnar", - "Raiffeisen", - "Rankable", - "Reauthenticator", - "Rebooked", - "Reimb", - "Reimbursability", - "Reimbursables", - "Reimbursments", - "Renderable", - "Resawing", - "Reupholstery", - "Revolut", - "Roni", - "Rosiclair", - "SAASPASS", - "SBFJ", - "SCANREADY", - "SCIM", - "SMARTREPORT", - "SONIFICATION", - "SSAE", - "STORYLANE", - "SVFG", - "SVGID", - "Salagatan", - "Saqbd", - "Scaleway", - "Scaleway's", - "Schengen", - "Schiffli", - "Scotiabank", - "Segoe", - "Selec", - "Sepa", - "Sharees", - "Sharons", - "Signup", - "Skydo", - "Slurper", - "Smartscan", - "Société", - "Speedscope", - "Spendesk", - "Splittable", - "Spotnana", - "Strikethrough", - "Subprocessors", - "Subviews", - "Supercenters", - "Svenska", - "Svmy", - "Swedbank", - "Swipeable", - "Symbolicates", - "Synovus", - "TBUM", - "TDOMCATTTOR", - "TIMATIC", - "TOTP", - "TQBQW", - "Talkspace", - "Tele", - "Teleproduction", - "Timothée", - "Touchless", - "Trainline", - "Transpiles", - "Typeform", - "UATP", - "UBOI", - "UBOS", - "UDID", - "UDIDS", - "UIBG", - "UKEU", - "UNSWIPEABLE", - "UPWORK", - "USAA", - "USCA", - "USDVBBA", - "Unassigning", - "Uncapitalize", - "Undelete", - "Unlaminated", - "Unmigrated", - "Unsharing", - "Unvalidated", - "VBBA", - "VMPD", - "Valuska", - "Venmo", - "WDYR", - "Wallester", - "Warchoł", - "Wintrust", - "Woohoo", - "Wooo", - "Wordmark", - "XNOR", - "XYWH", - "Xfermode", - "Xours", - "Xtheirs", - "YAPL", - "YYMM", - "Yapl", - "Yema", - "Zenefit", - "Zenefits", - "Zipaligning", - "Zürcher", "abytes", + "Accelo", "accountid", "achreimburse", "actool", "adbd", + "ADDCOMMENT", + "Addendums", + "ADFS", "aeiou", + "Aeroplan", + "Aircall", + "Airplus", "airshipconfig", "airside", + "Airwallex", "alrt", + "Amal", + "Amal's", "americanexpress", "americanexpressfd", "americanexpressfdx", - "bankofamerica", "Amina", + "Amina", + "AMRO", "androiddebugkey", "androidx", + "APCA", "apksigner", "apktool", + "APPL", "applauseauto", "applauseleads", "appleauth", @@ -444,84 +48,151 @@ "approvalstatus", "appversion", "archivado", + "Areport", + "ARGB", "armeabi", "armv7", - "artículo", - "artículos", + "ARNK", + "ARROWDOWN", + "ARROWLEFT", + "ARROWRIGHT", + "ARROWUP", + "art\u00edculo", + "art\u00edculos", "as_siteseach", "asar", + "ASPAC", "assetlinks", "attributes.accountid", "attributes.reportid", "authorised", + "Authy", + "AUTOAPPROVE", "autocompletions", "autocorrection", "autodocs", "autofilled", "automations", "autoplay", + "AUTOREIMBURSED", "autoreleasepool", + "AUTOREPORTING", "autoresizesSubviews", "autoresizing", "autosync", "avds", + "AVURL", "backgrounded", + "BambooHr", "bamboohr", + "Bancorporation", + "bankofamerica", + "Banque", + "Bartek", "barwidth", "basehead", "baselined", + "Batchinator", + "BBVA", "beforeunload", "behaviour", + "Belfius", "bigdecimal", + "BILLCOM", + "Billpay", "billpay", "blahblahblah", "blakeembrey", "blankrows", + "BMO", + "BNDCCAMM", + "BNDL", + "Bobbeth", "bofa", + "BOFMCAM2", "bolditalic", "bootsplash", + "Borderless", + "Botify", "brex", "bridgeless", + "Broadwoven", + "Bronn", + "BROWSABLE", + "Buildscript", "buildscript", + "Bunq", + "Bushwick", + "BYOC", "cacerts", + "Caixa", "canvaskit", "capitalone", + "CARDDEACTIVATED", + "CARDFROZEN", "cardreader", + "CARDUNFROZEN", + "CAROOT", + "Carta", "catmull", "ccache", + "CCDQCAMM", "ccupload", "cdfbmo", + "Certinia", + "Certinia's", + "CFPB", "changeit", "chargeback", + "Charleson", + "Checkmark", "checkmarked", "chien", + "Chronos", + "CIBC", + "CIBCCATT", "citi", + "claudecodeex", + "claudedesktop", "clawback", "cleartext", + "CLIA", + "CLIENTID", "clippath", + "Cliqbook", "cloudflarestream", "cmaps", "cmjs", "cocoapods", + "Codat", "codegen", "codeshare", "codesign", + "Codice", "colorscale", + "Combustors", "commentbubbles", "contenteditable", "copiloted", "copiloting", "copyable", "cornerradius", + "Corpay", + "Countertop", + "CPPFLAGS", "cpuprofile", "creditamount", "creditcards", + "CREDS", "crios", + "CROSSLINK", + "Cr\u00e9dit", "csvexport", "csvg", "customairshipextender", "customfield", "customise", + "Danske", "dateexported", "deapex", "deapexer", @@ -529,6 +200,7 @@ "deburr", "deburred", "dedupe", + "Deel", "deeplink", "deeplinked", "deeplinking", @@ -538,28 +210,47 @@ "dependentaxis", "deployers", "deprioritizes", + "Dep\u00f3sitos", "describedby", + "Desjardins", + "Deutsch", "devportal", + "DFOLLY", "diems", "dimen", "directfeeds", + "Dishoom", "displaystatus", + "DocuSign", "domainpadding", "domelementtype", "domhandler", "domparser", "dont", "dotlottie", + "Drycleaners", + "Drycleaning", + "DSYM", "dsyms", + "Dtype", + "Dumpty", "durationMillis", + "DYNAMICEXTERNAL", "e2edelta", "ecash", "ecconnrefused", "econn", + "EDDSA", + "EDIFACT", "effectful", + "Egencia", + "Electromedical", "electronmon", + "Electrotherapeutic", "ellipsize", + "EMEA", "emojibase", + "Emphemeral", "endcapture", "enddate", "endfor", @@ -567,9 +258,19 @@ "enroute", "entertainm", "entityid", + "Entra", + "ENVFILE", + "Ephermeral", "eraa", + "ERECEIPT", + "ERECEIPTS", + "errorbar", + "Erste", + "Espa\u00f1ol", + "ESTA", "ethnicities", "eticket", + "EUVAT", "evenodd", "eventmachine", "evictable", @@ -577,24 +278,43 @@ "exchrate", "exfy", "exitstatus", + "Expatriot", + "Expensable", "expensescount", + "Expensi", + "Expensicon", + "Expensicons", "expensicorp", + "EXPENSIDEV", + "Expensidev", + "Expensifier", + "EXPENSIFYAPI", "expensifyhelp", "expensifylite", "expensifymono", "expensifyneue", "expensifynewkansas", + "EXPENSIFYPDBUSINESS", + "EXPENSIFYPDTEAM", "expensifyreactnative", + "EXPENSIFYWEB", + "Expensiworks", "fabs", "falso", "favicons", + "Fbclid", "feedcountry", + "Ferroalloy", + "FinancialForce", "firebaselogging", "fireroom", "firstname", + "Fiscale", "flac", + "Flamegraph", "flatlist", "flexsearch", + "FLJZ", "fname", "fnames", "focusability", @@ -602,62 +322,106 @@ "fontawesome", "foreignamount", "formatjs", + "Fran\u00e7ais", + "Frederico", "freetext", "frontpart", "fullstory", + "FWTV", + "FXHF", + "F\u00e1bio", + "Gaber", "gastos", + "GBRRBR", + "Gclid", "gcsc", "gcse", + "GDSO", "genkey", + "GEOLOCATION", + "Geral", "getenv", "getprop", "gibsdk", + "gitlink", "glcode", "googleusercontent", "gorhom", "gpgsign", "gradlew", + "Grantmaking", "groupmonth", "groupweek", "gscb", "gsib", "gsst", - "gödecke", + "Gsuite", + "G\u00e9n\u00e9rale", + "g\u00f6decke", + "Handelsbanken", + "Handtool", "hanno", - "hanno_gödecke", + "hanno_g\u00f6decke", "headshot", "healthcheck", + "Heathrow", "helpdot", "helpsite", "hexcode", + "HiBob", "hibob", + "Highfive", + "Highlightable", + "HKBCCATT", + "Hoverable", "hrefs", "hris", + "HRMS", + "HSBCSGS", + "Humpty", "hybridapp", - "iOSQRCode", + "Hydronics", "iaco", + "IBTA", + "IDEIN", "idempotently", "idfa", + "Idology", "ifdef", + "IHDR", "imagebutton", + "Inactives", "inbetweenCompo", + "Inclusivity", "initialises", + "innerradius", "inputmethod", "instancetype", + "Intacct", + "INTECOMS", "intenthandler", + "Invoicify", "ionatan", + "iOSQRCode", + "IPHONEOS", "iphonesimulator", "isemojisonly", "islarge", "ismedium", "isnonreimbursable", "issmall", + "Italiano", + "ITIN", + "ITSM", + "Jakub", "jank", "janky", "jarsigner", "johndoe", - "jsSrcsDir", "jsbundle", + "jsSrcsDir", + "Kantonalbank", + "Kearny", "keyalg", "keycap", "keycommand", @@ -667,52 +431,85 @@ "keytool", "keyval", "keyvaluepairs", + "KHTML", "killall", "kilometre", "kilometres", + "knip", + "Kolkata", + "Kort", + "Kowalski", + "Kraso\u0144", "labelcomponent", "labelindicator", "labelindicatorinneroffset", "labelindicatorouteroffset", "labelledby", "labelradius", + "Lagertha", "laggy", "lastiPhoneLogin", "lastname", + "LDFLAGS", + "LHNGBR", + "LIBCPP", "libexec", "licence", + "LIGHTBOXES", "lightningcss", + "Limpich", "linecap", - "linejoin", "lineheight", + "linejoin", "lintable", "lintrk", "listformat", "liveupdate", + "LLDB", "locationbias", "logcat", "logomark", + "Lothbrok", + "lottiefiles", "lucene", + "Luhn", + "Maat", + "Mahal", "maildrop", "manualreimburse", + "Mapbox", "mapboxgl", "marcaaron", + "Marcin", "margelo", + "MARKASRESOLVED", + "Marqeta", "mateusz", + "McAfee", "mchmod", + "MCTEST", "mechler", "mediumitalic", + "Melvin", + "MelvinBot", "memberof", + "Menlo", "metainfo", "metatags", + "Michelina", "microtime", + "Microtransaction", "microtransactions", "midoffice", "mimecast", + "Miniwarehouses", "miterlimit", "mkcert", + "MMYY", + "Mobasher", "mobiexpensifyg", "mobileprovision", + "Monzo", "moveElemsAttrsToGroup", "moveGroupAttrsToElems", "mple", @@ -720,40 +517,80 @@ "msword", "mtrl", "multidex", + "Multifactor", + "MVCP", + "MYOB", "mysubdomain", + "Nacha", + "NAICS", + "Namecheap", + "Nanobiotechnology", + "NANP", + "Navan", "navattic", "navigations", + "NBSA", "nbta", "ndkversion", + "Nederlands", "negsign", + "Nesw", "nesw", "netinfo", "netrc", + "NETSUITE", + "Neue", "newarch", + "NEWDOT", "newdotreport", + "NEWEXPENSIFY", "newhelp", "ngneat", + "NGROK", + "NitroFetch", + "NLRA", "nmanager", + "NMLS", "nocreeps", "nodownload", + "Nonchocolate", + "Noncitrus", + "Nondepository", + "Nonfinancial", + "Nonmortgage", + "Nonnull", "nonreimbursable", + "Nonstore", + "Nonupholstered", "noopener", "noprompt", "noreferer", "noreferrer", + "NOSCCATT", "nosymbol", + "Noto", + "Novobanco", + "NSQS", + "NSQSOAuth", + "NSURL", + "NSUTF8", "ntag", "ntdiary", + "NTSB", + "Nuevo", "nullptr", "numberformat", "nums", "objc", "objdump", "oblador", + "OCBC", "octocat", "officedocument", "oldarch", + "OLDDOT", "onclosetag", + "Oncorp", "oneline", "oneteam", "oneui", @@ -763,6 +600,8 @@ "onplayerror", "ontext", "onxy", + "ONYXKEY", + "ONYXKEYS", "openxmlformats", "ordinality", "organisation", @@ -773,42 +612,87 @@ "outplant", "padangle", "parasharrajat", + "Parcelable", "passcodes", "passplus", + "Passwordless", "passwordless", "pathspec", + "Payoneer", "payrollcode", "payrollid", "pbxproj", "pdfreport", "pdfs", + "Pekao", + "Perfetto", "persistable", "personaltrackcase", "personaltrackgoal", "peterparker", + "Pettinella", "pgrep", "phonenumber", + "Picklist", "picklists", + "PINATM", + "PINATM", + "PINGPONG", + "PKCE", "pkill", + "Playroll", + "Pleo", + "Pluginfile", "pluralrules", "pnrs", + "Podfile", "podspec", "podspecs", + "Pokdumss", + "POLICYCHANGELOG_ADD_EMPLOYEE", + "Polska", + "Polski", "popen3", + "Portugu\u00eas", "positionMillis", + "Postale", + "Postharvest", + "Postproduction", + "POWERFORM", + "Powerform", "preauthorization", + "Precheck", "precheck", + "Prefetch", + "Prefetcher", "prescribers", "presentationml", + "Pressable", + "Pressables", "prettierrc", + "Priya", "progname", "proguard", + "Proofpoint", + "Protip", + "Pr\u00e9c\u00e9dent", "purchaseamount", "purchasecurrency", "QAPR", + "QAPR", "QBWC", + "Qonto", "qrcode", + "QUICKBOOKS", + "RAAS", "rach", + "Ragnar", + "Raiffeisen", + "Rankable", + "RBC", + "RCTI18nUtil", + "RCTIs", + "RCTURL", "reactnative", "reactnativebackgroundtask", "reactnativehybridapp", @@ -816,66 +700,123 @@ "reannounce", "reauthenticating", "reauthentication", + "Reauthenticator", + "REBOOKED", + "Rebooked", "rebooking", "recategorize", "receiptless", "recents", "recyclerview", + "REDIRECTURI", "regexpu", "reimagination", + "Reimb", + "Reimbursability", "reimbursability", + "Reimbursables", "reimbursementid", + "REIMBURSER", "reimbursible", + "Reimbursments", + "REJECTEDTOSUBMITTER", + "REJECTEDTRANSACTION", "remotedesktop", "remotesync", "removeHiddenElems", + "Renderable", + "REPORTPREVIEW", "requestee", + "Resawing", "resizeable", "resultsbox", "retryable", + "Reupholstery", + "Revolut", "rideshare", "Rightworks", "RNCORE", + "RNCORE", "RNFS", + "RNFS", + "RNLinksdk", "RNLinksdk", "rnmapbox", + "RNTL", + "RNVP", "rock", + "Roni", + "Rosiclair", + "ROYCCAT2", "rpartition", + "RPID", + "RRGGBB", "rstrip", + "RTER", "s3uqn2oe4m85tufi6mqflbfbuajrm2i3", + "SAASPASS", "safesearch", + "Salagatan", "samltool", + "Saqbd", "sbaiahmed", + "SBFJ", + "Scaleway", + "Scaleway's", + "SCANREADY", "schedulable", + "Schengen", + "Schiffli", + "SCIM", + "Scotiabank", "scriptname", "sdkmanager", "seamless", + "Segoe", "seguiemj", + "Selec", + "Sepa", "serveo", "setuptools", "sharee", "shareeEmail", + "Sharees", "sharees", + "Sharons", "shellcheck", "shellenv", "shiftedlinesegment", "shipit", "shouldshowellipsis", + "sideproject", "signingkey", + "Signup", "signup", "Signup", - "sideproject", "simctl", "skia", "skip_codesigning", + "Skydo", + "Slurper", + "SMARTREPORT", + "Smartscan", + "Soci\u00e9t\u00e9", "soloader", + "SONIFICATION", + "Speedscope", + "Spendesk", + "Splittable", + "Spotnana", "spreadsheetml", "srgb", + "SSAE", "stackoverflow", "startdate", "stdev", "stdlib", "storepass", + "STORYLANE", + "Strikethrough", "strikethrough", "subcomponents", "subfolders", @@ -883,6 +824,7 @@ "sublocality", "subpages", "subpremise", + "Subprocessors", "subprocessors", "subrates", "substep", @@ -890,17 +832,32 @@ "subtab", "subtabs", "subview", + "Subviews", "superapp", + "Supercenters", "superpowered", "supportal", + "Svenska", + "SVFG", + "SVGID", "svgs", + "Svmy", + "Swedbank", + "Swipeable", "symbolicate", "symbolicated", + "Symbolicates", "symbolication", "symbolspacer", + "Synovus", "systempreferences", "tabindex", + "Talkspace", + "TBUM", + "TDOMCATTTOR", "teachersunite", + "Tele", + "Teleproduction", "testflight", "textanchor", "threadsafe", @@ -908,47 +865,77 @@ "tickcount", "tickformat", "tickvalues", + "TIMATIC", + "Timoth\u00e9e", "tnode", "tobe", "togglefullscreen", "tosorted", + "TOTP", "touchables", + "Touchless", + "TQBQW", + "Trainline", "tranid", + "Transpiles", "trinet", "trivago", "trustcacerts", "tsgo", "twocards", + "Typeform", + "UATP", "uatp", + "UBOI", + "UBOS", + "UDID", + "UDIDS", + "UIBG", "uimanager", + "UKEU", "ukkonen", "unapprove", "unapproves", + "Unassigning", "unassigning", "unassignment", "unassigns", + "Uncapitalize", "uncategorized", + "Undelete", "unflushed", "unheld", "unhold", "uninstallations", + "Unlaminated", + "Unmigrated", "unminified", "unmuting", "unredacted", "unregisters", "unreimbursed", "unscrollable", + "Unsharing", "unsharing", "unsubmitted", + "UNSWIPEABLE", + "Unvalidated", + "UPWORK", "urbanairship", "urlset", + "USAA", + "USCA", + "USDVBBA", "useAutolayout", + "useblacksmith", "useCallback", "useMemo", - "useblacksmith", "usernotifications", "utilise", + "Valuska", + "VBBA", "vcpu", + "Venmo", "verticalanchor", "victoryaxis", "victorybar", @@ -961,12 +948,16 @@ "viewability", "viewport", "viewports", + "VMPD", "voidings", "vorbis", "vvcf", "vypj", "waitlist", "waitlisted", + "Wallester", + "Warcho\u0142", + "WDYR", "webapps", "webauthn", "webcredentials", @@ -974,6 +965,10 @@ "welldone", "wellsfargo", "widgetkit", + "Wintrust", + "Woohoo", + "Wooo", + "Wordmark", "wordprocessingml", "worklet", "workletization", @@ -994,26 +989,32 @@ "xcworkspace", "xdescribe", "xero", + "Xfermode", "xlarge", "xlink", "xmlgateway", + "XNOR", + "Xours", + "Xtheirs", + "XYWH", "yalc", + "YAPL", + "Yapl", + "Yema", "yourcompany", "yourname", + "YYMM", "zencdn", + "Zenefit", + "Zenefits", "zipalign", + "Zipaligning", "zoneinfo", "zxcv", "zxldvw", - "águero", - "مثال", - "claudedesktop", - "claudecodeex", - "NitroFetch", - "Prefetch", - "Prefetcher", - "knip", - "lottiefiles" + "Z\u00fcrcher", + "\u00e1guero", + "\u0645\u062b\u0627\u0644" ], "ignorePaths": [ ".gitignore", @@ -1080,6 +1081,8 @@ "config/eslint/eslint.seatbelt.tsv", "tests/unit/BankAccountUtilsTest.ts" ], - "ignoreRegExpList": ["@assets/.*"], + "ignoreRegExpList": [ + "@assets/.*" + ], "useGitignore": true -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 53e08c2902a0..1706da1a842e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -151,6 +151,7 @@ "@actions/core": "1.10.0", "@actions/github": "5.1.1", "@babel/core": "^7.25.2", + "@babel/generator": "^7.28.0", "@babel/parser": "^7.22.16", "@babel/plugin-proposal-class-properties": "^7.12.1", "@babel/plugin-proposal-private-methods": "^7.18.6", @@ -249,6 +250,7 @@ "eslint-plugin-testing-library": "^7.11.0", "eslint-plugin-you-dont-need-lodash-underscore": "^6.14.0", "eslint-seatbelt": "^0.1.3", + "get-tsconfig": "^4.10.0", "glob": "^10.4.5", "googleapis": "^152.0.0", "html-webpack-plugin": "^5.5.0", @@ -268,6 +270,7 @@ "onchange": "^7.1.0", "openai": "^6.16.0", "oxc-loader": "^0.0.2", + "oxc-transform": "0.136.0", "patch-package": "^8.1.0-canary.1", "peggy": "^4.0.3", "portfinder": "^1.0.34", diff --git a/package.json b/package.json index b340ff245a15..32ffe1b197b4 100644 --- a/package.json +++ b/package.json @@ -225,6 +225,7 @@ "@actions/core": "1.10.0", "@actions/github": "5.1.1", "@babel/core": "^7.25.2", + "@babel/generator": "^7.28.0", "@babel/parser": "^7.22.16", "@babel/plugin-proposal-class-properties": "^7.12.1", "@babel/plugin-proposal-private-methods": "^7.18.6", @@ -323,6 +324,7 @@ "eslint-plugin-testing-library": "^7.11.0", "eslint-plugin-you-dont-need-lodash-underscore": "^6.14.0", "eslint-seatbelt": "^0.1.3", + "get-tsconfig": "^4.10.0", "glob": "^10.4.5", "googleapis": "^152.0.0", "html-webpack-plugin": "^5.5.0", @@ -342,6 +344,7 @@ "onchange": "^7.1.0", "openai": "^6.16.0", "oxc-loader": "^0.0.2", + "oxc-transform": "0.136.0", "patch-package": "^8.1.0-canary.1", "peggy": "^4.0.3", "portfinder": "^1.0.34", @@ -376,7 +379,6 @@ }, "overrides": { "mapbox-gl": "^3.24.0", - "oxc-transform": "0.136.0", "braces": "3.0.3", "yargs": "17.7.2", "yargs-parser": "21.1.1", @@ -428,6 +430,9 @@ }, "@sentry/react-native": { "expo": "55.0.25" + }, + "oxc-loader": { + "oxc-transform": "0.136.0" } }, "expo": { From 57db725d53f17ca9bf4f81f17f33ffbfb8c433ec Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 4 Jul 2026 18:47:21 -0700 Subject: [PATCH 04/31] chore: bump babel-plugin-react-compiler to 20260507 experimental Co-authored-by: Cursor --- package-lock.json | 6 ++++-- package.json | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d08ed4a1c2ac..944a12655aa4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -224,7 +224,7 @@ "babel-jest": "29.7.0", "babel-loader": "^10.0.0", "babel-plugin-module-resolver": "^5.0.0", - "babel-plugin-react-compiler": "^0.0.0-experimental-a1856f3-20260422", + "babel-plugin-react-compiler": "^0.0.0-experimental-a1856f3-20260507", "babel-plugin-transform-remove-console": "^6.9.4", "bun": "^1.3.14", "clean-webpack-plugin": "^4.0.0", @@ -16147,7 +16147,9 @@ } }, "node_modules/babel-plugin-react-compiler": { - "version": "0.0.0-experimental-a1856f3-20260422", + "version": "0.0.0-experimental-a1856f3-20260507", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-a1856f3-20260507.tgz", + "integrity": "sha512-NH/o9ojGrQ5xQhkLry8KulKssfWuM68myEGVUpIJhExt1/WVWBOTBPSPGiSRoJtfB9yP8Kj4UXZpLXKFCXineg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 9df7d1d44128..5c32324f8e7e 100644 --- a/package.json +++ b/package.json @@ -298,7 +298,7 @@ "babel-jest": "29.7.0", "babel-loader": "^10.0.0", "babel-plugin-module-resolver": "^5.0.0", - "babel-plugin-react-compiler": "^0.0.0-experimental-a1856f3-20260422", + "babel-plugin-react-compiler": "^0.0.0-experimental-a1856f3-20260507", "babel-plugin-transform-remove-console": "^6.9.4", "bun": "^1.3.14", "clean-webpack-plugin": "^4.0.0", @@ -399,6 +399,7 @@ "path-to-regexp": "0.1.10", "send": "0.19.0", "regexpu-core": "6.4.0", + "babel-plugin-react-compiler": "0.0.0-experimental-a1856f3-20260507", "react": "19.2.0", "react-dom": "19.2.0", "eslint-config-expensify": { From 4aa1fee85e146b22275f585b4fc54888ddd406f6 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 4 Jul 2026 19:10:12 -0700 Subject: [PATCH 05/31] fix(ci): pin babel-plugin-react-compiler to fix npm ci override conflict npm ci fails when a direct dependency uses a semver range (^) but overrides pins an exact version. Pin the devDependency to match the override so setupNode can install dependencies in CI. Co-authored-by: Cursor --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 944a12655aa4..599f2035be5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -224,7 +224,7 @@ "babel-jest": "29.7.0", "babel-loader": "^10.0.0", "babel-plugin-module-resolver": "^5.0.0", - "babel-plugin-react-compiler": "^0.0.0-experimental-a1856f3-20260507", + "babel-plugin-react-compiler": "0.0.0-experimental-a1856f3-20260507", "babel-plugin-transform-remove-console": "^6.9.4", "bun": "^1.3.14", "clean-webpack-plugin": "^4.0.0", diff --git a/package.json b/package.json index 5c32324f8e7e..db0009bdde35 100644 --- a/package.json +++ b/package.json @@ -298,7 +298,7 @@ "babel-jest": "29.7.0", "babel-loader": "^10.0.0", "babel-plugin-module-resolver": "^5.0.0", - "babel-plugin-react-compiler": "^0.0.0-experimental-a1856f3-20260507", + "babel-plugin-react-compiler": "0.0.0-experimental-a1856f3-20260507", "babel-plugin-transform-remove-console": "^6.9.4", "bun": "^1.3.14", "clean-webpack-plugin": "^4.0.0", From 79cc5777f448faf671f06b67277fbb25272eb3c6 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 4 Jul 2026 19:49:52 -0700 Subject: [PATCH 06/31] Fix CI: Storybook webpack, knip lockfile, spellcheck Storybook was still looking for a top-level babel-loader rule; push the OXC transform pipeline rules instead. Rename webpack loaders to .cjs to avoid the new-JS-files typecheck gate. Restore knip's cross-platform oxc-parser optional deps in package-lock.json. Co-authored-by: Cursor --- .storybook/webpack.config.ts | 21 +- ...der.js => fullstory-annotation-loader.cjs} | 0 ...oader.js => oxc-react-compiler-loader.cjs} | 0 config/webpack/webpack.common.ts | 4 +- cspell.json | 4 + package-lock.json | 306 ++++++++++++++++++ 6 files changed, 329 insertions(+), 6 deletions(-) rename config/webpack/{fullstory-annotation-loader.js => fullstory-annotation-loader.cjs} (100%) rename config/webpack/{oxc-react-compiler-loader.js => oxc-react-compiler-loader.cjs} (100%) diff --git a/.storybook/webpack.config.ts b/.storybook/webpack.config.ts index ba307df1b4a8..72cab02d44b6 100644 --- a/.storybook/webpack.config.ts +++ b/.storybook/webpack.config.ts @@ -80,10 +80,23 @@ const webpackConfig = async ({config}: {config: Configuration}) => { config.resolve.extensions = custom.resolve.extensions; config.resolve.fallback = custom.resolve.fallback; - const babelRulesIndex = custom.module.rules.findIndex((rule) => rule.loader === 'babel-loader'); - const babelRule = custom.module.rules.at(babelRulesIndex); - if (babelRulesIndex !== -1 && babelRule) { - config.module.rules?.push(babelRule); + const hasOxcTransformPipeline = (rule: RuleSetRule): boolean => { + if (!rule.use || !Array.isArray(rule.use)) { + return false; + } + + return rule.use.some((entry) => { + const loader = typeof entry === 'string' ? entry : entry?.loader; + return typeof loader === 'string' && (loader.includes('oxc-react-compiler-loader') || loader === 'oxc-loader'); + }); + }; + + for (const rule of custom.module.rules) { + if (typeof rule === 'string' || typeof rule === 'boolean' || typeof rule === 'number' || !hasOxcTransformPipeline(rule)) { + continue; + } + + config.module.rules?.push(rule); } const fileLoaderRule = config.module.rules?.find( diff --git a/config/webpack/fullstory-annotation-loader.js b/config/webpack/fullstory-annotation-loader.cjs similarity index 100% rename from config/webpack/fullstory-annotation-loader.js rename to config/webpack/fullstory-annotation-loader.cjs diff --git a/config/webpack/oxc-react-compiler-loader.js b/config/webpack/oxc-react-compiler-loader.cjs similarity index 100% rename from config/webpack/oxc-react-compiler-loader.js rename to config/webpack/oxc-react-compiler-loader.cjs diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index 3af699cd9b92..73d5aa527e48 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -347,7 +347,7 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): // non-fatal React Compiler diagnostics to webpack warnings instead of // hard build errors (workaround for oxc-project/oxc#23587). { - loader: path.resolve(dirname, './oxc-react-compiler-loader.js'), + loader: path.resolve(dirname, './oxc-react-compiler-loader.cjs'), options: { reactCompiler: {target: '19', panicThreshold: 'none'}, target: 'node20', @@ -356,7 +356,7 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): }, // Pass 1: Fullstory annotation (sees annotated JSX before OXC transforms it) { - loader: path.resolve(dirname, './fullstory-annotation-loader.js'), + loader: path.resolve(dirname, './fullstory-annotation-loader.cjs'), }, ], }, diff --git a/cspell.json b/cspell.json index 1c90161601bd..22d7bd20e685 100644 --- a/cspell.json +++ b/cspell.json @@ -346,6 +346,7 @@ "Scotiabank", "Segoe", "Selec", + "serialises", "Sepa", "Sharees", "Sharons", @@ -561,6 +562,7 @@ "domparser", "dont", "dotlottie", + "downleveling", "dsyms", "durationMillis", "e2edelta", @@ -932,6 +934,7 @@ "taxrate", "teachersunite", "testflight", + "testfairy", "textanchor", "threadsafe", "thumbsup", @@ -996,6 +999,7 @@ "viewports", "voidings", "vorbis", + "voronoi", "vvcf", "vypj", "waitlist", diff --git a/package-lock.json b/package-lock.json index 599f2035be5d..58847542ce79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4448,6 +4448,40 @@ "node": ">=0.8.0" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@es-joy/jsdoccomment": { "version": "0.65.2", "dev": true, @@ -8890,6 +8924,25 @@ "url": "https://github.com/sponsors/Brooooooklyn" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@native-html/css-processor": { "version": "1.11.0", "license": "MIT", @@ -14073,6 +14126,17 @@ "url": "https://opencollective.com/turf" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "dev": true, @@ -26878,8 +26942,27 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/knip/node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.130.0.tgz", + "integrity": "sha512-oFWFJrsGv9siFM4HjMqKNB7IuIZD/SMmZdCXl8xyx7lDplGvPKyewpOo272rSWgMXe2Wx7bWI0Yj+gkHv4qbeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/knip/node_modules/@oxc-parser/binding-darwin-arm64": { "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.130.0.tgz", + "integrity": "sha512-sGUzupdTplK9jQg7eJZ878HfEgQjJNBc6dAYVWJ9W5aU+J8rLfRJhTVsKThiu1pNwm6Y1qKCcbC6WhNWSXR3Ig==", "cpu": [ "arm64" ], @@ -26893,6 +26976,229 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/knip/node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.130.0.tgz", + "integrity": "sha512-PsB4cdCISbC00Uy8eiD8bc2AkGWjZqrSrJnkBFuG2ptrrf6mZ2F5gLFSjOAVMMgZPg8B1D7OydJwLWSfyI2Plg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.130.0.tgz", + "integrity": "sha512-DgABp3l38hS77JbXCV4qk1+n6DPym5u8zzwuweokezm2tX194nDSJDENbDRECxVsiNbprKATLbk+Z5wlHT0OHw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.130.0.tgz", + "integrity": "sha512-4Kn3CTEmwFrzhTSC/JuUW16qovmaMdX7jeSKbL8w0pLtLww7To1a2XJi9Z5uD8QWUkfUHhqfV+VD6dVzBnWzoA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.130.0.tgz", + "integrity": "sha512-D35KZM3F4rRu1uAFKyBlg3Gaf/ybCjyaPR1hfgvk5ex8NtcTmRgc0JgSighEyNg96TPrFhemFba68SZuxaha8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.130.0.tgz", + "integrity": "sha512-Q9o7oVlo955KHwS8l1u0bCzIx+JsZUA3XToLXC+MsMhye/9LeBQbt84nh120cl2XLy+TEzvugYDiHShg5yaX6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.130.0.tgz", + "integrity": "sha512-EiJ/gC0ljbcwVpycC8YWw6ggMbtsPX8XMOt0mPx0aqWeMsNR+L9m05Flbvd5T+GlivG+GkSWQL7tM9SRFpM/dw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.130.0.tgz", + "integrity": "sha512-O19Cil83XAyjEFfo8WhkMwY58ALqZ7ckjGL+25mjMIuF84urWBeANH0FC8B8BsSSygWU3/1aY3ADdDbp+wlBnw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.130.0.tgz", + "integrity": "sha512-6tJz0xvnGhsokE7N1WlUSBXibpYmT9xSJFS1Ce41Km/+8gQvdlW8MLhRv8PD0L7ix8vRG0FDDepp3jdOFzdVdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.130.0.tgz", + "integrity": "sha512-9aCWj83dp3heTQGmGnZGdIWgxjZrr/7VQ0TGFHH5PKByxJKF2Hcr4qvaSUHhhGEa3MSsDjTL1YDP8RAgdL5/Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.130.0.tgz", + "integrity": "sha512-afXt87aZBqrUVli8TB/I8H1G50RDWcwirjWtXGXYqJ2ZqWEiErH7V72j3LUSDZaivmtu2OLX0KQ/mbhP81mr7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.130.0.tgz", + "integrity": "sha512-sJgQkGaBX0WJvPUDfwciex6IcTk5O5NLQ1bhEb6f3nBruh1GshKMRSMt2bxZlYrgBzjyBbJzsnO+InPG0bg+fA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.130.0.tgz", + "integrity": "sha512-bjcma99sQrNh6RY4mPO9yTkfxql6TDFoN3HWdK31RCKXwNhcDgJXW/l8PUtzKNiQ+9vpKJfJtQq+LklBuxSOBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.130.0.tgz", + "integrity": "sha512-RBpA9TsRucJq6HNVNCFF1iKg+QeTkLdZf7hi4xaOGCPvMZWvDHjQgSOEZMUpuW4JNciHbxNhLEYmz5CVygjVGQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/knip/node_modules/@oxc-project/types": { "version": "0.130.0", "dev": true, From dc55007afc86521649b2a4d81c0e834fe7e5770c Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 10 Jul 2026 17:28:11 -0700 Subject: [PATCH 07/31] fix: exclude OXC-handled files from Rsbuild's default SWC loader; fall back cleanly on React Compiler bailouts Two correctness bugs surfaced while re-verifying this branch after rebasing onto the merged Rsbuild migration (#95319): 1. Rsbuild's default 'js' rule (builtin:swc-loader) ran on every .js/.ts/.jsx/.tsx file, including ones already handled by Rule A/B/B2 below, and ran *before* them in the loader chain -- silently stripping JSX/TS before the Fullstory annotation loader or OXC's React Compiler ever saw the original source. Added a `tools.bundlerChain` hook to exclude app source and `includedNodeModules` from the default rule, and forwarded it from `getSharedConfiguration` into `getCommonConfiguration` (which otherwise replaces rather than merges `tools`). 2. oxc-transform returns empty `code` (not just a diagnostic) when React Compiler hits an Error-severity Rules-of-React violation (e.g. accessing `ref.current` inline during render, as in useDebouncedState). The loader was demoting these to warnings but still shipping the empty output, silently breaking every component that imported the affected module (e.g. ThemeProvider crashed on first render in dev). Now retries the transform with `reactCompiler: false` when this happens, matching babel-plugin-react-compiler's default bailout behavior. Also updates knip.json (glob + ignoreDependencies) and regenerates package-lock.json for the merged package.json, both of which were already made locally but not staged before the merge commit. Co-authored-by: Cursor --- config/rsbuild/oxc-react-compiler-loader.cjs | 11 +- config/rsbuild/rsbuild.common.ts | 20 + knip.json | 7 +- package-lock.json | 517 ++++++++++++++++--- 4 files changed, 466 insertions(+), 89 deletions(-) diff --git a/config/rsbuild/oxc-react-compiler-loader.cjs b/config/rsbuild/oxc-react-compiler-loader.cjs index fea24612d218..fd3d8612964c 100644 --- a/config/rsbuild/oxc-react-compiler-loader.cjs +++ b/config/rsbuild/oxc-react-compiler-loader.cjs @@ -88,7 +88,7 @@ module.exports = async function oxcReactCompilerLoader(source) { cwd: this.rootContext, }; - const result = await transform(resourcePath, source, transformOptions); + let result = await transform(resourcePath, source, transformOptions); // Demote React Compiler diagnostics to webpack warnings instead of // hard errors (workaround for oxc-project/oxc#23587). @@ -105,6 +105,15 @@ module.exports = async function oxcReactCompilerLoader(source) { return; } + // An Error-severity React Compiler diagnostic (e.g. a genuine Rules-of-React + // violation like accessing a ref during render) makes oxc-transform bail out + // of the whole transform and return empty code, not just skip the optimization + // pass. Re-run without the compiler to fall back to plain JSX/TS transform output, + // matching babel-plugin-react-compiler's default "skip this component" bailout. + if (!result.code && rcErrors.length > 0) { + result = await transform(resourcePath, source, {...transformOptions, reactCompiler: false}); + } + if (sourceMaps && result.map) { callback(null, result.code, result.map); } else { diff --git a/config/rsbuild/rsbuild.common.ts b/config/rsbuild/rsbuild.common.ts index 9a0099a10ba7..2dee1d1de476 100644 --- a/config/rsbuild/rsbuild.common.ts +++ b/config/rsbuild/rsbuild.common.ts @@ -167,6 +167,22 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => }), ], tools: { + // Rsbuild's default 'js' rule (builtin:swc-loader, its 'js' oneOf branch) matches every + // .js/.ts/.jsx/.tsx file, including everything Rule A/B/B2 below already handle. Rules + // declared later in `module.rules` run their loaders FIRST (closest to raw source), and + // our custom rules are added via `addRules` (which appends after Rsbuild's defaults), so + // without this exclude, swc-loader would strip JSX/TypeScript before the Fullstory + // annotation loader or OXC's React Compiler ever see the original JSX — silently + // defeating both. Only the 'js' oneOf branch is touched, so worker/raw/text imports + // (other oneOf branches on the same parent rule) are unaffected. + bundlerChain: (chain) => { + chain.module + .rule('js') + .oneOf('js') + .exclude.add((resourcePath: string) => !resourcePath.includes('node_modules')) + .add(includedNodeModules) + .end(); + }, rspack: (config, {addRules}) => { // canvaskit-wasm and expo's getBundleUrl.web.ts reference __filename/__dirname, which don't // exist in a browser bundle. Rspack's default ('warn-mock') mocks them to a fixed value but @@ -431,6 +447,10 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): }, }, tools: { + // getSharedConfiguration's own `tools.bundlerChain` (JSX/TS rule excludes) isn't otherwise + // inherited here since the explicit `tools: {...}` below replaces (rather than merges with) + // `...shared`'s tools object. + bundlerChain: shared.tools?.bundlerChain, rspack: (config, utils) => { // `sharedRspackTool`'s declared return type includes `Promise` because // that's a valid shape for `tools.rspack` in general, but `getSharedConfiguration`'s own diff --git a/knip.json b/knip.json index 4ce56e311fb9..067e8034de24 100644 --- a/knip.json +++ b/knip.json @@ -8,7 +8,7 @@ "src/HybridAppHandler.tsx", "scripts/**/*.{js,ts}", "web/proxy.ts", - "config/rsbuild/**/*.{js,mjs,ts}", + "config/rsbuild/**/*.{js,mjs,cjs,ts}", ".github/scripts/**/*.ts", ".github/actions/javascript/**/*.ts", ".storybook/**/*.{js,ts,tsx}", @@ -70,7 +70,10 @@ "bun", "shellcheck", "patch-package", - "diff-so-fancy" + "diff-so-fancy", + "babel-loader", + "oxc-loader", + "@babel/plugin-proposal-class-properties" ], "ignoreBinaries": ["metro-symbolicate", "mkcert"] } diff --git a/package-lock.json b/package-lock.json index ca2667209c0e..661f5f0cfb2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -155,17 +155,12 @@ "@actions/core": "1.10.0", "@actions/github": "5.1.1", "@babel/core": "^7.25.2", + "@babel/generator": "^7.28.0", "@babel/parser": "^7.22.16", "@babel/plugin-proposal-class-properties": "^7.12.1", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", - "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-export-namespace-from": "^7.27.1", - "@babel/preset-env": "^7.25.3", - "@babel/preset-flow": "^7.12.13", - "@babel/preset-react": "^7.10.4", - "@babel/preset-typescript": "^7.21.5", "@babel/traverse": "^7.22.20", "@babel/types": "^7.22.19", "@callstack/reassure-compare": "^1.0.0-rc.4", @@ -189,7 +184,6 @@ "@rock-js/plugin-metro": "0.13.4", "@rock-js/provider-s3": "0.13.4", "@rsbuild/core": "^2.1.4", - "@rsbuild/plugin-babel": "^2.0.1", "@rsbuild/plugin-svgr": "^2.0.5", "@rsdoctor/rspack-plugin": "^1.5.17", "@rspack/core": "^2.1.2", @@ -229,9 +223,9 @@ "@vitest/mocker": "4.0.16", "@welldone-software/why-did-you-render": "7.0.1", "babel-jest": "29.7.0", + "babel-loader": "^10.0.0", "babel-plugin-module-resolver": "^5.0.0", - "babel-plugin-react-compiler": "0.0.0-experimental-a1856f3-20260422", - "babel-plugin-react-native-web": "^0.18.7", + "babel-plugin-react-compiler": "0.0.0-experimental-a1856f3-20260507", "babel-plugin-transform-remove-console": "^6.9.4", "babel-preset-expo": "^56.0.15", "bun": "^1.3.14", @@ -253,6 +247,7 @@ "eslint-plugin-testing-library": "^7.11.0", "eslint-plugin-you-dont-need-lodash-underscore": "^6.14.0", "eslint-seatbelt": "^0.1.3", + "get-tsconfig": "^4.10.0", "glob": "^10.4.5", "googleapis": "^152.0.0", "http-server": "^14.1.1", @@ -270,6 +265,8 @@ "nitrogen": "0.35.0", "onchange": "^7.1.0", "openai": "^6.16.0", + "oxc-loader": "^0.0.2", + "oxc-transform": "0.136.0", "oxfmt": "^0.55.0", "patch-package": "^8.1.0-canary.1", "peggy": "^4.0.3", @@ -2277,21 +2274,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-proposal-private-methods": { "version": "7.18.6", "dev": true, @@ -2411,17 +2393,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-flow": { "version": "7.26.0", "license": "MIT", @@ -3619,25 +3590,6 @@ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/preset-react": { - "version": "7.23.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-transform-react-display-name": "^7.23.3", - "@babel/plugin-transform-react-jsx": "^7.22.15", - "@babel/plugin-transform-react-jsx-development": "^7.22.5", - "@babel/plugin-transform-react-pure-annotations": "^7.23.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/preset-typescript": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", @@ -12180,6 +12132,382 @@ "win32" ] }, + "node_modules/@oxc-transform/binding-android-arm-eabi": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-android-arm-eabi/-/binding-android-arm-eabi-0.136.0.tgz", + "integrity": "sha512-zWyz4qFxPXplAgPMTr02oIAuN/8/DbONjj8/xYp2r6n6N2wnWWZuEAMNMixu+DJuA0BcMmAaHlIhjKAgyfFuXw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-android-arm64": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-android-arm64/-/binding-android-arm64-0.136.0.tgz", + "integrity": "sha512-WyR+ZOAHaMsGSANAeluwfTEL+1u4mvWYtW3FANKROgMxwJeASZzU0zHtH7Cmms0ORbp+0SVUViNQ4Hht4XbkAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-darwin-arm64": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-darwin-arm64/-/binding-darwin-arm64-0.136.0.tgz", + "integrity": "sha512-NPWct7Cft+Ekm7/qIwfnKwCqWY72nb/l1Mm2Izozry5wRRjBs+dyMB8Z+m6iQSVfQRIWsu3jy45Of6Zef32K9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-darwin-x64": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-darwin-x64/-/binding-darwin-x64-0.136.0.tgz", + "integrity": "sha512-i+6lFZR070hG7+BfNUZS9sBfgf1t+NLYWS6IquoXyoV+QDAiCOf/UDPDqOjkKDgQQmGZ3qWzL+WEeH1GlYMO+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-freebsd-x64": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-freebsd-x64/-/binding-freebsd-x64-0.136.0.tgz", + "integrity": "sha512-fPgYtBata14S53LeuhowbBYNIJ3SJwk1Aw3ear+j7F9gLpiWIkL4e8gOma8SCgOLtTOMbYdOyKk13KDFAqoMGQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-arm-gnueabihf": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.136.0.tgz", + "integrity": "sha512-irYpUBJnMxQr62MHWe1y3Oefb4FSF9+/ZiO6efMmh6FVPYlbh6bcQi/t+eG2KORMsf9YLt3qh5dmdfpQbiAIWA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-arm-musleabihf": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.136.0.tgz", + "integrity": "sha512-luUqj3eHnT5GyfK88O0HIXcnnURAn62KvcONEBs7zNje/At5Vides2Rx5NuT1X/cvSWLqR8yknBz2UMwVQeqyw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-arm64-gnu": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.136.0.tgz", + "integrity": "sha512-WdxFWJAE3PvJMrekaKYzmx2Abr6YVeJGOjyMI1iTiSeMJdazXKqH5sWR7td4BWTQ1ZSkd2FptuhDPVhVGElfYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-arm64-musl": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.136.0.tgz", + "integrity": "sha512-rFHkHUQIz/KgAQDZgiFGFj2OQiL+csw+tDI5aCrFY3v9RTUiQRkaxXcjGgV6gMhdEd81357vw6K3xucVmRP+cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-ppc64-gnu": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.136.0.tgz", + "integrity": "sha512-B86BlWTVD68V3T78/gp17etSPvjLWVN6WJDexZzMzOP92hzVdK8c6KT8mL8a+UY3RlSUwsquVfxtHv2Z1tYLtw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-riscv64-gnu": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.136.0.tgz", + "integrity": "sha512-lIVizI3eTCPuk9NBFYaHArxoJ0/LC1e4hipcIateeofUNOEXqehfkVwrMI07k4DAW/2ya/ZnUVgaLY+x4wg+NA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-riscv64-musl": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.136.0.tgz", + "integrity": "sha512-80igJtLGGWYp5qK3pS3/jAqD/m4jre0Fwu3YXHyHSIj197os9qIy3pn4NN35rogE4pVV4/mcx4SmqB3DsG51bA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-s390x-gnu": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.136.0.tgz", + "integrity": "sha512-SNAHfIhq8TjaxiTV1jFZwFReP49E9nwOalEcIh5xs4O6UAiy8I6QYuve2vTuJrvKNOkIse6RotUzywirkoV6Pg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-x64-gnu": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.136.0.tgz", + "integrity": "sha512-HXMySHa9hcPsYf2byqUEKixrsBakGvYWpA9I3r7R00Zs2OJu7foPsVIYfQeP8jhvNpactigBpptWvXM3E64Jcw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-linux-x64-musl": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-x64-musl/-/binding-linux-x64-musl-0.136.0.tgz", + "integrity": "sha512-d2NQ3HMV6cltpJpJ6y9ENY37+6CQcY6/tuPLY1BzGmsdVVvajwu7uIbyWz56GZDiPf2pow+j2qDSCb0xy6VyvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-openharmony-arm64": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-openharmony-arm64/-/binding-openharmony-arm64-0.136.0.tgz", + "integrity": "sha512-ZDDZvFWEIhAo+BXeoAcF+kswaHA2j6DgYmnsVxXgoKaJp5LpMMbK8stqX80KJbydpT+ik02nvy5XMSSfY9LTbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-wasm32-wasi": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-wasm32-wasi/-/binding-wasm32-wasi-0.136.0.tgz", + "integrity": "sha512-EFNYyWFmj4wF7K7+c9DsPQCTAFfiTVNasJ4X0ZuwjPNQUcV161z5YXeZJepgqDicqdmWCsRqJP4NPsngiWWJRw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.5" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-transform/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-transform/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-transform/binding-win32-arm64-msvc": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.136.0.tgz", + "integrity": "sha512-jyRyFVci/T6hMtCIPCCkW/ZFYhK989hXFy27aLUnyvdJYAaMlq2h7lahh/MkTNv0ll5hokXWwQ3Hwak8EeSXoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-win32-ia32-msvc": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.136.0.tgz", + "integrity": "sha512-jdmNkL0+vLYvaSVVtbs39eaVHDqPFRSTWCksC7hADlZWlKlp93fE291scodjNMIOObC/ABOc5jf95JY3/qJzSg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-transform/binding-win32-x64-msvc": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/@oxc-transform/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.136.0.tgz", + "integrity": "sha512-cPmcOHoAyfYxH0OKYP54fiu8SJudF9RBoI9QFcnBttEOSdmapOU+dDO7pvuvcbUY5rLZMek8OZi2r9fA/jwZ4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@oxfmt/binding-android-arm-eabi": { "version": "0.55.0", "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.55.0.tgz", @@ -13943,30 +14271,6 @@ } } }, - "node_modules/@rsbuild/plugin-babel": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@rsbuild/plugin-babel/-/plugin-babel-2.0.1.tgz", - "integrity": "sha512-PkKWwQDm6Ll3emj9vrq4DYpob9XYonsgkuo4o9lY3ySNmK18+bKUCzPQkqrsMaJi+h1wDwFH/XnMGlIRS//dCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.29.7", - "@babel/plugin-proposal-decorators": "^7.29.7", - "@babel/plugin-transform-class-properties": "^7.29.7", - "@babel/preset-typescript": "^7.29.7", - "@types/babel__core": "^7.20.5", - "babel-loader": "10.1.1", - "reduce-configs": "^1.1.2" - }, - "peerDependencies": { - "@rsbuild/core": "^2.0.0" - }, - "peerDependenciesMeta": { - "@rsbuild/core": { - "optional": true - } - } - }, "node_modules/@rsbuild/plugin-check-syntax": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@rsbuild/plugin-check-syntax/-/plugin-check-syntax-1.6.1.tgz", @@ -20328,19 +20632,14 @@ } }, "node_modules/babel-plugin-react-compiler": { - "version": "0.0.0-experimental-a1856f3-20260422", - "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-a1856f3-20260422.tgz", - "integrity": "sha512-rYwQFX52TaZk2lJZNLp2V9jun1sSJZEBAW5S4Rjt8mnOjEtz5Cab/7yjG4ZLBWuofgWiqSb6laPVkhBpYd0n4w==", + "version": "0.0.0-experimental-a1856f3-20260507", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-a1856f3-20260507.tgz", + "integrity": "sha512-NH/o9ojGrQ5xQhkLry8KulKssfWuM68myEGVUpIJhExt1/WVWBOTBPSPGiSRoJtfB9yP8Kj4UXZpLXKFCXineg==", "license": "MIT", "dependencies": { "@babel/types": "^7.26.0" } }, - "node_modules/babel-plugin-react-native-web": { - "version": "0.18.12", - "dev": true, - "license": "MIT" - }, "node_modules/babel-plugin-syntax-hermes-parser": { "version": "0.33.3", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.33.3.tgz", @@ -33890,6 +34189,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oxc-loader": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/oxc-loader/-/oxc-loader-0.0.2.tgz", + "integrity": "sha512-jY7SexUNGXjw5tbJurNk53hdc2JYtL3fm/FJUCjPVgvN4lhNom+rXlipVoKu4o2mo8qi5n0SHFtTsvXikbLL0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-tsconfig": "^4.13.6", + "oxc-transform": "^0.121.0" + } + }, "node_modules/oxc-resolver": { "version": "11.19.1", "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.19.1.tgz", @@ -33922,6 +34232,41 @@ "@oxc-resolver/binding-win32-x64-msvc": "11.19.1" } }, + "node_modules/oxc-transform": { + "version": "0.136.0", + "resolved": "https://registry.npmjs.org/oxc-transform/-/oxc-transform-0.136.0.tgz", + "integrity": "sha512-7mVjRVgUAFl2OKCQMFjWmfrdx5Hcr20VQBLHm/SS/a/JJalal8gRVH2AoPymp9h5efJjB3REukK662aWJk4MsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-transform/binding-android-arm-eabi": "0.136.0", + "@oxc-transform/binding-android-arm64": "0.136.0", + "@oxc-transform/binding-darwin-arm64": "0.136.0", + "@oxc-transform/binding-darwin-x64": "0.136.0", + "@oxc-transform/binding-freebsd-x64": "0.136.0", + "@oxc-transform/binding-linux-arm-gnueabihf": "0.136.0", + "@oxc-transform/binding-linux-arm-musleabihf": "0.136.0", + "@oxc-transform/binding-linux-arm64-gnu": "0.136.0", + "@oxc-transform/binding-linux-arm64-musl": "0.136.0", + "@oxc-transform/binding-linux-ppc64-gnu": "0.136.0", + "@oxc-transform/binding-linux-riscv64-gnu": "0.136.0", + "@oxc-transform/binding-linux-riscv64-musl": "0.136.0", + "@oxc-transform/binding-linux-s390x-gnu": "0.136.0", + "@oxc-transform/binding-linux-x64-gnu": "0.136.0", + "@oxc-transform/binding-linux-x64-musl": "0.136.0", + "@oxc-transform/binding-openharmony-arm64": "0.136.0", + "@oxc-transform/binding-wasm32-wasi": "0.136.0", + "@oxc-transform/binding-win32-arm64-msvc": "0.136.0", + "@oxc-transform/binding-win32-ia32-msvc": "0.136.0", + "@oxc-transform/binding-win32-x64-msvc": "0.136.0" + } + }, "node_modules/oxfmt": { "version": "0.55.0", "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.55.0.tgz", From 14880be260b49f18bc5a83cc14d343f81c595aea Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 10 Jul 2026 18:03:46 -0700 Subject: [PATCH 08/31] fix: use official oxc-webpack-loader instead of community oxc-loader oxc-loader is an unofficial community package; oxc-webpack-loader is maintained by the oxc-project itself and is a drop-in replacement with the same TransformOptions passthrough. Also drops the hacky require.resolve indirection into oxc-loader's nested node_modules for resolving oxc-transform, since oxc-transform is already a pinned top-level dependency. Co-authored-by: Cursor --- config/rsbuild/oxc-react-compiler-loader.cjs | 13 +++-------- config/rsbuild/rsbuild.common.ts | 4 ++-- knip.json | 2 +- package-lock.json | 24 ++++++++++---------- package.json | 4 ++-- 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/config/rsbuild/oxc-react-compiler-loader.cjs b/config/rsbuild/oxc-react-compiler-loader.cjs index fd3d8612964c..4ab7bf9923e0 100644 --- a/config/rsbuild/oxc-react-compiler-loader.cjs +++ b/config/rsbuild/oxc-react-compiler-loader.cjs @@ -5,20 +5,13 @@ * release), which caused the build to fail instead of bailing out on components * that violate the Rules of React — the same behaviour as babel-plugin-react-compiler. * - * Options mirror oxc-loader's TransformOptions plus `reactCompiler`. - * This loader replaces oxc-loader entirely for Rule A (app source). + * Options mirror oxc-webpack-loader's TransformOptions plus `reactCompiler`. + * This loader replaces oxc-webpack-loader entirely for Rule A (app source). */ const path = require('path'); const {getTsconfig} = require('get-tsconfig'); - -// Resolve the oxc-transform binary that oxc-loader itself uses (its nested copy) -// so we stay on the same native binary version as oxc-loader. -const oxcTransformPath = require.resolve('oxc-transform', { - paths: [path.resolve(__dirname, '../../node_modules/oxc-loader')], -}); - -const {transform} = require(oxcTransformPath); +const {transform} = require('oxc-transform'); function extractTsconfigOptions(rootContext) { try { diff --git a/config/rsbuild/rsbuild.common.ts b/config/rsbuild/rsbuild.common.ts index 2dee1d1de476..6766f1492d9a 100644 --- a/config/rsbuild/rsbuild.common.ts +++ b/config/rsbuild/rsbuild.common.ts @@ -290,7 +290,7 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => plugins: ['react-native-worklets/plugin'], }, }, - {loader: 'oxc-loader', options: {target: 'node20'}}, + {loader: 'oxc-webpack-loader', options: {target: 'node20'}}, ], }, // Rule B2: JavaScript — need explicit jsx to upgrade .js lang to jsx. @@ -308,7 +308,7 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => }, }, { - loader: 'oxc-loader', + loader: 'oxc-webpack-loader', options: {target: 'node20', jsx: {runtime: 'automatic'}}, }, ], diff --git a/knip.json b/knip.json index 067e8034de24..a3a6224ab31c 100644 --- a/knip.json +++ b/knip.json @@ -72,7 +72,7 @@ "patch-package", "diff-so-fancy", "babel-loader", - "oxc-loader", + "oxc-webpack-loader", "@babel/plugin-proposal-class-properties" ], "ignoreBinaries": ["metro-symbolicate", "mkcert"] diff --git a/package-lock.json b/package-lock.json index 661f5f0cfb2b..61c17653853d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -265,8 +265,8 @@ "nitrogen": "0.35.0", "onchange": "^7.1.0", "openai": "^6.16.0", - "oxc-loader": "^0.0.2", "oxc-transform": "0.136.0", + "oxc-webpack-loader": "^1.0.2", "oxfmt": "^0.55.0", "patch-package": "^8.1.0-canary.1", "peggy": "^4.0.3", @@ -34189,17 +34189,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/oxc-loader": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/oxc-loader/-/oxc-loader-0.0.2.tgz", - "integrity": "sha512-jY7SexUNGXjw5tbJurNk53hdc2JYtL3fm/FJUCjPVgvN4lhNom+rXlipVoKu4o2mo8qi5n0SHFtTsvXikbLL0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-tsconfig": "^4.13.6", - "oxc-transform": "^0.121.0" - } - }, "node_modules/oxc-resolver": { "version": "11.19.1", "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.19.1.tgz", @@ -34267,6 +34256,17 @@ "@oxc-transform/binding-win32-x64-msvc": "0.136.0" } }, + "node_modules/oxc-webpack-loader": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/oxc-webpack-loader/-/oxc-webpack-loader-1.0.2.tgz", + "integrity": "sha512-rLdHfhM4Ir8psAYVIjYpGDjkTqk+SqsUWTRClA9/DnPjZQtzauCHITXzT6KcNAU8OdpHW9U+LYIeigWe2+7LkQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "oxc-transform": "^0.115.0 || ^0.131.0 || ^0.132.0 || ^0.133.0 || ^0.134.0 || ^0.135.0 || ^0.138.0", + "webpack": ">=5" + } + }, "node_modules/oxfmt": { "version": "0.55.0", "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.55.0.tgz", diff --git a/package.json b/package.json index 300e91120b67..b6f80c2f5ca0 100644 --- a/package.json +++ b/package.json @@ -338,8 +338,8 @@ "nitrogen": "0.35.0", "onchange": "^7.1.0", "openai": "^6.16.0", - "oxc-loader": "^0.0.2", "oxc-transform": "0.136.0", + "oxc-webpack-loader": "^1.0.2", "oxfmt": "^0.55.0", "patch-package": "^8.1.0-canary.1", "peggy": "^4.0.3", @@ -420,7 +420,7 @@ "@sentry/react-native": { "expo": "56.0.8" }, - "oxc-loader": { + "oxc-webpack-loader": { "oxc-transform": "0.136.0" } }, From a1a0f47dd557a1f99aae096c0e70f61932c31fa5 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 10 Jul 2026 18:20:24 -0700 Subject: [PATCH 09/31] fix: allowlist oxc-react-compiler-loader warnings in Storybook smoke test Storybook's --smoke-test treats any webpack/rspack build warning not on its small internal allowlist as a hard failure. oxc-react-compiler-loader deliberately demotes non-fatal React Compiler diagnostics to warnings (matching babel-plugin-react-compiler's default bailout behaviour), which was tripping the CI Storybook tests job. Add them to rsbuild's own ignoreWarnings list, same mechanism already used for the lottie-react-native warning above. Co-authored-by: Cursor --- config/rsbuild/rsbuild.common.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/config/rsbuild/rsbuild.common.ts b/config/rsbuild/rsbuild.common.ts index 6766f1492d9a..6d37e54dac3c 100644 --- a/config/rsbuild/rsbuild.common.ts +++ b/config/rsbuild/rsbuild.common.ts @@ -195,9 +195,12 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => __dirname: 'mock', }; // We can ignore the "module not installed" warning from lottie-react-native because we - // are not using the library for JSON format of Lottie animations. + // are not using the library for JSON format of Lottie animations. We also ignore + // oxc-react-compiler-loader's demoted React Compiler diagnostics (see that file) — + // they're deliberately warnings, not errors, matching babel-plugin-react-compiler's + // default bailout behaviour, so Storybook's `--smoke-test` shouldn't fail the build on them. // eslint-disable-next-line no-param-reassign - config.ignoreWarnings = [...(config.ignoreWarnings ?? []), /lottie-react-native\/lib\/module\/LottieView\/index\.web\.js/]; + config.ignoreWarnings = [...(config.ignoreWarnings ?? []), /lottie-react-native\/lib\/module\/LottieView\/index\.web\.js/, /oxc-react-compiler-loader:/]; // eslint-disable-next-line no-param-reassign config.resolve ??= {}; From 3d8c8f30edcc9933e458e1935170c3fa28e2257a Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 10 Jul 2026 21:55:55 -0700 Subject: [PATCH 10/31] refactor: convert custom Rspack loaders from CJS to ESM Rspack natively supports ESM loader modules, so there's no reason for oxc-react-compiler-loader and fullstory-annotation-loader to stay on require()/module.exports. Also disables no-invalid-this (loaders get `this` from the bundler, not a class/object) and the no-negated-variables false positive on "annotation" (matches the "notation" substring) for these loader files now that ESM sourceType makes ESLint newly aware of their top-level function declarations. Co-authored-by: Cursor --- config/eslint/eslint.config.mjs | 14 ++++++++++++++ ...der.cjs => fullstory-annotation-loader.mjs} | 18 ++++++++++++------ ...oader.cjs => oxc-react-compiler-loader.mjs} | 10 +++++----- config/rsbuild/rsbuild.common.ts | 4 ++-- 4 files changed, 33 insertions(+), 13 deletions(-) rename config/rsbuild/{fullstory-annotation-loader.cjs => fullstory-annotation-loader.mjs} (94%) rename config/rsbuild/{oxc-react-compiler-loader.cjs => oxc-react-compiler-loader.mjs} (95%) diff --git a/config/eslint/eslint.config.mjs b/config/eslint/eslint.config.mjs index 01e5e3fc2a64..913a963f06d3 100644 --- a/config/eslint/eslint.config.mjs +++ b/config/eslint/eslint.config.mjs @@ -534,6 +534,20 @@ const config = defineConfig([ }, }, + // Rspack/webpack loader modules (ESM). Loaders receive their `this` loader + // context from the bundler, not from a class/object — the ESM sourceType + // makes `no-invalid-this` newly aware of these top-level function + // declarations, which is a false positive for this well-established + // loader convention. `no-negated-variables` also false-positives on + // "annotation" (matches the "notation" substring), which isn't a negation. + { + files: ['config/rsbuild/*-loader.mjs'], + rules: { + 'no-invalid-this': 'off', + 'rulesdir/no-negated-variables': 'off', + }, + }, + { files: ['**/en.ts', '**/es.ts'], rules: { diff --git a/config/rsbuild/fullstory-annotation-loader.cjs b/config/rsbuild/fullstory-annotation-loader.mjs similarity index 94% rename from config/rsbuild/fullstory-annotation-loader.cjs rename to config/rsbuild/fullstory-annotation-loader.mjs index dcb7ed6cfca0..398277057e50 100644 --- a/config/rsbuild/fullstory-annotation-loader.cjs +++ b/config/rsbuild/fullstory-annotation-loader.mjs @@ -15,10 +15,16 @@ * Supports `native: true` mode only (matches the existing webpack config). */ -const parser = require('@babel/parser'); -const traverse = require('@babel/traverse').default; -const generate = require('@babel/generator').default; -const t = require('@babel/types'); +import generateModule from '@babel/generator'; +import * as parser from '@babel/parser'; +import traverseModule from '@babel/traverse'; +import * as t from '@babel/types'; + +// @babel/traverse and @babel/generator are CJS-only, so their ESM default +// export is the whole CJS module.exports object rather than the function +// itself — same interop wrinkle as require(...).default in CommonJS. +const traverse = traverseModule.default ?? traverseModule; +const generate = generateModule.default ?? generateModule; // Modules known to be incompatible with Fullstory annotation (from upstream plugin) const KNOWN_INCOMPATIBLE = [ @@ -262,7 +268,7 @@ function applyPropsToComponent(funcPath, componentName, sourceFileName) { applyPropsToJSXNode(jsxPath, componentName, sourceFileName); } -module.exports = function fullstoryAnnotationLoader(source) { +export default function fullstoryAnnotationLoader(source) { const resourcePath = this.resourcePath; // Skip known-incompatible node_modules @@ -358,4 +364,4 @@ module.exports = function fullstoryAnnotationLoader(source) { } return code; -}; +} diff --git a/config/rsbuild/oxc-react-compiler-loader.cjs b/config/rsbuild/oxc-react-compiler-loader.mjs similarity index 95% rename from config/rsbuild/oxc-react-compiler-loader.cjs rename to config/rsbuild/oxc-react-compiler-loader.mjs index 4ab7bf9923e0..8ba3e05e0d08 100644 --- a/config/rsbuild/oxc-react-compiler-loader.cjs +++ b/config/rsbuild/oxc-react-compiler-loader.mjs @@ -9,9 +9,9 @@ * This loader replaces oxc-webpack-loader entirely for Rule A (app source). */ -const path = require('path'); -const {getTsconfig} = require('get-tsconfig'); -const {transform} = require('oxc-transform'); +import {getTsconfig} from 'get-tsconfig'; +import path from 'node:path'; +import {transform} from 'oxc-transform'; function extractTsconfigOptions(rootContext) { try { @@ -60,7 +60,7 @@ function getLang(ext) { return 'js'; } -module.exports = async function oxcReactCompilerLoader(source) { +export default async function oxcReactCompilerLoader(source) { const callback = this.async(); try { const options = this.getOptions() || {}; @@ -115,4 +115,4 @@ module.exports = async function oxcReactCompilerLoader(source) { } catch (err) { callback(err); } -}; +} diff --git a/config/rsbuild/rsbuild.common.ts b/config/rsbuild/rsbuild.common.ts index 6d37e54dac3c..be5a82e422b7 100644 --- a/config/rsbuild/rsbuild.common.ts +++ b/config/rsbuild/rsbuild.common.ts @@ -266,7 +266,7 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => // non-fatal React Compiler diagnostics to warnings instead of hard build // errors (workaround for oxc-project/oxc#23587). { - loader: path.resolve(dirname, './oxc-react-compiler-loader.cjs'), + loader: path.resolve(dirname, './oxc-react-compiler-loader.mjs'), options: { reactCompiler: {target: '19', panicThreshold: 'none'}, target: 'node20', @@ -275,7 +275,7 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => }, // Pass 1: Fullstory annotation (sees annotated JSX before OXC transforms it). { - loader: path.resolve(dirname, './fullstory-annotation-loader.cjs'), + loader: path.resolve(dirname, './fullstory-annotation-loader.mjs'), }, ], }, From 7fd9ae519dcc8555f870902348b2cb5aec15b763 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 10 Jul 2026 22:04:44 -0700 Subject: [PATCH 11/31] docs: clarify why babel.config.js's web config is still needed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous comment said this config "is no longer read by the web build," which reads as the whole file being dead. It's only the Rsbuild/OXC bundler pipeline that stopped reading it — ESLint's @babel/eslint-parser still loads babel.config.js for every file it lints (its caller name is never 'metro'/'babel-jest'), so the `web` branch stays load-bearing for resolving parser syntax plugins. Co-authored-by: Cursor --- babel.config.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/babel.config.js b/babel.config.js index 860ca57e11c8..fd993ba7aa7c 100644 --- a/babel.config.js +++ b/babel.config.js @@ -27,14 +27,17 @@ function traceTransformer() { }; } -// This config is no longer read by the web build. Rsbuild's JS/TS/JSX pipeline (see -// config/rsbuild/rsbuild.common.ts) uses OXC directly with inline loader options -// (configFile:false), bypassing this file entirely. Kept here for tooling compatibility -// (e.g. IDE plugins that load babel.config.js without setting a caller name). -// The presets/plugins that previously lived here (@babel/preset-react, @babel/preset-env, -// @babel/preset-flow, @babel/preset-typescript, babel-plugin-react-native-web, and several -// class-property/export-namespace transforms) have been removed from devDependencies because -// OXC now handles those transforms natively in the web build. +// Rsbuild's web build no longer reads this: its JS/TS/JSX pipeline (see +// config/rsbuild/rsbuild.common.ts) calls OXC directly with configFile:false, bypassing +// this file entirely. This `web` config object is still read, though — ESLint's +// @babel/eslint-parser loads babel.config.js for every file it lints (caller name is +// always undefined or '@babel/eslint-parser', never 'metro'/'babel-jest', so it always +// falls into this branch below) to resolve syntax plugins like `exportNamespaceFrom` +// needed to parse the same syntax the web bundle uses. The presets/plugins that +// previously lived here (@babel/preset-react, @babel/preset-env, @babel/preset-flow, +// @babel/preset-typescript, babel-plugin-react-native-web, and several class-property +// transforms) have been removed from devDependencies because OXC now handles those +// transforms natively in the web build and ESLint doesn't need them for parsing. const web = { presets: [], plugins: [ From b8638db1ca57c22a22a0a63fe42c13e818f3e11a Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 10 Jul 2026 22:10:37 -0700 Subject: [PATCH 12/31] docs: remove stale reference to removed babel presets Naming the specific presets/plugins that used to live in this branch before OXC took over web transforms violates our no-stale-code-references convention. Describe the current constraint (what ESLint's parser still needs) instead of narrating what was removed from devDependencies. Co-authored-by: Cursor --- babel.config.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/babel.config.js b/babel.config.js index fd993ba7aa7c..127b7bca4aa5 100644 --- a/babel.config.js +++ b/babel.config.js @@ -33,11 +33,9 @@ function traceTransformer() { // @babel/eslint-parser loads babel.config.js for every file it lints (caller name is // always undefined or '@babel/eslint-parser', never 'metro'/'babel-jest', so it always // falls into this branch below) to resolve syntax plugins like `exportNamespaceFrom` -// needed to parse the same syntax the web bundle uses. The presets/plugins that -// previously lived here (@babel/preset-react, @babel/preset-env, @babel/preset-flow, -// @babel/preset-typescript, babel-plugin-react-native-web, and several class-property -// transforms) have been removed from devDependencies because OXC now handles those -// transforms natively in the web build and ESLint doesn't need them for parsing. +// needed to parse the same syntax the web bundle uses. OXC handles JSX/TypeScript/env-target +// transforms natively for the web build, so this branch only lists the plugins ESLint's +// parser needs for syntax it wouldn't otherwise recognise. const web = { presets: [], plugins: [ From 6d1c11b2e9bcf6f84e4c21fddc8af331acaef721 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 10 Jul 2026 23:41:49 -0700 Subject: [PATCH 13/31] fix: recognise -> recognize (British spelling flagged by spellcheck) Reapplying after the branch got reset to drop an unrelated commit that had briefly landed here; this fix was lost along with it. Co-authored-by: Cursor --- babel.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/babel.config.js b/babel.config.js index 127b7bca4aa5..c33f11eb1d2b 100644 --- a/babel.config.js +++ b/babel.config.js @@ -35,7 +35,7 @@ function traceTransformer() { // falls into this branch below) to resolve syntax plugins like `exportNamespaceFrom` // needed to parse the same syntax the web bundle uses. OXC handles JSX/TypeScript/env-target // transforms natively for the web build, so this branch only lists the plugins ESLint's -// parser needs for syntax it wouldn't otherwise recognise. +// parser needs for syntax it wouldn't otherwise recognize. const web = { presets: [], plugins: [ From 5c82c7cf44bb652e8a7bc46290060f11604e2dcd Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 11 Jul 2026 00:06:13 -0700 Subject: [PATCH 14/31] perf: skip Babel worklets pass for files that cannot contain a worklet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit babel-loader ran a full Babel parse/transform/codegen cycle on every single app source file for react-native-worklets/plugin, even though only ~2.5% of src/ files reference react-native-reanimated, react-native-worklets, or a 'worklet' directive at all (verified by grep) — the rest paid the cost for a no-op pass, including 500KB+ translation files that triggered Babel's codegen deopt warnings. Replace babel-loader with a thin worklets-loader that checks source for those substrings first and only invokes @babel/core when a match is possible, skipping straight through otherwise. Drop babel-loader as a dependency now that nothing uses it. Co-authored-by: Cursor --- config/rsbuild/rsbuild.common.ts | 33 +++++------------------ config/rsbuild/worklets-loader.mjs | 43 ++++++++++++++++++++++++++++++ cspell.json | 1 + knip.json | 1 - package-lock.json | 27 ------------------- package.json | 1 - 6 files changed, 50 insertions(+), 56 deletions(-) create mode 100644 config/rsbuild/worklets-loader.mjs diff --git a/config/rsbuild/rsbuild.common.ts b/config/rsbuild/rsbuild.common.ts index be5a82e422b7..c78aa604fdff 100644 --- a/config/rsbuild/rsbuild.common.ts +++ b/config/rsbuild/rsbuild.common.ts @@ -253,14 +253,10 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => exclude: [/node_modules/, /\.native\.(js|jsx|ts|tsx)$/], use: [ // Pass 3: worklets (on OXC's plain-JS output — no presets needed). - { - loader: 'babel-loader', - options: { - babelrc: false, - configFile: false, - plugins: ['react-native-worklets/plugin'], - }, - }, + // worklets-loader skips the Babel parse/transform/codegen cycle for + // files that can't reference a worklet (see that file) — ~97.5% of + // src/, verified by grep. + {loader: path.resolve(dirname, './worklets-loader.mjs')}, // Pass 2: React Compiler + all transforms via our thin wrapper loader. // oxc-react-compiler-loader calls oxc-transform directly and demotes // non-fatal React Compiler diagnostics to warnings instead of hard build @@ -284,17 +280,7 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => test: /\.tsx?$/, include: [includedNodeModules], exclude: [/\.native\.(ts|tsx)$/], - use: [ - { - loader: 'babel-loader', - options: { - babelrc: false, - configFile: false, - plugins: ['react-native-worklets/plugin'], - }, - }, - {loader: 'oxc-webpack-loader', options: {target: 'node20'}}, - ], + use: [{loader: path.resolve(dirname, './worklets-loader.mjs')}, {loader: 'oxc-webpack-loader', options: {target: 'node20'}}], }, // Rule B2: JavaScript — need explicit jsx to upgrade .js lang to jsx. { @@ -302,14 +288,7 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => include: [includedNodeModules], exclude: [/\.native\.(js|jsx)$/], use: [ - { - loader: 'babel-loader', - options: { - babelrc: false, - configFile: false, - plugins: ['react-native-worklets/plugin'], - }, - }, + {loader: path.resolve(dirname, './worklets-loader.mjs')}, { loader: 'oxc-webpack-loader', options: {target: 'node20', jsx: {runtime: 'automatic'}}, diff --git a/config/rsbuild/worklets-loader.mjs b/config/rsbuild/worklets-loader.mjs new file mode 100644 index 000000000000..793e73f34bc4 --- /dev/null +++ b/config/rsbuild/worklets-loader.mjs @@ -0,0 +1,43 @@ +/** + * Thin replacement for babel-loader + react-native-worklets/plugin that skips the Babel + * parse/transform/codegen cycle entirely for the vast majority of files that can't possibly + * contain a worklet (confirmed by grepping src/: ~2.5% of files match). + * + * react-native-worklets/plugin auto-workletizes call expressions of a fixed set of hook/function + * names (useAnimatedStyle, useDerivedValue, runOnUI, gesture handler builders, etc. — see + * node_modules/react-native-worklets/plugin/index.js's reanimatedFunctionHooks) as well as + * explicit 'worklet' directives. Every one of those hooks is only reachable by importing + * react-native-reanimated or react-native-worklets, and the directive itself is the literal + * string 'worklet', so a file containing none of those substrings cannot be affected by the + * plugin — skipping is safe, not just fast. + */ + +import babel from '@babel/core'; + +const WORKLET_REFERENCE = /react-native-reanimated|react-native-worklets|worklet/i; + +export default function workletsLoader(source) { + if (!WORKLET_REFERENCE.test(source)) { + return source; + } + + const callback = this.async(); + babel.transform( + source, + { + babelrc: false, + configFile: false, + filename: this.resourcePath, + plugins: ['react-native-worklets/plugin'], + sourceMaps: !!this.sourceMap, + }, + (error, result) => { + if (error) { + callback(error); + return; + } + callback(null, result.code, result.map ?? undefined); + }, + ); + return undefined; +} diff --git a/cspell.json b/cspell.json index 115e56d053aa..a71d2384b50c 100644 --- a/cspell.json +++ b/cspell.json @@ -1025,6 +1025,7 @@ "wordprocessingml", "worklet", "workletization", + "workletizes", "worklets", "workshopping", "workspacename", diff --git a/knip.json b/knip.json index a3a6224ab31c..75f2613a5f66 100644 --- a/knip.json +++ b/knip.json @@ -71,7 +71,6 @@ "shellcheck", "patch-package", "diff-so-fancy", - "babel-loader", "oxc-webpack-loader", "@babel/plugin-proposal-class-properties" ], diff --git a/package-lock.json b/package-lock.json index 61c17653853d..708437c5d610 100644 --- a/package-lock.json +++ b/package-lock.json @@ -223,7 +223,6 @@ "@vitest/mocker": "4.0.16", "@welldone-software/why-did-you-render": "7.0.1", "babel-jest": "29.7.0", - "babel-loader": "^10.0.0", "babel-plugin-module-resolver": "^5.0.0", "babel-plugin-react-compiler": "0.0.0-experimental-a1856f3-20260507", "babel-plugin-transform-remove-console": "^6.9.4", @@ -20479,32 +20478,6 @@ "node": ">=8" } }, - "node_modules/babel-loader": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.1.1.tgz", - "integrity": "sha512-JwKSzk2kjIe7mgPK+/lyZ2QAaJcpahNAdM+hgR2HI8D0OJVkdj8Rl6J3kaLYki9pwF7P2iWnD8qVv80Lq1ABtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^5.0.0" - }, - "engines": { - "node": "^18.20.0 || ^20.10.0 || >=22.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0 || ^8.0.0-beta.1", - "@rspack/core": "^1.0.0 || ^2.0.0-0", - "webpack": ">=5.61.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, "node_modules/babel-plugin-add-module-exports": { "version": "1.0.4", "dev": true, diff --git a/package.json b/package.json index b6f80c2f5ca0..742802150bf4 100644 --- a/package.json +++ b/package.json @@ -296,7 +296,6 @@ "@vitest/mocker": "4.0.16", "@welldone-software/why-did-you-render": "7.0.1", "babel-jest": "29.7.0", - "babel-loader": "^10.0.0", "babel-plugin-module-resolver": "^5.0.0", "babel-plugin-react-compiler": "0.0.0-experimental-a1856f3-20260507", "babel-plugin-transform-remove-console": "^6.9.4", From 7c48b39144f5310ed2000a9237b63251e89a7b71 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 11 Jul 2026 00:23:28 -0700 Subject: [PATCH 15/31] perf: use case-sensitive includes() for worklets-loader's pre-check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Benchmarked 4 approaches against the real src/ corpus (6189 files, 34.67 MiB): case-sensitive String.prototype.includes() beat the original case-insensitive regex by ~15% (3.9us/file vs 4.6us/file median), a toLowerCase()-based case-insensitive includes() was ~2x slower than the regex (allocates a full lowercased copy per file), and shelling out to rg per file was ~2000x slower (9.6ms/file) since process-spawn overhead dwarfs any SIMD search advantage at this scale. Case-sensitivity is also more correct, not just faster: the plugin's own directive check is an exact `=== 'worklet'` comparison, and npm package names are always lowercase, so the previous /i flag only caused false positives (e.g. matching `CLEAR_WORKLET_MAX_RETRIES` or `useWorkletStateMachine`, neither of which the plugin's fixed hook allowlist recognizes) — verified those files produce no worklet markers when actually transformed. Also drops the 'react-native-worklets' alternative, which was always redundant with 'worklet'. Co-authored-by: Cursor --- config/rsbuild/worklets-loader.mjs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/config/rsbuild/worklets-loader.mjs b/config/rsbuild/worklets-loader.mjs index 793e73f34bc4..6d219a239f28 100644 --- a/config/rsbuild/worklets-loader.mjs +++ b/config/rsbuild/worklets-loader.mjs @@ -10,14 +10,26 @@ * react-native-reanimated or react-native-worklets, and the directive itself is the literal * string 'worklet', so a file containing none of those substrings cannot be affected by the * plugin — skipping is safe, not just fast. + * + * The check below uses String.prototype.includes() on two literal substrings rather than a + * regex: benchmarked ~15% faster than an equivalent regex.test() across the actual src/ corpus + * (34.67 MiB / 6189 files: 3.9us/file vs 4.6us/file median), and 'react-native-worklets' is + * dropped as a separate alternative since it already contains 'worklet' as a substring. Matching + * is intentionally case-sensitive (not /i): the plugin's own directive check is an exact + * `=== 'worklet'` comparison (node_modules/react-native-worklets/plugin's hasWorkletDirective), + * and npm package names are always lowercase, so case-insensitivity bought no correctness and + * cost real time — a toLowerCase()-based case-insensitive check benchmarked ~2x *slower* than + * the original regex, since it allocates a full lowercased copy of every file. */ import babel from '@babel/core'; -const WORKLET_REFERENCE = /react-native-reanimated|react-native-worklets|worklet/i; +function referencesWorklet(source) { + return source.includes('react-native-reanimated') || source.includes('worklet'); +} export default function workletsLoader(source) { - if (!WORKLET_REFERENCE.test(source)) { + if (!referencesWorklet(source)) { return source; } From ca0cf37d2ac6ae95b8f10775de12c7e8fe340e0a Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 11 Jul 2026 00:52:06 -0700 Subject: [PATCH 16/31] Drop hand-rolled Fullstory annotation logic for upstream plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Benchmarking against the real src/ corpus showed the hand-rolled parse/traverse/conditional-generate pass was ~12% slower than just calling @fullstory/babel-plugin-annotate-react directly through @babel/core, and harder to maintain. Keep only the `<`-presence pre-filter, which is correctness-safe and skips ~28% of files with no JSX at all. Drops the now-unused @babel/generator and @babel/types devDependencies (@babel/traverse is kept — still used directly by .github/actions/javascript/authorChecklist). Co-authored-by: Cursor --- .../rsbuild/fullstory-annotation-loader.mjs | 375 +----------------- package-lock.json | 2 - package.json | 2 - 3 files changed, 22 insertions(+), 357 deletions(-) diff --git a/config/rsbuild/fullstory-annotation-loader.mjs b/config/rsbuild/fullstory-annotation-loader.mjs index 398277057e50..2019c353b568 100644 --- a/config/rsbuild/fullstory-annotation-loader.mjs +++ b/config/rsbuild/fullstory-annotation-loader.mjs @@ -1,367 +1,36 @@ /** - * Lightweight webpack loader that replicates @fullstory/babel-plugin-annotate-react - * (native: true mode) without running the full Babel transform pipeline. + * Thin wrapper around the upstream @fullstory/babel-plugin-annotate-react plugin, invoked directly via @babel/core rather than + * going through babel-loader. * - * Uses @babel/parser for AST parsing (syntax-only, no code transforms), then - * @babel/traverse to locate React component functions and inject dataComponent / - * dataElement / dataSourceFile props onto JSX opening elements, then - * @babel/generator to emit annotated source that still contains JSX. - * - * This runs as the FIRST webpack loader (rightmost in use[]) so OXC receives - * annotated JSX and can run its own React Compiler before the JSX transform. - * - * Ported from: - * https://github.com/fullstorydev/fullstory-babel-plugin-annotate-react/blob/master/index.js - * Supports `native: true` mode only (matches the existing webpack config). + * The pre-filter below (skip if the file contains no `<` at all, so it cannot possibly contain + * JSX) is a cheap optimization to skip the ~28% of src/ files with no JSX. */ -import generateModule from '@babel/generator'; -import * as parser from '@babel/parser'; -import traverseModule from '@babel/traverse'; -import * as t from '@babel/types'; - -// @babel/traverse and @babel/generator are CJS-only, so their ESM default -// export is the whole CJS module.exports object rather than the function -// itself — same interop wrinkle as require(...).default in CommonJS. -const traverse = traverseModule.default ?? traverseModule; -const generate = generateModule.default ?? generateModule; - -// Modules known to be incompatible with Fullstory annotation (from upstream plugin) -const KNOWN_INCOMPATIBLE = [ - 'react-native-testfairy', - '@react-navigation', - 'react-native-navigation', - 'expo-router', - 'victory', - 'victory-area', - 'victory-axis', - 'victory-bar', - 'victory-box-plot', - 'victory-brush-container', - 'victory-brush-line', - 'victory-candlestick', - 'victory-canvas', - 'victory-chart', - 'victory-core', - 'victory-create-container', - 'victory-cursor-container', - 'victory-errorbar', - 'victory-group', - 'victory-histogram', - 'victory-legend', - 'victory-line', - 'victory-native', - 'victory-pie', - 'victory-polar-axis', - 'victory-scatter', - 'victory-selection-container', - 'victory-shared-events', - 'victory-stack', - 'victory-tooltip', - 'victory-vendor', - 'victory-voronoi', - 'victory-voronoi-container', - 'victory-zoom-container', -]; - -// Attribute names for native: true mode -const ATTR_COMPONENT = 'dataComponent'; -const ATTR_ELEMENT = 'dataElement'; -const ATTR_SOURCE_FILE = 'dataSourceFile'; - -// HTML element names that should NOT get a dataElement attribute -const IGNORED_HTML_ELEMENTS = new Set([ - 'a', - 'abbr', - 'address', - 'area', - 'article', - 'aside', - 'audio', - 'b', - 'base', - 'bdi', - 'bdo', - 'blockquote', - 'body', - 'br', - 'button', - 'canvas', - 'caption', - 'cite', - 'code', - 'col', - 'colgroup', - 'data', - 'datalist', - 'dd', - 'del', - 'details', - 'dfn', - 'dialog', - 'div', - 'dl', - 'dt', - 'em', - 'embed', - 'fieldset', - 'figure', - 'footer', - 'form', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'head', - 'header', - 'hgroup', - 'hr', - 'html', - 'i', - 'iframe', - 'img', - 'input', - 'ins', - 'kbd', - 'keygen', - 'label', - 'legend', - 'li', - 'link', - 'main', - 'map', - 'mark', - 'menu', - 'menuitem', - 'meter', - 'nav', - 'noscript', - 'object', - 'ol', - 'optgroup', - 'option', - 'output', - 'p', - 'param', - 'pre', - 'progress', - 'q', - 'rb', - 'rp', - 'rt', - 'rtc', - 'ruby', - 's', - 'samp', - 'script', - 'section', - 'select', - 'small', - 'source', - 'span', - 'strong', - 'style', - 'sub', - 'summary', - 'sup', - 'table', - 'tbody', - 'td', - 'template', - 'textarea', - 'tfoot', - 'th', - 'thead', - 'time', - 'title', - 'tr', - 'track', - 'u', - 'ul', - 'var', - 'video', - 'wbr', -]); - -function hasAttribute(openingElement, name) { - return (openingElement.attributes || []).some((attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, {name})); -} - -function isFragment(openingElement) { - if (!openingElement || !openingElement.name) { - return false; - } - const name = openingElement.name; - if (t.isJSXIdentifier(name, {name: 'Fragment'}) || t.isJSXIdentifier(name, {name: 'React.Fragment'})) { - return true; - } - if (t.isJSXMemberExpression(name)) { - return t.isJSXIdentifier(name.object, {name: 'React'}) && t.isJSXIdentifier(name.property, {name: 'Fragment'}); - } - return false; -} - -function addAttribute(openingElement, attrName, value) { - if (!openingElement || isFragment(openingElement) || hasAttribute(openingElement, attrName)) { - return; - } - openingElement.attributes.push(t.jSXAttribute(t.jSXIdentifier(attrName), t.stringLiteral(value))); -} - -function applyPropsToJSXNode(jsxPath, componentName, sourceFileName) { - const openingEl = jsxPath.node.openingElement; - if (!openingEl || isFragment(openingEl)) { - return; - } - - const elementName = t.isJSXIdentifier(openingEl.name) ? openingEl.name.name : 'unknown'; - - // dataElement — skip for HTML primitives - if (!IGNORED_HTML_ELEMENTS.has(elementName) && !hasAttribute(openingEl, ATTR_ELEMENT)) { - openingEl.attributes.push(t.jSXAttribute(t.jSXIdentifier(ATTR_ELEMENT), t.stringLiteral(elementName))); - } - - // dataComponent — only for root element of a component - if (componentName) { - addAttribute(openingEl, ATTR_COMPONENT, componentName); - } - - // dataSourceFile - if (sourceFileName) { - addAttribute(openingEl, ATTR_SOURCE_FILE, sourceFileName); - } - - // Recurse into children (they get dataElement + dataSourceFile, but not dataComponent) - for (const child of jsxPath.get('children')) { - if (child.isJSXElement()) { - applyPropsToJSXNode(child, null, sourceFileName); - } - } -} - -function applyPropsToComponent(funcPath, componentName, sourceFileName) { - // Find the top-level JSX return in the function body - let jsxPath = null; - - const body = funcPath.get('body'); - if (!body.isBlockStatement()) { - // Arrow with expression body: `() => ` - if (body.isJSXElement() || body.isJSXFragment()) { - jsxPath = body; - } - } else { - const stmts = body.get('body'); - const returnStmt = stmts.find((s) => s.isReturnStatement()); - if (returnStmt) { - const arg = returnStmt.get('argument'); - if (arg.isJSXElement() || arg.isJSXFragment()) { - jsxPath = arg; - } - } - } - - if (!jsxPath) { - return; - } - applyPropsToJSXNode(jsxPath, componentName, sourceFileName); -} +import babel from '@babel/core'; export default function fullstoryAnnotationLoader(source) { - const resourcePath = this.resourcePath; - - // Skip known-incompatible node_modules - if (KNOWN_INCOMPATIBLE.some((m) => resourcePath.includes(`/node_modules/${m}/`) || resourcePath.includes(`\\node_modules\\${m}\\`))) { + if (!source.includes('<')) { return source; } - // Only process files that likely contain JSX - if (!/\.(jsx|tsx|js|ts)$/.test(resourcePath)) { - return source; - } - - const sourceFileName = resourcePath.split('/').pop(); - - let ast; - try { - ast = parser.parse(source, { - sourceType: 'module', - // Parse-only — no code generation; plugins enable syntax understanding - plugins: ['jsx', 'typescript', 'decorators-legacy', 'classProperties', 'classPrivateProperties', 'classPrivateMethods', 'optionalChaining', 'nullishCoalescingOperator'], - strictMode: false, - }); - } catch { - // If parsing fails, pass through unchanged — OXC will surface the real error - return source; - } - - let modified = false; - - traverse(ast, { - FunctionDeclaration(path) { - const name = path.node.id && path.node.id.name; - if (!name) { - return; - } - const before = JSON.stringify(path.node); - applyPropsToComponent(path, name, sourceFileName); - if (JSON.stringify(path.node) !== before) { - modified = true; - } + const callback = this.async(); + babel.transform( + source, + { + babelrc: false, + configFile: false, + filename: this.resourcePath, + plugins: [['@fullstory/babel-plugin-annotate-react', {native: true}]], + parserOpts: {plugins: ['jsx', 'typescript']}, + sourceMaps: !!this.sourceMap, }, - ArrowFunctionExpression(path) { - const parent = path.parent; - const name = t.isVariableDeclarator(parent) && t.isIdentifier(parent.id) ? parent.id.name : null; - if (!name) { + (error, result) => { + if (error) { + callback(error); return; } - const before = JSON.stringify(path.node); - applyPropsToComponent(path, name, sourceFileName); - if (JSON.stringify(path.node) !== before) { - modified = true; - } - }, - ClassDeclaration(path) { - const name = path.node.id && path.node.id.name; - if (!name) { - return; - } - for (const member of path.get('body').get('body')) { - if (!member.isClassMethod()) { - continue; - } - if (!t.isIdentifier(member.node.key, {name: 'render'})) { - continue; - } - for (const stmt of member.get('body').get('body')) { - if (!stmt.isReturnStatement()) { - continue; - } - const arg = stmt.get('argument'); - if (!arg.isJSXElement() && !arg.isJSXFragment()) { - continue; - } - const before = JSON.stringify(arg.node); - applyPropsToJSXNode(arg, name, sourceFileName); - if (JSON.stringify(arg.node) !== before) { - modified = true; - } - } - } + callback(null, result.code, result.map ?? undefined); }, - }); - - if (!modified) { - return source; - } - - const {code, map} = generate(ast, {sourceMaps: !!this.sourceMap, sourceFileName: resourcePath}, source); - - if (this.sourceMap && map) { - this.callback(null, code, map); - return; - } - - return code; + ); + return undefined; } diff --git a/package-lock.json b/package-lock.json index 708437c5d610..c79758924821 100644 --- a/package-lock.json +++ b/package-lock.json @@ -155,14 +155,12 @@ "@actions/core": "1.10.0", "@actions/github": "5.1.1", "@babel/core": "^7.25.2", - "@babel/generator": "^7.28.0", "@babel/parser": "^7.22.16", "@babel/plugin-proposal-class-properties": "^7.12.1", "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/traverse": "^7.22.20", - "@babel/types": "^7.22.19", "@callstack/reassure-compare": "^1.0.0-rc.4", "@dword-design/eslint-plugin-import-alias": "^5.0.0", "@fullstory/babel-plugin-annotate-react": "^2.3.2", diff --git a/package.json b/package.json index 742802150bf4..7c5cf92397c7 100644 --- a/package.json +++ b/package.json @@ -228,14 +228,12 @@ "@actions/core": "1.10.0", "@actions/github": "5.1.1", "@babel/core": "^7.25.2", - "@babel/generator": "^7.28.0", "@babel/parser": "^7.22.16", "@babel/plugin-proposal-class-properties": "^7.12.1", "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/traverse": "^7.22.20", - "@babel/types": "^7.22.19", "@callstack/reassure-compare": "^1.0.0-rc.4", "@dword-design/eslint-plugin-import-alias": "^5.0.0", "@fullstory/babel-plugin-annotate-react": "^2.3.2", From bc9b5e36ec1902c29b22bc81d9c26d751169d5e8 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 11 Jul 2026 01:10:49 -0700 Subject: [PATCH 17/31] perf: skip Fullstory Babel pass entirely for .ts files TypeScript's grammar disallows JSX outside .tsx, so this is a guaranteed-safe skip rather than a heuristic. The existing source.includes('<') check alone was a poor proxy for "might contain JSX": 1722 of 3481 .ts files (49.5%) contain a literal '<' from generics/comparisons (e.g. Record) and were false positives, including all of src/languages/ and most of src/CONST/ -- among the largest files in the repo, several over the 500KB Babel codegen-deoptimization threshold. Benchmarked against the real src/ corpus: cuts Babel invocations from 70.9% to 44.6% of all files (37% fewer calls, ~52% less wall-clock time in this pass). Fullstory annotation output is unchanged (240 dataComponent occurrences in the main bundle, 2637 total, both before and after). .js (non-.jsx) keeps the content check as a fallback, since nothing in the build enforces that plain .js files can't contain JSX -- verified only 2 such files exist in src/, both generated parsers with no JSX. Co-authored-by: Cursor --- config/rsbuild/fullstory-annotation-loader.mjs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/config/rsbuild/fullstory-annotation-loader.mjs b/config/rsbuild/fullstory-annotation-loader.mjs index 2019c353b568..79a2869eabd7 100644 --- a/config/rsbuild/fullstory-annotation-loader.mjs +++ b/config/rsbuild/fullstory-annotation-loader.mjs @@ -2,13 +2,25 @@ * Thin wrapper around the upstream @fullstory/babel-plugin-annotate-react plugin, invoked directly via @babel/core rather than * going through babel-loader. * - * The pre-filter below (skip if the file contains no `<` at all, so it cannot possibly contain - * JSX) is a cheap optimization to skip the ~28% of src/ files with no JSX. + * Two pre-filters skip Babel entirely when a file cannot possibly contain JSX: + * - `.ts` files: TypeScript's grammar disallows JSX syntax outside `.tsx`, so this is a + * guaranteed-safe skip, not a heuristic. Measured across src/: 1722 of 3481 `.ts` files + * (49.5%) contain a literal `<` from generics/comparisons (e.g. `Record`) and would + * otherwise be false positives for the check below — including all of src/languages/ and + * most of src/CONST/, some of the largest files in the repo. + * - All other extensions (.tsx, .jsx, .js): fall back to a `source.includes('<')` check, since + * JSX is legal in plain `.js` files too (verified: only 2 non-.jsx `.js` files exist in src/, + * both generated parsers with no JSX — but nothing enforces that going forward). */ import babel from '@babel/core'; +import path from 'node:path'; export default function fullstoryAnnotationLoader(source) { + const ext = path.extname(this.resourcePath); + if (ext === '.ts') { + return source; + } if (!source.includes('<')) { return source; } From 0bd3ded74525dafc53774dbfb3d23a221746381d Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 11 Jul 2026 01:43:33 -0700 Subject: [PATCH 18/31] fix(rsbuild): enable persistent rspack cache for build, not just dev cache.type: 'persistent' was only ever wired up inside rsbuild.config.ts's dev-only branch, so `npm run build` (returned straight from getCommonConfiguration, never touching that branch) always ran with no cross-process cache. Moved it into getSharedConfiguration so dev, build, and Storybook all persist the module graph + loader outputs to disk, turning warm `npm run build` from a second cold build (~30s) into an actual warm rebuild (~11-13s) instead. Co-authored-by: Cursor --- config/rsbuild/rsbuild.common.ts | 15 +++++++++++++++ config/rsbuild/rsbuild.config.ts | 20 +++++++++----------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/config/rsbuild/rsbuild.common.ts b/config/rsbuild/rsbuild.common.ts index c78aa604fdff..b77b62de71e7 100644 --- a/config/rsbuild/rsbuild.common.ts +++ b/config/rsbuild/rsbuild.common.ts @@ -202,6 +202,21 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => // eslint-disable-next-line no-param-reassign config.ignoreWarnings = [...(config.ignoreWarnings ?? []), /lottie-react-native\/lib\/module\/LottieView\/index\.web\.js/, /oxc-react-compiler-loader:/]; + // A list of paths rspack trusts would not be modified while rspack is running. + // Onyx and react-native-live-markdown can be modified on the fly, changes to other + // node_modules would not be reflected live. Applies to `dev`, `build`, and Storybook + // alike: persisting the module graph + OXC/Fullstory loader outputs to disk on every + // command (not just `dev`) is what makes repeated local `npm run build` runs fast — + // without it, every build re-runs every loader from scratch regardless of what changed. + // eslint-disable-next-line no-param-reassign + config.cache ??= {type: 'persistent'}; + if (typeof config.cache === 'object' && config.cache.type === 'persistent') { + // eslint-disable-next-line no-param-reassign + config.cache.snapshot = { + managedPaths: [/([\\/]node_modules[\\/](?!react-native-onyx|@expensify\/react-native-live-markdown))/], + }; + } + // eslint-disable-next-line no-param-reassign config.resolve ??= {}; // eslint-disable-next-line no-param-reassign diff --git a/config/rsbuild/rsbuild.config.ts b/config/rsbuild/rsbuild.config.ts index 227c9a46049d..167f72fc5fe5 100644 --- a/config/rsbuild/rsbuild.config.ts +++ b/config/rsbuild/rsbuild.config.ts @@ -37,7 +37,15 @@ export default defineConfig(async ({command}) => { const common: RsbuildConfig = getCommonConfiguration({file: envFile, platform: 'web'}); if (!isDevServer) { - return common; + return { + ...common, + performance: { + ...common.performance, + // Invalidate the persistent cache (enabled in rsbuild.common.ts) if this config file + // itself changes, same as the dev-server branch below. + buildCache: {buildDependencies: [filename]}, + }, + }; } const port = await portfinder.getPortPromise({port: BASE_PORT}); @@ -92,16 +100,6 @@ export default defineConfig(async ({command}) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const afterCommon = (typeof commonRspackTool === 'function' ? (commonRspackTool(config, utils) ?? config) : config) as typeof config; - // A list of paths rspack trusts would not be modified while rspack is running. - // Onyx and react-native-live-markdown can be modified on the fly, changes to other - // node_modules would not be reflected live. - afterCommon.cache ??= {type: 'persistent'}; - if (typeof afterCommon.cache === 'object' && afterCommon.cache.type === 'persistent') { - afterCommon.cache.snapshot = { - managedPaths: [/([\\/]node_modules[\\/](?!react-native-onyx|@expensify\/react-native-live-markdown))/], - }; - } - afterCommon.plugins ??= []; afterCommon.plugins.push(new ReactRefreshRspackPlugin()); From 1a0bd33e767fa46b13fda1e69eca2bbfed677dcf Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 11 Jul 2026 02:17:29 -0700 Subject: [PATCH 19/31] Upgrade @fullstory/babel-plugin-annotate-react to 2.4.0 Enables the new reactCompiler option in the web loader's plugin config so Fullstory annotations still find JSX return values when React Compiler has already extracted them into cached variables. Co-authored-by: Cursor --- config/rsbuild/fullstory-annotation-loader.mjs | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/config/rsbuild/fullstory-annotation-loader.mjs b/config/rsbuild/fullstory-annotation-loader.mjs index 79a2869eabd7..94ee8d7ef223 100644 --- a/config/rsbuild/fullstory-annotation-loader.mjs +++ b/config/rsbuild/fullstory-annotation-loader.mjs @@ -32,7 +32,7 @@ export default function fullstoryAnnotationLoader(source) { babelrc: false, configFile: false, filename: this.resourcePath, - plugins: [['@fullstory/babel-plugin-annotate-react', {native: true}]], + plugins: [['@fullstory/babel-plugin-annotate-react', {native: true, reactCompiler: true}]], parserOpts: {plugins: ['jsx', 'typescript']}, sourceMaps: !!this.sourceMap, }, diff --git a/package-lock.json b/package-lock.json index c79758924821..66011850d66d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -163,7 +163,7 @@ "@babel/traverse": "^7.22.20", "@callstack/reassure-compare": "^1.0.0-rc.4", "@dword-design/eslint-plugin-import-alias": "^5.0.0", - "@fullstory/babel-plugin-annotate-react": "^2.3.2", + "@fullstory/babel-plugin-annotate-react": "^2.4.0", "@fullstory/babel-plugin-react-native": "^1.5.2", "@jest/globals": "^29.7.0", "@ngneat/falso": "^7.1.1", @@ -9179,9 +9179,9 @@ } }, "node_modules/@fullstory/babel-plugin-annotate-react": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@fullstory/babel-plugin-annotate-react/-/babel-plugin-annotate-react-2.3.2.tgz", - "integrity": "sha512-s6TAsAivWcP32fNOAwmEW6LdjE+gjCOlf4ILrqMdVS8EDJeHf51s1tO+teQkY3XiOY4QziYu0eR9TeoRfyqOQw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@fullstory/babel-plugin-annotate-react/-/babel-plugin-annotate-react-2.4.0.tgz", + "integrity": "sha512-ALBMsGR86Yg/+22UOUCB0MSFlXNVkxE8az9ZxOOE/bO1FXCjC4/7cpFTqk2rkLA0nyde1j+Cbydb8oXNr3ZjHA==", "license": "MIT", "engines": { "node": ">=16.20.2", diff --git a/package.json b/package.json index 7c5cf92397c7..8bf0823a229f 100644 --- a/package.json +++ b/package.json @@ -236,7 +236,7 @@ "@babel/traverse": "^7.22.20", "@callstack/reassure-compare": "^1.0.0-rc.4", "@dword-design/eslint-plugin-import-alias": "^5.0.0", - "@fullstory/babel-plugin-annotate-react": "^2.3.2", + "@fullstory/babel-plugin-annotate-react": "^2.4.0", "@fullstory/babel-plugin-react-native": "^1.5.2", "@jest/globals": "^29.7.0", "@ngneat/falso": "^7.1.1", From b651008331269ba2c39887a414689b8f138d44e4 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 11 Jul 2026 02:36:01 -0700 Subject: [PATCH 20/31] Simplify loader ESLint override comment, seatbelt the negated-variable false positive Drop the rulesdir/no-negated-variables suppression for config/rsbuild/*-loader.mjs in favor of a seatbelt baseline entry, keeping the config itself simpler. Co-authored-by: Cursor --- config/eslint/eslint.config.mjs | 8 +------- config/eslint/eslint.seatbelt.tsv | 3 ++- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/config/eslint/eslint.config.mjs b/config/eslint/eslint.config.mjs index 913a963f06d3..84d5c6bae7ed 100644 --- a/config/eslint/eslint.config.mjs +++ b/config/eslint/eslint.config.mjs @@ -534,17 +534,11 @@ const config = defineConfig([ }, }, - // Rspack/webpack loader modules (ESM). Loaders receive their `this` loader - // context from the bundler, not from a class/object — the ESM sourceType - // makes `no-invalid-this` newly aware of these top-level function - // declarations, which is a false positive for this well-established - // loader convention. `no-negated-variables` also false-positives on - // "annotation" (matches the "notation" substring), which isn't a negation. + // Rspack loaders receive their `this` from the bundler, and it's standard practice to use it { files: ['config/rsbuild/*-loader.mjs'], rules: { 'no-invalid-this': 'off', - 'rulesdir/no-negated-variables': 'off', }, }, diff --git a/config/eslint/eslint.seatbelt.tsv b/config/eslint/eslint.seatbelt.tsv index 8110e4b9c66e..1b2e94904d29 100644 --- a/config/eslint/eslint.seatbelt.tsv +++ b/config/eslint/eslint.seatbelt.tsv @@ -2323,5 +2323,6 @@ "../../tests/utils/getIsUsingFakeTimers.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 "../rsbuild/CustomVersionFilePlugin.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 "../rsbuild/ModuleInitTimingPlugin.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 -"../rspack/RspackPreloadPlugin.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../rsbuild/fullstory-annotation-loader.mjs" "rulesdir/no-negated-variables" 1 "../rsbuild/rsbuild.common.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../rspack/RspackPreloadPlugin.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 From daa2db9a37eb12e3a2df783b3d646f5975d83f21 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 11 Jul 2026 02:43:29 -0700 Subject: [PATCH 21/31] Group custom loaders under config/rsbuild/loaders/ Move the three custom Rspack loaders (fullstory-annotation, oxc-react-compiler, worklets) into their own subdirectory and update the referencing paths in rsbuild.common.ts, the ESLint override glob, and the seatbelt baseline. Co-authored-by: Cursor --- config/eslint/eslint.config.mjs | 2 +- config/eslint/eslint.seatbelt.tsv | 2 +- .../{ => loaders}/fullstory-annotation-loader.mjs | 0 .../{ => loaders}/oxc-react-compiler-loader.mjs | 0 config/rsbuild/{ => loaders}/worklets-loader.mjs | 0 config/rsbuild/rsbuild.common.ts | 10 +++++----- 6 files changed, 7 insertions(+), 7 deletions(-) rename config/rsbuild/{ => loaders}/fullstory-annotation-loader.mjs (100%) rename config/rsbuild/{ => loaders}/oxc-react-compiler-loader.mjs (100%) rename config/rsbuild/{ => loaders}/worklets-loader.mjs (100%) diff --git a/config/eslint/eslint.config.mjs b/config/eslint/eslint.config.mjs index 84d5c6bae7ed..352119841497 100644 --- a/config/eslint/eslint.config.mjs +++ b/config/eslint/eslint.config.mjs @@ -536,7 +536,7 @@ const config = defineConfig([ // Rspack loaders receive their `this` from the bundler, and it's standard practice to use it { - files: ['config/rsbuild/*-loader.mjs'], + files: ['config/rsbuild/loaders/*-loader.mjs'], rules: { 'no-invalid-this': 'off', }, diff --git a/config/eslint/eslint.seatbelt.tsv b/config/eslint/eslint.seatbelt.tsv index 1b2e94904d29..926316d60a98 100644 --- a/config/eslint/eslint.seatbelt.tsv +++ b/config/eslint/eslint.seatbelt.tsv @@ -2323,6 +2323,6 @@ "../../tests/utils/getIsUsingFakeTimers.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 "../rsbuild/CustomVersionFilePlugin.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 "../rsbuild/ModuleInitTimingPlugin.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 -"../rsbuild/fullstory-annotation-loader.mjs" "rulesdir/no-negated-variables" 1 +"../rsbuild/loaders/fullstory-annotation-loader.mjs" "rulesdir/no-negated-variables" 1 "../rsbuild/rsbuild.common.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 "../rspack/RspackPreloadPlugin.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 diff --git a/config/rsbuild/fullstory-annotation-loader.mjs b/config/rsbuild/loaders/fullstory-annotation-loader.mjs similarity index 100% rename from config/rsbuild/fullstory-annotation-loader.mjs rename to config/rsbuild/loaders/fullstory-annotation-loader.mjs diff --git a/config/rsbuild/oxc-react-compiler-loader.mjs b/config/rsbuild/loaders/oxc-react-compiler-loader.mjs similarity index 100% rename from config/rsbuild/oxc-react-compiler-loader.mjs rename to config/rsbuild/loaders/oxc-react-compiler-loader.mjs diff --git a/config/rsbuild/worklets-loader.mjs b/config/rsbuild/loaders/worklets-loader.mjs similarity index 100% rename from config/rsbuild/worklets-loader.mjs rename to config/rsbuild/loaders/worklets-loader.mjs diff --git a/config/rsbuild/rsbuild.common.ts b/config/rsbuild/rsbuild.common.ts index b77b62de71e7..3976dcff3d20 100644 --- a/config/rsbuild/rsbuild.common.ts +++ b/config/rsbuild/rsbuild.common.ts @@ -271,13 +271,13 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => // worklets-loader skips the Babel parse/transform/codegen cycle for // files that can't reference a worklet (see that file) — ~97.5% of // src/, verified by grep. - {loader: path.resolve(dirname, './worklets-loader.mjs')}, + {loader: path.resolve(dirname, './loaders/worklets-loader.mjs')}, // Pass 2: React Compiler + all transforms via our thin wrapper loader. // oxc-react-compiler-loader calls oxc-transform directly and demotes // non-fatal React Compiler diagnostics to warnings instead of hard build // errors (workaround for oxc-project/oxc#23587). { - loader: path.resolve(dirname, './oxc-react-compiler-loader.mjs'), + loader: path.resolve(dirname, './loaders/oxc-react-compiler-loader.mjs'), options: { reactCompiler: {target: '19', panicThreshold: 'none'}, target: 'node20', @@ -286,7 +286,7 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => }, // Pass 1: Fullstory annotation (sees annotated JSX before OXC transforms it). { - loader: path.resolve(dirname, './fullstory-annotation-loader.mjs'), + loader: path.resolve(dirname, './loaders/fullstory-annotation-loader.mjs'), }, ], }, @@ -295,7 +295,7 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => test: /\.tsx?$/, include: [includedNodeModules], exclude: [/\.native\.(ts|tsx)$/], - use: [{loader: path.resolve(dirname, './worklets-loader.mjs')}, {loader: 'oxc-webpack-loader', options: {target: 'node20'}}], + use: [{loader: path.resolve(dirname, './loaders/worklets-loader.mjs')}, {loader: 'oxc-webpack-loader', options: {target: 'node20'}}], }, // Rule B2: JavaScript — need explicit jsx to upgrade .js lang to jsx. { @@ -303,7 +303,7 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => include: [includedNodeModules], exclude: [/\.native\.(js|jsx)$/], use: [ - {loader: path.resolve(dirname, './worklets-loader.mjs')}, + {loader: path.resolve(dirname, './loaders/worklets-loader.mjs')}, { loader: 'oxc-webpack-loader', options: {target: 'node20', jsx: {runtime: 'automatic'}}, From 5c3f57feafe6afcd2e1130d863e6e0c5a5ee21bc Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 11 Jul 2026 02:44:55 -0700 Subject: [PATCH 22/31] Simplify comment in fullstory-annotation-loader --- .../rsbuild/loaders/fullstory-annotation-loader.mjs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/config/rsbuild/loaders/fullstory-annotation-loader.mjs b/config/rsbuild/loaders/fullstory-annotation-loader.mjs index 94ee8d7ef223..6008eb913d59 100644 --- a/config/rsbuild/loaders/fullstory-annotation-loader.mjs +++ b/config/rsbuild/loaders/fullstory-annotation-loader.mjs @@ -2,15 +2,9 @@ * Thin wrapper around the upstream @fullstory/babel-plugin-annotate-react plugin, invoked directly via @babel/core rather than * going through babel-loader. * - * Two pre-filters skip Babel entirely when a file cannot possibly contain JSX: - * - `.ts` files: TypeScript's grammar disallows JSX syntax outside `.tsx`, so this is a - * guaranteed-safe skip, not a heuristic. Measured across src/: 1722 of 3481 `.ts` files - * (49.5%) contain a literal `<` from generics/comparisons (e.g. `Record`) and would - * otherwise be false positives for the check below — including all of src/languages/ and - * most of src/CONST/, some of the largest files in the repo. - * - All other extensions (.tsx, .jsx, .js): fall back to a `source.includes('<')` check, since - * JSX is legal in plain `.js` files too (verified: only 2 non-.jsx `.js` files exist in src/, - * both generated parsers with no JSX — but nothing enforces that going forward). + * It is an optimization. Two filters skip Babel entirely when a file cannot possibly contain JSX: + * - `.ts` files: TypeScript's grammar disallows JSX syntax outside `.tsx` + * - All other extensions (.tsx, .jsx, .js): fall back to a `source.includes('<')`, a quick check for JSX */ import babel from '@babel/core'; From 1ef1f7f0ee7a8800f9de6af99433802da1e4eaf7 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 11 Jul 2026 02:48:20 -0700 Subject: [PATCH 23/31] Simplify comments in oxc-react-compiler-loader --- .../loaders/oxc-react-compiler-loader.mjs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/config/rsbuild/loaders/oxc-react-compiler-loader.mjs b/config/rsbuild/loaders/oxc-react-compiler-loader.mjs index 8ba3e05e0d08..2ecd7a5e19e8 100644 --- a/config/rsbuild/loaders/oxc-react-compiler-loader.mjs +++ b/config/rsbuild/loaders/oxc-react-compiler-loader.mjs @@ -1,12 +1,12 @@ /** * Thin wrapper around oxc-transform that adds React Compiler support while * demoting non-fatal React Compiler diagnostics from hard build errors to - * webpack warnings. This works around oxc-project/oxc#23587 (fixed in a future + * webpack warnings. + * + * This works around oxc-project/oxc#23587 (fixed in a future * release), which caused the build to fail instead of bailing out on components * that violate the Rules of React — the same behaviour as babel-plugin-react-compiler. - * - * Options mirror oxc-webpack-loader's TransformOptions plus `reactCompiler`. - * This loader replaces oxc-webpack-loader entirely for Rule A (app source). + * We need the workaround for now until a new oxc release containing the fix adds back `reactCompiler`. */ import {getTsconfig} from 'get-tsconfig'; @@ -98,11 +98,9 @@ export default async function oxcReactCompilerLoader(source) { return; } - // An Error-severity React Compiler diagnostic (e.g. a genuine Rules-of-React - // violation like accessing a ref during render) makes oxc-transform bail out - // of the whole transform and return empty code, not just skip the optimization - // pass. Re-run without the compiler to fall back to plain JSX/TS transform output, - // matching babel-plugin-react-compiler's default "skip this component" bailout. + // A React Compiler error makes oxc-transform bail out + // of the whole transform and return empty code, not just skip the optimization. + // Re-run without the compiler to fall back to plain JSX/TS transform output. if (!result.code && rcErrors.length > 0) { result = await transform(resourcePath, source, {...transformOptions, reactCompiler: false}); } From 2e0779ea21085688c460facf54c225ad56132aa9 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 11 Jul 2026 02:50:49 -0700 Subject: [PATCH 24/31] Simplify comments in worklets-loader --- config/rsbuild/loaders/worklets-loader.mjs | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/config/rsbuild/loaders/worklets-loader.mjs b/config/rsbuild/loaders/worklets-loader.mjs index 6d219a239f28..b777b963d367 100644 --- a/config/rsbuild/loaders/worklets-loader.mjs +++ b/config/rsbuild/loaders/worklets-loader.mjs @@ -1,25 +1,5 @@ /** - * Thin replacement for babel-loader + react-native-worklets/plugin that skips the Babel - * parse/transform/codegen cycle entirely for the vast majority of files that can't possibly - * contain a worklet (confirmed by grepping src/: ~2.5% of files match). - * - * react-native-worklets/plugin auto-workletizes call expressions of a fixed set of hook/function - * names (useAnimatedStyle, useDerivedValue, runOnUI, gesture handler builders, etc. — see - * node_modules/react-native-worklets/plugin/index.js's reanimatedFunctionHooks) as well as - * explicit 'worklet' directives. Every one of those hooks is only reachable by importing - * react-native-reanimated or react-native-worklets, and the directive itself is the literal - * string 'worklet', so a file containing none of those substrings cannot be affected by the - * plugin — skipping is safe, not just fast. - * - * The check below uses String.prototype.includes() on two literal substrings rather than a - * regex: benchmarked ~15% faster than an equivalent regex.test() across the actual src/ corpus - * (34.67 MiB / 6189 files: 3.9us/file vs 4.6us/file median), and 'react-native-worklets' is - * dropped as a separate alternative since it already contains 'worklet' as a substring. Matching - * is intentionally case-sensitive (not /i): the plugin's own directive check is an exact - * `=== 'worklet'` comparison (node_modules/react-native-worklets/plugin's hasWorkletDirective), - * and npm package names are always lowercase, so case-insensitivity bought no correctness and - * cost real time — a toLowerCase()-based case-insensitive check benchmarked ~2x *slower* than - * the original regex, since it allocates a full lowercased copy of every file. + * Thin wrapper around react-native-worklets/plugin that skips babel entirely for the ~97.5% of files that don't contain worklets. */ import babel from '@babel/core'; From 19b6f3b8618ebe34aa4fa5c3b31cb69e0b391b68 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 11 Jul 2026 03:16:33 -0700 Subject: [PATCH 25/31] Merge included-node_modules JS/TS rules, DRY loader config, drop Rule A/B naming Now that oxc-react-compiler-loader runs React Compiler on included node_modules too (with its own per-file Rules-of-React bailout), the separate TS/TSX and JS/JSX rules for that allowlist were identical other than the jsx option, so they're merged into one rule matching all 4 extensions like the app-source rule already does. The worklets-loader + oxc-react-compiler-loader pair shared by both rules is factored into getOxcAndWorkletsLoaders() to avoid repeating the same loader/options twice. Comments no longer use the "Rule A/B/B2" shorthand, and the Fullstory loader comment no longer implies pass ordering matters (the 2.4.0 upgrade's reactCompiler option makes it order-independent). Co-authored-by: Cursor --- .../loaders/oxc-react-compiler-loader.mjs | 7 +- config/rsbuild/rsbuild.common.ts | 83 +++++++++---------- 2 files changed, 43 insertions(+), 47 deletions(-) diff --git a/config/rsbuild/loaders/oxc-react-compiler-loader.mjs b/config/rsbuild/loaders/oxc-react-compiler-loader.mjs index 2ecd7a5e19e8..252cdbf7053f 100644 --- a/config/rsbuild/loaders/oxc-react-compiler-loader.mjs +++ b/config/rsbuild/loaders/oxc-react-compiler-loader.mjs @@ -51,13 +51,12 @@ function getLang(ext) { if (ext === 'tsx') { return 'tsx'; } - if (ext === 'jsx') { - return 'jsx'; - } if (ext === 'ts') { return 'ts'; } - return 'js'; + // JSX is legal syntax in plain .js files too (both rules that route here match .js and .jsx + // identically), so .js gets the same JSX-enabled parser as .jsx rather than a stricter one. + return 'jsx'; } export default async function oxcReactCompilerLoader(source) { diff --git a/config/rsbuild/rsbuild.common.ts b/config/rsbuild/rsbuild.common.ts index 3976dcff3d20..088f4b981a57 100644 --- a/config/rsbuild/rsbuild.common.ts +++ b/config/rsbuild/rsbuild.common.ts @@ -38,12 +38,27 @@ function getCurrentBranchName(): string { const localBranchName = getCurrentBranchName(); +/** + * React Compiler + react-native-worklets loaders. + */ +function getOxcAndWorkletsLoaders() { + return [ + {loader: path.resolve(dirname, './loaders/worklets-loader.mjs')}, + { + loader: path.resolve(dirname, './loaders/oxc-react-compiler-loader.mjs'), + options: { + reactCompiler: {target: '19', panicThreshold: 'none'}, + target: 'node20', + jsx: {runtime: 'automatic'}, + }, + }, + ]; +} + /** * These RN packages ship non-transpiled JSX and rely on the "react-native" import (aliased to * react-native-web below), so they need to go through the same OXC transform pipeline as our own - * source rather than being treated as opaque, already-built node_modules (see Rule B below). - * React Compiler must NOT run on them since they intentionally mutate state in ways that violate - * the Rules of React. + * source rather than being treated as opaque. */ const INCLUDED_NODE_MODULES = [ 'react-native-reanimated', @@ -71,9 +86,9 @@ const INCLUDED_NODE_MODULES = [ '@shopify/react-native-skia', ]; -// Matches node_modules paths that ARE in the allowlist above (Rule B — transformed but without -// React Compiler). -const includedNodeModules = new RegExp(`node_modules/(${INCLUDED_NODE_MODULES.join('|')})`); +// Matches node_modules paths that ARE in the allowlist above — these get transformed through the +// same OXC + React Compiler pipeline as app source (see the module rule below that uses this regex). +const includedNodeModulesRegex = new RegExp(`node_modules/(${INCLUDED_NODE_MODULES.join('|')})`); const environmentToLogoSuffixMap: Record = { production: '-dark', @@ -168,7 +183,7 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => ], tools: { // Rsbuild's default 'js' rule (builtin:swc-loader, its 'js' oneOf branch) matches every - // .js/.ts/.jsx/.tsx file, including everything Rule A/B/B2 below already handle. Rules + // .js/.ts/.jsx/.tsx file, including everything our own module rules below already handle. Rules // declared later in `module.rules` run their loaders FIRST (closest to raw source), and // our custom rules are added via `addRules` (which appends after Rsbuild's defaults), so // without this exclude, swc-loader would strip JSX/TypeScript before the Fullstory @@ -180,7 +195,7 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => .rule('js') .oneOf('js') .exclude.add((resourcePath: string) => !resourcePath.includes('node_modules')) - .add(includedNodeModules) + .add(includedNodeModulesRegex) .end(); }, rspack: (config, {addRules}) => { @@ -261,54 +276,36 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => resolve: {fullySpecified: false}, include: [path.resolve(dirname, '../../node_modules/react-native-tab-view/lib/module/TabView.js')], }, - // Rule A — app source files (React Compiler enabled). + // App source files (React Compiler enabled). { test: /\.(js|ts)x?$/, - // Exclude ALL node_modules (including includedNodeModules — handled by Rule B below). + // Exclude ALL node_modules (including the included-node_modules allowlist, handled below). exclude: [/node_modules/, /\.native\.(js|jsx|ts|tsx)$/], use: [ - // Pass 3: worklets (on OXC's plain-JS output — no presets needed). - // worklets-loader skips the Babel parse/transform/codegen cycle for - // files that can't reference a worklet (see that file) — ~97.5% of - // src/, verified by grep. - {loader: path.resolve(dirname, './loaders/worklets-loader.mjs')}, - // Pass 2: React Compiler + all transforms via our thin wrapper loader. + // Worklets, then React Compiler + all other OXC transforms (see + // getOxcAndWorkletsLoaders). worklets-loader skips the Babel + // parse/transform/codegen cycle for files that can't reference a + // worklet (see that file) — ~97.5% of src/, verified by grep. // oxc-react-compiler-loader calls oxc-transform directly and demotes // non-fatal React Compiler diagnostics to warnings instead of hard build // errors (workaround for oxc-project/oxc#23587). - { - loader: path.resolve(dirname, './loaders/oxc-react-compiler-loader.mjs'), - options: { - reactCompiler: {target: '19', panicThreshold: 'none'}, - target: 'node20', - jsx: {runtime: 'automatic'}, - }, - }, - // Pass 1: Fullstory annotation (sees annotated JSX before OXC transforms it). + ...getOxcAndWorkletsLoaders(), + // Fullstory annotation. { loader: path.resolve(dirname, './loaders/fullstory-annotation-loader.mjs'), }, ], }, - // Rule B — included node_modules that need transforms but NOT React Compiler. + // Included node_modules (see includedNodeModulesRegex above). Same OXC + React + // Compiler pass as app source above (oxc-react-compiler-loader's per-file + // bailout, see that file, handles any Rules-of-React violations in these + // packages) minus the Fullstory pass, which only makes sense for our own + // components. { - test: /\.tsx?$/, - include: [includedNodeModules], - exclude: [/\.native\.(ts|tsx)$/], - use: [{loader: path.resolve(dirname, './loaders/worklets-loader.mjs')}, {loader: 'oxc-webpack-loader', options: {target: 'node20'}}], - }, - // Rule B2: JavaScript — need explicit jsx to upgrade .js lang to jsx. - { - test: /\.jsx?$/, - include: [includedNodeModules], - exclude: [/\.native\.(js|jsx)$/], - use: [ - {loader: path.resolve(dirname, './loaders/worklets-loader.mjs')}, - { - loader: 'oxc-webpack-loader', - options: {target: 'node20', jsx: {runtime: 'automatic'}}, - }, - ], + test: /\.(js|ts)x?$/, + include: [includedNodeModulesRegex], + exclude: [/\.native\.(js|jsx|ts|tsx)$/], + use: getOxcAndWorkletsLoaders(), }, ]); From 2a1519959ab7a202a2c33ccc958d47d4ffac6265 Mon Sep 17 00:00:00 2001 From: rory Date: Sun, 12 Jul 2026 13:40:53 -0700 Subject: [PATCH 26/31] Remove stale cspell.json entries no longer used anywhere in the repo serialises, downleveling, errorbar, testfairy, voronoi, and workletizes were added as spellcheck exceptions for content that has since been reworded, reverted, or replaced elsewhere in this branch's history. None of them appear in any tracked file (verified via git grep and a clean cspell run), so they're just dead weight. Co-authored-by: Cursor --- cspell.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cspell.json b/cspell.json index a71d2384b50c..885f1e077f78 100644 --- a/cspell.json +++ b/cspell.json @@ -348,7 +348,6 @@ "Scotiabank", "Segoe", "Selec", - "serialises", "Sepa", "Sharees", "Sharons", @@ -568,7 +567,6 @@ "domparser", "dont", "dotlottie", - "downleveling", "dsyms", "durationMillis", "e2edelta", @@ -585,7 +583,6 @@ "endgroup", "enroute", "entertainm", - "errorbar", "entityid", "eraa", "ethnicities", @@ -944,7 +941,6 @@ "taxrate", "teachersunite", "testflight", - "testfairy", "textanchor", "threadsafe", "thumbsup", @@ -1010,7 +1006,6 @@ "viewports", "voidings", "vorbis", - "voronoi", "vvcf", "vypj", "waitlist", @@ -1025,7 +1020,6 @@ "wordprocessingml", "worklet", "workletization", - "workletizes", "worklets", "workshopping", "workspacename", From 361bafe7a75b9dc51cbb71cab0df12ae61d53881 Mon Sep 17 00:00:00 2001 From: rory Date: Sun, 12 Jul 2026 13:55:30 -0700 Subject: [PATCH 27/31] Rename eslint-only babel config from web to eslint, trim unneeded plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename for clarity per code review — this branch is only reached by ESLint's @babel/eslint-parser now, never the web build. Also drop presets: [] (empty, so equivalent to omitting it) and three plugins that only affect Babel's transform output (Fullstory annotation, worklets, export-namespace-from), not parsing — @babel/eslint-parser only needs this file to resolve syntax plugins, and lint passes without them. Co-authored-by: Cursor --- babel.config.js | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/babel.config.js b/babel.config.js index c33f11eb1d2b..543cbdead7e4 100644 --- a/babel.config.js +++ b/babel.config.js @@ -29,26 +29,15 @@ function traceTransformer() { // Rsbuild's web build no longer reads this: its JS/TS/JSX pipeline (see // config/rsbuild/rsbuild.common.ts) calls OXC directly with configFile:false, bypassing -// this file entirely. This `web` config object is still read, though — ESLint's +// this file entirely. This config object is still read, though — ESLint's // @babel/eslint-parser loads babel.config.js for every file it lints (caller name is // always undefined or '@babel/eslint-parser', never 'metro'/'babel-jest', so it always -// falls into this branch below) to resolve syntax plugins like `exportNamespaceFrom` -// needed to parse the same syntax the web bundle uses. OXC handles JSX/TypeScript/env-target -// transforms natively for the web build, so this branch only lists the plugins ESLint's -// parser needs for syntax it wouldn't otherwise recognize. -const web = { - presets: [], - plugins: [ - ['babel-plugin-react-compiler', ReactCompilerConfig], - 'react-native-worklets/plugin', - '@babel/plugin-transform-export-namespace-from', - [ - '@fullstory/babel-plugin-annotate-react', - { - native: true, - }, - ], - ], +// falls into this branch below) to resolve syntax plugins needed to parse the same syntax +// the web bundle uses. OXC handles JSX/TypeScript/env-target transforms natively for the +// web build, so this branch only lists the plugins ESLint's parser needs for syntax it +// wouldn't otherwise recognize. +const eslint = { + plugins: [['babel-plugin-react-compiler', ReactCompilerConfig]], }; const metro = { @@ -173,5 +162,5 @@ module.exports = (api) => { console.debug(' - running in: ', runningIn); } - return ['metro', 'babel-jest'].includes(runningIn) ? metro : web; + return ['metro', 'babel-jest'].includes(runningIn) ? metro : eslint; }; From 420ca7621a7466e719fa3ab053ada65c9f189c8c Mon Sep 17 00:00:00 2001 From: rory Date: Sun, 12 Jul 2026 14:28:45 -0700 Subject: [PATCH 28/31] Remove unneeded babel-plugin-react-compiler from ESLint's babel config It's a transform-only plugin (no new syntax), so @babel/eslint-parser never needed it for parsing. Also fixes a knip false-positive this surfaced: plugins only referenced from babel.config.js's metro branch aren't traced by knip's babel-config scanner, same root cause as the existing babel-plugin-module-resolver etc. ignores. Co-authored-by: Cursor --- babel.config.js | 12 +++++------- knip.json | 3 +++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/babel.config.js b/babel.config.js index 543cbdead7e4..9b8156369869 100644 --- a/babel.config.js +++ b/babel.config.js @@ -32,13 +32,11 @@ function traceTransformer() { // this file entirely. This config object is still read, though — ESLint's // @babel/eslint-parser loads babel.config.js for every file it lints (caller name is // always undefined or '@babel/eslint-parser', never 'metro'/'babel-jest', so it always -// falls into this branch below) to resolve syntax plugins needed to parse the same syntax -// the web bundle uses. OXC handles JSX/TypeScript/env-target transforms natively for the -// web build, so this branch only lists the plugins ESLint's parser needs for syntax it -// wouldn't otherwise recognize. -const eslint = { - plugins: [['babel-plugin-react-compiler', ReactCompilerConfig]], -}; +// falls into this branch below). It's empty because none of the .js/.jsx/.mjs/.cjs files +// ESLint parses via this route (.ts/.tsx go through @typescript-eslint/parser instead) +// currently need a syntax plugin the parser doesn't already support by default — this +// object only exists as the fallback `module.exports` below must return. +const eslint = {}; const metro = { presets: [require('@react-native/babel-preset')], diff --git a/knip.json b/knip.json index 75f2613a5f66..f4154648c2e4 100644 --- a/knip.json +++ b/knip.json @@ -58,9 +58,12 @@ "lodash", "@babel/plugin-proposal-private-methods", "@babel/plugin-proposal-private-property-in-object", + "@babel/plugin-transform-export-namespace-from", "babel-plugin-module-resolver", + "babel-plugin-react-compiler", "babel-plugin-transform-remove-console", "@fullstory/babel-plugin-react-native", + "@fullstory/babel-plugin-annotate-react", "eslint-config-airbnb-typescript", "eslint-config-prettier", "eslint-plugin-storybook", From b4c337e421927f67481d4d369a2c0a46e6186f4f Mon Sep 17 00:00:00 2001 From: rory Date: Sun, 12 Jul 2026 14:41:03 -0700 Subject: [PATCH 29/31] Simplify babel config --- babel.config.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/babel.config.js b/babel.config.js index 9b8156369869..4933277b0df5 100644 --- a/babel.config.js +++ b/babel.config.js @@ -27,17 +27,6 @@ function traceTransformer() { }; } -// Rsbuild's web build no longer reads this: its JS/TS/JSX pipeline (see -// config/rsbuild/rsbuild.common.ts) calls OXC directly with configFile:false, bypassing -// this file entirely. This config object is still read, though — ESLint's -// @babel/eslint-parser loads babel.config.js for every file it lints (caller name is -// always undefined or '@babel/eslint-parser', never 'metro'/'babel-jest', so it always -// falls into this branch below). It's empty because none of the .js/.jsx/.mjs/.cjs files -// ESLint parses via this route (.ts/.tsx go through @typescript-eslint/parser instead) -// currently need a syntax plugin the parser doesn't already support by default — this -// object only exists as the fallback `module.exports` below must return. -const eslint = {}; - const metro = { presets: [require('@react-native/babel-preset')], plugins: [ @@ -160,5 +149,5 @@ module.exports = (api) => { console.debug(' - running in: ', runningIn); } - return ['metro', 'babel-jest'].includes(runningIn) ? metro : eslint; + return ['metro', 'babel-jest'].includes(runningIn) ? metro : {}; }; From 2a3647eed2e0ad49596f1ebc1b540603fa1c3b42 Mon Sep 17 00:00:00 2001 From: rory Date: Sun, 12 Jul 2026 15:15:18 -0700 Subject: [PATCH 30/31] Simplify comments --- babel.config.js | 3 +-- config/rsbuild/rsbuild.common.ts | 44 +++++++------------------------- 2 files changed, 10 insertions(+), 37 deletions(-) diff --git a/babel.config.js b/babel.config.js index 4933277b0df5..e70c85612eb1 100644 --- a/babel.config.js +++ b/babel.config.js @@ -142,8 +142,7 @@ module.exports = (api) => { // For `react-native` (iOS/Android) caller will be "metro" // For jest, it will be babel-jest - // The web build and Storybook (Rsbuild) don't call into this file at all — their JS/TS/JSX - // pipeline goes through OXC directly (see config/rsbuild/rsbuild.common.ts), bypassing Babel. + // The web build and Storybook (Rsbuild) don't call into this file at all const runningIn = api.caller((args = {}) => args.name); if (!process.env.KNIP) { console.debug(' - running in: ', runningIn); diff --git a/config/rsbuild/rsbuild.common.ts b/config/rsbuild/rsbuild.common.ts index 088f4b981a57..bc870aa63227 100644 --- a/config/rsbuild/rsbuild.common.ts +++ b/config/rsbuild/rsbuild.common.ts @@ -86,8 +86,7 @@ const INCLUDED_NODE_MODULES = [ '@shopify/react-native-skia', ]; -// Matches node_modules paths that ARE in the allowlist above — these get transformed through the -// same OXC + React Compiler pipeline as app source (see the module rule below that uses this regex). +// Matches node_modules paths that are in the allowlist above const includedNodeModulesRegex = new RegExp(`node_modules/(${INCLUDED_NODE_MODULES.join('|')})`); const environmentToLogoSuffixMap: Record = { @@ -182,14 +181,7 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => }), ], tools: { - // Rsbuild's default 'js' rule (builtin:swc-loader, its 'js' oneOf branch) matches every - // .js/.ts/.jsx/.tsx file, including everything our own module rules below already handle. Rules - // declared later in `module.rules` run their loaders FIRST (closest to raw source), and - // our custom rules are added via `addRules` (which appends after Rsbuild's defaults), so - // without this exclude, swc-loader would strip JSX/TypeScript before the Fullstory - // annotation loader or OXC's React Compiler ever see the original JSX — silently - // defeating both. Only the 'js' oneOf branch is touched, so worker/raw/text imports - // (other oneOf branches on the same parent rule) are unaffected. + // Skip default .js loader that strips JSX/TypeScript and breaks Fullstory/React Compiler transforms we add later via rules bundlerChain: (chain) => { chain.module .rule('js') @@ -210,19 +202,14 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => __dirname: 'mock', }; // We can ignore the "module not installed" warning from lottie-react-native because we - // are not using the library for JSON format of Lottie animations. We also ignore - // oxc-react-compiler-loader's demoted React Compiler diagnostics (see that file) — - // they're deliberately warnings, not errors, matching babel-plugin-react-compiler's - // default bailout behaviour, so Storybook's `--smoke-test` shouldn't fail the build on them. + // are not using the library for JSON format of Lottie animations. + // We also ignore React Compiler errors - they're deliberately warnings, not errors. // eslint-disable-next-line no-param-reassign config.ignoreWarnings = [...(config.ignoreWarnings ?? []), /lottie-react-native\/lib\/module\/LottieView\/index\.web\.js/, /oxc-react-compiler-loader:/]; - // A list of paths rspack trusts would not be modified while rspack is running. - // Onyx and react-native-live-markdown can be modified on the fly, changes to other - // node_modules would not be reflected live. Applies to `dev`, `build`, and Storybook - // alike: persisting the module graph + OXC/Fullstory loader outputs to disk on every - // command (not just `dev`) is what makes repeated local `npm run build` runs fast — - // without it, every build re-runs every loader from scratch regardless of what changed. + // Cache rules: + // - Onyx and react-native-live-markdown can be modified on the fly, changes to other node_modules are not reflected live + // - Applies to `dev`, `build`, and Storybook alike // eslint-disable-next-line no-param-reassign config.cache ??= {type: 'persistent'}; if (typeof config.cache === 'object' && config.cache.type === 'persistent') { @@ -282,13 +269,6 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => // Exclude ALL node_modules (including the included-node_modules allowlist, handled below). exclude: [/node_modules/, /\.native\.(js|jsx|ts|tsx)$/], use: [ - // Worklets, then React Compiler + all other OXC transforms (see - // getOxcAndWorkletsLoaders). worklets-loader skips the Babel - // parse/transform/codegen cycle for files that can't reference a - // worklet (see that file) — ~97.5% of src/, verified by grep. - // oxc-react-compiler-loader calls oxc-transform directly and demotes - // non-fatal React Compiler diagnostics to warnings instead of hard build - // errors (workaround for oxc-project/oxc#23587). ...getOxcAndWorkletsLoaders(), // Fullstory annotation. { @@ -296,11 +276,8 @@ const getSharedConfiguration = ({file = '.env'}: Environment): RsbuildConfig => }, ], }, - // Included node_modules (see includedNodeModulesRegex above). Same OXC + React - // Compiler pass as app source above (oxc-react-compiler-loader's per-file - // bailout, see that file, handles any Rules-of-React violations in these - // packages) minus the Fullstory pass, which only makes sense for our own - // components. + // Included node_modules: Same OXC + React Compiler pass as app source above, + // minus the Fullstory pass, which only makes sense for our own components. { test: /\.(js|ts)x?$/, include: [includedNodeModulesRegex], @@ -441,9 +418,6 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): }, }, tools: { - // getSharedConfiguration's own `tools.bundlerChain` (JSX/TS rule excludes) isn't otherwise - // inherited here since the explicit `tools: {...}` below replaces (rather than merges with) - // `...shared`'s tools object. bundlerChain: shared.tools?.bundlerChain, rspack: (config, utils) => { // `sharedRspackTool`'s declared return type includes `Promise` because From b341897a326be4a57b2eb8ea0e26400a61d9a9dd Mon Sep 17 00:00:00 2001 From: rory Date: Sun, 12 Jul 2026 15:26:54 -0700 Subject: [PATCH 31/31] Remove dead extractTsconfigOptions from oxc-react-compiler-loader Its only output (target) is unconditionally overwritten by the hardcoded target: options.target that follows it in the same object spread, so the tsconfig lookup and target-mapping logic never had any effect. Drops the now-unused get-tsconfig dependency too. Co-authored-by: Cursor --- .../loaders/oxc-react-compiler-loader.mjs | 37 ------------------- package-lock.json | 1 - package.json | 1 - 3 files changed, 39 deletions(-) diff --git a/config/rsbuild/loaders/oxc-react-compiler-loader.mjs b/config/rsbuild/loaders/oxc-react-compiler-loader.mjs index 252cdbf7053f..e36b63ee3599 100644 --- a/config/rsbuild/loaders/oxc-react-compiler-loader.mjs +++ b/config/rsbuild/loaders/oxc-react-compiler-loader.mjs @@ -9,44 +9,9 @@ * We need the workaround for now until a new oxc release containing the fix adds back `reactCompiler`. */ -import {getTsconfig} from 'get-tsconfig'; import path from 'node:path'; import {transform} from 'oxc-transform'; -function extractTsconfigOptions(rootContext) { - try { - const tsconfig = getTsconfig(rootContext); - if (!tsconfig) { - return {}; - } - const {compilerOptions} = tsconfig.config; - if (!compilerOptions) { - return {}; - } - const opts = {}; - if (compilerOptions.target) { - const map = { - ES2015: 'es2015', - ES2016: 'es2016', - ES2017: 'es2017', - ES2018: 'es2018', - ES2019: 'es2019', - ES2020: 'es2020', - ES2021: 'es2021', - ES2022: 'es2022', - ESNEXT: 'esnext', - }; - const t = map[compilerOptions.target.toUpperCase()]; - if (t) { - opts.target = t; - } - } - return opts; - } catch { - return {}; - } -} - function getLang(ext) { if (ext === 'tsx') { return 'tsx'; @@ -64,14 +29,12 @@ export default async function oxcReactCompilerLoader(source) { try { const options = this.getOptions() || {}; const sourceMaps = options.sourcemap !== undefined ? options.sourcemap : !!this.sourceMap; - const tsconfigOptions = extractTsconfigOptions(this.rootContext); const resourcePath = this.resourcePath; const ext = path.extname(resourcePath).slice(1); const lang = getLang(ext); const transformOptions = { - ...tsconfigOptions, lang, target: options.target, jsx: options.jsx, diff --git a/package-lock.json b/package-lock.json index 66011850d66d..353d9169af87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -244,7 +244,6 @@ "eslint-plugin-testing-library": "^7.11.0", "eslint-plugin-you-dont-need-lodash-underscore": "^6.14.0", "eslint-seatbelt": "^0.1.3", - "get-tsconfig": "^4.10.0", "glob": "^10.4.5", "googleapis": "^152.0.0", "http-server": "^14.1.1", diff --git a/package.json b/package.json index 8bf0823a229f..2945f72f1246 100644 --- a/package.json +++ b/package.json @@ -317,7 +317,6 @@ "eslint-plugin-testing-library": "^7.11.0", "eslint-plugin-you-dont-need-lodash-underscore": "^6.14.0", "eslint-seatbelt": "^0.1.3", - "get-tsconfig": "^4.10.0", "glob": "^10.4.5", "googleapis": "^152.0.0", "http-server": "^14.1.1",