@raystack/chronicle 0.12.3 → 0.12.5
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 +1763 -252
- package/package.json +8 -2
- package/src/cli/commands/build.ts +36 -7
- package/src/cli/commands/static-generate.ts +1071 -0
- package/src/cli/utils/mdx-error-report.ts +48 -0
- package/src/components/api/playground-dialog.tsx +65 -14
- package/src/components/mdx/code.tsx +8 -3
- package/src/components/mdx/image.tsx +15 -1
- package/src/components/mdx/index.tsx +10 -3
- package/src/components/ui/search.tsx +226 -64
- package/src/lib/data-urls.ts +23 -0
- package/src/lib/head.tsx +2 -1
- package/src/lib/mdx-component-names.ts +15 -0
- package/src/lib/mdx-error.test.ts +78 -0
- package/src/lib/mdx-error.ts +91 -0
- package/src/lib/openapi.ts +1 -1
- package/src/lib/page-context.tsx +29 -14
- package/src/lib/preload.ts +4 -2
- package/src/lib/remark-validate-mdx.test.ts +95 -0
- package/src/lib/remark-validate-mdx.ts +85 -0
- package/src/lib/route-resolver.ts +7 -0
- package/src/lib/static-mode.ts +3 -0
- package/src/pages/DocsPage.tsx +3 -2
- package/src/pages/NotFound.module.css +10 -0
- package/src/pages/RenderError.tsx +18 -0
- package/src/server/App.tsx +16 -12
- package/src/server/api/apis-proxy.ts +32 -7
- package/src/server/dev-error-page.ts +133 -0
- package/src/server/entry-server.tsx +6 -0
- package/src/server/entry-static.tsx +151 -0
- package/src/server/error.ts +13 -1
- package/src/server/plugins/telemetry.ts +3 -2
- package/src/server/vite-config.ts +22 -9
- package/src/themes/default/Page.module.css +2 -0
- package/src/themes/paper/Layout.module.css +0 -4
- package/src/themes/paper/Layout.tsx +0 -2
- package/src/themes/paper/Page.module.css +2 -0
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { defineHandler, HTTPError } from 'nitro';
|
|
2
|
+
import { StatusCodes } from 'http-status-codes';
|
|
2
3
|
import { loadConfig } from '@/lib/config';
|
|
3
4
|
import { loadApiSpecs } from '@/lib/openapi';
|
|
4
5
|
|
|
6
|
+
const MAX_BODY_SIZE = 10_485_760; // 10 MB
|
|
7
|
+
const UPSTREAM_TIMEOUT_MS = 120_000;
|
|
8
|
+
|
|
5
9
|
interface ProxyRequest {
|
|
6
10
|
specName: string;
|
|
7
11
|
method: string;
|
|
@@ -10,9 +14,26 @@ interface ProxyRequest {
|
|
|
10
14
|
body?: unknown;
|
|
11
15
|
}
|
|
12
16
|
|
|
17
|
+
function isPathSafe(p: string): boolean {
|
|
18
|
+
let decoded: string;
|
|
19
|
+
try {
|
|
20
|
+
decoded = decodeURIComponent(p);
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (/^[a-z]+:\/\//i.test(decoded)) return false;
|
|
25
|
+
const normalized = new URL(decoded, 'http://localhost').pathname;
|
|
26
|
+
return !normalized.split('/').includes('..');
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
export default defineHandler(async event => {
|
|
14
30
|
if (event.req.method !== 'POST') {
|
|
15
|
-
throw new HTTPError({ status:
|
|
31
|
+
throw new HTTPError({ status: StatusCodes.METHOD_NOT_ALLOWED, message: 'Method not allowed' });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const contentLength = parseInt(event.req.headers.get('content-length') ?? '0', 10);
|
|
35
|
+
if (contentLength > MAX_BODY_SIZE) {
|
|
36
|
+
throw new HTTPError({ status: StatusCodes.CONTENT_TOO_LARGE, message: `Request body too large (max ${MAX_BODY_SIZE} bytes)` });
|
|
16
37
|
}
|
|
17
38
|
|
|
18
39
|
const { specName, method, path, headers, body } =
|
|
@@ -20,7 +41,7 @@ export default defineHandler(async event => {
|
|
|
20
41
|
|
|
21
42
|
if (!specName || !method || !path) {
|
|
22
43
|
throw new HTTPError({
|
|
23
|
-
status:
|
|
44
|
+
status: StatusCodes.BAD_REQUEST,
|
|
24
45
|
message: 'Missing specName, method, or path'
|
|
25
46
|
});
|
|
26
47
|
}
|
|
@@ -30,11 +51,11 @@ export default defineHandler(async event => {
|
|
|
30
51
|
const spec = specs.find(s => s.name === specName);
|
|
31
52
|
|
|
32
53
|
if (!spec) {
|
|
33
|
-
throw new HTTPError({ status:
|
|
54
|
+
throw new HTTPError({ status: StatusCodes.NOT_FOUND, message: `Unknown spec: ${specName}` });
|
|
34
55
|
}
|
|
35
56
|
|
|
36
|
-
if (
|
|
37
|
-
throw new HTTPError({ status:
|
|
57
|
+
if (!isPathSafe(path)) {
|
|
58
|
+
throw new HTTPError({ status: StatusCodes.BAD_REQUEST, message: 'Invalid path' });
|
|
38
59
|
}
|
|
39
60
|
|
|
40
61
|
const url = spec.server.url + path;
|
|
@@ -43,7 +64,8 @@ export default defineHandler(async event => {
|
|
|
43
64
|
const response = await fetch(url, {
|
|
44
65
|
method,
|
|
45
66
|
headers,
|
|
46
|
-
body: body ? JSON.stringify(body) : undefined
|
|
67
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
68
|
+
signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
|
|
47
69
|
});
|
|
48
70
|
|
|
49
71
|
const contentType = response.headers.get('content-type') ?? '';
|
|
@@ -64,12 +86,15 @@ export default defineHandler(async event => {
|
|
|
64
86
|
headers: responseHeaders
|
|
65
87
|
});
|
|
66
88
|
} catch (error) {
|
|
89
|
+
if (error instanceof DOMException && error.name === 'TimeoutError') {
|
|
90
|
+
throw new HTTPError({ status: StatusCodes.GATEWAY_TIMEOUT, message: `Upstream request timed out after ${UPSTREAM_TIMEOUT_MS}ms` });
|
|
91
|
+
}
|
|
67
92
|
const message =
|
|
68
93
|
error instanceof Error
|
|
69
94
|
? `${error.message}${error.cause ? `: ${(error.cause as Error).message}` : ''}`
|
|
70
95
|
: 'Request failed';
|
|
71
96
|
throw new HTTPError({
|
|
72
|
-
status:
|
|
97
|
+
status: StatusCodes.BAD_GATEWAY,
|
|
73
98
|
message: `Could not reach ${url}\n${message}`
|
|
74
99
|
});
|
|
75
100
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { buildCodeFrame, type CodeFrame, parseMdxError } from '@/lib/mdx-error'
|
|
2
|
+
|
|
3
|
+
function escapeHtml(s: string): string {
|
|
4
|
+
return s
|
|
5
|
+
.replace(/&/g, '&')
|
|
6
|
+
.replace(/</g, '<')
|
|
7
|
+
.replace(/>/g, '>')
|
|
8
|
+
.replace(/"/g, '"')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function displayPath(file: string): string {
|
|
12
|
+
const roots = [
|
|
13
|
+
typeof __CHRONICLE_CONTENT_DIR__ !== 'undefined' ? __CHRONICLE_CONTENT_DIR__ : '',
|
|
14
|
+
typeof __CHRONICLE_PROJECT_ROOT__ !== 'undefined' ? __CHRONICLE_PROJECT_ROOT__ : '',
|
|
15
|
+
]
|
|
16
|
+
for (const root of roots) {
|
|
17
|
+
if (root && file.startsWith(root)) return file.slice(root.length).replace(/^\//, '')
|
|
18
|
+
}
|
|
19
|
+
return file
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function frameHtml(frame: CodeFrame): string {
|
|
23
|
+
const gutter = String(frame.lines[frame.lines.length - 1]?.number ?? 0).length
|
|
24
|
+
const rows = frame.lines.flatMap(({ number, text, target }) => {
|
|
25
|
+
const row = `<div class="row${target ? ' target' : ''}"><span class="ln">${target ? '>' : ' '} ${String(number).padStart(gutter)}</span><span class="src">${escapeHtml(text)}</span></div>`
|
|
26
|
+
if (target && frame.caretColumn) {
|
|
27
|
+
return [row, `<div class="row caret"><span class="ln">${' '.repeat(gutter + 1)}</span><span class="src">${' '.repeat(frame.caretColumn - 1)}^</span></div>`]
|
|
28
|
+
}
|
|
29
|
+
return [row]
|
|
30
|
+
})
|
|
31
|
+
return `<pre class="frame">${rows.join('')}</pre>`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Dev-only: renders a styled error page for MDX failures (syntax errors,
|
|
36
|
+
* unknown components) naming the file, position, and a source code frame.
|
|
37
|
+
* Returns null when the error is not MDX-related.
|
|
38
|
+
*/
|
|
39
|
+
export async function renderMdxErrorResponse(err: unknown): Promise<Response | null> {
|
|
40
|
+
const info = parseMdxError(err)
|
|
41
|
+
if (!info) return null
|
|
42
|
+
|
|
43
|
+
let frame: CodeFrame | null = null
|
|
44
|
+
if (info.file && info.line) {
|
|
45
|
+
try {
|
|
46
|
+
const fs = await import('node:fs/promises')
|
|
47
|
+
frame = buildCodeFrame(await fs.readFile(info.file, 'utf-8'), info.line, info.column)
|
|
48
|
+
} catch {
|
|
49
|
+
// source not readable — show the message without a code frame
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const location = info.file
|
|
54
|
+
? `${displayPath(info.file)}${info.line ? `:${info.line}${info.column ? `:${info.column}` : ''}` : ''}`
|
|
55
|
+
: null
|
|
56
|
+
|
|
57
|
+
const html = `<!DOCTYPE html>
|
|
58
|
+
<html lang="en">
|
|
59
|
+
<head>
|
|
60
|
+
<meta charset="UTF-8">
|
|
61
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
62
|
+
<title>MDX Error</title>
|
|
63
|
+
<style>
|
|
64
|
+
:root { color-scheme: light dark; }
|
|
65
|
+
body {
|
|
66
|
+
margin: 0; padding: 48px 24px;
|
|
67
|
+
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
68
|
+
background: #fff; color: #18181b;
|
|
69
|
+
}
|
|
70
|
+
.card { max-width: 860px; margin: 0 auto; }
|
|
71
|
+
.badge {
|
|
72
|
+
display: inline-block; padding: 4px 10px; border-radius: 6px;
|
|
73
|
+
background: #dc2626; color: #fff; font-size: 12px; font-weight: 600;
|
|
74
|
+
letter-spacing: 0.04em; text-transform: uppercase;
|
|
75
|
+
}
|
|
76
|
+
h1 { font-size: 20px; line-height: 1.5; margin: 16px 0 8px; }
|
|
77
|
+
.loc {
|
|
78
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
79
|
+
font-size: 13px; color: #71717a; margin-bottom: 24px;
|
|
80
|
+
}
|
|
81
|
+
.frame {
|
|
82
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
83
|
+
font-size: 13px; line-height: 1.7; overflow-x: auto;
|
|
84
|
+
background: #fafafa; border: 1px solid #e4e4e7; border-radius: 8px;
|
|
85
|
+
padding: 12px 0; margin: 0;
|
|
86
|
+
}
|
|
87
|
+
.row { display: flex; white-space: pre; }
|
|
88
|
+
.row .ln {
|
|
89
|
+
flex: none; padding: 0 12px; color: #a1a1aa;
|
|
90
|
+
border-right: 1px solid #e4e4e7; margin-right: 12px; user-select: none;
|
|
91
|
+
}
|
|
92
|
+
.row.target { background: rgba(220, 38, 38, 0.08); }
|
|
93
|
+
.row.target .ln, .row.caret .src { color: #dc2626; font-weight: 600; }
|
|
94
|
+
.hint { font-size: 13px; color: #71717a; margin-top: 24px; }
|
|
95
|
+
@media (prefers-color-scheme: dark) {
|
|
96
|
+
body { background: #09090b; color: #f4f4f5; }
|
|
97
|
+
.frame { background: #18181b; border-color: #27272a; }
|
|
98
|
+
.row .ln { border-color: #27272a; color: #52525b; }
|
|
99
|
+
.loc, .hint { color: #a1a1aa; }
|
|
100
|
+
}
|
|
101
|
+
</style>
|
|
102
|
+
</head>
|
|
103
|
+
<body>
|
|
104
|
+
<div class="card">
|
|
105
|
+
<span class="badge">MDX Error</span>
|
|
106
|
+
<h1>${escapeHtml(info.message)}</h1>
|
|
107
|
+
${location ? `<div class="loc">${escapeHtml(location)}</div>` : ''}
|
|
108
|
+
${frame ? frameHtml(frame) : ''}
|
|
109
|
+
<p class="hint">Fix the file and save — the page reloads automatically.</p>
|
|
110
|
+
</div>
|
|
111
|
+
<script>
|
|
112
|
+
(function poll(last) {
|
|
113
|
+
setTimeout(async () => {
|
|
114
|
+
try {
|
|
115
|
+
const res = await fetch(location.href, { headers: { accept: 'text/html' } });
|
|
116
|
+
if (res.ok) return location.reload();
|
|
117
|
+
const body = await res.text();
|
|
118
|
+
if (last !== null && body !== last) return location.reload();
|
|
119
|
+
poll(body);
|
|
120
|
+
} catch {
|
|
121
|
+
poll(last);
|
|
122
|
+
}
|
|
123
|
+
}, 1000);
|
|
124
|
+
})(null);
|
|
125
|
+
</script>
|
|
126
|
+
</body>
|
|
127
|
+
</html>`
|
|
128
|
+
|
|
129
|
+
return new Response(html, {
|
|
130
|
+
status: 500,
|
|
131
|
+
headers: { 'Content-Type': 'text/html;charset=utf-8' },
|
|
132
|
+
})
|
|
133
|
+
}
|
|
@@ -193,6 +193,7 @@ export default {
|
|
|
193
193
|
</body>
|
|
194
194
|
</html>,
|
|
195
195
|
);
|
|
196
|
+
await stream.allReady;
|
|
196
197
|
|
|
197
198
|
const renderDuration = performance.now() - renderStart;
|
|
198
199
|
|
|
@@ -207,6 +208,11 @@ export default {
|
|
|
207
208
|
});
|
|
208
209
|
} catch (err) {
|
|
209
210
|
console.error(`[chronicle] SSR error for ${pathname}:`, err);
|
|
211
|
+
if (import.meta.env.DEV) {
|
|
212
|
+
const { renderMdxErrorResponse } = await import('./dev-error-page');
|
|
213
|
+
const mdxError = await renderMdxErrorResponse(err);
|
|
214
|
+
if (mdxError) return mdxError;
|
|
215
|
+
}
|
|
210
216
|
return errorResponse(500, 'Internal Server Error', err instanceof Error ? err.message : String(err));
|
|
211
217
|
}
|
|
212
218
|
},
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import '@vitejs/plugin-react/preamble';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { BrowserRouter } from 'react-router';
|
|
5
|
+
import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
|
|
6
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
7
|
+
import { mdxComponents } from '@/components/mdx';
|
|
8
|
+
import { getApiConfigsForVersion } from '@/lib/config';
|
|
9
|
+
import { PageProvider } from '@/lib/page-context';
|
|
10
|
+
import { queryClient } from '@/lib/preload';
|
|
11
|
+
import { resolveRoute, resolveContentRootRedirect, RouteType } from '@/lib/route-resolver';
|
|
12
|
+
import { pageDataUrl, specsUrl } from '@/lib/data-urls';
|
|
13
|
+
import { resolveVersionFromUrl, type VersionContext } from '@/lib/version-source';
|
|
14
|
+
import type { ChronicleConfig, Frontmatter, PageNavLink, Root, TableOfContents } from '@/types';
|
|
15
|
+
import type { ApiSpec } from '@/lib/openapi';
|
|
16
|
+
import type { ReactNode } from 'react';
|
|
17
|
+
import { App } from './App';
|
|
18
|
+
|
|
19
|
+
interface StaticEmbeddedData {
|
|
20
|
+
config: ChronicleConfig;
|
|
21
|
+
tree: Root;
|
|
22
|
+
version: VersionContext;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const defaultConfig: ChronicleConfig = {
|
|
26
|
+
site: { title: 'Documentation' },
|
|
27
|
+
content: [{ dir: 'docs', label: 'Docs' }],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const contentModules = import.meta.glob<{ default?: React.ComponentType<any>; toc?: TableOfContents }>(
|
|
31
|
+
'../../.content/**/*.{mdx,md}'
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
async function loadMdxModule(relativePath: string): Promise<{ content: ReactNode; toc: TableOfContents }> {
|
|
35
|
+
const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
|
|
36
|
+
const key = relativePath.endsWith('.md')
|
|
37
|
+
? `../../.content/${withoutExt}.md`
|
|
38
|
+
: `../../.content/${withoutExt}.mdx`;
|
|
39
|
+
const loader = contentModules[key];
|
|
40
|
+
if (!loader) return { content: null, toc: [] };
|
|
41
|
+
const mod = await loader();
|
|
42
|
+
const content = mod.default
|
|
43
|
+
? React.createElement(mod.default, { components: mdxComponents })
|
|
44
|
+
: null;
|
|
45
|
+
return { content, toc: mod.toc ?? [] };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function fetchStaticPageData(slug: string[]) {
|
|
49
|
+
const res = await fetch(pageDataUrl(slug));
|
|
50
|
+
if (!res.ok) throw new Error(String(res.status));
|
|
51
|
+
const ct = res.headers.get('content-type') ?? '';
|
|
52
|
+
if (!ct.includes('json')) throw new Error('404');
|
|
53
|
+
return res.json();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function fetchStaticApiSpecs(version: VersionContext): Promise<ApiSpec[]> {
|
|
57
|
+
try {
|
|
58
|
+
const res = await fetch(specsUrl(version.dir));
|
|
59
|
+
if (!res.ok) return [];
|
|
60
|
+
const ct = res.headers.get('content-type') ?? '';
|
|
61
|
+
if (!ct.includes('json')) return [];
|
|
62
|
+
return res.json();
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function mount() {
|
|
69
|
+
try {
|
|
70
|
+
const embedded = (
|
|
71
|
+
window as unknown as { __PAGE_DATA__?: StaticEmbeddedData }
|
|
72
|
+
).__PAGE_DATA__;
|
|
73
|
+
|
|
74
|
+
const config: ChronicleConfig = embedded?.config ?? defaultConfig;
|
|
75
|
+
const tree: Root = embedded?.tree ?? { name: 'root', children: [] };
|
|
76
|
+
|
|
77
|
+
const route = resolveRoute(window.location.pathname, config);
|
|
78
|
+
|
|
79
|
+
if (route.type === RouteType.Redirect) {
|
|
80
|
+
window.location.replace(route.to);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (route.type === RouteType.DocsPage) {
|
|
85
|
+
const redirect = resolveContentRootRedirect(route.slug, config);
|
|
86
|
+
if (redirect) {
|
|
87
|
+
window.location.replace(redirect);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const routeVersion: VersionContext = resolveVersionFromUrl(
|
|
93
|
+
window.location.pathname,
|
|
94
|
+
config,
|
|
95
|
+
);
|
|
96
|
+
const version: VersionContext = embedded?.version ?? routeVersion;
|
|
97
|
+
|
|
98
|
+
const isApiRoute =
|
|
99
|
+
route.type === RouteType.ApiIndex || route.type === RouteType.ApiPage;
|
|
100
|
+
const apiConfigs = isApiRoute
|
|
101
|
+
? getApiConfigsForVersion(config, routeVersion.dir)
|
|
102
|
+
: [];
|
|
103
|
+
const apiSpecs: ApiSpec[] = apiConfigs.length
|
|
104
|
+
? await fetchStaticApiSpecs(routeVersion)
|
|
105
|
+
: [];
|
|
106
|
+
|
|
107
|
+
let page = null;
|
|
108
|
+
if (route.type === RouteType.DocsPage) {
|
|
109
|
+
try {
|
|
110
|
+
const data = await fetchStaticPageData(route.slug);
|
|
111
|
+
const mdxPath = data.originalPath || data.relativePath;
|
|
112
|
+
if (mdxPath && data.frontmatter) {
|
|
113
|
+
const { content, toc } = await loadMdxModule(mdxPath);
|
|
114
|
+
page = {
|
|
115
|
+
slug: route.slug,
|
|
116
|
+
frontmatter: data.frontmatter as Frontmatter,
|
|
117
|
+
prev: (data.prev as PageNavLink) ?? null,
|
|
118
|
+
next: (data.next as PageNavLink) ?? null,
|
|
119
|
+
content,
|
|
120
|
+
toc,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// page will remain null, context will show 404
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
createRoot(document.getElementById('root')!).render(
|
|
129
|
+
<QueryClientProvider client={queryClient}>
|
|
130
|
+
<BrowserRouter>
|
|
131
|
+
<ReactRouterProvider>
|
|
132
|
+
<PageProvider
|
|
133
|
+
initialConfig={config}
|
|
134
|
+
initialTree={tree}
|
|
135
|
+
initialPage={page}
|
|
136
|
+
initialApiSpecs={apiSpecs}
|
|
137
|
+
initialVersion={version}
|
|
138
|
+
loadMdx={loadMdxModule}
|
|
139
|
+
>
|
|
140
|
+
<App />
|
|
141
|
+
</PageProvider>
|
|
142
|
+
</ReactRouterProvider>
|
|
143
|
+
</BrowserRouter>
|
|
144
|
+
</QueryClientProvider>
|
|
145
|
+
);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error('Static mount failed:', err);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
mount();
|
package/src/server/error.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import { defineErrorHandler, HTTPError } from 'nitro';
|
|
2
2
|
|
|
3
|
-
export default defineErrorHandler((error, _event) => {
|
|
3
|
+
export default defineErrorHandler(async (error, _event) => {
|
|
4
4
|
const status = HTTPError.isError(error) ? error.status : 500;
|
|
5
5
|
const message = error.message || 'Internal Server Error';
|
|
6
6
|
|
|
7
|
+
// MDX failures in content (syntax errors, unknown components) surface here:
|
|
8
|
+
// the eager frontmatter glob in source.ts compiles every content file at
|
|
9
|
+
// module load, before the SSR fetch handler can catch anything.
|
|
10
|
+
if (import.meta.env.DEV) {
|
|
11
|
+
const { renderMdxErrorResponse } = await import('./dev-error-page');
|
|
12
|
+
const candidates = [error, (error as { cause?: unknown }).cause];
|
|
13
|
+
for (const candidate of candidates) {
|
|
14
|
+
const response = candidate ? await renderMdxErrorResponse(candidate) : null;
|
|
15
|
+
if (response) return response;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
return new Response(JSON.stringify({ error: true, status, message }), {
|
|
8
20
|
status,
|
|
9
21
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -82,7 +82,8 @@ export default definePlugin((nitroApp) => {
|
|
|
82
82
|
})
|
|
83
83
|
|
|
84
84
|
nitroApp.hooks.hook('chronicle:ssr-rendered', (route, status, durationMs) => {
|
|
85
|
-
|
|
85
|
+
const endpoint = status === 404 ? '/api/not-found' : toEndpoint(route)
|
|
86
|
+
ssrRenderDuration.record(durationMs, { route: endpoint, status })
|
|
86
87
|
})
|
|
87
88
|
|
|
88
89
|
nitroApp.hooks.hook('request', (event) => {
|
|
@@ -95,7 +96,7 @@ export default definePlugin((nitroApp) => {
|
|
|
95
96
|
const duration = performance.now() - start
|
|
96
97
|
const method = event.req.method
|
|
97
98
|
const route = new URL(event.req.url).pathname
|
|
98
|
-
const endpoint = toEndpoint(route)
|
|
99
|
+
const endpoint = res.status === 404 ? '/api/not-found' : toEndpoint(route)
|
|
99
100
|
|
|
100
101
|
const clientIp =
|
|
101
102
|
event.req.headers['x-forwarded-for']?.toString().split(',')[0].trim() ??
|
|
@@ -11,6 +11,7 @@ import remarkResolveImages from '../lib/remark-resolve-images';
|
|
|
11
11
|
import remarkResolveLinks from '../lib/remark-resolve-links';
|
|
12
12
|
import remarkReadingTime from 'remark-reading-time';
|
|
13
13
|
import remarkUnusedDirectives from '../lib/remark-unused-directives';
|
|
14
|
+
import remarkValidateMdx from '../lib/remark-validate-mdx';
|
|
14
15
|
|
|
15
16
|
function getDatabaseConnector(preset?: string): { connector: string; options?: Record<string, unknown> } {
|
|
16
17
|
switch (preset) {
|
|
@@ -27,7 +28,7 @@ function getDatabaseConnector(preset?: string): { connector: string; options?: R
|
|
|
27
28
|
|
|
28
29
|
const STATIC_PRESETS = new Set(['static', 'vercel-static', 'cloudflare-pages', 'github-pages']);
|
|
29
30
|
|
|
30
|
-
function isStaticPreset(preset?: string): boolean {
|
|
31
|
+
export function isStaticPreset(preset?: string): boolean {
|
|
31
32
|
return !!preset && STATIC_PRESETS.has(preset);
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -72,8 +73,9 @@ export async function createViteConfig(
|
|
|
72
73
|
plugins: [
|
|
73
74
|
nitro({
|
|
74
75
|
serverDir: path.resolve(packageRoot, 'src/server'),
|
|
75
|
-
...(preset && { preset }),
|
|
76
|
+
...(!isStaticPreset(preset) && preset && { preset }),
|
|
76
77
|
ignore: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx'],
|
|
78
|
+
...(isStaticPreset(preset) && { prerender: { routes: [] } }),
|
|
77
79
|
}),
|
|
78
80
|
mdx({
|
|
79
81
|
default: defineFumadocsConfig({
|
|
@@ -104,6 +106,7 @@ export async function createViteConfig(
|
|
|
104
106
|
[remarkResolveImages, { optimize: !isStaticPreset(preset) }],
|
|
105
107
|
remarkMdxMermaid,
|
|
106
108
|
remarkReadingTime,
|
|
109
|
+
remarkValidateMdx,
|
|
107
110
|
],
|
|
108
111
|
},
|
|
109
112
|
}),
|
|
@@ -146,8 +149,14 @@ export async function createViteConfig(
|
|
|
146
149
|
environments: {
|
|
147
150
|
client: {
|
|
148
151
|
build: {
|
|
152
|
+
manifest: isStaticPreset(preset),
|
|
149
153
|
rollupOptions: {
|
|
150
|
-
input: path.resolve(
|
|
154
|
+
input: path.resolve(
|
|
155
|
+
packageRoot,
|
|
156
|
+
isStaticPreset(preset)
|
|
157
|
+
? 'src/server/entry-static.tsx'
|
|
158
|
+
: 'src/server/entry-client.tsx',
|
|
159
|
+
)
|
|
151
160
|
}
|
|
152
161
|
}
|
|
153
162
|
}
|
|
@@ -166,12 +175,16 @@ export async function createViteConfig(
|
|
|
166
175
|
base: path.resolve(projectRoot, '.cache/images'),
|
|
167
176
|
},
|
|
168
177
|
},
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
...(isStaticPreset(preset)
|
|
179
|
+
? {}
|
|
180
|
+
: {
|
|
181
|
+
experimental: {
|
|
182
|
+
database: true,
|
|
183
|
+
},
|
|
184
|
+
database: {
|
|
185
|
+
default: getDatabaseConnector(preset),
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
175
188
|
},
|
|
176
189
|
};
|
|
177
190
|
}
|
|
@@ -126,6 +126,8 @@
|
|
|
126
126
|
table-layout: fixed;
|
|
127
127
|
width: 100%;
|
|
128
128
|
margin-bottom: var(--rs-space-5);
|
|
129
|
+
/* block inherited justify (e.g. from <p align="justify"> in content) without overriding GFM column alignment */
|
|
130
|
+
text-align: start;
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
.content table td,
|
|
@@ -6,7 +6,6 @@ import { useLocation, useNavigate } from 'react-router';
|
|
|
6
6
|
import { getLandingEntries } from '@/lib/config';
|
|
7
7
|
import { getActiveContentDir } from '@/lib/navigation';
|
|
8
8
|
import { usePageContext } from '@/lib/page-context';
|
|
9
|
-
import { Search } from '@/components/ui/search';
|
|
10
9
|
import type { ThemeLayoutProps } from '@/types';
|
|
11
10
|
import { ChapterNav } from './ChapterNav';
|
|
12
11
|
import styles from './Layout.module.css';
|
|
@@ -83,7 +82,6 @@ function LayoutInner({
|
|
|
83
82
|
</aside>
|
|
84
83
|
) : null}
|
|
85
84
|
<div className={cx(styles.content, classNames?.content, { [styles.contentFull]: !showSidebar })}>
|
|
86
|
-
{config.search?.enabled && <Search classNames={{ trigger: styles.hiddenTrigger }} />}
|
|
87
85
|
{children}
|
|
88
86
|
</div>
|
|
89
87
|
</Flex>
|
|
@@ -204,6 +204,8 @@
|
|
|
204
204
|
table-layout: fixed;
|
|
205
205
|
width: 100%;
|
|
206
206
|
margin-bottom: var(--rs-space-5);
|
|
207
|
+
/* block inherited justify (e.g. from <p align="justify"> in content) without overriding GFM column alignment */
|
|
208
|
+
text-align: start;
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
.content table td,
|