Skip to content
Open
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
13 changes: 6 additions & 7 deletions apps/www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@
"@types/mdast": "^4.0.4",
"class-variance-authority": "^0.7.1",
"dayjs": "^1.11.20",
"fumadocs-core": "16.0.7",
"fumadocs-docgen": "^1.3.8",
"fumadocs-mdx": "13.0.5",
"fumadocs-typescript": "^4.0.6",
"fumadocs-ui": "16.0.7",
"fumadocs-core": "16.17.9",
"fumadocs-mdx": "15.4.10",
"fumadocs-typescript": "5.6.0",
"fumadocs-ui": "16.17.9",
"lucide-react": "^1.45.0",
"next": "16.0.7",
"next": "16.4.3",
"prettier": "^2.8.8",
"react": "^19.2.1",
"react-dom": "^19.2.1",
Expand All @@ -33,7 +32,7 @@
"remark-gfm": "^4.0.1",
"remark-mdx": "^3.1.0",
"unist-util-visit": "^5.0.0",
"zod": "^4.1.12"
"zod": "^4.6.7"
},
"devDependencies": {
"@raystack/tools-config": "workspace:*",
Expand Down
22 changes: 3 additions & 19 deletions apps/www/source.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as path from 'node:path';
import {
rehypeToc,
remarkGfm,
Expand All @@ -13,25 +12,10 @@ import {
frontmatterSchema,
metaSchema
} from 'fumadocs-mdx/config';
import {
createGenerator,
GeneratorOptions,
remarkAutoTypeTable
} from 'fumadocs-typescript';
import { fileURLToPath } from 'url';
import { remarkAutoTypeTable } from 'fumadocs-typescript';
import { generator, transform } from '@/lib/type-table';
import { SourceSchema, TagSchema } from '@/lib/types';

const relative = (s: string): string =>
path.resolve(fileURLToPath(new URL(s, import.meta.url)));

const tsconfig: GeneratorOptions = {
tsconfigPath: relative('../tsconfig.json'),
basePath: relative('../'),
cache: false
};

const generator = createGenerator(tsconfig);

export const docs = defineDocs({
dir: 'src/content/docs',
docs: {
Expand Down Expand Up @@ -59,7 +43,7 @@ export default defineConfig({
remarkNpm,
remarkStructure,
// remarkInstall,
[remarkAutoTypeTable, { generator }]
[remarkAutoTypeTable, { generator, options: { transform } }]
];
},
rehypePlugins: () => [rehypeToc]
Expand Down
6 changes: 5 additions & 1 deletion apps/www/src/components/mdx/code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import { usePreContext } from './pre-context';
export const Code = ({ children, className }: HTMLAttributes<HTMLElement>) => {
const { hasPreParent } = usePreContext();

const code = String(children).trim();
// Already highlighted, for example a type in a props table.
if (typeof children !== 'string')
return <code className={className}>{children}</code>;

const code = children.trim();
if (!hasPreParent)
return <code className={styles['code-inline']}>{code}</code>;

Expand Down
17 changes: 11 additions & 6 deletions apps/www/src/components/typetable/typetable.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,18 @@
border-top: none;
position: relative;
}
.fieldCode {
background: transparent;
padding: 0;
.code {
font-family: var(--rs-font-mono);
font-size: var(--rs-font-size-mono-small);
line-height: var(--rs-line-height-mono-small);
letter-spacing: var(--rs-letter-spacing-mono-small);
overflow-wrap: anywhere;
}
.fieldCode :global(.token-line) > span {
display: flex;
flex-wrap: wrap;
.code :global(.shiki) span {
color: var(--shiki-light);
}
:global([data-theme="dark"]) .code :global(.shiki) span {
color: var(--shiki-dark);
}

.fieldLabel {
Expand Down
35 changes: 9 additions & 26 deletions apps/www/src/components/typetable/typetable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { Accordion, CodeBlock, Flex, Text } from '@raystack/apsara';
import { Accordion, Flex, Text } from '@raystack/apsara';
import { cx } from 'class-variance-authority';
import Link from 'fumadocs-core/link';
import { type ReactNode } from 'react';
Expand Down Expand Up @@ -92,17 +92,13 @@ function Item({
{name}
{required ? <span className={styles.required}>*</span> : ''}
</code>
<span className={styles.fieldValue}>
<span className={cx(styles.code, styles.fieldValue)}>
{typeDescriptionLink ? (
<Link href={typeDescriptionLink} className={styles.typeLink}>
{type}
</Link>
) : (
<CodeBlock hideLineNumbers>
<CodeBlock.Code language='tsx' className={styles.fieldCode}>
{String(type)}
</CodeBlock.Code>
</CodeBlock>
type
)}
</span>
</Accordion.Trigger>
Expand All @@ -128,31 +124,18 @@ function Item({
<Text size='small' className={styles.fieldLabel}>
Type
</Text>
<CodeBlock hideLineNumbers>
<CodeBlock.Code
language='tsx'
className={cx(styles.fieldCode, styles.fieldValue)}
>
{String(type) +
(!required && !String(type).includes('undefined')
? ' | undefined'
: '')}
</CodeBlock.Code>
</CodeBlock>
<span className={cx(styles.code, styles.fieldValue)}>
{typeDescription ?? type}
</span>
</Flex>
{defaultValue && (
<Flex gap={5}>
<Text size='small' className={styles.fieldLabel}>
Default
</Text>
<CodeBlock hideLineNumbers>
<CodeBlock.Code
language='tsx'
className={cx(styles.fieldCode, styles.fieldValue)}
>
{String(defaultValue)}
</CodeBlock.Code>
</CodeBlock>
<span className={cx(styles.code, styles.fieldValue)}>
{defaultValue}
</span>
</Flex>
)}
{parameters.length > 0 && (
Expand Down
3 changes: 2 additions & 1 deletion apps/www/src/lib/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import remarkGfm from 'remark-gfm';
import remarkMdx from 'remark-mdx';
import { docs } from '@/lib/source';
import { remarkTypeTableToMd } from './remark';
import { generator, transform } from './type-table';

const processor = remark()
.use(remarkMdx)
.use(remarkGfm)
.use(remarkAutoTypeTable)
.use(remarkAutoTypeTable, { generator, options: { transform } })
.use(remarkTypeTableToMd);

const REGEX = {
Expand Down
88 changes: 19 additions & 69 deletions apps/www/src/lib/remark.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
import type { Parent, Root, Table, TableRow, Text } from 'mdast';
import { type GeneratedDoc, typeTableToMarkdown } from 'fumadocs-typescript';
import type { Parent, Root } from 'mdast';
import { visit } from 'unist-util-visit';

interface TableEntry {
name: string;
type: string;
tags?: { defaultValue?: string };
required?: boolean;
description?: string;
}

interface TypeObj {
name: string;
entries: TableEntry[];
}

interface MdxJsxAttributeLike {
type: 'mdxJsxAttribute';
name: string;
Expand All @@ -27,6 +15,7 @@ interface MdxJsxFlowElementLike {
>;
}

/** Replaces each generated `TypeTable` with its props table in Markdown. */
export function remarkTypeTableToMd() {
return (tree: Root) => {
visit(
Expand All @@ -44,61 +33,22 @@ export function remarkTypeTableToMd() {
(attr): attr is MdxJsxAttributeLike =>
attr.type === 'mdxJsxAttribute' && attr.name === 'type'
);
if (
!typeProp ||
!typeProp.value ||
typeof typeProp.value === 'string' ||
typeof typeProp.value.value !== 'string'
)
return;

let typeObj: TypeObj;
try {
let expr = typeProp.value.value.trim();
if (expr.startsWith('{') && expr.endsWith('}')) {
expr = '(' + expr + ')';
}
// eslint-disable-next-line no-eval
typeObj = eval(expr);
} catch {
return;
}

const headers = ['Name', 'Type', 'Default', 'Required', 'Description'];
const rows = (typeObj.entries || []).map((entry: TableEntry) => [
entry.name,
entry.type || '',
entry.tags?.defaultValue || '',
entry.required ? 'Yes' : 'No',
entry.description || ''
]);

// Build MDAST table node with correct types
const tableNode: Table = {
type: 'table',
align: [],
children: [
{
type: 'tableRow',
children: headers.map(header => ({
type: 'tableCell',
children: [{ type: 'text', value: header } as Text]
}))
} as TableRow,
...rows.map(
row =>
({
type: 'tableRow',
children: row.map(cell => ({
type: 'tableCell',
children: [{ type: 'text', value: cell } as Text]
}))
}) as TableRow
)
]
};

parent.children.splice(index, 1, tableNode);
const value = typeProp?.value;
if (!value || typeof value === 'string' || !value.value) return;

const doc = JSON.parse(value.value) as GeneratedDoc;
// `typeTableToMarkdown` prints `simplifiedType`, which is the short
// label such as `function`. The LLM text gets the full type.
const entries = doc.entries.map(entry => ({
...entry,
simplifiedType: entry.type
.replace(/\s+/g, ' ')
.replace(/ \| undefined$/, '')
}));
parent.children.splice(index, 1, {
type: 'html',
value: typeTableToMarkdown({ ...doc, entries })
});
}
);
};
Expand Down
59 changes: 59 additions & 0 deletions apps/www/src/lib/type-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as path from 'node:path';
import { createGenerator, type GenerateOptions } from 'fumadocs-typescript';

type Transform = NonNullable<GenerateOptions['transform']>;

export const generator = createGenerator({
tsconfigPath: path.resolve('tsconfig.props.json')
});

const shortTypes: Record<string, string> = {
className: 'string | function',
style: 'CSSProperties | function',
render: 'ReactElement | function'
};

/** A type name, or a union of names and literals such as `"solid" | "outline"`. */
const SHORT =
/^(?:[\w.]+(?:\[\])?|"[^"]*"|-?\d+(?:\.\d+)?)(?: \| (?:[\w.]+(?:\[\])?|"[^"]*"|-?\d+(?:\.\d+)?))*$/;

const withoutUndefined = (type: string) => type.replace(/ \| undefined$/, '');

/** The type as written in the source, which keeps the order of a union. */
function writtenType(node: unknown): string | undefined {
const type = (node as { type?: { getText(): string } } | undefined)?.type;
const text = type
?.getText()
.replace(/\s+/g, ' ')
.replace(/^\| /, '')
.replace(/'([^']*)'/g, '"$1"');
return text && withoutUndefined(text);
}

/** Sets the short type of a table row. The expanded row shows the full type. */
export const transform: Transform = function (entry, _type, prop) {
// A `@remarks` type set by the author wins.
if (entry.tags.some(tag => tag.name === 'remarks' && tag.text[0] === '`'))
return;

const written = writtenType(prop.declarations?.[0]?.resolve(this.program));
const type = (written ?? withoutUndefined(entry.type)).replace(
/\bReact\./g,
''
);
if (written && SHORT.test(type))
entry.type = entry.required ? type : `${type} | undefined`;

if (
SHORT.test(type) ||
entry.name === 'children' ||
entry.name === 'ref' ||
entry.name.endsWith('Ref')
)
entry.simplifiedType = type;
else if (shortTypes[entry.name])
entry.simplifiedType = shortTypes[entry.name];
else if (/^(on|get)[A-Z]/.test(entry.name)) entry.simplifiedType = 'function';
else if (/ & |<.*\b(object|union)\b/.test(entry.simplifiedType))
entry.simplifiedType = 'object';
};
2 changes: 1 addition & 1 deletion apps/www/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"incremental": true,
"paths": {
"react": ["node_modules/@types/react"],
"@/.source": ["./.source/index.ts"],
"@/.source": ["./.source/server.ts"],
"@/*": ["./src/*"]
},
"plugins": [{ "name": "next" }]
Expand Down
6 changes: 6 additions & 0 deletions apps/www/tsconfig.props.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["react"]
}
}
Loading
Loading