@clidey/dory 0.9.0 → 0.11.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.
@@ -0,0 +1,158 @@
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---/);
39
+ if (!frontmatterMatch) return { data: {}, content };
40
+
41
+ const frontmatter = frontmatterMatch[1];
42
+ const data: Record<string, any> = {};
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
+ // Convert filename to path format used by the store
56
+ function pathFromFilename(filename: string, docsDir: string): string {
57
+ return filename
58
+ .replace(docsDir, '')
59
+ .replace(/\/?index\.mdx$/, '/')
60
+ .replace(/\.mdx$/, '')
61
+ .replace(/\\/g, '/')
62
+ .toLowerCase();
63
+ }
64
+
65
+ // Function to recursively find all MDX files and map by relative path (without .mdx)
66
+ function findMdxFilesMap(dir: string, baseDir: string): Record<string, string> {
67
+ const files: Record<string, string> = {};
68
+ const items = fs.readdirSync(dir, { withFileTypes: true });
69
+
70
+ for (const item of items) {
71
+ const fullPath = path.join(dir, item.name);
72
+ if (item.isDirectory()) {
73
+ Object.assign(files, findMdxFilesMap(fullPath, baseDir));
74
+ } else if (item.isFile() && item.name.endsWith('.mdx')) {
75
+ // Key: relative path without .mdx, with forward slashes
76
+ let rel = path.relative(baseDir, fullPath).replace(/\\/g, '/');
77
+ if (rel.endsWith('.mdx')) rel = rel.slice(0, -4);
78
+ files[rel] = fullPath;
79
+ }
80
+ }
81
+
82
+ return files;
83
+ }
84
+
85
+ // Function to generate frontmatter JSON in dory.json order, then append any extra files
86
+ function generateFrontmatterJson(docsDir: string): Array<Record<string, any>> {
87
+ const navOrder = getNavigationOrder(docsDir);
88
+ const mdxFilesMap = findMdxFilesMap(docsDir, docsDir);
89
+ const usedKeys = new Set<string>();
90
+ const frontmatterArray: Array<Record<string, any>> = [];
91
+
92
+ // Process files in navigation order first
93
+ for (const navKey of navOrder) {
94
+ // Try both with and without "index"
95
+ let fileKey = navKey;
96
+ if (mdxFilesMap[fileKey]) {
97
+ usedKeys.add(fileKey);
98
+ } else if (mdxFilesMap[`${fileKey}/index`]) {
99
+ fileKey = `${fileKey}/index`;
100
+ usedKeys.add(fileKey);
101
+ } else {
102
+ continue;
103
+ }
104
+
105
+ const filePath = mdxFilesMap[fileKey];
106
+ const rawContent = fs.readFileSync(filePath, 'utf-8');
107
+ const { data } = parseFrontmatter(rawContent);
108
+ const pathname = pathFromFilename(filePath, docsDir);
109
+
110
+ frontmatterArray.push({
111
+ ...data,
112
+ path: pathname
113
+ });
114
+ }
115
+
116
+ // Process any extra files not in navigation order, sorted
117
+ const extraKeys = Object.keys(mdxFilesMap).filter(k => !usedKeys.has(k)).sort();
118
+ for (const fileKey of extraKeys) {
119
+ const filePath = mdxFilesMap[fileKey];
120
+ const rawContent = fs.readFileSync(filePath, 'utf-8');
121
+ const { data } = parseFrontmatter(rawContent);
122
+ const pathname = pathFromFilename(filePath, docsDir);
123
+
124
+ frontmatterArray.push({
125
+ ...data,
126
+ path: pathname
127
+ });
128
+ }
129
+
130
+ return frontmatterArray;
131
+ }
132
+
133
+ export function frontmatterDevServer(): Plugin {
134
+ return {
135
+ name: 'frontmatter-dev-server',
136
+ configureServer(server) {
137
+ server.middlewares.use('/frontmatter.json', (req, res, next) => {
138
+ if (req.method !== 'GET') {
139
+ return next();
140
+ }
141
+
142
+ try {
143
+ const docsDir = path.join(process.cwd(), 'docs');
144
+ const frontmatterArray = generateFrontmatterJson(docsDir);
145
+ const jsonContent = JSON.stringify(frontmatterArray, null, 2);
146
+
147
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
148
+ res.setHeader('Content-Length', Buffer.byteLength(jsonContent));
149
+ res.end(jsonContent);
150
+ } catch (error) {
151
+ console.error('Error generating frontmatter JSON:', error);
152
+ res.statusCode = 500;
153
+ res.end('Internal Server Error');
154
+ }
155
+ });
156
+ }
157
+ };
158
+ }
@@ -0,0 +1,157 @@
1
+ import type { Plugin } from 'vite';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ interface FrontmatterGeneratorOptions {
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---/);
44
+ if (!frontmatterMatch) return { data: {}, content };
45
+
46
+ const frontmatter = frontmatterMatch[1];
47
+ const data: Record<string, any> = {};
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
+ // Convert filename to path format used by the store
61
+ function pathFromFilename(filename: string, docsDir: string): string {
62
+ return filename
63
+ .replace(docsDir, '')
64
+ .replace(/\/?index\.mdx$/, '/')
65
+ .replace(/\.mdx$/, '')
66
+ .replace(/\\/g, '/')
67
+ .toLowerCase();
68
+ }
69
+
70
+ // Function to recursively find all MDX files and map by relative path (without .mdx)
71
+ function findMdxFilesMap(dir: string, baseDir: string): Record<string, string> {
72
+ const files: Record<string, string> = {};
73
+ const items = fs.readdirSync(dir, { withFileTypes: true });
74
+
75
+ for (const item of items) {
76
+ const fullPath = path.join(dir, item.name);
77
+ if (item.isDirectory()) {
78
+ Object.assign(files, findMdxFilesMap(fullPath, baseDir));
79
+ } else if (item.isFile() && item.name.endsWith('.mdx')) {
80
+ // Key: relative path without .mdx, with forward slashes
81
+ let rel = path.relative(baseDir, fullPath).replace(/\\/g, '/');
82
+ if (rel.endsWith('.mdx')) rel = rel.slice(0, -4);
83
+ files[rel] = fullPath;
84
+ }
85
+ }
86
+
87
+ return files;
88
+ }
89
+
90
+ // Function to generate frontmatter JSON in dory.json order, then append any extra files
91
+ function generateFrontmatterJson(docsDir: string): Array<Record<string, any>> {
92
+ const navOrder = getNavigationOrder(docsDir);
93
+ const mdxFilesMap = findMdxFilesMap(docsDir, docsDir);
94
+ const usedKeys = new Set<string>();
95
+ const frontmatterArray: Array<Record<string, any>> = [];
96
+
97
+ // Process files in navigation order first
98
+ for (const navKey of navOrder) {
99
+ // Try both with and without "index"
100
+ let fileKey = navKey;
101
+ if (mdxFilesMap[fileKey]) {
102
+ usedKeys.add(fileKey);
103
+ } else if (mdxFilesMap[`${fileKey}/index`]) {
104
+ fileKey = `${fileKey}/index`;
105
+ usedKeys.add(fileKey);
106
+ } else {
107
+ continue;
108
+ }
109
+
110
+ const filePath = mdxFilesMap[fileKey];
111
+ const rawContent = fs.readFileSync(filePath, 'utf-8');
112
+ const { data } = parseFrontmatter(rawContent);
113
+ const pathname = pathFromFilename(filePath, docsDir);
114
+
115
+ frontmatterArray.push({
116
+ ...data,
117
+ path: pathname
118
+ });
119
+ }
120
+
121
+ // Process any extra files not in navigation order, sorted
122
+ const extraKeys = Object.keys(mdxFilesMap).filter(k => !usedKeys.has(k)).sort();
123
+ for (const fileKey of extraKeys) {
124
+ const filePath = mdxFilesMap[fileKey];
125
+ const rawContent = fs.readFileSync(filePath, 'utf-8');
126
+ const { data } = parseFrontmatter(rawContent);
127
+ const pathname = pathFromFilename(filePath, docsDir);
128
+
129
+ frontmatterArray.push({
130
+ ...data,
131
+ path: pathname
132
+ });
133
+ }
134
+
135
+ return frontmatterArray;
136
+ }
137
+
138
+ export function frontmatterGenerator(options: FrontmatterGeneratorOptions = {}): Plugin {
139
+ return {
140
+ name: 'frontmatter-generator',
141
+ generateBundle() {
142
+ const docsDir = options.docsDir || path.join(process.cwd(), 'docs');
143
+ const frontmatterArray = generateFrontmatterJson(docsDir);
144
+ const jsonContent = JSON.stringify(frontmatterArray, null, 2);
145
+
146
+ this.emitFile({
147
+ type: 'asset',
148
+ fileName: 'frontmatter.json',
149
+ source: jsonContent
150
+ });
151
+
152
+ console.log(`✅ Generated frontmatter JSON file`);
153
+ console.log(`📄 Processed ${frontmatterArray.length} MDX files`);
154
+ console.log(`📝 Total JSON size: ${jsonContent.length} characters`);
155
+ }
156
+ };
157
+ }
@@ -13,3 +13,43 @@ export const remarkSafeVars = () => {
13
13
  });
14
14
  };
15
15
  };
16
+
17
+ const KNOWN_COMPONENTS = [
18
+ // Text components
19
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'Row', 'Col', 'a', 'img',
20
+ // Code components
21
+ 'code', 'CodeGroup', 'pre',
22
+ // UI components
23
+ 'Accordion', 'AccordionGroup', 'Icon', 'Card', 'CardGroup',
24
+ 'Callout', 'Note', 'Warning', 'Info', 'Tip', 'Check',
25
+ 'Latex', 'API', 'Expandable', 'ResponseField', 'Properties', 'Property',
26
+ 'Steps', 'Step', 'table', 'th', 'td', 'ul', 'ol', 'li',
27
+ 'APIPlayground', 'WebSocketPlayground', 'AsyncAPI', 'Source',
28
+ // Common HTML elements that should be preserved
29
+ 'div', 'span', 'br', 'hr', 'strong', 'em', 'b', 'i', 'u', 's',
30
+ 'blockquote', 'nav', 'section', 'article', 'header', 'footer', 'main',
31
+ 'aside', 'figure', 'figcaption', 'details', 'summary', 'mark', 'small',
32
+ 'del', 'ins', 'sub', 'sup', 'kbd', 'samp', 'var', 'time', 'abbr',
33
+ 'address', 'cite', 'q', 'dfn'
34
+ ];
35
+
36
+ export function preprocessMdxTags() {
37
+ return {
38
+ name: 'preprocess-mdx-tags',
39
+ enforce: 'pre' as const,
40
+ transform(code: string, id: string) {
41
+ if (!id.endsWith('.mdx')) return;
42
+
43
+ // Replace unrecognized <someTag> with `"<someTag>"`
44
+ const processed = code.replace(/<([a-z][a-z0-9]+)>/gi, (match, tag) => {
45
+ const known = KNOWN_COMPONENTS.includes(tag);
46
+ return known ? match : `\`<${tag}>\``; // wrap in backticks to keep as code
47
+ });
48
+
49
+ return {
50
+ code: processed,
51
+ map: null,
52
+ };
53
+ },
54
+ };
55
+ }
package/src/ui/layout.tsx CHANGED
@@ -2,8 +2,9 @@ import type { ComponentChildren } from 'preact';
2
2
  import { useCallback, useEffect, useMemo, useState } from 'preact/hooks';
3
3
  import { useLocation } from 'wouter-preact';
4
4
  import { Navigation } from '../components/navigation';
5
- import { ALL_NAVIGATION, ALL_OPENAPI, completeFrontMatter, loadMDXFrontMatterForPath, loadAllMDXFrontMatter, ALL_PAGES, pathFromFilename } from '../components/store';
5
+ import { ALL_NAVIGATION, ALL_OPENAPI, completeFrontMatter, loadMDXFrontMatterForPath, loadAllMDXFrontMatter, ALL_PAGES, pathFromFilename, preloadFrontmatter, ALL_ASYNCAPI } from '../components/store';
6
6
  import { OpenAPI } from '../mdx/open-api';
7
+ import { AsyncAPI } from '../mdx/async-api';
7
8
  import { Header } from './header';
8
9
  import { PrevNextLinks } from './prev-next-link';
9
10
  import { TableOfContents } from './table-of-content';
@@ -63,14 +64,21 @@ export default function Layout({ children }: LayoutProps) {
63
64
  ], [handleOpenMDX, handleCopyMDX, handleOpenChatGPT, handleOpenAnthropic]);
64
65
 
65
66
  useEffect(() => {
66
- loadMDXFrontMatterForPath(pathname).then(() => {
67
- loadAllMDXFrontMatter(pathname).then(() => {
68
- setLoading(false);
67
+ // Try to preload frontmatter first (optimized approach)
68
+ preloadFrontmatter().then(() => {
69
+ // If preloading succeeds, we're done
70
+ setLoading(false);
71
+ }).catch(() => {
72
+ // Fallback to the original approach
73
+ loadMDXFrontMatterForPath(pathname).then(() => {
74
+ loadAllMDXFrontMatter(pathname).then(() => {
75
+ setLoading(false);
76
+ }).catch(() => {
77
+ setLoading(false);
78
+ });
69
79
  }).catch(() => {
70
80
  setLoading(false);
71
81
  });
72
- }).catch(() => {
73
- setLoading(false);
74
82
  });
75
83
  }, [pathname]);
76
84
 
@@ -116,6 +124,21 @@ export default function Layout({ children }: LayoutProps) {
116
124
  return { method: tab.openapi.split(' ')[0], path: tab.openapi.split(' ')[1] };
117
125
  }, [tab, loading]);
118
126
 
127
+ const asyncAPIJSON = useMemo(() => {
128
+ if (tab == null || tab.asyncapi == null) {
129
+ return null;
130
+ }
131
+ const firstDirectory = `/${tab.path.split('/')[1]}`;
132
+ return ALL_ASYNCAPI[`${firstDirectory}/asyncapi.json`].default;
133
+ }, [tab, loading]);
134
+
135
+ const { operation, channel } = useMemo(() => {
136
+ if (tab == null || tab.asyncapi == null) {
137
+ return { operation: '', channel: '' };
138
+ }
139
+ return { operation: tab.asyncapi.split(' ')[0].toLowerCase(), channel: tab.asyncapi.split(' ')[1] };
140
+ }, [tab, loading]);
141
+
119
142
  if (loading) return <div className="h-[25vh] flex grow"><Loading /></div>;
120
143
 
121
144
  return (
@@ -144,20 +167,22 @@ export default function Layout({ children }: LayoutProps) {
144
167
  )}
145
168
  </header>
146
169
  {
147
- openAPIJSON == null &&
170
+ openAPIJSON == null && asyncAPIJSON == null &&
148
171
  <Dropdown className='min-w-[120px]' buttonLabel="AI Actions" items={dropdownItems} />
149
172
  }
150
173
  </div>
151
174
  <div className="flex-1 min-h-[calc(100vh-2rem)]">
152
175
  {openAPIJSON && tab && "openapi" in tab && method && path ?
153
176
  <OpenAPI openAPIJson={JSON.parse(openAPIJSON)} method={method} path={path} />
177
+ : asyncAPIJSON && tab && "asyncapi" in tab && operation && channel ?
178
+ <AsyncAPI asyncAPIJson={JSON.parse(asyncAPIJSON)} operation={operation} channel={channel} />
154
179
  : children}
155
180
  </div>
156
181
  <PrevNextLinks />
157
182
  </article>
158
183
  </div>
159
184
  {
160
- openAPIJSON == null &&
185
+ openAPIJSON == null && asyncAPIJSON == null &&
161
186
  <TableOfContents />
162
187
  }
163
188
  </main>
package/vite.config.ts CHANGED
@@ -3,12 +3,15 @@ import preact from '@preact/preset-vite';
3
3
  import mdx from '@mdx-js/rollup';
4
4
  import tailwindcss from '@tailwindcss/vite'
5
5
  import remarkGfm from 'remark-gfm';
6
- import { remarkSafeVars } from './src/plugins/sanitize';
6
+ import { preprocessMdxTags, remarkSafeVars } from './src/plugins/sanitize';
7
7
  import { llmTxtGenerator } from './src/plugins/llm-txt-generator';
8
8
  import { llmTxtDevServer } from './src/plugins/llm-txt-dev-server';
9
+ import { frontmatterGenerator } from './src/plugins/frontmatter-generator';
10
+ import { frontmatterDevServer } from './src/plugins/frontmatter-dev-server';
9
11
 
10
12
  export default defineConfig({
11
13
  plugins: [
14
+ preprocessMdxTags(),
12
15
  preact(),
13
16
  mdx({
14
17
  providerImportSource: '@mdx-js/preact',
@@ -17,6 +20,8 @@ export default defineConfig({
17
20
  tailwindcss(),
18
21
  llmTxtGenerator(),
19
22
  llmTxtDevServer(),
23
+ frontmatterGenerator(),
24
+ frontmatterDevServer(),
20
25
  ],
21
26
  resolve: {
22
27
  alias: {