Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f8fb9fc
Check that webpack 4 and 5 apps can bundle Web Chat
Oct 28, 2026
4e427d4
Document the webpack 4 workarounds and check them in CI
Oct 28, 2026
57273ca
Transpile only the dependencies that need it for webpack 4, not Web Chat
Oct 28, 2026
8660475
Add the changelog fragment
Oct 28, 2026
cadcd12
Add the webpack 4 docs to the changelog fragment
Oct 28, 2026
9f5e517
Build the instrumented directlinespeech with the release browser targets
Oct 28, 2026
78dad1a
Ship a root schema.js so webpack 4 resolves botframework-webchat/sche…
Oct 28, 2026
aee0785
Read the Redux DevTools extension global directly and drop @redux-dev…
Oct 28, 2026
5ae2fda
Bundle with esbuild, Vite and webpack 4/5 on React 16, 18 and 19, and…
Oct 29, 2026
eff6b86
Exclude webpack 5 with React 19 until react-film is removed
Oct 29, 2026
2f91c03
Describe the Redux DevTools change without implying a breaking change
Oct 29, 2026
eceed01
Merge remote-tracking branch 'origin/main' into OEvgeny/fix-ci-webpac…
Oct 29, 2026
aa4bc07
Note that webpack 5 with React 19 needs exportsPresence until react-f…
Oct 29, 2026
3cfbed8
Time out Chrome start and DevTools calls in browser.mjs, and always c…
Oct 29, 2026
3bb3c22
Check that createStore uses the enhancer returned by the Redux DevToo…
Oct 29, 2026
bd8d545
Type the Redux DevTools global in createStore instead of globally, so…
Oct 29, 2026
7dc9d97
Bundle the consumer apps in production mode, also when NODE_ENV is test
Oct 29, 2026
a8d5455
Shorten the bundler-test workflow comments
Oct 29, 2026
4acc9c6
Bundler test: one app per bundler as in its getting-started docs, che…
Oct 29, 2026
5277f3b
Bundler test: React scripts install the tarballs in the same npm inst…
Oct 29, 2026
9588d4c
Bundler test: React 16 apps render with ReactDOM.render from their ow…
Oct 29, 2026
14e2b32
Bundler test: add bump scripts, and install React and Web Chat only f…
Oct 29, 2026
e3ced06
Merge remote-tracking branch 'origin/main' into OEvgeny/fix-ci-webpac…
Oct 29, 2026
a986de8
Bundler test: run webpack 5 with React 19 again, react-film no longer…
Oct 29, 2026
58dc7b8
Run the bundler apps' bump scripts from the root bump
Oct 29, 2026
97cb4d4
Bundler test: render in Chrome 141 in Docker over WebDriver instead o…
Oct 29, 2026
0dc701c
Bundler test: time out WebDriver calls, keep the first error, bind th…
Oct 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changelog/5977-fix-ci-webpack-consumers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
### Added

- 👷🏻 Pull request validation: added rendering Web Chat bundled by esbuild, Vite, webpack 4 and 5 with React 16, 18 and 19, in PR [#5977](https://github.com/microsoft/BotFramework-WebChat/pull/5977), by [@OEvgeny](https://github.com/OEvgeny)
- Docs: added [workarounds for webpack 4](./docs/HOSTING.md#bundling-with-webpack-4), in PR [#5977](https://github.com/microsoft/BotFramework-WebChat/pull/5977), by [@OEvgeny](https://github.com/OEvgeny)

### Changed

- `botframework-webchat-core` connects to Redux DevTools without the `@redux-devtools/extension` package, in PR [#5977](https://github.com/microsoft/BotFramework-WebChat/pull/5977), by [@OEvgeny](https://github.com/OEvgeny)

### Fixed

- Fixed webpack 4 failing to resolve `botframework-webchat/schema.js` and `@redux-devtools/extension`, in PR [#5977](https://github.com/microsoft/BotFramework-WebChat/pull/5977), by [@OEvgeny](https://github.com/OEvgeny)
53 changes: 53 additions & 0 deletions .github/workflows/pull-request-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,59 @@ jobs:
run: ../../node_modules/.bin/tsd -t ./dist/botframework-webchat-core.d.mts
working-directory: ./packages/core

bundler-test:
name: Bundler test (${{ matrix.bundler }}, React ${{ matrix.react }})
needs:
- build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
bundler: [esbuild, vite, webpack4, webpack5] # Folders under __tests__/bundlers/.
react: [16, 18, 19]
timeout-minutes: 10
env:
NODE_ENV: production # Apps ship production builds; with "test", Vite bundles React's development build.
npm_config_include: dev # NODE_ENV=production makes npm omit devDependencies, which have the bundlers.
defaults:
run:
working-directory: __tests__/bundlers/${{ matrix.bundler }}

steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
sparse-checkout: __tests__/bundlers

- name: Use Node.js ${{ env.node-version }}
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ env.node-version }}
cache: npm
cache-dependency-path: __tests__/bundlers/${{ matrix.bundler }}/package.json

- name: Download tarballs
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: tarballs

- name: Extract tarballs
run: tar -xvf tarballs.tar
working-directory: .

- name: Check that the app is the same in every folder
run: diff --recursive ../esbuild/src src

- run: npm install

- name: Install React ${{ matrix.react }} and the tarballs
run: npm run react${{ matrix.react }} -- ../../../packages/{api,bundle,component,core,directlinespeech,fluent-theme}/*.tgz

- run: npm run build

# Renders in the Chrome of the Docker image built from __tests__/bundlers/Dockerfile.
- name: Render in Chrome
run: node ../run.mjs ${{ matrix.bundler }}

html-test:
name: HTML test (${{ format('{0}/{1}', matrix.shard-index, matrix.shard-count) }})
needs:
Expand Down
2 changes: 2 additions & 0 deletions __tests__/bundlers/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/node_modules
/package-lock.json
19 changes: 19 additions & 0 deletions __tests__/bundlers/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Chrome 141 with the app in <APP>/dist/ served on port 8000, see README.md. Same Chrome as the HTML tests' grid
# (selenium/node-chrome:141.0), in the single-container "standalone" variant: WebDriver on port 4444, noVNC on port 7900.
#
# Usage: docker build --build-arg APP=<esbuild | vite | webpack4 | webpack5> .

FROM selenium/standalone-chrome:141.0@sha256:2315640266fa3a4a28c3023fa8edd190ca7f6b827810c2f9bd3b75cb88198cff

ARG APP

COPY --chown=seluser ${APP}/dist/ /home/seluser/app/
COPY report.js /home/seluser/app/__runner/

# Injects report.js before the app's scripts.
RUN sed -i 's|<head>|&<script src="/__runner/report.js"></script>|i' /home/seluser/app/index.html

COPY <<EOF /etc/supervisor/conf.d/app.conf
[program:app]
command=python3 -m http.server --directory /home/seluser/app 8000
EOF
35 changes: 35 additions & 0 deletions __tests__/bundlers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Bundler tests

Each folder is a Web Chat app, set up the way the getting-started docs of its bundler do it: `esbuild/`, `vite/`, `webpack4/` (with the workarounds from [`docs/HOSTING.md`](../../docs/HOSTING.md#bundling-with-webpack-4)) and `webpack5/`. `src/` is the same in every folder: CI checks it.

CI builds every app with React 16, 18 and 19 and the tarballs of the pull request, then `run.mjs` opens it in Chrome 141 in Docker: the app must show the bot message, without errors in the console, uncaught errors or failed loads.

`run.mjs` builds the [`Dockerfile`](Dockerfile) for the app (the [Selenium standalone Chrome image](https://github.com/SeleniumHQ/docker-selenium) with the app's `dist/` served on port 8000 and [`report.js`](report.js) injected), runs it, and talks WebDriver to it: opens `http://localhost:8000/` in the container, reads the result that `report.js` stores in `window.bundlerTestResult`, then removes the container.

## Debugging an app

Build the tarballs first (`npm run build`, then `npm exec --workspaces -- npm pack` in the repository root), then:

```sh
cd __tests__/bundlers/vite
npm install
npm run react16 -- ../../../packages/{api,bundle,component,core,directlinespeech,fluent-theme}/*.tgz # Or react18, react19
npm run build
```

Then open `dist/index.html` in a browser (Vite: `npm run preview`), or run the check like CI does (needs Docker):

```sh
node ../run.mjs vite
```

To look at the app as served to that Chrome, run the image yourself and browse `http://localhost:8000/`:

```sh
docker build --build-arg APP=vite --tag webchat-bundler-vite ..
docker run --rm --publish 127.0.0.1:8000:8000 webchat-bundler-vite
```

The image has a copy of `dist/`: build it again after `npm run build`.

The React scripts install the tarballs in the same `npm install`: a separate `npm install` would remove React and Web Chat, which are not in `package.json`.
4 changes: 4 additions & 0 deletions __tests__/bundlers/esbuild/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/dist/*
!/dist/index.html
/node_modules
/package-lock.json
13 changes: 13 additions & 0 deletions __tests__/bundlers/esbuild/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// esbuild's build script (https://esbuild.github.io/getting-started/#build-scripts): the CLI cannot pick the entry by the installed React.

import * as esbuild from 'esbuild';
import { createRequire } from 'node:module';

const { version: reactDOMVersion } = createRequire(import.meta.url)('react-dom/package.json');

await esbuild.build({
bundle: true,
entryPoints: [reactDOMVersion.startsWith('16.') ? 'src/index.react16.js' : 'src/index.js'],
minify: true,
outfile: 'dist/main.js'
});
11 changes: 11 additions & 0 deletions __tests__/bundlers/esbuild/dist/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>Web Chat bundled with esbuild</title>
</head>
<body>
<main id="webchat" style="height: 100vh"></main>
<script src="main.js"></script>
</body>
</html>
17 changes: 17 additions & 0 deletions __tests__/bundlers/esbuild/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@msinternal/botframework-webchat-bundlers-esbuild",
"version": "0.0.0-0",
"description": "A Web Chat app bundled with esbuild, see ../README.md",
"private": true,
"license": "MIT",
"scripts": {
"build": "node build.mjs",
"bump": "vg bump prod && vg bump dev && (npm audit fix || exit 0)",
"react16": "npm install --no-save react@16.8.6 react-dom@16.8.6",
"react18": "npm install --no-save react@18.3.1 react-dom@18.3.1",
"react19": "npm install --no-save react@19.3.1 react-dom@19.3.1"
},
Comment thread
compulim marked this conversation as resolved.
"devDependencies": {
"esbuild": "0.28.2"
}
}
54 changes: 54 additions & 0 deletions __tests__/bundlers/esbuild/src/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Same in every app folder (CI diffs them): a Web Chat app, rendering a message from a fake chat adapter.
// index.js (React 18 and later) and index.react16.js render it.

import ReactWebChat, { createDirectLine, renderWebChat } from 'botframework-webchat';
import { FluentThemeProvider } from 'botframework-webchat-fluent-theme';
import { Composer } from 'botframework-webchat/component.js';
import { WebChatDecorator } from 'botframework-webchat/decorator.js';
import { useActivities } from 'botframework-webchat/hook.js';
import { createActivityPolymiddleware } from 'botframework-webchat/middleware.js';
import { createElement } from 'react';

// Every import must be defined: a bundler can resolve a subpath to an empty module without an error.
// "botframework-webchat/schema.js" is imported by the Fluent theme.
Object.entries({
Composer,
createActivityPolymiddleware,
createDirectLine,
FluentThemeProvider,
ReactWebChat,
renderWebChat,
useActivities,
WebChatDecorator
}).forEach(([name, value]) => {
if (!value) {
throw new Error(`Import "${name}" is ${value}.`);
}
});

// Emits the values on every subscription, asynchronously like DirectLineJS.
function observableOf(...values) {
return Object.freeze({
subscribe: ({ next }) => {
const timeout = setTimeout(() => values.forEach(value => next(value)));

return Object.freeze({ unsubscribe: () => clearTimeout(timeout) });
}
});
}

// A chat adapter that connects at once and receives one message from the bot.
const directLine = Object.freeze({
activity$: observableOf({
from: { id: 'bot', role: 'bot' },
id: 'a-00001',
text: 'Hello from the bot.',
timestamp: new Date().toISOString(),
type: 'message'
}),
connectionStatus$: observableOf(0, 1, 2), // Uninitialized, connecting, online.
end: () => undefined,
postActivity: () => observableOf('a-00002')
});

export default createElement(FluentThemeProvider, {}, createElement(ReactWebChat, { directLine }));
7 changes: 7 additions & 0 deletions __tests__/bundlers/esbuild/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// The entry for React 18 and later.

import { createRoot } from 'react-dom/client';

import app from './app.js';

createRoot(document.getElementById('webchat')).render(app);
7 changes: 7 additions & 0 deletions __tests__/bundlers/esbuild/src/index.react16.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// The entry for React 16, as apps were written then: the bundler config picks it when React 16 is installed.

import ReactDOM from 'react-dom';

import app from './app.js';

ReactDOM.render(app, document.getElementById('webchat'));
38 changes: 38 additions & 0 deletions __tests__/bundlers/report.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Injected before the app's scripts (see Dockerfile): records errors, and reports in `window.bundlerTestResult` once Web
// Chat shows the bot message, for run.mjs to read over WebDriver.

(() => {
const errors = [];
const { error } = console;

console.error = (...args) => {
errors.push(args.map(arg => arg?.stack || String(arg)).join(' '));
error.apply(console, args);
};

// Capture phase: a script or style sheet that fails to load fires a non-bubbling "error" on its element.
addEventListener(
'error',
event =>
errors.push(
event.target === window
? event.error?.stack || event.message
: `Failed to load ${event.target.src || event.target.href}.`
),
true
);

addEventListener('unhandledrejection', ({ reason }) => errors.push(reason?.stack || String(reason)));

const interval = setInterval(() => {
const rendered =
document.readyState === 'complete' &&
!!document.querySelector('[role="feed"]')?.textContent.includes('Hello from the bot.') &&
!!document.querySelector('textarea');

if (rendered || errors.length) {
clearInterval(interval);
window.bundlerTestResult = Object.freeze({ errors: Object.freeze([...errors]), ok: rendered && !errors.length });
}
}, 100);
})();
Loading
Loading