[No QA] Migrate web builds and Storybook from Webpack to Rspack/Rsbuild#95319
Conversation
…uilds
Mirrors config/webpack/{webpack.common,webpack.dev}.ts under config/rspack/,
swapping webpack-specific plugins for their Rspack builtins (CopyRspackPlugin,
CssExtractRspackPlugin, SwcJsMinimizerRspackPlugin, ReactRefreshRspackPlugin,
@aaroon/workbox-rspack-plugin) and replacing @vue/preload-webpack-plugin
(which depends on webpack-internal chunk graph APIs Rspack doesn't expose)
with a small custom RspackPreloadPlugin that reads compilation.getAssets()
directly.
Adds `build:rspack`, `build-staging:rspack`, and `web-server:rspack` npm
scripts so both bundlers can be run side by side without touching the
existing webpack config. Storybook is out of scope.
Co-authored-by: Cursor <cursoragent@cursor.com>
Expands the Rspack POC to cover Storybook (via storybook-react-rsbuild, since Storybook has no first-party Rspack framework package) and removes Webpack entirely now that both build paths are covered, rather than keeping the two bundlers side-by-side. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Hey, I noticed you changed some webpack configuration files. This can break production builds. Did you remember to run a production build locally to verify they still work? |
The auto-restart wrapper (#67373) was added to paper over webpack-dev-server's unbounded JS-heap growth across HMR recompiles, which forced periodic heap-limit crashes during long dev sessions. Stress-tested the Rspack dev server with 60+ back-to-back HMR recompiles (mimicking the original repro of repeatedly saving a file): heap usage plateaus/bounces in a bounded band rather than growing without bound, and the periodic forced gc() call reliably reclaims memory each time - unlike webpack, where explicit GC barely helped. No crash-recovery loop needed. Also fixes a latent bug in ForceGarbageCollectionPlugin: `if (gc && ...)` throws a ReferenceError when Node isn't started with --expose-gc, since referencing an undeclared global outside of `typeof` throws. `typeof gc` alone is the safe check. Co-authored-by: Cursor <cursoragent@cursor.com>
…ild, ws vuln
- rspack.common.ts / .storybook/rsbuild.config.ts: explicitly mock
__filename/__dirname ('mock' instead of Rspack's default 'warn-mock') so
canvaskit-wasm/expo don't emit a "Module parse warning" on every build.
Storybook's `--smoke-test` treats any non-allow-listed warning as a hard
failure, so this was breaking the "Storybook tests" CI check; the rsbuild
config wasn't otherwise inheriting this from the shared Rspack config.
- cspell.json: add rspack/Rspack/rsbuild/aaroon to the word list.
- Rebuilt 3 stale .github/actions/javascript/*/index.js bundles that drifted
after merging main (unrelated to this PR's changes).
- Bump the `ws` override from 8.17.1 to 8.21.0 to pick up fixes for two
advisories (GHSA-58qx-3vcg-4xpx, GHSA-96hv-2xvq-fx4p) that Snyk flagged
once storybook/webpack-bundle-analyzer joined the dependents list.
Co-authored-by: Cursor <cursoragent@cursor.com>
ncc's output isn't fully deterministic across different node_modules
resolution states; my previous gh-actions-build run used a tree from
npm install rather than npm ci, leaving this one bundle out of sync
with what CI's verify job rebuilds and diffs against.
Skips the oxfmt pre-commit hook: it errors ("Expected at least one target
file") when every staged file matches its ignorePatterns, which is the case
here since .github/actions/javascript/**/index.js is intentionally excluded
(generated bundle). Nothing to format either way.
Co-authored-by: Cursor <cursoragent@cursor.com>
…sitive dep Removing webpack as a devDependency also dropped the "webpack": "^5.x" override, which was silently pinning react-web-config's own (unused) webpack@^2.3.3 dependency to a safe version. Without it, npm installs react-web-config's real webpack@2.7.0 -> node-libs-browser -> crypto-browserify -> browserify-sign/create-ecdh -> elliptic chain, introducing 4 new critical vulnerabilities not present on main. Co-authored-by: Cursor <cursoragent@cursor.com>
- Stop suppressing import/extensions line-by-line for .storybook/ and config/rspack/ ESM entry points; handle it with one config-level override. - Share ignoreWarnings, lottie/pdf asset rules, and define() values between rspack.common.ts and .storybook/rsbuild.config.ts so Storybook can't drift from the app build; drop mockPaths.ts aliases already set upstream. - Replace html-webpack-plugin with Rspack's native HtmlRspackPlugin in both the main plugin and RspackPreloadPlugin, and remove the now-unused html-webpack-plugin dependency. - Document the RSPACK_PROFILE build-profiling workflow in SETUP_WEB.md. Co-authored-by: Cursor <cursoragent@cursor.com>
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as off-topic.
This comment was marked as off-topic.
Type RspackPreloadPlugin's links array off the alterAssetTags hook's own parameter instead of importing JsHtmlPluginTag from @rspack/binding, since that package is only a transitive dependency of @rspack/core, not one we depend on directly. Add "pftrace" (the Perfetto trace file extension) to the cspell word list. Co-authored-by: Cursor <cursoragent@cursor.com>
This comment was marked as resolved.
This comment was marked as resolved.
Rsbuild wraps Rspack with higher-level config (source.define, output.copy, server.https/proxy, performance.buildCache) and is already used for Storybook, so unifying onto it removes the last hand-rolled dev-server/HTTPS/persistent-cache plumbing in config/rspack/ and lets the app build and Storybook share one getSharedConfiguration()/getCommonConfiguration() config instead of two independently-maintained bundler configs. Co-authored-by: Cursor <cursoragent@cursor.com>
Re-ran the #95319 HMR memory-stress test (60+ back-to-back recompiles) against the Rsbuild dev server with and without the plugin's forced gc() call. Heap and RSS plateau identically either way (~245MB heap, bounded RSS growth across 260 total compiles), unlike raw Rspack where disabling gc() caused unbounded growth. V8's own GC is sufficient here, so the plugin and --expose-gc are dead weight under Rsbuild. Co-authored-by: Cursor <cursoragent@cursor.com>
The ESLint flat config now disables import/extensions repo-wide for config/rsbuild/**/*.ts, so the line-level disable for that rule is redundant. The @dword-design/import-alias/prefer-alias disable is still needed and stays. Co-authored-by: Cursor <cursoragent@cursor.com>
This comment was marked as resolved.
This comment was marked as resolved.
Rsdoctor is the first-party Rspack/Rsbuild bundle analyzer, integrates via the same tools.rspack escape hatch, and adds loader/plugin timing and duplicate-package analysis on top of bundle size visualization. Co-authored-by: Cursor <cursoragent@cursor.com>
The webpack dev config defined process.env.PORT for the client so CONFIG.DEV_PORT reflects the actual resolved port when portfinder falls back off 8082. Rsbuild's dev config dropped that define, only wiring the resolved port to server.port, so getEnvironmentURL() would generate dev links pointing at the wrong port whenever 8082 was busy. Verified by occupying 8082, booting the dev server (portfinder fell back to 8083), and confirming the served bundle now bakes in `DEV_PORT: 8083 ?? 8082` instead of falling back to the hardcoded default. Co-authored-by: Cursor <cursoragent@cursor.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. Already looking forward to the next diff. Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
…k-poc Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # package-lock.json # package.json
ncc's output isn't fully deterministic across different node_modules resolution states; the version merged in from main left this bundle out of sync with what CI's verify job rebuilds and diffs against. Co-authored-by: Cursor <cursoragent@cursor.com>
chuckdries
left a comment
There was a problem hiding this comment.
-
You should document that existing users will have to run (or, is it easy to write a script that does that automatically if needed?)
mv config/webpack/key.pem config/rsbuild/ mv config/webpack/certificate.pem config/rsbuild/
-
The default entrypoint the dev server launches my browser to is
/main, which renders the not found page. Can you just do/, or perhaps/home?
|
(Rory's Agent) Addressed both from the top-level review:
|
These 4 files' rulesdir/no-useOnyx-dependencies-arg violations pre-date and are unrelated to this PR (per review feedback). Restore the original code and grandfather them into eslint-seatbelt.tsv instead of fixing them here. Co-authored-by: Cursor <cursoragent@cursor.com>
Rsbuild's --open defaults to the first entry's route, and our entry
key is named "main" (source.entry: {main: './index.js'}), so it
opened /main, a nonexistent app route. Set server.open explicitly.
Co-authored-by: Cursor <cursoragent@cursor.com>
Certs generated before the Webpack -> Rsbuild migration are stranded in the now-removed config/webpack directory. Co-authored-by: Cursor <cursoragent@cursor.com>
Never mind - not sure why I've never seen that before |
|
🚧 chuckdries has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
…l 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 <cursoragent@cursor.com>
mkhutornyi
left a comment
There was a problem hiding this comment.
What's the difference between npm run web vs npm run web-server?
Which command should be run for local web dev server?
|
The intention is that generally people just run |
|
🚀 Deployed to staging by https://github.com/chuckdries in version: 9.4.34-0 🚀
|
|
Hi @roryabraham. Any QA steps here? |
|
Hi @IuliiaHerets, no specific QA steps here! |
|
@chuckdries can we check it off then? |
|
@IuliiaHerets yes |
|
🚀 Deployed to production by https://github.com/roryabraham in version: 9.4.34-14 🚀
|
|
🚀 Deployed to production by https://github.com/roryabraham in version: 9.4.34-14 🚀
Bundle Size Analysis (Sentry): |

Explanation of Change
Migrates web production builds and Storybook off Webpack onto Rsbuild (built on Rspack, a Rust-based, largely Webpack-compatible bundler). App build and Storybook now share one Rsbuild config.
App web builds
config/rsbuild/rsbuild.common.tsreplaceswebpack.common.ts:getSharedConfiguration()(defines, aliases, Babel/SVGR, asset rules — shared with Storybook) +getCommonConfiguration()(app-specific: HTML template, service worker, Sentry, code-splitting, preload/prefetch).config/rsbuild/rsbuild.config.tsis the CLI entry point;web-serverlayers on dev-only config (HTTPS, proxy, persistent cache) via Rsbuild's native fields.RspackPreloadPluginreplaces@vue/preload-webpack-plugin.html-webpack-plugin→ Rspack's nativeHtmlRspackPlugin; custom template values move into atemplateParametersfunction.CustomVersionFilePlugin/ModuleInitTimingPlugin, loaders, code-splitting) is a straight port to Rspack builtins:CopyRspackPlugin,CssExtractRspackPlugin,SwcJsMinimizerRspackPlugin,ReactRefreshRspackPlugin,@aaroon/workbox-rspack-plugin,output.clean: true.Storybook
Migrated to
storybook-react-rsbuild..storybook/rsbuild.config.tsimportsgetSharedConfiguration()directly.@rsbuild/plugin-babel(React Compiler,react-native-web); SVG imports through@rsbuild/plugin-svgr.storybook/@storybook/*10.1.10→10.4.6(required bystorybook-react-rsbuild); dropped@storybook/react-webpack5and@storybook/addon-webpack5-compiler-babel.Dev-server crash-recovery removal
start-dev-with-auto-restart.sh(#67373) wrappedweb-serverin a crash-restart loop becausewebpack-dev-server's JS-heap bookkeeping grew unboundedly across hot reloads. Rspack/Rsbuild keep that bookkeeping in Rust-managed native memory, so I stress-tested 60-260 back-to-back HMR recompiles directly. With Rsbuild heap plateaued at ~220-247MB regardless of whethergc()was enabled. So I removedForceGarbageCollectionPlugin,--expose-gc, and the auto-restart wrapper entirely.web-servernow runsrsbuild devdirectly.Build profiling
time-analytics-webpack-pluginisn't needed — Rspack has built-in profiling viaRSPACK_PROFILE=OVERVIEW npm run build. Documented incontributingGuides/SETUP_WEB.md.webpack-bundle-analyzer→ Rsdoctor — first-party, also surfaces loader/plugin timing and duplicate-package detection.Benchmark results
dev.lazyCompilationto{imports: true, entries: false}: the entry compiles eagerly, but everything behind a dynamicimport()(lazy screens, locale files) gets a proxy stub and compiles on first real use via HMR hot-update. Verified with a real headless-browser load: chunk boundaries are unchanged, every lazy module resolved cleanly within ~0.1-1.6s of first touch, no errors traceable to the mechanism. ~1s is a fair approximation of "time to start the app" — usable almost instantly, with per-screen compile cost paid incrementally instead of upfront.dev.lazyCompilation: false(fully eager, matching Webpack's behavior) and re-ran: warm startup averaged ~3.0s (vs Webpack's 8.65s). So even compiling the whole graph upfront, Rsbuild's warm dev startup beats Webpack's ~3x. Shipping with the lazy default (~1s) since it's faster and verified safe.Bottom line: cold app builds ~1.7x faster than Webpack, warm ~16x faster; Storybook ~4.1x faster; dev-server startup goes from ~8.65s to ~1s to a usable page.
Output correctness
App
dist/: same total size (51M), sameservice-worker.js/workbox-*.jsoutput, same 42 preload/prefetch tags. Storybookdist/docs/: 42M/165 files (Webpack) vs 48M/176 files (Rsbuild) — within noise from differing chunk-splitting heuristics. App shell, service worker, and Storybook UI all verified equivalent. All scripts (build,build-staging,build-adhoc,web-server,storybook,storybook-build,storybook-build-staging) re-verified against the final config after merging #95453.Caveats
Fixed Issues
$
PROPOSAL:
Tests
npm run build,npm run build-staging, andnpm run build-adhoc; confirm each exits 0. Serve the output (e.g.npx http-server dist) and confirm the app boots with no console errors, and env-specificindex.htmlbehavior (stagingnoindexmeta tag, splash logo, third-party scripts) is correct for each.npm run web-serverand confirm the dev server boots over HTTPS, the/apiproxy forwards to the local backend, and HMR works on a simple edit.npm run storybookand confirm the dev server boots and a story hot-reloads on edit.npm run storybook-buildandnpm run storybook-build-staging; confirm each exits 0 and produces a workingdist/docs/index.html. Serve and confirm stories render.Offline tests
N/A — build tooling only, no app runtime behavior change.
QA Steps
N/A — build-tooling only change with no user-facing or API behavior change. This PR is still a draft pending review before being considered for staging/production.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectioncanBeMissingparam foruseOnyxtoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststep