@raystack/chronicle 0.5.4 → 0.6.1

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 (68) hide show
  1. package/dist/cli/index.js +258 -80
  2. package/package.json +7 -5
  3. package/src/cli/commands/build.ts +5 -8
  4. package/src/cli/commands/dev.ts +5 -6
  5. package/src/cli/commands/init.test.ts +77 -0
  6. package/src/cli/commands/init.ts +73 -40
  7. package/src/cli/commands/serve.ts +6 -9
  8. package/src/cli/commands/start.ts +5 -5
  9. package/src/cli/utils/config.ts +6 -12
  10. package/src/cli/utils/scaffold.test.ts +179 -0
  11. package/src/cli/utils/scaffold.ts +70 -9
  12. package/src/components/api/field-row.tsx +1 -1
  13. package/src/components/api/field-section.tsx +2 -2
  14. package/src/components/mdx/index.tsx +1 -1
  15. package/src/components/ui/breadcrumbs.tsx +4 -2
  16. package/src/components/ui/client-theme-switcher.tsx +21 -4
  17. package/src/components/ui/search.module.css +16 -41
  18. package/src/components/ui/search.tsx +30 -41
  19. package/src/lib/config.test.ts +493 -0
  20. package/src/lib/config.ts +123 -22
  21. package/src/lib/head.tsx +23 -5
  22. package/src/lib/llms.test.ts +94 -0
  23. package/src/lib/llms.ts +41 -0
  24. package/src/lib/navigation.test.ts +94 -0
  25. package/src/lib/navigation.ts +51 -0
  26. package/src/lib/page-context.tsx +51 -32
  27. package/src/lib/route-resolver.test.ts +173 -0
  28. package/src/lib/route-resolver.ts +73 -0
  29. package/src/lib/source.ts +94 -1
  30. package/src/lib/version-source.test.ts +163 -0
  31. package/src/lib/version-source.ts +101 -0
  32. package/src/pages/ApiPage.tsx +1 -1
  33. package/src/pages/DocsLayout.tsx +24 -3
  34. package/src/pages/DocsPage.tsx +3 -6
  35. package/src/pages/LandingPage.module.css +56 -0
  36. package/src/pages/LandingPage.tsx +39 -0
  37. package/src/pages/NotFound.tsx +2 -0
  38. package/src/server/App.tsx +21 -23
  39. package/src/server/api/page.ts +5 -1
  40. package/src/server/api/search.ts +51 -24
  41. package/src/server/api/specs.ts +17 -5
  42. package/src/server/entry-client.tsx +42 -14
  43. package/src/server/entry-server.tsx +33 -11
  44. package/src/server/routes/[...slug].md.ts +0 -6
  45. package/src/server/routes/[version]/llms.txt.ts +26 -0
  46. package/src/server/routes/llms.txt.ts +10 -13
  47. package/src/server/routes/og.tsx +2 -2
  48. package/src/server/routes/sitemap.xml.ts +14 -6
  49. package/src/server/vite-config.ts +5 -5
  50. package/src/themes/default/ContentDirButtons.tsx +66 -0
  51. package/src/themes/default/Layout.module.css +187 -40
  52. package/src/themes/default/Layout.tsx +166 -65
  53. package/src/themes/default/OpenInAI.tsx +112 -0
  54. package/src/themes/default/Page.module.css +30 -0
  55. package/src/themes/default/Page.tsx +1 -3
  56. package/src/themes/default/SidebarLogo.tsx +26 -0
  57. package/src/themes/default/Toc.module.css +102 -25
  58. package/src/themes/default/Toc.tsx +56 -10
  59. package/src/themes/default/VersionSwitcher.tsx +59 -0
  60. package/src/themes/paper/ContentDirDropdown.tsx +47 -0
  61. package/src/themes/paper/Layout.module.css +7 -0
  62. package/src/themes/paper/Layout.tsx +20 -13
  63. package/src/themes/paper/VersionSwitcher.tsx +60 -0
  64. package/src/types/config.ts +145 -23
  65. package/src/types/content.ts +11 -1
  66. package/src/types/theme.ts +1 -0
  67. package/src/components/ui/footer.module.css +0 -27
  68. package/src/components/ui/footer.tsx +0 -30
package/dist/cli/index.js CHANGED
@@ -91,8 +91,9 @@ async function readChronicleConfig(projectRoot, configPath) {
91
91
  }
92
92
  }
93
93
  async function createViteConfig(options) {
94
- const { packageRoot, projectRoot, contentDir, configPath, preset } = options;
94
+ const { packageRoot, projectRoot, configPath, preset } = options;
95
95
  const rawConfig = await readChronicleConfig(projectRoot, configPath);
96
+ const contentMirror = path4.resolve(packageRoot, ".content");
96
97
  return {
97
98
  root: packageRoot,
98
99
  configFile: false,
@@ -134,9 +135,9 @@ async function createViteConfig(options) {
134
135
  ],
135
136
  resolve: {
136
137
  alias: {
137
- "@": path4.resolve(packageRoot, "src")
138
+ "@": path4.resolve(packageRoot, "src"),
139
+ tslib: "tslib/tslib.es6.js"
138
140
  },
139
- conditions: ["module-sync", "import", "node"],
140
141
  dedupe: [
141
142
  "react",
142
143
  "react-dom",
@@ -147,11 +148,11 @@ async function createViteConfig(options) {
147
148
  },
148
149
  server: {
149
150
  fs: {
150
- allow: [packageRoot, projectRoot, contentDir]
151
+ allow: [packageRoot, projectRoot, contentMirror]
151
152
  }
152
153
  },
153
154
  define: {
154
- __CHRONICLE_CONTENT_DIR__: JSON.stringify(contentDir),
155
+ __CHRONICLE_CONTENT_DIR__: JSON.stringify(contentMirror),
155
156
  __CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot),
156
157
  __CHRONICLE_CONFIG_RAW__: JSON.stringify(rawConfig)
157
158
  },
@@ -199,6 +200,7 @@ import chalk from "chalk";
199
200
  import { parse } from "yaml";
200
201
 
201
202
  // src/types/config.ts
203
+ import uniqBy from "lodash/uniqBy.js";
202
204
  import { z } from "zod";
203
205
  var logoSchema = z.object({
204
206
  light: z.string().optional(),
@@ -237,16 +239,10 @@ var apiSchema = z.object({
237
239
  name: z.string(),
238
240
  spec: z.string(),
239
241
  basePath: z.string(),
242
+ icon: z.string().optional(),
240
243
  server: apiServerSchema,
241
244
  auth: apiAuthSchema.optional()
242
245
  });
243
- var footerSchema = z.object({
244
- copyright: z.string().optional(),
245
- links: z.array(navLinkSchema).optional()
246
- });
247
- var llmsSchema = z.object({
248
- enabled: z.boolean().optional()
249
- });
250
246
  var googleAnalyticsSchema = z.object({
251
247
  measurementId: z.string()
252
248
  });
@@ -259,21 +255,112 @@ var telemetrySchema = z.object({
259
255
  serviceName: z.string().optional(),
260
256
  port: z.number().int().min(1).max(65535).default(9090)
261
257
  });
262
- var chronicleConfigSchema = z.object({
258
+ var siteSchema = z.object({
263
259
  title: z.string(),
264
- description: z.string().optional(),
260
+ description: z.string().optional()
261
+ });
262
+ var DIR_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
263
+ var dirNameSchema = z.string().min(1).refine((s) => DIR_NAME_PATTERN.test(s) && s !== "." && s !== "..", {
264
+ message: 'dir must start with a letter or digit and contain only letters, digits, ".", "_", or "-"'
265
+ });
266
+ var contentEntrySchema = z.object({
267
+ dir: dirNameSchema,
268
+ label: z.string().min(1),
269
+ icon: z.string().optional()
270
+ });
271
+ var badgeVariantSchema = z.enum([
272
+ "accent",
273
+ "warning",
274
+ "danger",
275
+ "success",
276
+ "neutral",
277
+ "gradient"
278
+ ]);
279
+ var badgeSchema = z.object({
280
+ label: z.string().min(1),
281
+ variant: badgeVariantSchema.default("accent")
282
+ });
283
+ var latestSchema = z.object({
284
+ label: z.string().min(1),
285
+ landing: z.boolean().optional()
286
+ });
287
+ var versionSchema = z.object({
288
+ dir: dirNameSchema,
289
+ label: z.string().min(1),
290
+ badge: badgeSchema.optional(),
291
+ landing: z.boolean().optional(),
292
+ content: z.array(contentEntrySchema).min(1),
293
+ api: z.array(apiSchema).optional()
294
+ });
295
+ var allUnique = (items, key) => uniqBy(items, key).length === items.length;
296
+ var RESERVED_ROUTE_SEGMENTS = [
297
+ "api",
298
+ "apis",
299
+ "og",
300
+ "llms.txt",
301
+ "robots.txt",
302
+ "sitemap.xml"
303
+ ];
304
+ var chronicleConfigSchema = z.object({
305
+ site: siteSchema,
265
306
  url: z.string().optional(),
266
- content: z.string().optional(),
307
+ content: z.array(contentEntrySchema).min(1),
308
+ latest: latestSchema.optional(),
309
+ versions: z.array(versionSchema).optional(),
267
310
  preset: z.string().optional(),
268
311
  logo: logoSchema.optional(),
269
312
  theme: themeSchema.optional(),
270
313
  navigation: navigationSchema.optional(),
271
314
  search: searchSchema.optional(),
272
- footer: footerSchema.optional(),
273
315
  api: z.array(apiSchema).optional(),
274
- llms: llmsSchema.optional(),
275
316
  analytics: analyticsSchema.optional(),
276
317
  telemetry: telemetrySchema.optional()
318
+ }).strict().refine((cfg) => allUnique(cfg.content, (c) => c.dir), {
319
+ message: "content[].dir must be unique",
320
+ path: ["content"]
321
+ }).refine((cfg) => !cfg.versions || allUnique(cfg.versions, (v) => v.dir), {
322
+ message: "versions[].dir must be unique",
323
+ path: ["versions"]
324
+ }).refine((cfg) => !cfg.versions || cfg.versions.every((v) => allUnique(v.content, (c) => c.dir)), {
325
+ message: "versions[].content[].dir must be unique within each version",
326
+ path: ["versions"]
327
+ }).refine((cfg) => !cfg.versions || cfg.versions.length === 0 || !!cfg.latest, {
328
+ message: "latest is required when versions are declared",
329
+ path: ["latest"]
330
+ }).refine((cfg) => {
331
+ if (!cfg.versions)
332
+ return true;
333
+ const contentDirs = new Set(cfg.content.map((c) => c.dir));
334
+ return !cfg.versions.some((v) => contentDirs.has(v.dir));
335
+ }, {
336
+ message: "versions[].dir must not overlap with content[].dir — the URL segment would be shadowed",
337
+ path: ["versions"]
338
+ }).superRefine((cfg, ctx) => {
339
+ const reserved = new Set(RESERVED_ROUTE_SEGMENTS);
340
+ const message = `dir must not be a reserved route segment: ${RESERVED_ROUTE_SEGMENTS.join(", ")}`;
341
+ cfg.content.forEach((c, i) => {
342
+ if (reserved.has(c.dir)) {
343
+ ctx.addIssue({ code: "custom", message, path: ["content", i, "dir"] });
344
+ }
345
+ });
346
+ cfg.versions?.forEach((v, vi) => {
347
+ if (reserved.has(v.dir)) {
348
+ ctx.addIssue({
349
+ code: "custom",
350
+ message,
351
+ path: ["versions", vi, "dir"]
352
+ });
353
+ }
354
+ v.content.forEach((c, ci) => {
355
+ if (reserved.has(c.dir)) {
356
+ ctx.addIssue({
357
+ code: "custom",
358
+ message,
359
+ path: ["versions", vi, "content", ci, "dir"]
360
+ });
361
+ }
362
+ });
363
+ });
277
364
  });
278
365
  // src/cli/utils/config.ts
279
366
  function resolveConfigPath(configPath) {
@@ -299,20 +386,13 @@ function validateConfig(raw, configPath) {
299
386
  if (!result.success) {
300
387
  console.log(chalk.red(`Error: Invalid chronicle.yaml at '${configPath}'`));
301
388
  for (const issue of result.error.issues) {
302
- const path2 = issue.path.join(".");
303
- console.log(chalk.gray(` ${path2 ? `${path2}: ` : ""}${issue.message}`));
389
+ const issuePath = issue.path.join(".");
390
+ console.log(chalk.gray(` ${issuePath ? `${issuePath}: ` : ""}${issue.message}`));
304
391
  }
305
392
  process.exit(1);
306
393
  }
307
394
  return result.data;
308
395
  }
309
- function resolveContentDir(config2, configPath, contentFlag) {
310
- if (contentFlag)
311
- return path.resolve(contentFlag);
312
- if (config2.content)
313
- return path.resolve(path.dirname(configPath), config2.content);
314
- return path.resolve("content");
315
- }
316
396
  function resolvePreset(config2, presetFlag) {
317
397
  return presetFlag ?? config2.preset;
318
398
  }
@@ -320,9 +400,9 @@ async function loadCLIConfig(configPath, options) {
320
400
  const resolvedConfigPath = resolveConfigPath(configPath) ?? path.join(process.cwd(), "chronicle.yaml");
321
401
  const raw = await readConfig(resolvedConfigPath);
322
402
  const config2 = validateConfig(raw, resolvedConfigPath);
323
- const contentDir = resolveContentDir(config2, resolvedConfigPath, options?.content);
403
+ const projectRoot = path.dirname(resolvedConfigPath);
324
404
  const preset = resolvePreset(config2, options?.preset);
325
- return { config: config2, configPath: resolvedConfigPath, contentDir, preset };
405
+ return { config: config2, configPath: resolvedConfigPath, projectRoot, preset };
326
406
  }
327
407
 
328
408
  // src/cli/utils/resolve.ts
@@ -333,36 +413,116 @@ var PACKAGE_ROOT = path2.resolve(path2.dirname(fileURLToPath(import.meta.url)),
333
413
  // src/cli/utils/scaffold.ts
334
414
  import fs2 from "node:fs/promises";
335
415
  import path3 from "node:path";
336
- async function linkContent(contentDir) {
337
- const linkPath = path3.join(PACKAGE_ROOT, ".content");
338
- const target = path3.resolve(contentDir);
416
+
417
+ // src/lib/config.ts
418
+ import { parse as parse2 } from "yaml";
419
+ var defaultConfig = chronicleConfigSchema.parse({
420
+ site: { title: "Documentation" },
421
+ content: [{ dir: "docs", label: "Docs" }],
422
+ theme: { name: "default" },
423
+ search: { enabled: true, placeholder: "Search..." }
424
+ });
425
+ function getLatestContentRoots(config2) {
426
+ return config2.content.map((c) => ({
427
+ versionDir: null,
428
+ versionLabel: config2.latest?.label ?? null,
429
+ contentDir: c.dir,
430
+ contentLabel: c.label,
431
+ contentIcon: c.icon,
432
+ fsPath: `content/${c.dir}`,
433
+ urlPrefix: `/${c.dir}`
434
+ }));
435
+ }
436
+ function getVersionContentRoots(config2, versionDir) {
437
+ const version = config2.versions?.find((v) => v.dir === versionDir);
438
+ if (!version)
439
+ return [];
440
+ return version.content.map((c) => ({
441
+ versionDir: version.dir,
442
+ versionLabel: version.label,
443
+ contentDir: c.dir,
444
+ contentLabel: c.label,
445
+ contentIcon: c.icon,
446
+ fsPath: `versions/${version.dir}/${c.dir}`,
447
+ urlPrefix: `/${version.dir}/${c.dir}`
448
+ }));
449
+ }
450
+
451
+ // src/cli/utils/scaffold.ts
452
+ async function buildContentMirror(mirrorRoot, projectRoot, config2) {
453
+ await removeMirror(mirrorRoot);
454
+ await fs2.mkdir(mirrorRoot, { recursive: true });
455
+ for (const root of getLatestContentRoots(config2)) {
456
+ const source = path3.resolve(projectRoot, root.fsPath);
457
+ const dest = path3.join(mirrorRoot, root.contentDir);
458
+ await mirrorTree(source, dest);
459
+ }
460
+ for (const version of config2.versions ?? []) {
461
+ const versionMirror = path3.join(mirrorRoot, version.dir);
462
+ await fs2.mkdir(versionMirror, { recursive: true });
463
+ for (const root of getVersionContentRoots(config2, version.dir)) {
464
+ const source = path3.resolve(projectRoot, root.fsPath);
465
+ const dest = path3.join(versionMirror, root.contentDir);
466
+ await mirrorTree(source, dest);
467
+ }
468
+ }
469
+ }
470
+ function linkContent(projectRoot, config2) {
471
+ return buildContentMirror(path3.join(PACKAGE_ROOT, ".content"), projectRoot, config2);
472
+ }
473
+ async function mirrorTree(source, dest) {
474
+ let entries;
339
475
  try {
340
- const existing = await fs2.readlink(linkPath);
341
- if (existing === target)
476
+ entries = await fs2.readdir(source, { withFileTypes: true });
477
+ } catch (error) {
478
+ const err = error;
479
+ if (err.code === "ENOENT") {
480
+ throw new Error(`Content directory not found: ${source}`);
481
+ }
482
+ throw error;
483
+ }
484
+ await fs2.mkdir(dest, { recursive: true });
485
+ for (const entry of entries) {
486
+ const sourcePath = path3.join(source, entry.name);
487
+ const destPath = path3.join(dest, entry.name);
488
+ if (entry.isDirectory()) {
489
+ await mirrorTree(sourcePath, destPath);
490
+ } else if (entry.isFile() || entry.isSymbolicLink()) {
491
+ await fs2.symlink(sourcePath, destPath);
492
+ }
493
+ }
494
+ }
495
+ async function removeMirror(mirrorRoot) {
496
+ try {
497
+ const stat = await fs2.lstat(mirrorRoot);
498
+ if (stat.isSymbolicLink() || stat.isFile()) {
499
+ await fs2.unlink(mirrorRoot);
500
+ } else if (stat.isDirectory()) {
501
+ await fs2.rm(mirrorRoot, { recursive: true, force: true });
502
+ }
503
+ } catch (error) {
504
+ if (error.code === "ENOENT")
342
505
  return;
343
- await fs2.unlink(linkPath);
344
- } catch {}
345
- await fs2.symlink(target, linkPath);
506
+ throw error;
507
+ }
346
508
  }
347
509
 
348
510
  // src/cli/commands/build.ts
349
- var buildCommand = new Command("build").description("Build for production").option("--content <path>", "Content directory").option("--config <path>", "Path to chronicle.yaml").option("--preset <preset>", "Deploy preset (vercel, cloudflare, node-server)").action(async (options) => {
350
- const { contentDir, configPath, preset } = await loadCLIConfig(options.config, {
351
- content: options.content,
511
+ var buildCommand = new Command("build").description("Build for production").option("--config <path>", "Path to chronicle.yaml").option("--preset <preset>", "Deploy preset (vercel, cloudflare, node-server)").action(async (options) => {
512
+ const { config: config2, projectRoot, configPath, preset } = await loadCLIConfig(options.config, {
352
513
  preset: options.preset
353
514
  });
354
- await linkContent(contentDir);
515
+ await linkContent(projectRoot, config2);
355
516
  console.log(chalk2.cyan("Building for production..."));
356
517
  const { createBuilder } = await import("vite");
357
518
  const { createViteConfig: createViteConfig2 } = await Promise.resolve().then(() => (init_vite_config(), exports_vite_config));
358
- const config2 = await createViteConfig2({
519
+ const viteConfig = await createViteConfig2({
359
520
  packageRoot: PACKAGE_ROOT,
360
- projectRoot: process.cwd(),
361
- contentDir,
521
+ projectRoot,
362
522
  configPath,
363
523
  preset
364
524
  });
365
- const builder = await createBuilder({ ...config2, builder: {} });
525
+ const builder = await createBuilder({ ...viteConfig, builder: {} });
366
526
  await builder.buildApp();
367
527
  console.log(chalk2.green("Build complete"));
368
528
  console.log(chalk2.cyan("Run `chronicle start` to start the server"));
@@ -371,17 +531,17 @@ var buildCommand = new Command("build").description("Build for production").opti
371
531
  // src/cli/commands/dev.ts
372
532
  import chalk3 from "chalk";
373
533
  import { Command as Command2 } from "commander";
374
- var devCommand = new Command2("dev").description("Start development server").option("-p, --port <port>", "Port number", "3000").option("--content <path>", "Content directory").option("--config <path>", "Path to chronicle.yaml").option("--host <host>", "Host address", "localhost").action(async (options) => {
375
- const { contentDir, configPath } = await loadCLIConfig(options.config, { content: options.content });
534
+ var devCommand = new Command2("dev").description("Start development server").option("-p, --port <port>", "Port number", "3000").option("--config <path>", "Path to chronicle.yaml").option("--host <host>", "Host address", "localhost").action(async (options) => {
535
+ const { config: config2, projectRoot, configPath } = await loadCLIConfig(options.config);
376
536
  const port = parseInt(options.port, 10);
377
- await linkContent(contentDir);
537
+ await linkContent(projectRoot, config2);
378
538
  console.log(chalk3.cyan("Starting dev server..."));
379
539
  const { createServer } = await import("vite");
380
540
  const { createViteConfig: createViteConfig2 } = await Promise.resolve().then(() => (init_vite_config(), exports_vite_config));
381
- const config2 = await createViteConfig2({ packageRoot: PACKAGE_ROOT, projectRoot: process.cwd(), contentDir, configPath });
541
+ const viteConfig = await createViteConfig2({ packageRoot: PACKAGE_ROOT, projectRoot, configPath });
382
542
  const server = await createServer({
383
- ...config2,
384
- server: { ...config2.server, port, host: options.host }
543
+ ...viteConfig,
544
+ server: { ...viteConfig.server, port, host: options.host }
385
545
  });
386
546
  await server.listen();
387
547
  server.printUrls();
@@ -393,9 +553,12 @@ import path5 from "node:path";
393
553
  import chalk4 from "chalk";
394
554
  import { Command as Command3 } from "commander";
395
555
  import { stringify } from "yaml";
396
- var defaultConfig = {
397
- title: "My Documentation",
398
- description: "Documentation powered by Chronicle",
556
+ var defaultInitConfig = {
557
+ site: {
558
+ title: "My Documentation",
559
+ description: "Documentation powered by Chronicle"
560
+ },
561
+ content: [{ dir: "docs", label: "Docs" }],
399
562
  theme: { name: "default" },
400
563
  search: { enabled: true, placeholder: "Search documentation..." }
401
564
  };
@@ -409,44 +572,61 @@ order: 1
409
572
 
410
573
  This is your documentation home page.
411
574
  `;
412
- var initCommand = new Command3("init").description("Initialize a new Chronicle project").option("-c, --content <path>", "Content directory name", "content").action((options) => {
413
- const projectDir = process.cwd();
414
- const contentDir = path5.join(projectDir, options.content);
575
+ var GITIGNORE_ENTRIES = ["node_modules", "dist", ".output"];
576
+ function runInit(projectDir) {
577
+ const events = [];
578
+ const defaultDir = defaultInitConfig.content[0].dir;
579
+ const contentDir = path5.join(projectDir, "content", defaultDir);
415
580
  if (!fs4.existsSync(contentDir)) {
416
581
  fs4.mkdirSync(contentDir, { recursive: true });
417
- console.log(chalk4.green("✓"), "Created", contentDir);
582
+ events.push({ type: "created", path: contentDir });
418
583
  }
419
584
  const configPath = path5.join(projectDir, "chronicle.yaml");
420
585
  if (!fs4.existsSync(configPath)) {
421
- fs4.writeFileSync(configPath, stringify(defaultConfig));
422
- console.log(chalk4.green("✓"), "Created", configPath);
586
+ fs4.writeFileSync(configPath, stringify(defaultInitConfig));
587
+ events.push({ type: "created", path: configPath });
423
588
  } else {
424
- console.log(chalk4.yellow(""), configPath, "already exists");
589
+ events.push({ type: "skipped", path: configPath, detail: "already exists" });
425
590
  }
426
591
  const contentFiles = fs4.readdirSync(contentDir);
427
592
  if (contentFiles.length === 0) {
428
593
  const indexPath = path5.join(contentDir, "index.mdx");
429
594
  fs4.writeFileSync(indexPath, sampleMdx);
430
- console.log(chalk4.green("✓"), "Created", indexPath);
595
+ events.push({ type: "created", path: indexPath });
431
596
  }
432
597
  const gitignorePath = path5.join(projectDir, ".gitignore");
433
- const gitignoreEntries = ["node_modules", "dist", ".output"];
434
598
  if (fs4.existsSync(gitignorePath)) {
435
599
  const existing = fs4.readFileSync(gitignorePath, "utf-8");
436
- const missing = gitignoreEntries.filter((e) => !existing.includes(e));
600
+ const existingLines = new Set(existing.split(/\r?\n/).map((l) => l.trim()).filter(Boolean));
601
+ const missing = GITIGNORE_ENTRIES.filter((e) => !existingLines.has(e));
437
602
  if (missing.length > 0) {
438
603
  fs4.appendFileSync(gitignorePath, `
439
604
  ${missing.join(`
440
605
  `)}
441
606
  `);
442
- console.log(chalk4.green(""), "Added", missing.join(", "), "to .gitignore");
607
+ events.push({ type: "updated", path: gitignorePath, detail: missing.join(", ") });
443
608
  }
444
609
  } else {
445
- fs4.writeFileSync(gitignorePath, `${gitignoreEntries.join(`
610
+ fs4.writeFileSync(gitignorePath, `${GITIGNORE_ENTRIES.join(`
446
611
  `)}
447
612
  `);
448
- console.log(chalk4.green(""), "Created .gitignore");
613
+ events.push({ type: "created", path: gitignorePath });
614
+ }
615
+ return events;
616
+ }
617
+ function formatEvent(e) {
618
+ if (e.type === "skipped") {
619
+ return `${chalk4.yellow("⚠")} ${e.path}${e.detail ? ` ${e.detail}` : ""}`;
449
620
  }
621
+ if (e.type === "updated") {
622
+ return `${chalk4.green("✓")} Updated ${e.path}${e.detail ? ` (+${e.detail})` : ""}`;
623
+ }
624
+ return `${chalk4.green("✓")} Created ${e.path}`;
625
+ }
626
+ var initCommand = new Command3("init").description("Initialize a new Chronicle project").action(() => {
627
+ const events = runInit(process.cwd());
628
+ for (const e of events)
629
+ console.log(formatEvent(e));
450
630
  console.log(chalk4.green(`
451
631
  ✓ Chronicle initialized!`));
452
632
  console.log(`
@@ -456,27 +636,25 @@ Run`, chalk4.cyan("chronicle dev"), "to start development server");
456
636
  // src/cli/commands/serve.ts
457
637
  import chalk5 from "chalk";
458
638
  import { Command as Command4 } from "commander";
459
- var serveCommand = new Command4("serve").description("Build and start production server").option("-p, --port <port>", "Port number", "3000").option("--content <path>", "Content directory").option("--config <path>", "Path to chronicle.yaml").option("--host <host>", "Host address", "localhost").option("--preset <preset>", "Deploy preset (vercel, cloudflare, node-server)").action(async (options) => {
460
- const { contentDir, configPath, preset } = await loadCLIConfig(options.config, {
461
- content: options.content,
639
+ var serveCommand = new Command4("serve").description("Build and start production server").option("-p, --port <port>", "Port number", "3000").option("--config <path>", "Path to chronicle.yaml").option("--host <host>", "Host address", "localhost").option("--preset <preset>", "Deploy preset (vercel, cloudflare, node-server)").action(async (options) => {
640
+ const { config: config2, projectRoot, configPath, preset } = await loadCLIConfig(options.config, {
462
641
  preset: options.preset
463
642
  });
464
643
  const port = parseInt(options.port, 10);
465
- await linkContent(contentDir);
644
+ await linkContent(projectRoot, config2);
466
645
  const { build, preview } = await import("vite");
467
646
  const { createViteConfig: createViteConfig2 } = await Promise.resolve().then(() => (init_vite_config(), exports_vite_config));
468
- const config2 = await createViteConfig2({
647
+ const viteConfig = await createViteConfig2({
469
648
  packageRoot: PACKAGE_ROOT,
470
- projectRoot: process.cwd(),
471
- contentDir,
649
+ projectRoot,
472
650
  configPath,
473
651
  preset
474
652
  });
475
653
  console.log(chalk5.cyan("Building for production..."));
476
- await build(config2);
654
+ await build(viteConfig);
477
655
  console.log(chalk5.cyan("Starting production server..."));
478
656
  const server = await preview({
479
- ...config2,
657
+ ...viteConfig,
480
658
  preview: { port, host: options.host }
481
659
  });
482
660
  server.printUrls();
@@ -485,16 +663,16 @@ var serveCommand = new Command4("serve").description("Build and start production
485
663
  // src/cli/commands/start.ts
486
664
  import chalk6 from "chalk";
487
665
  import { Command as Command5 } from "commander";
488
- var startCommand = new Command5("start").description("Start production server").option("-p, --port <port>", "Port number", "3000").option("--content <path>", "Content directory").option("--host <host>", "Host address", "localhost").action(async (options) => {
489
- const { contentDir, configPath } = await loadCLIConfig(undefined, { content: options.content });
666
+ var startCommand = new Command5("start").description("Start production server").option("-p, --port <port>", "Port number", "3000").option("--config <path>", "Path to chronicle.yaml").option("--host <host>", "Host address", "localhost").action(async (options) => {
667
+ const { config: config2, projectRoot, configPath } = await loadCLIConfig(options.config);
490
668
  const port = parseInt(options.port, 10);
491
- await linkContent(contentDir);
669
+ await linkContent(projectRoot, config2);
492
670
  console.log(chalk6.cyan("Starting production server..."));
493
671
  const { preview } = await import("vite");
494
672
  const { createViteConfig: createViteConfig2 } = await Promise.resolve().then(() => (init_vite_config(), exports_vite_config));
495
- const config2 = await createViteConfig2({ packageRoot: PACKAGE_ROOT, projectRoot: process.cwd(), contentDir, configPath });
673
+ const viteConfig = await createViteConfig2({ packageRoot: PACKAGE_ROOT, projectRoot, configPath });
496
674
  const server = await preview({
497
- ...config2,
675
+ ...viteConfig,
498
676
  preview: { port, host: options.host }
499
677
  });
500
678
  server.printUrls();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raystack/chronicle",
3
- "version": "0.5.4",
3
+ "version": "0.6.1",
4
4
  "description": "Config-driven documentation framework",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -16,7 +16,8 @@
16
16
  },
17
17
  "scripts": {
18
18
  "build:cli": "bun build-cli.ts",
19
- "lint": "biome lint src/"
19
+ "lint": "biome lint src/",
20
+ "test": "bun test"
20
21
  },
21
22
  "devDependencies": {
22
23
  "@biomejs/biome": "^2.3.13",
@@ -41,15 +42,15 @@
41
42
  "@opentelemetry/resources": "^2.6.1",
42
43
  "@opentelemetry/sdk-metrics": "^2.6.1",
43
44
  "@opentelemetry/semantic-conventions": "^1.40.0",
44
- "@raystack/apsara": "0.55.1",
45
+ "@raystack/apsara": "1.0.0-rc.4",
45
46
  "@shikijs/rehype": "^4.0.2",
46
47
  "@vitejs/plugin-react": "^6.0.1",
47
48
  "chalk": "^5.6.2",
48
49
  "class-variance-authority": "^0.7.1",
49
50
  "codemirror": "^6.0.2",
50
51
  "commander": "^14.0.2",
51
- "fumadocs-core": "16.6.15",
52
- "fumadocs-mdx": "14.2.6",
52
+ "fumadocs-core": "16.8.1",
53
+ "fumadocs-mdx": "14.3.1",
53
54
  "glob": "^11.0.0",
54
55
  "gray-matter": "^4.0.3",
55
56
  "h3": "^2.0.1-rc.16",
@@ -68,6 +69,7 @@
68
69
  "remark-parse": "^11.0.0",
69
70
  "satori": "^0.25.0",
70
71
  "slugify": "^1.6.6",
72
+ "std-env": "^4.1.0",
71
73
  "unified": "^11.0.5",
72
74
  "unist-util-visit": "^5.1.0",
73
75
  "vite": "8.0.3",
@@ -6,33 +6,30 @@ import { linkContent } from '@/cli/utils/scaffold';
6
6
 
7
7
  export const buildCommand = new Command('build')
8
8
  .description('Build for production')
9
- .option('--content <path>', 'Content directory')
10
9
  .option('--config <path>', 'Path to chronicle.yaml')
11
10
  .option(
12
11
  '--preset <preset>',
13
12
  'Deploy preset (vercel, cloudflare, node-server)'
14
13
  )
15
14
  .action(async options => {
16
- const { contentDir, configPath, preset } = await loadCLIConfig(options.config, {
17
- content: options.content,
15
+ const { config, projectRoot, configPath, preset } = await loadCLIConfig(options.config, {
18
16
  preset: options.preset,
19
17
  });
20
- await linkContent(contentDir);
18
+ await linkContent(projectRoot, config);
21
19
 
22
20
  console.log(chalk.cyan('Building for production...'));
23
21
 
24
22
  const { createBuilder } = await import('vite');
25
23
  const { createViteConfig } = await import('@/server/vite-config');
26
24
 
27
- const config = await createViteConfig({
25
+ const viteConfig = await createViteConfig({
28
26
  packageRoot: PACKAGE_ROOT,
29
- projectRoot: process.cwd(),
30
- contentDir,
27
+ projectRoot,
31
28
  configPath,
32
29
  preset
33
30
  });
34
31
 
35
- const builder = await createBuilder({ ...config, builder: {} });
32
+ const builder = await createBuilder({ ...viteConfig, builder: {} });
36
33
  await builder.buildApp();
37
34
 
38
35
  console.log(chalk.green('Build complete'));
@@ -7,24 +7,23 @@ import { linkContent } from '@/cli/utils/scaffold';
7
7
  export const devCommand = new Command('dev')
8
8
  .description('Start development server')
9
9
  .option('-p, --port <port>', 'Port number', '3000')
10
- .option('--content <path>', 'Content directory')
11
10
  .option('--config <path>', 'Path to chronicle.yaml')
12
11
  .option('--host <host>', 'Host address', 'localhost')
13
12
  .action(async options => {
14
- const { contentDir, configPath } = await loadCLIConfig(options.config, { content: options.content });
13
+ const { config, projectRoot, configPath } = await loadCLIConfig(options.config);
15
14
  const port = parseInt(options.port, 10);
16
15
 
17
- await linkContent(contentDir);
16
+ await linkContent(projectRoot, config);
18
17
 
19
18
  console.log(chalk.cyan('Starting dev server...'));
20
19
 
21
20
  const { createServer } = await import('vite');
22
21
  const { createViteConfig } = await import('@/server/vite-config');
23
22
 
24
- const config = await createViteConfig({ packageRoot: PACKAGE_ROOT, projectRoot: process.cwd(), contentDir, configPath });
23
+ const viteConfig = await createViteConfig({ packageRoot: PACKAGE_ROOT, projectRoot, configPath });
25
24
  const server = await createServer({
26
- ...config,
27
- server: { ...config.server, port, host: options.host }
25
+ ...viteConfig,
26
+ server: { ...viteConfig.server, port, host: options.host }
28
27
  });
29
28
 
30
29
  await server.listen();