@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 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('๐Ÿ—๏ธ Starting Dory build...');
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 not found in current directory');
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(`๐Ÿ“‹ Using config: ${config.name || 'Unnamed project'}`);
36
+ console.log(`๐Ÿ“‹ Building: ${config.name || 'Your awesome project'}`);
30
37
  } catch (error) {
31
- console.error('โŒ Invalid dory.json file:', error.message);
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
- // Copy dory.json to docs folder
44
+ console.log('๐Ÿ“ Gathering your project files...');
44
45
  cpSync(doryConfigPath, resolve(docsDir, 'dory.json'));
45
- console.log('๐Ÿ“„ Copied dory.json to docs folder');
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('โšก Running build command...');
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('โœ… Build completed successfully!');
60
- console.log(`๐Ÿ“ฆ Build output available in: ${distDir}`);
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(`๐Ÿ“ฆ Build output moved to: ${resolve(currentDir, 'dist')}`);
85
+ console.log('โœจ Your docs are ready in the dist folder!');
64
86
  } else {
65
- console.log('โš ๏ธ Build completed but no dist folder found');
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 Dory preview...');
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(`๐Ÿš€ Serving Dory at http://localhost:${port}`);
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 Build Tool
122
+ ๐ŸŸ Dory CLI - Your Friendly Documentation Builder
96
123
 
97
124
  Usage:
98
125
  dory <command>
99
126
 
100
127
  Commands:
101
- build Build the documentation site
102
- - Requires dory.json in current directory
103
- - Clears and copies to docs folder
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 the built documentation site
108
- - Requires dist folder (run build first)
109
- - Starts preview server
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 the documentation
115
- dory preview # Preview the built site
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:20-alpine AS builder
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
- 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
5
+ COPY ./k8s/config/nginx-default.conf /app/k8s/config/nginx-default.conf
14
6
 
15
- CMD ["nginx", "-g", "daemon off;"]
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.3.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: {