veslx 0.1.14 → 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.
Files changed (82) hide show
  1. package/README.md +262 -55
  2. package/bin/lib/build.ts +65 -13
  3. package/bin/lib/import-config.ts +10 -9
  4. package/bin/lib/init.ts +21 -22
  5. package/bin/lib/serve.ts +66 -12
  6. package/bin/veslx.ts +2 -2
  7. package/dist/client/App.js +3 -9
  8. package/dist/client/App.js.map +1 -1
  9. package/dist/client/components/front-matter.js +11 -25
  10. package/dist/client/components/front-matter.js.map +1 -1
  11. package/dist/client/components/gallery/components/figure-caption.js +6 -4
  12. package/dist/client/components/gallery/components/figure-caption.js.map +1 -1
  13. package/dist/client/components/gallery/components/figure-header.js +3 -3
  14. package/dist/client/components/gallery/components/figure-header.js.map +1 -1
  15. package/dist/client/components/gallery/components/lightbox.js +13 -13
  16. package/dist/client/components/gallery/components/lightbox.js.map +1 -1
  17. package/dist/client/components/gallery/components/loading-image.js +11 -10
  18. package/dist/client/components/gallery/components/loading-image.js.map +1 -1
  19. package/dist/client/components/gallery/hooks/use-gallery-images.js +31 -7
  20. package/dist/client/components/gallery/hooks/use-gallery-images.js.map +1 -1
  21. package/dist/client/components/gallery/index.js +22 -15
  22. package/dist/client/components/gallery/index.js.map +1 -1
  23. package/dist/client/components/header.js +5 -3
  24. package/dist/client/components/header.js.map +1 -1
  25. package/dist/client/components/mdx-components.js +42 -8
  26. package/dist/client/components/mdx-components.js.map +1 -1
  27. package/dist/client/components/post-list.js +97 -90
  28. package/dist/client/components/post-list.js.map +1 -1
  29. package/dist/client/components/running-bar.js +1 -1
  30. package/dist/client/components/running-bar.js.map +1 -1
  31. package/dist/client/components/slide.js +18 -0
  32. package/dist/client/components/slide.js.map +1 -0
  33. package/dist/client/components/slides-renderer.js +7 -71
  34. package/dist/client/components/slides-renderer.js.map +1 -1
  35. package/dist/client/hooks/use-mdx-content.js +55 -9
  36. package/dist/client/hooks/use-mdx-content.js.map +1 -1
  37. package/dist/client/main.js +1 -0
  38. package/dist/client/main.js.map +1 -1
  39. package/dist/client/pages/content-router.js +19 -0
  40. package/dist/client/pages/content-router.js.map +1 -0
  41. package/dist/client/pages/home.js +11 -7
  42. package/dist/client/pages/home.js.map +1 -1
  43. package/dist/client/pages/post.js +8 -20
  44. package/dist/client/pages/post.js.map +1 -1
  45. package/dist/client/pages/slides.js +62 -86
  46. package/dist/client/pages/slides.js.map +1 -1
  47. package/dist/client/plugin/src/client.js +58 -96
  48. package/dist/client/plugin/src/client.js.map +1 -1
  49. package/dist/client/plugin/src/directory-tree.js +111 -0
  50. package/dist/client/plugin/src/directory-tree.js.map +1 -0
  51. package/index.html +1 -1
  52. package/package.json +27 -15
  53. package/plugin/src/client.tsx +64 -116
  54. package/plugin/src/directory-tree.ts +171 -0
  55. package/plugin/src/lib.ts +6 -249
  56. package/plugin/src/plugin.ts +93 -50
  57. package/plugin/src/remark-slides.ts +100 -0
  58. package/plugin/src/types.ts +22 -0
  59. package/src/App.tsx +3 -6
  60. package/src/components/front-matter.tsx +14 -29
  61. package/src/components/gallery/components/figure-caption.tsx +15 -7
  62. package/src/components/gallery/components/figure-header.tsx +3 -3
  63. package/src/components/gallery/components/lightbox.tsx +15 -13
  64. package/src/components/gallery/components/loading-image.tsx +15 -12
  65. package/src/components/gallery/hooks/use-gallery-images.ts +51 -10
  66. package/src/components/gallery/index.tsx +32 -26
  67. package/src/components/header.tsx +14 -9
  68. package/src/components/mdx-components.tsx +61 -8
  69. package/src/components/post-list.tsx +149 -115
  70. package/src/components/running-bar.tsx +1 -1
  71. package/src/components/slide.tsx +22 -5
  72. package/src/components/slides-renderer.tsx +7 -115
  73. package/src/components/welcome.tsx +11 -14
  74. package/src/hooks/use-mdx-content.ts +94 -9
  75. package/src/index.css +159 -0
  76. package/src/main.tsx +1 -0
  77. package/src/pages/content-router.tsx +27 -0
  78. package/src/pages/home.tsx +16 -2
  79. package/src/pages/post.tsx +10 -13
  80. package/src/pages/slides.tsx +75 -88
  81. package/src/vite-env.d.ts +7 -17
  82. package/vite.config.ts +25 -6
@@ -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
- import { readdir, writeFile, stat, readFile } from 'fs/promises';
2
- import { join, relative, basename, extname } from 'path';
3
- import matter from 'gray-matter';
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 };
@@ -1,13 +1,57 @@
1
1
  import { type Plugin, type Connect } from 'vite'
2
2
  import path from 'path'
3
3
  import fs from 'fs'
4
- import { buildAll } from './lib'
5
- import chokidar, { type FSWatcher } from 'chokidar'
6
4
  import type { IncomingMessage, ServerResponse } from 'http'
5
+ import { type VeslxConfig, type ResolvedSiteConfig, DEFAULT_SITE_CONFIG } from './types'
6
+ import matter from 'gray-matter'
7
+
8
+ /**
9
+ * Extract frontmatter from all MDX files in a directory
10
+ */
11
+ function extractFrontmatters(dir: string): Record<string, { title?: string; description?: string; date?: string }> {
12
+ const frontmatters: Record<string, { title?: string; description?: string; date?: string }> = {};
13
+
14
+ function scanDir(currentDir: string, relativePath: string = '') {
15
+ if (!fs.existsSync(currentDir)) return;
16
+
17
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
18
+ for (const entry of entries) {
19
+ const fullPath = path.join(currentDir, entry.name);
20
+ const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
21
+
22
+ if (entry.isDirectory()) {
23
+ // Skip hidden directories and node_modules
24
+ if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
25
+ scanDir(fullPath, relPath);
26
+ }
27
+ } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {
28
+ try {
29
+ const content = fs.readFileSync(fullPath, 'utf-8');
30
+ const { data } = matter(content);
31
+ // Use @content prefix to match glob keys
32
+ const key = `@content/${relPath}`;
33
+ frontmatters[key] = {
34
+ title: data.title,
35
+ description: data.description,
36
+ date: data.date instanceof Date ? data.date.toISOString() : data.date,
37
+ };
38
+ } catch {
39
+ // Skip files that can't be parsed
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+ scanDir(dir);
46
+ return frontmatters;
47
+ }
7
48
 
8
49
  const VIRTUAL_MODULE_ID = 'virtual:content-modules'
9
50
  const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID
10
51
 
52
+ const VIRTUAL_CONFIG_ID = 'virtual:veslx-config'
53
+ const RESOLVED_VIRTUAL_CONFIG_ID = '\0' + VIRTUAL_CONFIG_ID
54
+
11
55
  /**
12
56
  * Recursively copy a directory
13
57
  */
@@ -27,7 +71,7 @@ function copyDirSync(src: string, dest: string) {
27
71
  }
28
72
  }
29
73
 
30
- export default function contentPlugin(contentDir: string): Plugin {
74
+ export default function contentPlugin(contentDir: string, config?: VeslxConfig): Plugin {
31
75
 
32
76
  if (!contentDir) {
33
77
  throw new Error('Content directory must be specified.')
@@ -39,9 +83,11 @@ export default function contentPlugin(contentDir: string): Plugin {
39
83
 
40
84
  const dir = contentDir
41
85
 
42
- const buildFn = () => buildAll([dir])
43
-
44
- let watchers: FSWatcher[] = []
86
+ // Resolve site config with defaults
87
+ const siteConfig: ResolvedSiteConfig = {
88
+ ...DEFAULT_SITE_CONFIG,
89
+ ...config?.site,
90
+ }
45
91
 
46
92
  // Server middleware for serving content files
47
93
  const urlToDir = new Map<string, string>()
@@ -101,76 +147,73 @@ export default function contentPlugin(contentDir: string): Plugin {
101
147
  },
102
148
  },
103
149
  optimizeDeps: {
104
- exclude: ['virtual:content-modules'],
150
+ exclude: ['virtual:content-modules', 'virtual:veslx-config'],
105
151
  },
106
152
  }
107
153
  },
108
154
 
109
- // Virtual module for content MDX imports
155
+ // Virtual modules for content MDX imports and site config
110
156
  resolveId(id) {
111
157
  if (id === VIRTUAL_MODULE_ID) {
112
158
  return RESOLVED_VIRTUAL_MODULE_ID
113
159
  }
160
+ if (id === VIRTUAL_CONFIG_ID) {
161
+ return RESOLVED_VIRTUAL_CONFIG_ID
162
+ }
114
163
  },
115
164
 
116
165
  load(id) {
117
166
  if (id === RESOLVED_VIRTUAL_MODULE_ID) {
167
+ // Extract frontmatter from MDX files at build time (avoids MDX hook issues)
168
+ const frontmatterData = extractFrontmatters(dir);
169
+
118
170
  // Generate virtual module with import.meta.glob for MDX files
119
171
  return `
120
- export const modules = import.meta.glob('@content/**/README.mdx');
121
- export const slides = import.meta.glob('@content/**/SLIDES.mdx');
122
- export const index = import.meta.glob('@content/.veslx.json', { eager: true });
172
+ export const posts = import.meta.glob('@content/**/*.mdx', {
173
+ import: 'default',
174
+ query: { skipSlides: true }
175
+ });
176
+ export const allMdx = import.meta.glob('@content/**/*.mdx');
177
+ export const slides = import.meta.glob(['@content/**/SLIDES.mdx', '@content/**/*.slides.mdx']);
178
+
179
+ // All files for directory tree building (web-compatible files only)
180
+ export const files = import.meta.glob([
181
+ '@content/**/*.mdx',
182
+ '@content/**/*.md',
183
+ '@content/**/*.tsx',
184
+ '@content/**/*.ts',
185
+ '@content/**/*.jsx',
186
+ '@content/**/*.js',
187
+ '@content/**/*.png',
188
+ '@content/**/*.jpg',
189
+ '@content/**/*.jpeg',
190
+ '@content/**/*.gif',
191
+ '@content/**/*.svg',
192
+ '@content/**/*.webp',
193
+ '@content/**/*.css',
194
+ ], { eager: false });
195
+
196
+ // Frontmatter extracted at build time (no MDX execution required)
197
+ export const frontmatters = ${JSON.stringify(frontmatterData)};
198
+
199
+ // Legacy aliases for backwards compatibility
200
+ export const modules = import.meta.glob('@content/**/*.mdx');
123
201
  `
124
202
  }
203
+ if (id === RESOLVED_VIRTUAL_CONFIG_ID) {
204
+ // Generate virtual module with site config
205
+ return `export default ${JSON.stringify(siteConfig)};`
206
+ }
125
207
  },
126
208
 
127
- async buildStart() {
128
- await buildFn()
129
- },
130
209
  configureServer(server) {
131
210
  // Add middleware for serving content files
132
211
  server.middlewares.use(middleware)
133
-
134
- // Watch all content directories and rebuild on changes
135
- watchers = [dir].map(dir => {
136
- const watcher = chokidar.watch(dir, {
137
- ignored: (path: string) => path.endsWith('.veslx.json'),
138
- persistent: true,
139
- })
140
-
141
- watcher.on('change', async () => {
142
- const runningFilePath = path.join(dir, '.running')
143
- if (!fs.existsSync(runningFilePath)) {
144
- await buildFn()
145
- server.ws.send({
146
- type: 'full-reload',
147
- path: '*',
148
- })
149
- }
150
- })
151
-
152
- watcher.on('unlink', async (filePath) => {
153
- if (path.basename(filePath) === '.running') {
154
- await buildFn()
155
- server.ws.send({
156
- type: 'full-reload',
157
- path: '*',
158
- })
159
- }
160
- })
161
-
162
- return watcher
163
- })
164
-
165
212
  },
166
213
  configurePreviewServer(server) {
167
214
  // Add middleware for preview server too
168
215
  server.middlewares.use(middleware)
169
216
  },
170
- async buildEnd() {
171
- await Promise.all(watchers.map(w => w.close()))
172
- watchers = []
173
- },
174
217
  writeBundle(options) {
175
218
  // Copy content directory to dist/raw during production build
176
219
  const outDir = options.dir || 'dist'