docs: Add llms.txt and llms-full.txt (#4603)
Co-authored-by: Pieczasz <bartekp854@gmail.com>
This commit is contained in:
2
.github/actions/setup-node/action.yml
vendored
2
.github/actions/setup-node/action.yml
vendored
@@ -13,7 +13,7 @@ runs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
|
||||
- name: Cache dependencies
|
||||
id: yarn-cache
|
||||
|
||||
5
.github/workflows/deploy-docs.yml
vendored
5
.github/workflows/deploy-docs.yml
vendored
@@ -31,6 +31,11 @@ jobs:
|
||||
${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}
|
||||
${{ runner.os }}-nextjs-
|
||||
|
||||
- name: Generate llms.txt and llms-full.txt
|
||||
run: |
|
||||
bun run docs:llms
|
||||
bun run docs:llms-full
|
||||
|
||||
- name: Build docs
|
||||
run: |
|
||||
bun --cwd docs build
|
||||
|
||||
5
.github/workflows/test-build-docs.yml
vendored
5
.github/workflows/test-build-docs.yml
vendored
@@ -29,6 +29,11 @@ jobs:
|
||||
${{ runner.os }}-nextjs-${{ hashFiles('**/bun.lockb') }}
|
||||
${{ runner.os }}-nextjs-
|
||||
|
||||
- name: Generate llms.txt and llms-full.txt
|
||||
run: |
|
||||
bun run docs:llms
|
||||
bun run docs:llms-full
|
||||
|
||||
- name: Build docs
|
||||
run: |
|
||||
bun --cwd docs build
|
||||
|
||||
4
docs/.gitignore
vendored
4
docs/.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
node_modules/
|
||||
out/
|
||||
.next/
|
||||
.next/
|
||||
public/llms.txt
|
||||
public/llms-full.txt
|
||||
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
"prepare": "yarn build",
|
||||
"xbasic": "yarn --cwd examples/basic",
|
||||
"docs": "yarn --cwd docs build",
|
||||
"docs:llms": "node scripts/generate-llms.js",
|
||||
"docs:llms-full": "node scripts/generate-llms-full.js",
|
||||
"release": "release-it",
|
||||
"test": "echo no test available",
|
||||
"check-ios": "scripts/swift-format.sh && scripts/swift-lint.sh && scripts/clang-format.sh",
|
||||
|
||||
68
scripts/generate-llms-full.js
Normal file
68
scripts/generate-llms-full.js
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const CWD = process.cwd();
|
||||
const STATIC_DIR = path.join(CWD, 'docs', 'public');
|
||||
|
||||
const LLMS_FILE = path.join(STATIC_DIR, 'llms.txt');
|
||||
const OUTPUT_PATH_STATIC = path.join(STATIC_DIR, 'llms-full.txt');
|
||||
|
||||
function readIfExists(p) {
|
||||
return fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '';
|
||||
}
|
||||
|
||||
function* walk(dir) {
|
||||
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
yield* walk(full);
|
||||
} else if (/\.(md|mdx)$/.test(entry.name)) {
|
||||
yield full;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function gatherDocs() {
|
||||
// Crawl all markdown files in the docs directory (excluding the static subdir)
|
||||
const docsDir = path.join(CWD, 'docs', 'pages');
|
||||
if (!fs.existsSync(docsDir)) {
|
||||
throw new Error(`Docs directory not found! Tried to read ${docsDir}`);
|
||||
}
|
||||
|
||||
return Array.from(walk(docsDir)).sort();
|
||||
}
|
||||
|
||||
function renderFileContent(filepath) {
|
||||
const rel = path.relative(CWD, filepath);
|
||||
const content = readIfExists(filepath);
|
||||
return `\n\n## ${rel}\n\n${content}\n`;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const parts = [];
|
||||
// 1. quick overview
|
||||
parts.push(readIfExists(LLMS_FILE));
|
||||
|
||||
// 2. root README & package README (if present)
|
||||
const rootReadme = path.join(CWD, 'README.md');
|
||||
if (fs.existsSync(rootReadme)) {
|
||||
parts.push('\n\n## README (root)\n\n' + readIfExists(rootReadme));
|
||||
}
|
||||
|
||||
// 3. all docs
|
||||
for (const file of gatherDocs()) {
|
||||
parts.push(renderFileContent(file));
|
||||
}
|
||||
|
||||
const output = parts.join('\n');
|
||||
|
||||
fs.mkdirSync(STATIC_DIR, {recursive: true});
|
||||
// Write to docs/public so it will be available at <site>/llms-full.txt
|
||||
fs.writeFileSync(OUTPUT_PATH_STATIC, output, 'utf8');
|
||||
console.log(
|
||||
`✔︎ wrote llms-full.txt (size: ${output.length.toLocaleString()} chars)`,
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
173
scripts/generate-llms.js
Normal file
173
scripts/generate-llms.js
Normal file
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
/** Convenience utils */
|
||||
const CWD = process.cwd();
|
||||
|
||||
/** Simple helper that returns the first N lines of README.md (sans markdown headings). */
|
||||
function getProjectOverview(maxLines = 30) {
|
||||
const readmePath = path.join(CWD, 'README.md');
|
||||
if (!fs.existsSync(readmePath)) {
|
||||
return '';
|
||||
}
|
||||
const lines = fs.readFileSync(readmePath, 'utf8').split(/\r?\n/);
|
||||
return lines.slice(0, maxLines).join('\n');
|
||||
}
|
||||
|
||||
/** Recursively build a filtered tree representation – depth-limited for brevity. */
|
||||
function buildFileTree(startPath, depth = 0, maxDepth = 2) {
|
||||
const ignore = new Set([
|
||||
'node_modules',
|
||||
'.git',
|
||||
'build',
|
||||
'android',
|
||||
'ios',
|
||||
'.docusaurus',
|
||||
'.next',
|
||||
'.expo',
|
||||
]);
|
||||
|
||||
if (depth > maxDepth) {
|
||||
return [];
|
||||
}
|
||||
const entries = fs.readdirSync(startPath, {withFileTypes: true});
|
||||
const lines = [];
|
||||
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
if (ignore.has(entry.name) || entry.name.startsWith('.')) {
|
||||
continue;
|
||||
}
|
||||
const relPath = `${' '.repeat(depth)}- ${entry.name}${
|
||||
entry.isDirectory() ? '/' : ''
|
||||
}`;
|
||||
lines.push(relPath);
|
||||
if (entry.isDirectory()) {
|
||||
const childLines = buildFileTree(
|
||||
path.join(startPath, entry.name),
|
||||
depth + 1,
|
||||
maxDepth,
|
||||
);
|
||||
lines.push(...childLines);
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** Minimal usage examples that are helpful for agents. */
|
||||
function getUsageExamples() {
|
||||
return "1. Basic playback\n\n ```tsx\n import Video, { VideoRef } from 'react-native-video';\n import { useRef } from 'react';\n \n export default function Example() {\n const videoRef = useRef<VideoRef>(null);\n \n return (\n <Video\n source={{ uri: 'https://example.com/video.mp4' }}\n ref={videoRef}\n style={{ width: '100%', aspectRatio: 16 / 9 }}\n controls\n />\n );\n }\n ```\n\n2. Advanced control (seek & pause)\n\n ```tsx\n // Using ref to control playback\n videoRef.current?.setNativeProps({ paused: true });\n videoRef.current?.seek(10); // seconds\n ```";
|
||||
}
|
||||
|
||||
/** Load docs metadata and build navigation structure */
|
||||
function generateDocsSections() {
|
||||
const docsRoot = path.join(CWD, 'docs', 'pages');
|
||||
if (!fs.existsSync(docsRoot)) {
|
||||
return [];
|
||||
}
|
||||
const baseUrl = 'https://docs.thewidlarzgroup.com/react-native-video';
|
||||
|
||||
const sections = [];
|
||||
const dirEntries = fs.readdirSync(docsRoot, {withFileTypes: true});
|
||||
for (const entry of dirEntries) {
|
||||
const absPath = path.join(docsRoot, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
const title = toTitle(entry.name);
|
||||
const items = collectMarkdownLinks(absPath, `${baseUrl}/${entry.name}`);
|
||||
if (items.length) {
|
||||
sections.push({title, items});
|
||||
}
|
||||
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
||||
// root-level docs
|
||||
const title =
|
||||
getMarkdownTitle(absPath) || toTitle(entry.name.replace(/\.mdx?$/, ''));
|
||||
sections.push({
|
||||
title: 'General',
|
||||
items: [
|
||||
{title, url: `${baseUrl}/${entry.name.replace(/\.mdx?$/, '')}`},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
function toTitle(slug) {
|
||||
return slug
|
||||
.replace(/[-_]/g, ' ')
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase())
|
||||
.trim();
|
||||
}
|
||||
|
||||
function getMarkdownTitle(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const match = content.match(/^#\s+(.+)/m);
|
||||
return match ? match[1].trim() : null;
|
||||
}
|
||||
|
||||
function collectMarkdownLinks(dir, urlPrefix) {
|
||||
const links = [];
|
||||
for (const file of fs.readdirSync(dir)) {
|
||||
if (!file.endsWith('.md') && !file.endsWith('.mdx')) {
|
||||
continue;
|
||||
}
|
||||
if (file.startsWith('_')) {
|
||||
continue; // skip _category_.json etc
|
||||
}
|
||||
const abs = path.join(dir, file);
|
||||
const title = getMarkdownTitle(abs) || toTitle(file.replace(/\.mdx?$/, ''));
|
||||
links.push({
|
||||
title,
|
||||
url: `${urlPrefix}/${file.replace(/\.mdx?$/, '')}`,
|
||||
});
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
function formatDocsSections(sections) {
|
||||
if (!sections.length) {
|
||||
return '';
|
||||
}
|
||||
let md = '\n## Documentation\n';
|
||||
sections.forEach((sec) => {
|
||||
md += `\n### ${sec.title}\n`;
|
||||
sec.items.forEach((it) => {
|
||||
md += `- [${it.title}](${it.url})\n`;
|
||||
});
|
||||
});
|
||||
return md + '\n';
|
||||
}
|
||||
|
||||
function generateContent() {
|
||||
const overview = getProjectOverview();
|
||||
const treeLines = buildFileTree(path.join(CWD), 0, 2).join('\n');
|
||||
const usage = getUsageExamples();
|
||||
const docsSections = formatDocsSections(generateDocsSections());
|
||||
|
||||
return `# react-native-video\n\n> A <Video /> element for React Native - the most battle-tested video player component with support for DRM, offline playback, HLS/DASH streaming, and more.\n\n## Project Overview\n${overview}\n\n## Key Concepts & Terminology\n- **Video**: Main React component for rendering video content\n- **VideoRef**: Imperative API for controlling playback via refs\n- **Source**: Video source configuration (local files, URLs, streams)\n- **DRM**: Built-in support for Widevine & FairPlay DRM\n- **Tracks**: Audio, video, and text track selection and management\n- **Events**: Comprehensive event system for playback state, progress, errors, etc.\n${docsSections}\n## Repository Structure (truncated)\n${treeLines}\n\n## Usage Examples\n${usage}\n\n## Version\nPackage version: ${getPackageVersion()}\n`;
|
||||
}
|
||||
|
||||
function getPackageVersion() {
|
||||
const pkgPath = path.join(CWD, 'package.json');
|
||||
if (!fs.existsSync(pkgPath)) {
|
||||
return 'unknown';
|
||||
}
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||
return pkg.version || 'unknown';
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function writeFileSafe(targetPath, content) {
|
||||
fs.mkdirSync(path.dirname(targetPath), {recursive: true});
|
||||
fs.writeFileSync(targetPath, content, 'utf8');
|
||||
console.log(`✔︎ wrote ${path.relative(CWD, targetPath)}`);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const content = generateContent();
|
||||
writeFileSafe(path.join(CWD, 'docs', 'public', 'llms.txt'), content);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user