@aravindc26/velu 0.11.0 → 0.11.3

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 (60) hide show
  1. package/package.json +15 -6
  2. package/schema/velu.schema.json +1251 -115
  3. package/src/build.ts +1121 -304
  4. package/src/cli.ts +90 -26
  5. package/src/engine/_server.mjs +1684 -277
  6. package/src/engine/app/(docs)/[...slug]/layout.tsx +371 -0
  7. package/src/engine/app/(docs)/[...slug]/page.tsx +926 -0
  8. package/src/engine/app/api/proxy/route.ts +23 -0
  9. package/src/engine/app/copy-page.css +59 -1
  10. package/src/engine/app/global.css +3157 -3
  11. package/src/engine/app/layout.tsx +56 -1
  12. package/src/engine/app/llms-file/route.ts +87 -0
  13. package/src/engine/app/llms-full-file/route.ts +62 -0
  14. package/src/engine/app/md-file/[...slug]/route.ts +409 -0
  15. package/src/engine/app/page.tsx +45 -0
  16. package/src/engine/app/robots.txt/route.ts +63 -0
  17. package/src/engine/app/rss-file/[...slug]/route.ts +169 -0
  18. package/src/engine/app/sitemap.xml/route.ts +82 -0
  19. package/src/engine/components/assistant.tsx +16 -5
  20. package/src/engine/components/changelog-filters.tsx +114 -0
  21. package/src/engine/components/code-group.tsx +383 -0
  22. package/src/engine/components/color.tsx +118 -0
  23. package/src/engine/components/expandable.tsx +77 -0
  24. package/src/engine/components/icon.tsx +136 -0
  25. package/src/engine/components/image-zoom-fallback.tsx +147 -0
  26. package/src/engine/components/image.tsx +111 -0
  27. package/src/engine/components/manual-api-playground.tsx +154 -0
  28. package/src/engine/components/mermaid.tsx +142 -0
  29. package/src/engine/components/openapi-toc-sync.tsx +59 -0
  30. package/src/engine/components/openapi.tsx +1682 -0
  31. package/src/engine/components/page-feedback.tsx +153 -0
  32. package/src/engine/components/product-switcher.tsx +27 -3
  33. package/src/engine/components/prompt.tsx +90 -0
  34. package/src/engine/components/providers.tsx +1 -6
  35. package/src/engine/components/search.tsx +4 -0
  36. package/src/engine/components/sidebar-links.tsx +13 -15
  37. package/src/engine/components/synced-tabs.tsx +57 -0
  38. package/src/engine/components/toc-examples.tsx +110 -0
  39. package/src/engine/components/view.tsx +344 -0
  40. package/src/engine/generated/redirects.ts +3 -0
  41. package/src/engine/lib/changelog.ts +246 -0
  42. package/src/engine/lib/layout.shared.ts +30 -2
  43. package/src/engine/lib/llms.ts +444 -0
  44. package/src/engine/lib/navigation-normalize.mjs +481 -412
  45. package/src/engine/lib/navigation-normalize.ts +261 -54
  46. package/src/engine/lib/redirects.ts +194 -0
  47. package/src/engine/lib/source.ts +107 -4
  48. package/src/engine/lib/velu.ts +368 -2
  49. package/src/engine/mdx-components.tsx +648 -0
  50. package/src/engine/middleware.ts +66 -0
  51. package/src/engine/public/icons/cursor-dark.svg +12 -0
  52. package/src/engine/public/icons/cursor-light.svg +12 -0
  53. package/src/engine/source.config.ts +98 -1
  54. package/src/engine/src/components/PageTitle.astro +16 -5
  55. package/src/engine/src/lib/velu.ts +11 -3
  56. package/src/navigation-normalize.ts +252 -54
  57. package/src/themes.ts +6 -6
  58. package/src/validate.ts +119 -6
  59. package/src/engine/app/(docs)/[[...slug]]/layout.tsx +0 -87
  60. package/src/engine/app/(docs)/[[...slug]]/page.tsx +0 -146
package/src/cli.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { resolve, join, dirname, delimiter } from "node:path";
2
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, writeFileSync, readdirSync, copyFileSync, cpSync, rmSync } from "node:fs";
3
3
  import { spawn } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
@@ -7,6 +7,16 @@ const __filename = fileURLToPath(import.meta.url);
7
7
  const PACKAGE_ROOT = resolve(dirname(__filename), "..");
8
8
  const SCHEMA_PATH = join(PACKAGE_ROOT, "schema", "velu.schema.json");
9
9
  const NODE_MODULES_PATH = join(PACKAGE_ROOT, "node_modules");
10
+ const PRIMARY_CONFIG_NAME = "docs.json";
11
+ const LEGACY_CONFIG_NAME = "velu.json";
12
+
13
+ function resolveConfigPath(dir: string): string | null {
14
+ const primary = join(dir, PRIMARY_CONFIG_NAME);
15
+ if (existsSync(primary)) return primary;
16
+ const legacy = join(dir, LEGACY_CONFIG_NAME);
17
+ if (existsSync(legacy)) return legacy;
18
+ return null;
19
+ }
10
20
 
11
21
  /** Build env that lets spawned processes resolve deps from the CLI's own node_modules */
12
22
  function engineEnv(docsDir?: string): NodeJS.ProcessEnv {
@@ -26,7 +36,7 @@ function printHelp() {
26
36
 
27
37
  Usage:
28
38
  velu init Scaffold a new docs project with example files
29
- velu lint Validate velu.json and check referenced pages
39
+ velu lint Validate docs.json (or velu.json) and check referenced pages
30
40
  velu run [--port N] Build site and start dev server (default: 4321)
31
41
  velu build Build a deployable static site (SSG)
32
42
  velu paths Output all navigation paths and their source files as JSON
@@ -35,19 +45,19 @@ function printHelp() {
35
45
  --port <number> Port for the dev server (default: 4321)
36
46
  --help Show this help message
37
47
 
38
- Run lint/run/build/paths from a directory containing velu.json.
48
+ Run lint/run/build/paths from a directory containing docs.json (or velu.json).
39
49
  `);
40
50
  }
41
51
 
42
52
  // ── init ────────────────────────────────────────────────────────────────────────
43
53
 
44
54
  function init(targetDir: string) {
45
- if (existsSync(join(targetDir, "velu.json"))) {
46
- console.error("❌ velu.json already exists in this directory.");
55
+ if (resolveConfigPath(targetDir)) {
56
+ console.error("❌ docs.json or velu.json already exists in this directory.");
47
57
  process.exit(1);
48
58
  }
49
59
 
50
- // velu.json
60
+ // docs.json
51
61
  const config = {
52
62
  $schema: "https://raw.githubusercontent.com/aravindc26/velu/main/schema/velu.schema.json",
53
63
  theme: "neutral" as const,
@@ -86,13 +96,13 @@ function init(targetDir: string) {
86
96
  ],
87
97
  },
88
98
  };
89
- writeFileSync(join(targetDir, "velu.json"), JSON.stringify(config, null, 2) + "\n", "utf-8");
99
+ writeFileSync(join(targetDir, PRIMARY_CONFIG_NAME), JSON.stringify(config, null, 2) + "\n", "utf-8");
90
100
 
91
101
  // Example pages
92
102
  const pages: Record<string, string> = {
93
- "quickstart.md": `# Quickstart\n\nWelcome to your new documentation site!\n\n## Prerequisites\n\n- Node.js 20.9+\n- npm\n\n## Getting Started\n\n1. Edit the markdown files in this directory\n2. Update \`velu.json\` to configure navigation\n3. Run \`velu run\` to start the dev server\n\n\`\`\`bash\nvelu run\n\`\`\`\n\nYour site is live at \`http://localhost:4321\`.\n`,
103
+ "quickstart.md": `# Quickstart\n\nWelcome to your new documentation site!\n\n## Prerequisites\n\n- Node.js 20.9+\n- npm\n\n## Getting Started\n\n1. Edit the markdown files in this directory\n2. Update \`docs.json\` to configure navigation\n3. Run \`velu run\` to start the dev server\n\n\`\`\`bash\nvelu run\n\`\`\`\n\nYour site is live at \`http://localhost:4321\`.\n`,
94
104
  "installation.md": `# Installation\n\nInstall Velu globally:\n\n\`\`\`bash\nnpm install -g @aravindc26/velu\n\`\`\`\n\nOr run directly with npx:\n\n\`\`\`bash\nnpx @aravindc26/velu run\n\`\`\`\n`,
95
- "guides/configuration.md": `# Configuration\n\nVelu uses a \`velu.json\` file to define your site's navigation.\n\n## Navigation Structure\n\n- **Tabs** — Top-level horizontal navigation\n- **Groups** — Collapsible sidebar sections within a tab\n- **Pages** — Individual markdown documents\n\n## Example\n\n\`\`\`json\n{\n "navigation": {\n "tabs": [\n {\n "tab": "Getting Started",\n "slug": "getting-started",\n "groups": [\n {\n "group": "Basics",\n "slug": "getting-started",\n "pages": ["quickstart"]\n }\n ]\n }\n ]\n }\n}\n\`\`\`\n`,
105
+ "guides/configuration.md": `# Configuration\n\nVelu uses a \`docs.json\` file to define your site's navigation.\n\n## Navigation Structure\n\n- **Tabs** — Top-level horizontal navigation\n- **Groups** — Collapsible sidebar sections within a tab\n- **Pages** — Individual markdown documents\n\n## Example\n\n\`\`\`json\n{\n "navigation": {\n "tabs": [\n {\n "tab": "Getting Started",\n "slug": "getting-started",\n "groups": [\n {\n "group": "Basics",\n "slug": "getting-started",\n "pages": ["quickstart"]\n }\n ]\n }\n ]\n }\n}\n\`\`\`\n`,
96
106
  "guides/deployment.md": `# Deployment\n\nBuild your site for production:\n\n\`\`\`bash\nvelu build\n\`\`\`\n\nThe output is a static site you can deploy anywhere — Netlify, Vercel, GitHub Pages, etc.\n`,
97
107
  "api-reference/overview.md": `# API Overview\n\nThis section covers the API reference for your project.\n\n## Endpoints\n\n| Method | Path | Description |\n|--------|------|-------------|\n| GET | /api/health | Health check |\n| POST | /api/data | Create data |\n`,
98
108
  "api-reference/authentication.md": `# Authentication\n\nAll API requests require an API key.\n\n## Headers\n\n\`\`\`\nAuthorization: Bearer YOUR_API_KEY\n\`\`\`\n\n## Getting an API Key\n\nVisit the dashboard to generate your API key.\n`,
@@ -108,7 +118,7 @@ function init(targetDir: string) {
108
118
  console.log(" \x1b[36mvelu\x1b[0m project initialized");
109
119
  console.log("");
110
120
  console.log(" Created:");
111
- console.log(" velu.json");
121
+ console.log(" docs.json");
112
122
  for (const filePath of Object.keys(pages)) {
113
123
  console.log(` ${filePath}`);
114
124
  }
@@ -126,7 +136,7 @@ async function lint(docsDir: string) {
126
136
  const result = validateVeluConfig(docsDir, SCHEMA_PATH);
127
137
 
128
138
  if (result.valid) {
129
- console.log("✅ velu.json is valid. All referenced pages exist.");
139
+ console.log("✅ docs.json/velu.json is valid. All referenced pages exist.");
130
140
  } else {
131
141
  console.error("❌ Validation failed:\n");
132
142
  for (const err of result.errors) {
@@ -149,9 +159,9 @@ async function paths(docsDir: string) {
149
159
  const { readFileSync, existsSync } = await import("node:fs");
150
160
  const { join } = await import("node:path");
151
161
 
152
- const configPath = join(docsDir, "velu.json");
153
- if (!existsSync(configPath)) {
154
- console.error("❌ velu.json not found.");
162
+ const configPath = resolveConfigPath(docsDir);
163
+ if (!configPath) {
164
+ console.error("❌ docs.json or velu.json not found.");
155
165
  process.exit(1);
156
166
  }
157
167
 
@@ -185,13 +195,25 @@ async function paths(docsDir: string) {
185
195
 
186
196
  async function generateProject(docsDir: string): Promise<string> {
187
197
  const { build } = await import("./build.js");
188
- // Place .velu-out inside CLI package dir so node_modules resolves
189
- // naturally by walking up — avoids symlinks that Turbopack rejects.
190
- const outDir = join(PACKAGE_ROOT, ".velu-out");
198
+ // Generate into the active docs project directory.
199
+ const outDir = join(docsDir, ".velu-out");
191
200
  build(docsDir, outDir);
192
201
  return outDir;
193
202
  }
194
203
 
204
+ function samePath(a: string, b: string): boolean {
205
+ return resolve(a).replace(/\\/g, "/").toLowerCase() === resolve(b).replace(/\\/g, "/").toLowerCase();
206
+ }
207
+
208
+ function prepareRuntimeOutDir(docsOutDir: string): string {
209
+ const runtimeOutDir = join(PACKAGE_ROOT, ".velu-out");
210
+ if (samePath(docsOutDir, runtimeOutDir)) return runtimeOutDir;
211
+
212
+ rmSync(runtimeOutDir, { recursive: true, force: true });
213
+ cpSync(docsOutDir, runtimeOutDir, { recursive: true, force: true });
214
+ return runtimeOutDir;
215
+ }
216
+
195
217
  async function buildStatic(outDir: string, docsDir: string) {
196
218
  await new Promise<void>((res, rej) => {
197
219
  const child = spawn("node", ["_server.mjs", "build"], {
@@ -203,10 +225,51 @@ async function buildStatic(outDir: string, docsDir: string) {
203
225
  });
204
226
  }
205
227
 
228
+ function exportMarkdownRoutes(outDir: string) {
229
+ const distDir = join(outDir, "dist");
230
+ const mdRouteRoot = join(distDir, "md-file");
231
+ if (!existsSync(mdRouteRoot)) return;
232
+
233
+ let copied = 0;
234
+
235
+ function walk(relDir: string) {
236
+ const absDir = join(mdRouteRoot, relDir);
237
+ const entries = readdirSync(absDir, { withFileTypes: true });
238
+
239
+ for (const entry of entries) {
240
+ const relPath = relDir ? join(relDir, entry.name) : entry.name;
241
+ if (entry.isDirectory()) {
242
+ walk(relPath);
243
+ continue;
244
+ }
245
+ if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".md")) continue;
246
+
247
+ const src = join(mdRouteRoot, relPath);
248
+ const dest = join(distDir, relPath);
249
+ mkdirSync(dirname(dest), { recursive: true });
250
+ copyFileSync(src, dest);
251
+ copied += 1;
252
+ }
253
+ }
254
+
255
+ walk("");
256
+ console.log(`📝 Exported ${copied} markdown files to static route paths`);
257
+ }
258
+
206
259
  async function buildSite(docsDir: string) {
207
- const outDir = await generateProject(docsDir);
208
- await buildStatic(outDir, docsDir);
209
- const staticOutDir = join(outDir, "dist");
260
+ const docsOutDir = await generateProject(docsDir);
261
+ const runtimeOutDir = prepareRuntimeOutDir(docsOutDir);
262
+ await buildStatic(runtimeOutDir, docsDir);
263
+ exportMarkdownRoutes(runtimeOutDir);
264
+
265
+ if (!samePath(docsOutDir, runtimeOutDir)) {
266
+ const docsDistDir = join(docsOutDir, "dist");
267
+ const runtimeDistDir = join(runtimeOutDir, "dist");
268
+ rmSync(docsDistDir, { recursive: true, force: true });
269
+ cpSync(runtimeDistDir, docsDistDir, { recursive: true, force: true });
270
+ }
271
+
272
+ const staticOutDir = join(docsOutDir, "dist");
210
273
  console.log(`\n📁 Static site output: ${staticOutDir}`);
211
274
  }
212
275
 
@@ -227,8 +290,9 @@ function spawnServer(outDir: string, command: string, port: number, docsDir: str
227
290
  }
228
291
 
229
292
  async function run(docsDir: string, port: number) {
230
- const outDir = await generateProject(docsDir);
231
- spawnServer(outDir, "dev", port, docsDir);
293
+ const docsOutDir = await generateProject(docsDir);
294
+ const runtimeOutDir = prepareRuntimeOutDir(docsOutDir);
295
+ spawnServer(runtimeOutDir, "dev", port, docsDir);
232
296
  }
233
297
 
234
298
  // ── Parse args ───────────────────────────────────────────────────────────────────
@@ -243,15 +307,15 @@ if (!command || command === "--help" || command === "-h") {
243
307
 
244
308
  const docsDir = process.cwd();
245
309
 
246
- // init doesn't require velu.json
310
+ // init doesn't require docs.json
247
311
  if (command === "init") {
248
312
  init(docsDir);
249
313
  process.exit(0);
250
314
  }
251
315
 
252
- if (!existsSync(join(docsDir, "velu.json"))) {
253
- console.error("❌ No velu.json found in the current directory.");
254
- console.error(" Run `velu init` to scaffold a new project, or run from a directory containing velu.json.");
316
+ if (!resolveConfigPath(docsDir)) {
317
+ console.error("❌ No docs.json or velu.json found in the current directory.");
318
+ console.error(" Run `velu init` to scaffold a new project, or run from a directory containing docs.json.");
255
319
  process.exit(1);
256
320
  }
257
321