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.
- package/LICENSE +21 -0
- package/README.md +5 -15
- package/bin/lib/build.ts +26 -8
- package/bin/lib/import-config.ts +7 -6
- package/bin/lib/init.ts +3 -2
- package/bin/lib/serve.ts +27 -8
- package/bin/lib/start.ts +17 -5
- package/bin/lib/stop.ts +12 -3
- package/bin/veslx.ts +25 -10
- package/dist/bin/lib/build.js +121 -0
- package/dist/bin/lib/import-config.js +11 -0
- package/dist/bin/lib/init.js +24 -0
- package/dist/bin/lib/log.js +15 -0
- package/dist/bin/lib/serve.js +132 -0
- package/dist/bin/lib/start.js +49 -0
- package/dist/bin/lib/stop.js +28 -0
- package/dist/bin/veslx.js +47 -0
- package/dist/client/components/front-matter.js +31 -5
- package/dist/client/components/front-matter.js.map +1 -1
- package/dist/client/components/gallery/hooks/use-gallery-images.js +10 -12
- package/dist/client/components/gallery/hooks/use-gallery-images.js.map +1 -1
- package/dist/client/components/header.js +2 -0
- package/dist/client/components/header.js.map +1 -1
- package/dist/client/components/mdx-components.js +3 -0
- package/dist/client/components/mdx-components.js.map +1 -1
- package/dist/client/components/post-list-item.js +43 -29
- package/dist/client/components/post-list-item.js.map +1 -1
- package/dist/client/components/post-list.js +11 -3
- package/dist/client/components/post-list.js.map +1 -1
- package/dist/client/components/veslx-search.js +119 -0
- package/dist/client/components/veslx-search.js.map +1 -0
- package/dist/client/hooks/use-mdx-content.js +74 -24
- package/dist/client/hooks/use-mdx-content.js.map +1 -1
- package/dist/client/lib/frontmatter-context.js.map +1 -1
- package/dist/client/plugin/src/client.js +13 -11
- package/dist/client/plugin/src/client.js.map +1 -1
- package/dist/client/plugin/src/directory-tree.js.map +1 -1
- package/dist/client/src/index.css +474 -0
- package/dist/plugin/src/client.js +171 -0
- package/dist/plugin/src/directory-tree.js +143 -0
- package/dist/plugin/src/lib.js +5 -0
- package/dist/plugin/src/plugin.js +738 -0
- package/dist/plugin/src/remark-slides.js +93 -0
- package/dist/plugin/src/types.js +13 -0
- package/package.json +21 -12
- package/plugin/src/client.tsx +16 -15
- package/plugin/src/directory-tree.ts +1 -1
- package/plugin/src/lib.ts +1 -0
- package/plugin/src/plugin.ts +252 -19
- package/plugin/src/remark-slides.ts +1 -1
- package/postcss.config.js +5 -0
- package/src/components/front-matter.tsx +36 -5
- package/src/components/gallery/hooks/use-gallery-images.ts +13 -14
- package/src/components/header.tsx +2 -0
- package/src/components/mdx-components.tsx +5 -0
- package/src/components/post-list-item.tsx +55 -35
- package/src/components/post-list.tsx +18 -3
- package/src/components/veslx-search.tsx +155 -0
- package/src/hooks/use-mdx-content.ts +94 -37
- package/src/lib/frontmatter-context.tsx +1 -0
- package/tailwind.config.js +130 -0
- package/tsconfig.build.json +17 -0
- package/vite.lib.config.ts +16 -2
- package/bin/lib/export.ts +0 -203
- package/plugin/README.md +0 -21
- package/plugin/package.json +0 -26
- 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
|
|
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 (
|
|
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
|
|
5
|
-
import
|
|
6
|
-
import
|
|
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
|
|
34
|
-
if (!
|
|
50
|
+
const packagePath = path.join(cwd, 'package.json');
|
|
51
|
+
if (!fs.existsSync(packagePath)) return null;
|
|
35
52
|
try {
|
|
36
|
-
|
|
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 =
|
|
81
|
-
const configFile =
|
|
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')
|
package/bin/lib/import-config.ts
CHANGED
|
@@ -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
|
|
7
|
+
const configPath = path.join(root, 'veslx.yaml');
|
|
6
8
|
|
|
7
|
-
if (!
|
|
9
|
+
if (!fs.existsSync(configPath)) {
|
|
8
10
|
return undefined;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
const content = await
|
|
12
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
14
|
-
if (!
|
|
15
|
+
const packagePath = path.join(cwd, 'package.json');
|
|
16
|
+
if (!fs.existsSync(packagePath)) return null;
|
|
15
17
|
try {
|
|
16
|
-
|
|
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 =
|
|
106
|
-
const configFile =
|
|
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 =
|
|
18
|
+
const name = toDaemonName(contentDir);
|
|
14
19
|
|
|
15
20
|
// Build args for veslx serve
|
|
16
|
-
const args = dir ? ['
|
|
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:
|
|
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
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { log } from './log.js'
|
|
3
4
|
|
|
4
|
-
|
|
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
|
|
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
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { cac } from "cac";
|
|
4
|
-
import
|
|
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
|
|
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
|
+
}
|