@clidey/dory 0.46.0 → 0.48.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/bin/dist/dory.js +103 -45
- package/bin/dist/dory.js.map +1 -1
- package/index.html +11 -15
- package/package.json +35 -29
- package/scripts/render-routes.mjs +55 -15
- package/src/components/routes.tsx +38 -93
- package/src/components/search.tsx +1 -0
- package/src/components/store.ts +64 -88
- package/src/config/mdx.js +7 -3
- package/src/config/mdx.ts +6 -2
- package/src/entry-server.tsx +189 -66
- package/src/main.tsx +27 -5
- package/src/mdx/api-playground.tsx +2 -0
- package/src/mdx/code.tsx +8 -5
- package/src/mdx/fence.tsx +10 -2
- package/src/mdx/icon.tsx +21 -14
- package/src/mdx/latex.tsx +5 -0
- package/src/mdx/mdx.tsx +19 -11
- package/src/mdx/mermaid.tsx +10 -1
- package/src/mdx/processor.test.ts +47 -0
- package/src/mdx/step.tsx +3 -3
- package/src/mdx/websocket-playground.tsx +2 -0
- package/src/plugins/analytics-injector.ts +7 -5
- package/src/plugins/docs-assets.ts +5 -4
- package/src/plugins/frontmatter-dev-server.ts +23 -137
- package/src/plugins/frontmatter-generator.ts +64 -111
- package/src/plugins/html-files-middleware.test.ts +21 -0
- package/src/plugins/html-files-middleware.ts +27 -5
- package/src/plugins/html-metadata-injector.ts +94 -53
- package/src/plugins/llm-txt-dev-server.ts +25 -178
- package/src/plugins/llm-txt-generator.ts +94 -133
- package/src/plugins/prerender.ts +156 -50
- package/src/plugins/robots-generator.ts +15 -16
- package/src/plugins/sanitize.test.ts +104 -0
- package/src/plugins/sanitize.ts +91 -60
- package/src/plugins/shared.ts +139 -0
- package/src/plugins/sitemap-generator.ts +58 -44
- package/src/theme.scss +138 -15
- package/src/types/config.ts +42 -2
- package/src/types/globals.d.ts +7 -0
- package/src/ui/header.tsx +12 -4
- package/src/ui/layout.tsx +72 -52
- package/src/ui/table-of-content.tsx +19 -2
- package/src/utils/colors.ts +38 -18
- package/src/utils/fonts.ts +55 -12
- package/src/utils/theme.ts +29 -0
- package/vite.config.embed-loader.ts +3 -11
- package/vite.config.embed-widget.ts +3 -10
- package/vite.config.ssr.ts +2 -1
- package/vite.config.ts +13 -8
- package/scripts/generate-frontmatter-json.js +0 -154
- package/scripts/generate-llm-txt.js +0 -115
|
@@ -37,8 +37,9 @@ export function docsAssetsPlugin(): Plugin {
|
|
|
37
37
|
const shouldSkip = (name: string) => {
|
|
38
38
|
if (skipPatterns.has(name)) return true;
|
|
39
39
|
const ext = extname(name).toLowerCase();
|
|
40
|
-
// Skip
|
|
41
|
-
|
|
40
|
+
// Skip Markdown and JSON files (they're processed by Vite).
|
|
41
|
+
// MDX sources ARE copied so /route.mdx URLs work (Open MDX / LLM prompts).
|
|
42
|
+
if (ext === '.md' || ext === '.json') return true;
|
|
42
43
|
return false;
|
|
43
44
|
};
|
|
44
45
|
|
|
@@ -60,8 +61,8 @@ export function docsAssetsPlugin(): Plugin {
|
|
|
60
61
|
copyAssets(srcPath, destPath, itemRelativePath);
|
|
61
62
|
} else if (stats.isFile()) {
|
|
62
63
|
const ext = extname(item).toLowerCase();
|
|
63
|
-
// Copy
|
|
64
|
-
if (assetExtensions.has(ext)) {
|
|
64
|
+
// Copy asset files, plus source .mdx files at their route paths
|
|
65
|
+
if (assetExtensions.has(ext) || ext === '.mdx') {
|
|
65
66
|
cpSync(srcPath, destPath, { force: true });
|
|
66
67
|
console.log(`[docs-assets] Copied ${itemRelativePath}`);
|
|
67
68
|
}
|
|
@@ -1,146 +1,32 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
|
-
import fs from 'fs';
|
|
3
2
|
import path from 'path';
|
|
3
|
+
import { generateFrontmatterJson, generateSearchContentJson } from './frontmatter-generator';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
let doryJson;
|
|
10
|
-
try {
|
|
11
|
-
doryJson = JSON.parse(fs.readFileSync(doryJsonPath, 'utf-8'));
|
|
12
|
-
} catch (error) {
|
|
13
|
-
const detail = error instanceof SyntaxError ? error.message : String(error);
|
|
14
|
-
console.error(`Failed to parse ${doryJsonPath}: ${detail}`);
|
|
15
|
-
return [];
|
|
16
|
-
}
|
|
17
|
-
const order: string[] = [];
|
|
18
|
-
|
|
19
|
-
function walkPages(pages: any[]) {
|
|
20
|
-
for (const page of pages) {
|
|
21
|
-
if (typeof page === 'string') {
|
|
22
|
-
order.push(page);
|
|
23
|
-
} else if (typeof page === 'object' && page.pages) {
|
|
24
|
-
walkPages(page.pages);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (doryJson.navigation && Array.isArray(doryJson.navigation.tabs)) {
|
|
30
|
-
for (const tab of doryJson.navigation.tabs) {
|
|
31
|
-
if (tab.groups && Array.isArray(tab.groups)) {
|
|
32
|
-
for (const group of tab.groups) {
|
|
33
|
-
if (group.pages && Array.isArray(group.pages)) {
|
|
34
|
-
walkPages(group.pages);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return order;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Function to parse frontmatter from MDX content
|
|
44
|
-
function parseFrontmatter(content: string) {
|
|
45
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
46
|
-
if (!frontmatterMatch) return { data: {}, content };
|
|
47
|
-
|
|
48
|
-
const frontmatter = frontmatterMatch[1];
|
|
49
|
-
const data: Record<string, any> = {};
|
|
50
|
-
|
|
51
|
-
frontmatter.split('\n').forEach(line => {
|
|
52
|
-
const [key, ...valueParts] = line.split(':');
|
|
53
|
-
if (key && valueParts.length) {
|
|
54
|
-
const value = valueParts.join(':').trim();
|
|
55
|
-
data[key.trim()] = value.replace(/^["']|["']$/g, '');
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
return { data, content: content.slice(frontmatterMatch[0].length) };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Convert filename to path format used by the store
|
|
63
|
-
function pathFromFilename(filename: string, docsDir: string): string {
|
|
64
|
-
return filename
|
|
65
|
-
.replace(docsDir, '')
|
|
66
|
-
.replace(/\/?index\.mdx$/, '/')
|
|
67
|
-
.replace(/\.mdx$/, '')
|
|
68
|
-
.replace(/\\/g, '/')
|
|
69
|
-
.toLowerCase();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Function to recursively find all MDX files and map by relative path (without .mdx)
|
|
73
|
-
function findMdxFilesMap(dir: string, baseDir: string): Record<string, string> {
|
|
74
|
-
const files: Record<string, string> = {};
|
|
75
|
-
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
76
|
-
|
|
77
|
-
for (const item of items) {
|
|
78
|
-
const fullPath = path.join(dir, item.name);
|
|
79
|
-
if (item.isDirectory()) {
|
|
80
|
-
Object.assign(files, findMdxFilesMap(fullPath, baseDir));
|
|
81
|
-
} else if (item.isFile() && item.name.endsWith('.mdx')) {
|
|
82
|
-
// Key: relative path without .mdx, with forward slashes
|
|
83
|
-
let rel = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
84
|
-
if (rel.endsWith('.mdx')) rel = rel.slice(0, -4);
|
|
85
|
-
files[rel] = fullPath;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return files;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Function to generate frontmatter JSON in dory.json order, then append any extra files
|
|
93
|
-
function generateFrontmatterJson(docsDir: string): Array<Record<string, any>> {
|
|
94
|
-
const navOrder = getNavigationOrder(docsDir);
|
|
95
|
-
const mdxFilesMap = findMdxFilesMap(docsDir, docsDir);
|
|
96
|
-
const usedKeys = new Set<string>();
|
|
97
|
-
const frontmatterArray: Array<Record<string, any>> = [];
|
|
98
|
-
|
|
99
|
-
// Process files in navigation order first
|
|
100
|
-
for (const navKey of navOrder) {
|
|
101
|
-
// Try both with and without "index"
|
|
102
|
-
let fileKey = navKey;
|
|
103
|
-
if (mdxFilesMap[fileKey]) {
|
|
104
|
-
usedKeys.add(fileKey);
|
|
105
|
-
} else if (mdxFilesMap[`${fileKey}/index`]) {
|
|
106
|
-
fileKey = `${fileKey}/index`;
|
|
107
|
-
usedKeys.add(fileKey);
|
|
108
|
-
} else {
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const filePath = mdxFilesMap[fileKey];
|
|
113
|
-
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
114
|
-
const { data } = parseFrontmatter(rawContent);
|
|
115
|
-
const pathname = pathFromFilename(filePath, docsDir);
|
|
116
|
-
|
|
117
|
-
frontmatterArray.push({
|
|
118
|
-
...data,
|
|
119
|
-
path: pathname
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Process any extra files not in navigation order, sorted
|
|
124
|
-
const extraKeys = Object.keys(mdxFilesMap).filter(k => !usedKeys.has(k)).sort();
|
|
125
|
-
for (const fileKey of extraKeys) {
|
|
126
|
-
const filePath = mdxFilesMap[fileKey];
|
|
127
|
-
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
128
|
-
const { data } = parseFrontmatter(rawContent);
|
|
129
|
-
const pathname = pathFromFilename(filePath, docsDir);
|
|
130
|
-
|
|
131
|
-
frontmatterArray.push({
|
|
132
|
-
...data,
|
|
133
|
-
path: pathname
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return frontmatterArray;
|
|
138
|
-
}
|
|
139
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Dev server plugin that serves frontmatter.json and search-content.json
|
|
7
|
+
* on demand, generated fresh per request from the docs directory.
|
|
8
|
+
*/
|
|
140
9
|
export function frontmatterDevServer(): Plugin {
|
|
141
10
|
return {
|
|
142
11
|
name: 'frontmatter-dev-server',
|
|
143
12
|
configureServer(server) {
|
|
13
|
+
// Serve search content for client-side indexing
|
|
14
|
+
server.middlewares.use('/search-content.json', (req, res, next) => {
|
|
15
|
+
if (req.method !== 'GET') return next();
|
|
16
|
+
try {
|
|
17
|
+
const docsDir = path.join(process.cwd(), 'docs');
|
|
18
|
+
const frontmatterArray = generateFrontmatterJson(docsDir);
|
|
19
|
+
const searchContent = generateSearchContentJson(docsDir, frontmatterArray);
|
|
20
|
+
const json = JSON.stringify(searchContent);
|
|
21
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
22
|
+
res.end(json);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('Error generating search content:', error);
|
|
25
|
+
res.statusCode = 500;
|
|
26
|
+
res.end('Internal Server Error');
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
144
30
|
server.middlewares.use('/frontmatter.json', (req, res, next) => {
|
|
145
31
|
if (req.method !== 'GET') {
|
|
146
32
|
return next();
|
|
@@ -162,4 +48,4 @@ export function frontmatterDevServer(): Plugin {
|
|
|
162
48
|
});
|
|
163
49
|
}
|
|
164
50
|
};
|
|
165
|
-
}
|
|
51
|
+
}
|
|
@@ -1,106 +1,38 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import {
|
|
5
|
+
getNavigationOrder,
|
|
6
|
+
parseFrontmatter,
|
|
7
|
+
pathFromFilename,
|
|
8
|
+
findMdxFilesMap,
|
|
9
|
+
stripMdxToText,
|
|
10
|
+
} from './shared';
|
|
4
11
|
|
|
5
12
|
interface FrontmatterGeneratorOptions {
|
|
6
13
|
docsDir?: string;
|
|
7
|
-
outputPath?: string;
|
|
8
14
|
}
|
|
9
15
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
doryJson = JSON.parse(fs.readFileSync(doryJsonPath, 'utf-8'));
|
|
17
|
-
} catch (error) {
|
|
18
|
-
const detail = error instanceof SyntaxError ? error.message : String(error);
|
|
19
|
-
console.error(`Failed to parse ${doryJsonPath}: ${detail}`);
|
|
20
|
-
return [];
|
|
21
|
-
}
|
|
22
|
-
const order: string[] = [];
|
|
23
|
-
|
|
24
|
-
function walkPages(pages: any[]) {
|
|
25
|
-
for (const page of pages) {
|
|
26
|
-
if (typeof page === 'string') {
|
|
27
|
-
order.push(page);
|
|
28
|
-
} else if (typeof page === 'object' && page.pages) {
|
|
29
|
-
walkPages(page.pages);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (doryJson.navigation && Array.isArray(doryJson.navigation.tabs)) {
|
|
35
|
-
for (const tab of doryJson.navigation.tabs) {
|
|
36
|
-
if (tab.groups && Array.isArray(tab.groups)) {
|
|
37
|
-
for (const group of tab.groups) {
|
|
38
|
-
if (group.pages && Array.isArray(group.pages)) {
|
|
39
|
-
walkPages(group.pages);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return order;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Function to parse frontmatter from MDX content
|
|
49
|
-
function parseFrontmatter(content: string) {
|
|
50
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
51
|
-
if (!frontmatterMatch) return { data: {}, content };
|
|
52
|
-
|
|
53
|
-
const frontmatter = frontmatterMatch[1];
|
|
54
|
-
const data: Record<string, any> = {};
|
|
55
|
-
|
|
56
|
-
frontmatter.split('\n').forEach(line => {
|
|
57
|
-
const [key, ...valueParts] = line.split(':');
|
|
58
|
-
if (key && valueParts.length) {
|
|
59
|
-
const value = valueParts.join(':').trim();
|
|
60
|
-
data[key.trim()] = value.replace(/^["']|["']$/g, '');
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
return { data, content: content.slice(frontmatterMatch[0].length) };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Convert filename to path format used by the store
|
|
68
|
-
function pathFromFilename(filename: string, docsDir: string): string {
|
|
69
|
-
return filename
|
|
70
|
-
.replace(docsDir, '')
|
|
71
|
-
.replace(/\/?index\.mdx$/, '/')
|
|
72
|
-
.replace(/\.mdx$/, '')
|
|
73
|
-
.replace(/\\/g, '/')
|
|
74
|
-
.toLowerCase();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Function to recursively find all MDX files and map by relative path (without .mdx)
|
|
78
|
-
function findMdxFilesMap(dir: string, baseDir: string): Record<string, string> {
|
|
79
|
-
const files: Record<string, string> = {};
|
|
80
|
-
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
81
|
-
|
|
82
|
-
for (const item of items) {
|
|
83
|
-
const fullPath = path.join(dir, item.name);
|
|
84
|
-
if (item.isDirectory()) {
|
|
85
|
-
Object.assign(files, findMdxFilesMap(fullPath, baseDir));
|
|
86
|
-
} else if (item.isFile() && item.name.endsWith('.mdx')) {
|
|
87
|
-
// Key: relative path without .mdx, with forward slashes
|
|
88
|
-
let rel = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
89
|
-
if (rel.endsWith('.mdx')) rel = rel.slice(0, -4);
|
|
90
|
-
files[rel] = fullPath;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return files;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Function to generate frontmatter JSON in dory.json order, then append any extra files
|
|
98
|
-
function generateFrontmatterJson(docsDir: string): Array<Record<string, any>> {
|
|
16
|
+
/**
|
|
17
|
+
* Generates the frontmatter array for all MDX files in dory.json navigation
|
|
18
|
+
* order, then appends any extra files (sorted).
|
|
19
|
+
*/
|
|
20
|
+
export function generateFrontmatterJson(docsDir: string): Array<Record<string, any>> {
|
|
99
21
|
const navOrder = getNavigationOrder(docsDir);
|
|
100
22
|
const mdxFilesMap = findMdxFilesMap(docsDir, docsDir);
|
|
101
23
|
const usedKeys = new Set<string>();
|
|
102
24
|
const frontmatterArray: Array<Record<string, any>> = [];
|
|
103
25
|
|
|
26
|
+
const pushEntry = (fileKey: string) => {
|
|
27
|
+
const filePath = mdxFilesMap[fileKey];
|
|
28
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
29
|
+
const { data } = parseFrontmatter(rawContent);
|
|
30
|
+
frontmatterArray.push({
|
|
31
|
+
...data,
|
|
32
|
+
path: pathFromFilename(filePath, docsDir),
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
104
36
|
// Process files in navigation order first
|
|
105
37
|
for (const navKey of navOrder) {
|
|
106
38
|
// Try both with and without "index"
|
|
@@ -113,35 +45,46 @@ function generateFrontmatterJson(docsDir: string): Array<Record<string, any>> {
|
|
|
113
45
|
} else {
|
|
114
46
|
continue;
|
|
115
47
|
}
|
|
116
|
-
|
|
117
|
-
const filePath = mdxFilesMap[fileKey];
|
|
118
|
-
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
119
|
-
const { data } = parseFrontmatter(rawContent);
|
|
120
|
-
const pathname = pathFromFilename(filePath, docsDir);
|
|
121
|
-
|
|
122
|
-
frontmatterArray.push({
|
|
123
|
-
...data,
|
|
124
|
-
path: pathname
|
|
125
|
-
});
|
|
48
|
+
pushEntry(fileKey);
|
|
126
49
|
}
|
|
127
50
|
|
|
128
51
|
// Process any extra files not in navigation order, sorted
|
|
129
52
|
const extraKeys = Object.keys(mdxFilesMap).filter(k => !usedKeys.has(k)).sort();
|
|
130
53
|
for (const fileKey of extraKeys) {
|
|
131
|
-
|
|
132
|
-
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
133
|
-
const { data } = parseFrontmatter(rawContent);
|
|
134
|
-
const pathname = pathFromFilename(filePath, docsDir);
|
|
135
|
-
|
|
136
|
-
frontmatterArray.push({
|
|
137
|
-
...data,
|
|
138
|
-
path: pathname
|
|
139
|
-
});
|
|
54
|
+
pushEntry(fileKey);
|
|
140
55
|
}
|
|
141
56
|
|
|
142
57
|
return frontmatterArray;
|
|
143
58
|
}
|
|
144
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Generates search content entries: [{ path, title, content }] with MDX
|
|
62
|
+
* stripped to plain text for client-side indexing.
|
|
63
|
+
*/
|
|
64
|
+
export function generateSearchContentJson(
|
|
65
|
+
docsDir: string,
|
|
66
|
+
frontmatterArray: Array<Record<string, any>>
|
|
67
|
+
): Array<Record<string, any>> {
|
|
68
|
+
const mdxFilesMap = findMdxFilesMap(docsDir, docsDir);
|
|
69
|
+
const searchContent: Array<Record<string, any>> = [];
|
|
70
|
+
|
|
71
|
+
for (const fm of frontmatterArray) {
|
|
72
|
+
const fileEntry = Object.entries(mdxFilesMap).find(([, filePath]) =>
|
|
73
|
+
pathFromFilename(filePath, docsDir) === fm.path
|
|
74
|
+
);
|
|
75
|
+
if (!fileEntry) continue;
|
|
76
|
+
|
|
77
|
+
const rawContent = fs.readFileSync(fileEntry[1], 'utf-8');
|
|
78
|
+
const { content } = parseFrontmatter(rawContent);
|
|
79
|
+
searchContent.push({ path: fm.path, title: fm.title || '', content: stripMdxToText(content) });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return searchContent;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build plugin that emits frontmatter.json and search-content.json.
|
|
87
|
+
*/
|
|
145
88
|
export function frontmatterGenerator(options: FrontmatterGeneratorOptions = {}): Plugin {
|
|
146
89
|
return {
|
|
147
90
|
name: 'frontmatter-generator',
|
|
@@ -156,9 +99,19 @@ export function frontmatterGenerator(options: FrontmatterGeneratorOptions = {}):
|
|
|
156
99
|
source: jsonContent
|
|
157
100
|
});
|
|
158
101
|
|
|
102
|
+
// Also generate search content for client-side indexing
|
|
103
|
+
const searchContent = generateSearchContentJson(docsDir, frontmatterArray);
|
|
104
|
+
const searchJson = JSON.stringify(searchContent);
|
|
105
|
+
|
|
106
|
+
this.emitFile({
|
|
107
|
+
type: 'asset',
|
|
108
|
+
fileName: 'search-content.json',
|
|
109
|
+
source: searchJson
|
|
110
|
+
});
|
|
111
|
+
|
|
159
112
|
console.log(`✅ Generated frontmatter JSON file`);
|
|
160
113
|
console.log(`📄 Processed ${frontmatterArray.length} MDX files`);
|
|
161
|
-
console.log(
|
|
114
|
+
console.log(`🔍 Generated search content (${searchContent.length} pages, ${(searchJson.length / 1024).toFixed(0)} KB)`);
|
|
162
115
|
}
|
|
163
116
|
};
|
|
164
|
-
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { isPathInside } from './html-files-middleware';
|
|
4
|
+
|
|
5
|
+
describe('isPathInside', () => {
|
|
6
|
+
const root = resolve('/srv/app/dist');
|
|
7
|
+
|
|
8
|
+
it('accepts the root itself and children', () => {
|
|
9
|
+
expect(isPathInside(root, root)).toBe(true);
|
|
10
|
+
expect(isPathInside(root, resolve(root, 'assets/app.js'))).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('rejects traversal outside the root', () => {
|
|
14
|
+
expect(isPathInside(root, resolve(root, '../../../etc/passwd'))).toBe(false);
|
|
15
|
+
expect(isPathInside(root, resolve('/etc/passwd'))).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('rejects sibling directories sharing the root as a string prefix', () => {
|
|
19
|
+
expect(isPathInside(root, resolve('/srv/app/dist-other/file'))).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
2
|
import { readFileSync, existsSync, statSync } from 'fs';
|
|
3
|
-
import { resolve, extname } from 'path';
|
|
3
|
+
import { resolve, extname, sep } from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns true if `target` is the `root` path itself or is contained within it.
|
|
7
|
+
* Both paths must already be resolved (absolute, normalized).
|
|
8
|
+
*/
|
|
9
|
+
export function isPathInside(root: string, target: string): boolean {
|
|
10
|
+
return target === root || target.startsWith(root + sep);
|
|
11
|
+
}
|
|
4
12
|
|
|
5
13
|
/**
|
|
6
14
|
* Vite plugin to serve HTML files and assets directly from the docs directory
|
|
@@ -53,17 +61,24 @@ export function htmlFilesMiddleware(): Plugin {
|
|
|
53
61
|
|
|
54
62
|
// Check if the request is for a file in the dist directory
|
|
55
63
|
if (url.startsWith('/dist/')) {
|
|
64
|
+
const distRoot = resolve(process.cwd(), 'dist');
|
|
56
65
|
const distPath = resolve(process.cwd(), url.slice(1));
|
|
57
66
|
|
|
67
|
+
// Reject paths that escape the dist directory (e.g. /dist/../../etc/passwd)
|
|
68
|
+
if (!isPathInside(distRoot, distPath)) {
|
|
69
|
+
res.statusCode = 403;
|
|
70
|
+
res.end('Forbidden');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
58
74
|
if (existsSync(distPath)) {
|
|
59
75
|
try {
|
|
60
|
-
const content = readFileSync(distPath
|
|
76
|
+
const content = readFileSync(distPath);
|
|
61
77
|
const ext = extname(url).toLowerCase();
|
|
62
78
|
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
63
79
|
|
|
64
80
|
res.setHeader('Content-Type', contentType);
|
|
65
81
|
res.setHeader('Cache-Control', 'no-cache');
|
|
66
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
67
82
|
res.end(content);
|
|
68
83
|
return;
|
|
69
84
|
} catch (error) {
|
|
@@ -74,7 +89,15 @@ export function htmlFilesMiddleware(): Plugin {
|
|
|
74
89
|
|
|
75
90
|
// Check if the request is for a file in the docs directory
|
|
76
91
|
// This handles both direct file requests and directory requests
|
|
77
|
-
const
|
|
92
|
+
const docsRoot = resolve(process.cwd(), 'docs');
|
|
93
|
+
const docsPath = resolve(docsRoot, url.slice(1));
|
|
94
|
+
|
|
95
|
+
// Reject paths that escape the docs directory
|
|
96
|
+
if (!isPathInside(docsRoot, docsPath)) {
|
|
97
|
+
res.statusCode = 403;
|
|
98
|
+
res.end('Forbidden');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
78
101
|
|
|
79
102
|
if (existsSync(docsPath)) {
|
|
80
103
|
try {
|
|
@@ -91,7 +114,6 @@ export function htmlFilesMiddleware(): Plugin {
|
|
|
91
114
|
|
|
92
115
|
res.setHeader('Content-Type', contentType);
|
|
93
116
|
res.setHeader('Cache-Control', 'no-cache');
|
|
94
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
95
117
|
res.end(content);
|
|
96
118
|
return;
|
|
97
119
|
}
|