Skip to content

Commit e1ca792

Browse files
Add a script to generate llms.txt (#2004)
1 parent 783a97e commit e1ca792

File tree

3 files changed

+133
-1
lines changed

3 files changed

+133
-1
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,6 @@ out/
2525

2626
# TS build files
2727
tsconfig.tsbuildinfo
28+
29+
# llms.txt
30+
public/llms.txt

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"private": true,
44
"scripts": {
55
"start": "next dev --webpack",
6-
"build": "bun data:update && next build --webpack",
6+
"build": "bun data:update && bun llms:generate && next build --webpack",
77
"preview": "next build --webpack && next start",
88
"lint": "eslint .",
99
"data:update": "bun scripts/build-and-score-data",
@@ -13,6 +13,7 @@
1313
"libraries:recalculate": "bun scripts/recalculate-scores",
1414
"libraries:format": "prettier --experimental-cli --write react-native-libraries.json",
1515
"libraries:check": "bun scripts/check-resources",
16+
"llms:generate": "bun scripts/generate-llms",
1617
"ci:cleanup-blobs": "bun scripts/cleanup-data-blobs",
1718
"ci:validate": "bun scripts/validate-new-entries",
1819
"precommit": "simple-git-hooks && lint-staged"

scripts/generate-llms.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import fs from 'node:fs/promises';
2+
import path from 'node:path';
3+
4+
import { FILTER_COMPATIBILITY, FILTER_PLATFORMS } from '~/components/Filters/helpers';
5+
import { type DataAssetType, type LibraryType } from '~/types';
6+
import { getNewArchSupportStatus, NewArchSupportStatus } from '~/util/newArchStatus';
7+
8+
const OUTPUT_PATH = path.resolve('public', 'llms.txt');
9+
const DATASET_PATH = path.resolve('assets', 'data.json');
10+
const INTRODUCTION = [
11+
'# reactnative.directory',
12+
'Browse the full catalog at https://reactnative.directory for fresh React Native libraries.',
13+
];
14+
15+
const SUPPORTED_FILTERS = [...FILTER_PLATFORMS, ...FILTER_COMPATIBILITY];
16+
const NEW_ARCHITECTURE_STATUS_LABELS: Record<NewArchSupportStatus, string> = {
17+
[NewArchSupportStatus.NewArchOnly]: 'Only Supports New Architecture',
18+
[NewArchSupportStatus.Supported]: 'Supports New Architecture',
19+
[NewArchSupportStatus.Unsupported]: 'Does not support New Architecture',
20+
[NewArchSupportStatus.Untested]: 'Untested with New Architecture',
21+
};
22+
23+
function formatRecord(library: LibraryType, repoUrl: string, rawDescription: string): string {
24+
const displayName = library.npmPkg;
25+
const platforms = getSupportedPlatforms(library);
26+
const supportText = platforms.length ? platforms.join(', ') : 'Not specified';
27+
const header = `[${displayName}](${repoUrl}): ${rawDescription}`;
28+
const newArchStatus = getNewArchSupportStatus(library);
29+
const newArch = formatNewArchitectureStatus(newArchStatus, library.newArchitectureNote);
30+
const downloads = formatDownloads(library);
31+
const website = library.github?.urls.homepage;
32+
const latestVersion = library.npm?.latestRelease;
33+
const latestReleaseDate = library.npm?.latestReleaseDate;
34+
const lines = [`${header}`, `Supports: ${supportText}`, `New Architecture: ${newArch}`];
35+
if (downloads) {
36+
lines.push(`Downloads: ${downloads}`);
37+
}
38+
if (website) {
39+
lines.push(`Website: ${website}`);
40+
}
41+
if (latestVersion) {
42+
lines.push(`Latest Version: ${latestVersion}`);
43+
}
44+
if (latestReleaseDate) {
45+
const date = new Date(latestReleaseDate).toISOString().split('T')[0];
46+
lines.push(`Latest Release Date: ${date}`);
47+
}
48+
return lines.join('\n');
49+
}
50+
51+
function getSupportedPlatforms(library: LibraryType) {
52+
const platforms: string[] = [];
53+
SUPPORTED_FILTERS.forEach(filter => {
54+
const key = filter.param as keyof LibraryType;
55+
const value = library[key];
56+
const title = filter.title;
57+
58+
if (typeof value === 'boolean' && value) {
59+
platforms.push(title);
60+
return;
61+
}
62+
63+
if (typeof value === 'string' && value) {
64+
platforms.push(`${title} (${value})`);
65+
}
66+
});
67+
return platforms;
68+
}
69+
70+
function formatNewArchitectureStatus(status: NewArchSupportStatus, note?: string) {
71+
const value = NEW_ARCHITECTURE_STATUS_LABELS[status];
72+
73+
if (note) {
74+
return `${value} (${note})`;
75+
}
76+
return value;
77+
}
78+
79+
function formatDownloads(library: LibraryType) {
80+
const downloads = library.npm?.downloads;
81+
const weekDownloads = library.npm?.weekDownloads;
82+
83+
if (!downloads && !weekDownloads) {
84+
return null;
85+
}
86+
87+
const parts: string[] = [];
88+
89+
if (typeof downloads === 'number' && downloads > 0) {
90+
parts.push(`${downloads} total`);
91+
}
92+
93+
if (typeof weekDownloads === 'number' && weekDownloads > 0) {
94+
parts.push(`${weekDownloads} last week`);
95+
}
96+
97+
return parts.join(', ');
98+
}
99+
100+
async function generateLlmsFile() {
101+
const assetLibraries = await loadAssetLibraries();
102+
const entries = assetLibraries
103+
.filter(library => !library.template)
104+
.map(library => formatRecord(library, library.githubUrl, library.github?.description ?? ''));
105+
106+
const content = [...INTRODUCTION, ...entries].join('\n\n---\n\n');
107+
108+
await fs.writeFile(OUTPUT_PATH, `${content}\n`, 'utf8');
109+
console.log(
110+
`Generated ${entries.length} entries in ${path.relative(process.cwd(), OUTPUT_PATH)}`
111+
);
112+
}
113+
114+
async function loadAssetLibraries(): Promise<DataAssetType['libraries']> {
115+
try {
116+
const raw = await fs.readFile(DATASET_PATH, 'utf8');
117+
const parsed = JSON.parse(raw) as DataAssetType;
118+
return parsed.libraries ?? [];
119+
} catch (error) {
120+
console.warn(
121+
'Unable to read cached assets/data.json. Continuing without cached descriptions.',
122+
error
123+
);
124+
return [];
125+
}
126+
}
127+
128+
await generateLlmsFile();

0 commit comments

Comments
 (0)