veslx 0.1.5 → 0.1.15
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 +262 -55
- package/bin/lib/build.ts +65 -13
- package/bin/lib/import-config.ts +10 -9
- package/bin/lib/init.ts +21 -22
- package/bin/lib/serve.ts +66 -12
- package/bin/veslx.ts +2 -2
- package/dist/client/App.js +3 -9
- package/dist/client/App.js.map +1 -1
- package/dist/client/components/front-matter.js +11 -25
- package/dist/client/components/front-matter.js.map +1 -1
- package/dist/client/components/gallery/components/figure-caption.js +6 -4
- package/dist/client/components/gallery/components/figure-caption.js.map +1 -1
- package/dist/client/components/gallery/components/figure-header.js +3 -3
- package/dist/client/components/gallery/components/figure-header.js.map +1 -1
- package/dist/client/components/gallery/components/lightbox.js +13 -13
- package/dist/client/components/gallery/components/lightbox.js.map +1 -1
- package/dist/client/components/gallery/components/loading-image.js +11 -10
- package/dist/client/components/gallery/components/loading-image.js.map +1 -1
- package/dist/client/components/gallery/hooks/use-gallery-images.js +31 -7
- package/dist/client/components/gallery/hooks/use-gallery-images.js.map +1 -1
- package/dist/client/components/gallery/index.js +22 -15
- package/dist/client/components/gallery/index.js.map +1 -1
- package/dist/client/components/header.js +5 -3
- package/dist/client/components/header.js.map +1 -1
- package/dist/client/components/mdx-components.js +42 -8
- package/dist/client/components/mdx-components.js.map +1 -1
- package/dist/client/components/post-list.js +97 -90
- package/dist/client/components/post-list.js.map +1 -1
- package/dist/client/components/running-bar.js +1 -1
- package/dist/client/components/running-bar.js.map +1 -1
- package/dist/client/components/slide.js +18 -0
- package/dist/client/components/slide.js.map +1 -0
- package/dist/client/components/slides-renderer.js +7 -71
- package/dist/client/components/slides-renderer.js.map +1 -1
- package/dist/client/hooks/use-mdx-content.js +55 -9
- package/dist/client/hooks/use-mdx-content.js.map +1 -1
- package/dist/client/main.js +1 -0
- package/dist/client/main.js.map +1 -1
- package/dist/client/pages/content-router.js +19 -0
- package/dist/client/pages/content-router.js.map +1 -0
- package/dist/client/pages/home.js +11 -7
- package/dist/client/pages/home.js.map +1 -1
- package/dist/client/pages/post.js +8 -20
- package/dist/client/pages/post.js.map +1 -1
- package/dist/client/pages/slides.js +62 -86
- package/dist/client/pages/slides.js.map +1 -1
- package/dist/client/plugin/src/client.js +58 -96
- package/dist/client/plugin/src/client.js.map +1 -1
- package/dist/client/plugin/src/directory-tree.js +111 -0
- package/dist/client/plugin/src/directory-tree.js.map +1 -0
- package/index.html +1 -1
- package/package.json +34 -15
- package/plugin/src/client.tsx +64 -116
- package/plugin/src/directory-tree.ts +171 -0
- package/plugin/src/lib.ts +6 -249
- package/plugin/src/plugin.ts +93 -50
- package/plugin/src/remark-slides.ts +100 -0
- package/plugin/src/types.ts +22 -0
- package/src/App.tsx +3 -6
- package/src/components/front-matter.tsx +14 -29
- package/src/components/gallery/components/figure-caption.tsx +15 -7
- package/src/components/gallery/components/figure-header.tsx +3 -3
- package/src/components/gallery/components/lightbox.tsx +15 -13
- package/src/components/gallery/components/loading-image.tsx +15 -12
- package/src/components/gallery/hooks/use-gallery-images.ts +51 -10
- package/src/components/gallery/index.tsx +32 -26
- package/src/components/header.tsx +14 -9
- package/src/components/mdx-components.tsx +61 -8
- package/src/components/post-list.tsx +149 -115
- package/src/components/running-bar.tsx +1 -1
- package/src/components/slide.tsx +22 -5
- package/src/components/slides-renderer.tsx +7 -115
- package/src/components/welcome.tsx +11 -14
- package/src/hooks/use-mdx-content.ts +94 -9
- package/src/index.css +159 -0
- package/src/main.tsx +1 -0
- package/src/pages/content-router.tsx +27 -0
- package/src/pages/home.tsx +16 -2
- package/src/pages/post.tsx +10 -13
- package/src/pages/slides.tsx +75 -88
- package/src/vite-env.d.ts +7 -17
- package/vite.config.ts +25 -6
- package/dist/assets/README-NSyLDlyP.js +0 -7
- package/dist/assets/SLIDES-C12TOqNU.js +0 -10
- package/dist/assets/_virtual_content-modules-DK3Yb9K2.js +0 -2
- package/dist/assets/index-BUMwRZ7d.js +0 -468
- package/dist/assets/index-C8sJQuOZ.js +0 -1
- package/dist/assets/index-PspMxLnH.css +0 -1
- package/dist/index.html +0 -18
- package/dist/logo_dark.png +0 -0
- package/dist/logo_light.png +0 -0
- package/dist/raw/.veslx.json +0 -61
- package/dist/raw/README.md +0 -33
- package/dist/raw/test-post/Chart.tsx +0 -16
- package/dist/raw/test-post/README.mdx +0 -21
- package/dist/raw/test-slides/Counter.tsx +0 -25
- package/dist/raw/test-slides/SLIDES.mdx +0 -27
package/plugin/src/client.tsx
CHANGED
|
@@ -1,53 +1,54 @@
|
|
|
1
|
-
import { useState, useEffect } from "react";
|
|
1
|
+
import { useState, useEffect, useMemo } from "react";
|
|
2
2
|
import { DirectoryEntry, FileEntry } from "./lib";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// Otherwise, look for a directory
|
|
27
|
-
const nextDir = currentDir.children.find(
|
|
28
|
-
(child) => child.type === "directory" && child.name === part
|
|
29
|
-
) as DirectoryEntry | undefined;
|
|
30
|
-
|
|
31
|
-
if (!nextDir) {
|
|
32
|
-
throw new Error(`Path not found: ${path}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
currentDir = nextDir;
|
|
3
|
+
import { buildDirectoryTree, navigateToPath } from "./directory-tree";
|
|
4
|
+
// @ts-ignore - virtual module
|
|
5
|
+
import { files, frontmatters } from "virtual:content-modules";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Find the main content file for a directory.
|
|
9
|
+
* Supports (in order of preference):
|
|
10
|
+
* - index.mdx / index.md (modern convention)
|
|
11
|
+
* - README.mdx / README.md (traditional convention)
|
|
12
|
+
*/
|
|
13
|
+
export function findReadme(directory: DirectoryEntry): FileEntry | null {
|
|
14
|
+
const indexFiles = [
|
|
15
|
+
"index.mdx", "index.md",
|
|
16
|
+
"README.mdx", "Readme.mdx", "readme.mdx",
|
|
17
|
+
"README.md", "Readme.md", "readme.md",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
for (const filename of indexFiles) {
|
|
21
|
+
const found = directory.children.find((child) =>
|
|
22
|
+
child.type === "file" && child.name === filename
|
|
23
|
+
) as FileEntry | undefined;
|
|
24
|
+
if (found) return found;
|
|
36
25
|
}
|
|
37
26
|
|
|
38
|
-
return
|
|
27
|
+
return null;
|
|
39
28
|
}
|
|
40
29
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Find all MDX files in a directory (excluding index/README and slides)
|
|
32
|
+
*/
|
|
33
|
+
export function findMdxFiles(directory: DirectoryEntry): FileEntry[] {
|
|
34
|
+
const indexFiles = [
|
|
35
|
+
"index.mdx", "index.md",
|
|
36
|
+
"README.mdx", "Readme.mdx", "readme.mdx",
|
|
37
|
+
"README.md", "Readme.md", "readme.md",
|
|
38
|
+
];
|
|
39
|
+
const slideFiles = [
|
|
40
|
+
"SLIDES.mdx", "Slides.mdx", "slides.mdx",
|
|
41
|
+
"SLIDES.md", "Slides.md", "slides.md",
|
|
42
|
+
];
|
|
43
|
+
const excludeFiles = [...indexFiles, ...slideFiles];
|
|
44
|
+
|
|
45
|
+
return directory.children.filter((child): child is FileEntry =>
|
|
46
|
+
child.type === "file" &&
|
|
47
|
+
(child.name.endsWith('.mdx') || child.name.endsWith('.md')) &&
|
|
48
|
+
!excludeFiles.includes(child.name) &&
|
|
49
|
+
!child.name.endsWith('.slides.mdx') &&
|
|
50
|
+
!child.name.endsWith('.slides.md')
|
|
51
|
+
);
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
export function findSlides(directory: DirectoryEntry): FileEntry | null {
|
|
@@ -64,83 +65,30 @@ export function findSlides(directory: DirectoryEntry): FileEntry | null {
|
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
export type DirectoryError =
|
|
67
|
-
| { type: '
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
| { type: 'path_not_found'; message: string; status: 404 };
|
|
69
|
+
|
|
70
|
+
// Build directory tree once from glob keys, with frontmatter metadata
|
|
71
|
+
const directoryTree = buildDirectoryTree(Object.keys(files), frontmatters as Record<string, FileEntry['frontmatter']>);
|
|
71
72
|
|
|
72
73
|
export function useDirectory(path: string = ".") {
|
|
73
|
-
const [directory, setDirectory] = useState<DirectoryEntry | null>(null);
|
|
74
|
-
const [file, setFile] = useState<FileEntry | null>(null);
|
|
75
|
-
const [loading, setLoading] = useState(true);
|
|
76
74
|
const [error, setError] = useState<DirectoryError | null>(null);
|
|
77
75
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (!response.ok) {
|
|
87
|
-
if (response.status === 404) {
|
|
88
|
-
setError({ type: 'config_not_found', message: '.veslx.json not found' });
|
|
89
|
-
} else {
|
|
90
|
-
setError({ type: 'fetch_error', message: `Failed to fetch: ${response.status} ${response.statusText}` });
|
|
91
|
-
}
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
let json;
|
|
96
|
-
try {
|
|
97
|
-
json = await response.json();
|
|
98
|
-
} catch {
|
|
99
|
-
setError({ type: 'parse_error', message: 'Failed to parse .veslx.json' });
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
let parsed: { directory: DirectoryEntry; file: FileEntry | null };
|
|
104
|
-
try {
|
|
105
|
-
parsed = await parsePath(json, path);
|
|
106
|
-
} catch {
|
|
107
|
-
setError({ type: 'path_not_found', message: `Path not found: ${path}`, status: 404 });
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
parsed.directory.children.sort((a: any, b: any) => {
|
|
112
|
-
let aDate, bDate;
|
|
113
|
-
if (a.children) {
|
|
114
|
-
const readme = findReadme(a);
|
|
115
|
-
if (readme && readme.frontmatter && readme.frontmatter.date) {
|
|
116
|
-
aDate = new Date(readme.frontmatter.date as string | number | Date)
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (b.children) {
|
|
120
|
-
const readme = findReadme(b);
|
|
121
|
-
if (readme && readme.frontmatter && readme.frontmatter.date) {
|
|
122
|
-
bDate = new Date(readme.frontmatter.date as string | number | Date)
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (aDate && bDate) {
|
|
126
|
-
return bDate.getTime() - aDate.getTime()
|
|
127
|
-
}
|
|
128
|
-
return 0;
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
setDirectory(parsed.directory);
|
|
132
|
-
setFile(parsed.file);
|
|
133
|
-
} catch (err: any) {
|
|
134
|
-
setError({ type: 'fetch_error', message: err.message || 'Unknown error' });
|
|
135
|
-
} finally {
|
|
136
|
-
setLoading(false);
|
|
137
|
-
}
|
|
138
|
-
})();
|
|
139
|
-
|
|
140
|
-
return () => {};
|
|
76
|
+
const result = useMemo(() => {
|
|
77
|
+
try {
|
|
78
|
+
const { directory, file } = navigateToPath(directoryTree, path);
|
|
79
|
+
return { directory, file };
|
|
80
|
+
} catch {
|
|
81
|
+
setError({ type: 'path_not_found', message: `Path not found: ${path}`, status: 404 });
|
|
82
|
+
return { directory: null, file: null };
|
|
83
|
+
}
|
|
141
84
|
}, [path]);
|
|
142
85
|
|
|
143
|
-
return {
|
|
86
|
+
return {
|
|
87
|
+
directory: result.directory,
|
|
88
|
+
file: result.file,
|
|
89
|
+
loading: false, // No async loading needed
|
|
90
|
+
error
|
|
91
|
+
};
|
|
144
92
|
}
|
|
145
93
|
|
|
146
94
|
export function useFileContent(path: string) {
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { DirectoryEntry, FileEntry } from './lib';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Find the common directory prefix among all paths.
|
|
5
|
+
* E.g., ["/docs/a.mdx", "/docs/b/c.mdx"] -> "/docs"
|
|
6
|
+
* Only considers directory segments, not filenames.
|
|
7
|
+
*/
|
|
8
|
+
function findCommonPrefix(paths: string[]): string {
|
|
9
|
+
if (paths.length === 0) return '';
|
|
10
|
+
|
|
11
|
+
// Extract directory parts only (exclude filename from each path)
|
|
12
|
+
const dirPaths = paths.map(p => {
|
|
13
|
+
const parts = p.split('/').filter(Boolean);
|
|
14
|
+
// Remove the last part (filename)
|
|
15
|
+
return parts.slice(0, -1);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (dirPaths.length === 0 || dirPaths[0].length === 0) return '';
|
|
19
|
+
|
|
20
|
+
// Find how many directory segments are common to all paths
|
|
21
|
+
const firstDirParts = dirPaths[0];
|
|
22
|
+
let commonLength = 0;
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < firstDirParts.length; i++) {
|
|
25
|
+
const segment = firstDirParts[i];
|
|
26
|
+
const allMatch = dirPaths.every(parts => parts[i] === segment);
|
|
27
|
+
if (allMatch) {
|
|
28
|
+
commonLength = i + 1;
|
|
29
|
+
} else {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (commonLength > 0) {
|
|
35
|
+
return '/' + firstDirParts.slice(0, commonLength).join('/');
|
|
36
|
+
}
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build a directory tree from glob keys.
|
|
42
|
+
* Keys are paths like "/docs/file.mdx" (Vite-resolved from @content alias)
|
|
43
|
+
* We auto-detect and strip the common prefix (content directory).
|
|
44
|
+
* @param globKeys - Array of file paths from import.meta.glob
|
|
45
|
+
* @param frontmatters - Optional map of paths to frontmatter objects
|
|
46
|
+
*/
|
|
47
|
+
export function buildDirectoryTree(
|
|
48
|
+
globKeys: string[],
|
|
49
|
+
frontmatters?: Record<string, FileEntry['frontmatter']>
|
|
50
|
+
): DirectoryEntry {
|
|
51
|
+
const root: DirectoryEntry = {
|
|
52
|
+
type: 'directory',
|
|
53
|
+
name: '.',
|
|
54
|
+
path: '.',
|
|
55
|
+
children: []
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (globKeys.length === 0) return root;
|
|
59
|
+
|
|
60
|
+
// Auto-detect the content directory prefix
|
|
61
|
+
// Vite resolves @content to the actual path, so keys look like "/docs/file.mdx"
|
|
62
|
+
const commonPrefix = findCommonPrefix(globKeys);
|
|
63
|
+
|
|
64
|
+
for (const key of globKeys) {
|
|
65
|
+
// Strip the common prefix to get path relative to content root
|
|
66
|
+
let relativePath = key;
|
|
67
|
+
if (commonPrefix && key.startsWith(commonPrefix)) {
|
|
68
|
+
relativePath = key.slice(commonPrefix.length);
|
|
69
|
+
}
|
|
70
|
+
// Remove leading slash if present
|
|
71
|
+
if (relativePath.startsWith('/')) {
|
|
72
|
+
relativePath = relativePath.slice(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Skip hidden files and directories
|
|
76
|
+
if (relativePath.split('/').some(part => part.startsWith('.'))) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const parts = relativePath.split('/').filter(Boolean);
|
|
81
|
+
if (parts.length === 0) continue;
|
|
82
|
+
|
|
83
|
+
let current = root;
|
|
84
|
+
|
|
85
|
+
// Navigate/create directories for all but the last part
|
|
86
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
87
|
+
const dirName = parts[i];
|
|
88
|
+
let dir = current.children.find(
|
|
89
|
+
c => c.type === 'directory' && c.name === dirName
|
|
90
|
+
) as DirectoryEntry | undefined;
|
|
91
|
+
|
|
92
|
+
if (!dir) {
|
|
93
|
+
dir = {
|
|
94
|
+
type: 'directory',
|
|
95
|
+
name: dirName,
|
|
96
|
+
path: parts.slice(0, i + 1).join('/'),
|
|
97
|
+
children: []
|
|
98
|
+
};
|
|
99
|
+
current.children.push(dir);
|
|
100
|
+
}
|
|
101
|
+
current = dir;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Add file entry (last part)
|
|
105
|
+
const filename = parts[parts.length - 1];
|
|
106
|
+
|
|
107
|
+
// Don't add duplicates
|
|
108
|
+
const exists = current.children.some(
|
|
109
|
+
c => c.type === 'file' && c.name === filename
|
|
110
|
+
);
|
|
111
|
+
if (exists) continue;
|
|
112
|
+
|
|
113
|
+
// Look up frontmatter using the original key (before prefix stripping)
|
|
114
|
+
const frontmatter = frontmatters?.[key];
|
|
115
|
+
|
|
116
|
+
const fileEntry: FileEntry = {
|
|
117
|
+
type: 'file',
|
|
118
|
+
name: filename,
|
|
119
|
+
path: relativePath,
|
|
120
|
+
size: 0, // Size not available from glob keys
|
|
121
|
+
frontmatter
|
|
122
|
+
};
|
|
123
|
+
current.children.push(fileEntry);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return root;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Navigate to a path within the directory tree.
|
|
131
|
+
* Returns the directory and optionally a file if the path points to one.
|
|
132
|
+
*/
|
|
133
|
+
export function navigateToPath(
|
|
134
|
+
root: DirectoryEntry,
|
|
135
|
+
path: string
|
|
136
|
+
): { directory: DirectoryEntry; file: FileEntry | null } {
|
|
137
|
+
const parts = path === '.' || path === '' ? [] : path.split('/').filter(Boolean);
|
|
138
|
+
|
|
139
|
+
let currentDir = root;
|
|
140
|
+
let file: FileEntry | null = null;
|
|
141
|
+
|
|
142
|
+
for (let i = 0; i < parts.length; i++) {
|
|
143
|
+
const part = parts[i];
|
|
144
|
+
const isLastPart = i === parts.length - 1;
|
|
145
|
+
|
|
146
|
+
// Check if this part matches a file (only on last part)
|
|
147
|
+
if (isLastPart) {
|
|
148
|
+
const matchedFile = currentDir.children.find(
|
|
149
|
+
child => child.type === 'file' && child.name === part
|
|
150
|
+
) as FileEntry | undefined;
|
|
151
|
+
|
|
152
|
+
if (matchedFile) {
|
|
153
|
+
file = matchedFile;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Otherwise, look for a directory
|
|
159
|
+
const nextDir = currentDir.children.find(
|
|
160
|
+
child => child.type === 'directory' && child.name === part
|
|
161
|
+
) as DirectoryEntry | undefined;
|
|
162
|
+
|
|
163
|
+
if (!nextDir) {
|
|
164
|
+
throw new Error(`Path not found: ${path}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
currentDir = nextDir;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { directory: currentDir, file };
|
|
171
|
+
}
|
package/plugin/src/lib.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export const GITIGNORE_FILENAME = '.gitignore'
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for directory tree structure.
|
|
3
|
+
* Used by the Gallery component and useDirectory hook.
|
|
4
|
+
*/
|
|
6
5
|
|
|
7
6
|
export type FileEntry = {
|
|
8
7
|
type: 'file';
|
|
@@ -13,6 +12,8 @@ export type FileEntry = {
|
|
|
13
12
|
title?: string;
|
|
14
13
|
description?: string;
|
|
15
14
|
date?: string;
|
|
15
|
+
draft?: boolean;
|
|
16
|
+
visibility?: string;
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -22,247 +23,3 @@ export type DirectoryEntry = {
|
|
|
22
23
|
path: string;
|
|
23
24
|
children: (FileEntry | DirectoryEntry)[];
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
-
const MARKDOWN_EXTENSIONS = ['.md', '.mdx', '.markdown'];
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Check if a file is a markdown file
|
|
30
|
-
*/
|
|
31
|
-
function isMarkdownFile(filename: string): boolean {
|
|
32
|
-
const ext = extname(filename).toLowerCase();
|
|
33
|
-
return MARKDOWN_EXTENSIONS.includes(ext);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Parse frontmatter from a markdown file
|
|
38
|
-
* Returns undefined if no frontmatter exists or file is not markdown
|
|
39
|
-
*/
|
|
40
|
-
async function parseFrontmatter(filePath: string): Promise<Record<string, unknown> | undefined> {
|
|
41
|
-
try {
|
|
42
|
-
const content = await readFile(filePath, 'utf-8');
|
|
43
|
-
const { data } = matter(content);
|
|
44
|
-
// Only return frontmatter if it has content
|
|
45
|
-
if (Object.keys(data).length > 0) {
|
|
46
|
-
return data;
|
|
47
|
-
}
|
|
48
|
-
return undefined;
|
|
49
|
-
} catch {
|
|
50
|
-
return undefined;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Check if a path is a directory
|
|
56
|
-
*/
|
|
57
|
-
async function isDirectory(path: string): Promise<boolean> {
|
|
58
|
-
try {
|
|
59
|
-
const stats = await stat(path);
|
|
60
|
-
return stats.isDirectory();
|
|
61
|
-
} catch {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Parse .gitignore file and return array of patterns
|
|
68
|
-
*/
|
|
69
|
-
async function parseGitignore(gitignorePath: string): Promise<string[]> {
|
|
70
|
-
try {
|
|
71
|
-
const content = await readFile(gitignorePath, 'utf-8');
|
|
72
|
-
return content
|
|
73
|
-
.split('\n')
|
|
74
|
-
.map(line => line.trim())
|
|
75
|
-
.filter(line => line && !line.startsWith('#')); // Skip empty lines and comments
|
|
76
|
-
} catch {
|
|
77
|
-
return []; // Return empty array if .gitignore doesn't exist
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Convert gitignore pattern to regex-like matching function
|
|
83
|
-
* Supports:
|
|
84
|
-
* - *.ext (any file ending with .ext)
|
|
85
|
-
* - dir/ (directory anywhere in tree)
|
|
86
|
-
* - dir/** (directory and all contents)
|
|
87
|
-
* - **\/pattern (pattern anywhere)
|
|
88
|
-
*/
|
|
89
|
-
function createGitignoreMatcher(patterns: string[]): (relativePath: string, isDir: boolean) => boolean {
|
|
90
|
-
return (relativePath: string, isDir: boolean) => {
|
|
91
|
-
const pathParts = relativePath.split('/');
|
|
92
|
-
const filename = basename(relativePath);
|
|
93
|
-
|
|
94
|
-
for (const pattern of patterns) {
|
|
95
|
-
// Handle directory patterns (ending with /)
|
|
96
|
-
if (pattern.endsWith('/')) {
|
|
97
|
-
const dirPattern = pattern.slice(0, -1);
|
|
98
|
-
if (isDir) {
|
|
99
|
-
// Match if any part of the path matches the directory name
|
|
100
|
-
if (pathParts.includes(dirPattern) || relativePath === dirPattern || relativePath.startsWith(dirPattern + '/')) {
|
|
101
|
-
return true;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Handle ** patterns
|
|
108
|
-
if (pattern.includes('**')) {
|
|
109
|
-
const regexPattern = pattern
|
|
110
|
-
.replace(/\./g, '\\.')
|
|
111
|
-
.replace(/\*\*/g, '.*')
|
|
112
|
-
.replace(/\*/g, '[^/]*');
|
|
113
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
114
|
-
if (regex.test(relativePath)) {
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Handle simple wildcard patterns (*.ext)
|
|
121
|
-
if (pattern.includes('*')) {
|
|
122
|
-
const regexPattern = pattern
|
|
123
|
-
.replace(/\./g, '\\.')
|
|
124
|
-
.replace(/\*/g, '[^/]*');
|
|
125
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
126
|
-
|
|
127
|
-
// Check if the pattern matches the full path or just the filename
|
|
128
|
-
if (regex.test(relativePath) || regex.test(filename)) {
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Exact match - check both full path and filename
|
|
135
|
-
if (relativePath === pattern || filename === pattern) {
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Prefix match for directories
|
|
140
|
-
if (relativePath.startsWith(pattern + '/')) {
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return false;
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Recursively scan directory and build flat index
|
|
151
|
-
*/
|
|
152
|
-
async function scanDirectory(
|
|
153
|
-
dirPath: string,
|
|
154
|
-
rootPath: string,
|
|
155
|
-
shouldIgnore: (relativePath: string, isDir: boolean) => boolean,
|
|
156
|
-
depth: number = 0
|
|
157
|
-
): Promise<DirectoryEntry> {
|
|
158
|
-
const entries = await readdir(dirPath);
|
|
159
|
-
const children: (FileEntry | DirectoryEntry)[] = [];
|
|
160
|
-
|
|
161
|
-
for (const entry of entries) {
|
|
162
|
-
// Skip hidden files and the index file itself
|
|
163
|
-
if (entry.startsWith('.')) {
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const fullPath = join(dirPath, entry);
|
|
168
|
-
const relativePath = relative(rootPath, fullPath);
|
|
169
|
-
const isDir = await isDirectory(fullPath);
|
|
170
|
-
|
|
171
|
-
// Check if this path should be ignored
|
|
172
|
-
if (shouldIgnore(relativePath, isDir)) {
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (isDir) {
|
|
177
|
-
// Recursively scan subdirectories
|
|
178
|
-
const subDir = await scanDirectory(fullPath, rootPath, shouldIgnore, depth + 1);
|
|
179
|
-
children.push(subDir);
|
|
180
|
-
} else {
|
|
181
|
-
// Add file entry
|
|
182
|
-
const stats = await stat(fullPath);
|
|
183
|
-
const fileEntry: FileEntry = {
|
|
184
|
-
type: 'file',
|
|
185
|
-
name: entry,
|
|
186
|
-
path: relativePath,
|
|
187
|
-
size: stats.size,
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
// Parse frontmatter for markdown files
|
|
191
|
-
if (isMarkdownFile(entry)) {
|
|
192
|
-
const frontmatter = await parseFrontmatter(fullPath);
|
|
193
|
-
if (frontmatter) {
|
|
194
|
-
fileEntry.frontmatter = frontmatter;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
children.push(fileEntry);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
type: 'directory',
|
|
204
|
-
name: basename(dirPath),
|
|
205
|
-
path: relative(rootPath, dirPath) || '.',
|
|
206
|
-
children,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Build content for a single target
|
|
212
|
-
*/
|
|
213
|
-
async function buildTarget(target: string): Promise<void> {
|
|
214
|
-
console.log(`${'-'.repeat(80)}`);
|
|
215
|
-
console.log(`Building: ${target}`);
|
|
216
|
-
console.log(`${'-'.repeat(80)}`);
|
|
217
|
-
|
|
218
|
-
const gitignorePath = join(target, GITIGNORE_FILENAME);
|
|
219
|
-
const ignorePatterns = await parseGitignore(gitignorePath);
|
|
220
|
-
const shouldIgnore = createGitignoreMatcher(ignorePatterns);
|
|
221
|
-
|
|
222
|
-
if (ignorePatterns.length > 0) {
|
|
223
|
-
console.log(` Found .gitignore with ${ignorePatterns.length} pattern${ignorePatterns.length !== 1 ? 's' : ''}`);
|
|
224
|
-
} else {
|
|
225
|
-
console.log(` No .gitignore found or empty\n`);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const index = await scanDirectory(target, target, shouldIgnore);
|
|
229
|
-
|
|
230
|
-
// Count files and directories
|
|
231
|
-
function countEntries(entry: DirectoryEntry): { files: number; dirs: number } {
|
|
232
|
-
let files = 0;
|
|
233
|
-
let dirs = 0;
|
|
234
|
-
|
|
235
|
-
for (const child of entry.children) {
|
|
236
|
-
if (child.type === 'file') {
|
|
237
|
-
files++;
|
|
238
|
-
} else {
|
|
239
|
-
dirs++;
|
|
240
|
-
const counts = countEntries(child);
|
|
241
|
-
files += counts.files;
|
|
242
|
-
dirs += counts.dirs;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return { files, dirs };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const counts = countEntries(index);
|
|
250
|
-
console.log(` Found ${counts.dirs} directories and ${counts.files} files`);
|
|
251
|
-
|
|
252
|
-
// Write output
|
|
253
|
-
const outputFile = join(target, '.veslx.json');
|
|
254
|
-
await writeFile(outputFile, JSON.stringify(index, null, 2));
|
|
255
|
-
|
|
256
|
-
console.log(` Generated ${outputFile}`);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Build content for all configured targets
|
|
261
|
-
*/
|
|
262
|
-
async function buildAll(targets: string[]): Promise<void> {
|
|
263
|
-
for (const target of targets) {
|
|
264
|
-
await buildTarget(target);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export { buildAll };
|