@raystack/chronicle 0.1.0-canary.e11f924 → 0.1.0-canary.f0d9bde

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 (81) hide show
  1. package/dist/cli/index.js +878 -9684
  2. package/package.json +17 -12
  3. package/src/cli/__tests__/config.test.ts +25 -0
  4. package/src/cli/__tests__/scaffold.test.ts +10 -0
  5. package/src/cli/commands/build.ts +66 -19
  6. package/src/cli/commands/dev.ts +9 -22
  7. package/src/cli/commands/init.ts +107 -11
  8. package/src/cli/commands/serve.ts +36 -35
  9. package/src/cli/commands/start.ts +11 -21
  10. package/src/cli/utils/config.ts +2 -2
  11. package/src/cli/utils/index.ts +1 -1
  12. package/src/cli/utils/resolve.ts +6 -0
  13. package/src/cli/utils/scaffold.ts +20 -0
  14. package/src/components/mdx/code.tsx +10 -1
  15. package/src/components/mdx/details.module.css +1 -24
  16. package/src/components/mdx/details.tsx +2 -3
  17. package/src/components/mdx/image.tsx +5 -19
  18. package/src/components/mdx/index.tsx +3 -3
  19. package/src/components/mdx/link.tsx +10 -11
  20. package/src/components/ui/footer.tsx +3 -2
  21. package/src/components/ui/search.module.css +7 -0
  22. package/src/components/ui/search.tsx +62 -87
  23. package/src/lib/config.ts +9 -0
  24. package/src/lib/head.tsx +45 -0
  25. package/src/lib/page-context.tsx +95 -0
  26. package/src/lib/source.ts +92 -21
  27. package/src/{app/apis/[[...slug]]/layout.tsx → pages/ApiLayout.tsx} +10 -7
  28. package/src/pages/ApiPage.tsx +68 -0
  29. package/src/pages/DocsLayout.tsx +18 -0
  30. package/src/pages/DocsPage.tsx +43 -0
  31. package/src/pages/NotFound.tsx +10 -0
  32. package/src/pages/__tests__/head.test.tsx +57 -0
  33. package/src/server/App.tsx +59 -0
  34. package/src/server/__tests__/entry-server.test.tsx +35 -0
  35. package/src/server/__tests__/handlers.test.ts +77 -0
  36. package/src/server/__tests__/og.test.ts +23 -0
  37. package/src/server/__tests__/router.test.ts +72 -0
  38. package/src/server/__tests__/vite-config.test.ts +25 -0
  39. package/src/server/adapters/vercel.ts +133 -0
  40. package/src/server/build-search-index.ts +107 -0
  41. package/src/server/dev.ts +158 -0
  42. package/src/server/entry-client.tsx +74 -0
  43. package/src/server/entry-prod.ts +98 -0
  44. package/src/server/entry-server.tsx +35 -0
  45. package/src/server/entry-vercel.ts +28 -0
  46. package/src/server/handlers/apis-proxy.ts +57 -0
  47. package/src/{app/api/health/route.ts → server/handlers/health.ts} +1 -1
  48. package/src/server/handlers/llms.ts +58 -0
  49. package/src/server/handlers/og.ts +87 -0
  50. package/src/server/handlers/robots.ts +11 -0
  51. package/src/server/handlers/search.ts +172 -0
  52. package/src/server/handlers/sitemap.ts +39 -0
  53. package/src/server/handlers/specs.ts +9 -0
  54. package/src/server/index.html +12 -0
  55. package/src/server/prod.ts +18 -0
  56. package/src/server/request-handler.ts +64 -0
  57. package/src/server/router.ts +42 -0
  58. package/src/server/utils/safe-path.ts +14 -0
  59. package/src/server/vite-config.ts +71 -0
  60. package/src/themes/default/Layout.tsx +9 -10
  61. package/src/themes/default/Page.module.css +60 -0
  62. package/src/themes/default/font.ts +4 -6
  63. package/src/themes/paper/ChapterNav.tsx +5 -6
  64. package/src/themes/paper/Page.tsx +8 -9
  65. package/src/types/config.ts +11 -0
  66. package/src/types/content.ts +1 -0
  67. package/tsconfig.json +29 -0
  68. package/next.config.mjs +0 -10
  69. package/source.config.ts +0 -50
  70. package/src/app/[[...slug]]/layout.tsx +0 -15
  71. package/src/app/[[...slug]]/page.tsx +0 -57
  72. package/src/app/api/apis-proxy/route.ts +0 -59
  73. package/src/app/api/search/route.ts +0 -90
  74. package/src/app/apis/[[...slug]]/page.tsx +0 -57
  75. package/src/app/layout.tsx +0 -26
  76. package/src/app/llms-full.txt/route.ts +0 -18
  77. package/src/app/llms.txt/route.ts +0 -15
  78. package/src/app/providers.tsx +0 -8
  79. package/src/cli/utils/process.ts +0 -7
  80. package/src/lib/get-llm-text.ts +0 -10
  81. /package/src/{app/apis/[[...slug]]/layout.module.css → pages/ApiLayout.module.css} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raystack/chronicle",
3
- "version": "0.1.0-canary.e11f924",
3
+ "version": "0.1.0-canary.f0d9bde",
4
4
  "description": "Config-driven documentation framework",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -9,8 +9,7 @@
9
9
  "dist",
10
10
  "src",
11
11
  "templates",
12
- "next.config.mjs",
13
- "source.config.ts"
12
+ "tsconfig.json"
14
13
  ],
15
14
  "bin": {
16
15
  "chronicle": "./bin/chronicle.js"
@@ -27,7 +26,6 @@
27
26
  "@types/react": "^19.2.10",
28
27
  "@types/react-dom": "^19.2.3",
29
28
  "@types/semver": "^7.7.1",
30
- "openapi-types": "^12.1.3",
31
29
  "semver": "^7.7.4",
32
30
  "typescript": "5.9.3"
33
31
  },
@@ -37,26 +35,33 @@
37
35
  "@codemirror/theme-one-dark": "^6.1.3",
38
36
  "@codemirror/view": "^6.39.14",
39
37
  "@heroicons/react": "^2.2.0",
38
+ "@mdx-js/rollup": "^3.1.1",
40
39
  "@raystack/apsara": "^0.56.0",
41
- "@types/unist": "^3.0.3",
40
+ "@vitejs/plugin-react": "^6.0.1",
42
41
  "chalk": "^5.6.2",
43
42
  "class-variance-authority": "^0.7.1",
44
43
  "codemirror": "^6.0.2",
45
44
  "commander": "^14.0.2",
46
- "fumadocs-core": "16.6.15",
47
- "fumadocs-mdx": "^14.2.6",
45
+ "glob": "^11.0.0",
46
+ "gray-matter": "^4.0.3",
48
47
  "lodash": "^4.17.23",
49
48
  "mermaid": "^11.13.0",
50
- "next": "16.1.6",
49
+ "openapi-types": "^12.1.3",
51
50
  "react": "^19.0.0",
52
- "react-device-detect": "^2.2.3",
53
51
  "react-dom": "^19.0.0",
54
- "remark-attr": "^0.11.1",
52
+ "react-router-dom": "^7.13.1",
55
53
  "remark-directive": "^4.0.0",
54
+ "remark-gfm": "^4.0.1",
55
+ "@shikijs/rehype": "^4.0.2",
56
+ "remark-frontmatter": "^5.0.0",
57
+ "remark-mdx-frontmatter": "^5.2.0",
58
+ "satori": "^0.25.0",
59
+ "sirv": "^3.0.1",
56
60
  "slugify": "^1.6.6",
57
61
  "unified": "^11.0.5",
58
62
  "unist-util-visit": "^5.1.0",
59
- "yaml": "^2.8.2",
60
- "zod": "^4.3.6"
63
+ "minisearch": "^7.2.0",
64
+ "vite": "^8.0.0",
65
+ "yaml": "^2.8.2"
61
66
  }
62
67
  }
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { resolveContentDir } from '../utils/config'
3
+
4
+ describe('resolveContentDir', () => {
5
+ it('returns flag value when provided', () => {
6
+ const result = resolveContentDir('/custom/path')
7
+ expect(result).toBe('/custom/path')
8
+ })
9
+
10
+ it('returns env var when set', () => {
11
+ const original = process.env.CHRONICLE_CONTENT_DIR
12
+ process.env.CHRONICLE_CONTENT_DIR = '/env/content'
13
+ const result = resolveContentDir()
14
+ expect(result).toContain('env/content')
15
+ process.env.CHRONICLE_CONTENT_DIR = original
16
+ })
17
+
18
+ it('defaults to content directory', () => {
19
+ const original = process.env.CHRONICLE_CONTENT_DIR
20
+ delete process.env.CHRONICLE_CONTENT_DIR
21
+ const result = resolveContentDir()
22
+ expect(result).toContain('content')
23
+ process.env.CHRONICLE_CONTENT_DIR = original
24
+ })
25
+ })
@@ -0,0 +1,10 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { detectPackageManager } from '../utils/scaffold'
3
+
4
+ describe('detectPackageManager', () => {
5
+ it('returns a string', () => {
6
+ const result = detectPackageManager()
7
+ expect(typeof result).toBe('string')
8
+ expect(['npm', 'bun', 'pnpm', 'yarn']).toContain(result)
9
+ })
10
+ })
@@ -1,33 +1,80 @@
1
1
  import { Command } from 'commander'
2
- import { spawn } from 'child_process'
3
2
  import path from 'path'
4
- import { fileURLToPath } from 'url'
5
- import { createRequire } from 'module'
6
3
  import chalk from 'chalk'
7
- import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers } from '@/cli/utils'
8
-
9
- const require = createRequire(import.meta.url)
10
- const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
11
- const nextCli = require.resolve('next/dist/bin/next')
4
+ import { resolveContentDir } from '@/cli/utils/config'
5
+ import { PACKAGE_ROOT } from '@/cli/utils/resolve'
12
6
 
13
7
  export const buildCommand = new Command('build')
14
8
  .description('Build for production')
15
9
  .option('-c, --content <path>', 'Content directory')
16
- .action((options) => {
10
+ .option('-o, --outDir <path>', 'Output directory', 'dist')
11
+ .option('--adapter <adapter>', 'Deploy adapter (vercel)')
12
+ .action(async (options) => {
17
13
  const contentDir = resolveContentDir(options.content)
18
- loadCLIConfig(contentDir)
14
+ const outDir = path.resolve(options.outDir)
15
+
16
+ const VALID_ADAPTERS = ['vercel']
17
+ if (options.adapter && !VALID_ADAPTERS.includes(options.adapter)) {
18
+ console.error(chalk.red(`Unknown adapter: ${options.adapter}. Valid adapters: ${VALID_ADAPTERS.join(', ')}`))
19
+ process.exit(1)
20
+ }
21
+
22
+ process.env.CHRONICLE_PROJECT_ROOT = process.cwd()
23
+ process.env.CHRONICLE_CONTENT_DIR = contentDir
19
24
 
20
25
  console.log(chalk.cyan('Building for production...'))
21
- console.log(chalk.gray(`Content: ${contentDir}`))
22
-
23
- const child = spawn(process.execPath, [nextCli, 'build'], {
24
- stdio: 'inherit',
25
- cwd: PACKAGE_ROOT,
26
- env: {
27
- ...process.env,
28
- CHRONICLE_CONTENT_DIR: contentDir,
26
+
27
+ const { build } = await import('vite')
28
+ const { createViteConfig } = await import('@/server/vite-config')
29
+
30
+ const baseConfig = await createViteConfig({ root: PACKAGE_ROOT, contentDir })
31
+
32
+ // Build client bundle
33
+ console.log(chalk.gray('Building client...'))
34
+ await build({
35
+ ...baseConfig,
36
+ build: {
37
+ outDir: path.join(outDir, 'client'),
38
+ ssrManifest: true,
39
+ rolldownOptions: {
40
+ input: path.resolve(PACKAGE_ROOT, 'src/server/index.html'),
41
+ },
29
42
  },
30
43
  })
31
44
 
32
- attachLifecycleHandlers(child)
45
+ // Build server bundle
46
+ const serverEntry = options.adapter === 'vercel'
47
+ ? path.resolve(PACKAGE_ROOT, 'src/server/entry-vercel.ts')
48
+ : path.resolve(PACKAGE_ROOT, 'src/server/entry-prod.ts')
49
+
50
+ console.log(chalk.gray('Building server...'))
51
+ await build({
52
+ ...baseConfig,
53
+ ssr: {
54
+ noExternal: true,
55
+ },
56
+ build: {
57
+ outDir: path.join(outDir, 'server'),
58
+ ssr: serverEntry,
59
+ target: 'node22',
60
+ },
61
+ })
62
+
63
+ // Generate search index
64
+ console.log(chalk.gray('Building search index...'))
65
+ const { generateSearchIndex } = await import('@/server/build-search-index')
66
+ const docCount = await generateSearchIndex(contentDir, path.join(outDir, 'server'))
67
+ console.log(chalk.gray(` Indexed ${docCount} documents`))
68
+
69
+ console.log(chalk.green('Build complete →'), outDir)
70
+
71
+ // Run Vercel adapter post-build
72
+ if (options.adapter === 'vercel') {
73
+ const { buildVercelOutput } = await import('@/server/adapters/vercel')
74
+ await buildVercelOutput({
75
+ distDir: outDir,
76
+ contentDir,
77
+ projectRoot: process.cwd(),
78
+ })
79
+ }
33
80
  })
@@ -1,34 +1,21 @@
1
1
  import { Command } from 'commander'
2
- import { spawn } from 'child_process'
3
- import path from 'path'
4
- import { fileURLToPath } from 'url'
5
- import { createRequire } from 'module'
6
2
  import chalk from 'chalk'
7
- import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers } from '@/cli/utils'
8
-
9
- const require = createRequire(import.meta.url)
10
- const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
11
- const nextCli = require.resolve('next/dist/bin/next')
3
+ import { resolveContentDir } from '@/cli/utils/config'
4
+ import { PACKAGE_ROOT } from '@/cli/utils/resolve'
12
5
 
13
6
  export const devCommand = new Command('dev')
14
7
  .description('Start development server')
15
8
  .option('-p, --port <port>', 'Port number', '3000')
16
9
  .option('-c, --content <path>', 'Content directory')
17
- .action((options) => {
10
+ .action(async (options) => {
18
11
  const contentDir = resolveContentDir(options.content)
19
- loadCLIConfig(contentDir)
12
+ const port = parseInt(options.port, 10)
20
13
 
21
- console.log(chalk.cyan('Starting dev server...'))
22
- console.log(chalk.gray(`Content: ${contentDir}`))
14
+ process.env.CHRONICLE_PROJECT_ROOT = process.cwd()
15
+ process.env.CHRONICLE_CONTENT_DIR = contentDir
23
16
 
24
- const child = spawn(process.execPath, [nextCli, 'dev', '-p', options.port], {
25
- stdio: 'inherit',
26
- cwd: PACKAGE_ROOT,
27
- env: {
28
- ...process.env,
29
- CHRONICLE_CONTENT_DIR: contentDir,
30
- },
31
- })
17
+ console.log(chalk.cyan('Starting dev server...'))
32
18
 
33
- attachLifecycleHandlers(child)
19
+ const { startDevServer } = await import('@/server/dev')
20
+ await startDevServer({ port, root: PACKAGE_ROOT, contentDir })
34
21
  })
@@ -1,9 +1,11 @@
1
1
  import { Command } from 'commander'
2
+ import { execSync } from 'child_process'
2
3
  import fs from 'fs'
3
4
  import path from 'path'
4
5
  import chalk from 'chalk'
5
6
  import { stringify } from 'yaml'
6
7
  import type { ChronicleConfig } from '@/types'
8
+ import { detectPackageManager, getChronicleVersion } from '@/cli/utils/scaffold'
7
9
 
8
10
  function createConfig(): ChronicleConfig {
9
11
  return {
@@ -14,6 +16,29 @@ function createConfig(): ChronicleConfig {
14
16
  }
15
17
  }
16
18
 
19
+ function createPackageJson(name: string): Record<string, unknown> {
20
+ return {
21
+ name,
22
+ private: true,
23
+ type: 'module',
24
+ scripts: {
25
+ dev: 'chronicle dev',
26
+ build: 'chronicle build',
27
+ start: 'chronicle start',
28
+ },
29
+ dependencies: {
30
+ '@raystack/chronicle': `^${getChronicleVersion()}`,
31
+ },
32
+ devDependencies: {
33
+ '@raystack/tools-config': '0.56.0',
34
+ 'openapi-types': '^12.1.3',
35
+ typescript: '5.9.3',
36
+ '@types/react': '^19.2.10',
37
+ '@types/node': '^25.1.0',
38
+ },
39
+ }
40
+ }
41
+
17
42
  const sampleMdx = `---
18
43
  title: Welcome
19
44
  description: Getting started with your documentation
@@ -27,32 +52,103 @@ This is your documentation home page.
27
52
 
28
53
  export const initCommand = new Command('init')
29
54
  .description('Initialize a new Chronicle project')
30
- .option('-d, --dir <path>', 'Content directory', '.')
55
+ .option('-c, --content <path>', 'Content directory name', 'content')
31
56
  .action((options) => {
32
- const contentDir = path.resolve(options.dir)
57
+ const projectDir = process.cwd()
58
+ const dirName = path.basename(projectDir) || 'docs'
59
+ const contentDir = path.join(projectDir, options.content)
33
60
 
34
61
  // Create content directory
35
62
  if (!fs.existsSync(contentDir)) {
36
63
  fs.mkdirSync(contentDir, { recursive: true })
37
- console.log(chalk.green(''), 'Created', contentDir)
64
+ console.log(chalk.green('\u2713'), 'Created', contentDir)
65
+ }
66
+
67
+ // Create or update package.json
68
+ const packageJsonPath = path.join(projectDir, 'package.json')
69
+ if (!fs.existsSync(packageJsonPath)) {
70
+ fs.writeFileSync(packageJsonPath, JSON.stringify(createPackageJson(dirName), null, 2) + '\n')
71
+ console.log(chalk.green('\u2713'), 'Created', packageJsonPath)
72
+ } else {
73
+ const existing = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
74
+ const template = createPackageJson(dirName)
75
+ let updated = false
76
+
77
+ if (existing.type !== 'module') {
78
+ existing.type = 'module'
79
+ updated = true
80
+ }
81
+
82
+ if (!existing.scripts) existing.scripts = {}
83
+ for (const [key, value] of Object.entries(template.scripts as Record<string, string>)) {
84
+ if (!existing.scripts[key]) {
85
+ existing.scripts[key] = value
86
+ updated = true
87
+ }
88
+ }
89
+
90
+ if (!existing.dependencies) existing.dependencies = {}
91
+ for (const [key, value] of Object.entries(template.dependencies as Record<string, string>)) {
92
+ if (!existing.dependencies[key]) {
93
+ existing.dependencies[key] = value
94
+ updated = true
95
+ }
96
+ }
97
+
98
+ if (!existing.devDependencies) existing.devDependencies = {}
99
+ for (const [key, value] of Object.entries(template.devDependencies as Record<string, string>)) {
100
+ if (!existing.devDependencies[key]) {
101
+ existing.devDependencies[key] = value
102
+ updated = true
103
+ }
104
+ }
105
+
106
+ if (updated) {
107
+ fs.writeFileSync(packageJsonPath, JSON.stringify(existing, null, 2) + '\n')
108
+ console.log(chalk.green('\u2713'), 'Updated', packageJsonPath)
109
+ } else {
110
+ console.log(chalk.yellow('\u26a0'), packageJsonPath, 'already has all required entries')
111
+ }
38
112
  }
39
113
 
40
114
  // Create chronicle.yaml
41
- const configPath = path.join(contentDir, 'chronicle.yaml')
115
+ const configPath = path.join(projectDir, 'chronicle.yaml')
42
116
  if (!fs.existsSync(configPath)) {
43
117
  fs.writeFileSync(configPath, stringify(createConfig()))
44
- console.log(chalk.green(''), 'Created', configPath)
118
+ console.log(chalk.green('\u2713'), 'Created', configPath)
45
119
  } else {
46
- console.log(chalk.yellow(''), configPath, 'already exists')
120
+ console.log(chalk.yellow('\u26a0'), configPath, 'already exists')
47
121
  }
48
122
 
49
123
  // Create sample index.mdx
50
- const indexPath = path.join(contentDir, 'index.mdx')
51
- if (!fs.existsSync(indexPath)) {
124
+ const contentFiles = fs.readdirSync(contentDir)
125
+ if (contentFiles.length === 0) {
126
+ const indexPath = path.join(contentDir, 'index.mdx')
52
127
  fs.writeFileSync(indexPath, sampleMdx)
53
- console.log(chalk.green(''), 'Created', indexPath)
128
+ console.log(chalk.green('\u2713'), 'Created', indexPath)
54
129
  }
55
130
 
56
- console.log(chalk.green('\n✓ Chronicle initialized!'))
57
- console.log('\nRun', chalk.cyan('chronicle dev'), 'to start development server')
131
+ // Update .gitignore
132
+ const gitignorePath = path.join(projectDir, '.gitignore')
133
+ const gitignoreEntries = ['node_modules', 'dist']
134
+ if (fs.existsSync(gitignorePath)) {
135
+ const existing = fs.readFileSync(gitignorePath, 'utf-8')
136
+ const missing = gitignoreEntries.filter(e => !existing.includes(e))
137
+ if (missing.length > 0) {
138
+ fs.appendFileSync(gitignorePath, `\n${missing.join('\n')}\n`)
139
+ console.log(chalk.green('\u2713'), 'Added', missing.join(', '), 'to .gitignore')
140
+ }
141
+ } else {
142
+ fs.writeFileSync(gitignorePath, `${gitignoreEntries.join('\n')}\n`)
143
+ console.log(chalk.green('\u2713'), 'Created .gitignore')
144
+ }
145
+
146
+ // Install dependencies
147
+ const pm = detectPackageManager()
148
+ console.log(chalk.cyan(`\nInstalling dependencies with ${pm}...`))
149
+ execSync(`${pm} install`, { cwd: projectDir, stdio: 'inherit' })
150
+
151
+ const runCmd = pm === 'npm' ? 'npx' : pm === 'bun' ? 'bunx' : `${pm} dlx`
152
+ console.log(chalk.green('\n\u2713 Chronicle initialized!'))
153
+ console.log('\nRun', chalk.cyan(`${runCmd} chronicle dev`), 'to start development server')
58
154
  })
@@ -1,54 +1,55 @@
1
1
  import { Command } from 'commander'
2
- import { spawn } from 'child_process'
3
2
  import path from 'path'
4
- import { fileURLToPath } from 'url'
5
- import { createRequire } from 'module'
6
3
  import chalk from 'chalk'
7
- import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers } from '@/cli/utils'
8
-
9
- const require = createRequire(import.meta.url)
10
- const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
11
- const nextCli = require.resolve('next/dist/bin/next')
4
+ import { resolveContentDir } from '@/cli/utils/config'
5
+ import { PACKAGE_ROOT } from '@/cli/utils/resolve'
12
6
 
13
7
  export const serveCommand = new Command('serve')
14
8
  .description('Build and start production server')
15
9
  .option('-p, --port <port>', 'Port number', '3000')
16
10
  .option('-c, --content <path>', 'Content directory')
17
- .action((options) => {
11
+ .option('-o, --outDir <path>', 'Output directory', 'dist')
12
+ .action(async (options) => {
18
13
  const contentDir = resolveContentDir(options.content)
19
- loadCLIConfig(contentDir)
14
+ const port = parseInt(options.port, 10)
15
+ const outDir = path.resolve(options.outDir)
20
16
 
21
- const env = {
22
- ...process.env,
23
- CHRONICLE_CONTENT_DIR: contentDir,
24
- }
17
+ process.env.CHRONICLE_PROJECT_ROOT = process.cwd()
18
+ process.env.CHRONICLE_CONTENT_DIR = contentDir
25
19
 
20
+ // Build
26
21
  console.log(chalk.cyan('Building for production...'))
27
- console.log(chalk.gray(`Content: ${contentDir}`))
28
22
 
29
- const buildChild = spawn(process.execPath, [nextCli, 'build'], {
30
- stdio: 'inherit',
31
- cwd: PACKAGE_ROOT,
32
- env,
33
- })
23
+ const { build } = await import('vite')
24
+ const { createViteConfig } = await import('@/server/vite-config')
34
25
 
35
- process.once('SIGINT', () => buildChild.kill('SIGINT'))
36
- process.once('SIGTERM', () => buildChild.kill('SIGTERM'))
26
+ const baseConfig = await createViteConfig({ root: PACKAGE_ROOT, contentDir })
37
27
 
38
- buildChild.on('close', (code) => {
39
- if (code !== 0) {
40
- console.log(chalk.red('Build failed'))
41
- process.exit(code ?? 1)
42
- }
28
+ await build({
29
+ ...baseConfig,
30
+ build: {
31
+ outDir: path.join(outDir, 'client'),
32
+ ssrManifest: true,
33
+ rolldownOptions: {
34
+ input: path.resolve(PACKAGE_ROOT, 'src/server/index.html'),
35
+ },
36
+ },
37
+ })
43
38
 
44
- console.log(chalk.cyan('Starting production server...'))
39
+ await build({
40
+ ...baseConfig,
41
+ ssr: {
42
+ noExternal: true,
43
+ },
44
+ build: {
45
+ outDir: path.join(outDir, 'server'),
46
+ ssr: path.resolve(PACKAGE_ROOT, 'src/server/entry-prod.ts'),
47
+ },
48
+ })
45
49
 
46
- const startChild = spawn(process.execPath, [nextCli, 'start', '-p', options.port], {
47
- stdio: 'inherit',
48
- cwd: PACKAGE_ROOT,
49
- env,
50
- })
50
+ // Start
51
+ console.log(chalk.cyan('Starting production server...'))
51
52
 
52
- attachLifecycleHandlers(startChild)
53
- })
53
+ const { startProdServer } = await import('@/server/prod')
54
+ await startProdServer({ port, root: PACKAGE_ROOT, distDir: outDir })
54
55
  })
@@ -1,34 +1,24 @@
1
1
  import { Command } from 'commander'
2
- import { spawn } from 'child_process'
3
2
  import path from 'path'
4
- import { fileURLToPath } from 'url'
5
- import { createRequire } from 'module'
6
3
  import chalk from 'chalk'
7
- import { resolveContentDir, loadCLIConfig, attachLifecycleHandlers } from '@/cli/utils'
8
-
9
- const require = createRequire(import.meta.url)
10
- const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
11
- const nextCli = require.resolve('next/dist/bin/next')
4
+ import { resolveContentDir } from '@/cli/utils/config'
5
+ import { PACKAGE_ROOT } from '@/cli/utils/resolve'
12
6
 
13
7
  export const startCommand = new Command('start')
14
8
  .description('Start production server')
15
9
  .option('-p, --port <port>', 'Port number', '3000')
16
10
  .option('-c, --content <path>', 'Content directory')
17
- .action((options) => {
11
+ .option('-d, --dist <path>', 'Dist directory', 'dist')
12
+ .action(async (options) => {
18
13
  const contentDir = resolveContentDir(options.content)
19
- loadCLIConfig(contentDir)
14
+ const port = parseInt(options.port, 10)
15
+ const distDir = path.resolve(options.dist)
20
16
 
21
- console.log(chalk.cyan('Starting production server...'))
22
- console.log(chalk.gray(`Content: ${contentDir}`))
17
+ process.env.CHRONICLE_PROJECT_ROOT = process.cwd()
18
+ process.env.CHRONICLE_CONTENT_DIR = contentDir
23
19
 
24
- const child = spawn(process.execPath, [nextCli, 'start', '-p', options.port], {
25
- stdio: 'inherit',
26
- cwd: PACKAGE_ROOT,
27
- env: {
28
- ...process.env,
29
- CHRONICLE_CONTENT_DIR: contentDir,
30
- },
31
- })
20
+ console.log(chalk.cyan('Starting production server...'))
32
21
 
33
- attachLifecycleHandlers(child)
22
+ const { startProdServer } = await import('@/server/prod')
23
+ await startProdServer({ port, root: PACKAGE_ROOT, distDir })
34
24
  })
@@ -13,7 +13,7 @@ export interface CLIConfig {
13
13
  export function resolveContentDir(contentFlag?: string): string {
14
14
  if (contentFlag) return path.resolve(contentFlag)
15
15
  if (process.env.CHRONICLE_CONTENT_DIR) return path.resolve(process.env.CHRONICLE_CONTENT_DIR)
16
- return process.cwd()
16
+ return path.resolve('content')
17
17
  }
18
18
 
19
19
  function resolveConfigPath(contentDir: string): string | null {
@@ -28,7 +28,7 @@ export function loadCLIConfig(contentDir: string): CLIConfig {
28
28
  const configPath = resolveConfigPath(contentDir)
29
29
 
30
30
  if (!configPath) {
31
- console.log(chalk.red('Error: chronicle.yaml not found in'), process.cwd(), 'or', contentDir)
31
+ console.log(chalk.red(`Error: chronicle.yaml not found in '${process.cwd()}' or '${contentDir}'`))
32
32
  console.log(chalk.gray(`Run 'chronicle init' to create one`))
33
33
  process.exit(1)
34
34
  }
@@ -1,2 +1,2 @@
1
1
  export * from './config'
2
- export * from './process'
2
+ export * from './resolve'
@@ -0,0 +1,6 @@
1
+ import path from 'path'
2
+ import { fileURLToPath } from 'url'
3
+
4
+ // After bundling: dist/cli/index.js → ../.. = package root
5
+ // After install: node_modules/@raystack/chronicle/dist/cli/index.js → ../.. = package root
6
+ export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
@@ -0,0 +1,20 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { PACKAGE_ROOT } from './resolve'
4
+
5
+ export function detectPackageManager(): string {
6
+ if (process.env.npm_config_user_agent) {
7
+ return process.env.npm_config_user_agent.split('/')[0]
8
+ }
9
+ const cwd = process.cwd()
10
+ if (fs.existsSync(path.join(cwd, 'bun.lock')) || fs.existsSync(path.join(cwd, 'bun.lockb'))) return 'bun'
11
+ if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
12
+ if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn'
13
+ return 'npm'
14
+ }
15
+
16
+ export function getChronicleVersion(): string {
17
+ const pkgPath = path.join(PACKAGE_ROOT, 'package.json')
18
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
19
+ return pkg.version
20
+ }
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
- import type { ComponentProps } from 'react'
3
+ import { type ComponentProps, isValidElement, Children } from 'react'
4
+ import { Mermaid } from './mermaid'
4
5
  import styles from './code.module.css'
5
6
 
6
7
  type PreProps = ComponentProps<'pre'> & {
@@ -16,6 +17,14 @@ export function MdxCode({ children, className, ...props }: ComponentProps<'code'
16
17
  }
17
18
 
18
19
  export function MdxPre({ children, title, className, ...props }: PreProps) {
20
+ // Detect mermaid code blocks
21
+ if (isValidElement(children)) {
22
+ const childProps = children.props as { className?: string; children?: string }
23
+ if (childProps.className?.includes('language-mermaid') && typeof childProps.children === 'string') {
24
+ return <Mermaid chart={childProps.children} />
25
+ }
26
+ }
27
+
19
28
  return (
20
29
  <div className={styles.codeBlock}>
21
30
  {title && <div className={styles.codeHeader}>{title}</div>}
@@ -4,32 +4,9 @@
4
4
  margin: var(--rs-space-5) 0;
5
5
  }
6
6
 
7
- .summary {
8
- padding: var(--rs-space-4) var(--rs-space-5);
9
- cursor: pointer;
7
+ .trigger {
10
8
  font-weight: 500;
11
9
  font-size: var(--rs-font-size-small);
12
- color: var(--rs-color-text-base-primary);
13
- background: var(--rs-color-background-base-secondary);
14
- list-style: none;
15
- display: flex;
16
- align-items: center;
17
- gap: var(--rs-space-3);
18
- }
19
-
20
- .summary::-webkit-details-marker {
21
- display: none;
22
- }
23
-
24
- .summary::before {
25
- content: '▶';
26
- font-size: 10px;
27
- transition: transform 0.2s ease;
28
- color: var(--rs-color-text-base-secondary);
29
- }
30
-
31
- .details[open] > .summary::before {
32
- transform: rotate(90deg);
33
10
  }
34
11
 
35
12
  .content {