create-opendocs 0.1.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.
@@ -0,0 +1,275 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import matter from 'gray-matter';
4
+ import { cache } from 'react';
5
+
6
+ const DOCS_DIRECTORY = path.join(process.cwd(), 'content');
7
+
8
+ export type DocItem = {
9
+ slug: string[];
10
+ title: string;
11
+ content: string;
12
+ };
13
+
14
+ export type DocTree = {
15
+ title: string;
16
+ path?: string[];
17
+ slug?: string; // e.g. /docs/foo/bar
18
+ children: DocTree[];
19
+ };
20
+
21
+ type ConfigSidebarCategory = {
22
+ title: string;
23
+ items: string[];
24
+ };
25
+
26
+ type ConfigContactSupport = {
27
+ title?: string;
28
+ description?: string;
29
+ buttonText?: string;
30
+ buttonLink?: string;
31
+ };
32
+
33
+ type DocsConfig = {
34
+ homepage?: string;
35
+ sidebar: ConfigSidebarCategory[];
36
+ contactSupport?: ConfigContactSupport;
37
+ };
38
+
39
+ function normalizeRelativePath(filePath: string): string {
40
+ return path.relative(DOCS_DIRECTORY, filePath).split(path.sep).join('/');
41
+ }
42
+
43
+ function normalizeSlugParams(slugParams: string[] = []): string[] {
44
+ return slugParams.filter(Boolean);
45
+ }
46
+
47
+ const readConfig = cache((): DocsConfig => {
48
+ const configPath = path.join(DOCS_DIRECTORY, 'config.json');
49
+ if (fs.existsSync(configPath)) {
50
+ try {
51
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
52
+ } catch (e) {
53
+ console.error("Failed to parse config.json", e);
54
+ }
55
+ }
56
+ return { sidebar: [] };
57
+ });
58
+
59
+ function getAllMarkdownFiles(dirPath: string): string[] {
60
+ if (!fs.existsSync(dirPath)) return [];
61
+
62
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
63
+ const markdownFiles: string[] = [];
64
+
65
+ for (const entry of entries) {
66
+ const entryPath = path.join(dirPath, entry.name);
67
+ if (entry.isDirectory()) {
68
+ markdownFiles.push(...getAllMarkdownFiles(entryPath));
69
+ continue;
70
+ }
71
+
72
+ if (entry.name.endsWith('.md') || entry.name.endsWith('.mdx')) {
73
+ markdownFiles.push(entryPath);
74
+ }
75
+ }
76
+
77
+ return markdownFiles;
78
+ }
79
+
80
+ const getMarkdownFiles = cache(() => getAllMarkdownFiles(DOCS_DIRECTORY));
81
+
82
+ export function getConfig(): DocsConfig {
83
+ return readConfig();
84
+ }
85
+
86
+ const getAllDocsSlugsCached = cache((): string[][] => {
87
+ const config = readConfig();
88
+ const files = getMarkdownFiles();
89
+ const slugs: string[][] = [];
90
+
91
+ files.forEach(file => {
92
+ const relativePath = normalizeRelativePath(file);
93
+
94
+ if (config.homepage && relativePath === config.homepage) {
95
+ slugs.push([]);
96
+ } else {
97
+ const withoutExt = relativePath.replace(/\.mdx?$/, '');
98
+ if (withoutExt === 'index') {
99
+ slugs.push([]);
100
+ } else {
101
+ const parts = withoutExt.split('/');
102
+ if (parts[parts.length - 1] === 'index') {
103
+ parts.pop();
104
+ }
105
+ slugs.push(parts);
106
+ }
107
+ }
108
+ });
109
+
110
+ // Deduplicate
111
+ return Array.from(new Set(slugs.map(s => JSON.stringify(s)))).map(s => JSON.parse(s));
112
+ });
113
+
114
+ export function getAllDocsSlugs(): string[][] {
115
+ return getAllDocsSlugsCached();
116
+ }
117
+
118
+ const getDocBySlugCached = cache((slugKey: string): DocItem | null => {
119
+ const slugParams = slugKey ? slugKey.split('/') : [];
120
+ const config = readConfig();
121
+
122
+ const possiblePaths = [
123
+ path.join(DOCS_DIRECTORY, ...slugParams) + '.md',
124
+ path.join(DOCS_DIRECTORY, ...slugParams) + '.mdx',
125
+ path.join(DOCS_DIRECTORY, ...slugParams, 'index.md'),
126
+ path.join(DOCS_DIRECTORY, ...slugParams, 'index.mdx'),
127
+ ];
128
+
129
+ if (slugParams.length === 0 || (slugParams.length === 1 && slugParams[0] === '')) {
130
+ if (config.homepage) {
131
+ possiblePaths.unshift(path.join(DOCS_DIRECTORY, config.homepage));
132
+ } else {
133
+ possiblePaths.unshift(path.join(DOCS_DIRECTORY, 'index.mdx'));
134
+ possiblePaths.unshift(path.join(DOCS_DIRECTORY, 'index.md'));
135
+ }
136
+ }
137
+
138
+ let validPath = null;
139
+ for (const p of possiblePaths) {
140
+ if (fs.existsSync(p)) {
141
+ validPath = p;
142
+ break;
143
+ }
144
+ }
145
+
146
+ if (!validPath) {
147
+ return null;
148
+ }
149
+
150
+ const fileContents = fs.readFileSync(validPath, 'utf8');
151
+ const { data, content } = matter(fileContents);
152
+
153
+ let docTitle = data.title;
154
+ if (!docTitle) {
155
+ const match = content.match(/^#\s+(.*)$/m);
156
+ docTitle = match ? match[1].trim() : (slugParams[slugParams.length - 1] || 'Documentation');
157
+ }
158
+
159
+ return {
160
+ slug: slugParams,
161
+ title: docTitle,
162
+ content,
163
+ };
164
+ });
165
+
166
+ export function getDocBySlug(slugParams: string[] = []): DocItem | null {
167
+ return getDocBySlugCached(normalizeSlugParams(slugParams).join('/'));
168
+ }
169
+
170
+ const buildSidebarTreeCached = cache((): DocTree => {
171
+ const config = readConfig();
172
+ const root: DocTree = { title: 'Root', children: [] };
173
+ const usedFiles = new Set<string>();
174
+
175
+ for (const category of config.sidebar || []) {
176
+ const categoryNode: DocTree = {
177
+ title: category.title,
178
+ children: []
179
+ };
180
+
181
+ for (const itemPath of category.items || []) {
182
+ // itemPath is now string like "features/analytics.md" or "index.md"
183
+ const relativePathWoExt = itemPath.replace(/\.mdx?$/, '');
184
+ let slugParams = relativePathWoExt === 'index' ? [] : relativePathWoExt.split('/');
185
+
186
+ if (config.homepage && itemPath === config.homepage) {
187
+ slugParams = [];
188
+ } else if (slugParams[slugParams.length - 1] === 'index') {
189
+ slugParams.pop();
190
+ }
191
+
192
+ const slugStr = slugParams.length === 0 ? '/' : '/' + slugParams.join('/');
193
+ const doc = getDocBySlug(slugParams);
194
+ const title = doc?.title || relativePathWoExt;
195
+
196
+ categoryNode.children.push({
197
+ title: title,
198
+ slug: slugStr,
199
+ children: []
200
+ });
201
+ usedFiles.add(itemPath);
202
+ usedFiles.add(relativePathWoExt);
203
+ }
204
+
205
+ if (categoryNode.children.length > 0) {
206
+ root.children.push(categoryNode);
207
+ }
208
+ }
209
+
210
+ const allFiles = getMarkdownFiles();
211
+ allFiles.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
212
+
213
+ for (const file of allFiles) {
214
+ const relativePath = normalizeRelativePath(file);
215
+ const withoutExt = relativePath.replace(/\.mdx?$/, '');
216
+
217
+ // Make sure we haven't already mapped this file AND it's not the homepage
218
+ if (!usedFiles.has(relativePath) && !usedFiles.has(withoutExt) && relativePath !== config.homepage) {
219
+ const slugParams = withoutExt === 'index' ? [] : withoutExt.split('/');
220
+ if (slugParams[slugParams.length - 1] === 'index') {
221
+ slugParams.pop();
222
+ }
223
+ const slugStr = slugParams.length === 0 ? '/' : '/' + slugParams.join('/');
224
+ const doc = getDocBySlug(slugParams);
225
+ const title = doc?.title || withoutExt;
226
+
227
+ root.children.push({
228
+ title: title,
229
+ slug: slugStr,
230
+ children: []
231
+ });
232
+
233
+ usedFiles.add(relativePath);
234
+ usedFiles.add(withoutExt);
235
+ }
236
+ }
237
+
238
+ return root;
239
+ });
240
+
241
+ export function buildSidebarTree(): DocTree {
242
+ return buildSidebarTreeCached();
243
+ }
244
+
245
+ export type FlatDocLink = {
246
+ title: string;
247
+ slug: string;
248
+ categoryTitle: string;
249
+ };
250
+
251
+ const getFlatDocLinksCached = cache((): FlatDocLink[] => {
252
+ const tree = buildSidebarTreeCached();
253
+ const links: FlatDocLink[] = [];
254
+
255
+ for (const node of tree.children) {
256
+ if (node.children && node.children.length > 0) {
257
+ for (const child of node.children) {
258
+ if (child.slug) {
259
+ links.push({ title: child.title, slug: child.slug, categoryTitle: node.title });
260
+ }
261
+ }
262
+ continue;
263
+ }
264
+
265
+ if (node.slug) {
266
+ links.push({ title: node.title, slug: node.slug, categoryTitle: '' });
267
+ }
268
+ }
269
+
270
+ return links;
271
+ });
272
+
273
+ export function getFlatDocLinks(): FlatDocLink[] {
274
+ return getFlatDocLinksCached();
275
+ }
@@ -0,0 +1,34 @@
1
+ import type { Config } from "tailwindcss";
2
+
3
+ const config: Config = {
4
+ content: [
5
+ "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6
+ "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7
+ "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
8
+ ],
9
+ theme: {
10
+ extend: {
11
+ colors: {
12
+ background: "var(--background)",
13
+ foreground: "var(--foreground)",
14
+ border: "var(--border)",
15
+ brand: {
16
+ 50: '#f0f9ff',
17
+ 100: '#e0f2fe',
18
+ 200: '#bae6fd',
19
+ 300: '#7dd3fc',
20
+ 400: '#38bdf8',
21
+ 500: '#0ea5e9',
22
+ 600: '#0284c7',
23
+ 700: '#0369a1',
24
+ 800: '#075985',
25
+ 900: '#0c4a6e',
26
+ }
27
+ },
28
+ },
29
+ },
30
+ plugins: [
31
+ require('@tailwindcss/typography'),
32
+ ],
33
+ };
34
+ export default config;
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": ["dom", "dom.iterable", "esnext"],
4
+ "allowJs": true,
5
+ "skipLibCheck": true,
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "esModuleInterop": true,
9
+ "module": "esnext",
10
+ "moduleResolution": "bundler",
11
+ "resolveJsonModule": true,
12
+ "isolatedModules": true,
13
+ "jsx": "preserve",
14
+ "incremental": true,
15
+ "plugins": [
16
+ {
17
+ "name": "next"
18
+ }
19
+ ],
20
+ "paths": {
21
+ "@/*": ["./src/*"]
22
+ }
23
+ },
24
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25
+ "exclude": ["node_modules"]
26
+ }