doccupine 0.0.1-1
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/README.md +65 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +532 -0
- package/dist/templates/click-outside.d.ts +1 -0
- package/dist/templates/click-outside.js +28 -0
- package/dist/templates/code.d.ts +1 -0
- package/dist/templates/code.js +192 -0
- package/dist/templates/components/ClickOutside.d.ts +1 -0
- package/dist/templates/components/ClickOutside.js +27 -0
- package/dist/templates/components/Docs.d.ts +1 -0
- package/dist/templates/components/Docs.js +52 -0
- package/dist/templates/components/SideBar.d.ts +1 -0
- package/dist/templates/components/SideBar.js +80 -0
- package/dist/templates/components/layout/Code.d.ts +1 -0
- package/dist/templates/components/layout/Code.js +192 -0
- package/dist/templates/components/layout/DocsComponents.d.ts +1 -0
- package/dist/templates/components/layout/DocsComponents.js +444 -0
- package/dist/templates/components/layout/Footer.d.ts +1 -0
- package/dist/templates/components/layout/Footer.js +8 -0
- package/dist/templates/components/layout/Header.d.ts +1 -0
- package/dist/templates/components/layout/Header.js +280 -0
- package/dist/templates/components/layout/Icon.d.ts +1 -0
- package/dist/templates/components/layout/Icon.js +19 -0
- package/dist/templates/components/layout/Pictograms.d.ts +1 -0
- package/dist/templates/components/layout/Pictograms.js +66 -0
- package/dist/templates/components/layout/SharedStyles.d.ts +1 -0
- package/dist/templates/components/layout/SharedStyles.js +791 -0
- package/dist/templates/components/layout/ThemeToggle.d.ts +1 -0
- package/dist/templates/components/layout/ThemeToggle.js +123 -0
- package/dist/templates/components/layout/Typography.d.ts +1 -0
- package/dist/templates/components/layout/Typography.js +51 -0
- package/dist/templates/docs-components.d.ts +1 -0
- package/dist/templates/docs-components.js +441 -0
- package/dist/templates/docs.d.ts +1 -0
- package/dist/templates/docs.js +48 -0
- package/dist/templates/footer.d.ts +1 -0
- package/dist/templates/footer.js +9 -0
- package/dist/templates/gitignore.d.ts +1 -0
- package/dist/templates/gitignore.js +41 -0
- package/dist/templates/header.d.ts +1 -0
- package/dist/templates/header.js +275 -0
- package/dist/templates/home.d.ts +1 -0
- package/dist/templates/home.js +79 -0
- package/dist/templates/icon.d.ts +1 -0
- package/dist/templates/icon.js +20 -0
- package/dist/templates/layout.d.ts +1 -0
- package/dist/templates/layout.js +66 -0
- package/dist/templates/not-found.d.ts +1 -0
- package/dist/templates/not-found.js +22 -0
- package/dist/templates/pictograms.d.ts +1 -0
- package/dist/templates/pictograms.js +67 -0
- package/dist/templates/shared-styles.d.ts +1 -0
- package/dist/templates/shared-styles.js +792 -0
- package/dist/templates/theme-toggle.d.ts +1 -0
- package/dist/templates/theme-toggle.js +111 -0
- package/dist/templates/theme.d.ts +1 -0
- package/dist/templates/theme.js +291 -0
- package/dist/templates/typography.d.ts +1 -0
- package/dist/templates/typography.js +52 -0
- package/dist/templates/utils/orderNavItems.d.ts +1 -0
- package/dist/templates/utils/orderNavItems.js +45 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Welcome to Doccupine
|
|
2
|
+
|
|
3
|
+
Doccupine is a free and open-source document management system that allows you to store, organize, and share your documentation with ease. Using Doccupine, you simply create your documentation in MDX files with traditional Markdown syntax, Doccupine monitors your changes automatically generating a beautiful, modern documentation website.
|
|
4
|
+
|
|
5
|
+
## Open Source and Extensible
|
|
6
|
+
|
|
7
|
+
Doccupine is built on open standards, enabling customization and extensibility for different documentation needs. You stay in control of your content, with the option to host docs yourself and tailor the websiteโs look and features to match your organizationโs requirements.
|
|
8
|
+
|
|
9
|
+
## Getting Started
|
|
10
|
+
|
|
11
|
+
To get started with Doccupine, make sure you have [Node.js](https://nodejs.org) and npm installed on your machine. Then, follow these steps:
|
|
12
|
+
|
|
13
|
+
- **Install Doccupine CLI:**
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g doccupine-beta
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- **Create a new Doccupine project:**
|
|
20
|
+
Create a new directory for your project and navigate to it in your terminal. Run the following command to create a new Doccupine project:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
doccupine-beta
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Once you run the command, Doccupine will ask you to select a directory to store your MDX files. Choose the directory where you want to create your documentation files.
|
|
27
|
+
After selecting the directory, Doccupine will ask you to enter the name of the directory for the generated website. Enter the name of the directory where you want to create your website.
|
|
28
|
+
|
|
29
|
+
This will start the development server on port 3000. Open your browser and navigate to http://localhost:3000 to view your documentation.
|
|
30
|
+
|
|
31
|
+
- **Generate the website:**
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
doccupine build
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This will generate the build files for your documentation website without starting the development server. You can then deploy the generated files to a hosting service of your choice.
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- ๐ Markdown-based content
|
|
42
|
+
- ๐ฆ Built-in file structure
|
|
43
|
+
- โก Live Preview & Auto-Update
|
|
44
|
+
- ๐ Easy Deployment
|
|
45
|
+
|
|
46
|
+
## Start documenting
|
|
47
|
+
|
|
48
|
+
Start documenting your project by creating a new **index.mdx** file in the choosen MDX directory. You can use the following template as a starting point:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
---
|
|
52
|
+
title: "Home"
|
|
53
|
+
description: "This is my first Doccupine project"
|
|
54
|
+
date: "2025-01-15"
|
|
55
|
+
category: "General"
|
|
56
|
+
categoryOrder: 0
|
|
57
|
+
order: 0
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
# Home
|
|
61
|
+
|
|
62
|
+
This is some **markdown** content with MDX support.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
In your MDX directory, you can structure your content using folders and files. Doccupine will automatically generate a navigation menu based on the configured categories and order.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from "commander";
|
|
3
|
+
import chokidar from "chokidar";
|
|
4
|
+
import fs from "fs-extra";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import matter from "gray-matter";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import prompts from "prompts";
|
|
9
|
+
import { gitignoreTemplate } from "./templates/gitignore.js";
|
|
10
|
+
import { homeTemplate } from "./templates/home.js";
|
|
11
|
+
import { notFoundTemplate } from "./templates/not-found.js";
|
|
12
|
+
import { layoutTemplate } from "./templates/layout.js";
|
|
13
|
+
import { themeTemplate } from "./templates/theme.js";
|
|
14
|
+
import { iconTemplate } from "./templates/components/layout/Icon.js";
|
|
15
|
+
import { pictogramsTemplate } from "./templates/components/layout/Pictograms.js";
|
|
16
|
+
import { typographyTemplate } from "./templates/components/layout/Typography.js";
|
|
17
|
+
import { headerTemplate } from "./templates/components/layout/Header.js";
|
|
18
|
+
import { footerTemplate } from "./templates/components/layout/Footer.js";
|
|
19
|
+
import { themeToggleTemplate } from "./templates/components/layout/ThemeToggle.js";
|
|
20
|
+
import { sharedStyledTemplate } from "./templates/components/layout/SharedStyles.js";
|
|
21
|
+
import { codeTemplate } from "./templates/components/layout/Code.js";
|
|
22
|
+
import { docsComponentsTemplate } from "./templates/components/layout/DocsComponents.js";
|
|
23
|
+
import { clickOutsideTemplate } from "./templates/components/ClickOutside.js";
|
|
24
|
+
import { docsTemplate } from "./templates/components/Docs.js";
|
|
25
|
+
import { orderNavItemsTemplate } from "./templates/utils/orderNavItems.js";
|
|
26
|
+
import { sideBarTemplate } from "./templates/components/SideBar.js";
|
|
27
|
+
class MDXToNextJSGenerator {
|
|
28
|
+
watchDir;
|
|
29
|
+
outputDir;
|
|
30
|
+
watcher = null;
|
|
31
|
+
constructor(watchDir, outputDir) {
|
|
32
|
+
this.watchDir = path.resolve(watchDir);
|
|
33
|
+
this.outputDir = path.resolve(outputDir);
|
|
34
|
+
}
|
|
35
|
+
async init() {
|
|
36
|
+
console.log(chalk.blue("๐ Initializing MDX to Next.js generator..."));
|
|
37
|
+
// Ensure directories exist
|
|
38
|
+
await fs.ensureDir(this.watchDir);
|
|
39
|
+
await fs.ensureDir(this.outputDir);
|
|
40
|
+
// Create initial Next.js structure
|
|
41
|
+
await this.createNextJSStructure();
|
|
42
|
+
// Process existing files
|
|
43
|
+
await this.processAllMDXFiles();
|
|
44
|
+
console.log(chalk.green("โ
Initial setup complete!"));
|
|
45
|
+
console.log(chalk.cyan("๐ก To start the Next.js dev server:"));
|
|
46
|
+
console.log(chalk.white(` cd ${path.relative(process.cwd(), this.outputDir)}`));
|
|
47
|
+
console.log(chalk.white(" npm install && npm run dev"));
|
|
48
|
+
}
|
|
49
|
+
async createNextJSStructure() {
|
|
50
|
+
const structure = {
|
|
51
|
+
".gitignore": this.generateGitIgnore(),
|
|
52
|
+
"package.json": this.generatePackageJson(),
|
|
53
|
+
"next.config.ts": this.generateNextConfig(),
|
|
54
|
+
"tsconfig.json": this.generateTSConfig(),
|
|
55
|
+
"app/layout.tsx": await this.generateRootLayout(),
|
|
56
|
+
"app/page.tsx": this.generateHomePage(),
|
|
57
|
+
"app/not-found.tsx": this.generateNotFoundPage(),
|
|
58
|
+
"app/theme.ts": this.generateTheme(),
|
|
59
|
+
"components/layout/Icon.tsx": this.generateIcon(),
|
|
60
|
+
"components/layout/Pictograms.tsx": this.generatePictograms(),
|
|
61
|
+
"components/layout/Typography.ts": this.generateTypography(),
|
|
62
|
+
"components/layout/Header.tsx": this.generateHeader(),
|
|
63
|
+
"components/layout/Footer.tsx": this.generateFooter(),
|
|
64
|
+
"components/layout/ThemeToggle.tsx": this.generateThemeToggle(),
|
|
65
|
+
"components/layout/SharedStyled.ts": this.generateSharedStyled(),
|
|
66
|
+
"components/layout/Code.tsx": this.generateCode(),
|
|
67
|
+
"components/layout/DocsComponents.tsx": this.generateDocsComponents(),
|
|
68
|
+
"components/ClickOutside.ts": this.generateClickOutside(),
|
|
69
|
+
"components/Docs.tsx": this.generateDocs(),
|
|
70
|
+
"components/SideBar.tsx": this.generateSideBar(),
|
|
71
|
+
"utils/orderNavItems.ts": this.generateOrderNavItems(),
|
|
72
|
+
};
|
|
73
|
+
for (const [filePath, content] of Object.entries(structure)) {
|
|
74
|
+
const fullPath = path.join(this.outputDir, filePath);
|
|
75
|
+
await fs.ensureDir(path.dirname(fullPath));
|
|
76
|
+
await fs.writeFile(fullPath, String(content), "utf8");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async startWatching() {
|
|
80
|
+
console.log(chalk.yellow(`๐ Watching for changes in: ${this.watchDir}`));
|
|
81
|
+
this.watcher = chokidar.watch("**/*.mdx", {
|
|
82
|
+
cwd: this.watchDir,
|
|
83
|
+
persistent: true,
|
|
84
|
+
ignoreInitial: false,
|
|
85
|
+
});
|
|
86
|
+
this.watcher
|
|
87
|
+
.on("add", (filePath) => this.handleFileChange("added", filePath))
|
|
88
|
+
.on("change", (filePath) => this.handleFileChange("changed", filePath))
|
|
89
|
+
.on("unlink", (filePath) => this.handleFileDelete(filePath));
|
|
90
|
+
}
|
|
91
|
+
async handleFileChange(action, filePath) {
|
|
92
|
+
console.log(chalk.cyan(`๐ File ${action}: ${filePath}`));
|
|
93
|
+
const fullPath = path.join(this.watchDir, filePath);
|
|
94
|
+
try {
|
|
95
|
+
const content = await fs.readFile(fullPath, "utf8");
|
|
96
|
+
const { data: frontmatter, content: mdxContent } = matter(content);
|
|
97
|
+
if (filePath === "index.mdx" || filePath === "./index.mdx") {
|
|
98
|
+
console.log(chalk.blue("๐ Updating homepage with index.mdx content"));
|
|
99
|
+
await this.updatePagesIndex();
|
|
100
|
+
await this.updateRootLayout();
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const mdxFile = {
|
|
104
|
+
path: filePath,
|
|
105
|
+
content: mdxContent,
|
|
106
|
+
frontmatter,
|
|
107
|
+
slug: this.generateSlug(filePath),
|
|
108
|
+
};
|
|
109
|
+
await this.generatePageFromMDX(mdxFile);
|
|
110
|
+
await this.updatePagesIndex();
|
|
111
|
+
await this.updateRootLayout();
|
|
112
|
+
}
|
|
113
|
+
console.log(chalk.green(`โ
Generated page for: ${filePath}`));
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
console.error(chalk.red(`โ Error processing ${filePath}:`), error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async handleFileDelete(filePath) {
|
|
120
|
+
console.log(chalk.red(`๐๏ธ File deleted: ${filePath}`));
|
|
121
|
+
try {
|
|
122
|
+
if (filePath === "index.mdx" || filePath === "./index.mdx") {
|
|
123
|
+
console.log(chalk.blue("๐ Updating homepage - index.mdx deleted"));
|
|
124
|
+
await this.updatePagesIndex();
|
|
125
|
+
await this.updateRootLayout();
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const slug = this.generateSlug(filePath);
|
|
129
|
+
const pagePath = path.join(this.outputDir, "app", slug);
|
|
130
|
+
await fs.remove(pagePath);
|
|
131
|
+
await this.updatePagesIndex();
|
|
132
|
+
await this.updateRootLayout();
|
|
133
|
+
}
|
|
134
|
+
console.log(chalk.green(`โ
Removed page for: ${filePath}`));
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
console.error(chalk.red(`โ Error removing page for ${filePath}:`), error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async processAllMDXFiles() {
|
|
141
|
+
const files = await this.getAllMDXFiles();
|
|
142
|
+
for (const file of files) {
|
|
143
|
+
await this.handleFileChange("processed", file);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async getAllMDXFiles() {
|
|
147
|
+
const files = [];
|
|
148
|
+
async function scanDir(dir, relativePath = "") {
|
|
149
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
const fullPath = path.join(dir, entry.name);
|
|
152
|
+
const relPath = path.join(relativePath, entry.name);
|
|
153
|
+
if (entry.isDirectory()) {
|
|
154
|
+
await scanDir(fullPath, relPath);
|
|
155
|
+
}
|
|
156
|
+
else if (entry.name.endsWith(".mdx")) {
|
|
157
|
+
files.push(relPath);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
await scanDir(this.watchDir);
|
|
162
|
+
return files;
|
|
163
|
+
}
|
|
164
|
+
generateSlug(filePath) {
|
|
165
|
+
if (filePath === "index.mdx" || filePath === "./index.mdx") {
|
|
166
|
+
return "";
|
|
167
|
+
}
|
|
168
|
+
return filePath
|
|
169
|
+
.replace(/\.mdx$/, "")
|
|
170
|
+
.replace(/\\/g, "/")
|
|
171
|
+
.replace(/[^a-zA-Z0-9\/\-_]/g, "-")
|
|
172
|
+
.toLowerCase();
|
|
173
|
+
}
|
|
174
|
+
async generatePageFromMDX(mdxFile) {
|
|
175
|
+
const files = await this.getAllMDXFiles();
|
|
176
|
+
const pages = [];
|
|
177
|
+
for (const file of files) {
|
|
178
|
+
const fullPath = path.join(this.watchDir, file);
|
|
179
|
+
const content = await fs.readFile(fullPath, "utf8");
|
|
180
|
+
const { data: frontmatter } = matter(content);
|
|
181
|
+
pages.push({
|
|
182
|
+
slug: this.generateSlug(file),
|
|
183
|
+
title: frontmatter.title || "Untitled",
|
|
184
|
+
description: frontmatter.description || "",
|
|
185
|
+
date: frontmatter.date || null,
|
|
186
|
+
category: frontmatter.category || "",
|
|
187
|
+
path: file,
|
|
188
|
+
categoryOrder: frontmatter.categoryOrder || 0,
|
|
189
|
+
order: frontmatter.order || 0,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
const pageContent = `import { Metadata } from "next";
|
|
193
|
+
import { Docs } from "@/components/Docs";
|
|
194
|
+
|
|
195
|
+
const content = \`${mdxFile.content.replace(/`/g, "\\`")}\`;
|
|
196
|
+
|
|
197
|
+
export const metadata: Metadata = {
|
|
198
|
+
title: '${mdxFile.frontmatter.title || "Generated with Doccupine"}',
|
|
199
|
+
description: '${mdxFile.frontmatter.description || "Automatically generated from MDX files using Doccupine"}',
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export default function Page() {
|
|
203
|
+
return (
|
|
204
|
+
<Docs content={content} />
|
|
205
|
+
);
|
|
206
|
+
}`;
|
|
207
|
+
const pagePath = path.join(this.outputDir, "app", mdxFile.slug, "page.tsx");
|
|
208
|
+
await fs.ensureDir(path.dirname(pagePath));
|
|
209
|
+
await fs.writeFile(pagePath, pageContent, "utf8");
|
|
210
|
+
}
|
|
211
|
+
async updatePagesIndex() {
|
|
212
|
+
const files = await this.getAllMDXFiles();
|
|
213
|
+
let indexMDX = null;
|
|
214
|
+
for (const file of files) {
|
|
215
|
+
const fullPath = path.join(this.watchDir, file);
|
|
216
|
+
const content = await fs.readFile(fullPath, "utf8");
|
|
217
|
+
const { data: frontmatter, content: mdxContent } = matter(content);
|
|
218
|
+
if (file === "index.mdx" || file === "./index.mdx") {
|
|
219
|
+
indexMDX = {
|
|
220
|
+
content: mdxContent,
|
|
221
|
+
frontmatter,
|
|
222
|
+
title: frontmatter.title || "Welcome",
|
|
223
|
+
category: frontmatter.category || "",
|
|
224
|
+
description: frontmatter.description || "",
|
|
225
|
+
categoryOrder: frontmatter.categoryOrder || 0,
|
|
226
|
+
order: frontmatter.order || 0,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const indexContent = `import { Metadata } from "next";
|
|
231
|
+
import { Docs } from "@/components/Docs";
|
|
232
|
+
|
|
233
|
+
${indexMDX ? `const indexContent = \`${indexMDX.content.replace(/`/g, "\\`")}\`;` : `const indexContent = null;`}
|
|
234
|
+
|
|
235
|
+
${indexMDX
|
|
236
|
+
? `export const metadata: Metadata = {
|
|
237
|
+
title: '${indexMDX.title}',
|
|
238
|
+
description: '${indexMDX.description}',
|
|
239
|
+
};`
|
|
240
|
+
: `export const metadata: Metadata = {
|
|
241
|
+
title: 'Generated with Doccupine',
|
|
242
|
+
description: 'Automatically generated from MDX files using Doccupine',
|
|
243
|
+
};`}
|
|
244
|
+
|
|
245
|
+
export default function Home() {
|
|
246
|
+
return (
|
|
247
|
+
<Docs content={indexContent} />
|
|
248
|
+
);
|
|
249
|
+
}`;
|
|
250
|
+
await fs.writeFile(path.join(this.outputDir, "app", "page.tsx"), indexContent, "utf8");
|
|
251
|
+
}
|
|
252
|
+
generateGitIgnore() {
|
|
253
|
+
return gitignoreTemplate;
|
|
254
|
+
}
|
|
255
|
+
// Configuration file generators
|
|
256
|
+
generatePackageJson() {
|
|
257
|
+
return JSON.stringify({
|
|
258
|
+
name: "doccupine",
|
|
259
|
+
version: "0.1.0",
|
|
260
|
+
private: true,
|
|
261
|
+
scripts: {
|
|
262
|
+
dev: "next dev",
|
|
263
|
+
build: "next build",
|
|
264
|
+
start: "next start",
|
|
265
|
+
lint: "next lint",
|
|
266
|
+
},
|
|
267
|
+
dependencies: {
|
|
268
|
+
next: "15.5.2",
|
|
269
|
+
react: "19.1.1",
|
|
270
|
+
"react-dom": "19.1.1",
|
|
271
|
+
},
|
|
272
|
+
devDependencies: {
|
|
273
|
+
"@types/node": "^24",
|
|
274
|
+
"@types/react": "^19",
|
|
275
|
+
"@types/react-dom": "^19",
|
|
276
|
+
"cherry-styled-components": "^0.1.0-43",
|
|
277
|
+
eslint: "^9",
|
|
278
|
+
"eslint-config-next": "15.5.2",
|
|
279
|
+
"lucide-react": "^0.542.0",
|
|
280
|
+
polished: "^4.3.1",
|
|
281
|
+
prettier: "^3.6.2",
|
|
282
|
+
"react-markdown": "^10.1.0",
|
|
283
|
+
"rehype-highlight": "^7.0.2",
|
|
284
|
+
"rehype-parse": "^9.0.1",
|
|
285
|
+
"rehype-stringify": "^10.0.1",
|
|
286
|
+
"remark-gfm": "^4.0.1",
|
|
287
|
+
"styled-components": "^6.1.19",
|
|
288
|
+
typescript: "^5",
|
|
289
|
+
},
|
|
290
|
+
}, null, 2);
|
|
291
|
+
}
|
|
292
|
+
generateNextConfig() {
|
|
293
|
+
return `import type { NextConfig } from "next";
|
|
294
|
+
|
|
295
|
+
const nextConfig: NextConfig = {
|
|
296
|
+
compiler: {
|
|
297
|
+
styledComponents: true,
|
|
298
|
+
},
|
|
299
|
+
transpilePackages: ["lucide-react"],
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
export default nextConfig;
|
|
303
|
+
`;
|
|
304
|
+
}
|
|
305
|
+
generateTSConfig() {
|
|
306
|
+
return JSON.stringify({
|
|
307
|
+
compilerOptions: {
|
|
308
|
+
target: "es5",
|
|
309
|
+
lib: ["dom", "dom.iterable", "es6"],
|
|
310
|
+
allowJs: true,
|
|
311
|
+
skipLibCheck: true,
|
|
312
|
+
strict: true,
|
|
313
|
+
noEmit: true,
|
|
314
|
+
esModuleInterop: true,
|
|
315
|
+
module: "esnext",
|
|
316
|
+
moduleResolution: "bundler",
|
|
317
|
+
resolveJsonModule: true,
|
|
318
|
+
isolatedModules: true,
|
|
319
|
+
jsx: "preserve",
|
|
320
|
+
incremental: true,
|
|
321
|
+
plugins: [{ name: "next" }],
|
|
322
|
+
baseUrl: ".",
|
|
323
|
+
paths: {
|
|
324
|
+
"@/*": ["./*"],
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
include: [
|
|
328
|
+
"next-env.d.ts",
|
|
329
|
+
"**/*.ts",
|
|
330
|
+
"**/*.tsx",
|
|
331
|
+
".next/types/**/*.ts",
|
|
332
|
+
],
|
|
333
|
+
exclude: ["node_modules"],
|
|
334
|
+
}, null, 2);
|
|
335
|
+
}
|
|
336
|
+
async generateRootLayout() {
|
|
337
|
+
const files = await this.getAllMDXFiles();
|
|
338
|
+
const pages = [];
|
|
339
|
+
for (const file of files) {
|
|
340
|
+
const fullPath = path.join(this.watchDir, file);
|
|
341
|
+
const content = await fs.readFile(fullPath, "utf8");
|
|
342
|
+
const { data: frontmatter } = matter(content);
|
|
343
|
+
pages.push({
|
|
344
|
+
slug: this.generateSlug(file),
|
|
345
|
+
title: frontmatter.title || "Untitled",
|
|
346
|
+
description: frontmatter.description || "",
|
|
347
|
+
date: frontmatter.date || null,
|
|
348
|
+
category: frontmatter.category || "",
|
|
349
|
+
path: file,
|
|
350
|
+
categoryOrder: frontmatter.categoryOrder || 0,
|
|
351
|
+
order: frontmatter.order || 0,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return layoutTemplate(pages);
|
|
355
|
+
}
|
|
356
|
+
async updateRootLayout() {
|
|
357
|
+
const layoutContent = await this.generateRootLayout();
|
|
358
|
+
await fs.writeFile(path.join(this.outputDir, "app", "layout.tsx"), layoutContent, "utf8");
|
|
359
|
+
}
|
|
360
|
+
generateHomePage() {
|
|
361
|
+
return homeTemplate;
|
|
362
|
+
}
|
|
363
|
+
generateNotFoundPage() {
|
|
364
|
+
return notFoundTemplate;
|
|
365
|
+
}
|
|
366
|
+
generateTheme() {
|
|
367
|
+
return themeTemplate;
|
|
368
|
+
}
|
|
369
|
+
generateIcon() {
|
|
370
|
+
return iconTemplate;
|
|
371
|
+
}
|
|
372
|
+
generatePictograms() {
|
|
373
|
+
return pictogramsTemplate;
|
|
374
|
+
}
|
|
375
|
+
generateClickOutside() {
|
|
376
|
+
return clickOutsideTemplate;
|
|
377
|
+
}
|
|
378
|
+
generateTypography() {
|
|
379
|
+
return typographyTemplate;
|
|
380
|
+
}
|
|
381
|
+
generateHeader() {
|
|
382
|
+
return headerTemplate;
|
|
383
|
+
}
|
|
384
|
+
generateFooter() {
|
|
385
|
+
return footerTemplate;
|
|
386
|
+
}
|
|
387
|
+
generateThemeToggle() {
|
|
388
|
+
return themeToggleTemplate;
|
|
389
|
+
}
|
|
390
|
+
generateSharedStyled() {
|
|
391
|
+
return sharedStyledTemplate;
|
|
392
|
+
}
|
|
393
|
+
generateCode() {
|
|
394
|
+
return codeTemplate;
|
|
395
|
+
}
|
|
396
|
+
generateDocsComponents() {
|
|
397
|
+
return docsComponentsTemplate;
|
|
398
|
+
}
|
|
399
|
+
generateDocs() {
|
|
400
|
+
return docsTemplate;
|
|
401
|
+
}
|
|
402
|
+
generateSideBar() {
|
|
403
|
+
return sideBarTemplate;
|
|
404
|
+
}
|
|
405
|
+
generateOrderNavItems() {
|
|
406
|
+
return orderNavItemsTemplate;
|
|
407
|
+
}
|
|
408
|
+
stop() {
|
|
409
|
+
if (this.watcher) {
|
|
410
|
+
this.watcher.close();
|
|
411
|
+
console.log(chalk.yellow("๐ Stopped watching for changes"));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// CLI Commands
|
|
416
|
+
program
|
|
417
|
+
.name("doccupine")
|
|
418
|
+
.description("Watch MDX files and generate Next.js documentation pages automatically")
|
|
419
|
+
.version("0.0.1");
|
|
420
|
+
program
|
|
421
|
+
.command("watch", { isDefault: true })
|
|
422
|
+
.description("Watch a directory for MDX changes and generate Next.js app")
|
|
423
|
+
.option("--port <port>", "Port for Next.js dev server", "3000")
|
|
424
|
+
.option("--verbose", "Show verbose output")
|
|
425
|
+
.action(async (options) => {
|
|
426
|
+
const questions = [
|
|
427
|
+
{
|
|
428
|
+
type: "text",
|
|
429
|
+
name: "watchDir",
|
|
430
|
+
message: "Enter directory to watch for MDX files:",
|
|
431
|
+
initial: "docs",
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
type: "text",
|
|
435
|
+
name: "outputDir",
|
|
436
|
+
message: "Enter output directory for Next.js app:",
|
|
437
|
+
initial: "nextjs-app",
|
|
438
|
+
},
|
|
439
|
+
];
|
|
440
|
+
const { watchDir: watchDirInput, outputDir: outputDirInput } = (await prompts(questions));
|
|
441
|
+
const watchDir = path.resolve(process.cwd(), watchDirInput);
|
|
442
|
+
const outputDir = path.resolve(process.cwd(), outputDirInput);
|
|
443
|
+
const generator = new MDXToNextJSGenerator(watchDir, outputDir);
|
|
444
|
+
await generator.init();
|
|
445
|
+
let devServer = null;
|
|
446
|
+
console.log(chalk.blue("๐ฆ Installing dependencies..."));
|
|
447
|
+
const { spawn } = await import("child_process");
|
|
448
|
+
// Install dependencies first
|
|
449
|
+
const install = spawn("npm", ["install"], {
|
|
450
|
+
cwd: outputDir,
|
|
451
|
+
stdio: "pipe",
|
|
452
|
+
});
|
|
453
|
+
await new Promise((resolve, reject) => {
|
|
454
|
+
install.on("close", (code) => {
|
|
455
|
+
if (code === 0) {
|
|
456
|
+
console.log(chalk.green("โ
Dependencies installed"));
|
|
457
|
+
resolve(void 0);
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
reject(new Error(`npm install failed with code ${code}`));
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
install.on("error", reject);
|
|
464
|
+
});
|
|
465
|
+
// Start dev server
|
|
466
|
+
console.log(chalk.blue(`๐ Starting Next.js dev server on port ${options.port}...`));
|
|
467
|
+
devServer = spawn("npm", ["run", "dev", "--", "--port", options.port], {
|
|
468
|
+
cwd: outputDir,
|
|
469
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
470
|
+
});
|
|
471
|
+
devServer.stdout.on("data", (data) => {
|
|
472
|
+
const output = data.toString();
|
|
473
|
+
if (output.includes("Ready") || output.includes("started")) {
|
|
474
|
+
console.log(chalk.green(`๐ Next.js ready at http://localhost:${options.port}`));
|
|
475
|
+
}
|
|
476
|
+
// Filter out noisy Next.js logs, show important ones
|
|
477
|
+
if (output.includes("compiled") ||
|
|
478
|
+
output.includes("error") ||
|
|
479
|
+
output.includes("Ready")) {
|
|
480
|
+
process.stdout.write(chalk.gray("[Next.js] ") + output);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
if (options.verbose) {
|
|
484
|
+
devServer.stderr.on("data", (data) => {
|
|
485
|
+
process.stderr.write(chalk.red("[Next.js Error] ") + data.toString());
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
devServer.on("error", (error) => {
|
|
489
|
+
console.error(chalk.red("โ Error starting dev server:"), error);
|
|
490
|
+
});
|
|
491
|
+
await generator.startWatching();
|
|
492
|
+
// Handle graceful shutdown
|
|
493
|
+
process.on("SIGINT", () => {
|
|
494
|
+
console.log(chalk.yellow("\n๐ Shutting down..."));
|
|
495
|
+
generator.stop();
|
|
496
|
+
if (devServer) {
|
|
497
|
+
devServer.kill();
|
|
498
|
+
}
|
|
499
|
+
process.exit(0);
|
|
500
|
+
});
|
|
501
|
+
console.log(chalk.green("๐ Generator is running! Press Ctrl+C to stop."));
|
|
502
|
+
if (options.dev) {
|
|
503
|
+
console.log(chalk.cyan(`๐ Edit your MDX files in: ${watchDir}`));
|
|
504
|
+
console.log(chalk.cyan(`๐ View changes at: http://localhost:${options.port}`));
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
program
|
|
508
|
+
.command("build")
|
|
509
|
+
.description("One-time build of Next.js app from MDX files")
|
|
510
|
+
.action(async () => {
|
|
511
|
+
const questions = [
|
|
512
|
+
{
|
|
513
|
+
type: "text",
|
|
514
|
+
name: "watchDir",
|
|
515
|
+
message: "Enter directory to watch for MDX files:",
|
|
516
|
+
initial: "docs",
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
type: "text",
|
|
520
|
+
name: "outputDir",
|
|
521
|
+
message: "Enter output directory for Next.js app:",
|
|
522
|
+
initial: "nextjs-app",
|
|
523
|
+
},
|
|
524
|
+
];
|
|
525
|
+
const { watchDir: watchDirInput, outputDir: outputDirInput } = (await prompts(questions));
|
|
526
|
+
const watchDir = path.resolve(process.cwd(), watchDirInput);
|
|
527
|
+
const outputDir = path.resolve(process.cwd(), outputDirInput);
|
|
528
|
+
const generator = new MDXToNextJSGenerator(watchDir, outputDir);
|
|
529
|
+
await generator.init();
|
|
530
|
+
console.log(chalk.green("๐ Build complete!"));
|
|
531
|
+
});
|
|
532
|
+
program.parse();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const clickOutsideTemplate = "\nimport { RefObject, useEffect } from \"react\";\n\nexport function useOnClickOutside(\n refs: RefObject<HTMLElement | null>[],\n cb: () => void,\n) {\n useEffect(() => {\n function handleClickOutside(event: MouseEvent) {\n if (\n refs &&\n refs\n .map(\n (ref) =>\n ref && ref.current && ref.current.contains(event.target as Node),\n )\n .every((i) => i === false)\n ) {\n cb();\n }\n }\n document.addEventListener(\"mousedown\", handleClickOutside);\n return () => {\n document.removeEventListener(\"mousedown\", handleClickOutside);\n };\n }, [refs, cb]);\n}\n";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const clickOutsideTemplate = `
|
|
2
|
+
import { RefObject, useEffect } from "react";
|
|
3
|
+
|
|
4
|
+
export function useOnClickOutside(
|
|
5
|
+
refs: RefObject<HTMLElement | null>[],
|
|
6
|
+
cb: () => void,
|
|
7
|
+
) {
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
function handleClickOutside(event: MouseEvent) {
|
|
10
|
+
if (
|
|
11
|
+
refs &&
|
|
12
|
+
refs
|
|
13
|
+
.map(
|
|
14
|
+
(ref) =>
|
|
15
|
+
ref && ref.current && ref.current.contains(event.target as Node),
|
|
16
|
+
)
|
|
17
|
+
.every((i) => i === false)
|
|
18
|
+
) {
|
|
19
|
+
cb();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
23
|
+
return () => {
|
|
24
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
25
|
+
};
|
|
26
|
+
}, [refs, cb]);
|
|
27
|
+
}
|
|
28
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const codeTemplate = "\"use client\";\n\"use client\";\nimport styled from \"styled-components\";\nimport { Theme, styledCode } from \"cherry-styled-components/src/lib\";\nimport { rgba } from \"polished\";\nimport { unified } from \"unified\";\nimport rehypeParse from \"rehype-parse\";\nimport rehypeHighlight from \"rehype-highlight\";\nimport rehypeStringify from \"rehype-stringify\";\nimport { editableContent } from \"@/app/components/layout/SharedStyled\";\n\ninterface CodeProps extends React.HTMLAttributes<HTMLDivElement> {\n code: string;\n language?: string;\n theme?: Theme;\n}\n\nconst CodeWrapper = styled.span<{ theme: Theme }>`\n position: relative;\n z-index: 2;\n display: block;\n width: 100%;\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n border: solid 1px\n ${({ theme }) =>\n theme.isDark ? rgba(theme.colors.dark, 0.2) : rgba(theme.colors.dark, 0)};\n`;\n\nconst TopBar = styled.div<{ theme: Theme }>`\n background: #0d1117;\n border-top-left-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-top-right-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-bottom: solid 1px ${rgba(\"#ffffff\", 0.1)};\n height: 33px;\n width: 100%;\n display: flex;\n justify-content: flex-start;\n gap: 5px;\n padding: 10px;\n`;\n\nconst Dot = styled.span<{ theme: Theme }>`\n margin: auto 0;\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: ${rgba(\"#ffffff\", 0.1)};\n`;\n\nconst Body = styled.div<{ theme: Theme }>`\n background: #0d1117;\n border-bottom-left-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-bottom-right-radius: ${({ theme }) => theme.spacing.radius.lg};\n color: #ffffff;\n padding: 20px;\n font-family: ${({ theme }) => theme.fonts.mono};\n text-align: left;\n overflow-x: auto;\n overflow-y: auto;\n max-height: calc(100svh - 400px);\n ${({ theme }) => styledCode(theme)};\n\n &[contenteditable=\"true\"] {\n ${editableContent};\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n }\n\n & .hljs {\n color: #c9d1d9;\n background: #0d1117;\n }\n\n & .hljs-doctag,\n & .hljs-keyword,\n & .hljs-meta .hljs-keyword,\n & .hljs-template-tag,\n & .hljs-template-variable,\n & .hljs-type,\n & .hljs-variable.language_ {\n color: #ff7b72;\n }\n\n & .hljs-title,\n & .hljs-title.class_,\n & .hljs-title.class_.inherited__,\n & .hljs-title.function_ {\n color: #d2a8ff;\n }\n\n & .hljs-attr,\n & .hljs-attribute,\n & .hljs-literal,\n & .hljs-meta,\n & .hljs-number,\n & .hljs-operator,\n & .hljs-selector-attr,\n & .hljs-selector-class,\n & .hljs-selector-id,\n & .hljs-variable {\n color: #79c0ff;\n }\n\n & .hljs-meta .hljs-string,\n & .hljs-regexp,\n & .hljs-string {\n color: #a5d6ff;\n }\n\n & .hljs-built_in,\n & .hljs-symbol {\n color: #ffa657;\n }\n\n & .hljs-code,\n & .hljs-comment,\n & .hljs-formula {\n color: #8b949e;\n }\n\n & .hljs-name,\n & .hljs-quote,\n & .hljs-selector-pseudo,\n & .hljs-selector-tag {\n color: #7ee787;\n }\n\n & .hljs-subst {\n color: #c9d1d9;\n }\n\n & .hljs-section {\n color: #1f6feb;\n font-weight: 700;\n }\n\n & .hljs-bullet {\n color: #f2cc60;\n }\n\n & .hljs-emphasis {\n color: #c9d1d9;\n font-style: italic;\n }\n\n & .hljs-strong {\n color: #c9d1d9;\n font-weight: 700;\n }\n\n & .hljs-addition {\n color: #aff5b4;\n background-color: #033a16;\n }\n\n & .hljs-deletion {\n color: #ffdcd7;\n background-color: #67060c;\n }\n`;\n\nconst highlightCode = (code: string, language: string): string => {\n const result = unified()\n .use(rehypeParse, { fragment: true })\n .use(rehypeHighlight, {\n detect: true,\n ignoreMissing: true,\n })\n .use(rehypeStringify)\n .processSync(\n `<pre><code class=\"language-${language}\">${code}</code></pre>`,\n );\n\n return String(result);\n};\n\nfunction Code({ code, language = \"javascript\", ...props }: CodeProps) {\n const highlightedCode = highlightCode(code, language);\n return (\n <CodeWrapper>\n <TopBar>\n <Dot />\n <Dot />\n <Dot />\n </TopBar>\n <Body dangerouslySetInnerHTML={{ __html: highlightedCode }} {...props} />\n </CodeWrapper>\n );\n}\n\nexport { Code };\n";
|