Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,10 +322,12 @@ Mock modules with `vi.mock('../../EmptyStateIndicator', () => ({ … }))`; use `
| Import path | Source |
| ---------------------------------- | ---------------------------------- |
| `stream-chat-react` | `src/index.ts` |
| `stream-chat-react/a2ui` | `src/plugins/A2UI/` |
| `stream-chat-react/ai-components` | `src/plugins/AIComponents/` |
| `stream-chat-react/channel-detail` | `src/plugins/ChannelDetail/` |
| `stream-chat-react/emojis` | `src/plugins/Emojis/` |
| `stream-chat-react/mp3-encoder` | `src/plugins/encoders/mp3.ts` |
| `stream-chat-react/openui` | `src/plugins/OpenUI/` |
| `stream-chat-react/slot-geometry` | `src/plugins/SlotGeometry/` |
| `stream-chat-react/slot-layout` | `src/plugins/SlotLayout/index.tsx` |
| `stream-chat-react/css/*` | `dist/css/*` |
Expand Down
3 changes: 2 additions & 1 deletion ai-docs/i18n-v15-key-map.json
Original file line number Diff line number Diff line change
Expand Up @@ -1220,7 +1220,8 @@
"Search": {
"key": "common.search.ariaLabel",
"prose": true,
"shared": true
"shared": true,
"also": ["aiComponents.a2ui.choicePickerFilter.placeholder"]
},
"Send": {
"key": "common.send.label",
Expand Down
1 change: 1 addition & 0 deletions examples/vite/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const deTranslations = {
'a11y.interactionAnnouncements.voiceMessageSent.ariaLabel': 'Sprachnachricht gesendet',
'a11y.interactionAnnouncements.voiceRecordingAttached.ariaLabel':
'Sprachaufnahme angehängt',
'aiComponents.a2ui.choicePickerFilter.placeholder': 'Suchen',
'aiComponents.attachmentPreview.deleteAttachment.ariaLabel': 'Anhang löschen',
'aiComponents.attachmentPreview.unknownFileName.text': 'Unbekannter Dateiname',
'aiComponents.chart.loading.text': 'Diagramm wird geladen...',
Expand Down
1 change: 1 addition & 0 deletions examples/vite/src/i18n/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const itTranslations = {
'a11y.interactionAnnouncements.voiceMessageSent.ariaLabel': 'Messaggio vocale inviato',
'a11y.interactionAnnouncements.voiceRecordingAttached.ariaLabel':
'Registrazione vocale allegata',
'aiComponents.a2ui.choicePickerFilter.placeholder': 'Cerca',
'aiComponents.attachmentPreview.deleteAttachment.ariaLabel': 'Elimina allegato',
'aiComponents.attachmentPreview.unknownFileName.text': 'Nome file sconosciuto',
'aiComponents.chart.loading.text': 'Caricamento del grafico...',
Expand Down
44 changes: 42 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@
"require": "./dist/cjs/ai-components.js",
"default": "./dist/cjs/ai-components.js"
},
"./a2ui": {
"types": "./dist/types/plugins/A2UI/index.d.ts",
"import": "./dist/es/a2ui.mjs",
"require": "./dist/cjs/a2ui-require.js",
"default": "./dist/es/a2ui.mjs"
},
"./openui": {
"types": "./dist/types/plugins/OpenUI/index.d.ts",
"import": "./dist/es/openui.mjs",
"require": "./dist/cjs/openui.js",
"default": "./dist/es/openui.mjs"
},
"./dist/css/*": {
"default": "./dist/css/*"
},
Expand All @@ -84,6 +96,12 @@
],
"ai-components": [
"./dist/types/plugins/AIComponents/index.d.ts"
],
"a2ui": [
"./dist/types/plugins/A2UI/index.d.ts"
],
"openui": [
"./dist/types/plugins/OpenUI/index.d.ts"
]
}
},
Expand Down Expand Up @@ -132,27 +150,43 @@
"unist-builder": "^4.0.0",
"unist-util-visit": "^5.1.0",
"use-sync-external-store": "^1.7.0",
"zod": "^4.1.12"
"zod": "^4.2.0"
},
"optionalDependencies": {
"@stream-io/transliterate": "^1.5.5"
},
"peerDependencies": {
"@a2ui/react": "~0.15.0",
"@a2ui/web_core": "~0.15.0",
"@breezystack/lamejs": "^1.2.7",
"@emoji-mart/data": "^1.1.0",
"@openuidev/react-lang": "~1.0.2",
"@openuidev/react-ui": "~0.18.1",
"emoji-mart": "^5.4.0",
"modern-normalize": "^3.0.1",
"react": "^19.0.0 || ^18.0.0 || ^17.0.0",
"react-dom": "^19.0.0 || ^18.0.0 || ^17.0.0",
"stream-chat": "^10.0.0-rc.32"
},
"peerDependenciesMeta": {
"@a2ui/react": {
"optional": true
},
"@a2ui/web_core": {
"optional": true
},
"@breezystack/lamejs": {
"optional": true
},
"@emoji-mart/data": {
"optional": true
},
"@openuidev/react-lang": {
"optional": true
},
"@openuidev/react-ui": {
"optional": true
},
"emoji-mart": {
"optional": true
},
Expand All @@ -166,11 +200,16 @@
"README.md"
],
"devDependencies": {
"@a2ui/react": "0.15.0",
"@a2ui/web_core": "0.15.0",
"@breezystack/lamejs": "^1.2.7",
"@commitlint/cli": "^21.0.1",
"@commitlint/config-conventional": "^21.0.1",
"@emoji-mart/data": "^1.2.1",
"@eslint/js": "^9.39.4",
"@openuidev/react-headless": "0.18.1",
"@openuidev/react-lang": "1.0.2",
"@openuidev/react-ui": "0.18.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^12.0.8",
Expand Down Expand Up @@ -215,7 +254,8 @@
"typescript-eslint": "^8.59.4",
"vite": "^8.1.3",
"vitest": "^4.1.9",
"vitest-axe": "^0.1.0"
"vitest-axe": "^0.1.0",
"zustand": "4.5.7"
},
"scripts": {
"clean": "rm -rf dist",
Expand Down
9 changes: 9 additions & 0 deletions scripts/validate-cjs-node-bundle.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,12 @@

require('../dist/cjs/index.js');
require('../dist/cjs/ai-components.js');
require('../dist/cjs/openui.js');

// The A2UI renderer is an ES module only, and requiring it says so.
try {
require('stream-chat-react/a2ui');
throw new Error('stream-chat-react/a2ui loaded with require');
} catch (error) {
if (!/an ES module only/.test(error.message)) throw error;
}
87 changes: 62 additions & 25 deletions scripts/validate-entry-isolation.mts
Original file line number Diff line number Diff line change
@@ -1,35 +1,72 @@
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';

const FORBIDDEN_BARE = ['chart.js', 'react-chartjs-2', 'react-syntax-highlighter', 'zod'];
const FORBIDDEN_PATH = '/plugins/AIComponents/';
// The generated UI renderers' packages are optional peers: only `stream-chat-react/a2ui` and
// `stream-chat-react/openui` may reach them, and only those entries may reach the renderers.
const RENDERER_BARE = ['@a2ui/', '@openuidev/'];
const RENDERER_PATHS = ['/plugins/A2UI/', '/plugins/OpenUI/'];

const CHECKS = [
{
entry: 'index.mjs',
forbiddenBare: [
'chart.js',
'react-chartjs-2',
'react-syntax-highlighter',
'zod',
...RENDERER_BARE,
],
forbiddenPaths: ['/plugins/AIComponents/', ...RENDERER_PATHS],
name: 'main entry isolated from ai-components and the generated UI renderers',
},
{
entry: 'ai-components.mjs',
forbiddenBare: RENDERER_BARE,
forbiddenPaths: RENDERER_PATHS,
name: 'ai-components isolated from the generated UI renderers',
},
];

const IMPORT_RE = /(?:import|export)\s*(?:[^'"]*?from\s*)?['"]([^'"]+)['"]/g;
// lazy chunks: `import('./x.mjs')` must be walked too
const DYNAMIC_IMPORT_RE = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;

const entry = resolve(import.meta.dirname, '../dist/es/index.mjs');
const seen = new Set<string>();
const violations: string[] = [];
const queue = [entry];

while (queue.length) {
const file = queue.pop() as string;
if (seen.has(file)) continue;
seen.add(file);
if (file.includes(FORBIDDEN_PATH)) violations.push(`reaches ${file}`);
const source = readFileSync(file, 'utf8');
const specs = [...source.matchAll(IMPORT_RE), ...source.matchAll(DYNAMIC_IMPORT_RE)];
for (const [, spec] of specs) {
if (spec.startsWith('.')) queue.push(resolve(dirname(file), spec));
else if (FORBIDDEN_BARE.some((b) => spec === b || spec.startsWith(`${b}/`)))
violations.push(`${file} imports ${spec}`);
// `@scope/` matches the whole scope; a package name matches it and its subpaths.
const matchesBare = (spec: string, bare: string) =>
bare.endsWith('/')
? spec.startsWith(bare)
: spec === bare || spec.startsWith(`${bare}/`);

let failed = false;

for (const { entry, forbiddenBare, forbiddenPaths, name } of CHECKS) {
const seen = new Set<string>();
const violations: string[] = [];
const queue = [resolve(import.meta.dirname, '../dist/es', entry)];

while (queue.length) {
const file = queue.pop() as string;
if (seen.has(file)) continue;
seen.add(file);
if (forbiddenPaths.some((path) => file.includes(path)))
violations.push(`reaches ${file}`);
const source = readFileSync(file, 'utf8');
const specs = [...source.matchAll(IMPORT_RE), ...source.matchAll(DYNAMIC_IMPORT_RE)];
for (const [, spec] of specs) {
if (spec.startsWith('.')) queue.push(resolve(dirname(file), spec));
else if (forbiddenBare.some((bare) => matchesBare(spec, bare)))
violations.push(`${file} imports ${spec}`);
}
}
}

if (seen.size < 50)
throw new Error(`walked only ${seen.size} modules — graph walk is broken`);
if (violations.length) {
console.error(violations.join('\n'));
process.exit(1);
if (seen.size < 50)
throw new Error(`${entry}: walked only ${seen.size} modules — graph walk is broken`);
if (violations.length) {
console.error(`${entry}:\n${violations.join('\n')}`);
failed = true;
} else {
console.log(`${name} (${seen.size} modules checked)`);
}
}
console.log(`main entry isolated from ai-components (${seen.size} modules checked)`);

if (failed) process.exit(1);
2 changes: 2 additions & 0 deletions scripts/validate-esm-node-bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@

import '../dist/es/index.mjs';
import '../dist/es/ai-components.mjs';
import '../dist/es/a2ui.mjs';
import '../dist/es/openui.mjs';
1 change: 1 addition & 0 deletions src/i18n/__tests__/catalog.fixture.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"a11y.interactionAnnouncements.userSelected.ariaLabel": "User selected: {{ user }}",
"a11y.interactionAnnouncements.voiceMessageSent.ariaLabel": "Voice message sent",
"a11y.interactionAnnouncements.voiceRecordingAttached.ariaLabel": "Voice recording attached",
"aiComponents.a2ui.choicePickerFilter.placeholder": "Search",
"aiComponents.attachmentPreview.deleteAttachment.ariaLabel": "Delete attachment",
"aiComponents.attachmentPreview.unknownFileName.text": "Unknown file name",
"aiComponents.chart.loading.text": "Loading chart...",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export type TranslationCatalog = {
'a11y.interactionAnnouncements.userSelected.ariaLabel': 'User selected: {{ user }}';
'a11y.interactionAnnouncements.voiceMessageSent.ariaLabel': 'Voice message sent';
'a11y.interactionAnnouncements.voiceRecordingAttached.ariaLabel': 'Voice recording attached';
'aiComponents.a2ui.choicePickerFilter.placeholder': 'Search';
'aiComponents.attachmentPreview.deleteAttachment.ariaLabel': 'Delete attachment';
'aiComponents.attachmentPreview.unknownFileName.text': 'Unknown file name';
'aiComponents.chart.loading.text': 'Loading chart...';
Expand Down
Loading
Loading