@odvi/create-dtt-framework 0.1.2 → 0.1.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.
Files changed (114) hide show
  1. package/dist/commands/create.d.ts.map +1 -1
  2. package/dist/commands/create.js +16 -13
  3. package/dist/commands/create.js.map +1 -1
  4. package/dist/utils/template.d.ts.map +1 -1
  5. package/dist/utils/template.js +5 -0
  6. package/dist/utils/template.js.map +1 -1
  7. package/package.json +3 -2
  8. package/template/.env.example +103 -0
  9. package/template/components.json +22 -0
  10. package/template/docs/framework/01-overview.md +289 -0
  11. package/template/docs/framework/02-techstack.md +503 -0
  12. package/template/docs/framework/api-layer.md +681 -0
  13. package/template/docs/framework/clerk-authentication.md +649 -0
  14. package/template/docs/framework/cli-installation.md +564 -0
  15. package/template/docs/framework/deployment/ci-cd.md +907 -0
  16. package/template/docs/framework/deployment/digitalocean.md +991 -0
  17. package/template/docs/framework/deployment/domain-setup.md +972 -0
  18. package/template/docs/framework/deployment/environment-variables.md +863 -0
  19. package/template/docs/framework/deployment/monitoring.md +927 -0
  20. package/template/docs/framework/deployment/production-checklist.md +649 -0
  21. package/template/docs/framework/deployment/vercel.md +791 -0
  22. package/template/docs/framework/environment-variables.md +658 -0
  23. package/template/docs/framework/health-check-system.md +582 -0
  24. package/template/docs/framework/implementation.md +559 -0
  25. package/template/docs/framework/snowflake-integration.md +591 -0
  26. package/template/docs/framework/state-management.md +615 -0
  27. package/template/docs/framework/supabase-integration.md +581 -0
  28. package/template/docs/framework/testing-guide.md +544 -0
  29. package/template/docs/framework/what-did-i-miss.md +526 -0
  30. package/template/drizzle.config.ts +12 -0
  31. package/template/next.config.js +21 -0
  32. package/template/postcss.config.js +5 -0
  33. package/template/prettier.config.js +4 -0
  34. package/template/public/favicon.ico +0 -0
  35. package/template/src/app/(auth)/layout.tsx +4 -0
  36. package/template/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +10 -0
  37. package/template/src/app/(auth)/sign-up/[[...sign-up]]/page.tsx +10 -0
  38. package/template/src/app/(dashboard)/dashboard/page.tsx +8 -0
  39. package/template/src/app/(dashboard)/health/page.tsx +16 -0
  40. package/template/src/app/(dashboard)/layout.tsx +17 -0
  41. package/template/src/app/api/[[...route]]/route.ts +11 -0
  42. package/template/src/app/api/debug-files/route.ts +33 -0
  43. package/template/src/app/api/webhooks/clerk/route.ts +112 -0
  44. package/template/src/app/layout.tsx +28 -0
  45. package/template/src/app/page.tsx +12 -0
  46. package/template/src/app/providers.tsx +20 -0
  47. package/template/src/components/layouts/navbar.tsx +14 -0
  48. package/template/src/components/shared/loading-spinner.tsx +6 -0
  49. package/template/src/components/ui/badge.tsx +46 -0
  50. package/template/src/components/ui/button.tsx +62 -0
  51. package/template/src/components/ui/card.tsx +92 -0
  52. package/template/src/components/ui/collapsible.tsx +33 -0
  53. package/template/src/components/ui/scroll-area.tsx +58 -0
  54. package/template/src/components/ui/sheet.tsx +139 -0
  55. package/template/src/config/__tests__/env.test.ts +166 -0
  56. package/template/src/config/__tests__/site.test.ts +46 -0
  57. package/template/src/config/env.ts +36 -0
  58. package/template/src/config/site.ts +10 -0
  59. package/template/src/env.js +44 -0
  60. package/template/src/features/__tests__/health-check-config.test.ts +142 -0
  61. package/template/src/features/__tests__/health-check-types.test.ts +201 -0
  62. package/template/src/features/documentation/components/doc-sidebar.tsx +109 -0
  63. package/template/src/features/documentation/components/doc-viewer.tsx +70 -0
  64. package/template/src/features/documentation/index.tsx +92 -0
  65. package/template/src/features/documentation/utils/doc-loader.ts +177 -0
  66. package/template/src/features/health-check/components/health-dashboard.tsx +363 -0
  67. package/template/src/features/health-check/config.ts +72 -0
  68. package/template/src/features/health-check/index.ts +4 -0
  69. package/template/src/features/health-check/stores/health-store.ts +14 -0
  70. package/template/src/features/health-check/types.ts +18 -0
  71. package/template/src/hooks/__tests__/use-debounce.test.tsx +28 -0
  72. package/template/src/hooks/queries/use-health-checks.ts +16 -0
  73. package/template/src/hooks/utils/use-debounce.ts +20 -0
  74. package/template/src/lib/__tests__/utils.test.ts +52 -0
  75. package/template/src/lib/__tests__/validators.test.ts +114 -0
  76. package/template/src/lib/nextbank/client.ts +37 -0
  77. package/template/src/lib/snowflake/client.ts +53 -0
  78. package/template/src/lib/supabase/admin.ts +7 -0
  79. package/template/src/lib/supabase/client.ts +7 -0
  80. package/template/src/lib/supabase/server.ts +23 -0
  81. package/template/src/lib/utils.ts +6 -0
  82. package/template/src/lib/validators.ts +9 -0
  83. package/template/src/middleware.ts +22 -0
  84. package/template/src/server/api/index.ts +22 -0
  85. package/template/src/server/api/middleware/auth.ts +19 -0
  86. package/template/src/server/api/middleware/logger.ts +4 -0
  87. package/template/src/server/api/routes/health/clerk.ts +214 -0
  88. package/template/src/server/api/routes/health/database.ts +117 -0
  89. package/template/src/server/api/routes/health/edge-functions.ts +75 -0
  90. package/template/src/server/api/routes/health/framework.ts +45 -0
  91. package/template/src/server/api/routes/health/index.ts +102 -0
  92. package/template/src/server/api/routes/health/nextbank.ts +67 -0
  93. package/template/src/server/api/routes/health/snowflake.ts +83 -0
  94. package/template/src/server/api/routes/health/storage.ts +163 -0
  95. package/template/src/server/api/routes/users.ts +95 -0
  96. package/template/src/server/db/index.ts +17 -0
  97. package/template/src/server/db/queries/users.ts +8 -0
  98. package/template/src/server/db/schema/__tests__/health-checks.test.ts +31 -0
  99. package/template/src/server/db/schema/__tests__/users.test.ts +46 -0
  100. package/template/src/server/db/schema/health-checks.ts +11 -0
  101. package/template/src/server/db/schema/index.ts +2 -0
  102. package/template/src/server/db/schema/users.ts +16 -0
  103. package/template/src/server/db/schema.ts +26 -0
  104. package/template/src/stores/__tests__/ui-store.test.ts +87 -0
  105. package/template/src/stores/ui-store.ts +14 -0
  106. package/template/src/styles/globals.css +129 -0
  107. package/template/src/test/mocks/clerk.ts +35 -0
  108. package/template/src/test/mocks/snowflake.ts +28 -0
  109. package/template/src/test/mocks/supabase.ts +37 -0
  110. package/template/src/test/setup.ts +69 -0
  111. package/template/src/test/utils/test-helpers.ts +158 -0
  112. package/template/src/types/index.ts +14 -0
  113. package/template/tsconfig.json +43 -0
  114. package/template/vitest.config.ts +44 -0
@@ -0,0 +1,109 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import Link from 'next/link';
5
+ import { useSearchParams } from 'next/navigation';
6
+ import { cn } from '@/lib/utils';
7
+ import { FileText, Folder, FolderOpen, ChevronRight, ChevronDown } from 'lucide-react';
8
+ import type { DocNode } from '../utils/doc-loader';
9
+ import { Button } from '@/components/ui/button';
10
+ import { ScrollArea } from '@/components/ui/scroll-area';
11
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
12
+
13
+ interface DocSidebarProps {
14
+ nodes: DocNode[];
15
+ className?: string;
16
+ }
17
+
18
+ export function DocSidebar({ nodes, className }: DocSidebarProps) {
19
+ const searchParams = useSearchParams();
20
+ const currentDoc = searchParams.get('doc');
21
+
22
+ // If no doc is selected, we might want to highlight README or nothing
23
+ // But usually README corresponds to 'README.md' id if we clicked it.
24
+
25
+ return (
26
+ <div className={cn('pb-12 w-64 border-r h-full bg-muted/10', className)}>
27
+ <div className="space-y-4 py-4">
28
+ <div className="px-3 py-2">
29
+ <h2 className="mb-2 px-4 text-lg font-semibold tracking-tight">
30
+ Documentation
31
+ </h2>
32
+ <ScrollArea className="h-[calc(100vh-8rem)] px-1">
33
+ <div className="space-y-1 p-2">
34
+ {nodes.map((node) => (
35
+ <DocNodeItem key={node.id} node={node} currentDoc={currentDoc} />
36
+ ))}
37
+ </div>
38
+ </ScrollArea>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ );
43
+ }
44
+
45
+ function DocNodeItem({ node, currentDoc }: { node: DocNode; currentDoc: string | null }) {
46
+ const [isOpen, setIsOpen] = React.useState(false);
47
+
48
+ // Auto-expand if a child is active
49
+ React.useEffect(() => {
50
+ if (node.type === 'directory' && node.children) {
51
+ const hasActiveChild = checkActiveChild(node, currentDoc);
52
+ if (hasActiveChild) {
53
+ setIsOpen(true);
54
+ }
55
+ }
56
+ }, [currentDoc, node]);
57
+
58
+ if (node.type === 'directory') {
59
+ return (
60
+ <Collapsible open={isOpen} onOpenChange={setIsOpen} className="w-full">
61
+ <CollapsibleTrigger asChild>
62
+ <Button
63
+ variant="ghost"
64
+ size="sm"
65
+ className="w-full justify-start font-normal"
66
+ >
67
+ {isOpen ? (
68
+ <ChevronDown className="mr-2 h-4 w-4" />
69
+ ) : (
70
+ <ChevronRight className="mr-2 h-4 w-4" />
71
+ )}
72
+ <Folder className="mr-2 h-4 w-4 text-sky-500" />
73
+ {node.title}
74
+ </Button>
75
+ </CollapsibleTrigger>
76
+ <CollapsibleContent className="pl-4 space-y-1">
77
+ {node.children?.map((child) => (
78
+ <DocNodeItem key={child.id} node={child} currentDoc={currentDoc} />
79
+ ))}
80
+ </CollapsibleContent>
81
+ </Collapsible>
82
+ );
83
+ }
84
+
85
+ const isActive = currentDoc === node.id || (!currentDoc && node.id === 'README.md');
86
+
87
+ return (
88
+ <Button
89
+ asChild
90
+ variant={isActive ? 'secondary' : 'ghost'}
91
+ size="sm"
92
+ className={cn('w-full justify-start font-normal', isActive && 'bg-accent')}
93
+ >
94
+ <Link href={`/?doc=${encodeURIComponent(node.id)}`} scroll={false}>
95
+ <FileText className="mr-2 h-4 w-4 text-slate-500" />
96
+ {node.title}
97
+ </Link>
98
+ </Button>
99
+ );
100
+ }
101
+
102
+ function checkActiveChild(node: DocNode, currentDoc: string | null): boolean {
103
+ if (node.id === currentDoc) return true;
104
+ if (node.children) {
105
+ return node.children.some(child => checkActiveChild(child, currentDoc));
106
+ }
107
+ return false;
108
+ }
109
+
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import ReactMarkdown from 'react-markdown';
3
+ import remarkGfm from 'remark-gfm';
4
+ import rehypeHighlight from 'rehype-highlight';
5
+ import 'highlight.js/styles/github-dark.css'; // or any other style
6
+ import { cn } from '@/lib/utils';
7
+ import type { DocContent } from '../utils/doc-loader';
8
+
9
+ interface DocViewerProps {
10
+ doc: DocContent;
11
+ className?: string;
12
+ }
13
+
14
+ export function DocViewer({ doc, className }: DocViewerProps) {
15
+ return (
16
+ <div className={cn("max-w-4xl mx-auto py-8 px-6", className)}>
17
+ <div className="mb-8 border-b pb-4">
18
+ <h1 className="text-4xl font-bold tracking-tight text-foreground">
19
+ {doc.title}
20
+ </h1>
21
+ {doc.frontmatter.description && (
22
+ <p className="mt-2 text-xl text-muted-foreground">
23
+ {doc.frontmatter.description}
24
+ </p>
25
+ )}
26
+ </div>
27
+
28
+ <article className="prose prose-slate dark:prose-invert max-w-none
29
+ prose-headings:scroll-m-20
30
+ prose-h1:text-3xl prose-h1:font-extrabold prose-h1:tracking-tight prose-h1:mb-4
31
+ prose-h2:text-2xl prose-h2:font-semibold prose-h2:tracking-tight prose-h2:mt-10 prose-h2:mb-4
32
+ prose-h3:text-xl prose-h3:font-semibold prose-h3:tracking-tight prose-h3:mt-8 prose-h3:mb-4
33
+ prose-p:leading-7 prose-p:mb-4
34
+ prose-ul:my-6 prose-ul:list-disc prose-ul:pl-6
35
+ prose-code:relative prose-code:rounded prose-code:bg-muted prose-code:px-[0.3rem] prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-semibold
36
+ prose-pre:p-4 prose-pre:rounded-lg prose-pre:bg-muted/50 prose-pre:border
37
+ ">
38
+ <ReactMarkdown
39
+ remarkPlugins={[remarkGfm]}
40
+ rehypePlugins={[rehypeHighlight]}
41
+ components={{
42
+ // Override components if needed for custom styling or functionality
43
+ a: ({node, ...props}) => {
44
+ // Check if link is local markdown file
45
+ const href = props.href || '';
46
+ if (href.startsWith('http')) {
47
+ return <a target="_blank" rel="noopener noreferrer" {...props} className="text-primary hover:underline font-medium" />
48
+ }
49
+ // Convert relative markdown links to ?doc= links
50
+ // This is a bit complex as we need to resolve relative paths against current doc ID
51
+ // For now, let's just leave them as is, or handle basic cases
52
+ return <a {...props} className="text-primary hover:underline font-medium" />
53
+ },
54
+ blockquote: ({node, ...props}) => (
55
+ <blockquote className="mt-6 border-l-2 border-primary pl-6 italic" {...props} />
56
+ ),
57
+ table: ({node, ...props}) => (
58
+ <div className="my-6 w-full overflow-y-auto">
59
+ <table className="w-full" {...props} />
60
+ </div>
61
+ )
62
+ }}
63
+ >
64
+ {doc.content}
65
+ </ReactMarkdown>
66
+ </article>
67
+ </div>
68
+ );
69
+ }
70
+
@@ -0,0 +1,92 @@
1
+ import React from 'react';
2
+ import { getDocsStructure, getDocContent } from './utils/doc-loader';
3
+ import { DocSidebar } from './components/doc-sidebar';
4
+ import { DocViewer } from './components/doc-viewer';
5
+ import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
6
+ import { Button } from '@/components/ui/button';
7
+ import { Menu, Activity } from 'lucide-react';
8
+ import Link from 'next/link';
9
+ import { UserButton } from '@clerk/nextjs';
10
+
11
+ interface DocumentationLayoutProps {
12
+ currentDocId?: string;
13
+ }
14
+
15
+ export async function DocumentationFeature({ currentDocId }: DocumentationLayoutProps) {
16
+ const structure = await getDocsStructure();
17
+
18
+ // Default to README.md if no doc selected, or if selected doc not found
19
+ const targetId = currentDocId || 'README.md';
20
+ let doc = await getDocContent(targetId);
21
+
22
+ // Fallback if not found (e.g. invalid ID)
23
+ if (!doc && targetId !== 'README.md') {
24
+ doc = await getDocContent('README.md');
25
+ }
26
+
27
+ // If still no doc (shouldn't happen), show error
28
+ if (!doc) {
29
+ return <div>Documentation not found.</div>;
30
+ }
31
+
32
+ return (
33
+ <div className="flex min-h-screen flex-col">
34
+ {/* Header */}
35
+ <header className="sticky top-0 z-40 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
36
+ <div className="container mx-auto flex h-14 items-center justify-between">
37
+ <div className="flex items-center">
38
+ <Sheet>
39
+ <SheetTrigger asChild>
40
+ <Button variant="ghost" className="mr-2 px-0 text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 lg:hidden">
41
+ <Menu className="h-6 w-6" />
42
+ <span className="sr-only">Toggle Menu</span>
43
+ </Button>
44
+ </SheetTrigger>
45
+ <SheetContent side="left" className="pr-0">
46
+ <div className="px-7">
47
+ <Link href="/" className="font-bold">
48
+ DTT Framework
49
+ </Link>
50
+ </div>
51
+ <DocSidebar nodes={structure} className="mt-4 border-none" />
52
+ </SheetContent>
53
+ </Sheet>
54
+ <div className="mr-4 hidden md:flex">
55
+ <Link href="/" className="mr-6 flex items-center space-x-2">
56
+ <span className="hidden font-bold sm:inline-block">
57
+ DTT Framework
58
+ </span>
59
+ </Link>
60
+ </div>
61
+ </div>
62
+
63
+ <div className="flex items-center gap-4">
64
+ <Link href="/health">
65
+ <Button variant="outline" size="sm" className="gap-2">
66
+ <Activity className="h-4 w-4" />
67
+ Health Check
68
+ </Button>
69
+ </Link>
70
+ <UserButton afterSignOutUrl="/sign-in" />
71
+ </div>
72
+ </div>
73
+ </header>
74
+
75
+ <div className="flex-1 items-start md:grid md:grid-cols-[240px_minmax(0,1fr)] lg:grid-cols-[280px_minmax(0,1fr)]">
76
+ {/* Desktop Sidebar */}
77
+ <aside className="fixed top-14 z-30 -ml-2 hidden h-[calc(100vh-3.5rem)] w-full shrink-0 md:sticky md:block">
78
+ <div className="h-full py-6 pl-8 pr-6 lg:py-8">
79
+ <DocSidebar nodes={structure} className="h-full border-none bg-transparent" />
80
+ </div>
81
+ </aside>
82
+
83
+ {/* Main Content */}
84
+ <main className="relative py-6 lg:gap-10 lg:py-8 xl:grid xl:grid-cols-[1fr_300px]">
85
+ <div className="mx-auto w-full min-w-0">
86
+ <DocViewer doc={doc} />
87
+ </div>
88
+ </main>
89
+ </div>
90
+ </div>
91
+ );
92
+ }
@@ -0,0 +1,177 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import matter from 'gray-matter';
4
+
5
+ const ROOT_DIR = (() => {
6
+ let currentDir = process.cwd();
7
+ // In Vercel, process.cwd() is usually /var/task
8
+ // But we can try to find package.json to be sure
9
+ try {
10
+ // Check if we are already at root
11
+ if (fs.existsSync(path.join(currentDir, 'package.json'))) {
12
+ return currentDir;
13
+ }
14
+
15
+ // Look up a few levels
16
+ for (let i = 0; i < 3; i++) {
17
+ const parent = path.dirname(currentDir);
18
+ if (parent === currentDir) break;
19
+ if (fs.existsSync(path.join(parent, 'package.json'))) {
20
+ return parent;
21
+ }
22
+ currentDir = parent;
23
+ }
24
+ } catch (e) {
25
+ console.error('Error finding root dir:', e);
26
+ }
27
+ return process.cwd();
28
+ })();
29
+
30
+ const DOCS_DIR = path.join(ROOT_DIR, 'docs');
31
+
32
+ export interface DocNode {
33
+ id: string;
34
+ title: string;
35
+ type: 'file' | 'directory';
36
+ children?: DocNode[];
37
+ path?: string; // real file path relative to root
38
+ }
39
+
40
+ export interface DocContent {
41
+ title: string;
42
+ content: string;
43
+ frontmatter: Record<string, any>;
44
+ }
45
+
46
+ const ALLOWED_ROOT_FILES = ['README.md', 'DESIGN.md'];
47
+ const ALLOWED_CLI_FILES = ['cli/template/README.md'];
48
+
49
+ // Helper to format title from filename
50
+ function formatTitle(filename: string): string {
51
+ return filename
52
+ .replace(/\.mdx?$/, '')
53
+ .replace(/^\d+-/, '') // Remove leading numbers like 01-
54
+ .replace(/-/g, ' ')
55
+ .replace(/\b\w/g, (c) => c.toUpperCase());
56
+ }
57
+
58
+ export async function getDocsStructure(): Promise<DocNode[]> {
59
+ console.log('[Debug] ROOT_DIR:', ROOT_DIR);
60
+ try {
61
+ if (fs.existsSync(ROOT_DIR)) {
62
+ const entries = await fs.promises.readdir(ROOT_DIR);
63
+ console.log('[Debug] Root entries:', entries);
64
+ }
65
+ } catch (error) {
66
+ console.error('[Debug] Error listing root:', error);
67
+ }
68
+
69
+ const nodes: DocNode[] = [];
70
+
71
+ // 1. Root files
72
+ for (const filename of ALLOWED_ROOT_FILES) {
73
+ if (fs.existsSync(path.join(ROOT_DIR, filename))) {
74
+ nodes.push({
75
+ id: filename,
76
+ title: filename === 'README.md' ? 'Introduction' : formatTitle(filename),
77
+ type: 'file',
78
+ path: filename
79
+ });
80
+ }
81
+ }
82
+
83
+ // 2. CLI Docs
84
+ for (const filepath of ALLOWED_CLI_FILES) {
85
+ if (fs.existsSync(path.join(ROOT_DIR, filepath))) {
86
+ nodes.push({
87
+ id: filepath,
88
+ title: 'CLI Template',
89
+ type: 'file',
90
+ path: filepath
91
+ });
92
+ }
93
+ }
94
+
95
+ // 3. Docs folder
96
+ const docsNodes = await scanDirectory(DOCS_DIR, 'docs');
97
+ nodes.push(...docsNodes);
98
+
99
+ return nodes;
100
+ }
101
+
102
+ async function scanDirectory(dir: string, parentId: string): Promise<DocNode[]> {
103
+ if (!fs.existsSync(dir)) return [];
104
+
105
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
106
+ const nodes: DocNode[] = [];
107
+
108
+ for (const entry of entries) {
109
+ const fullPath = path.join(dir, entry.name);
110
+ const relativePath = path.relative(ROOT_DIR, fullPath);
111
+ const id = relativePath.replace(/\\/g, '/'); // Normalize for URLs
112
+
113
+ if (entry.isDirectory()) {
114
+ const children = await scanDirectory(fullPath, id);
115
+ if (children.length > 0) {
116
+ nodes.push({
117
+ id,
118
+ title: formatTitle(entry.name),
119
+ type: 'directory',
120
+ children
121
+ });
122
+ }
123
+ } else if (entry.name.endsWith('.md') || entry.name.endsWith('.mdx')) {
124
+ // Read frontmatter for title if possible, else derive from filename
125
+ const fileContent = await fs.promises.readFile(fullPath, 'utf-8');
126
+ const { data } = matter(fileContent);
127
+
128
+ nodes.push({
129
+ id,
130
+ title: data.title || formatTitle(entry.name),
131
+ type: 'file',
132
+ path: relativePath
133
+ });
134
+ }
135
+ }
136
+
137
+ // Sort: Directories first, then files alphabetically (or by prefix if present)
138
+ return nodes.sort((a, b) => {
139
+ if (a.type === b.type) return a.id.localeCompare(b.id);
140
+ return a.type === 'directory' ? 1 : -1; // Keep directories at bottom usually? Or top?
141
+ // Usually folders like "framework" contain the files.
142
+ // Let's just sort by ID for now, which handles 01-overview vs 02-techstack
143
+ });
144
+ }
145
+
146
+ export async function getDocContent(id: string): Promise<DocContent | null> {
147
+ // Security check: ensure id resolves to a file within allowed paths
148
+ const cleanId = id.replace(/\.\./g, ''); // Prevent directory traversal
149
+ const fullPath = path.join(ROOT_DIR, cleanId);
150
+
151
+ // Validate that the resolved path is within ROOT_DIR and is one of the allowed types/locations
152
+ if (!fullPath.startsWith(ROOT_DIR)) return null;
153
+
154
+ // Basic check: file must exist and be md/mdx
155
+ if (!fs.existsSync(fullPath)) return null;
156
+ if (!fullPath.endsWith('.md') && !fullPath.endsWith('.mdx')) return null;
157
+
158
+ try {
159
+ const fileContent = await fs.promises.readFile(fullPath, 'utf-8');
160
+ const { data, content } = matter(fileContent);
161
+
162
+ let title = data.title;
163
+ if (!title) {
164
+ title = formatTitle(path.basename(fullPath));
165
+ }
166
+
167
+ return {
168
+ title,
169
+ content,
170
+ frontmatter: data,
171
+ };
172
+ } catch (error) {
173
+ console.error(`Error reading doc ${id} at ${fullPath}:`, error);
174
+ return null;
175
+ }
176
+ }
177
+