build: move the UI off create-react-app and onto Vite - #916
Conversation
29e6d1c to
2529863
Compare
2529863 to
75ec2f7
Compare
|
Fixing some errors |
|
I am getting sporatic test failures (always a different test) after around 190+ seconds. Not sure if we are hitting a timeout or something. |
|
Working on it |
Steps two and three of the sequence in the Path to Vite thread, taken
together. Splitting them would mean an index.html that has to satisfy two
bundlers at once, which is worse to review than the whole move.
The ejected create-react-app machinery goes: scripts/build.js, scripts/start.js,
config/webpack.config.js, config/webpackDevServer.config.js, and the env,
paths, modules, getHttpsConfig, themeMetaLoader, webpack and devUtils files
beside them. 34 devDependencies go with it, 62 down to 28, typescript among
them, which the thread called out for having no .ts file to check.
Three pieces of source were written against webpack rather than against a
bundler, and each is now written against neither:
themeRegistry.js discovered the shipped themes with require.context, twice,
once for the stylesheets and once through config/themeMetaLoader.js, a
webpack loader that read a /* Name: */ comment out of each file. Both are
import.meta.glob now, the metadata parsing inlined. This was the one real
blocker the sizing named. It also means the module works under the test
runner, so testUtils/themeRegistryMock.js and its alias are deleted rather
than ported.
setupCSP.js assigned __webpack_nonce__, which only webpack understands.
styled-components looks for a <meta name="sc-nonce"> before it looks for
that global, so Django renders the nonce into index.html and the source
stops caring what bundles it.
i18nLoader.js imported `./locales/${locale}/messages`. The static part of a
dynamic import has to carry an extension or the catalogues are not bundled.
index.html moves to the root as Vite expects. It stays a Django template,
because awx/ui/build is a template directory and awx/ui/build/static is a
staticfiles directory, so the output layout is a contract rather than a
preference: build/static/{js,css,media} is what the Makefile copies and what
STATICFILES_DIRS points at. The {% load static %}, the nonce and the policy
are injected at build time by a plugin in vite.config.mjs, which keeps the
file servable as-is by the dev server.
Three things the build got wrong before the output was read rather than
trusted. A web worker was written to build/assets, which Django does not
serve, so it would have 404'd at runtime with the build reporting success.
The favicon was linked twice, once for the dev server and once through
{% static %}. And server.https: {} leaves Vite with no certificate, where
HTTPS=true generated one, so @vitejs/plugin-basic-ssl keeps the dev server on
https://127.0.0.1:3001 as CONTRIBUTING.md documents.
The babel pass and the alias list are shared with vitest.config.mjs rather
than restated, so the application and its tests cannot disagree about what a
module means. Babel transforms the JSX itself because this codebase keeps JSX
in .js files, a create-react-app convention, and Vite 8 offers no way to say
so.
Two things fixed in passing, both already broken on main. awx/ui/Dockerfile
adds .eslintignore, .eslintrc.json and .linguirc, none of which have existed
since the flat eslint config landed. And the babel block in package.json was
dead configuration that only eslint's parser still read, which is why eslint
started failing on every file the moment its plugin was uninstalled.
Verified: production build clean with the output layout checked file by file,
dev server serving over https in 458ms with the JSX, lingui macro and theme
glob confirmed through it, full UI suite 553 files and 2985 tests green,
eslint and prettier clean.
75ec2f7 to
56697bf
Compare
|
Thanks, the varying counts with no failures is what points at it. That run was against Worth a pull and another run. I could not reproduce the worker exit here, with the heap capped at 1.5GB, with and without a |
Vitest splits jest's one timeout in two, and only testTimeout came across with the rest of the config. hookTimeout kept its 10s default, which a beforeEach that renders a whole form can outrun while every other worker in the vm pool is rendering one of its own. Testing Library's findBy* and waitFor have a third timeout, again separate, and its one second default is the same story. Neither masks a real failure: a query that will never match still fails, it just takes longer to say so. Measured on the Template screens, 264 tests: without these, four of seven runs failed somewhere. With them, eight runs of that directory and a full run of all 553 files pass.
|
Tests are not completing for me as is. My dev server has 12 vcpus and 48 GB RAM. When I start the tests, I have 36GB unused, and the process dies with a OOM error before it finishes. At about that time, the process has chewed through roughly 20GB (plenty of ram left on the server). Sometimes it will finish but 4/5 runs dies OOM. I see we are using Running it with |
There was a problem hiding this comment.
🟡 Changes recommended
The containerized dev server is unreachable externally, browser targeting no longer follows the declared policy, and the Node tooling does not meet the pnpm requirement.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Replaces the ejected Create React App/Webpack toolchain with Vite while preserving Django static assets, CSP, themes, localization, workers, and Vitest integration.
Changes:
- Adds shared Vite/Vitest Babel and alias configuration.
- Converts Lingui catalogs to ESM and updates theme/worker handling.
- Removes obsolete Webpack infrastructure and dependencies.
File summaries
| File | Description |
|---|---|
Makefile |
Documents the Vite development target. |
awx/ui/vitest.config.mjs |
Reuses shared Babel and alias configuration. |
awx/ui/vite.config.mjs |
Defines the Vite build, proxy, assets, workers, and Django template integration. |
awx/ui/testUtils/themeRegistryMock.js |
Removes the obsolete Webpack-specific theme mock. |
awx/ui/testUtils/rtlContexts.js |
Uses the ESM Lingui catalog export. |
awx/ui/src/util/validators.test.js |
Updates the English catalog import. |
awx/ui/src/util/simulationWorker.js |
Uses stable absolute dependency URLs. |
awx/ui/src/util/getRelatedResourceDeleteDetails.test.js |
Updates the catalog import. |
awx/ui/src/util/debounce.test.js |
Restores real timers after tests. |
awx/ui/src/themeRegistry.js |
Replaces require.context with Vite globs. |
awx/ui/src/setupTests.js |
Adjusts Testing Library timeouts and timer cleanup. |
awx/ui/src/setupProxy.js |
Removes the Webpack proxy module. |
awx/ui/src/setupCSP.js |
Removes the Webpack nonce global. |
awx/ui/src/screens/WorkflowApproval/shared/WorkflowApprovalUtils.test.js |
Loads the ESM catalog. |
awx/ui/src/screens/Template/shared/PlaybookSelect.test.js |
Uses the named catalog export. |
awx/ui/src/screens/Inventory/shared/ConstructedInventoryHint.test.js |
Uses the named catalog export. |
awx/ui/src/screens/Inventory/ConstructedInventoryDetail/ConstructedInventorySyncButton.test.js |
Uses the named catalog export. |
awx/ui/src/screens/Instances/Shared/RemoveInstanceButton.test.js |
Uses the named catalog export. |
awx/ui/src/locales/en/messages.js |
Removes the CommonJS catalog. |
awx/ui/src/i18nLoader.js |
Dynamically imports ESM catalogs. |
awx/ui/src/customTheme.js |
Updates bundler-related documentation. |
awx/ui/scripts/start.js |
Removes the CRA development launcher. |
awx/ui/scripts/build.js |
Removes the CRA production builder. |
awx/ui/public/index.html |
Removes the CRA HTML template. |
awx/ui/package.json |
Adds Vite and removes Webpack dependencies and scripts. |
awx/ui/package-lock.json |
Updates the npm dependency graph. |
awx/ui/lingui.config.js |
Emits ES-module catalogs. |
awx/ui/index.html |
Adds the Vite/Django HTML entry point. |
awx/ui/eslint.config.mjs |
Ignores the Vite configuration. |
awx/ui/Dockerfile |
Packages the Vite configuration and entry point. |
awx/ui/config/webpackDevServer.config.js |
Removes Webpack dev-server configuration. |
awx/ui/config/webpack/persistentCache/createEnvironmentHash.js |
Removes Webpack cache support. |
awx/ui/config/webpack/ModuleScopePlugin.js |
Removes CRA module-scope enforcement. |
awx/ui/config/webpack/ModuleNotFoundPlugin.js |
Removes Webpack error rewriting. |
awx/ui/config/webpack/InterpolateHtmlPlugin.js |
Removes CRA HTML interpolation. |
awx/ui/config/webpack.config.js |
Removes the Webpack build configuration. |
awx/ui/config/themeMetaLoader.js |
Removes the custom theme loader. |
awx/ui/config/paths.js |
Removes CRA path resolution. |
awx/ui/config/modules.js |
Removes CRA module resolution. |
awx/ui/config/getHttpsConfig.js |
Removes CRA HTTPS configuration. |
awx/ui/config/env.js |
Removes CRA environment handling. |
awx/ui/config/devUtils/printBuildError.js |
Removes build-error formatting. |
awx/ui/config/devUtils/formatWebpackMessages.js |
Removes Webpack message formatting. |
awx/ui/config/devUtils/fileSizeReporter.js |
Removes CRA bundle-size reporting. |
awx/ui/config/devUtils/eslintFormatter.js |
Removes the Webpack ESLint formatter. |
awx/ui/config/devUtils/devServerUtils.js |
Removes CRA server utilities. |
awx/ui/config/devUtils/colors.js |
Removes terminal-color helpers. |
awx/ui/config/devUtils/clearConsole.js |
Removes console-clearing support. |
awx/ui/config/devUtils/checkRequiredFiles.js |
Removes CRA file validation. |
awx/ui/config/devUtils/checkBrowsers.js |
Removes CRA browser-target validation. |
awx/ui/config/build/babel.mjs |
Shares Babel transformation across Vite and Vitest. |
awx/ui/config/build/aliases.mjs |
Centralizes source aliases. |
Review details
- Files reviewed: 50/69 changed files
- Comments generated: 3
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| server: { | ||
| port: 3001, |
| // /static/, so an absolute base is what makes the two line up. | ||
| base: '/', | ||
| publicDir: resolvePath('./public'), | ||
| build: { |
Three things the review asked for. The VM pool is what this suite's speed comes from, and the contexts it keeps are what it costs memory in. A thread pool holds them all in one process heap: on a twelve core machine the run reached 20GB and died in V8's own allocator, four runs in five, with the machine far from full. Forks give each worker its own heap, and the run takes the same time. The dev server binds to 0.0.0.0 and accepts any Host header, which is what the ejected one did. The UI image starts it and publishes the port, so the browser reaching it is not on the loopback interface Vite listens on by default. The build target is stated rather than left to Vite's default, because Vite does not read browserslist for JavaScript the way the ejected build did. package.json now declares the same set, so the policy and the build agree.
|
Thanks @cigamit
Two review points went in with it:
|
Sits on top of #914, which is still open, so the first commit here is that pull request unchanged and this one should land after it.
Steps two and three of Path to Vite, taken together. Splitting them the way the thread lays out would mean an
index.htmlthat has to satisfy webpack and Vite at the same time, and reviewing that is harder than reviewing the whole move.What goes
The ejected create-react-app machinery:
scripts/build.js,scripts/start.js,config/webpack.config.js,config/webpackDevServer.config.js, and theenv,paths,modules,getHttpsConfig,themeMetaLoader,webpackanddevUtilsfiles beside them. 17,202 lines deleted against 5,866 added.34 devDependencies go with them, 62 down to 28. The thread estimated 29, and
typescriptis in the set as it predicted: no.tsfile, notsconfig.json, nothing to check.The three pieces of source that were written against webpack
Each is now written against neither bundler rather than ported to Vite:
themeRegistry.jsusedrequire.contexttwice, once to pull the stylesheets into the build and once throughconfig/themeMetaLoader.js, a custom webpack loader that read a/* Name: */comment out of each theme and detected the dark variant. Both areimport.meta.globnow with the metadata parsing inlined, which is the one real blocker the sizing named. It also means the module works under the test runner, sotestUtils/themeRegistryMock.jsand its alias are deleted rather than ported.setupCSP.jsassigned__webpack_nonce__, which only webpack understands, and which would have thrown in a strict-mode ES module. styled-components v6 looks for a<meta name="sc-nonce">before it looks for that global, so Django now renders the nonce intoindex.htmland the source stops caring what bundles it. The blanket/* eslint-disable */at the top of that file went with it: it was there for the undeclared global, and the file lints clean with no suppression now, which is worth having since a blanket disable hid everything else in the file too.i18nLoader.jsimported`./locales/${locale}/messages`. The static part of a dynamic import has to carry an extension, or the import cannot be analysed and no catalogue is bundled.That one has a tail.
lingui.config.jssetcompileNamespace: 'cjs', so the compiled catalogues weremodule.exports = .... The production bundler converts CommonJS on the way through, so the built application was fine and every check passed, but the dev server hands modules to the browser as they are written and there is nomoduleto assign to.dynamicActivatethrew, and the application sat on its pre-i18n "Loading..." forever. It iscompileNamespace: 'es'now, which makes lingui emitmessages.mjs, and the import follows.index.html is a Django template, and that shapes the config
awx/ui/buildis inTEMPLATES.DIRSandawx/ui/build/staticis inSTATICFILES_DIRS, so the output layout is a contract rather than a preference:build/static/{js,css,media}is what the Makefile copies into/var/lib/awx/public/staticand what Django serves at/static/.The file moves to the repository root as Vite expects, and stays servable as plain HTML by the dev server. The
{% load static %}, the CSP nonce, the policy and the{% static %}favicon are injected at build time by a small plugin invite.config.mjs.The dev server proxy, and an option that should not have been there
src/setupProxy.jshanded webpack-dev-server a target and nothing else, and two departures from that turned out to matter. Both broke the dev server while leaving the built application perfectly healthy.changeOrigin: truereads as harmless and is not. It rewrites the Host header to the target, so the browser still sendsOrigin: https://localhost:3001while the request claims to be for:8043. Django compares the two,CSRF_TRUSTED_ORIGINSis empty, andPOST /api/login/comes back 403. The option is gone, which is what the original had.server.cors, which Vite enables by default, answers everyOPTIONSrequest itself as a preflight, 204 with no body, and never forwards it. This API usesOPTIONSto describe its fields, so any screen readingdata.actions.GETgotundefinedand threwCannot read properties of undefined. Jobs, Templates, Credentials and the rest of the list views all failed this way. It is off now: the API is reached through the proxy on the same origin, so there is nothing for CORS to permit in the first place.Three things the build got wrong before the output was read
The build exiting zero is not the same as the build being right, and none of these would have failed until runtime:
build/assets. That is not one of Django's static directories, sosimulationWorkerwould have 404'd in the browser while the build reported success. Workers take their ownrollupOptions.simulationWorker.jscallsimportScripts('d3-collection.v1.min.js')and four siblings, relative to the worker's own URL. That worked only because webpack emitted the worker into/static/js, beside those files. The dev server serves it from/src/util, so all five 404'd and the topology view threw. They are absolute/static/js/...paths now, which resolve identically under the dev server and under Django.{% static %}.server.https: {}leaves Vite with no certificate.HTTPS=truegenerated one under the ejected dev server, so@vitejs/plugin-basic-sslkeeps the UI onhttps://127.0.0.1:3001, which is what CONTRIBUTING.md tells people to open.Shared with the test config
The babel pass and the alias list live in
config/build/and are used by bothvite.config.mjsandvitest.config.mjs, so the application and its tests cannot come to disagree about what a module means. Babel transforms the JSX itself rather than leaving it to oxc, because this codebase keeps JSX in.jsfiles, a create-react-app convention, and Vite 8 offers no way to declare that.Two things already broken on main, fixed in passing
awx/ui/Dockerfileadds.eslintignore,.eslintrc.jsonand.linguirc. None have existed since the flat eslint config landed, sodocker buildthere fails on the first of them today.babelblock inpackage.jsonwas dead configuration that only eslint's parser still read. Removing one of its plugins from devDependencies made eslint fail to parse all 1,084 files, which is how it surfaced.The build chain, not just the build
npm run buildis not what CI or the image runs, so the whole chain was exercised from a wipedbuild/and flag file:npm cifrom an empty directory, with nothing butpackage.jsonand the lockfile, since an incrementally grownnode_modulescan hide a lockfile that does not resolve. 695 packages in 37s. On main the same command installs 1,676, so this removes 981 packages, a little under 60% of the tree.make ui-release, which is whatmake sdistreaches through$(UI_BUILD_FLAG_FILE)and therefore what the image build runs:npm ci,compilemessages.py,compile-strings, then the build. Clean, 9.62s.awx-manage collectstatic, the step in the Dockerfile that actually depends on this change, since it readsawx/ui/build/staticthroughSTATICFILES_DIRS. 100 files collected, and the application serves from the collected tree with the Playwright suite green against it.One thing that is worth knowing and is not a defect:
index.htmlis a Django template, so after a UI rebuild the web process has to be restarted before it serves the newly hashed filenames. The image bakes both before anything starts, so it only affects a running development stack.Checks
npm run buildclean, and the output checked file by file rather than trusted: onlybuild/staticat the top level, the worker insidestatic/js, one favicon link, fonts resolving both for the copiedpatternfly.min.cssand for the processed theme CSS.eslint .andprettier --checkclean.Both paths are also verified in a real browser against the running stack, which is what caught the catalogue format above. Django renders the template with a real nonce and no unrendered tags, every referenced asset returns 200 including the worker and the fonts, and the login screen renders with
data-themeapplied, no console errors, no failed requests and no CSP violations. The dev server on 3001 does the same.Signing in is exercised too, on both, which is what turned up the proxy option above and is also the only way to reach styled-components: it injects nothing until there is a page to style. After login it emits its stylesheet carrying the nonce from the
sc-noncemeta, 21 rules, no CSP violations, so the replacement for__webpack_nonce__is confirmed working rather than merely present.Worth saying plainly, because it is the argument for doing that last check at all. Two defects survived every automated gate here, and both were dev-server only:
compileNamespace: 'cjs'compileNamespace: 'es'changeOrigin: trueserver.corsdefault| worker
importScripts| the topology view on the dev server | all of the above, plus the topology view on the built application |None of the four is visible to anything this repository runs in CI, and they share a shape: the configuration ported cleanly, but anything that depended on where a file physically sat broke when the serving path changed.
The catalogue fix has its own tail, and it is the reason the row above lists what still passed. Moving lingui to ES modules made
messagesa named export where it had beenmodule.exports, and seven files import the compiled catalogue as a default. That broketestUtils/rtlContexts.js, and through it 502 of the 553 test files. The production build did not notice, because the bundler converts CommonJS on the way through, and neither did the Playwright suite, because it runs against the built application. Only the unit suite loads those catalogues directly. Three forms needed correcting: the default imports, the bare references at their call sites, and one dynamicimport()built from a template literal that a grep for static imports does not find.A pre-existing leak, found while confirming the above
src/util/debounce.test.jscallsvi.useFakeTimers()and never puts the real ones back. Under the VM pool the environment is shared by every file in a worker, so from that point on each file that ran after it inherited fake timers, and anything waiting on a real clock,waitForincluded, stopped behaving. It presented as one test failing per full run, a different one each time, in a file that passes on its own.That file now restores them, and
setupTests.jsdoes the same in its global teardown so the next one cannot leak them either. This is the third leak of that shape here, after the shared url and the unrestoredwindow.localStorage: sharing an environment is what makes the suite four times faster, and the price is that whatever a file installs globally it has to put back.Walked on both the dev server and the built application, each clean with no console errors: twelve list views, Jobs, Templates, Credentials, Projects, Inventories, Hosts, Users, Organizations, Schedules, Instance Groups, Applications and Notification Templates; the job template form, which reads
actions.POST; the topology view, which drives the worker; and the API websocket, which completes its subscribe and receives its acknowledgement through the proxy. Catalogue loading was checked in Japanese, Spanish and Arabic, the last of which also confirmsdir="rtl"still lands on the root element.The Playwright suite in
awx/ui/e2epasses, 11 tests, against the built application and against the dev server. It is the right suite for this change: #762 added it for what jsdom cannot reach, event bubbling, focus and portal rendered content, and the workflow job selector specs in it exist because a jsdom test once passed on a click that did nothing in every real browser./ssowas compared path by path rather than exercised, since no backend is configured here:/sso/complete/,/sso/login/and/sso/metadata/saml/return the same status through the proxy as they do direct, the last of them a real 200. The job output view, the heaviest screen in the application, renders a completed job identically on both, PLAY RECAP and all.Theme switching gets its own paragraph, because it is what this change rewrote. All five themes were switched through on the built application under the enforced CSP:
dark,light,awx,defaultand an uploadedcustomone. Each applies, and the dark variants pick uppf-v6-theme-darkwhile the light ones do not, which is the part worth checking: that flag used to be decided byconfig/themeMetaLoader.jsreading the stylesheet at build time, and it is now decided by the same regular expression inlined intothemeRegistry.js. Same answer from both. The custom theme is listed last after the four shipped ones and its stylesheet applies, so the interaction with #898 is intact, with no CSP violations.