@clidey/dory 0.3.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/bin/dory.js +57 -32
- 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/bin/dory.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { execSync } from 'child_process';
|
|
4
|
-
import { existsSync, rmSync, mkdirSync, cpSync, readFileSync } from 'fs';
|
|
4
|
+
import { existsSync, rmSync, mkdirSync, cpSync, readFileSync, readdirSync } from 'fs';
|
|
5
5
|
import { resolve, dirname } from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url'
|
|
7
7
|
import http from 'http';
|
|
@@ -12,40 +12,63 @@ const __dirname = dirname(__filename);
|
|
|
12
12
|
|
|
13
13
|
const commands = {
|
|
14
14
|
build: () => {
|
|
15
|
-
console.log('
|
|
15
|
+
console.log('๐ Dory is ready to build your docs!');
|
|
16
|
+
|
|
17
|
+
// Clear the docs folder if it exists
|
|
18
|
+
const docsDir = resolve(__dirname, '..', 'docs');
|
|
19
|
+
if (existsSync(docsDir)) {
|
|
20
|
+
console.log('๐งน Tidying up the workspace...');
|
|
21
|
+
rmSync(docsDir, { recursive: true, force: true });
|
|
22
|
+
}
|
|
16
23
|
|
|
17
24
|
// Check if dory.json exists in current directory
|
|
18
25
|
const currentDir = process.cwd();
|
|
19
26
|
const doryConfigPath = resolve(currentDir, 'dory.json');
|
|
20
27
|
|
|
21
28
|
if (!existsSync(doryConfigPath)) {
|
|
22
|
-
console.error('โ dory.json
|
|
29
|
+
console.error('โ Oops! dory.json is missing from your project');
|
|
23
30
|
process.exit(1);
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
// Read and validate dory.json
|
|
27
34
|
try {
|
|
28
35
|
const config = JSON.parse(readFileSync(doryConfigPath, 'utf8'));
|
|
29
|
-
console.log(`๐
|
|
36
|
+
console.log(`๐ Building: ${config.name || 'Your awesome project'}`);
|
|
30
37
|
} catch (error) {
|
|
31
|
-
console.error('โ
|
|
38
|
+
console.error('โ Your dory.json seems to have some issues:', error.message);
|
|
32
39
|
process.exit(1);
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
// Clear and prepare docs folder
|
|
36
|
-
const docsDir = resolve(currentDir, 'docs');
|
|
37
|
-
if (existsSync(docsDir)) {
|
|
38
|
-
console.log('๐งน Clearing docs folder...');
|
|
39
|
-
rmSync(docsDir, { recursive: true, force: true });
|
|
40
|
-
}
|
|
41
42
|
mkdirSync(docsDir, { recursive: true });
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
console.log('๐ Gathering your project files...');
|
|
44
45
|
cpSync(doryConfigPath, resolve(docsDir, 'dory.json'));
|
|
45
|
-
|
|
46
|
+
|
|
47
|
+
const copyFiles = (src, dest) => {
|
|
48
|
+
if (existsSync(src)) {
|
|
49
|
+
if (existsSync(dest)) {
|
|
50
|
+
rmSync(dest, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
cpSync(src, dest, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Copy all files and directories except node_modules, dist, and .git
|
|
57
|
+
const excludeDirs = ['node_modules', 'dist', '.git', 'docs', 'pnpm-lock.yaml'];
|
|
58
|
+
|
|
59
|
+
const items = readdirSync(currentDir);
|
|
60
|
+
for (const item of items) {
|
|
61
|
+
if (!excludeDirs.includes(item)) {
|
|
62
|
+
const srcPath = resolve(currentDir, item);
|
|
63
|
+
const destPath = resolve(docsDir, item);
|
|
64
|
+
copyFiles(srcPath, destPath);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log('โ
All files gathered successfully!');
|
|
46
69
|
|
|
47
70
|
// Run the build command
|
|
48
|
-
console.log('โก
|
|
71
|
+
console.log('โก Magic is happening...');
|
|
49
72
|
try {
|
|
50
73
|
execSync('npm run build', { stdio: 'inherit', cwd: __dirname });
|
|
51
74
|
} catch (error) {
|
|
@@ -56,19 +79,23 @@ const commands = {
|
|
|
56
79
|
// Check if dist folder was created
|
|
57
80
|
const distDir = resolve(__dirname, '..', 'dist');
|
|
58
81
|
if (existsSync(distDir)) {
|
|
59
|
-
console.log('
|
|
60
|
-
console.log(
|
|
61
|
-
console.log('๐ Moving build output to current directory...');
|
|
82
|
+
console.log('๐ Build completed successfully!');
|
|
83
|
+
console.log('๐ฆ Moving your docs to the right place...');
|
|
62
84
|
cpSync(distDir, resolve(currentDir, 'dist'), { recursive: true });
|
|
63
|
-
console.log(
|
|
85
|
+
console.log('โจ Your docs are ready in the dist folder!');
|
|
64
86
|
} else {
|
|
65
|
-
console.log('โ ๏ธ Build completed but no
|
|
87
|
+
console.log('โ ๏ธ Build completed but no output found');
|
|
66
88
|
}
|
|
67
89
|
rmSync(distDir, { recursive: true, force: true });
|
|
90
|
+
|
|
91
|
+
// Revert docs folder back to original state
|
|
92
|
+
console.log('๐งน Cleaning up...');
|
|
93
|
+
rmSync(docsDir, { recursive: true, force: true });
|
|
94
|
+
console.log('โ
All done!');
|
|
68
95
|
},
|
|
69
96
|
|
|
70
97
|
preview: () => {
|
|
71
|
-
console.log('๐ Starting
|
|
98
|
+
console.log('๐ Starting your docs preview...');
|
|
72
99
|
const currentDir = process.cwd();
|
|
73
100
|
const distDir = resolve(currentDir, 'dist');
|
|
74
101
|
const port = process.env.PORT || 3000;
|
|
@@ -86,33 +113,31 @@ const commands = {
|
|
|
86
113
|
});
|
|
87
114
|
|
|
88
115
|
server.listen(port, () => {
|
|
89
|
-
console.log(`๐
|
|
116
|
+
console.log(`๐ Your docs are live at http://localhost:${port}`);
|
|
90
117
|
});
|
|
91
118
|
},
|
|
92
119
|
|
|
93
120
|
help: () => {
|
|
94
121
|
console.log(`
|
|
95
|
-
๐ Dory CLI - Documentation
|
|
122
|
+
๐ Dory CLI - Your Friendly Documentation Builder
|
|
96
123
|
|
|
97
124
|
Usage:
|
|
98
125
|
dory <command>
|
|
99
126
|
|
|
100
127
|
Commands:
|
|
101
|
-
build Build
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
- Runs build command
|
|
105
|
-
- Creates dist folder with build output
|
|
128
|
+
build Build your documentation site
|
|
129
|
+
- Needs dory.json in your project
|
|
130
|
+
- Creates a beautiful dist folder
|
|
106
131
|
|
|
107
|
-
preview Preview
|
|
108
|
-
-
|
|
109
|
-
-
|
|
132
|
+
preview Preview your built documentation
|
|
133
|
+
- Shows your docs in the browser
|
|
134
|
+
- Run build first!
|
|
110
135
|
|
|
111
136
|
help Show this help message
|
|
112
137
|
|
|
113
138
|
Examples:
|
|
114
|
-
dory build # Build
|
|
115
|
-
dory preview # Preview
|
|
139
|
+
dory build # Build your docs
|
|
140
|
+
dory preview # Preview your docs
|
|
116
141
|
`);
|
|
117
142
|
}
|
|
118
143
|
};
|
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: {
|