Repository navigation
Check that esbuild, Vite and webpack 4/5 apps can bundle and render Web Chat - #5977
Conversation
Adds a webpack-test job to pull request validation: a consumer app installs the packed tarballs from the build job with webpack 4.47.0 or 5.107.2 and React 16.8.6 or 18.3.1, bundles them, and loads the bundle in jsdom. webpack 4 legs do not fail the job yet: "@redux-devtools/extension" and "botframework-webchat/schema.js" are only resolvable through "exports".
- webpack 4 legs apply the workarounds from docs/HOSTING.md and must pass - Check names use major versions only - Drop the unused render() from the consumer app, also import decorator.js - Resolve dependencies fresh, without --prefer-offline
The test env replaced preset-env with targets: { node: 'current' }, so the
instrumented lib/ that CI packs kept optional chaining, which webpack 4
cannot parse. The test env now only adds babel-plugin-istanbul and inherits
the release preset-env.
…ma.js without an alias
… render in Chrome instead of jsdom
William Wong (compulim)
left a comment
There was a problem hiding this comment.
Add esbuild and vite to the mix.
Also add React 19.
Test it in real browser than jsdom.
…ilm is removed, and name both webpack 4 fixes
…lean up Chrome, the server and the profile
|
Added esbuild 0.28 and Vite 8 next to webpack 4 and 5, and React 19 next to 16 and 18: 11 legs (all 12 combinations except webpack 5 × React 19), each bundling the packed tarballs and rendering a bot message from a fake chat adapter in the runner's preinstalled Chrome (a small DevTools-protocol script, no new dependencies), failing on uncaught errors, The PR also ships a root |
| // Never leaves Chrome or its profile behind, also when the script throws, times out, or is interrupted. | ||
| function killChrome(signal) { | ||
| try { | ||
| process.kill(-chrome.pid, signal); |
There was a problem hiding this comment.
Why -chrome.pid but not chrome.pid?
There was a problem hiding this comment.
detached: true starts Chrome as the leader of its own process group, whose ID is chrome.pid, and a negative PID signals the whole group. So the cleanup also reaches Chrome's renderer, GPU and zygote processes; chrome.pid alone would signal only the browser process, and after the SIGKILL in the exit handler it can't take its children down, so they could be left running. Negative PIDs are POSIX-only, which is fine for the Ubuntu runner.
| process.env.CHROME || 'google-chrome', | ||
| [ | ||
| '--headless=new', | ||
| '--no-first-run', | ||
| '--no-default-browser-check', | ||
| // The page only loads our own bundle, on an ephemeral CI runner, so we do not depend on the runner allowing the sandbox. | ||
| '--no-sandbox', | ||
| '--remote-debugging-port=0', | ||
| `--user-data-dir=${profileDir}`, | ||
| 'about:blank' | ||
| ], | ||
| // In its own process group, so the cleanup below also kills its renderer and GPU processes. | ||
| { detached: true, stdio: ['ignore', 'ignore', 'pipe'] } |
There was a problem hiding this comment.
Let's brainstorm methods for starting chrome without touching potentially unstable chrome CLI API.
There was a problem hiding this comment.
Options, roughly from least to most of Chrome's own interface we'd own:
- Selenium service container + WebDriver (no new dependencies): run
selenium/standalone-chromepinned to the html2 grid's version (141.0) as a jobservices:container, and drive it withselenium-webdriver, which the repo already uses. We pass no Chrome flags, the browser version is pinned and matches html2, and the protocol is W3C WebDriver. Costs an image pull per leg, and the app server must be reachable from the container (--add-host=host.docker.internal:host-gateway). - Puppeteer (
puppeteer.launch()): Chrome for Testing pinned by the Puppeteer version, and the Chrome team maintains the flags. Adds a fixture-only dependency plus a ~170 MB browser download per leg (cacheable). - Playwright (
chromium.launch()): same idea, heavier; could later add Firefox and WebKit. chrome-launcher(Lighthouse): small, but only wraps the same flags.
Either way, I'd move error capture into the fixture page: error/unhandledrejection listeners and a console.error wrapper installed before the bundle, read back with one script call. The driver then only navigates, waits and reads, so no CDP is needed.
I'd go with 1: no new dependencies, the same browser as our html2 tests, and nothing of Chrome's CLI in our code.
There was a problem hiding this comment.
We would like to go with 4 for this particular scenario.
There was a problem hiding this comment.
Done in 4acc9c6: run.mjs starts Chrome with chrome-launcher (only --headless), and the page reports its result back over HTTP, so there is no DevTools protocol code.
| @@ -0,0 +1,205 @@ | |||
| // Serves dist/ and loads it in headless Chrome over the DevTools protocol: Web Chat must render the bot message | |||
There was a problem hiding this comment.
This is quite hard to maintain, let's brainstorm some other ideas on how to organize this.
There was a problem hiding this comment.
Agreed. Three ways to organize it, each removing most of browser.mjs:
- Bundle in the matrix, render in html2. The bundler legs only install and bundle (20–55 s each) and upload
dist/as artifacts. One html2 test page per bundle loads it in the existing Selenium grid, and the harness's existing checks apply: uncaught errors fail the test (run.js), and so does anyconsole.error(done.js). That deletesbrowser.mjs, adds no new runner code, and reviewers already know the pattern. Costs: the HTML test job needs the bundles (needs:plus an artifact download, about 1 minute later start), and local runs need the grid. - Playwright Test in
__tests__/bundlersonly. Aplaywright.configwhosewebServerservesdist/, plus a spec of about 15 lines (pageerror/consolelisteners,expect(page.getByRole('feed')).toContainText(...)). The framework owns browser launch, the server, timeouts, retries and traces. Costs a fixture-only dependency and a cached Chromium download. - Selenium service container with in-page checks. The fixture page records errors and sets
data-resulton<body>; a script of about 30 lines usingselenium-webdriver(already used) loads it and waits for that attribute. No new dependencies, but a Docker pull per leg.
Separately, one folder per bundler with its own plain config (webpack.config.js, vite.config.js, ...) instead of the switch in build.mjs would make each setup readable on its own.
I'd pick 1 if the extra minute on the HTML test job is fine, and 2 otherwise.
There was a problem hiding this comment.
Upon private discussion with William it be beneficial to have configuration for apps visible as the specific bundler docs mention: different folder, each mimic a esbuild/webpack4/5/vite environment.
Build things under each folder (with our recently built tarballs), and then put them under a unified testing environment.
When we need to debug issues related to specific build, we would "go under the folder, run some commands to produce build, then use browser to browse that folder and we can see it in our eyes."
and React version is a script to install the specific version
There was a problem hiding this comment.
Reworked as discussed in 4acc9c6 and 5277f3b: __tests__/bundlers/{esbuild,vite,webpack4,webpack5}/ are each a small app set up as in that bundler's getting-started docs (own package.json, config, index.html, src/), webpack 4's config is the HOSTING.md snippet. React is a script: npm run react16 / react18 / react19. To debug one: cd __tests__/bundlers/vite && npm install && npm run react16 -- ../../../packages/{api,bundle,component,core,directlinespeech,fluent-theme}/*.tgz && npm run build, then open dist/ (Vite: npm run preview). The same check for every folder is node ../run.mjs vite: it serves dist/, injects a small report.js before the app's scripts and starts Chrome with chrome-launcher (option 4, no Chrome flags of ours besides --headless, no CDP code). browser.mjs, build.mjs and test.sh are gone. The React scripts take the tarballs as arguments, so React and Web Chat come from one npm install --no-save (a second install would put back the versions from package.json).
…cked in Chrome by run.mjs with chrome-launcher Each folder under __tests__/bundlers/ (esbuild, vite, webpack4, webpack5) is a self-contained app with its own package.json, config and src/ (the same in every folder, CI diffs it). npm scripts react16/18/19 install a React version; the configs alias react-dom/client to a shim when React 16 is installed. run.mjs serves dist/ with report.js injected and opens it with chrome-launcher, replacing browser.mjs (CDP), build.mjs and test.sh.
…all, run.mjs answers an invalid report with 400 `npm run react16 -- <tarballs>` keeps package.json untouched: a separate install would put back package.json's React. Also README: WSL needs CHROME_PATH; webpack4 config comment.
|
|
||
| import { render } from 'react-dom'; | ||
|
|
||
| export function createRoot(container) { |
There was a problem hiding this comment.
I think we shouldn't ponyfill createRoot in this test case.
This test case is to emulate what was happening during React 16 days, i.e. no one know about createRoot.
There was a problem hiding this comment.
Agreed, removed the shim: src/index.react16.js renders with ReactDOM.render like a React 16 app, and build.mjs picks it as the entry when React 16 is installed; the shared part is src/app.js. (9588d4c)
| @@ -0,0 +1,7 @@ | |||
| // React 16 has no "react-dom/client": the bundler config points it here when React 16 is installed. | |||
|
|
|||
| import { render } from 'react-dom'; | |||
There was a problem hiding this comment.
Same as comment on esbuild, it should back in the React 16 days.
There was a problem hiding this comment.
Same here: vite.config.js points index.html's script (%VITE_ENTRY%, Vite's HTML constant replacement) to src/index.react16.js on React 16, so there is still one index.html/dist/index.html. (9588d4c)
| @@ -0,0 +1,7 @@ | |||
| // React 16 has no "react-dom/client": the bundler config points it here when React 16 is installed. | |||
|
|
|||
| import { render } from 'react-dom'; | |||
There was a problem hiding this comment.
Same as others.
There was a problem hiding this comment.
Done the same way: entry is ./src/index.react16.js on React 16 (also in webpack 5), no alias. (9588d4c)
…n entry instead of a createRoot shim
| "react19": "npm install --no-save react@19.3.1 react-dom@19.3.1" | ||
| }, | ||
| "dependencies": { | ||
| "botframework-webchat": "4.19.2", |
There was a problem hiding this comment.
Should remove all entries here as we are going to install the tarball. We don't need these entries.
There was a problem hiding this comment.
Removed; React and Web Chat now come only from npm run react<N> -- <tarballs>, so package.json has just the bundler (and Babel for webpack 4). (14e2b32)
…rom the React scripts
…f chrome-launcher One image per app on selenium/standalone-chrome:141.0 (pinned by digest) serves the app's dist with report.js injected; run.mjs talks bare-HTTP WebDriver to it and removes the container on every path.
…e debug port to localhost
Changelog Entry
Added
Changed
botframework-webchat-coreconnects to Redux DevTools without the@redux-devtools/extensionpackageFixed
botframework-webchat/schema.jsand@redux-devtools/extensionDescription
A new job builds an app per bundler from the tarballs on React 16, 18 or 19 and checks it renders in Chrome.
Web Chat ships a root
schema.jsand drops@redux-devtools/extension: webpack 4 only needs Babel.Design
createStorereads the DevTools extension's global like Redux Toolkit, not breaking.Specific Changes
pull-request-validation.yml:bundler-testjob, 12 legs__tests__/bundlers/<bundler>/: the apps (React 16 usesReactDOM.render);run.mjschecks them in theDockerfile's Chrome;HOSTING.mdhas webpack 4's configbundle/schema.js: shim likedecorator.jscreateStore.ts: reads__REDUX_DEVTOOLS_EXTENSION_COMPOSE__, has a specdirectlinespeech/babel.config.json: test build uses release targetsI have added tests and executed them locally
I have added
.changelog/<PR#>-<slug>.mdI have updated documentation
Review Checklist
z-index)package.jsonandpackage-lock.jsonreviewed