@raystack/chronicle 0.10.2 → 0.10.4

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 CHANGED
@@ -46,6 +46,15 @@ var __export = (target, all) => {
46
46
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
47
47
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
48
48
 
49
+ // src/lib/mdx-utils.ts
50
+ var MdxNodeType;
51
+ var init_mdx_utils = __esm(() => {
52
+ MdxNodeType = {
53
+ JsxFlow: "mdxJsxFlowElement",
54
+ JsxText: "mdxJsxTextElement"
55
+ };
56
+ });
57
+
49
58
  // src/lib/remark-resolve-images.ts
50
59
  import path4 from "node:path";
51
60
  import { visit } from "unist-util-visit";
@@ -72,16 +81,29 @@ var remarkResolveImages = () => {
72
81
  return;
73
82
  const relative = filePath.slice(contentIdx + "/content/".length);
74
83
  const dir = path4.posix.dirname(relative);
84
+ const seen = new Set;
85
+ const images = [];
86
+ function collect(src) {
87
+ if (!src || seen.has(src) || /^data:/i.test(src))
88
+ return;
89
+ seen.add(src);
90
+ images.push(src);
91
+ }
75
92
  visit(tree, "image", (node) => {
76
93
  if (!node.url)
77
94
  return;
78
95
  node.url = resolveUrl(node.url, dir);
96
+ collect(node.url);
79
97
  });
80
98
  visit(tree, "html", (node) => {
81
- node.value = node.value.replace(/(<img\b[^>]*\bsrc=["'])([^"']+)(["'])/gi, (_, before, src, after) => `${before}${resolveUrl(src, dir)}${after}`);
99
+ node.value = node.value.replace(/(<img\b[^>]*\bsrc=["'])([^"']+)(["'])/gi, (_, before, src, after) => {
100
+ const resolved = resolveUrl(src, dir);
101
+ collect(resolved);
102
+ return `${before}${resolved}${after}`;
103
+ });
82
104
  });
83
105
  visit(tree, (node) => {
84
- if (node.type !== "mdxJsxFlowElement" && node.type !== "mdxJsxTextElement")
106
+ if (node.type !== MdxNodeType.JsxFlow && node.type !== MdxNodeType.JsxText)
85
107
  return;
86
108
  const jsx = node;
87
109
  if (jsx.name !== "img")
@@ -90,6 +112,7 @@ var remarkResolveImages = () => {
90
112
  if (!srcAttr?.value || typeof srcAttr.value !== "string")
91
113
  return;
92
114
  srcAttr.value = resolveUrl(srcAttr.value, dir);
115
+ collect(srcAttr.value);
93
116
  });
94
117
  visit(tree, "element", (node) => {
95
118
  if (node.tagName !== "img")
@@ -98,10 +121,13 @@ var remarkResolveImages = () => {
98
121
  if (typeof src !== "string")
99
122
  return;
100
123
  node.properties.src = resolveUrl(src, dir);
124
+ collect(node.properties.src);
101
125
  });
126
+ file.data.images = images;
102
127
  };
103
128
  }, remark_resolve_images_default;
104
129
  var init_remark_resolve_images = __esm(() => {
130
+ init_mdx_utils();
105
131
  remark_resolve_images_default = remarkResolveImages;
106
132
  });
107
133
 
@@ -360,7 +386,7 @@ async function createViteConfig(options) {
360
386
  default: defineFumadocsConfig({
361
387
  mdxOptions: {
362
388
  remarkImageOptions: false,
363
- valueToExport: ["readingTime"],
389
+ valueToExport: ["readingTime", "images"],
364
390
  remarkPlugins: [
365
391
  remarkDirective,
366
392
  [remarkDirectiveAdmonition, {
@@ -433,6 +459,7 @@ async function createViteConfig(options) {
433
459
  },
434
460
  nitro: {
435
461
  logLevel: 2,
462
+ errorHandler: path6.resolve(packageRoot, "src/server/error.ts"),
436
463
  publicAssets: [{ dir: path6.resolve(projectRoot, "public") }],
437
464
  output: {
438
465
  dir: resolveOutputDir(projectRoot, preset)
@@ -808,14 +835,14 @@ var buildCommand = new Command("build").description("Build for production").opti
808
835
  // src/cli/commands/dev.ts
809
836
  import chalk3 from "chalk";
810
837
  import { Command as Command2 } from "commander";
811
- 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) => {
838
+ 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").option("--preset <preset>", "Deploy preset (bun, node-server, etc.)").action(async (options) => {
812
839
  const { config: config2, projectRoot, configPath } = await loadCLIConfig(options.config);
813
840
  const port = parseInt(options.port, 10);
814
841
  await linkContent(projectRoot, config2);
815
842
  console.log(chalk3.cyan("Starting dev server..."));
816
843
  const { createServer } = await import("vite");
817
844
  const { createViteConfig: createViteConfig2 } = await Promise.resolve().then(() => (init_vite_config(), exports_vite_config));
818
- const viteConfig = await createViteConfig2({ packageRoot: PACKAGE_ROOT, projectRoot, configPath });
845
+ const viteConfig = await createViteConfig2({ packageRoot: PACKAGE_ROOT, projectRoot, configPath, preset: options.preset });
819
846
  const server = await createServer({
820
847
  ...viteConfig,
821
848
  server: { ...viteConfig.server, port, host: options.host }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raystack/chronicle",
3
- "version": "0.10.2",
3
+ "version": "0.10.4",
4
4
  "description": "Config-driven documentation framework",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -9,6 +9,7 @@ export const devCommand = new Command('dev')
9
9
  .option('-p, --port <port>', 'Port number', '3000')
10
10
  .option('--config <path>', 'Path to chronicle.yaml')
11
11
  .option('--host <host>', 'Host address', 'localhost')
12
+ .option('--preset <preset>', 'Deploy preset (bun, node-server, etc.)')
12
13
  .action(async options => {
13
14
  const { config, projectRoot, configPath } = await loadCLIConfig(options.config);
14
15
  const port = parseInt(options.port, 10);
@@ -20,7 +21,7 @@ export const devCommand = new Command('dev')
20
21
  const { createServer } = await import('vite');
21
22
  const { createViteConfig } = await import('@/server/vite-config');
22
23
 
23
- const viteConfig = await createViteConfig({ packageRoot: PACKAGE_ROOT, projectRoot, configPath });
24
+ const viteConfig = await createViteConfig({ packageRoot: PACKAGE_ROOT, projectRoot, configPath, preset: options.preset });
24
25
  const server = await createServer({
25
26
  ...viteConfig,
26
27
  server: { ...viteConfig.server, port, host: options.host }
@@ -1,7 +1,7 @@
1
1
  import { Children, isValidElement, type ComponentProps } from 'react'
2
2
  import styles from './paragraph.module.css'
3
3
 
4
- const BLOCK_ELEMENTS = new Set(['summary', 'details', 'div', 'table', 'ul', 'ol'])
4
+ const BLOCK_ELEMENTS = new Set(['summary', 'details', 'div', 'table', 'ul', 'ol', 'p'])
5
5
 
6
6
  function hasBlockChild(children: React.ReactNode): boolean {
7
7
  return Children.toArray(children).some(
@@ -1,6 +1,8 @@
1
1
  import { useEffect } from 'react';
2
2
  import { prefetchPageData } from '@/lib/preload';
3
3
 
4
+ const NO_PREFETCH_ATTR = 'data-no-prefetch';
5
+
4
6
  function resolvePathname(href: string | null): string | null {
5
7
  if (!href) return null;
6
8
  try {
@@ -16,14 +18,14 @@ export function PrefetchProvider({ children }: { children: React.ReactNode }) {
16
18
  useEffect(() => {
17
19
  const handleMouseOver = (e: MouseEvent) => {
18
20
  const anchor = (e.target as HTMLElement).closest?.('a[href]');
19
- if (!anchor) return;
21
+ if (!anchor || anchor.hasAttribute(NO_PREFETCH_ATTR)) return;
20
22
  const pathname = resolvePathname(anchor.getAttribute('href'));
21
23
  if (pathname) prefetchPageData(pathname);
22
24
  };
23
25
 
24
26
  const handleFocusIn = (e: FocusEvent) => {
25
27
  const anchor = (e.target as HTMLElement).closest?.('a[href]');
26
- if (!anchor) return;
28
+ if (!anchor || anchor.hasAttribute(NO_PREFETCH_ATTR)) return;
27
29
  const pathname = resolvePathname(anchor.getAttribute('href'));
28
30
  if (pathname) prefetchPageData(pathname);
29
31
  };
@@ -45,7 +47,7 @@ export function PrefetchProvider({ children }: { children: React.ReactNode }) {
45
47
  );
46
48
 
47
49
  const observeLinks = () => {
48
- document.querySelectorAll('a[href]:not([data-prefetch-observed])').forEach((link) => {
50
+ document.querySelectorAll(`a[href]:not([data-prefetch-observed]):not([${NO_PREFETCH_ATTR}])`).forEach((link) => {
49
51
  const pathname = resolvePathname(link.getAttribute('href'));
50
52
  if (pathname) {
51
53
  link.setAttribute('data-prefetch-observed', '');
@@ -0,0 +1,4 @@
1
+ export const MdxNodeType = {
2
+ JsxFlow: 'mdxJsxFlowElement',
3
+ JsxText: 'mdxJsxTextElement',
4
+ } as const
@@ -110,6 +110,7 @@ export function PageProvider({
110
110
  frontmatter: Frontmatter;
111
111
  relativePath: string;
112
112
  originalPath?: string;
113
+ images?: string[];
113
114
  prev?: PageNavLink | null;
114
115
  next?: PageNavLink | null;
115
116
  }
@@ -132,6 +133,12 @@ export function PageProvider({
132
133
  try {
133
134
  const data = await fetchPageData(slug);
134
135
  if (cancelled.current) return;
136
+ if (data.images?.length) {
137
+ for (const src of data.images) {
138
+ const img = new Image();
139
+ img.src = src;
140
+ }
141
+ }
135
142
  const { content, toc } = await loadMdx(data.originalPath || data.relativePath);
136
143
  if (cancelled.current) return;
137
144
  setErrorStatus(null);
@@ -28,8 +28,13 @@ function isApisRoute(pathname: string): boolean {
28
28
  return pathname === '/apis' || pathname.startsWith('/apis/');
29
29
  }
30
30
 
31
+ function hasFileExtension(pathname: string): boolean {
32
+ const lastSegment = pathname.split('/').pop() ?? '';
33
+ return lastSegment.includes('.');
34
+ }
35
+
31
36
  export function prefetchPageData(pathname: string) {
32
- if (isApisRoute(pathname)) return;
37
+ if (isApisRoute(pathname) || hasFileExtension(pathname)) return;
33
38
  queryClient.prefetchQuery({
34
39
  queryKey: pageDataQueryKey(pathname),
35
40
  queryFn: () => fetchPageDataByPathname(pathname),
@@ -4,6 +4,7 @@ import type { Plugin } from 'unified'
4
4
  import type { Image, Html } from 'mdast'
5
5
  import type { Element } from 'hast'
6
6
  import type { MdxJsxFlowElement, MdxJsxTextElement, MdxJsxAttribute } from 'mdast-util-mdx-jsx'
7
+ import { MdxNodeType } from './mdx-utils'
7
8
 
8
9
  function resolveUrl(src: string, dir: string): string {
9
10
  if (/^[a-z][a-z0-9+\-.]*:/i.test(src)) return src
@@ -26,25 +27,40 @@ const remarkResolveImages: Plugin = () => {
26
27
  const relative = filePath.slice(contentIdx + '/content/'.length)
27
28
  const dir = path.posix.dirname(relative)
28
29
 
30
+ const seen = new Set<string>()
31
+ const images: string[] = []
32
+
33
+ function collect(src: string) {
34
+ if (!src || seen.has(src) || /^data:/i.test(src)) return
35
+ seen.add(src)
36
+ images.push(src)
37
+ }
38
+
29
39
  visit(tree, 'image', (node: Image) => {
30
40
  if (!node.url) return
31
41
  node.url = resolveUrl(node.url, dir)
42
+ collect(node.url)
32
43
  })
33
44
 
34
45
  visit(tree, 'html', (node: Html) => {
35
46
  node.value = node.value.replace(
36
47
  /(<img\b[^>]*\bsrc=["'])([^"']+)(["'])/gi,
37
- (_, before, src, after) => `${before}${resolveUrl(src, dir)}${after}`
48
+ (_, before, src, after) => {
49
+ const resolved = resolveUrl(src, dir)
50
+ collect(resolved)
51
+ return `${before}${resolved}${after}`
52
+ }
38
53
  )
39
54
  })
40
55
 
41
56
  visit(tree, (node) => {
42
- if (node.type !== 'mdxJsxFlowElement' && node.type !== 'mdxJsxTextElement') return
57
+ if (node.type !== MdxNodeType.JsxFlow && node.type !== MdxNodeType.JsxText) return
43
58
  const jsx = node as MdxJsxFlowElement | MdxJsxTextElement
44
59
  if (jsx.name !== 'img') return
45
60
  const srcAttr = jsx.attributes.find((a): a is MdxJsxAttribute => a.type === 'mdxJsxAttribute' && a.name === 'src')
46
61
  if (!srcAttr?.value || typeof srcAttr.value !== 'string') return
47
62
  srcAttr.value = resolveUrl(srcAttr.value, dir)
63
+ collect(srcAttr.value)
48
64
  })
49
65
 
50
66
  visit(tree, 'element', (node: Element) => {
@@ -52,7 +68,10 @@ const remarkResolveImages: Plugin = () => {
52
68
  const src = node.properties?.src
53
69
  if (typeof src !== 'string') return
54
70
  node.properties.src = resolveUrl(src, dir)
71
+ collect(node.properties.src as string)
55
72
  })
73
+
74
+ file.data.images = images
56
75
  }
57
76
  }
58
77
 
package/src/lib/source.ts CHANGED
@@ -37,6 +37,11 @@ const readingTimeGlob: Record<string, { text: string; minutes: number; words: nu
37
37
  { eager: true, import: 'readingTime' }
38
38
  );
39
39
 
40
+ const imagesGlob: Record<string, string[] | undefined> = import.meta.glob(
41
+ '../../.content/**/*.{mdx,md}',
42
+ { eager: true, import: 'images' }
43
+ );
44
+
40
45
  const metaGlob: Record<string, Record<string, unknown>> = import.meta.glob(
41
46
  '../../.content/**/meta.json',
42
47
  { eager: true }
@@ -54,10 +59,11 @@ function buildFiles() {
54
59
  const relativePath = originalPath.replace(/readme\.(mdx?)$/i, 'index.$1');
55
60
  const rt = readingTimeGlob[key];
56
61
  const _readingTime = rt?.minutes != null ? Math.max(1, Math.round(rt.minutes)) : undefined;
62
+ const _images = imagesGlob[key] ?? [];
57
63
  files.push({
58
64
  type: 'page',
59
65
  path: relativePath,
60
- data: { ...data, _readingTime, _relativePath: relativePath, _originalPath: originalPath }
66
+ data: { ...data, _readingTime, _images, _relativePath: relativePath, _originalPath: originalPath }
61
67
  });
62
68
  }
63
69
 
@@ -285,6 +291,10 @@ export function getOriginalPath(page: { data: unknown }): string {
285
291
  return ((page.data as Record<string, unknown>)._originalPath as string) ?? '';
286
292
  }
287
293
 
294
+ export function getPageImages(page: { data: unknown }): string[] {
295
+ return ((page.data as Record<string, unknown>)._images as string[]) ?? [];
296
+ }
297
+
288
298
  export async function getPageSearchContent(page: { data: unknown }): Promise<{ headings: string; body: string }> {
289
299
  const originalPath = getOriginalPath(page);
290
300
  if (!originalPath) return { headings: '', body: '' };
@@ -1,5 +1,5 @@
1
1
  import { defineHandler, HTTPError } from 'nitro';
2
- import { getPage, getPageNav, extractFrontmatter, getRelativePath, getOriginalPath, isDraft } from '@/lib/source';
2
+ import { getPage, getPageNav, extractFrontmatter, getRelativePath, getOriginalPath, getPageImages, isDraft } from '@/lib/source';
3
3
 
4
4
  export default defineHandler(async event => {
5
5
  const slugParam = event.url.searchParams.get('slug') ?? '';
@@ -16,6 +16,7 @@ export default defineHandler(async event => {
16
16
  frontmatter: extractFrontmatter(page, slug[slug.length - 1]),
17
17
  relativePath: getRelativePath(page),
18
18
  originalPath: getOriginalPath(page),
19
+ images: getPageImages(page),
19
20
  prev: nav.prev,
20
21
  next: nav.next,
21
22
  });
@@ -1,15 +1,19 @@
1
1
  import { defineHandler } from 'nitro';
2
- import { isSearchReady } from './search';
2
+ import { ensureIndex, isSearchReady } from './search';
3
+ import { LATEST_CONTEXT } from '@/lib/version-source';
3
4
 
4
- export default defineHandler(() => {
5
- const searchReady = isSearchReady();
5
+ export default defineHandler(async () => {
6
+ ensureIndex(LATEST_CONTEXT).catch(e => console.error('[search:index]', e));
6
7
 
7
- if (!searchReady) {
8
- return Response.json(
9
- { status: 'not_ready', search: false },
10
- { status: 503 },
11
- );
8
+ if (!isSearchReady()) {
9
+ return new Response(JSON.stringify({ status: 'not_ready', search: false }), {
10
+ status: 503,
11
+ headers: { 'Content-Type': 'application/json' },
12
+ });
12
13
  }
13
14
 
14
- return Response.json({ status: 'ready', search: true });
15
+ return new Response(JSON.stringify({ status: 'ready', search: true }), {
16
+ status: 200,
17
+ headers: { 'Content-Type': 'application/json' },
18
+ });
15
19
  });
@@ -9,7 +9,7 @@ import { getApiConfigsForVersion, loadConfig } from '@/lib/config';
9
9
  import { loadApiSpecs } from '@/lib/openapi';
10
10
  import { PageProvider } from '@/lib/page-context';
11
11
  import { resolveRoute, RouteType } from '@/lib/route-resolver';
12
- import { getPageTree, getPage, getPageNav, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath, isDraft } from '@/lib/source';
12
+ import { getPageTree, getPage, getPageNav, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath, getPageImages, isDraft } from '@/lib/source';
13
13
  import { getFirstApiUrl } from '@/lib/api-routes';
14
14
  import { StatusCodes } from 'http-status-codes';
15
15
  import { resolveDocsRedirect } from '@/lib/tree-utils';
@@ -79,6 +79,7 @@ export default {
79
79
  const relativePath = page ? getRelativePath(page) : null;
80
80
  const originalPath = page ? getOriginalPath(page) : null;
81
81
  const mdxModule = (originalPath || relativePath) ? await loadPageModule(originalPath || relativePath!) : null;
82
+ const pageImages = page ? getPageImages(page) : [];
82
83
 
83
84
  const pageData = page
84
85
  ? {
@@ -125,6 +126,9 @@ export default {
125
126
  {assets.js.map((attr: { href: string }) => (
126
127
  <link key={attr.href} rel="modulepreload" {...attr} />
127
128
  ))}
129
+ {pageImages.map((src: string) => (
130
+ <link key={src} rel="preload" as="image" href={src} />
131
+ ))}
128
132
  <script type="module" src={assets.entry} />
129
133
  <script dangerouslySetInnerHTML={{ __html: `window.__PAGE_DATA__ = ${safeJson}` }} />
130
134
  </head>
@@ -0,0 +1,11 @@
1
+ import { defineErrorHandler, HTTPError } from 'nitro';
2
+
3
+ export default defineErrorHandler((error, _event) => {
4
+ const status = HTTPError.isError(error) ? error.status : 500;
5
+ const message = error.message || 'Internal Server Error';
6
+
7
+ return new Response(JSON.stringify({ error: true, status, message }), {
8
+ status,
9
+ headers: { 'Content-Type': 'application/json' },
10
+ });
11
+ });
@@ -72,7 +72,7 @@ export async function createViteConfig(
72
72
  default: defineFumadocsConfig({
73
73
  mdxOptions: {
74
74
  remarkImageOptions: false,
75
- valueToExport: ['readingTime'],
75
+ valueToExport: ['readingTime', 'images'],
76
76
  remarkPlugins: [
77
77
  remarkDirective,
78
78
  [remarkDirectiveAdmonition, {
@@ -145,6 +145,7 @@ export async function createViteConfig(
145
145
  },
146
146
  nitro: {
147
147
  logLevel: 2,
148
+ errorHandler: path.resolve(packageRoot, 'src/server/error.ts'),
148
149
  publicAssets: [{ dir: path.resolve(projectRoot, 'public') }],
149
150
  output: {
150
151
  dir: resolveOutputDir(projectRoot, preset),
@@ -138,7 +138,7 @@ export function Layout({
138
138
  <DocumentTextIcon width={16} height={16} />
139
139
  )}
140
140
  classNames={{ root: styles.topLinkItem, text: styles.topLinkText }}
141
- render={<RouterLink to={entry.href} />}
141
+ render={<RouterLink to={entry.href} data-no-prefetch />}
142
142
  >
143
143
  {entry.label}
144
144
  </Sidebar.Item>
@@ -154,7 +154,7 @@ export function Layout({
154
154
  <CodeBracketSquareIcon width={16} height={16} />
155
155
  )}
156
156
  classNames={{ root: styles.topLinkItem, text: styles.topLinkText }}
157
- render={<RouterLink to={api.basePath} />}
157
+ render={<RouterLink to={api.basePath} data-no-prefetch />}
158
158
  >
159
159
  {api.name} API
160
160
  </Sidebar.Item>