@raystack/chronicle 0.5.3 → 0.6.0
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/dist/cli/index.js +260 -81
- package/package.json +8 -6
- package/src/cli/commands/build.ts +5 -8
- package/src/cli/commands/dev.ts +5 -6
- package/src/cli/commands/init.test.ts +77 -0
- package/src/cli/commands/init.ts +73 -40
- package/src/cli/commands/serve.ts +6 -9
- package/src/cli/commands/start.ts +5 -5
- package/src/cli/utils/config.ts +6 -12
- package/src/cli/utils/scaffold.test.ts +179 -0
- package/src/cli/utils/scaffold.ts +70 -9
- package/src/components/api/field-row.tsx +1 -1
- package/src/components/api/field-section.tsx +2 -2
- package/src/components/mdx/index.tsx +1 -1
- package/src/components/mdx/mermaid.tsx +24 -21
- package/src/components/ui/breadcrumbs.tsx +4 -2
- package/src/components/ui/client-theme-switcher.tsx +21 -4
- package/src/components/ui/search.module.css +16 -41
- package/src/components/ui/search.tsx +30 -41
- package/src/lib/config.test.ts +493 -0
- package/src/lib/config.ts +123 -22
- package/src/lib/head.tsx +23 -5
- package/src/lib/llms.test.ts +94 -0
- package/src/lib/llms.ts +41 -0
- package/src/lib/navigation.test.ts +94 -0
- package/src/lib/navigation.ts +51 -0
- package/src/lib/page-context.tsx +79 -32
- package/src/lib/route-resolver.test.ts +173 -0
- package/src/lib/route-resolver.ts +73 -0
- package/src/lib/source.ts +94 -1
- package/src/lib/version-source.test.ts +163 -0
- package/src/lib/version-source.ts +101 -0
- package/src/pages/ApiPage.tsx +1 -1
- package/src/pages/DocsLayout.tsx +24 -3
- package/src/pages/DocsPage.tsx +7 -7
- package/src/pages/LandingPage.module.css +56 -0
- package/src/pages/LandingPage.tsx +39 -0
- package/src/pages/NotFound.module.css +3 -0
- package/src/pages/NotFound.tsx +9 -12
- package/src/server/App.tsx +21 -23
- package/src/server/api/{page/[...slug].ts → page.ts} +7 -3
- package/src/server/api/search.ts +51 -24
- package/src/server/api/specs.ts +17 -5
- package/src/server/entry-client.tsx +42 -14
- package/src/server/entry-server.tsx +35 -13
- package/src/server/plugins/telemetry.ts +47 -7
- package/src/server/routes/[...slug].md.ts +0 -6
- package/src/server/routes/[version]/llms.txt.ts +26 -0
- package/src/server/routes/llms.txt.ts +10 -13
- package/src/server/routes/og.tsx +2 -2
- package/src/server/routes/sitemap.xml.ts +14 -6
- package/src/server/vite-config.ts +5 -5
- package/src/themes/default/ContentDirButtons.tsx +66 -0
- package/src/themes/default/Layout.module.css +187 -40
- package/src/themes/default/Layout.tsx +166 -65
- package/src/themes/default/OpenInAI.tsx +112 -0
- package/src/themes/default/Page.module.css +30 -0
- package/src/themes/default/Page.tsx +1 -3
- package/src/themes/default/SidebarLogo.tsx +26 -0
- package/src/themes/default/Toc.module.css +102 -25
- package/src/themes/default/Toc.tsx +56 -10
- package/src/themes/default/VersionSwitcher.tsx +59 -0
- package/src/themes/paper/ContentDirDropdown.tsx +47 -0
- package/src/themes/paper/Layout.module.css +7 -0
- package/src/themes/paper/Layout.tsx +20 -13
- package/src/themes/paper/VersionSwitcher.tsx +60 -0
- package/src/types/config.ts +146 -23
- package/src/types/content.ts +11 -1
- package/src/types/theme.ts +1 -0
- package/src/components/ui/footer.module.css +0 -27
- package/src/components/ui/footer.tsx +0 -30
- package/src/server/api/metrics.ts +0 -23
- package/src/server/api/page/index.ts +0 -1
- package/src/server/telemetry.ts +0 -49
package/src/types/config.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import uniqBy from 'lodash/uniqBy.js'
|
|
1
2
|
import { z } from 'zod'
|
|
2
3
|
|
|
3
4
|
const logoSchema = z.object({
|
|
@@ -45,19 +46,11 @@ const apiSchema = z.object({
|
|
|
45
46
|
name: z.string(),
|
|
46
47
|
spec: z.string(),
|
|
47
48
|
basePath: z.string(),
|
|
49
|
+
icon: z.string().optional(),
|
|
48
50
|
server: apiServerSchema,
|
|
49
51
|
auth: apiAuthSchema.optional(),
|
|
50
52
|
})
|
|
51
53
|
|
|
52
|
-
const footerSchema = z.object({
|
|
53
|
-
copyright: z.string().optional(),
|
|
54
|
-
links: z.array(navLinkSchema).optional(),
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
const llmsSchema = z.object({
|
|
58
|
-
enabled: z.boolean().optional(),
|
|
59
|
-
})
|
|
60
|
-
|
|
61
54
|
const googleAnalyticsSchema = z.object({
|
|
62
55
|
measurementId: z.string(),
|
|
63
56
|
})
|
|
@@ -70,26 +63,158 @@ const analyticsSchema = z.object({
|
|
|
70
63
|
const telemetrySchema = z.object({
|
|
71
64
|
enabled: z.boolean().optional(),
|
|
72
65
|
serviceName: z.string().optional(),
|
|
66
|
+
port: z.number().int().min(1).max(65535).default(9090),
|
|
73
67
|
})
|
|
74
68
|
|
|
75
|
-
|
|
69
|
+
const siteSchema = z.object({
|
|
76
70
|
title: z.string(),
|
|
77
71
|
description: z.string().optional(),
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const DIR_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/
|
|
75
|
+
|
|
76
|
+
const dirNameSchema = z
|
|
77
|
+
.string()
|
|
78
|
+
.min(1)
|
|
79
|
+
.refine((s) => DIR_NAME_PATTERN.test(s) && s !== '.' && s !== '..', {
|
|
80
|
+
message:
|
|
81
|
+
'dir must start with a letter or digit and contain only letters, digits, ".", "_", or "-"',
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const contentEntrySchema = z.object({
|
|
85
|
+
dir: dirNameSchema,
|
|
86
|
+
label: z.string().min(1),
|
|
87
|
+
icon: z.string().optional(),
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Variants map to Apsara Badge color prop.
|
|
91
|
+
// https://apsara.raystack.org/docs/components/badge
|
|
92
|
+
const badgeVariantSchema = z.enum([
|
|
93
|
+
'accent',
|
|
94
|
+
'warning',
|
|
95
|
+
'danger',
|
|
96
|
+
'success',
|
|
97
|
+
'neutral',
|
|
98
|
+
'gradient',
|
|
99
|
+
])
|
|
100
|
+
|
|
101
|
+
const badgeSchema = z.object({
|
|
102
|
+
label: z.string().min(1),
|
|
103
|
+
variant: badgeVariantSchema.default('accent'),
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const latestSchema = z.object({
|
|
107
|
+
label: z.string().min(1),
|
|
108
|
+
landing: z.boolean().optional(),
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const versionSchema = z.object({
|
|
112
|
+
dir: dirNameSchema,
|
|
113
|
+
label: z.string().min(1),
|
|
114
|
+
badge: badgeSchema.optional(),
|
|
115
|
+
landing: z.boolean().optional(),
|
|
116
|
+
content: z.array(contentEntrySchema).min(1),
|
|
86
117
|
api: z.array(apiSchema).optional(),
|
|
87
|
-
llms: llmsSchema.optional(),
|
|
88
|
-
analytics: analyticsSchema.optional(),
|
|
89
|
-
telemetry: telemetrySchema.optional(),
|
|
90
118
|
})
|
|
91
119
|
|
|
120
|
+
const allUnique = <T>(items: T[], key: (item: T) => string): boolean =>
|
|
121
|
+
uniqBy(items, key).length === items.length
|
|
122
|
+
|
|
123
|
+
const RESERVED_ROUTE_SEGMENTS = [
|
|
124
|
+
'api',
|
|
125
|
+
'apis',
|
|
126
|
+
'og',
|
|
127
|
+
'llms.txt',
|
|
128
|
+
'robots.txt',
|
|
129
|
+
'sitemap.xml',
|
|
130
|
+
] as const
|
|
131
|
+
|
|
132
|
+
export const chronicleConfigSchema = z
|
|
133
|
+
.object({
|
|
134
|
+
site: siteSchema,
|
|
135
|
+
url: z.string().optional(),
|
|
136
|
+
content: z.array(contentEntrySchema).min(1),
|
|
137
|
+
latest: latestSchema.optional(),
|
|
138
|
+
versions: z.array(versionSchema).optional(),
|
|
139
|
+
preset: z.string().optional(),
|
|
140
|
+
logo: logoSchema.optional(),
|
|
141
|
+
theme: themeSchema.optional(),
|
|
142
|
+
navigation: navigationSchema.optional(),
|
|
143
|
+
search: searchSchema.optional(),
|
|
144
|
+
api: z.array(apiSchema).optional(),
|
|
145
|
+
analytics: analyticsSchema.optional(),
|
|
146
|
+
telemetry: telemetrySchema.optional(),
|
|
147
|
+
})
|
|
148
|
+
.strict()
|
|
149
|
+
.refine((cfg) => allUnique(cfg.content, (c) => c.dir), {
|
|
150
|
+
message: 'content[].dir must be unique',
|
|
151
|
+
path: ['content'],
|
|
152
|
+
})
|
|
153
|
+
.refine((cfg) => !cfg.versions || allUnique(cfg.versions, (v) => v.dir), {
|
|
154
|
+
message: 'versions[].dir must be unique',
|
|
155
|
+
path: ['versions'],
|
|
156
|
+
})
|
|
157
|
+
.refine(
|
|
158
|
+
(cfg) =>
|
|
159
|
+
!cfg.versions ||
|
|
160
|
+
cfg.versions.every((v) => allUnique(v.content, (c) => c.dir)),
|
|
161
|
+
{
|
|
162
|
+
message: 'versions[].content[].dir must be unique within each version',
|
|
163
|
+
path: ['versions'],
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
.refine((cfg) => !cfg.versions || cfg.versions.length === 0 || !!cfg.latest, {
|
|
167
|
+
message: 'latest is required when versions are declared',
|
|
168
|
+
path: ['latest'],
|
|
169
|
+
})
|
|
170
|
+
.refine(
|
|
171
|
+
(cfg) => {
|
|
172
|
+
if (!cfg.versions) return true
|
|
173
|
+
const contentDirs = new Set(cfg.content.map((c) => c.dir))
|
|
174
|
+
return !cfg.versions.some((v) => contentDirs.has(v.dir))
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
message:
|
|
178
|
+
'versions[].dir must not overlap with content[].dir — the URL segment would be shadowed',
|
|
179
|
+
path: ['versions'],
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
.superRefine((cfg, ctx) => {
|
|
183
|
+
const reserved = new Set<string>(RESERVED_ROUTE_SEGMENTS)
|
|
184
|
+
const message = `dir must not be a reserved route segment: ${RESERVED_ROUTE_SEGMENTS.join(', ')}`
|
|
185
|
+
|
|
186
|
+
cfg.content.forEach((c, i) => {
|
|
187
|
+
if (reserved.has(c.dir)) {
|
|
188
|
+
ctx.addIssue({ code: 'custom', message, path: ['content', i, 'dir'] })
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
cfg.versions?.forEach((v, vi) => {
|
|
192
|
+
if (reserved.has(v.dir)) {
|
|
193
|
+
ctx.addIssue({
|
|
194
|
+
code: 'custom',
|
|
195
|
+
message,
|
|
196
|
+
path: ['versions', vi, 'dir'],
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
v.content.forEach((c, ci) => {
|
|
200
|
+
if (reserved.has(c.dir)) {
|
|
201
|
+
ctx.addIssue({
|
|
202
|
+
code: 'custom',
|
|
203
|
+
message,
|
|
204
|
+
path: ['versions', vi, 'content', ci, 'dir'],
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
92
211
|
export type ChronicleConfig = z.infer<typeof chronicleConfigSchema>
|
|
212
|
+
export type SiteConfig = z.infer<typeof siteSchema>
|
|
213
|
+
export type ContentEntry = z.infer<typeof contentEntrySchema>
|
|
214
|
+
export type BadgeConfig = z.infer<typeof badgeSchema>
|
|
215
|
+
export type BadgeVariant = z.infer<typeof badgeVariantSchema>
|
|
216
|
+
export type LatestConfig = z.infer<typeof latestSchema>
|
|
217
|
+
export type VersionConfig = z.infer<typeof versionSchema>
|
|
93
218
|
export type LogoConfig = z.infer<typeof logoSchema>
|
|
94
219
|
export type ThemeConfig = z.infer<typeof themeSchema>
|
|
95
220
|
export type NavigationConfig = z.infer<typeof navigationSchema>
|
|
@@ -99,8 +224,6 @@ export type SearchConfig = z.infer<typeof searchSchema>
|
|
|
99
224
|
export type ApiConfig = z.infer<typeof apiSchema>
|
|
100
225
|
export type ApiServerConfig = z.infer<typeof apiServerSchema>
|
|
101
226
|
export type ApiAuthConfig = z.infer<typeof apiAuthSchema>
|
|
102
|
-
export type FooterConfig = z.infer<typeof footerSchema>
|
|
103
|
-
export type LlmsConfig = z.infer<typeof llmsSchema>
|
|
104
227
|
export type AnalyticsConfig = z.infer<typeof analyticsSchema>
|
|
105
228
|
export type GoogleAnalyticsConfig = z.infer<typeof googleAnalyticsSchema>
|
|
106
229
|
export type TelemetryConfig = z.infer<typeof telemetrySchema>
|
package/src/types/content.ts
CHANGED
|
@@ -12,7 +12,17 @@ export interface Frontmatter {
|
|
|
12
12
|
lastModified?: string
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export interface
|
|
15
|
+
export interface PageNavLink {
|
|
16
|
+
url: string
|
|
17
|
+
title: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PageNav {
|
|
21
|
+
prev: PageNavLink | null
|
|
22
|
+
next: PageNavLink | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Page extends PageNav {
|
|
16
26
|
slug: string[]
|
|
17
27
|
frontmatter: Frontmatter
|
|
18
28
|
content: ReactNode
|
package/src/types/theme.ts
CHANGED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
.footer {
|
|
2
|
-
border-top: 1px solid var(--rs-color-border-base-primary);
|
|
3
|
-
padding: var(--rs-space-5) var(--rs-space-7);
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
.container {
|
|
7
|
-
max-width: 1200px;
|
|
8
|
-
margin: 0 auto;
|
|
9
|
-
width: 100%;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
.copyright {
|
|
13
|
-
color: var(--rs-color-foreground-base-secondary);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
.links {
|
|
17
|
-
flex-wrap: wrap;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.link {
|
|
21
|
-
color: var(--rs-color-foreground-base-secondary);
|
|
22
|
-
font-size: 14px;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.link:hover {
|
|
26
|
-
color: var(--rs-color-foreground-base-primary);
|
|
27
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { Flex, Link, Text } from "@raystack/apsara";
|
|
2
|
-
import type { FooterConfig } from "@/types";
|
|
3
|
-
import styles from "./footer.module.css";
|
|
4
|
-
|
|
5
|
-
interface FooterProps {
|
|
6
|
-
config?: FooterConfig;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function Footer({ config }: FooterProps) {
|
|
10
|
-
return (
|
|
11
|
-
<footer className={styles.footer}>
|
|
12
|
-
<Flex align="center" justify="between" className={styles.container}>
|
|
13
|
-
{config?.copyright && (
|
|
14
|
-
<Text size={2} className={styles.copyright}>
|
|
15
|
-
{config.copyright}
|
|
16
|
-
</Text>
|
|
17
|
-
)}
|
|
18
|
-
{config?.links && config.links.length > 0 && (
|
|
19
|
-
<Flex gap="medium" className={styles.links}>
|
|
20
|
-
{config.links.map((link) => (
|
|
21
|
-
<Link key={link.href} href={link.href} className={styles.link}>
|
|
22
|
-
{link.label}
|
|
23
|
-
</Link>
|
|
24
|
-
))}
|
|
25
|
-
</Flex>
|
|
26
|
-
)}
|
|
27
|
-
</Flex>
|
|
28
|
-
</footer>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
-
import { defineHandler } from 'nitro'
|
|
3
|
-
import { getExporter } from '../telemetry'
|
|
4
|
-
|
|
5
|
-
export default defineHandler(async () => {
|
|
6
|
-
const exporter = getExporter()
|
|
7
|
-
if (!exporter) {
|
|
8
|
-
return new Response('Telemetry not enabled', { status: 404 })
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const metricsString = await new Promise<string>((resolve) => {
|
|
12
|
-
const mockRes = {
|
|
13
|
-
setHeader: () => mockRes,
|
|
14
|
-
end: (data: string) => resolve(data),
|
|
15
|
-
} as unknown as ServerResponse
|
|
16
|
-
|
|
17
|
-
exporter.getMetricsRequestHandler({} as unknown as IncomingMessage, mockRes)
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
return new Response(metricsString, {
|
|
21
|
-
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
22
|
-
})
|
|
23
|
-
})
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { default } from './[...slug]';
|
package/src/server/telemetry.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import type { Counter, Histogram } from '@opentelemetry/api'
|
|
2
|
-
import sdkMetrics from '@opentelemetry/sdk-metrics'
|
|
3
|
-
import prometheusExporter from '@opentelemetry/exporter-prometheus'
|
|
4
|
-
import resources from '@opentelemetry/resources'
|
|
5
|
-
import semconv from '@opentelemetry/semantic-conventions'
|
|
6
|
-
import type { ChronicleConfig } from '@/types/config'
|
|
7
|
-
|
|
8
|
-
const { MeterProvider } = sdkMetrics
|
|
9
|
-
const { PrometheusExporter } = prometheusExporter
|
|
10
|
-
const { resourceFromAttributes } = resources
|
|
11
|
-
const { ATTR_SERVICE_NAME } = semconv
|
|
12
|
-
|
|
13
|
-
let exporter: PrometheusExporter
|
|
14
|
-
let requestCounter: Counter
|
|
15
|
-
let requestDuration: Histogram
|
|
16
|
-
let ssrRenderDuration: Histogram
|
|
17
|
-
|
|
18
|
-
export function initTelemetry(config: ChronicleConfig) {
|
|
19
|
-
const resource = resourceFromAttributes({
|
|
20
|
-
[ATTR_SERVICE_NAME]: config.telemetry?.serviceName ?? 'chronicle',
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
exporter = new PrometheusExporter({ preventServerStart: true })
|
|
24
|
-
const provider = new MeterProvider({ resource, readers: [exporter] })
|
|
25
|
-
const meter = provider.getMeter('chronicle')
|
|
26
|
-
|
|
27
|
-
requestCounter = meter.createCounter('http_server_request_total', {
|
|
28
|
-
description: 'Total HTTP requests',
|
|
29
|
-
})
|
|
30
|
-
requestDuration = meter.createHistogram('http_server_request_duration_ms', {
|
|
31
|
-
description: 'HTTP request duration in ms',
|
|
32
|
-
})
|
|
33
|
-
ssrRenderDuration = meter.createHistogram('http_server_ssr_render_duration_ms', {
|
|
34
|
-
description: 'SSR render duration in ms',
|
|
35
|
-
})
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function getExporter() {
|
|
39
|
-
return exporter
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function recordRequest(method: string, route: string, status: number, durationMs: number) {
|
|
43
|
-
requestCounter?.add(1, { method, route, status })
|
|
44
|
-
requestDuration?.record(durationMs, { method, route, status })
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function recordSSRRender(route: string, status: number, durationMs: number) {
|
|
48
|
-
ssrRenderDuration?.record(durationMs, { route, status })
|
|
49
|
-
}
|