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
6 changes: 6 additions & 0 deletions .changeset/exact-optional-property-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"streamdown": patch
"remend": patch
---

Support `exactOptionalPropertyTypes`: add `| undefined` to optional props in the public types so possibly-`undefined` values can be forwarded to `<Streamdown>` and `remend` without type errors
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
- name: Build Packages
run: pnpm build:packages

- name: Check exactOptionalPropertyTypes compatibility
run: pnpm --filter streamdown exec tsc -p tsconfig.exact-optional.json

- name: Build Website
run: pnpm build --filter website

Expand Down
32 changes: 16 additions & 16 deletions packages/remend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export interface RemendHandler {
/** Unique identifier for this handler */
name: string;
/** Priority (lower runs first). Built-in priorities: 0-100. Default: 100 */
priority?: number;
priority?: number | undefined;
}

/**
Expand All @@ -53,42 +53,42 @@ export interface RemendHandler {
*/
export interface RemendOptions {
/** Complete bold formatting (e.g., `**text` → `**text**`) */
bold?: boolean;
bold?: boolean | undefined;
/** Complete bold-italic formatting (e.g., `***text` → `***text***`) */
boldItalic?: boolean;
boldItalic?: boolean | undefined;
/** Escape > as comparison operators in list items (e.g., `- > 25` → `- \> 25`) */
comparisonOperators?: boolean;
comparisonOperators?: boolean | undefined;
/** Custom handlers to extend remend */
handlers?: RemendHandler[];
handlers?: RemendHandler[] | undefined;
/** Strip incomplete HTML tags at end of streaming text (e.g., `text <custom` → `text`) */
htmlTags?: boolean;
htmlTags?: boolean | undefined;
/** Complete images (e.g., `![alt](url` → `![alt](streamdown:incomplete-image)`) */
images?: boolean;
images?: boolean | undefined;
/** Complete inline code formatting (e.g., `` `code `` → `` `code` ``) */
inlineCode?: boolean;
inlineCode?: boolean | undefined;
/**
* Complete inline KaTeX math (e.g., `$equation` → `$equation$`).
* Defaults to `false` — single `$` is ambiguous with currency symbols.
*/
inlineKatex?: boolean;
inlineKatex?: boolean | undefined;
/** Complete italic formatting (e.g., `*text` → `*text*` or `_text` → `_text_`) */
italic?: boolean;
italic?: boolean | undefined;
/** Complete block KaTeX math (e.g., `$$equation` → `$$equation$$`) */
katex?: boolean;
katex?: boolean | undefined;
/**
* How to handle incomplete links:
* - `'protocol'`: Use `streamdown:incomplete-link` placeholder URL (default)
* - `'text-only'`: Display only the link text without any link markup
*/
linkMode?: "protocol" | "text-only";
linkMode?: "protocol" | "text-only" | undefined;
/** Complete links and images (e.g., `[text](url` → `[text](streamdown:incomplete-link)`) */
links?: boolean;
links?: boolean | undefined;
/** Handle incomplete setext headings to prevent misinterpretation */
setextHeadings?: boolean;
setextHeadings?: boolean | undefined;
/** Escape single ~ between word characters to prevent false strikethrough (e.g., `20~25` → `20\~25`) */
singleTilde?: boolean;
singleTilde?: boolean | undefined;
/** Complete strikethrough formatting (e.g., `~~text` → `~~text~~`) */
strikethrough?: boolean;
strikethrough?: boolean | undefined;
}

// Helper to check if an option is enabled (defaults to true)
Expand Down
117 changes: 61 additions & 56 deletions packages/streamdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@ export type DownloadControlConfig = boolean | { filename: string };
export type CopyControlConfig =
| boolean
| {
onCopy?: () => void;
onError?: (error: Error) => void;
onCopy?: (() => void) | undefined;
onError?: ((error: Error) => void) | undefined;
};

export type ControlsConfig =
Expand All @@ -211,26 +211,29 @@ export type ControlsConfig =
table?:
| boolean
| {
copy?: boolean;
csvSeparator?: CSVSeparator;
download?: DownloadControlConfig;
fullscreen?: boolean;
};
copy?: boolean | undefined;
csvSeparator?: CSVSeparator | undefined;
download?: DownloadControlConfig | undefined;
fullscreen?: boolean | undefined;
}
| undefined;
code?:
| boolean
| {
copy?: CopyControlConfig;
download?: DownloadControlConfig;
};
copy?: CopyControlConfig | undefined;
download?: DownloadControlConfig | undefined;
}
| undefined;
mermaid?:
| boolean
| {
download?: DownloadControlConfig;
copy?: CopyControlConfig;
fullscreen?: boolean;
panZoom?: boolean;
};
image?: boolean | { download?: boolean };
download?: DownloadControlConfig | undefined;
copy?: CopyControlConfig | undefined;
fullscreen?: boolean | undefined;
panZoom?: boolean | undefined;
}
| undefined;
image?: boolean | { download?: boolean | undefined } | undefined;
};

export interface LinkSafetyModalProps {
Expand All @@ -242,8 +245,8 @@ export interface LinkSafetyModalProps {

export interface LinkSafetyConfig {
enabled: boolean;
onLinkCheck?: (url: string) => Promise<boolean> | boolean;
renderModal?: (props: LinkSafetyModalProps) => React.ReactNode;
onLinkCheck?: ((url: string) => Promise<boolean> | boolean) | undefined;
renderModal?: ((props: LinkSafetyModalProps) => React.ReactNode) | undefined;
}

export interface MermaidErrorComponentProps {
Expand All @@ -253,15 +256,15 @@ export interface MermaidErrorComponentProps {
}

export interface MermaidOptions {
config?: MermaidConfig;
errorComponent?: React.ComponentType<MermaidErrorComponentProps>;
config?: MermaidConfig | undefined;
errorComponent?: React.ComponentType<MermaidErrorComponentProps> | undefined;
}

export type AllowedTags = Record<string, string[]>;
export type PortalTarget = HTMLElement | null | (() => HTMLElement | null);

export type StreamdownProps = Options & {
mode?: "static" | "streaming";
mode?: "static" | "streaming" | undefined;
/**
* Text direction. `"ltr"` / `"rtl"` force a single direction.
* `"auto"` detects direction per block: in streaming mode via parsed
Expand All @@ -271,39 +274,39 @@ export type StreamdownProps = Options & {
* first-strong as the tie-breaker; fenced/inline code is excluded from
* the evidence and code blocks are always rendered LTR.
*/
dir?: "auto" | "ltr" | "rtl";
BlockComponent?: React.ComponentType<BlockProps>;
parseMarkdownIntoBlocksFn?: (markdown: string) => string[];
parseIncompleteMarkdown?: boolean;
dir?: "auto" | "ltr" | "rtl" | undefined;
BlockComponent?: React.ComponentType<BlockProps> | undefined;
parseMarkdownIntoBlocksFn?: ((markdown: string) => string[]) | undefined;
parseIncompleteMarkdown?: boolean | undefined;
/** Normalize HTML block indentation to prevent 4+ spaces being treated as code blocks. @default false */
normalizeHtmlIndentation?: boolean;
className?: string;
shikiTheme?: [ThemeInput, ThemeInput];
mermaid?: MermaidOptions;
normalizeHtmlIndentation?: boolean | undefined;
className?: string | undefined;
shikiTheme?: [ThemeInput, ThemeInput] | undefined;
mermaid?: MermaidOptions | undefined;
/**
* Max height for fenced code blocks. Numbers are treated as px.
* Set to `0` or `Infinity` to disable. @default 400
*/
codeBlockMaxHeight?: number | string;
controls?: ControlsConfig;
isAnimating?: boolean;
codeBlockMaxHeight?: number | string | undefined;
controls?: ControlsConfig | undefined;
isAnimating?: boolean | undefined;
/**
* Max height for tables. Numbers are treated as px.
* Set to `0` or `Infinity` to disable. @default 300
*/
tableMaxHeight?: number | string;
animated?: boolean | AnimateOptions;
caret?: keyof typeof carets;
plugins?: PluginConfig;
remend?: RemendOptions;
linkSafety?: LinkSafetyConfig;
tableMaxHeight?: number | string | undefined;
animated?: boolean | AnimateOptions | undefined;
caret?: keyof typeof carets | undefined;
plugins?: PluginConfig | undefined;
remend?: RemendOptions | undefined;
linkSafety?: LinkSafetyConfig | undefined;
/**
* DOM node for Streamdown overlays, or a function returning one.
* Defaults to `document.body`.
*/
portal?: PortalTarget;
portal?: PortalTarget | undefined;
/** Custom tags to allow through sanitization with their permitted attributes */
allowedTags?: AllowedTags;
allowedTags?: AllowedTags | undefined;
/**
* Fallback component for HTML tags or `allowedTags` entries that have no
* matching key in the `components` map. Built-in and explicit `components`
Expand All @@ -328,7 +331,9 @@ export type StreamdownProps = Options & {
* </Streamdown>
* ```
*/
fallbackComponent?: React.ComponentType<Record<string, unknown> & ExtraProps>;
fallbackComponent?:
| React.ComponentType<Record<string, unknown> & ExtraProps>
| undefined;
/**
* Tags whose children should be treated as plain text (no markdown parsing).
* Useful for mention/entity tags in AI UIs where child content is a data
Expand All @@ -344,7 +349,7 @@ export type StreamdownProps = Options & {
* </Streamdown>
* ```
*/
literalTagContent?: string[];
literalTagContent?: string[] | undefined;
/**
* Disable GFM / CommonMark autolinking for specific URL protocols (e.g. bare
* email addresses become `mailto:` autolinks). Accepts protocol names with
Expand All @@ -360,27 +365,27 @@ export type StreamdownProps = Options & {
* </Streamdown>
* ```
*/
disableAutolinkProtocols?: string[];
disableAutolinkProtocols?: string[] | undefined;
/** Override UI strings for i18n / custom labels */
translations?: Partial<StreamdownTranslations>;
translations?: Partial<StreamdownTranslations> | undefined;
/** Custom icons to override the default icons used in controls */
icons?: Partial<IconMap>;
icons?: Partial<IconMap> | undefined;
/** Tailwind CSS prefix to prepend to all utility classes (e.g. `"tw"` produces `tw:flex` instead of `flex`). Enables Tailwind v4's `prefix()` support. Note: user-supplied `className` values are also prefixed. */
prefix?: string;
prefix?: string | undefined;
/** Show line numbers in code blocks. @default true */
lineNumbers?: boolean;
lineNumbers?: boolean | undefined;
/**
* Code fence languages rendered verbatim as plain monospace text, without
* syntax highlighting, line numbers or controls. Useful for ASCII / Unicode
* box-drawing diagrams where column alignment matters.
* Custom renderers and the Mermaid plugin take precedence for matching languages.
* @default []
*/
codeBlockRawLanguages?: string[];
codeBlockRawLanguages?: string[] | undefined;
/** Called when isAnimating transitions from false to true. Suppressed in mode="static". */
onAnimationStart?: () => void;
onAnimationStart?: (() => void) | undefined;
/** Called when isAnimating transitions from true to false. Suppressed in mode="static". */
onAnimationEnd?: () => void;
onAnimationEnd?: (() => void) | undefined;
/** `data-*` attributes, like `aria-*` attributes, are set on the root element. */
[dataAttribute: `data-${string}`]:
| string
Expand Down Expand Up @@ -485,15 +490,15 @@ export interface StreamdownContextType {
/** Max height for fenced code blocks. @default 400 */
codeBlockMaxHeight: number | string;
/** Code fence languages rendered verbatim without highlighting. */
codeBlockRawLanguages?: string[];
codeBlockRawLanguages?: string[] | undefined;
controls: ControlsConfig;
isAnimating: boolean;
/** Show line numbers in code blocks. @default true */
lineNumbers: boolean;
linkSafety?: LinkSafetyConfig;
mermaid?: MermaidOptions;
linkSafety?: LinkSafetyConfig | undefined;
mermaid?: MermaidOptions | undefined;
mode: "static" | "streaming";
portal?: PortalTarget;
portal?: PortalTarget | undefined;
shikiTheme: [ThemeInput, ThemeInput];
/** Max height for tables. @default 300 */
tableMaxHeight: number | string;
Expand Down Expand Up @@ -539,9 +544,9 @@ export type BlockProps = Options & {
/** Whether this block is incomplete (still being streamed) */
isIncomplete: boolean;
/** Resolved text direction for this block */
dir?: "ltr" | "rtl";
dir?: "ltr" | "rtl" | undefined;
/** Animate plugin instance for tracking previous content length */
animatePlugin?: AnimatePlugin | null;
animatePlugin?: AnimatePlugin | null | undefined;
};

export const Block = memo(
Expand Down
16 changes: 8 additions & 8 deletions packages/streamdown/lib/animate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ export interface AnimateTimeline {
}

export interface CreateAnimateTimelineOptions {
maxBacklogMs?: number;
now?: () => number;
maxBacklogMs?: number | undefined;
now?: (() => number) | undefined;
}

const defaultNow = (): number =>
Expand Down Expand Up @@ -146,17 +146,17 @@ export interface AnimatePlugin {
}

export interface AnimateOptions {
animation?: "fadeIn" | "blurIn" | "slideUp" | (string & {});
duration?: number;
easing?: string;
animation?: "fadeIn" | "blurIn" | "slideUp" | (string & {}) | undefined;
duration?: number | undefined;
easing?: string | undefined;
/**
* Soft cap (ms) on how far ahead of wall-clock words may be scheduled.
* Larger values favour longer cascades / stricter cross-block serialization;
* smaller values keep a fast stream visually caught up. @default 320
*/
maxBacklogMs?: number;
sep?: "word" | "char";
stagger?: number;
maxBacklogMs?: number | undefined;
sep?: "word" | "char" | undefined;
stagger?: number | undefined;
}

const WHITESPACE_RE = /\s/;
Expand Down
2 changes: 1 addition & 1 deletion packages/streamdown/lib/icon-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from "./icons";

export type IconComponent = React.ComponentType<
SVGProps<SVGSVGElement> & { size?: number }
SVGProps<SVGSVGElement> & { size?: number | undefined }
>;

export interface IconMap {
Expand Down
Loading
Loading