From 49b5a739d8a15eda470ef1af01cea0d517548ede Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Thu, 29 Oct 2026 03:23:44 +0100 Subject: [PATCH] fix: support exactOptionalPropertyTypes in plugin option types Follow-up to #704 (see #703). #704 added `| undefined` to optional props in `streamdown` and `remend`, but the plugin packages still declared plain `prop?: T`. With `exactOptionalPropertyTypes` on, passing a possibly-`undefined` value to `createCodePlugin`, `createMathPlugin` or `createMermaidPlugin` failed with TS2375/TS2379. This change only touches types. The implementations already handle `undefined` through `??` and object spread. - `@streamdown/code`: `CodePluginOptions.themes` and `HighlightOptions.isIncomplete` (the second matches what #704 did in `streamdown/lib/shiki-types.ts`) - `@streamdown/math`: `MathPluginOptions.errorColor`, `MathPluginOptions.singleDollarTextMath`, `MathPlugin.getStyles` - `@streamdown/mermaid`: `MermaidPluginOptions.config` Optional function parameters such as `callback?` and `getMermaid(config?)` are unchanged, because the flag doesn't apply to them. `@streamdown/cjk` has no options type. **Regression tests:** each plugin package now has a `tsconfig.exact-optional.json` and a `type-tests/exact-optional-property-types.ts` file, following the pattern from #704. The tests type-check against the built `dist` declarations. Each package also gets a `check-types:exact-optional` script, and `type-tests` is excluded from each package's normal tsconfig. The existing CI step now runs the check for the three plugin packages as well as `streamdown`. Before the type change, the new tests fail with TS2375/TS2379; after it, they pass. Includes a patch changeset for `@streamdown/code`, `@streamdown/math` and `@streamdown/mermaid`. --- .../plugin-exact-optional-property-types.md | 7 +++++ .github/workflows/test.yml | 2 +- packages/streamdown-code/index.ts | 4 +-- packages/streamdown-code/package.json | 1 + .../tsconfig.exact-optional.json | 8 +++++ packages/streamdown-code/tsconfig.json | 2 +- .../exact-optional-property-types.ts | 27 ++++++++++++++++ packages/streamdown-math/index.ts | 6 ++-- packages/streamdown-math/package.json | 1 + .../tsconfig.exact-optional.json | 8 +++++ packages/streamdown-math/tsconfig.json | 2 +- .../exact-optional-property-types.ts | 31 +++++++++++++++++++ packages/streamdown-mermaid/index.ts | 2 +- packages/streamdown-mermaid/package.json | 1 + .../tsconfig.exact-optional.json | 8 +++++ packages/streamdown-mermaid/tsconfig.json | 2 +- .../exact-optional-property-types.ts | 19 ++++++++++++ 17 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 .changeset/plugin-exact-optional-property-types.md create mode 100644 packages/streamdown-code/tsconfig.exact-optional.json create mode 100644 packages/streamdown-code/type-tests/exact-optional-property-types.ts create mode 100644 packages/streamdown-math/tsconfig.exact-optional.json create mode 100644 packages/streamdown-math/type-tests/exact-optional-property-types.ts create mode 100644 packages/streamdown-mermaid/tsconfig.exact-optional.json create mode 100644 packages/streamdown-mermaid/type-tests/exact-optional-property-types.ts diff --git a/.changeset/plugin-exact-optional-property-types.md b/.changeset/plugin-exact-optional-property-types.md new file mode 100644 index 000000000..b954c0be2 --- /dev/null +++ b/.changeset/plugin-exact-optional-property-types.md @@ -0,0 +1,7 @@ +--- +"@streamdown/code": patch +"@streamdown/math": patch +"@streamdown/mermaid": patch +--- + +Support `exactOptionalPropertyTypes`: add `| undefined` to optional props in the plugin option types so possibly-`undefined` values can be forwarded to `createCodePlugin`, `createMathPlugin` and `createMermaidPlugin` without type errors diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 892f27f76..aabb83ae4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,7 +32,7 @@ jobs: run: pnpm build:packages - name: Check exactOptionalPropertyTypes compatibility - run: pnpm --filter streamdown exec tsc -p tsconfig.exact-optional.json + run: pnpm --filter streamdown --filter @streamdown/code --filter @streamdown/math --filter @streamdown/mermaid exec tsc -p tsconfig.exact-optional.json - name: Build Website run: pnpm build --filter website diff --git a/packages/streamdown-code/index.ts b/packages/streamdown-code/index.ts index 146046b15..a9b015fd2 100644 --- a/packages/streamdown-code/index.ts +++ b/packages/streamdown-code/index.ts @@ -33,7 +33,7 @@ export interface HighlightOptions { * Whether the code block is still streaming. Its result is superseded by * the next update, so it is not cached. */ - isIncomplete?: boolean; + isIncomplete?: boolean | undefined; language: BundledLanguage; themes: [ThemeInput, ThemeInput]; } @@ -75,7 +75,7 @@ export interface CodePluginOptions { * Default themes for syntax highlighting [light, dark] * @default ["github-light", "github-dark"] */ - themes?: [ThemeInput, ThemeInput]; + themes?: [ThemeInput, ThemeInput] | undefined; } const languageAliases = Object.fromEntries( diff --git a/packages/streamdown-code/package.json b/packages/streamdown-code/package.json index 4fa4eebc0..e63c76fd2 100644 --- a/packages/streamdown-code/package.json +++ b/packages/streamdown-code/package.json @@ -28,6 +28,7 @@ "scripts": { "bench": "vitest bench --run > results.txt", "build": "tsup", + "check-types:exact-optional": "pnpm build && tsc -p tsconfig.exact-optional.json", "test": "vitest run", "test:coverage": "vitest --coverage run" }, diff --git a/packages/streamdown-code/tsconfig.exact-optional.json b/packages/streamdown-code/tsconfig.exact-optional.json new file mode 100644 index 000000000..fc13af8e1 --- /dev/null +++ b/packages/streamdown-code/tsconfig.exact-optional.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "exactOptionalPropertyTypes": true + }, + "include": ["type-tests/exact-optional-property-types.ts"], + "exclude": ["node_modules", "build", "dist"] +} diff --git a/packages/streamdown-code/tsconfig.json b/packages/streamdown-code/tsconfig.json index 17da37c98..fa5817035 100644 --- a/packages/streamdown-code/tsconfig.json +++ b/packages/streamdown-code/tsconfig.json @@ -16,5 +16,5 @@ "noEmit": true }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "build", "dist"] + "exclude": ["node_modules", "build", "dist", "type-tests"] } diff --git a/packages/streamdown-code/type-tests/exact-optional-property-types.ts b/packages/streamdown-code/type-tests/exact-optional-property-types.ts new file mode 100644 index 000000000..ca742d360 --- /dev/null +++ b/packages/streamdown-code/type-tests/exact-optional-property-types.ts @@ -0,0 +1,27 @@ +/** + * Type-level regression test for `exactOptionalPropertyTypes` support. + * + * This file is type-checked with `tsconfig.exact-optional.json` + * (`pnpm check-types:exact-optional`) against the built declarations, with + * `exactOptionalPropertyTypes` enabled. It is never executed. + */ +import { createCodePlugin, type ThemeInput } from "../dist/index"; + +interface CodeOptions { + isIncomplete?: boolean; + themes?: [ThemeInput, ThemeInput]; +} + +// Forwarding possibly-`undefined` optional props must type-check. +export const createPlugin = (options: CodeOptions) => + createCodePlugin({ themes: options.themes }); + +export const plugin = createCodePlugin({ themes: undefined }); + +export const highlight = (options: CodeOptions) => + plugin.highlight({ + code: "", + isIncomplete: options.isIncomplete, + language: "ts", + themes: plugin.getThemes(), + }); diff --git a/packages/streamdown-math/index.ts b/packages/streamdown-math/index.ts index fc7d4f13a..87d33efa9 100644 --- a/packages/streamdown-math/index.ts +++ b/packages/streamdown-math/index.ts @@ -11,7 +11,7 @@ export interface MathPlugin { /** * Get CSS styles path for math rendering */ - getStyles?: () => string; + getStyles?: (() => string) | undefined; name: "katex"; /** * Rehype plugin for rendering math @@ -32,12 +32,12 @@ export interface MathPluginOptions { * KaTeX error color * @default "var(--color-muted-foreground)" */ - errorColor?: string; + errorColor?: string | undefined; /** * Enable single dollar sign for inline math ($...$) * @default false */ - singleDollarTextMath?: boolean; + singleDollarTextMath?: boolean | undefined; } /** diff --git a/packages/streamdown-math/package.json b/packages/streamdown-math/package.json index b4024a75f..98e9de4a5 100644 --- a/packages/streamdown-math/package.json +++ b/packages/streamdown-math/package.json @@ -24,6 +24,7 @@ ], "scripts": { "build": "tsup", + "check-types:exact-optional": "pnpm build && tsc -p tsconfig.exact-optional.json", "test": "vitest run", "test:coverage": "vitest --coverage run" }, diff --git a/packages/streamdown-math/tsconfig.exact-optional.json b/packages/streamdown-math/tsconfig.exact-optional.json new file mode 100644 index 000000000..fc13af8e1 --- /dev/null +++ b/packages/streamdown-math/tsconfig.exact-optional.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "exactOptionalPropertyTypes": true + }, + "include": ["type-tests/exact-optional-property-types.ts"], + "exclude": ["node_modules", "build", "dist"] +} diff --git a/packages/streamdown-math/tsconfig.json b/packages/streamdown-math/tsconfig.json index 17da37c98..fa5817035 100644 --- a/packages/streamdown-math/tsconfig.json +++ b/packages/streamdown-math/tsconfig.json @@ -16,5 +16,5 @@ "noEmit": true }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "build", "dist"] + "exclude": ["node_modules", "build", "dist", "type-tests"] } diff --git a/packages/streamdown-math/type-tests/exact-optional-property-types.ts b/packages/streamdown-math/type-tests/exact-optional-property-types.ts new file mode 100644 index 000000000..117782203 --- /dev/null +++ b/packages/streamdown-math/type-tests/exact-optional-property-types.ts @@ -0,0 +1,31 @@ +/** + * Type-level regression test for `exactOptionalPropertyTypes` support. + * + * This file is type-checked with `tsconfig.exact-optional.json` + * (`pnpm check-types:exact-optional`) against the built declarations, with + * `exactOptionalPropertyTypes` enabled. It is never executed. + */ +import { createMathPlugin, type MathPlugin } from "../dist/index"; + +interface MathOptions { + errorColor?: string; + getStyles?: () => string; + singleDollarTextMath?: boolean; +} + +// Forwarding possibly-`undefined` optional props must type-check. +export const createPlugin = (options: MathOptions) => + createMathPlugin({ + errorColor: options.errorColor, + singleDollarTextMath: options.singleDollarTextMath, + }); + +export const plugin = createMathPlugin({ + errorColor: undefined, + singleDollarTextMath: undefined, +}); + +export const withStyles = (options: MathOptions): MathPlugin => ({ + ...plugin, + getStyles: options.getStyles, +}); diff --git a/packages/streamdown-mermaid/index.ts b/packages/streamdown-mermaid/index.ts index 704808de3..64b70f226 100644 --- a/packages/streamdown-mermaid/index.ts +++ b/packages/streamdown-mermaid/index.ts @@ -34,7 +34,7 @@ export interface MermaidPluginOptions { /** * Default Mermaid configuration */ - config?: MermaidConfig; + config?: MermaidConfig | undefined; } const defaultConfig: MermaidConfig = { diff --git a/packages/streamdown-mermaid/package.json b/packages/streamdown-mermaid/package.json index cf3d0c8a0..f94e1bb54 100644 --- a/packages/streamdown-mermaid/package.json +++ b/packages/streamdown-mermaid/package.json @@ -24,6 +24,7 @@ ], "scripts": { "build": "tsup", + "check-types:exact-optional": "pnpm build && tsc -p tsconfig.exact-optional.json", "test": "vitest run", "test:coverage": "vitest --coverage run" }, diff --git a/packages/streamdown-mermaid/tsconfig.exact-optional.json b/packages/streamdown-mermaid/tsconfig.exact-optional.json new file mode 100644 index 000000000..fc13af8e1 --- /dev/null +++ b/packages/streamdown-mermaid/tsconfig.exact-optional.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "exactOptionalPropertyTypes": true + }, + "include": ["type-tests/exact-optional-property-types.ts"], + "exclude": ["node_modules", "build", "dist"] +} diff --git a/packages/streamdown-mermaid/tsconfig.json b/packages/streamdown-mermaid/tsconfig.json index 17da37c98..fa5817035 100644 --- a/packages/streamdown-mermaid/tsconfig.json +++ b/packages/streamdown-mermaid/tsconfig.json @@ -16,5 +16,5 @@ "noEmit": true }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "build", "dist"] + "exclude": ["node_modules", "build", "dist", "type-tests"] } diff --git a/packages/streamdown-mermaid/type-tests/exact-optional-property-types.ts b/packages/streamdown-mermaid/type-tests/exact-optional-property-types.ts new file mode 100644 index 000000000..35fa40557 --- /dev/null +++ b/packages/streamdown-mermaid/type-tests/exact-optional-property-types.ts @@ -0,0 +1,19 @@ +/** + * Type-level regression test for `exactOptionalPropertyTypes` support. + * + * This file is type-checked with `tsconfig.exact-optional.json` + * (`pnpm check-types:exact-optional`) against the built declarations, with + * `exactOptionalPropertyTypes` enabled. It is never executed. + */ +import type { MermaidConfig } from "mermaid"; +import { createMermaidPlugin } from "../dist/index"; + +interface MermaidOptions { + config?: MermaidConfig; +} + +// Forwarding possibly-`undefined` optional props must type-check. +export const createPlugin = (options: MermaidOptions) => + createMermaidPlugin({ config: options.config }); + +export const plugin = createMermaidPlugin({ config: undefined });