Converse.js is a modern, feature-rich XMPP chat client that runs in web browsers. It's a plugin-based architecture written in JavaScript with TypeScript type definitions, using the Lit framework for UI components.
This is a monorepo with npm workspaces:
/): Main Converse.js package with UI pluginssrc/headless/): Core XMPP logic without UI (separate npm package @converse/headless)src/log/): Logging utility (separate npm package @converse/log)# Development build (unminified, with debugger statements)
npm run dev # Build everything in dev mode
npm run dev:headless # Build only headless package
# Watch mode (auto-rebuild on changes)
npm run watch # Watch both headless and main
npm run watch:headless # Watch only headless
npm run watch:main # Watch only main package
# Dev server with live reload
npm run devserver # Starts on http://localhost:8080
# Serve static files
npm run serve # Serves on http://localhost:8000 (default)
npm run serve -- -p 8008 # Serve on custom port
npm run serve-tls # HTTPS server (requires certs/)
# Production build (minified)
npm run build # Full build: headless + ESM + CJS + CSS
npm run build:headless # Build headless package only
npm run build:esm # Build ESM bundle
npm run build:cjs # Build CommonJS bundle
npm run build:website-css # Build website CSS
npm run build:website-min-css # Build and minify website CSS
# Special builds
npm run nodeps # Build without dependencies
npm run cdn # Build for CDN deployment
# Run tests
npm test # Run main tests (Karma)
npm run test:all # Run both headless and main tests
npm run test:headless # Run headless tests only
cd src/headless && npm test # Alternative way to run headless tests
# Single run (for CI)
npm test -- --single-run
npm run test:all # Already includes --single-run
# Full test suite (as used in CI)
make check # Runs lint + types + all tests
# Linting
npm run lint # Run ESLint on all source files
# Type checking
npm run types:check # TypeScript type checking (no emit)
npm run types # Generate type definitions
# Clean
npm run clean # Remove node_modules, dist, builds
make dev # Same as npm run dev
make devserver # Same as npm run devserver
make watch # Same as npm run watch
make check # Lint + types + tests (full CI suite)
make test # Run tests
make test-headless # Run headless tests
make serve # Serve on port 8008
Converse.js uses a plugin-based architecture powered by pluggable.js:
src/headless/plugins/): Core XMPP logic, no UI
chat, muc, disco, roster, ping, bookmarkssrc/plugins/): Visual components that depend on headless
chatview, muc-views, rosterview, controlboxEvery plugin follows this pattern:
import { _converse, api, converse } from '@converse/headless';
converse.plugins.add('plugin-name', {
dependencies: ['other-plugin-1', 'other-plugin-2'], // Required plugins
initialize () {
// Configure plugin settings
api.settings.extend({
'some_setting': 'default_value',
});
// Export models/views for other plugins
const exports = { MyClass, myFunction };
Object.assign(_converse, exports); // DEPRECATED pattern
Object.assign(_converse.exports, exports); // Current pattern
// Extend API
Object.assign(api, my_api_methods);
// Register event listeners
api.listen.on('connected', () => { /* ... */ });
}
});
src/
├── headless/ # Core XMPP logic (separate package)
│ ├── plugins/ # Headless plugins (chat, muc, roster, etc.)
│ ├── shared/ # Shared headless utilities
│ ├── types/ # Generated TypeScript definitions
│ └── dist/ # Built headless package
├── plugins/ # UI plugins
│ ├── chatview/ # Chat UI
│ ├── muc-views/ # Multi-user chat UI
│ ├── rosterview/ # Contact list UI
│ └── controlbox/ # Main control panel
├── shared/ # Shared UI components
│ ├── components/ # Reusable Lit components
│ ├── chat/ # Chat-related shared components
│ ├── modals/ # Modal dialogs
│ └── styles/ # Shared SCSS files
├── templates/ # Lit template functions
├── i18n/ # Internationalization
│ └── locales/ # Translation files (.po)
├── types/ # Generated TypeScript definitions
└── utils/ # Utility functions
Converse.js uses a custom fork of Backbone.js called @converse/skeletor for state management by means of Models and Collections of Models.
As Converse.js evolved, the team created @converse/skeletor as a fork of Backbone.js to:
// Creating a model instance
const chatroom = new _converse.exports.ChatRoom({
jid: 'room@muc.example.com',
nick: 'user123'
});
// Accessing model attributes
const jid = chatroom.get('jid');
// Listening to model changes
chatroom.on('change:subject', () => {
console.log('Room subject changed');
});
// Saving model changes
chatroom.save({'subject': 'New Subject'});
// Accessing the chatboxes collection
const chatboxes = _converse.state.chatboxes;
// Finding a specific model in a collection
const chatbox = chatboxes.get('user@example.com');
// Adding a model to a collection
chatboxes.add(new _converse.ChatBox({...}));
// Listening to collection events
chatboxes.on('add', (chatbox) => {
console.log('New chatbox added:', chatbox.get('jid'));
});
While UI components use Lit, they integrate with Skeletor models:
class ChatRoomView extends CustomElement {
async initialize() {
// Wait for model to be ready
await this.model.initialized;
// Listen to model changes to trigger re-render
this.listenTo(this.model, 'change', () => this.requestUpdate());
this.listenTo(this.model.messages, 'add', () => this.requestUpdate());
this.requestUpdate();
}
render() {
return html`
<div class="chatroom">
<header>${this.model.get('name')}</header>
<!-- Render messages, participants, etc. -->
</div>
`;
}
}
{
"singleQuote": true, // Use single quotes
"printWidth": 120, // Max line length 120 chars
"tabWidth": 4, // 4-space indentation
"useTabs": false, // Spaces, not tabs
"spaceBeforeFunctionParen": true // function () not function()
}
kebab-case.js (e.g., chat-content.js, message-history.js)snake_case (e.g., muc_jid)camelCase (e.g., getMessage, chatBoxView)PascalCase (e.g., ChatBox, CustomElement)UPPER_CASE (e.g., PRIVATE_CHAT_TYPE, WINDOW_SIZE)# (e.g., #markScrolled())tpl (e.g., tplPlaceholder)// Headless core imports
import { _converse, api, converse } from '@converse/headless';
// Logging
import { log } from '@converse/log';
// Lit framework
import { html, css } from 'lit';
// Relative imports for local files
import ChatView from './chat.js';
import './styles/index.scss';
// Utilities
import { u } from '@converse/headless'; // Utility functions
const { dayjs, Strophe, sizzle } = converse.env; // Common libraries
Lit Components extend CustomElement:
import { html } from 'lit';
import { CustomElement } from 'shared/components/element.js';
export default class MyComponent extends CustomElement {
static get properties() {
return {
model: { type: Object },
some_state: { state: true }, // Internal state
};
}
async initialize() {
await this.model.initialized;
this.listenTo(this.model, 'change', () => this.requestUpdate());
this.requestUpdate();
}
render() {
return html`<div>...</div>`;
}
}
customElements.define('my-component', MyComponent);
Templates are functions returning html tagged templates:
import { html } from 'lit';
export default (model) => html`
<div class="chat-message">
<span>${model.get('from')}</span>
<p>${model.get('body')}</p>
</div>
`;
// Settings
api.settings.extend({ 'my_setting': 'default' });
api.settings.get('my_setting');
// Events
api.listen.on('connected', callback);
api.trigger('customEvent', data);
// Promises
await api.waitUntil('connected');
// Disco features
api.disco.own.features.add(Strophe.NS.SPOILER);
// User interaction
const confirmed = await api.confirm('Are you sure?');
await api.alert('Something happened');
allowJs: true, checkJs: true, declaration: true.js for implementation, .d.ts generated automaticallysrc/types/ and src/headless/types/Add JSDoc comments to document types in .js files:
/**
* @typedef {Object} MessageAttributes
* @property {string} body - Message body text
* @property {string} from - Sender JID
* @property {string} type - Message type
*/
/**
* @param {string} jid - The JID to check
* @returns {Promise<boolean>}
*/
async function isContact(jid) {
// ...
}
npm run types:check # Check types without generating files
npm run types # Generate type definitions
tests/ subdirectory of each plugin*.js (e.g., chatbox.js, actions.js, corrections.js)src/headless/tests/mock.js and src/shared/tests/mock.js/*global mock, converse */
const { api } = converse;
const u = converse.env.utils;
const sizzle = converse.env.sizzle;
describe("My Feature", function () {
it("does something", mock.initConverse(['chatBoxesFetched'],
{ view_mode: 'fullscreen' },
async function (_converse) {
// Setup
await mock.waitForRoster(_converse, 'current', 1);
await mock.openControlBox(_converse);
// Test action
const jid = 'user@example.com';
await mock.openChatBoxFor(_converse, jid);
const view = _converse.chatboxviews.get(jid);
// Assertions
await u.waitUntil(() => sizzle('.chat-msg', view).length === 1);
expect(view.querySelector('.chat-msg__text').textContent).toBe('hello');
}
));
});
To run a specific test file, add it to the files array in karma.conf.js:
files: [
// ... existing files
{ pattern: "src/plugins/my-plugin/tests/my-test.js", type: 'module' },
],
Or run headless tests:
cd src/headless
npm test # Runs karma with src/headless/karma.conf.js
src/shared/styles/ (alerts, badges, buttons, forms, etc.)node_modules/ and src/ as includePaths// Import Bootstrap utilities
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
// Component styles
.chat-content {
&__messages {
overflow-y: auto;
}
&__notifications {
padding: 1rem;
}
}
Styles are imported directly in JavaScript:
import './styles/chat-content.scss';
Rspack uses style-loader + css-loader + postcss-loader + sass-loader to process and inject styles.
src/i18n/locales/*/LC_MESSAGES/converse.poimport { __ } from '@converse/headless';
const message = __('Hello, %1$s!', username);
const plural = __('%1$d messages', count);
# Extract strings from source
npm run nodeps # Builds converse-no-dependencies.js
make pot # Generates src/i18n/converse.pot
# Update existing translations
make po # Merges pot into all locale .po files
rspack/rspack.common.js - Shared config (loaders, plugins)rspack/rspack.build.js - Main buildrspack/rspack.build.cjs.js - CommonJS buildrspack/rspack.build.esm.js - ESM buildrspack/rspack.headless.js - Headless buildrspack/rspack.serve.js - Dev server configrspack/rspack.nodeps.js - Build without dependencies.po translation files to Jed format@import and url() in CSSDROP_DEBUGGER=true # Remove debugger statements (production)
ASSET_PATH=/dist/ # Public path for assets (default)
ASSET_PATH=https://cdn.conversejs.org/dist/ # CDN path
Models and collections are initialized asynchronously:
await this.model.initialized; // Wait for model
await this.model.messages.fetched; // Wait for data fetch
Use Backbone-style event listeners (automatically cleaned up):
this.listenTo(this.model, 'change', () => this.requestUpdate());
this.listenTo(this.model.messages, 'add', this.onMessageAdded);
Use utility functions to wait:
await u.waitUntil(() => sizzle('.chat-msg', view).length > 0);
await api.waitUntil('connected');
import { _converse, api, converse } from '@converse/headless';
// Access global state via `_converse.state` (use sparingly, prefer api)
const { chatboxes } = _converse.state;
const chatbox = chatboxes.get(jid);
// Access 3rd party libraries
const { Strophe, $msg, $iq, $pres, $build, stx } = converse.env;
// XML stanzas (prefer stx template literal over old $msg, $pres, $iq, $build functions)
// The stx template literal is the current preferred method for creating XML stanzas
// When using stx, the `xmlns` attribute always needs to be set to "jabber:client".
const stanza_stx = stx`
<message to="${jid}" type="chat" xmlns="jabber:client">
<body>Hello</body>
<active xmlns="${Strophe.NS.CHATSTATES}"/>
</message>`;
// Legacy methods ($msg, $pres, $iq, $build) are deprecated - use stx instead
const stanza_legacy = $msg({ to: jid, type: 'chat' })
.c('body').t('Hello').up()
.c('active', { xmlns: Strophe.NS.CHATSTATES });
Always define custom elements:
class MyElement extends CustomElement { /* ... */ }
customElements.define('my-element', MyElement);
Use in HTML:
html`<my-element .model="${this.model}"></my-element>`
// Set properties with . prefix
.model="${this.model}"
// Set attributes with regular syntax
id="my-id"
class="my-class"
// Boolean attributes with ?
?disabled="${this.isDisabled}"
// Event handlers with @
@click="${this.handleClick}"
listenTo instead of on (auto-cleanup on disconnect)stopListening() in disconnectedCallback()u.debounce() for frequent operations_ to ignore (e.g., _unused)log.debug(), log.info(), log.error() instead)const by default, let when reassignment needed.github/workflows/karma-tests.ymlmake check ARGS=--single-runmake check # Runs: eslint + types + tests (headless + main)
This command:
npm run lint (ESLint)npm run types (generate types)cd src/headless && npm run test -- --single-runnpm run test -- --single-run// Access global converse object in browser console
window.converse
// Internal state (as represented by Models and Collections)
_converse.state
// Namespace for storing code that might be useful to 3rd party
// plugins. We want to make it possible for 3rd party plugins to have
// access to code (e.g. classes) without having to add converse.js
// as a dependency.
_converse.exports
// API methods
const { api } = _converse;
api.user.jid()
api.settings.get('jid')
api.chatboxes.get('user@example.com')
import { log } from '@converse/log';
log.debug('Debug message');
log.info('Info message');
log.warn('Warning message');
log.error('Error message');
Set log level in settings:
converse.initialize({
loglevel: 'debug', // 'debug', 'info', 'warn', 'error'
});
rspack.common.js resolve sectioncustomElements.define() is calledawait mock.waitForRoster() and other setup completesnpm run types to regenerate definitionsSee Makefile and RELEASE.md for full details:
# Update version in all files
make version VERSION=12.1.0
# Create release
make publish BRANCH=master
# Post-release (bump to dev version)
make postrelease VERSION=12.1.0
docs/source/ (ReStructuredText)make doc (requires Python + Sphinx)docs/html/Generate docs:
make docsdev # Install Python dependencies
make doc # Build HTML documentation
package.json - Main package config, scripts, dependenciessrc/headless/package.json - Headless package configkarma.conf.js - Main test configurationsrc/headless/karma.conf.js - Headless test configurationeslint.config.mjs - ESLint rules.prettierrc - Prettier formatting rulestsconfig.json - TypeScript configurationMakefile - Build targets and release automationsrc/index.js - Main entry point (imports all plugins)src/headless/index.js - Headless entry pointsrc/entry.js - Alternative entry point