@clidey/dory 0.34.1 → 0.35.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/index.html CHANGED
@@ -38,13 +38,6 @@
38
38
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
39
39
  <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Inter&display=swap" rel="stylesheet">
40
40
 
41
- <script>
42
- !function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init be ys Ss me gs ws capture Ne calculateEventProperties xs register register_once register_for_session unregister unregister_for_session Rs getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSurveysLoaded onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey canRenderSurveyAsync identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty Is ks createPersonProfile Ps bs opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing $s debug Es getPageViewId captureTraceFeedback captureTraceMetric".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
43
- posthog.init('phc_hxwfcMiIgSXYKkbWl9zpQE0HaPPmlRaVXCWODricYu1', {
44
- api_host: 'https://us.i.posthog.com',
45
- person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
46
- })
47
- </script>
48
41
  </head>
49
42
  <body>
50
43
  <noscript>You need to enable JavaScript to run this app.</noscript>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@clidey/dory",
3
3
  "private": false,
4
- "version": "0.34.1",
4
+ "version": "0.35.0",
5
5
  "type": "module",
6
6
  "description": "A CLI tool for building and previewing documentation sites",
7
7
  "keywords": [
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "scripts": {
36
36
  "dev": "vite",
37
- "build": "npm run build:cli && tsc -b && vite build && npm run build:embed",
37
+ "build": "npm run build:cli && tsc -b && vite build",
38
38
  "build:cli": "tsc -p tsconfig.bin.json",
39
39
  "build:docs": "tsc -b && vite build",
40
40
  "build:embed": "npm run build:embed:loader && npm run build:embed:widget",
@@ -45,47 +45,47 @@
45
45
  "prepublishOnly": "echo 'Skipping prepublishOnly - build handled by publish script'"
46
46
  },
47
47
  "dependencies": {
48
- "@algolia/autocomplete-core": "^1.19.2",
49
- "@clidey/ux": "^0.36.0",
50
- "@fortawesome/fontawesome-svg-core": "^6.7.2",
51
- "@fortawesome/free-brands-svg-icons": "^6.7.2",
52
- "@fortawesome/free-regular-svg-icons": "^6.7.2",
53
- "@fortawesome/free-solid-svg-icons": "^6.7.2",
54
- "@fortawesome/react-fontawesome": "^0.2.2",
55
- "@headlessui/react": "^2.2.9",
56
- "@heroicons/react": "^2.2.0",
57
- "@mdx-js/mdx": "^3.1.0",
58
- "@mdx-js/preact": "^3.1.0",
59
- "@mdx-js/rollup": "^3.1.0",
60
- "@preact/preset-vite": "^2.10.1",
61
- "@tailwindcss/vite": "^4.1.16",
62
- "@types/react-highlight-words": "^0.20.0",
63
- "classnames": "^2.5.1",
64
- "flexsearch": "^0.8.205",
65
- "gray-matter": "^4.0.3",
66
- "gzip-size-cli": "^5.1.0",
67
- "katex": "^0.16.22",
68
- "lucide-react": "^0.513.0",
69
- "mermaid": "^11.6.0",
70
- "motion": "^12.16.0",
71
- "preact": "^10.26.5",
72
- "prism-react-renderer": "^2.4.1",
73
- "react-highlight-words": "^0.21.0",
74
- "remark-gfm": "^4.0.1",
75
- "saas": "^1.0.0",
76
- "sass-embedded": "^1.89.2",
77
- "sirv": "^3.0.1",
78
- "tailwindcss": "^4.1.16",
79
- "terser": "^5.44.1",
80
- "tsx": "^4.20.4",
48
+ "@algolia/autocomplete-core": "1.19.2",
49
+ "@clidey/ux": "0.36.0",
50
+ "@fortawesome/fontawesome-svg-core": "6.7.2",
51
+ "@fortawesome/free-brands-svg-icons": "6.7.2",
52
+ "@fortawesome/free-regular-svg-icons": "6.7.2",
53
+ "@fortawesome/free-solid-svg-icons": "6.7.2",
54
+ "@fortawesome/react-fontawesome": "0.2.2",
55
+ "@headlessui/react": "2.2.9",
56
+ "@heroicons/react": "2.2.0",
57
+ "@mdx-js/mdx": "3.1.0",
58
+ "@mdx-js/preact": "3.1.0",
59
+ "@mdx-js/rollup": "3.1.0",
60
+ "@preact/preset-vite": "2.10.1",
61
+ "@tailwindcss/vite": "4.1.16",
62
+ "@types/react-highlight-words": "0.20.0",
63
+ "classnames": "2.5.1",
64
+ "flexsearch": "0.8.205",
65
+ "gray-matter": "4.0.3",
66
+ "gzip-size-cli": "5.1.0",
67
+ "katex": "0.16.22",
68
+ "lucide-react": "0.513.0",
69
+ "mermaid": "11.6.0",
70
+ "motion": "12.16.0",
71
+ "preact": "10.26.5",
72
+ "prism-react-renderer": "2.4.1",
73
+ "react-highlight-words": "0.21.0",
74
+ "remark-gfm": "4.0.1",
75
+ "saas": "1.0.0",
76
+ "sass-embedded": "1.89.2",
77
+ "sirv": "3.0.1",
78
+ "tailwindcss": "4.1.16",
79
+ "tsx": "4.20.4",
81
80
  "typescript": "~5.8.3",
82
- "unist-util-visit": "^5.0.0",
83
- "vite": "^6.3.5",
84
- "vite-plugin-mdx": "^3.6.1",
85
- "wouter-preact": "^3.7.1"
81
+ "unist-util-visit": "5.0.0",
82
+ "vite": "6.3.5",
83
+ "vite-plugin-mdx": "3.6.1",
84
+ "wouter-preact": "3.7.1"
86
85
  },
87
- "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39",
86
+ "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a",
88
87
  "devDependencies": {
89
- "@types/node": "^24.0.12"
88
+ "@types/node": "24.0.12",
89
+ "terser": "5.44.1"
90
90
  }
91
91
  }
@@ -0,0 +1,59 @@
1
+ import { Component, type ComponentChildren } from 'preact';
2
+ import { ExclamationTriangleIcon } from '@heroicons/react/24/outline';
3
+
4
+ interface ErrorBoundaryProps {
5
+ children: ComponentChildren;
6
+ fallback?: (error: Error) => ComponentChildren;
7
+ }
8
+
9
+ interface ErrorBoundaryState {
10
+ hasError: boolean;
11
+ error: Error | null;
12
+ }
13
+
14
+ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
15
+ constructor(props: ErrorBoundaryProps) {
16
+ super(props);
17
+ this.state = { hasError: false, error: null };
18
+ }
19
+
20
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
21
+ return { hasError: true, error };
22
+ }
23
+
24
+ componentDidCatch(error: Error, errorInfo: any) {
25
+ console.error('Error caught by boundary:', error, errorInfo);
26
+ }
27
+
28
+ render() {
29
+ if (this.state.hasError && this.state.error) {
30
+ if (this.props.fallback) {
31
+ return this.props.fallback(this.state.error);
32
+ }
33
+ return (
34
+ <div className="flex flex-col items-center justify-center min-h-[50vh] p-8">
35
+ <div className="w-16 h-16 mb-4 flex items-center justify-center bg-red-100 dark:bg-red-900/30 rounded-full">
36
+ <ExclamationTriangleIcon className="w-8 h-8 text-red-500" />
37
+ </div>
38
+ <h2 className="text-2xl font-semibold text-red-700 dark:text-red-300 mb-2">
39
+ Something went wrong
40
+ </h2>
41
+ <p className="text-red-600 dark:text-red-400 mb-4 text-center max-w-md">
42
+ An error occurred while rendering this page.
43
+ </p>
44
+ <pre className="text-sm text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-md p-3 max-w-md mb-4 overflow-auto">
45
+ {this.state.error.message}
46
+ </pre>
47
+ <button
48
+ onClick={() => window.location.reload()}
49
+ className="px-4 py-2 text-sm font-medium rounded-md bg-[var(--brand-primary)] text-white hover:opacity-90 transition-opacity"
50
+ >
51
+ Reload Page
52
+ </button>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ return this.props.children;
58
+ }
59
+ }
@@ -70,10 +70,27 @@ export default function Routes() {
70
70
  useEffect(() => {
71
71
  const page = ALL_NAVIGATION.find((tab) => tab.groups.find((group) => group.pages.find((page) => page.href === pathname)));
72
72
  if (!page && pathname !== '/') {
73
- navigate(ALL_NAVIGATION[0].groups[0].pages[0].href);
74
- window.location.reload();
73
+ setRoutes([{
74
+ path: pathname,
75
+ component: () => (
76
+ <div className="flex flex-col items-center justify-center min-h-[50vh] p-8">
77
+ <h1 className="text-6xl font-bold text-gray-300 dark:text-gray-700 mb-4">404</h1>
78
+ <h2 className="text-2xl font-semibold mb-4">Page Not Found</h2>
79
+ <p className="text-gray-600 dark:text-gray-400 mb-6 text-center max-w-md">
80
+ The page <code className="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 rounded text-sm">{pathname}</code> doesn't exist.
81
+ </p>
82
+ <button
83
+ onClick={() => navigate(ALL_NAVIGATION[0].groups[0].pages[0].href)}
84
+ className="px-4 py-2 text-sm font-medium rounded-md bg-[var(--brand-primary)] text-white hover:opacity-90 transition-opacity"
85
+ >
86
+ Go to Home
87
+ </button>
88
+ </div>
89
+ )
90
+ }]);
91
+ setLoading(false);
75
92
  }
76
- }, [routes, pathname]);
93
+ }, [pathname]);
77
94
 
78
95
  useEffect(() => {
79
96
  window.scrollTo({ top: 0, behavior: 'smooth' });
@@ -1,41 +1,52 @@
1
- import { Document } from "flexsearch";
1
+ let searchIndex: any = null;
2
2
 
3
- export const searchIndex = new Document({
4
- tokenize: 'full',
5
- document: {
6
- id: 'url',
7
- index: 'content',
8
- store: ['title', 'pageTitle'],
9
- },
10
- context: {
11
- resolution: 9,
12
- depth: 2,
13
- bidirectional: true
3
+ async function getSearchIndex() {
4
+ if (!searchIndex) {
5
+ const { Document } = await import('flexsearch');
6
+ searchIndex = new Document({
7
+ tokenize: 'full',
8
+ document: {
9
+ id: 'url',
10
+ index: 'content',
11
+ store: ['title', 'pageTitle'],
12
+ },
13
+ context: {
14
+ resolution: 9,
15
+ depth: 2,
16
+ bidirectional: true
17
+ }
18
+ });
14
19
  }
15
- });
20
+ return searchIndex;
21
+ }
16
22
 
17
- export function addToSearchIndex(filename: string, data: any, content: string) {
18
- searchIndex.add({
19
- url: filename,
20
- title: data.title || '',
21
- content: content,
22
- pageTitle: data.title
23
- });
23
+ export async function addToSearchIndex(filename: string, data: any, content: string) {
24
+ try {
25
+ const index = await getSearchIndex();
26
+ index.add({
27
+ url: filename,
28
+ title: data.title || '',
29
+ content: content,
30
+ pageTitle: data.title
31
+ });
32
+ } catch (error) {
33
+ console.warn(`Failed to index ${filename}:`, error);
34
+ }
24
35
  }
25
36
 
26
37
  export function search(query: string, options = {}) {
27
38
  if (!searchIndex) return [];
28
-
39
+
29
40
  const result = searchIndex.search(query, {
30
41
  ...options,
31
42
  enrich: true,
32
- });
43
+ }) as any[];
33
44
 
34
45
  if (result.length === 0) {
35
46
  return [];
36
47
  }
37
48
 
38
- return result[0].result.map((item) => ({
49
+ return result[0].result.map((item: any) => ({
39
50
  url: item.id,
40
51
  title: item.doc?.title || '',
41
52
  pageTitle: item.doc?.pageTitle || '',
@@ -2,7 +2,7 @@ import type { ComponentType } from 'preact';
2
2
  import { type RouteComponentProps } from 'wouter-preact';
3
3
  import docsConfig from '../../docs/dory.json' with { type: 'json' };
4
4
  import type { DoryConfig } from '../types/config';
5
- import { searchIndex } from './search-index';
5
+ import { addToSearchIndex } from './search-index';
6
6
 
7
7
  const config = docsConfig as DoryConfig;
8
8
 
@@ -108,16 +108,26 @@ export async function preloadFrontmatter() {
108
108
  // Add preloaded content to search index
109
109
  async function addPreloadedContentToSearch() {
110
110
  if (!preloadedFrontMatter) return;
111
+
112
+ const mdxFiles = import.meta.glob<{ default: string }>('../../docs/**/*.mdx', { query: 'raw' });
113
+ const fileEntries = Object.entries(mdxFiles);
111
114
 
112
- // Content is already included in the preloaded frontmatter, no need to fetch raw files
113
- preloadedFrontMatter.forEach((fm) => {
114
- searchIndex.add({
115
- url: fm.path,
116
- title: fm.title || '',
117
- content: fm.content || '',
118
- pageTitle: fm.title || ''
119
- });
120
- });
115
+ await Promise.all(
116
+ preloadedFrontMatter.map(async (fm) => {
117
+ const fileEntry = fileEntries.find(([filePath]) => pathFromFilename(filePath) === fm.path);
118
+ if (fileEntry) {
119
+ try {
120
+ const [, loader] = fileEntry;
121
+ const rawContent = (await loader()).default;
122
+ const { content } = parseFrontMatter(rawContent);
123
+
124
+ await addToSearchIndex(fm.path, { title: fm.title }, content);
125
+ } catch (error) {
126
+ console.warn(`Failed to index ${fm.path}:`, error);
127
+ }
128
+ }
129
+ })
130
+ );
121
131
  }
122
132
 
123
133
  // Load frontmatter for a specific pathname (optimized with fallback)
@@ -143,12 +153,7 @@ export async function loadMDXFrontMatterForPath(pathname: string) {
143
153
  const filename = pathFromFilename(path);
144
154
 
145
155
  updateNavigationTitle(filename, data.title);
146
- searchIndex.add({
147
- url: filename,
148
- title: data.title || '',
149
- content,
150
- pageTitle: data.title || ''
151
- });
156
+ await addToSearchIndex(filename, data, content);
152
157
 
153
158
  const frontmatterItem = { ...data, path: filename };
154
159
  completeFrontMatter = [...completeFrontMatter, frontmatterItem];
@@ -175,12 +180,7 @@ export async function loadAllMDXFrontMatter(pathname: string) {
175
180
  const filename = pathFromFilename(path);
176
181
 
177
182
  updateNavigationTitle(filename, data.title);
178
- searchIndex.add({
179
- url: filename,
180
- title: data.title || '',
181
- content,
182
- pageTitle: data.title || ''
183
- });
183
+ await addToSearchIndex(filename, data, content);
184
184
 
185
185
  newFrontmatter.push({ ...data, path: filename });
186
186
  })
@@ -47,8 +47,9 @@ if (typeof window !== 'undefined' && window.DoryDocs) {
47
47
  widgetLoading = true;
48
48
  widgetLoadPromise = new Promise((resolve, reject) => {
49
49
  const script = document.createElement('script');
50
- script.src = `${config.baseUrl}/embed-widget.js`;
50
+ script.src = `${baseUrl}/embed-widget.js`;
51
51
  script.async = true;
52
+ script.type = 'module';
52
53
 
53
54
  script.onload = () => {
54
55
  // Widget script exposes window.__DoryWidget__
@@ -296,9 +296,6 @@ export class DoryWidget {
296
296
  }
297
297
  }
298
298
 
299
- // Export as default for IIFE build
300
- export default DoryWidget;
301
-
302
299
  // Expose widget globally for loader script
303
300
  if (typeof window !== 'undefined') {
304
301
  window.__DoryWidget__ = DoryWidget;
package/src/main.tsx CHANGED
@@ -2,6 +2,7 @@ import { render } from 'preact'
2
2
  import '@clidey/ux/styles.css';
3
3
  import { App } from './app.tsx'
4
4
  import { withMDX } from './components/mdx-provider.tsx'
5
+ import { ErrorBoundary } from './components/error-boundary.tsx'
5
6
  import { loadFonts } from './utils/fonts'
6
7
  import { loadColors } from './utils/colors'
7
8
  import { Toaster } from '@clidey/ux'
@@ -16,7 +17,9 @@ const AppWithMDX = withMDX(App)
16
17
  render(
17
18
  <ThemeProvider storageKey="@clidey/dory/theme">
18
19
  <Toaster />
19
- <AppWithMDX />
20
+ <ErrorBoundary>
21
+ <AppWithMDX />
22
+ </ErrorBoundary>
20
23
  </ThemeProvider>,
21
24
  document.getElementById('app')!
22
25
  )
package/src/mdx/latex.tsx CHANGED
@@ -1,6 +1,5 @@
1
- import { useEffect, useRef } from 'react'
2
- import katex from 'katex'
3
- import 'katex/dist/katex.min.css'
1
+ import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
2
+ import { useEffect, useRef, useState } from 'react'
4
3
 
5
4
  interface LatexProps {
6
5
  children: string
@@ -8,19 +7,77 @@ interface LatexProps {
8
7
 
9
8
  export function Latex({ children }: LatexProps) {
10
9
  const containerRef = useRef<HTMLDivElement>(null)
10
+ const [error, setError] = useState<string | null>(null)
11
+ const [isLoading, setIsLoading] = useState(true)
11
12
 
12
13
  useEffect(() => {
13
- if (containerRef.current) {
14
+ let mounted = true
15
+
16
+ const renderLatex = async () => {
14
17
  try {
18
+ setError(null)
19
+ setIsLoading(true)
20
+
21
+ const [katexModule] = await Promise.all([
22
+ import('katex'),
23
+ import('katex/dist/katex.min.css'),
24
+ ])
25
+ const katex = katexModule.default
26
+
27
+ if (!mounted || !containerRef.current) return
28
+
15
29
  katex.render(children, containerRef.current, {
16
30
  displayMode: true,
17
31
  throwOnError: false,
18
32
  })
33
+ setIsLoading(false)
19
34
  } catch (error) {
20
- console.error('Error rendering LaTeX:', error)
35
+ if (mounted) {
36
+ console.error('Error rendering LaTeX:', error)
37
+ setError(error instanceof Error ? error.message : 'Failed to render LaTeX')
38
+ setIsLoading(false)
39
+ }
21
40
  }
22
41
  }
42
+
43
+ renderLatex()
44
+
45
+ return () => {
46
+ mounted = false
47
+ }
23
48
  }, [children])
24
49
 
25
- return <div ref={containerRef} className="my-4" />
50
+ if (error) {
51
+ return (
52
+ <div className="my-4">
53
+ <div className="flex flex-col items-center justify-center py-12 px-6 text-center bg-gradient-to-br from-red-50 to-red-100 dark:from-red-900/20 dark:to-red-800/20 border-2 border-dashed border-red-300 dark:border-red-700 rounded-lg">
54
+ <div className="w-16 h-16 mb-4 flex items-center justify-center bg-red-100 dark:bg-red-900/30 rounded-full">
55
+ <ExclamationTriangleIcon className="w-8 h-8 text-red-500" />
56
+ </div>
57
+ <h3 className="text-lg font-semibold text-red-700 dark:text-red-300 mb-2">
58
+ LaTeX Rendering Error
59
+ </h3>
60
+ <p className="text-red-600 dark:text-red-400 mb-4 max-w-md">
61
+ The LaTeX expression could not be rendered.
62
+ </p>
63
+ <div className="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-md p-3 max-w-md">
64
+ <p className="text-sm text-red-700 dark:text-red-300 font-mono break-all">
65
+ {error}
66
+ </p>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ )
71
+ }
72
+
73
+ return (
74
+ <div className="my-4">
75
+ {isLoading && (
76
+ <div className="flex items-center justify-center py-4">
77
+ <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500" />
78
+ </div>
79
+ )}
80
+ <div ref={containerRef} className={isLoading ? 'hidden' : ''} />
81
+ </div>
82
+ )
26
83
  }
package/src/mdx/mdx.tsx CHANGED
@@ -1,6 +1,8 @@
1
1
  import { useEffect, useRef } from 'preact/hooks';
2
+ import { lazy, Suspense } from 'preact/compat';
2
3
  import { usePathname } from 'wouter-preact/use-browser-location';
3
4
  import { completeFrontMatter } from '../components/store';
5
+ import { Loading } from '../components/loading';
4
6
 
5
7
  export { h1, h2, h3, h4, h5, h6, p, Row, Col, Anchor as a, Image as img } from './text';
6
8
  export { Code as code, CodeGroup } from './code';
@@ -17,11 +19,24 @@ export { ResponseField, Properties, Property } from './open-api';
17
19
  export { Steps, Step } from './step';
18
20
  export { Table as table, Th as th, Td as td } from './table';
19
21
  export { UnorderedList as ul, OrderedList as ol, ListItem as li } from './list';
20
- export { APIPlayground } from './api-playground';
21
- export { WebSocketPlayground } from './websocket-playground';
22
22
  export { AsyncAPI } from './async-api';
23
23
  export { Source } from './source';
24
24
 
25
+ // Lazy-loaded heavy components — only downloaded when a page uses them
26
+ const LazyAPIPlayground = lazy(() => import('./api-playground').then(m => ({ default: m.APIPlayground })));
27
+ export const APIPlayground = (props: any) => (
28
+ <Suspense fallback={<Loading />}>
29
+ <LazyAPIPlayground {...props} />
30
+ </Suspense>
31
+ );
32
+
33
+ const LazyWebSocketPlayground = lazy(() => import('./websocket-playground').then(m => ({ default: m.WebSocketPlayground })));
34
+ export const WebSocketPlayground = (props: any) => (
35
+ <Suspense fallback={<Loading />}>
36
+ <LazyWebSocketPlayground {...props} />
37
+ </Suspense>
38
+ );
39
+
25
40
  export const wrapper = ({ children }: { children: preact.ComponentChildren }) => {
26
41
  const ref = useRef<HTMLDivElement>(null);
27
42
  const pathname = usePathname();
@@ -0,0 +1,43 @@
1
+ import type { Plugin } from 'vite';
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { resolve } from 'path';
4
+
5
+ /**
6
+ * Conditionally injects analytics scripts based on dory.json config.
7
+ * PostHog is only injected if `analytics.posthog.token` is configured.
8
+ */
9
+ export function analyticsInjector(): Plugin {
10
+ return {
11
+ name: 'analytics-injector',
12
+ transformIndexHtml(html) {
13
+ const doryJsonPath = resolve(process.cwd(), 'docs', 'dory.json');
14
+ if (!existsSync(doryJsonPath)) return html;
15
+
16
+ try {
17
+ const config = JSON.parse(readFileSync(doryJsonPath, 'utf-8'));
18
+
19
+ if (config.analytics?.posthog?.token) {
20
+ const token = config.analytics.posthog.token;
21
+ const apiHost = config.analytics.posthog.apiHost || 'https://us.i.posthog.com';
22
+
23
+ const script = `
24
+ <script>
25
+ !function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture identify setPersonProperties group resetGroups reset get_distinct_id getGroups get_session_id opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
26
+ posthog.init('${token}', {
27
+ api_host: '${apiHost}',
28
+ person_profiles: 'identified_only',
29
+ })
30
+ </script>`;
31
+
32
+ return html.replace('</head>', `${script}\n </head>`);
33
+ }
34
+
35
+ return html;
36
+ } catch (error) {
37
+ const detail = error instanceof SyntaxError ? error.message : String(error);
38
+ console.error(`Failed to parse dory.json for analytics injection: ${detail}`);
39
+ return html;
40
+ }
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,68 @@
1
+ import type { Plugin } from 'vite';
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { resolve } from 'path';
4
+
5
+ /**
6
+ * Generates a Content-Security-Policy meta tag based on dory.json config.
7
+ * Dynamically adds font sources and analytics domains as needed.
8
+ */
9
+ export function cspGenerator(): Plugin {
10
+ return {
11
+ name: 'csp-generator',
12
+ transformIndexHtml(html) {
13
+ const doryJsonPath = resolve(process.cwd(), 'docs', 'dory.json');
14
+ if (!existsSync(doryJsonPath)) return html;
15
+
16
+ try {
17
+ const config = JSON.parse(readFileSync(doryJsonPath, 'utf-8'));
18
+
19
+ const scriptSrc = ["'self'", "'unsafe-inline'", "'unsafe-eval'"];
20
+ const styleSrc = ["'self'", "'unsafe-inline'"];
21
+ const fontSrc = ["'self'", "data:"];
22
+ const connectSrc = ["'self'"];
23
+ const imgSrc = ["'self'", "data:", "https:"];
24
+
25
+ // Add Google Fonts if fonts are configured with external source
26
+ if (config.fonts?.source) {
27
+ try {
28
+ const fontUrl = new URL(config.fonts.source);
29
+ fontSrc.push(fontUrl.origin);
30
+ } catch {
31
+ // Not a URL, skip
32
+ }
33
+ }
34
+ styleSrc.push("https://fonts.googleapis.com");
35
+ fontSrc.push("https://fonts.gstatic.com");
36
+
37
+ // Add PostHog if analytics configured
38
+ if (config.analytics?.posthog?.token) {
39
+ const apiHost = config.analytics.posthog.apiHost || 'https://us.i.posthog.com';
40
+ const assetsHost = apiHost.replace('.i.posthog.com', '-assets.i.posthog.com');
41
+ scriptSrc.push(apiHost);
42
+ scriptSrc.push(assetsHost);
43
+ connectSrc.push(apiHost);
44
+ }
45
+
46
+ // Note: frame-ancestors is omitted because it's ignored in <meta> CSP tags.
47
+ // Clickjacking protection is handled via X-Frame-Options in nginx config.
48
+ const csp = [
49
+ `default-src 'self'`,
50
+ `script-src ${scriptSrc.join(' ')}`,
51
+ `style-src ${styleSrc.join(' ')}`,
52
+ `font-src ${fontSrc.join(' ')}`,
53
+ `img-src ${imgSrc.join(' ')}`,
54
+ `connect-src ${connectSrc.join(' ')}`,
55
+ ].join('; ');
56
+
57
+ return html.replace(
58
+ '</head>',
59
+ ` <meta http-equiv="Content-Security-Policy" content="${csp}">\n </head>`
60
+ );
61
+ } catch (error) {
62
+ const detail = error instanceof SyntaxError ? error.message : String(error);
63
+ console.error(`Failed to generate CSP: ${detail}`);
64
+ return html;
65
+ }
66
+ },
67
+ };
68
+ }