Skip to content

Commit 3aa7b89

Browse files
wicksipediaclaude
andauthored
fix(@tinacms/astro): astro check rejects generated rich-text fields passed to TinaMarkdown (tinacms#7605)
## Problem `@tinacms/cli` 3.0.0 (tinacms#7229) types rich-text fields as `TinaMarkdownContent` from `tinacms`: ```ts body?: Maybe<Scalars['RichText']['output']>; // TinaMarkdownContent ``` `<TinaMarkdown>` in `@tinacms/astro` types `content` as `TinaRichTextContent`, whose root has the literal `type: 'root'`. `TinaMarkdownContent` has `type: string`, so TypeScript rejects the generated field: ``` Type 'TinaMarkdownContent | null' is not assignable to type 'TinaRichTextContent'. Type 'TinaMarkdownContent' is not assignable to type 'TinaRichTextRoot'. Types of property 'type' are incompatible. Type 'string' is not assignable to type '"root"'. ``` An Astro project that passes a queried body straight to the renderer gets this error from `astro check`. `examples/astro/visual-editing` passes `data._body` in `PostBody.astro` and `BlogBody.astro`. That example does not run `astro check`, so CI did not catch it. Before tinacms#7229 the field was `any`, and the same call compiled. ## Change `content` accepts both types, as a root or as an array of nodes. The React `<TinaMarkdown>` accepts the same two forms. ```ts type GeneratedRichTextContent = { type: string; children: GeneratedRichTextContent[]; }; export interface TinaMarkdownProps { content: | TinaRichTextContent | GeneratedRichTextContent | GeneratedRichTextContent[]; components?: CustomComponentsMap; } ``` - `TinaMarkdown.astro` and the placeholder in `index.ts` both use `TinaMarkdownProps`. The two prop declarations can no longer drift apart. - `@tinacms/astro` does not depend on `tinacms`, so `GeneratedRichTextContent` copies the shape instead of importing it. It stays private to `types.ts`. - `TinaRichTextContent` does not change. Code that narrows on it keeps its literal types. - Rendering does not change. Both types describe the same Plate AST. A component that wraps `<TinaMarkdown>` can type its own prop as `TinaMarkdownProps['content']`. ## Why not change the CLI The CLI could detect `@tinacms/astro` and emit `TinaRichTextContent` instead. That adds framework detection to codegen, and Astro users who already moved to CLI 3 would see their generated types change a second time. The mismatch sits in the renderer's prop, so the fix goes there. ## Testing - `src/TinaMarkdown.test-d.ts` checks that `content` accepts the CLI shape with `null` and `undefined`, an array of CLI nodes, and every `TinaRichTextContent` value. It also checks that `content` rejects a `string`, so a regression to `any` fails. If you remove either generated member from the union, its case fails. - `pnpm exec vitest run` in `packages/@tinacms/astro`: 84 tests pass, no type errors. - `pnpm exec tsc --noEmit`: passes. - On an Astro 7 site with CLI 3.0.0, released `@tinacms/astro` 0.7.0 fails `astro check` with the error above. With this build, `astro check` passes with the generated body passed through and no cast. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 6cd24ce commit 3aa7b89

6 files changed

Lines changed: 62 additions & 16 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@tinacms/astro': patch
3+
---
4+
5+
`<TinaMarkdown>` accepts the rich-text type that `@tinacms/cli` 3 generates. The CLI types rich-text fields as `TinaMarkdownContent` from `tinacms`, and `content` accepted only `TinaRichTextContent`, so passing a field such as `data._body` straight from a query failed `astro check`. Both types describe the same AST, so the rendered output does not change.
6+
7+
To type a component that wraps `<TinaMarkdown>`, use `TinaMarkdownProps['content']` from `@tinacms/astro/types`.

packages/@tinacms/astro/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ Trade-off: a page that uses `<TinaIsland>` carries that one-line inline bootstra
7676
| `@tinacms/astro/TinaMarkdown.astro` | `<TinaMarkdown content components />` — rich-text renderer. Import from this subpath so Astro's check sees a real `.astro` component (the bare-package default resolves through the types condition to a placeholder). |
7777
| `@tinacms/astro/integration` | `tina()` integration — auto-wires the middleware and stages the static `/admin/bridge.js` asset so `requestWithMetadata()` works without you threading `Astro.request` or writing wiring components |
7878
| `@tinacms/astro/TinaIsland.astro` | `<TinaIsland name wrapper params [primary] />` — marker wrapper for an editable region; pass `primary` on the page's main region so the editor opens that form instead of the "Referenced Files" list |
79-
| `@tinacms/astro/types` | `TinaRichTextContent`, `CustomComponentsMap`, `TinaRichTextNode`, `MdxElement`, `TextElement` |
79+
| `@tinacms/astro/types` | `TinaRichTextContent`, `TinaMarkdownProps`, `CustomComponentsMap`, `TinaRichTextNode`, `MdxElement`, `TextElement` |
8080
| `@tinacms/astro/sanitize` | `sanitizeHref` / `sanitizeImageSrc` for CMS-supplied URLs |
8181
| `@tinacms/astro/bridge` | `init`, `refreshForms`, and the rest of `@tinacms/bridge` |
8282
| `@tinacms/astro/tina-field` | `tinaField()` helper |

packages/@tinacms/astro/src/TinaMarkdown.astro

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,14 @@ export { requestWithMetadata } from './data';
1818
export { tinaField } from './tina-field';
1919
2020
import Node from './Node.astro';
21-
import type { CustomComponentsMap, TinaRichTextContent } from './types';
21+
import type { TinaMarkdownProps, TinaRichTextNode } from './types';
2222
23-
interface Props {
24-
content: TinaRichTextContent;
25-
components?: CustomComponentsMap;
26-
}
23+
type Props = TinaMarkdownProps;
2724
2825
const { content, components = {} } = Astro.props;
2926
30-
const nodes = !content
31-
? []
32-
: Array.isArray(content)
33-
? content
34-
: (content.children ?? []);
27+
const nodes = (
28+
!content ? [] : Array.isArray(content) ? content : (content.children ?? [])
29+
) as TinaRichTextNode[];
3530
---
3631
{nodes.map((node) => <Node node={node} components={components} />)}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, expectTypeOf, it } from 'vitest';
2+
import type { TinaMarkdownProps, TinaRichTextContent } from './types';
3+
4+
// `TinaMarkdownContent` from `tinacms/dist/rich-text`, which `@tinacms/cli`
5+
// writes into the generated client types for every rich-text field.
6+
type CliRichText = { type: string; children: CliRichText[] };
7+
8+
describe('TinaMarkdownProps content', () => {
9+
it('accepts an optional rich-text field from the generated client', () => {
10+
expectTypeOf<CliRichText | null | undefined>().toExtend<
11+
TinaMarkdownProps['content']
12+
>();
13+
});
14+
15+
it('accepts the children of a generated rich-text field', () => {
16+
expectTypeOf<CliRichText[]>().toExtend<TinaMarkdownProps['content']>();
17+
});
18+
19+
it('accepts the Astro rich-text types', () => {
20+
expectTypeOf<TinaRichTextContent>().toExtend<
21+
TinaMarkdownProps['content']
22+
>();
23+
});
24+
25+
it('does not accept values that are not rich text', () => {
26+
expectTypeOf<string>().not.toExtend<TinaMarkdownProps['content']>();
27+
});
28+
});

packages/@tinacms/astro/src/index.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* throws with a clear redirect if someone reaches it.
1111
*/
1212
import type { AstroComponentFactory } from 'astro/runtime/server/index.js';
13-
import type { CustomComponentsMap, TinaRichTextContent } from './types';
13+
import type { TinaMarkdownProps } from './types';
1414

1515
export { requestWithMetadata, type QueryResult } from './data';
1616
export { tinaField } from './tina-field';
@@ -19,6 +19,7 @@ export type {
1919
CustomComponentsMap,
2020
MdxElement,
2121
TextElement,
22+
TinaMarkdownProps,
2223
TinaRichTextContent,
2324
TinaRichTextNode,
2425
TinaRichTextRoot,
@@ -37,10 +38,7 @@ export type {
3738
* component AND offer prop completions / type errors at the call site.
3839
*/
3940
type TinaMarkdownComponent = AstroComponentFactory & {
40-
(props: {
41-
content: TinaRichTextContent;
42-
components?: CustomComponentsMap;
43-
}): unknown;
41+
(props: TinaMarkdownProps): unknown;
4442
};
4543

4644
const TinaMarkdownPlaceholder = (() => {

packages/@tinacms/astro/src/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,24 @@ export type TinaRichTextContent =
143143
| null
144144
| undefined;
145145

146+
/**
147+
* The type `@tinacms/cli` generates for a rich-text field. It is the same AST
148+
* as `TinaRichTextContent`, but each `type` is a `string`, not a literal.
149+
* Keep in sync with `TinaMarkdownContent` in `tinacms`.
150+
*/
151+
type GeneratedRichTextContent = {
152+
type: string;
153+
children: GeneratedRichTextContent[];
154+
};
155+
156+
export interface TinaMarkdownProps {
157+
content:
158+
| TinaRichTextContent
159+
| GeneratedRichTextContent
160+
| GeneratedRichTextContent[];
161+
components?: CustomComponentsMap;
162+
}
163+
146164
/**
147165
* An Astro (or framework) component that accepts props `P`. Astro's language
148166
* server types an imported `.astro` component as `(props: Props) => any`, so

0 commit comments

Comments
 (0)