@clidey/dory 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/k8s/Dockerfile +7 -11
- package/k8s/docs.Dockerfile +15 -0
- package/package.json +5 -2
- package/scripts/generate-llm-txt.js +115 -0
- package/src/plugins/llm-txt-dev-server.ts +183 -0
- package/src/plugins/llm-txt-generator.ts +186 -0
- package/vite.config.ts +4 -0
package/README.md
CHANGED
|
@@ -150,6 +150,16 @@ This will create a `dist` directory containing the fully built static site, read
|
|
|
150
150
|
|
|
151
151
|
## 🐳 Docker Deployment (Optional)
|
|
152
152
|
|
|
153
|
+
You can use our official Docker image `clidey/dory` to create a static server. Simply create a Dockerfile inside the project and build the following Dockerfile:
|
|
154
|
+
|
|
155
|
+
```dockerfile
|
|
156
|
+
FROM clidey/dory
|
|
157
|
+
WORKDIR /app
|
|
158
|
+
COPY . .
|
|
159
|
+
RUN dory build
|
|
160
|
+
CMD ["dory", "preview"]
|
|
161
|
+
```
|
|
162
|
+
|
|
153
163
|
You can also build and run the static site inside a Docker container using Nginx:
|
|
154
164
|
|
|
155
165
|
### Build Docker Image
|
package/k8s/Dockerfile
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
FROM node:
|
|
1
|
+
FROM node:22.16-alpine
|
|
2
2
|
|
|
3
|
-
RUN corepack enable && corepack prepare pnpm@10.10.0 --activate
|
|
4
3
|
WORKDIR /app
|
|
5
|
-
COPY package.json pnpm-lock.yaml ./
|
|
6
|
-
RUN pnpm install --frozen-lockfile
|
|
7
|
-
COPY . .
|
|
8
|
-
RUN pnpm build
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
COPY --from=builder /app/dist /usr/share/nginx/html
|
|
12
|
-
COPY --from=builder /app/k8s/config/nginx-default.conf /etc/nginx/conf.d/default.conf
|
|
13
|
-
EXPOSE 80
|
|
5
|
+
COPY ./k8s/config/nginx-default.conf /app/k8s/config/nginx-default.conf
|
|
14
6
|
|
|
15
|
-
|
|
7
|
+
# Install dory globally
|
|
8
|
+
RUN npm install -g @clidey/dory
|
|
9
|
+
EXPOSE 3000
|
|
10
|
+
|
|
11
|
+
CMD ["dory", "preview"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
FROM node:20-alpine AS builder
|
|
2
|
+
|
|
3
|
+
RUN corepack enable && corepack prepare pnpm@10.10.0 --activate
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
COPY package.json pnpm-lock.yaml ./
|
|
6
|
+
RUN pnpm install --frozen-lockfile
|
|
7
|
+
COPY . .
|
|
8
|
+
RUN pnpm build
|
|
9
|
+
|
|
10
|
+
FROM nginx:alpine
|
|
11
|
+
COPY --from=builder /app/dist /usr/share/nginx/html
|
|
12
|
+
COPY --from=builder /app/k8s/config/nginx-default.conf /etc/nginx/conf.d/default.conf
|
|
13
|
+
EXPOSE 80
|
|
14
|
+
|
|
15
|
+
CMD ["nginx", "-g", "daemon off;"]
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clidey/dory",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A CLI tool for building and previewing documentation sites",
|
|
7
7
|
"keywords": [
|
|
@@ -66,5 +66,8 @@
|
|
|
66
66
|
"vite-plugin-mdx": "^3.6.1",
|
|
67
67
|
"wouter-preact": "^3.7.1"
|
|
68
68
|
},
|
|
69
|
-
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
|
69
|
+
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39",
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@types/node": "^24.0.12"
|
|
72
|
+
}
|
|
70
73
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
// Function to parse frontmatter from MDX content
|
|
11
|
+
function parseFrontmatter(content) {
|
|
12
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
|
|
13
|
+
if (!frontmatterMatch) return { data: {}, content };
|
|
14
|
+
|
|
15
|
+
const frontmatter = frontmatterMatch[1];
|
|
16
|
+
const data = {};
|
|
17
|
+
|
|
18
|
+
frontmatter.split('\n').forEach(line => {
|
|
19
|
+
const [key, ...valueParts] = line.split(':');
|
|
20
|
+
if (key && valueParts.length) {
|
|
21
|
+
const value = valueParts.join(':').trim();
|
|
22
|
+
data[key.trim()] = value.replace(/^["']|["']$/g, '');
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return { data, content: content.slice(frontmatterMatch[0].length) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Function to recursively find all MDX files
|
|
30
|
+
function findMdxFiles(dir) {
|
|
31
|
+
const files = [];
|
|
32
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
33
|
+
|
|
34
|
+
for (const item of items) {
|
|
35
|
+
const fullPath = path.join(dir, item.name);
|
|
36
|
+
if (item.isDirectory()) {
|
|
37
|
+
files.push(...findMdxFiles(fullPath));
|
|
38
|
+
} else if (item.isFile() && item.name.endsWith('.mdx')) {
|
|
39
|
+
files.push(fullPath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return files;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Function to clean MDX content for LLM consumption
|
|
47
|
+
function cleanMdxContent(content) {
|
|
48
|
+
// Remove JSX components but keep their content
|
|
49
|
+
let cleaned = content
|
|
50
|
+
// Remove JSX opening/closing tags but keep content
|
|
51
|
+
.replace(/<(\w+)([^>]*?)>/g, '')
|
|
52
|
+
.replace(/<\/\w+>/g, '')
|
|
53
|
+
// Remove code block language specifiers
|
|
54
|
+
.replace(/```(\w+)/g, '```')
|
|
55
|
+
// Remove HTML comments
|
|
56
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
57
|
+
// Remove excessive whitespace
|
|
58
|
+
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
|
59
|
+
// Remove leading/trailing whitespace
|
|
60
|
+
.trim();
|
|
61
|
+
|
|
62
|
+
return cleaned;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Main function to generate LLM text
|
|
66
|
+
async function generateLlmTxt() {
|
|
67
|
+
const docsDir = path.join(__dirname, '..', 'docs');
|
|
68
|
+
const outputDir = path.join(__dirname, '..', 'dist');
|
|
69
|
+
|
|
70
|
+
// Ensure output directory exists
|
|
71
|
+
if (!fs.existsSync(outputDir)) {
|
|
72
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const mdxFiles = findMdxFiles(docsDir);
|
|
76
|
+
let aggregatedContent = '';
|
|
77
|
+
|
|
78
|
+
// Sort files for consistent output
|
|
79
|
+
mdxFiles.sort();
|
|
80
|
+
|
|
81
|
+
for (const filePath of mdxFiles) {
|
|
82
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
83
|
+
const { data, content } = parseFrontmatter(rawContent);
|
|
84
|
+
const cleanedContent = cleanMdxContent(content);
|
|
85
|
+
|
|
86
|
+
// Add file header
|
|
87
|
+
const relativePath = path.relative(docsDir, filePath);
|
|
88
|
+
aggregatedContent += `\n# ${relativePath}\n`;
|
|
89
|
+
|
|
90
|
+
// Add title if available
|
|
91
|
+
if (data.title) {
|
|
92
|
+
aggregatedContent += `## ${data.title}\n\n`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Add description if available
|
|
96
|
+
if (data.description) {
|
|
97
|
+
aggregatedContent += `${data.description}\n\n`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Add cleaned content
|
|
101
|
+
aggregatedContent += cleanedContent + '\n\n';
|
|
102
|
+
aggregatedContent += '---\n\n';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Write to output file
|
|
106
|
+
const outputPath = path.join(outputDir, 'llm.txt');
|
|
107
|
+
fs.writeFileSync(outputPath, aggregatedContent.trim());
|
|
108
|
+
|
|
109
|
+
console.log(`✅ Generated LLM text file: ${outputPath}`);
|
|
110
|
+
console.log(`📄 Processed ${mdxFiles.length} MDX files`);
|
|
111
|
+
console.log(`📝 Total content size: ${aggregatedContent.length} characters`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Run the script
|
|
115
|
+
generateLlmTxt().catch(console.error);
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
// Read dory.json to get navigation order (flat list, in order)
|
|
6
|
+
function getNavigationOrder(docsDir: string): string[] {
|
|
7
|
+
const doryJsonPath = path.join(docsDir, 'dory.json');
|
|
8
|
+
if (!fs.existsSync(doryJsonPath)) return [];
|
|
9
|
+
const doryJson = JSON.parse(fs.readFileSync(doryJsonPath, 'utf-8'));
|
|
10
|
+
const order: string[] = [];
|
|
11
|
+
|
|
12
|
+
function walkPages(pages: any[]) {
|
|
13
|
+
for (const page of pages) {
|
|
14
|
+
if (typeof page === 'string') {
|
|
15
|
+
order.push(page);
|
|
16
|
+
} else if (typeof page === 'object' && page.pages) {
|
|
17
|
+
walkPages(page.pages);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (doryJson.navigation && Array.isArray(doryJson.navigation.tabs)) {
|
|
23
|
+
for (const tab of doryJson.navigation.tabs) {
|
|
24
|
+
if (tab.groups && Array.isArray(tab.groups)) {
|
|
25
|
+
for (const group of tab.groups) {
|
|
26
|
+
if (group.pages && Array.isArray(group.pages)) {
|
|
27
|
+
walkPages(group.pages);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return order;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Function to parse frontmatter from MDX content
|
|
37
|
+
function parseFrontmatter(content: string) {
|
|
38
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
|
|
39
|
+
if (!frontmatterMatch) return { data: {}, content };
|
|
40
|
+
|
|
41
|
+
const frontmatter = frontmatterMatch[1];
|
|
42
|
+
const data: Record<string, string> = {};
|
|
43
|
+
|
|
44
|
+
frontmatter.split('\n').forEach(line => {
|
|
45
|
+
const [key, ...valueParts] = line.split(':');
|
|
46
|
+
if (key && valueParts.length) {
|
|
47
|
+
const value = valueParts.join(':').trim();
|
|
48
|
+
data[key.trim()] = value.replace(/^["']|["']$/g, '');
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return { data, content: content.slice(frontmatterMatch[0].length) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Function to recursively find all MDX files and map by relative path (without .mdx)
|
|
56
|
+
function findMdxFilesMap(dir: string, baseDir: string): Record<string, string> {
|
|
57
|
+
const files: Record<string, string> = {};
|
|
58
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
59
|
+
|
|
60
|
+
for (const item of items) {
|
|
61
|
+
const fullPath = path.join(dir, item.name);
|
|
62
|
+
if (item.isDirectory()) {
|
|
63
|
+
Object.assign(files, findMdxFilesMap(fullPath, baseDir));
|
|
64
|
+
} else if (item.isFile() && item.name.endsWith('.mdx')) {
|
|
65
|
+
// Key: relative path without .mdx, with forward slashes
|
|
66
|
+
let rel = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
67
|
+
if (rel.endsWith('.mdx')) rel = rel.slice(0, -4);
|
|
68
|
+
files[rel] = fullPath;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return files;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Function to clean MDX content for LLM consumption
|
|
76
|
+
function cleanMdxContent(content: string): string {
|
|
77
|
+
// Remove JSX components but keep their content
|
|
78
|
+
let cleaned = content
|
|
79
|
+
// Remove JSX opening/closing tags but keep content
|
|
80
|
+
.replace(/<(\w+)([^>]*?)>/g, '')
|
|
81
|
+
.replace(/<\/\w+>/g, '')
|
|
82
|
+
// Remove code block language specifiers
|
|
83
|
+
.replace(/```(\w+)/g, '```')
|
|
84
|
+
// Remove HTML comments
|
|
85
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
86
|
+
// Remove excessive whitespace
|
|
87
|
+
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
|
88
|
+
// Remove leading/trailing whitespace
|
|
89
|
+
.trim();
|
|
90
|
+
|
|
91
|
+
return cleaned;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Function to generate LLM text content in dory.json order, then append any extra files
|
|
95
|
+
function generateLlmContent(docsDir: string): string {
|
|
96
|
+
const navOrder = getNavigationOrder(docsDir);
|
|
97
|
+
const mdxFilesMap = findMdxFilesMap(docsDir, docsDir);
|
|
98
|
+
const usedKeys = new Set<string>();
|
|
99
|
+
let aggregatedContent = '';
|
|
100
|
+
|
|
101
|
+
// Output files in navigation order, in order as in dory.json
|
|
102
|
+
for (const navKey of navOrder) {
|
|
103
|
+
// Try both with and without "index"
|
|
104
|
+
let fileKey = navKey;
|
|
105
|
+
if (mdxFilesMap[fileKey]) {
|
|
106
|
+
usedKeys.add(fileKey);
|
|
107
|
+
} else if (mdxFilesMap[`${fileKey}/index`]) {
|
|
108
|
+
fileKey = `${fileKey}/index`;
|
|
109
|
+
usedKeys.add(fileKey);
|
|
110
|
+
} else {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const filePath = mdxFilesMap[fileKey];
|
|
114
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
115
|
+
const { data, content } = parseFrontmatter(rawContent);
|
|
116
|
+
const cleanedContent = cleanMdxContent(content);
|
|
117
|
+
|
|
118
|
+
// Add file header
|
|
119
|
+
aggregatedContent += `\n# ${fileKey}.mdx\n`;
|
|
120
|
+
|
|
121
|
+
// Add title if available
|
|
122
|
+
if (data.title) {
|
|
123
|
+
aggregatedContent += `## ${data.title}\n\n`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Add description if available
|
|
127
|
+
if (data.description) {
|
|
128
|
+
aggregatedContent += `${data.description}\n\n`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Add cleaned content
|
|
132
|
+
aggregatedContent += cleanedContent + '\n\n';
|
|
133
|
+
aggregatedContent += '---\n\n';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Output any extra files not in navigation order, sorted
|
|
137
|
+
const extraKeys = Object.keys(mdxFilesMap).filter(k => !usedKeys.has(k)).sort();
|
|
138
|
+
for (const fileKey of extraKeys) {
|
|
139
|
+
const filePath = mdxFilesMap[fileKey];
|
|
140
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
141
|
+
const { data, content } = parseFrontmatter(rawContent);
|
|
142
|
+
const cleanedContent = cleanMdxContent(content);
|
|
143
|
+
|
|
144
|
+
aggregatedContent += `\n# ${fileKey}.mdx\n`;
|
|
145
|
+
if (data.title) {
|
|
146
|
+
aggregatedContent += `## ${data.title}\n\n`;
|
|
147
|
+
}
|
|
148
|
+
if (data.description) {
|
|
149
|
+
aggregatedContent += `${data.description}\n\n`;
|
|
150
|
+
}
|
|
151
|
+
aggregatedContent += cleanedContent + '\n\n';
|
|
152
|
+
aggregatedContent += '---\n\n';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Remove leading newlines for the very first file, so the first file is at the very top
|
|
156
|
+
return aggregatedContent.replace(/^\n+/, '').trim();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function llmTxtDevServer(): Plugin {
|
|
160
|
+
return {
|
|
161
|
+
name: 'llm-txt-dev-server',
|
|
162
|
+
configureServer(server) {
|
|
163
|
+
server.middlewares.use('/llm.txt', (req, res, next) => {
|
|
164
|
+
if (req.method !== 'GET') {
|
|
165
|
+
return next();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const docsDir = path.join(process.cwd(), 'docs');
|
|
170
|
+
const llmContent = generateLlmContent(docsDir);
|
|
171
|
+
|
|
172
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
173
|
+
res.setHeader('Content-Length', Buffer.byteLength(llmContent));
|
|
174
|
+
res.end(llmContent);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error('Error generating LLM text:', error);
|
|
177
|
+
res.statusCode = 500;
|
|
178
|
+
res.end('Internal Server Error');
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
interface LlmTxtGeneratorOptions {
|
|
6
|
+
docsDir?: string;
|
|
7
|
+
outputPath?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Read dory.json to get navigation order (flat list, in order)
|
|
11
|
+
function getNavigationOrder(docsDir: string): string[] {
|
|
12
|
+
const doryJsonPath = path.join(docsDir, 'dory.json');
|
|
13
|
+
if (!fs.existsSync(doryJsonPath)) return [];
|
|
14
|
+
const doryJson = JSON.parse(fs.readFileSync(doryJsonPath, 'utf-8'));
|
|
15
|
+
const order: string[] = [];
|
|
16
|
+
|
|
17
|
+
function walkPages(pages: any[]) {
|
|
18
|
+
for (const page of pages) {
|
|
19
|
+
if (typeof page === 'string') {
|
|
20
|
+
order.push(page);
|
|
21
|
+
} else if (typeof page === 'object' && page.pages) {
|
|
22
|
+
walkPages(page.pages);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (doryJson.navigation && Array.isArray(doryJson.navigation.tabs)) {
|
|
28
|
+
for (const tab of doryJson.navigation.tabs) {
|
|
29
|
+
if (tab.groups && Array.isArray(tab.groups)) {
|
|
30
|
+
for (const group of tab.groups) {
|
|
31
|
+
if (group.pages && Array.isArray(group.pages)) {
|
|
32
|
+
walkPages(group.pages);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return order;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Function to parse frontmatter from MDX content
|
|
42
|
+
function parseFrontmatter(content: string) {
|
|
43
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
|
|
44
|
+
if (!frontmatterMatch) return { data: {}, content };
|
|
45
|
+
|
|
46
|
+
const frontmatter = frontmatterMatch[1];
|
|
47
|
+
const data: Record<string, string> = {};
|
|
48
|
+
|
|
49
|
+
frontmatter.split('\n').forEach(line => {
|
|
50
|
+
const [key, ...valueParts] = line.split(':');
|
|
51
|
+
if (key && valueParts.length) {
|
|
52
|
+
const value = valueParts.join(':').trim();
|
|
53
|
+
data[key.trim()] = value.replace(/^["']|["']$/g, '');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return { data, content: content.slice(frontmatterMatch[0].length) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Function to recursively find all MDX files and map by relative path (without .mdx)
|
|
61
|
+
function findMdxFilesMap(dir: string, baseDir: string): Record<string, string> {
|
|
62
|
+
const files: Record<string, string> = {};
|
|
63
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
64
|
+
|
|
65
|
+
for (const item of items) {
|
|
66
|
+
const fullPath = path.join(dir, item.name);
|
|
67
|
+
if (item.isDirectory()) {
|
|
68
|
+
Object.assign(files, findMdxFilesMap(fullPath, baseDir));
|
|
69
|
+
} else if (item.isFile() && item.name.endsWith('.mdx')) {
|
|
70
|
+
// Key: relative path without .mdx, with forward slashes
|
|
71
|
+
let rel = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
72
|
+
if (rel.endsWith('.mdx')) rel = rel.slice(0, -4);
|
|
73
|
+
files[rel] = fullPath;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return files;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Function to clean MDX content for LLM consumption
|
|
81
|
+
function cleanMdxContent(content: string): string {
|
|
82
|
+
// Remove JSX components but keep their content
|
|
83
|
+
let cleaned = content
|
|
84
|
+
// Remove JSX opening/closing tags but keep content
|
|
85
|
+
.replace(/<(\w+)([^>]*?)>/g, '')
|
|
86
|
+
.replace(/<\/\w+>/g, '')
|
|
87
|
+
// Remove code block language specifiers
|
|
88
|
+
.replace(/```(\w+)/g, '```')
|
|
89
|
+
// Remove HTML comments
|
|
90
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
91
|
+
// Remove excessive whitespace
|
|
92
|
+
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
|
93
|
+
// Remove leading/trailing whitespace
|
|
94
|
+
.trim();
|
|
95
|
+
|
|
96
|
+
return cleaned;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Function to generate LLM text content in dory.json order, then append any extra files
|
|
100
|
+
function generateLlmContent(docsDir: string): string {
|
|
101
|
+
const navOrder = getNavigationOrder(docsDir);
|
|
102
|
+
const mdxFilesMap = findMdxFilesMap(docsDir, docsDir);
|
|
103
|
+
const usedKeys = new Set<string>();
|
|
104
|
+
let aggregatedContent = '';
|
|
105
|
+
|
|
106
|
+
// Output files in navigation order, in order as in dory.json
|
|
107
|
+
for (const navKey of navOrder) {
|
|
108
|
+
// Try both with and without "index"
|
|
109
|
+
let fileKey = navKey;
|
|
110
|
+
if (mdxFilesMap[fileKey]) {
|
|
111
|
+
usedKeys.add(fileKey);
|
|
112
|
+
} else if (mdxFilesMap[`${fileKey}/index`]) {
|
|
113
|
+
fileKey = `${fileKey}/index`;
|
|
114
|
+
usedKeys.add(fileKey);
|
|
115
|
+
} else {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const filePath = mdxFilesMap[fileKey];
|
|
119
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
120
|
+
const { data, content } = parseFrontmatter(rawContent);
|
|
121
|
+
const cleanedContent = cleanMdxContent(content);
|
|
122
|
+
|
|
123
|
+
// Add file header
|
|
124
|
+
aggregatedContent += `\n# ${fileKey}.mdx\n`;
|
|
125
|
+
|
|
126
|
+
// Add title if available
|
|
127
|
+
if (data.title) {
|
|
128
|
+
aggregatedContent += `## ${data.title}\n\n`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Add description if available
|
|
132
|
+
if (data.description) {
|
|
133
|
+
aggregatedContent += `${data.description}\n\n`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Add cleaned content
|
|
137
|
+
aggregatedContent += cleanedContent + '\n\n';
|
|
138
|
+
aggregatedContent += '---\n\n';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Output any extra files not in navigation order, sorted
|
|
142
|
+
const extraKeys = Object.keys(mdxFilesMap).filter(k => !usedKeys.has(k)).sort();
|
|
143
|
+
for (const fileKey of extraKeys) {
|
|
144
|
+
const filePath = mdxFilesMap[fileKey];
|
|
145
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
146
|
+
const { data, content } = parseFrontmatter(rawContent);
|
|
147
|
+
const cleanedContent = cleanMdxContent(content);
|
|
148
|
+
|
|
149
|
+
aggregatedContent += `\n# ${fileKey}.mdx\n`;
|
|
150
|
+
if (data.title) {
|
|
151
|
+
aggregatedContent += `## ${data.title}\n\n`;
|
|
152
|
+
}
|
|
153
|
+
if (data.description) {
|
|
154
|
+
aggregatedContent += `${data.description}\n\n`;
|
|
155
|
+
}
|
|
156
|
+
aggregatedContent += cleanedContent + '\n\n';
|
|
157
|
+
aggregatedContent += '---\n\n';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Remove leading newlines for the very first file, so the first file is at the very top
|
|
161
|
+
return aggregatedContent.replace(/^\n+/, '').trim();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function llmTxtGenerator(options: LlmTxtGeneratorOptions = {}): Plugin {
|
|
165
|
+
return {
|
|
166
|
+
name: 'llm-txt-generator',
|
|
167
|
+
generateBundle() {
|
|
168
|
+
const docsDir = options.docsDir || path.join(process.cwd(), 'docs');
|
|
169
|
+
const aggregatedContent = generateLlmContent(docsDir);
|
|
170
|
+
|
|
171
|
+
this.emitFile({
|
|
172
|
+
type: 'asset',
|
|
173
|
+
fileName: 'llm.txt',
|
|
174
|
+
source: aggregatedContent
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// For logging, count the number of MDX files processed
|
|
178
|
+
const mdxFilesMap = findMdxFilesMap(docsDir, docsDir);
|
|
179
|
+
const mdxFilesCount = Object.keys(mdxFilesMap).length;
|
|
180
|
+
|
|
181
|
+
console.log(`✅ Generated LLM text file`);
|
|
182
|
+
console.log(`📄 Processed ${mdxFilesCount} MDX files`);
|
|
183
|
+
console.log(`📝 Total content size: ${aggregatedContent.length} characters`);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
package/vite.config.ts
CHANGED
|
@@ -4,6 +4,8 @@ import mdx from '@mdx-js/rollup';
|
|
|
4
4
|
import tailwindcss from '@tailwindcss/vite'
|
|
5
5
|
import remarkGfm from 'remark-gfm';
|
|
6
6
|
import { remarkSafeVars } from './src/plugins/sanitize';
|
|
7
|
+
import { llmTxtGenerator } from './src/plugins/llm-txt-generator';
|
|
8
|
+
import { llmTxtDevServer } from './src/plugins/llm-txt-dev-server';
|
|
7
9
|
|
|
8
10
|
export default defineConfig({
|
|
9
11
|
plugins: [
|
|
@@ -13,6 +15,8 @@ export default defineConfig({
|
|
|
13
15
|
remarkPlugins: [remarkGfm, remarkSafeVars],
|
|
14
16
|
}),
|
|
15
17
|
tailwindcss(),
|
|
18
|
+
llmTxtGenerator(),
|
|
19
|
+
llmTxtDevServer(),
|
|
16
20
|
],
|
|
17
21
|
resolve: {
|
|
18
22
|
alias: {
|