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.
Files changed (62) hide show
  1. package/README.md +65 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +532 -0
  4. package/dist/templates/click-outside.d.ts +1 -0
  5. package/dist/templates/click-outside.js +28 -0
  6. package/dist/templates/code.d.ts +1 -0
  7. package/dist/templates/code.js +192 -0
  8. package/dist/templates/components/ClickOutside.d.ts +1 -0
  9. package/dist/templates/components/ClickOutside.js +27 -0
  10. package/dist/templates/components/Docs.d.ts +1 -0
  11. package/dist/templates/components/Docs.js +52 -0
  12. package/dist/templates/components/SideBar.d.ts +1 -0
  13. package/dist/templates/components/SideBar.js +80 -0
  14. package/dist/templates/components/layout/Code.d.ts +1 -0
  15. package/dist/templates/components/layout/Code.js +192 -0
  16. package/dist/templates/components/layout/DocsComponents.d.ts +1 -0
  17. package/dist/templates/components/layout/DocsComponents.js +444 -0
  18. package/dist/templates/components/layout/Footer.d.ts +1 -0
  19. package/dist/templates/components/layout/Footer.js +8 -0
  20. package/dist/templates/components/layout/Header.d.ts +1 -0
  21. package/dist/templates/components/layout/Header.js +280 -0
  22. package/dist/templates/components/layout/Icon.d.ts +1 -0
  23. package/dist/templates/components/layout/Icon.js +19 -0
  24. package/dist/templates/components/layout/Pictograms.d.ts +1 -0
  25. package/dist/templates/components/layout/Pictograms.js +66 -0
  26. package/dist/templates/components/layout/SharedStyles.d.ts +1 -0
  27. package/dist/templates/components/layout/SharedStyles.js +791 -0
  28. package/dist/templates/components/layout/ThemeToggle.d.ts +1 -0
  29. package/dist/templates/components/layout/ThemeToggle.js +123 -0
  30. package/dist/templates/components/layout/Typography.d.ts +1 -0
  31. package/dist/templates/components/layout/Typography.js +51 -0
  32. package/dist/templates/docs-components.d.ts +1 -0
  33. package/dist/templates/docs-components.js +441 -0
  34. package/dist/templates/docs.d.ts +1 -0
  35. package/dist/templates/docs.js +48 -0
  36. package/dist/templates/footer.d.ts +1 -0
  37. package/dist/templates/footer.js +9 -0
  38. package/dist/templates/gitignore.d.ts +1 -0
  39. package/dist/templates/gitignore.js +41 -0
  40. package/dist/templates/header.d.ts +1 -0
  41. package/dist/templates/header.js +275 -0
  42. package/dist/templates/home.d.ts +1 -0
  43. package/dist/templates/home.js +79 -0
  44. package/dist/templates/icon.d.ts +1 -0
  45. package/dist/templates/icon.js +20 -0
  46. package/dist/templates/layout.d.ts +1 -0
  47. package/dist/templates/layout.js +66 -0
  48. package/dist/templates/not-found.d.ts +1 -0
  49. package/dist/templates/not-found.js +22 -0
  50. package/dist/templates/pictograms.d.ts +1 -0
  51. package/dist/templates/pictograms.js +67 -0
  52. package/dist/templates/shared-styles.d.ts +1 -0
  53. package/dist/templates/shared-styles.js +792 -0
  54. package/dist/templates/theme-toggle.d.ts +1 -0
  55. package/dist/templates/theme-toggle.js +111 -0
  56. package/dist/templates/theme.d.ts +1 -0
  57. package/dist/templates/theme.js +291 -0
  58. package/dist/templates/typography.d.ts +1 -0
  59. package/dist/templates/typography.js +52 -0
  60. package/dist/templates/utils/orderNavItems.d.ts +1 -0
  61. package/dist/templates/utils/orderNavItems.js +45 -0
  62. 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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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";