veslx 0.1.50 → 0.1.52

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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -15
  3. package/bin/lib/build.ts +26 -8
  4. package/bin/lib/import-config.ts +7 -6
  5. package/bin/lib/init.ts +3 -2
  6. package/bin/lib/serve.ts +27 -8
  7. package/bin/lib/start.ts +17 -5
  8. package/bin/lib/stop.ts +12 -3
  9. package/bin/veslx.ts +25 -10
  10. package/dist/bin/lib/build.js +121 -0
  11. package/dist/bin/lib/import-config.js +11 -0
  12. package/dist/bin/lib/init.js +24 -0
  13. package/dist/bin/lib/log.js +15 -0
  14. package/dist/bin/lib/serve.js +132 -0
  15. package/dist/bin/lib/start.js +49 -0
  16. package/dist/bin/lib/stop.js +28 -0
  17. package/dist/bin/veslx.js +47 -0
  18. package/dist/client/components/front-matter.js +31 -5
  19. package/dist/client/components/front-matter.js.map +1 -1
  20. package/dist/client/components/gallery/hooks/use-gallery-images.js +10 -12
  21. package/dist/client/components/gallery/hooks/use-gallery-images.js.map +1 -1
  22. package/dist/client/components/header.js +2 -0
  23. package/dist/client/components/header.js.map +1 -1
  24. package/dist/client/components/mdx-components.js +3 -0
  25. package/dist/client/components/mdx-components.js.map +1 -1
  26. package/dist/client/components/post-list-item.js +43 -29
  27. package/dist/client/components/post-list-item.js.map +1 -1
  28. package/dist/client/components/post-list.js +11 -3
  29. package/dist/client/components/post-list.js.map +1 -1
  30. package/dist/client/components/veslx-search.js +119 -0
  31. package/dist/client/components/veslx-search.js.map +1 -0
  32. package/dist/client/hooks/use-mdx-content.js +74 -24
  33. package/dist/client/hooks/use-mdx-content.js.map +1 -1
  34. package/dist/client/lib/frontmatter-context.js.map +1 -1
  35. package/dist/client/plugin/src/client.js +13 -11
  36. package/dist/client/plugin/src/client.js.map +1 -1
  37. package/dist/client/plugin/src/directory-tree.js.map +1 -1
  38. package/dist/client/src/index.css +474 -0
  39. package/dist/plugin/src/client.js +171 -0
  40. package/dist/plugin/src/directory-tree.js +143 -0
  41. package/dist/plugin/src/lib.js +5 -0
  42. package/dist/plugin/src/plugin.js +738 -0
  43. package/dist/plugin/src/remark-slides.js +93 -0
  44. package/dist/plugin/src/types.js +13 -0
  45. package/package.json +21 -12
  46. package/plugin/src/client.tsx +16 -15
  47. package/plugin/src/directory-tree.ts +1 -1
  48. package/plugin/src/lib.ts +1 -0
  49. package/plugin/src/plugin.ts +252 -19
  50. package/plugin/src/remark-slides.ts +1 -1
  51. package/postcss.config.js +5 -0
  52. package/src/components/front-matter.tsx +36 -5
  53. package/src/components/gallery/hooks/use-gallery-images.ts +13 -14
  54. package/src/components/header.tsx +2 -0
  55. package/src/components/mdx-components.tsx +5 -0
  56. package/src/components/post-list-item.tsx +55 -35
  57. package/src/components/post-list.tsx +18 -3
  58. package/src/components/veslx-search.tsx +155 -0
  59. package/src/hooks/use-mdx-content.ts +94 -37
  60. package/src/lib/frontmatter-context.tsx +1 -0
  61. package/tailwind.config.js +130 -0
  62. package/tsconfig.build.json +17 -0
  63. package/vite.lib.config.ts +16 -2
  64. package/bin/lib/export.ts +0 -203
  65. package/plugin/README.md +0 -21
  66. package/plugin/package.json +0 -26
  67. package/plugin/src/cli.ts +0 -30
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Eoin Murray
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -23,7 +23,7 @@
23
23
 
24
24
  ## Why veslx?
25
25
 
26
- **veslx** is a zero-config CLI that transforms your markdown files into a polished documentation site. Write in MDX, import React components, render LaTeX equations, display image galleries, and create slide and pdf presentations—all from simple markdown files.
26
+ **veslx** is a zero-config CLI that transforms your markdown files into a polished documentation site. Write in MDX, import React components, render LaTeX equations, display image galleries, and create slide presentations—all from simple markdown files.
27
27
 
28
28
  Built on Vite + React + Tailwind. Fast builds. Instant hot reload. Beautiful defaults.
29
29
 
@@ -41,13 +41,15 @@ That's it. Your docs are live at `localhost:3000` (or the next available port).
41
41
  ### Install
42
42
 
43
43
  ```bash
44
- # Using bun (recommended)
44
+ # Using bun (fast)
45
45
  bun install -g veslx
46
46
 
47
- # Or npm
47
+ # Or npm (Node 18+)
48
48
  npm install -g veslx
49
49
  ```
50
50
 
51
+ Requires Node >= 18 or Bun >= 1.0.
52
+
51
53
  ### Create Your First Post
52
54
 
53
55
  ```bash
@@ -94,7 +96,6 @@ Open the URL printed in the console (defaults to [localhost:3000](http://localho
94
96
  | **Parameter Tables** | Display YAML/JSON configs with collapsible sections |
95
97
  | **Dark Mode** | Automatic theme switching |
96
98
  | **Hot Reload** | Instant updates during development |
97
- | **Print to PDF** | Export slides as landscape PDFs |
98
99
 
99
100
  ---
100
101
 
@@ -243,17 +244,6 @@ Final thoughts
243
244
  | `↑` `←` `k` | Previous slide |
244
245
  | Scroll | Natural trackpad scrolling |
245
246
 
246
- ### Print to PDF
247
-
248
- 1. Open slides in browser
249
- 2. Press `Cmd+P` (or `Ctrl+P`)
250
- 3. Select "Save as PDF"
251
- 4. Choose **Landscape** orientation
252
-
253
- Each slide becomes one PDF page, centered and optimized for print.
254
-
255
- ---
256
-
257
247
  ## CLI Commands
258
248
 
259
249
  ```bash
package/bin/lib/build.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { build } from 'vite'
2
2
  import path from 'path'
3
3
  import fs from 'fs'
4
- import importConfig from "./import-config";
5
- import veslxPlugin from '../../plugin/src/plugin'
6
- import { log } from './log'
4
+ import { fileURLToPath } from 'url'
5
+ import importConfig from "./import-config.js";
6
+ import veslxPlugin from '../../plugin/src/plugin.js'
7
+ import { log } from './log.js'
7
8
 
8
9
  /**
9
10
  * Recursively copy a directory
@@ -24,16 +25,33 @@ function copyDirSync(src: string, dest: string) {
24
25
  }
25
26
  }
26
27
 
28
+ function resolveVeslxRoot() {
29
+ const candidates = [
30
+ new URL('../..', import.meta.url),
31
+ new URL('../../..', import.meta.url),
32
+ ];
33
+
34
+ for (const candidate of candidates) {
35
+ const candidatePath = fileURLToPath(candidate);
36
+ if (fs.existsSync(path.join(candidatePath, 'vite.config.ts'))) {
37
+ return candidatePath;
38
+ }
39
+ }
40
+
41
+ return fileURLToPath(new URL('../..', import.meta.url));
42
+ }
43
+
27
44
  interface PackageJson {
28
45
  name?: string;
29
46
  description?: string;
30
47
  }
31
48
 
32
49
  async function readPackageJson(cwd: string): Promise<PackageJson | null> {
33
- const file = Bun.file(path.join(cwd, 'package.json'));
34
- if (!await file.exists()) return null;
50
+ const packagePath = path.join(cwd, 'package.json');
51
+ if (!fs.existsSync(packagePath)) return null;
35
52
  try {
36
- return await file.json();
53
+ const content = await fs.promises.readFile(packagePath, 'utf-8');
54
+ return JSON.parse(content) as PackageJson;
37
55
  } catch {
38
56
  return null;
39
57
  }
@@ -77,8 +95,8 @@ export default async function buildApp(dir?: string) {
77
95
  posts: fileConfig?.posts,
78
96
  };
79
97
 
80
- const veslxRoot = new URL('../..', import.meta.url).pathname;
81
- const configFile = new URL('../../vite.config.ts', import.meta.url).pathname;
98
+ const veslxRoot = resolveVeslxRoot();
99
+ const configFile = path.join(veslxRoot, 'vite.config.ts');
82
100
 
83
101
  // Build inside veslxRoot first (Vite requires outDir to be within or relative to root)
84
102
  const tempOutDir = path.join(veslxRoot, '.veslx-build')
@@ -1,14 +1,15 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
1
3
  import yaml from 'js-yaml';
2
- import type { VeslxConfig } from '../../plugin/src/types';
4
+ import type { VeslxConfig } from '../../plugin/src/types.js';
3
5
 
4
6
  export default async function importConfig(root: string): Promise<VeslxConfig | undefined> {
5
- const file = Bun.file(`${root}/veslx.yaml`);
7
+ const configPath = path.join(root, 'veslx.yaml');
6
8
 
7
- if (!await file.exists()) {
9
+ if (!fs.existsSync(configPath)) {
8
10
  return undefined;
9
11
  }
10
12
 
11
- const content = await file.text();
12
- const config = yaml.load(content) as VeslxConfig;
13
- return config;
13
+ const content = await fs.promises.readFile(configPath, 'utf-8');
14
+ return yaml.load(content) as VeslxConfig;
14
15
  }
package/bin/lib/init.ts CHANGED
@@ -1,10 +1,11 @@
1
+ import fs from "fs";
1
2
  import nodePath from "path";
2
3
  import yaml from "js-yaml";
3
4
 
4
5
  export default async function createNewConfig() {
5
6
  const configPath = "veslx.yaml";
6
7
 
7
- if (await Bun.file(configPath).exists()) {
8
+ if (fs.existsSync(configPath)) {
8
9
  console.error(`Configuration file '${configPath}' already exists.`);
9
10
  return;
10
11
  }
@@ -22,7 +23,7 @@ export default async function createNewConfig() {
22
23
 
23
24
  const configStr = yaml.dump(config, { indent: 2, quotingType: '"' });
24
25
 
25
- await Bun.write(configPath, configStr);
26
+ await fs.promises.writeFile(configPath, configStr, "utf-8");
26
27
 
27
28
  console.log(`Created veslx.yaml`);
28
29
  console.log(`\nEdit the file to customize your site, then run:`);
package/bin/lib/serve.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import { createServer } from 'vite'
2
- import importConfig from "./import-config";
3
- import veslxPlugin from '../../plugin/src/plugin'
2
+ import importConfig from "./import-config.js";
3
+ import veslxPlugin from '../../plugin/src/plugin.js'
4
4
  import path from 'path'
5
- import { log } from './log'
5
+ import fs from 'fs'
6
+ import { fileURLToPath } from 'url'
7
+ import { log } from './log.js'
6
8
 
7
9
  interface PackageJson {
8
10
  name?: string;
@@ -10,10 +12,11 @@ interface PackageJson {
10
12
  }
11
13
 
12
14
  async function readPackageJson(cwd: string): Promise<PackageJson | null> {
13
- const file = Bun.file(path.join(cwd, 'package.json'));
14
- if (!await file.exists()) return null;
15
+ const packagePath = path.join(cwd, 'package.json');
16
+ if (!fs.existsSync(packagePath)) return null;
15
17
  try {
16
- return await file.json();
18
+ const content = await fs.promises.readFile(packagePath, 'utf-8');
19
+ return JSON.parse(content) as PackageJson;
17
20
  } catch {
18
21
  return null;
19
22
  }
@@ -64,6 +67,22 @@ async function listenWithFallback(server: Awaited<ReturnType<typeof createServer
64
67
  process.exit(1);
65
68
  }
66
69
 
70
+ function resolveVeslxRoot() {
71
+ const candidates = [
72
+ new URL('../..', import.meta.url),
73
+ new URL('../../..', import.meta.url),
74
+ ];
75
+
76
+ for (const candidate of candidates) {
77
+ const candidatePath = fileURLToPath(candidate);
78
+ if (fs.existsSync(path.join(candidatePath, 'vite.config.ts'))) {
79
+ return candidatePath;
80
+ }
81
+ }
82
+
83
+ return fileURLToPath(new URL('../..', import.meta.url));
84
+ }
85
+
67
86
  export default async function serve(dir?: string) {
68
87
  const cwd = process.cwd()
69
88
 
@@ -102,8 +121,8 @@ export default async function serve(dir?: string) {
102
121
  posts: fileConfig?.posts,
103
122
  };
104
123
 
105
- const veslxRoot = new URL('../..', import.meta.url).pathname;
106
- const configFile = new URL('../../vite.config.ts', import.meta.url).pathname;
124
+ const veslxRoot = resolveVeslxRoot();
125
+ const configFile = path.join(veslxRoot, 'vite.config.ts');
107
126
 
108
127
  // Final content directory: CLI arg already resolved, or resolve from config
109
128
  const finalContentDir = dir
package/bin/lib/start.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import pm2 from "pm2";
2
2
  import path from "path";
3
- import { log } from './log'
3
+ import { log } from './log.js'
4
+
5
+ function toDaemonName(contentDir: string) {
6
+ const normalized = contentDir.replace(/[:\\/]+/g, '-').replace(/^-+/, '');
7
+ return `veslx-${normalized}`.toLowerCase();
8
+ }
4
9
 
5
10
  export default async function start(dir?: string) {
6
11
  const cwd = process.cwd();
@@ -10,10 +15,16 @@ export default async function start(dir?: string) {
10
15
  ? (path.isAbsolute(dir) ? dir : path.resolve(cwd, dir))
11
16
  : cwd;
12
17
 
13
- const name = `veslx-${contentDir.replace(/\//g, '-').replace(/^-/, '')}`.toLowerCase();
18
+ const name = toDaemonName(contentDir);
14
19
 
15
20
  // Build args for veslx serve
16
- const args = dir ? ['veslx', 'serve', dir] : ['veslx', 'serve'];
21
+ const args = dir ? ['serve', dir] : ['serve'];
22
+ const cliPath = process.argv[1];
23
+ if (!cliPath) {
24
+ log.error('unable to resolve veslx binary path');
25
+ process.exit(1);
26
+ return;
27
+ }
17
28
 
18
29
  pm2.connect((err) => {
19
30
  if (err) {
@@ -24,7 +35,8 @@ export default async function start(dir?: string) {
24
35
 
25
36
  pm2.start({
26
37
  name: name,
27
- script: 'bunx',
38
+ script: cliPath,
39
+ interpreter: process.execPath,
28
40
  args: args,
29
41
  cwd: cwd,
30
42
  autorestart: true,
@@ -43,4 +55,4 @@ export default async function start(dir?: string) {
43
55
  process.exit(0);
44
56
  });
45
57
  })
46
- }
58
+ }
package/bin/lib/stop.ts CHANGED
@@ -1,9 +1,18 @@
1
1
  import pm2 from "pm2";
2
- import { log } from './log'
2
+ import path from "path";
3
+ import { log } from './log.js'
3
4
 
4
- export default async function stop() {
5
+ function toDaemonName(contentDir: string) {
6
+ const normalized = contentDir.replace(/[:\\/]+/g, '-').replace(/^-+/, '');
7
+ return `veslx-${normalized}`.toLowerCase();
8
+ }
9
+
10
+ export default async function stop(dir?: string) {
5
11
  const cwd = process.cwd();
6
- const name = `veslx-${cwd.replace(/\//g, '-').replace(/^-/, '')}`.toLowerCase();
12
+ const contentDir = dir
13
+ ? (path.isAbsolute(dir) ? dir : path.resolve(cwd, dir))
14
+ : cwd;
15
+ const name = toDaemonName(contentDir);
7
16
 
8
17
  pm2.connect((err) => {
9
18
  if (err) {
package/bin/veslx.ts CHANGED
@@ -1,13 +1,28 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { cac } from "cac";
4
- import pkg from "../package.json";
5
- import init from "./lib/init";
6
- import serve from "./lib/serve";
7
- import start from "./lib/start";
8
- import stop from "./lib/stop";
9
- import build from "./lib/build";
10
- import { banner } from "./lib/log";
4
+ import { createRequire } from "module";
5
+ import init from "./lib/init.js";
6
+ import serve from "./lib/serve.js";
7
+ import start from "./lib/start.js";
8
+ import stop from "./lib/stop.js";
9
+ import build from "./lib/build.js";
10
+ import { banner } from "./lib/log.js";
11
+
12
+ const require = createRequire(import.meta.url);
13
+ function tryRequire<T>(id: string): T | null {
14
+ try {
15
+ return require(id) as T;
16
+ } catch (err: any) {
17
+ if (err?.code === "MODULE_NOT_FOUND") return null;
18
+ throw err;
19
+ }
20
+ }
21
+
22
+ const pkg =
23
+ tryRequire<{ version?: string }>("../package.json") ??
24
+ tryRequire<{ version?: string }>("../../package.json") ??
25
+ {};
11
26
 
12
27
  const cli = cac("veslx");
13
28
 
@@ -26,7 +41,7 @@ cli
26
41
  .action(start);
27
42
 
28
43
  cli
29
- .command("stop", "Stop the veslx deamon")
44
+ .command("stop [dir]", "Stop the veslx daemon")
30
45
  .action(stop);
31
46
 
32
47
  cli
@@ -34,7 +49,7 @@ cli
34
49
  .action(build)
35
50
 
36
51
  cli.help();
37
- cli.version(pkg.version);
52
+ cli.version(pkg.version ?? "0.0.0");
38
53
  cli.parse();
39
54
 
40
55
  if (!cli.matchedCommand && process.argv.length <= 2) {
@@ -0,0 +1,121 @@
1
+ import { build } from 'vite';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import { fileURLToPath } from 'url';
5
+ import importConfig from "./import-config.js";
6
+ import veslxPlugin from '../../plugin/src/plugin.js';
7
+ import { log } from './log.js';
8
+ /**
9
+ * Recursively copy a directory
10
+ */
11
+ function copyDirSync(src, dest) {
12
+ fs.mkdirSync(dest, { recursive: true });
13
+ const entries = fs.readdirSync(src, { withFileTypes: true });
14
+ for (const entry of entries) {
15
+ const srcPath = path.join(src, entry.name);
16
+ const destPath = path.join(dest, entry.name);
17
+ if (entry.isDirectory()) {
18
+ copyDirSync(srcPath, destPath);
19
+ }
20
+ else {
21
+ fs.copyFileSync(srcPath, destPath);
22
+ }
23
+ }
24
+ }
25
+ function resolveVeslxRoot() {
26
+ const candidates = [
27
+ new URL('../..', import.meta.url),
28
+ new URL('../../..', import.meta.url),
29
+ ];
30
+ for (const candidate of candidates) {
31
+ const candidatePath = fileURLToPath(candidate);
32
+ if (fs.existsSync(path.join(candidatePath, 'vite.config.ts'))) {
33
+ return candidatePath;
34
+ }
35
+ }
36
+ return fileURLToPath(new URL('../..', import.meta.url));
37
+ }
38
+ async function readPackageJson(cwd) {
39
+ const packagePath = path.join(cwd, 'package.json');
40
+ if (!fs.existsSync(packagePath))
41
+ return null;
42
+ try {
43
+ const content = await fs.promises.readFile(packagePath, 'utf-8');
44
+ return JSON.parse(content);
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ async function getDefaultConfig(cwd) {
51
+ const pkg = await readPackageJson(cwd);
52
+ const folderName = path.basename(cwd);
53
+ const name = pkg?.name || folderName;
54
+ return {
55
+ dir: '.',
56
+ site: {
57
+ name,
58
+ description: pkg?.description || '',
59
+ }
60
+ };
61
+ }
62
+ export default async function buildApp(dir) {
63
+ const cwd = process.cwd();
64
+ // Resolve content directory from CLI arg
65
+ const contentDir = dir
66
+ ? (path.isAbsolute(dir) ? dir : path.resolve(cwd, dir))
67
+ : cwd;
68
+ // Get defaults first, then merge with config file if it exists
69
+ // Look for config in content directory first, then fall back to cwd
70
+ const defaults = await getDefaultConfig(contentDir);
71
+ const fileConfig = await importConfig(contentDir) || await importConfig(cwd);
72
+ // CLI argument takes precedence over config file
73
+ const config = {
74
+ dir: dir || fileConfig?.dir || defaults.dir,
75
+ site: {
76
+ ...defaults.site,
77
+ ...fileConfig?.site,
78
+ },
79
+ slides: fileConfig?.slides,
80
+ posts: fileConfig?.posts,
81
+ };
82
+ const veslxRoot = resolveVeslxRoot();
83
+ const configFile = path.join(veslxRoot, 'vite.config.ts');
84
+ // Build inside veslxRoot first (Vite requires outDir to be within or relative to root)
85
+ const tempOutDir = path.join(veslxRoot, '.veslx-build');
86
+ const finalOutDir = path.join(cwd, 'dist');
87
+ // Final content directory: CLI arg already resolved, or resolve from config
88
+ const finalContentDir = dir
89
+ ? contentDir
90
+ : (path.isAbsolute(config.dir) ? config.dir : path.resolve(cwd, config.dir));
91
+ await build({
92
+ root: veslxRoot,
93
+ configFile,
94
+ mode: 'production',
95
+ // Cache in user's project so it persists across bunx runs
96
+ cacheDir: path.join(cwd, 'node_modules/.vite'),
97
+ build: {
98
+ outDir: tempOutDir,
99
+ emptyOutDir: true,
100
+ watch: null, // Explicitly disable watch mode
101
+ rollupOptions: {
102
+ input: path.join(veslxRoot, 'index.html'),
103
+ },
104
+ },
105
+ plugins: [
106
+ veslxPlugin(finalContentDir, config)
107
+ ],
108
+ logLevel: 'info',
109
+ });
110
+ // Copy built files to user's dist directory
111
+ if (fs.existsSync(finalOutDir)) {
112
+ fs.rmSync(finalOutDir, { recursive: true });
113
+ }
114
+ copyDirSync(tempOutDir, finalOutDir);
115
+ // Copy index.html to 404.html for SPA fallback routing
116
+ // This works with GitHub Pages, Netlify, and many static servers
117
+ fs.copyFileSync(path.join(finalOutDir, 'index.html'), path.join(finalOutDir, '404.html'));
118
+ // Clean up temp build directory
119
+ fs.rmSync(tempOutDir, { recursive: true });
120
+ log.success(`dist/`);
121
+ }
@@ -0,0 +1,11 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import yaml from 'js-yaml';
4
+ export default async function importConfig(root) {
5
+ const configPath = path.join(root, 'veslx.yaml');
6
+ if (!fs.existsSync(configPath)) {
7
+ return undefined;
8
+ }
9
+ const content = await fs.promises.readFile(configPath, 'utf-8');
10
+ return yaml.load(content);
11
+ }
@@ -0,0 +1,24 @@
1
+ import fs from "fs";
2
+ import nodePath from "path";
3
+ import yaml from "js-yaml";
4
+ export default async function createNewConfig() {
5
+ const configPath = "veslx.yaml";
6
+ if (fs.existsSync(configPath)) {
7
+ console.error(`Configuration file '${configPath}' already exists.`);
8
+ return;
9
+ }
10
+ const cwd = process.cwd();
11
+ const folderName = nodePath.basename(cwd);
12
+ const config = {
13
+ dir: ".",
14
+ site: {
15
+ name: folderName,
16
+ github: "",
17
+ },
18
+ };
19
+ const configStr = yaml.dump(config, { indent: 2, quotingType: '"' });
20
+ await fs.promises.writeFile(configPath, configStr, "utf-8");
21
+ console.log(`Created veslx.yaml`);
22
+ console.log(`\nEdit the file to customize your site, then run:`);
23
+ console.log(` veslx serve`);
24
+ }
@@ -0,0 +1,15 @@
1
+ // Minimal CLI logger with subtle styling
2
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
3
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
4
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
5
+ const red = (s) => `\x1b[31m${s}\x1b[0m`;
6
+ export const log = {
7
+ info: (msg) => console.log(dim(` ${msg}`)),
8
+ success: (msg) => console.log(` ${green('✓')} ${msg}`),
9
+ error: (msg) => console.error(` ${red('✗')} ${msg}`),
10
+ url: (url) => console.log(` ${cyan(url)}`),
11
+ blank: () => console.log(),
12
+ };
13
+ export const banner = () => {
14
+ console.log(dim(` veslx`));
15
+ };
@@ -0,0 +1,132 @@
1
+ import { createServer } from 'vite';
2
+ import importConfig from "./import-config.js";
3
+ import veslxPlugin from '../../plugin/src/plugin.js';
4
+ import path from 'path';
5
+ import fs from 'fs';
6
+ import { fileURLToPath } from 'url';
7
+ import { log } from './log.js';
8
+ async function readPackageJson(cwd) {
9
+ const packagePath = path.join(cwd, 'package.json');
10
+ if (!fs.existsSync(packagePath))
11
+ return null;
12
+ try {
13
+ const content = await fs.promises.readFile(packagePath, 'utf-8');
14
+ return JSON.parse(content);
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ async function getDefaultConfig(cwd) {
21
+ const pkg = await readPackageJson(cwd);
22
+ const folderName = path.basename(cwd);
23
+ const name = pkg?.name || folderName;
24
+ return {
25
+ dir: '.',
26
+ site: {
27
+ name,
28
+ description: pkg?.description || '',
29
+ }
30
+ };
31
+ }
32
+ function isAddressInUse(err) {
33
+ if (!err || typeof err !== 'object')
34
+ return false;
35
+ const anyErr = err;
36
+ if (anyErr.code === 'EADDRINUSE')
37
+ return true;
38
+ return typeof anyErr.message === 'string' && anyErr.message.includes('already in use');
39
+ }
40
+ async function listenWithFallback(server) {
41
+ const startPort = server.config.server.port ?? 3000;
42
+ const maxAttempts = 50;
43
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
44
+ const port = startPort + attempt;
45
+ try {
46
+ await server.listen(port);
47
+ const address = server.httpServer?.address();
48
+ const resolvedPort = typeof address === 'object' && address ? address.port : port;
49
+ log.success(`listening :${resolvedPort}`);
50
+ return;
51
+ }
52
+ catch (err) {
53
+ if (!isAddressInUse(err)) {
54
+ throw err;
55
+ }
56
+ log.info(`busy :${port} → :${port + 1}`);
57
+ }
58
+ }
59
+ log.error(`no available ports from ${startPort} to ${startPort + maxAttempts - 1}`);
60
+ process.exit(1);
61
+ }
62
+ function resolveVeslxRoot() {
63
+ const candidates = [
64
+ new URL('../..', import.meta.url),
65
+ new URL('../../..', import.meta.url),
66
+ ];
67
+ for (const candidate of candidates) {
68
+ const candidatePath = fileURLToPath(candidate);
69
+ if (fs.existsSync(path.join(candidatePath, 'vite.config.ts'))) {
70
+ return candidatePath;
71
+ }
72
+ }
73
+ return fileURLToPath(new URL('../..', import.meta.url));
74
+ }
75
+ export default async function serve(dir) {
76
+ const cwd = process.cwd();
77
+ // Resolve content directory - CLI arg takes precedence
78
+ const contentDir = dir
79
+ ? (path.isAbsolute(dir) ? dir : path.resolve(cwd, dir))
80
+ : cwd;
81
+ // Get defaults first, then merge with config file if it exists
82
+ // Look for config in content directory first, then fall back to cwd
83
+ const defaults = await getDefaultConfig(contentDir);
84
+ // Track which config file was found for hot reload
85
+ let configPath;
86
+ const contentConfigPath = path.join(contentDir, 'veslx.yaml');
87
+ const cwdConfigPath = path.join(cwd, 'veslx.yaml');
88
+ let fileConfig = await importConfig(contentDir);
89
+ if (fileConfig) {
90
+ configPath = contentConfigPath;
91
+ }
92
+ else {
93
+ fileConfig = await importConfig(cwd);
94
+ if (fileConfig) {
95
+ configPath = cwdConfigPath;
96
+ }
97
+ }
98
+ // CLI argument takes precedence over config file
99
+ const config = {
100
+ dir: dir || fileConfig?.dir || defaults.dir,
101
+ site: {
102
+ ...defaults.site,
103
+ ...fileConfig?.site,
104
+ },
105
+ slides: fileConfig?.slides,
106
+ posts: fileConfig?.posts,
107
+ };
108
+ const veslxRoot = resolveVeslxRoot();
109
+ const configFile = path.join(veslxRoot, 'vite.config.ts');
110
+ // Final content directory: CLI arg already resolved, or resolve from config
111
+ const finalContentDir = dir
112
+ ? contentDir
113
+ : (path.isAbsolute(config.dir) ? config.dir : path.resolve(cwd, config.dir));
114
+ const server = await createServer({
115
+ root: veslxRoot,
116
+ configFile,
117
+ server: {
118
+ strictPort: true,
119
+ },
120
+ // Cache in user's project so it persists across bunx runs
121
+ cacheDir: path.join(cwd, 'node_modules/.vite'),
122
+ plugins: [
123
+ veslxPlugin(finalContentDir, config, { configPath })
124
+ ],
125
+ });
126
+ await listenWithFallback(server);
127
+ const info = server.resolvedUrls;
128
+ if (info?.local[0]) {
129
+ log.url(info.local[0]);
130
+ }
131
+ server.bindCLIShortcuts({ print: false });
132
+ }