@clidey/dory 0.4.0 → 0.6.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/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.4.0",
4
+ "version": "0.6.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, 'llms.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('/llms.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: 'llms.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: {