Hi all,
One of the roadmap threads, alongside the UI one, the backend one and Path to Vitest: what it takes to get the UI off an ejected create-react-app and onto Vite. The test runner is the companion thread, since the two are independent.
What CRA is
create-react-app was the zero-config starter for React projects: it owned the webpack, babel and jest setup behind a single react-scripts dependency so applications did not have to. AWX ejected from it, which copies that machinery into the repository for good, and awx/ui/scripts and awx/ui/config are what came out. CRA was sunset in February 2025, and react-dev-utils, which those ejected scripts still import, last shipped in April 2022.
What Vite is
Vite is the dev server and build tool that replaces webpack here, serving source over native ES modules while you work and building with Rollup for production. Because nothing is bundled before the dev server answers, start and reload times stop scaling with the size of the application. It is what most of the React ecosystem moved to as CRA wound down.
The move
Two pieces of the build move at once, because Vite supplies the bundler as well as the dev server:
create-react-app, ejected -> Vite
webpack 5.108.1 -> Rollup, through Vite
No runtime effect: the shipped bundle is the artifact, so a regression shows up in the UI test suite and in a manual pass over the form-heavy screens.
What it buys
29 of the 62 devDependencies exist only to drive webpack and babel: webpack and eight plugins, five loaders, five postcss packages, the babel core, presets and runtime, and react-dev-utils. A few of those get Vite equivalents rather than disappearing outright, but the bulk of the set goes.
typescript goes with them, and it is worth naming on its own. It sits at ^6.0.3 in devDependencies and nothing in src uses it: there are no .ts or .tsx files and no tsconfig.json. The only references are config/modules.js and config/webpack.config.js, resolving its path for a CRA type checker that has nothing to check.
It also removes the standing pressure to keep a dead toolchain patched. The ESLint 10 hold is downstream of exactly that: three plugin peer ranges still cap at ^9, and the ejected scripts are why the cap matters.
What has to keep working
- lingui: the build runs
lingui compile in four prescripts and uses @lingui/babel-plugin-lingui-macro. Vite needs @lingui/vite-plugin.
- styled-components:
config/babel/jsx-compat-plugin.js wraps babel-plugin-styled-components. The Vite path is the SWC or babel plugin, and displayName behaviour wants checking against snapshots.
- The Makefile:
ui-devel shells out to the npm scripts. That target keeps its name and changes what it invokes.
The sequence
Three steps, each reviewable on its own:
- Vitest first, since tests are independent of the bundler.
- Vite for the dev server, keeping webpack for production builds.
- Vite for production, then delete
scripts/, config/webpack* and the bundler devDependencies.
Two and three land together in the end. Splitting them means an index.html that has to satisfy webpack and Vite at the same time, which is harder to review than the whole move.
The devDependency count came out at 34 rather than 29, 62 down to 28, and typescript went with them as expected. What the sizing did not see was that index.html is a Django template rather than just an entry point: awx/ui/build is a template directory and awx/ui/build/static a staticfiles directory, so the output layout is a contract rather than a preference.
Pull requests
Thanks for reading.