Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d8fc774
init generate-llms-txt
siriwatknp Jun 9, 2025
1b98e4a
able to generate components and api
siriwatknp Jun 9, 2025
8b9bfef
wip: simpliify the script
siriwatknp Jun 10, 2025
7070b18
fix table generation
siriwatknp Jun 10, 2025
7094df1
accept project settings from cli
siriwatknp Jun 10, 2025
81838cf
able to process non component markdown
siriwatknp Jun 10, 2025
02a80c6
remove includeApi from option
siriwatknp Jun 10, 2025
b0def28
add comment to the file
siriwatknp Jun 10, 2025
a1bba87
fix htmlToMarkdown to handle proper line break
siriwatknp Jun 10, 2025
b90b45c
generate llms.txt
siriwatknp Jun 10, 2025
b109ced
update comment
siriwatknp Jun 10, 2025
c4062fc
add script to build llms before docs
siriwatknp Jun 10, 2025
7bb07a8
run prettier
siriwatknp Jun 10, 2025
5aed49d
fix lint
siriwatknp Jun 10, 2025
fbae974
fix types
siriwatknp Jun 10, 2025
2e6281f
fix test
siriwatknp Jun 10, 2025
0d42397
concat components api into the main component
siriwatknp Jun 10, 2025
4d1c916
fix first failed test
siriwatknp Jun 10, 2025
844eb25
fix type generation and update script
siriwatknp Jun 10, 2025
f944892
run prettier
siriwatknp Jun 10, 2025
338c2a3
only warn on dev
siriwatknp Jun 11, 2025
e528014
fix(buildLlmsDocs): exclude headers from being captured as descriptions
siriwatknp Jun 13, 2025
7abb41c
cleanup markdown
siriwatknp Jun 13, 2025
bebf7c7
Merge branch 'master' into docs/gen-llms
siriwatknp Jun 13, 2025
112751c
feat(buildLlmsDocs): add configurable domain for absolute URLs in llm…
siriwatknp Jun 16, 2025
76e2e59
Merge branches 'docs/gen-llms' and 'docs/gen-llms' of github.com:siri…
siriwatknp Jun 16, 2025
bbdc9d5
refactor(buildLlmsDocs): move nonComponentFolders config from CLI to …
siriwatknp Jun 16, 2025
9523d2d
revert: restore domain CLI feature for llms.txt generation
siriwatknp Jun 16, 2025
41c9728
Revert "revert: restore domain CLI feature for llms.txt generation"
siriwatknp Jun 17, 2025
929cfd6
remove dot prefix
siriwatknp Jun 17, 2025
7c9e60e
add baseDir to relative links
siriwatknp Jun 17, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
/docs/export
/docs/pages/playground/
/docs/public/feed/
/docs/public/material-ui/
/examples/**/.cache
/packages/mui-codemod/lib
/packages/mui-envinfo/*.tgz
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"release:pack": "tsx scripts/releasePack.mts",
"docs:api": "rimraf --glob ./docs/pages/**/api-docs ./docs/pages/**/api && pnpm docs:api:build",
"docs:api:build": "tsx ./scripts/buidApiDocs/index.ts",
"docs:build": "pnpm --filter docs build",
"docs:llms:build": "rimraf --glob ./docs/public/material-ui/ && tsx ./scripts/buildLlmsDocs/index.ts --projectSettings ./packages/api-docs-builder-core/materialUi/projectSettings.ts",
"docs:build": "pnpm docs:llms:build && pnpm --filter docs build",
"docs:build-sw": "pnpm --filter docs build-sw",
"docs:build-color-preview": "babel-node scripts/buildColorTypes",
"docs:deploy": "pnpm --filter docs run deploy",
Expand Down
3 changes: 3 additions & 0 deletions packages-internal/scripts/generate-llms-txt/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Export all functions from both modules
export * from './processComponent';
export * from './processApi';
331 changes: 331 additions & 0 deletions packages-internal/scripts/generate-llms-txt/src/processApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
import * as fs from 'fs';

interface ApiProp {
type: {
name: string;
description?: string;
};
required?: boolean;
default?: string;
deprecated?: boolean;
deprecationInfo?: string;
signature?: {
type: string;
describedArgs?: string[];
};
additionalInfo?: {
cssApi?: boolean;
sx?: boolean;
};
}

interface ApiSlot {
name: string;
description: string;
default: string;
class: string | null;
}

interface ApiClass {
key: string;
className: string;
description: string;
isGlobal: boolean;
}

interface ApiInheritance {
component: string;
pathname: string;
}

interface ApiJson {
props: Record<string, ApiProp>;
name: string;
imports: string[];
slots?: ApiSlot[];
classes?: ApiClass[];
spread?: boolean;
themeDefaultProps?: boolean;
muiName?: string;
forwardsRefTo?: string | null;
filename?: string;
inheritance?: ApiInheritance;
demos?: string;
cssComponent?: boolean;
deprecated?: boolean;
deprecationInfo?: string;
}

/**
* Convert prop type description from HTML format
*/
function formatPropTypeDescription(html: string): string {
// Decode HTML entities
const result = html
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#124;/g, '|')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
// Replace <br> tags with space to maintain readability
.replace(/<br\s*\/?>/gi, ' ')
// Clean up excessive whitespace
.replace(/\s+/g, ' ')
.trim();

return result;
}

/**
* Convert HTML to markdown
*/
function htmlToMarkdown(html: string): string {
// First pass: decode entities and handle inline elements
let markdown = html
// Decode HTML entities first
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#124;/g, '|')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
// Convert <code> to backticks
.replace(/<code>([^<]+)<\/code>/gi, '`$1`')
// Convert <a> to markdown links
.replace(/<a\s+href="([^"]+)">([^<]+)<\/a>/gi, '[$2]($1)');

// Handle lists - process them as complete units to avoid extra line breaks
markdown = markdown.replace(/<ul[^>]*>(.*?)<\/ul>/gis, (match, listContent: string) => {
// Process each list item
const items = listContent
.split(/<\/li>/)
.map((item) => item.replace(/<li[^>]*>/, '').trim())
.filter((item) => item.length > 0)
.map((item) => `- ${item}`)
.join('\n');

return `\n${items}\n`;
});

// Handle other block elements
markdown = markdown
// Convert <br> to newline
.replace(/<br\s*\/?>/gi, '\n')
// Convert <p> to double newline
.replace(/<p[^>]*>/gi, '\n\n')
.replace(/<\/p>/gi, '')
// Remove any remaining HTML tags
.replace(/<[^>]+>/g, '')
// Clean up excessive whitespace (but preserve intentional line breaks)
.replace(/[ \t]+/g, ' ')
.replace(/ *\n */g, '\n')
.replace(/\n{3,}/g, '\n\n')
.trim();

return markdown;
}

/**
* Format prop type for markdown
*/
function formatPropType(prop: ApiProp): string {
let type = prop.type.name;

if (prop.type.description) {
// Use specialized function for prop type descriptions
type = formatPropTypeDescription(prop.type.description);
}

if (prop.signature) {
type = prop.signature.type;
}

// Escape pipes in union types for better markdown readability
type = type.replace(/\s*\|\s*/g, ' \\| ');

// Wrap all prop types in backticks to prevent markdown table issues with pipes
return `\`${type}\``;
}

/**
* Generate props table
*/
function generatePropsTable(props: Record<string, ApiProp>): string {
const propEntries = Object.entries(props);
if (propEntries.length === 0) {
return '';
}

let table = '## Props\n\n';
table += '| Name | Type | Default | Required | Description |\n';
table += '|------|------|---------|----------|-------------|\n';

for (const [propName, prop] of propEntries) {
const name = prop.deprecated ? `${propName} (deprecated)` : propName;
const type = formatPropType(prop);
const defaultValue = prop.default ? `\`${prop.default}\`` : '-';
const required = prop.required ? 'Yes' : 'No';

let description = '';
if (prop.deprecated && prop.deprecationInfo) {
description = `⚠️ ${htmlToMarkdown(prop.deprecationInfo)}`;
} else if (prop.additionalInfo?.cssApi) {
description = 'Override or extend the styles applied to the component.';
} else if (prop.additionalInfo?.sx) {
description =
'The system prop that allows defining system overrides as well as additional CSS styles.';
}

table += `| ${name} | ${type} | ${defaultValue} | ${required} | ${description} |\n`;
}

return table;
}

/**
* Generate slots table
*/
function generateSlotsTable(slots: ApiSlot[]): string {
if (!slots || slots.length === 0) {
return '';
}

let table = '## Slots\n\n';
table += '| Name | Default | Class | Description |\n';
table += '|------|---------|-------|-------------|\n';

for (const slot of slots) {
const className = slot.class ? `\`.${slot.class}\`` : '-';
const description = htmlToMarkdown(slot.description);
table += `| ${slot.name} | \`${slot.default}\` | ${className} | ${description} |\n`;
}

return table;
}

/**
* Generate classes table
*/
function generateClassesTable(classes: ApiClass[]): string {
if (!classes || classes.length === 0) {
return '';
}

let table = '## CSS\n\n';
table += '### Rule name\n\n';
table += '| Global class | Rule name | Description |\n';
table += '|--------------|-----------|-------------|\n';

for (const cls of classes) {
const globalClass = cls.isGlobal ? `\`.${cls.className}\`` : '-';
const ruleName = cls.isGlobal ? '-' : cls.key;
const description = htmlToMarkdown(cls.description);
table += `| ${globalClass} | ${ruleName} | ${description} |\n`;
}

return table;
}

/**
* Process API JSON and convert to markdown
*/
export function processApiJson(apiJson: ApiJson | string): string {
const api: ApiJson = typeof apiJson === 'string' ? JSON.parse(apiJson) : apiJson;

let markdown = `# ${api.name} API\n\n`;

// Add deprecation warning if applicable
if (api.deprecated) {
const warningText = api.deprecationInfo
? htmlToMarkdown(api.deprecationInfo)
: 'This component is deprecated. Consider using an alternative component.';
markdown += `> ⚠️ **Warning**: ${warningText}\n\n`;
}

// Add demos section
if (api.demos) {
markdown += '## Demos\n\n';
markdown +=
'For examples and details on the usage of this React component, visit the component demo pages:\n\n';
markdown += `${htmlToMarkdown(api.demos)}\n\n`;
}

// Add import section
markdown += '## Import\n\n';
markdown += '```jsx\n';
markdown += api.imports.join('\n// or\n');
markdown += '\n```\n\n';

// Add props section
const propsTable = generatePropsTable(api.props);
if (propsTable) {
markdown += `${propsTable}\n`;
}

// Add ref information
if (api.forwardsRefTo === null) {
markdown += '> **Note**: This component cannot hold a ref.\n\n';
} else {
markdown += `> **Note**: The \`ref\` is forwarded to the root element${api.forwardsRefTo ? ` (${api.forwardsRefTo})` : ''}.\n\n`;
}

// Add spread information
if (api.spread) {
const spreadElement = api.inheritance
? `[${api.inheritance.component}](${api.inheritance.pathname})`
: 'native element';
markdown += `> Any other props supplied will be provided to the root element (${spreadElement}).\n\n`;
}

// Add inheritance section
if (api.inheritance) {
markdown += '## Inheritance\n\n';
markdown += `While not explicitly documented above, the props of the [${api.inheritance.component}](${api.inheritance.pathname}) component are also available on ${api.name}.`;
if (api.inheritance.component === 'Transition') {
markdown +=
' A subset of components support [react-transition-group](https://reactcommunity.org/react-transition-group/transition/) out of the box.';
}
markdown += '\n\n';
}

// Add theme default props section
if (api.themeDefaultProps && api.muiName) {
markdown += '## Theme default props\n\n';
markdown += `You can use \`${api.muiName}\` to change the default props of this component with the theme.\n\n`;
}

// Add slots section
const slotsTable = generateSlotsTable(api.slots || []);
if (slotsTable) {
markdown += `${slotsTable}\n`;
}

// Add classes section
const classesTable = generateClassesTable(api.classes || []);
if (classesTable) {
markdown += `${classesTable}\n`;
}

// Add CSS component note
if (api.cssComponent) {
markdown += `> **Note**: As a CSS utility, the \`${api.name}\` component also supports all system properties. You can use them as props directly on the component.\n\n`;
}

// Add source code section
if (api.filename) {
markdown += '## Source code\n\n';
markdown += `If you did not find the information on this page, consider having a look at the implementation of the component for more detail.\n\n`;
markdown += `- [${api.filename}](https://github.com/mui/material-ui/tree/HEAD${api.filename})\n\n`;
}

return markdown.trim();
}

/**
* Process API JSON file and return markdown
*/
export function processApiFile(filePath: string): string {
const content = fs.readFileSync(filePath, 'utf-8');
return processApiJson(content);
}
Loading