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
8 changes: 4 additions & 4 deletions scripts/lint/ban-chat-antipatterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,14 @@ const FILE_SIZE_CEILINGS: Record<string, number> = {
// only for the explicit conversation-persistence contracts exported here.
"src/react/components/chat/chat/index.tsx": 280,
// Message.Sources extracted to composition/message-sources.tsx.
"src/react/components/chat/chat/composition/message.tsx": 987,
"src/react/components/chat/chat/composition/message.tsx": 906,
// Includes the ChatSidebar.Item menu compound (Item.Menu/.Rename/.Delete).
// Split responsibilities before adding more behavior to this file.
"src/react/components/chat/chat/components/sidebar.tsx": 736,
"src/react/components/chat/chat/components/sidebar.tsx": 721,
// useComposerValue extracted to composition/use-composer-value.ts.
"src/react/components/chat/chat/composition/chat-composer.tsx": 619,
"src/react/components/chat/agent-picker.tsx": 502,
"src/react/components/chat/chat-actions.tsx": 515,
"src/react/components/chat/agent-picker.tsx": 494,
"src/react/components/chat/chat-actions.tsx": 315,
"src/react/components/chat/chat/controlled-chat.tsx": 242,
"src/react/components/chat/chat/app-mode-chat.tsx": 177,
};
Expand Down
5 changes: 0 additions & 5 deletions scripts/lint/module-boundaries-baseline.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
{
"broadBarrelImports": [
"browser:src/react/components/LayoutComponent.tsx -> #veryfront/utils",
"browser:src/react/components/ProviderComponent.tsx -> #veryfront/utils",
"browser:src/react/components/optimized-image/OptimizedImage.tsx -> #veryfront/utils",
"browser:src/react/components/optimized-image/SimpleOptimizedImage.tsx -> #veryfront/utils",
"browser:src/react/components/optimized-image/useOptimizedImage.ts -> #veryfront/utils",
"browser:src/rendering/client/prefetch.ts -> #veryfront/utils",
"browser:src/rendering/client/router.ts -> #veryfront/utils",
"browser:src/rendering/client/state-bridge.ts -> #veryfront/utils",
Expand Down
5 changes: 0 additions & 5 deletions scripts/lint/test-typecheck-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,6 @@
"src/platform/adapters/fs/veryfront/adapter-helpers.test.ts",
"src/platform/adapters/fs/veryfront/directory-operations.test.ts",
"src/platform/adapters/redis/node.test.ts",
"src/react/components/chat/chat.test.tsx",
"src/react/components/chat/chat/chat.characterization.test.tsx",
"src/react/components/chat/chat/chat.index.test.tsx",
"src/react/components/chat/chat/controlled-chat.test.tsx",
"src/react/components/optimized-image/helpers.test.ts",
"src/rendering/chunk-optimizer.test.ts",
"src/rendering/client/browser-logger.test.ts",
"src/rendering/element-validator/validator-core.test.ts",
Expand Down
4 changes: 2 additions & 2 deletions src/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ react/
│ ├── chat.tsx # Complete chat interface
│ ├── agent-card.tsx # Agent status visualization
│ ├── message.tsx # Standalone message component
│ ├── markdown.tsx # Markdown rendering component
│ ├── markdown.tsx # Plain source + rich-renderer capability boundary
│ ├── model-selector.tsx # Model selection UI
│ ├── chat-with-sidebar.tsx # Chat with sidebar layout
│ ├── theme.ts # Theme system and defaults
Expand Down Expand Up @@ -84,7 +84,7 @@ react/
- `Chat` - Complete chat interface with theme system and composition API
- `AgentCard` - Agent status and tool visualization
- `Message` - Standalone message component (parts-based format)
- `Markdown` - Markdown rendering with syntax highlighting
- `Markdown` - Escaped Markdown source, with semantic rendering supplied by an extension
- `ModelSelector` - Model selection UI component
- `ChatSidebar` - Conversation rail (pairs with `ConversationsProvider`)
- `OptimizedImage` - Responsive image with lazy loading
Expand Down
141 changes: 141 additions & 0 deletions src/react/chat-barrels.check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,144 @@ export type ChatReactBarrelContracts = [
ChatUseConversationsPersistenceState,
ChatUseConversationsResult,
];

type CompoundChatRuntimeExport =
| "AgentAvatar"
| "ChatEmptyState"
| "ChatMessagesSkeleton"
| "SkillBadge"
| "SourcePill"
| "isSkillToolPart"
| "useAttachmentPill"
| "useAttachments"
| "useAttachmentsPanel"
| "useModelSelector"
| "useSources"
| "useStepIndicator"
| "useStickToBottom"
| "useUpload"
| "useUploadsRegistry";

type ContainsRuntimeExports<TModule, TName extends PropertyKey> =
Exclude<TName, keyof TModule> extends never ? true : never;

const compoundRuntimeParity: [
ContainsRuntimeExports<typeof import("./public.ts"), CompoundChatRuntimeExport>,
ContainsRuntimeExports<
typeof import("./components/chat/index.ts"),
CompoundChatRuntimeExport
>,
] = [true, true];

void compoundRuntimeParity;

export type CompoundChatPublicTypeContracts = [
import("./public.ts").AgentAvatarProps,
import("./public.ts").AttachmentPillContextValue,
import("./public.ts").AttachmentsPanelActionProps,
import("./public.ts").AttachmentsPanelContextValue,
import("./public.ts").AttachmentsPanelEmptyProps,
import("./public.ts").AttachmentsPanelHeaderProps,
import("./public.ts").AttachmentsPanelItemProps,
import("./public.ts").AttachmentsPanelListProps,
import("./public.ts").AttachmentsPanelLoadingProps,
import("./public.ts").ChatEmptyStateAvatarProps,
import("./public.ts").ChatEmptyStateHeadingProps,
import("./public.ts").ChatEmptyStateRootProps,
import("./public.ts").ChatEmptyStateSuggestionProps,
import("./public.ts").ChatEmptyStateSuggestionsProps,
import("./public.ts").ChatMessagesSkeletonProps,
import("./public.ts").ModelSelectorContentProps,
import("./public.ts").ModelSelectorContextValue,
import("./public.ts").ModelSelectorItemProps,
import("./public.ts").ModelSelectorTriggerProps,
import("./public.ts").SkillBadgeProps,
import("./public.ts").SourcePillProps,
import("./public.ts").SourcesContextValue,
import("./public.ts").SourcesListProps,
import("./public.ts").StepIndicatorContextValue,
import("./public.ts").UseAttachmentsOptions,
import("./public.ts").UseAttachmentsRequestState,
import("./public.ts").UseAttachmentsResult,
import("./public.ts").UseAttachmentsStorageState,
import("./public.ts").UseStickToBottomOptions,
import("./public.ts").UseStickToBottomResult<HTMLElement>,
import("./public.ts").UseUploadOptions,
import("./public.ts").UseUploadResult,
import("./public.ts").UseUploadsRegistryOptions,
import("./public.ts").UseUploadsRegistryResult,
];

export type CompoundChatComponentTypeContracts = [
import("./components/chat/index.ts").AgentAvatarProps,
import("./components/chat/index.ts").AttachmentPillContextValue,
import("./components/chat/index.ts").AttachmentsPanelActionProps,
import("./components/chat/index.ts").AttachmentsPanelContextValue,
import("./components/chat/index.ts").AttachmentsPanelEmptyProps,
import("./components/chat/index.ts").AttachmentsPanelHeaderProps,
import("./components/chat/index.ts").AttachmentsPanelItemProps,
import("./components/chat/index.ts").AttachmentsPanelListProps,
import("./components/chat/index.ts").AttachmentsPanelLoadingProps,
import("./components/chat/index.ts").ChatEmptyStateAvatarProps,
import("./components/chat/index.ts").ChatEmptyStateHeadingProps,
import("./components/chat/index.ts").ChatEmptyStateRootProps,
import("./components/chat/index.ts").ChatEmptyStateSuggestionProps,
import("./components/chat/index.ts").ChatEmptyStateSuggestionsProps,
import("./components/chat/index.ts").ChatMessagesSkeletonProps,
import("./components/chat/index.ts").ModelSelectorContentProps,
import("./components/chat/index.ts").ModelSelectorContextValue,
import("./components/chat/index.ts").ModelSelectorItemProps,
import("./components/chat/index.ts").ModelSelectorTriggerProps,
import("./components/chat/index.ts").SkillBadgeProps,
import("./components/chat/index.ts").SourcePillProps,
import("./components/chat/index.ts").SourcesContextValue,
import("./components/chat/index.ts").SourcesListProps,
import("./components/chat/index.ts").StepIndicatorContextValue,
import("./components/chat/index.ts").UseAttachmentsOptions,
import("./components/chat/index.ts").UseAttachmentsRequestState,
import("./components/chat/index.ts").UseAttachmentsResult,
import("./components/chat/index.ts").UseAttachmentsStorageState,
import("./components/chat/index.ts").UseStickToBottomOptions,
import("./components/chat/index.ts").UseStickToBottomResult<HTMLElement>,
import("./components/chat/index.ts").UseUploadOptions,
import("./components/chat/index.ts").UseUploadResult,
import("./components/chat/index.ts").UseUploadsRegistryOptions,
import("./components/chat/index.ts").UseUploadsRegistryResult,
];

export type CompoundCanonicalChatTypeContracts = [
import("../chat/index.ts").AgentAvatarProps,
import("../chat/index.ts").AttachmentPillContextValue,
import("../chat/index.ts").AttachmentsPanelActionProps,
import("../chat/index.ts").AttachmentsPanelContextValue,
import("../chat/index.ts").AttachmentsPanelEmptyProps,
import("../chat/index.ts").AttachmentsPanelHeaderProps,
import("../chat/index.ts").AttachmentsPanelItemProps,
import("../chat/index.ts").AttachmentsPanelListProps,
import("../chat/index.ts").AttachmentsPanelLoadingProps,
import("../chat/index.ts").ChatEmptyStateAvatarProps,
import("../chat/index.ts").ChatEmptyStateHeadingProps,
import("../chat/index.ts").ChatEmptyStateRootProps,
import("../chat/index.ts").ChatEmptyStateSuggestionProps,
import("../chat/index.ts").ChatEmptyStateSuggestionsProps,
import("../chat/index.ts").ChatMessagesSkeletonProps,
import("../chat/index.ts").ModelSelectorContentProps,
import("../chat/index.ts").ModelSelectorContextValue,
import("../chat/index.ts").ModelSelectorItemProps,
import("../chat/index.ts").ModelSelectorTriggerProps,
import("../chat/index.ts").SkillBadgeProps,
import("../chat/index.ts").SourcePillProps,
import("../chat/index.ts").SourcesContextValue,
import("../chat/index.ts").SourcesListProps,
import("../chat/index.ts").StepIndicatorContextValue,
import("../chat/index.ts").UseAttachmentsOptions,
import("../chat/index.ts").UseAttachmentsRequestState,
import("../chat/index.ts").UseAttachmentsResult,
import("../chat/index.ts").UseAttachmentsStorageState,
import("../chat/index.ts").UseStickToBottomOptions,
import("../chat/index.ts").UseStickToBottomResult<HTMLElement>,
import("../chat/index.ts").UseUploadOptions,
import("../chat/index.ts").UseUploadResult,
import("../chat/index.ts").UseUploadsRegistryOptions,
import("../chat/index.ts").UseUploadsRegistryResult,
];
76 changes: 48 additions & 28 deletions src/react/compat/config-generator.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { rendererLogger as logger } from "#veryfront/utils";
import { join } from "#veryfront/compat/path/index.ts";
import { createError, toError } from "#veryfront/errors";
import { createFileSystem } from "#veryfront/platform/compat/fs.ts";
import { createFileSystem, isNotFoundError } from "#veryfront/platform/compat/fs.ts";
import { getReactUrls } from "#veryfront/transforms/esm/react-cdn.ts";
import {
REACT_VERSION_17,
REACT_VERSION_18_2,
REACT_VERSION_19_RC,
REACT_VERSION_19,
} from "#veryfront/utils/constants/cdn.ts";

export type ReactVersion = "17" | "18" | "19";
Expand Down Expand Up @@ -36,11 +36,29 @@ export const REACT_CONFIGS: Record<ReactVersion, ReactVersionConfig> = {
},
"19": {
version: "19",
exact: REACT_VERSION_19_RC,
imports: getReactUrls(REACT_VERSION_19_RC),
exact: REACT_VERSION_19,
imports: getReactUrls(REACT_VERSION_19),
},
};

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

function getImportMap(
value: unknown,
source: string,
): Record<string, string> {
if (value === undefined) return {};
if (
!isRecord(value) ||
Object.values(value).some((specifier) => typeof specifier !== "string")
) {
throw new TypeError(`${source} imports must be an object with string values`);
}
return value as Record<string, string>;
}

function getReactConfig(version: ReactVersion): ReactVersionConfig {
const config = REACT_CONFIGS[version];
if (config) return config;
Expand All @@ -65,21 +83,28 @@ export async function generateReactVersionConfig(

let baseConfig: Record<string, unknown> = {};
try {
baseConfig = JSON.parse(await fs.readTextFile(baseConfigPath));
const parsed: unknown = JSON.parse(await fs.readTextFile(baseConfigPath));
if (!isRecord(parsed)) {
throw new TypeError(`Base config at ${baseConfigPath} must contain a JSON object`);
}
baseConfig = parsed;
} catch (error) {
logger.warn(`Could not read base config from ${baseConfigPath}`, error);
if (!isNotFoundError(error)) throw error;
logger.warn(`Base config not found at ${baseConfigPath}; generating without it`);
}

const baseImports = (baseConfig.imports as Record<string, string> | undefined) ?? {};
const additionalImports = (options.additional?.imports as Record<string, string> | undefined) ??
{};
const baseImports = getImportMap(baseConfig.imports, `Base config at ${baseConfigPath}`);
const additionalImports = getImportMap(
options.additional?.imports,
"Additional config",
);

const versionConfig = {
...baseConfig,
imports: {
...baseImports,
...config.imports,
...additionalImports,
...config.imports,
},
};

Expand Down Expand Up @@ -108,27 +133,22 @@ export async function detectReactVersionFromConfig(
const configPath = join(projectDir, "deno.json");

try {
const config = JSON.parse(await fs.readTextFile(configPath)) as {
imports?: { react?: string };
};

const reactImport = config.imports?.react;
if (!reactImport) return null;

for (const [version, versionConfig] of Object.entries(REACT_CONFIGS)) {
if (reactImport.includes(`@${versionConfig.exact}`)) {
return version as ReactVersion;
}
const parsed: unknown = JSON.parse(await fs.readTextFile(configPath));
if (!isRecord(parsed)) {
throw new TypeError(`React config at ${configPath} must contain a JSON object`);
}
const reactImport = getImportMap(
parsed.imports,
`React config at ${configPath}`,
).react;
if (!reactImport) return null;

if (reactImport.includes("@17")) return "17";
if (reactImport.includes("@18")) return "18";
if (reactImport.includes("@19")) return "19";

return null;
const match = /(?:^|[/:])react@(\d+)(?=$|[.\-+/?#&:])/u.exec(reactImport);
const major = match?.[1];
return major === "17" || major === "18" || major === "19" ? major : null;
} catch (error) {
logger.error("Failed to detect React version from config", error);
return null;
if (isNotFoundError(error)) return null;
throw error;
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/react/compat/hooks-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,10 @@ describe("hooks-adapter", () => {

assertEquals(info.features.renderToString, true);
assertEquals(info.features.renderToStaticMarkup, true);
assertEquals(info.features.renderToNodeStream, true);
assertEquals(
info.features.renderToNodeStream,
info.major < 19 && !info.isReact19,
);

if (info.major >= 18) {
assertEquals(info.features.renderToPipeableStream, true);
Expand Down
1 change: 0 additions & 1 deletion src/react/compat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ export {
} from "./ssr-adapter/index.ts";
export {
checkVersionCompatibility,
clearProjectVersionCache,
detectReactVersion,
getReactVersionInfo,
getReactVersionInfoForProject,
Expand Down
Loading