add and enable cem plugin for llms.txt

This commit is contained in:
Cory LaViska
2025-12-19 12:17:00 -05:00
parent 7e5f18ea97
commit 50472f6461
6 changed files with 271 additions and 0 deletions

View File

@@ -114,6 +114,9 @@
"listbox",
"listitem",
"litelement",
"llm",
"llms",
"llmstxt",
"longform",
"lowercasing",
"Lucide",

1
package-lock.json generated
View File

@@ -14040,6 +14040,7 @@
"@wc-toolkit/jsx-types": "^1.3.0",
"eleventy-plugin-git-commit-date": "^0.1.3",
"esbuild": "^0.25.11",
"gray-matter": "^4.0.3",
"npm-check-updates": "^19.1.2"
},
"engines": {

View File

@@ -7,6 +7,7 @@ import fs from 'fs';
import * as path from 'node:path';
import { pascalCase } from 'pascal-case';
import * as url from 'url';
import { llmsTxtPlugin } from './scripts/llms.js';
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
const packageData = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
@@ -188,6 +189,13 @@ export default {
},
}),
// Generate llms.txt
llmsTxtPlugin({
outdir,
docsDir: path.join(__dirname, 'docs'),
baseUrl: 'https://webawesome.com',
}),
//
// TODO - figure out why this broke when events were updated
//

View File

@@ -13,6 +13,7 @@ Components with the <wa-badge variant="warning">Experimental</wa-badge> badge sh
## Next
- Added llms.txt to assist AI agents with using Web Awesome [discuss:1100]
- Fixed a bug in `<wa-combobox>` that prevented the listbox from opening when options were preselected [issue:1883]
## 3.1.0

View File

@@ -92,6 +92,7 @@
"@wc-toolkit/jsx-types": "^1.3.0",
"eleventy-plugin-git-commit-date": "^0.1.3",
"esbuild": "^0.25.11",
"gray-matter": "^4.0.3",
"npm-check-updates": "^19.1.2"
}
}

View File

@@ -0,0 +1,257 @@
import fs from 'fs';
import matter from 'gray-matter';
import path from 'path';
import { fileURLToPath } from 'url';
import { getAllComponents } from './shared.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/** Removes newlines from text to keep llms.txt formatting clean. */
function removeNewlines(str) {
return str ? str.replace(/\n/g, ' ').trim() : '';
}
/** Loads front-matter from all component markdown files. */
function loadAllFrontMatter(components, docsDir) {
const cache = new Map();
for (const component of components) {
const componentName = component.tagName.replace(/^wa-/, '');
const mdPath = path.join(docsDir, 'docs/components', `${componentName}.md`);
if (fs.existsSync(mdPath)) {
try {
const content = fs.readFileSync(mdPath, 'utf-8');
const { data } = matter(content);
cache.set(component.tagName, data);
} catch {
// Skip if parsing fails
}
}
}
return cache;
}
/** Generates the API reference section for a single component. */
function generateComponentApiSection(component, frontMatterCache, baseUrl) {
const lines = [];
const frontMatter = frontMatterCache.get(component.tagName);
const componentSlug = component.tagName.replace(/^wa-/, '');
const description = removeNewlines(frontMatter?.description || component.summary || '');
lines.push(`#### \`<${component.tagName}>\``);
lines.push('');
lines.push(`**Description:** ${description || 'No description available.'}`);
lines.push('');
lines.push(`**Documentation:** ${baseUrl}/docs/components/${componentSlug}`);
lines.push('');
// Slots
if (component.slots?.length > 0) {
lines.push('**Slots:**');
lines.push('');
for (const slot of component.slots) {
const slotName = slot.name || '(default)';
lines.push(`- \`${slotName}\`: ${removeNewlines(slot.description) || 'No description.'}`);
}
lines.push('');
}
// Properties
const properties =
component.members?.filter(m => m.kind === 'field' && m.privacy !== 'private' && m.description) || [];
if (properties.length > 0) {
lines.push('**Properties:**');
lines.push('');
for (const prop of properties) {
// Find corresponding attribute if any
const attr = component.attributes?.find(a => a.fieldName === prop.name);
const attrNote = attr && attr.name !== prop.name ? ` (attribute: \`${attr.name}\`)` : '';
const typeStr = prop.type?.text ? `Type: \`${removeNewlines(prop.type.text)}\`` : '';
const defaultStr = prop.default ? `Default: \`${prop.default}\`` : '';
const meta = [typeStr, defaultStr].filter(Boolean).join(', ');
lines.push(
`- \`${prop.name}\`${attrNote}: ${removeNewlines(prop.description) || 'No description.'}${meta ? ` (${meta})` : ''}`,
);
}
lines.push('');
}
// Methods
const methods = component.members?.filter(m => m.kind === 'method' && m.privacy !== 'private' && m.description) || [];
if (methods.length > 0) {
lines.push('**Methods:**');
lines.push('');
for (const method of methods) {
const params = method.parameters?.length
? `(${method.parameters.map(p => `${p.name}: ${removeNewlines(p.type?.text) || 'unknown'}`).join(', ')})`
: '()';
lines.push(`- \`${method.name}${params}\`: ${removeNewlines(method.description) || 'No description.'}`);
}
lines.push('');
}
// Events
const events = component.events?.filter(e => e.name) || [];
if (events.length > 0) {
lines.push('**Events:**');
lines.push('');
for (const event of events) {
lines.push(`- \`${event.name}\`: ${removeNewlines(event.description) || 'No description.'}`);
}
lines.push('');
}
// CSS Custom Properties
if (component.cssProperties?.length > 0) {
lines.push('**CSS Custom Properties:**');
lines.push('');
for (const prop of component.cssProperties) {
const defaultStr = prop.default ? ` (Default: \`${prop.default}\`)` : '';
lines.push(`- \`${prop.name}\`: ${removeNewlines(prop.description) || 'No description.'}${defaultStr}`);
}
lines.push('');
}
// CSS Parts
if (component.cssParts?.length > 0) {
lines.push('**CSS Parts:**');
lines.push('');
for (const part of component.cssParts) {
lines.push(`- \`${part.name}\`: ${removeNewlines(part.description) || 'No description.'}`);
}
lines.push('');
}
// CSS States
if (component.cssStates?.length > 0) {
lines.push('**CSS States:**');
lines.push('');
for (const state of component.cssStates) {
lines.push(`- \`${state.name}\`: ${removeNewlines(state.description) || 'No description.'}`);
}
lines.push('');
}
return lines;
}
/**
* Generates the complete llms.txt content.
*/
function generateLlmsTxt({ components, packageData, frontMatterCache, baseUrl }) {
const lines = [];
// H1 Title (required by llmstxt.org spec)
lines.push('# Web Awesome');
lines.push('');
// Blockquote summary
lines.push(`> ${packageData.description} Version ${packageData.version}.`);
lines.push('');
// Overview section
lines.push(
`
Web Awesome provides a comprehensive set of customizable, accessible web components for building modern
web applications. All components use shadow DOM and are framework-agnostic, working with vanilla JavaScript
or any framework including React, Vue, Angular, and Svelte.
Form controls are form-associated custom elements that work with native form validation and the
Constraint Validation API.
Font Awesome is the default icon library, so \`<wa-icon name="...">\` values should reference Font Awesome
icon names.
`.trim(),
);
lines.push('');
//
// Documentation
//
lines.push('## Documentation');
lines.push('');
lines.push(`For comprehensive documentation, visit ${baseUrl}/docs/`);
lines.push('');
lines.push(`- [Getting Started](${baseUrl}/docs/getting-started): Installation and setup guide`);
lines.push(`- [Components Overview](${baseUrl}/docs/components): Complete component reference`);
lines.push(`- [Theming](${baseUrl}/docs/theming): Customization and design tokens`);
lines.push(`- [Form Controls](${baseUrl}/docs/form-controls): Form integration and validation`);
lines.push('');
//
// Components
//
lines.push('## Components');
lines.push('');
const sortedComponentsList = [...components].sort((a, b) => a.tagName.localeCompare(b.tagName));
for (const component of sortedComponentsList) {
const frontMatter = frontMatterCache.get(component.tagName);
const description = removeNewlines(frontMatter?.description || component.summary || '');
const componentSlug = component.tagName.replace(/^wa-/, '');
const title = frontMatter?.title || componentSlug;
lines.push(
`- [${title}](${baseUrl}/docs/components/${componentSlug}): ${description || 'No description available.'}`,
);
}
lines.push('');
//
// Optional
//
lines.push('## Optional');
lines.push('');
lines.push(
`The following is a quick reference describing every component's API. For comprehensive documentation, refer to the component documentation using the URLs provided above.`,
);
lines.push('');
// Sort components alphabetically by tag name for the API reference
const sortedComponents = [...components].sort((a, b) => a.tagName.localeCompare(b.tagName));
for (const component of sortedComponents) {
lines.push(...generateComponentApiSection(component, frontMatterCache, baseUrl));
}
return lines.join('\n').trim();
}
/**
* A CEM plugin that generates an llms.txt file following the llmstxt.org specification.
*/
export function llmsTxtPlugin(options = {}) {
const {
outdir = 'dist-cdn',
docsDir = path.resolve(__dirname, '../docs'),
baseUrl = 'https://webawesome.com',
} = options;
return {
name: 'wa-llms-txt',
packageLinkPhase({ customElementsManifest }) {
const components = getAllComponents(customElementsManifest);
const packageData = customElementsManifest.package || {};
const frontMatterCache = loadAllFrontMatter(components, docsDir);
const llmsTxt = generateLlmsTxt({
components,
packageData,
frontMatterCache,
baseUrl,
});
// Write to the output directory
const outputPath = path.join(outdir, 'llms.txt');
fs.writeFileSync(outputPath, llmsTxt, 'utf-8');
},
};
}
export default llmsTxtPlugin;