UNPKG

next-json-cms

Version:

Git-based JSON CMS for Next.js projects with real-time preview - supports both app and pages router

1,379 lines (1,230 loc) 43.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.initProject = initProject; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const child_process_1 = require("child_process"); const inquirer_1 = __importDefault(require("inquirer")); const TEMPLATE_FILES = { "content/example.json": { title: "Welcome to JSON CMS", description: "This is a dynamic content management system for your Next.js application. Edit the content below using the CMS editor.", features: [ { id: 1, title: "Easy Content Management", description: "Edit your JSON content through a user-friendly interface", }, { id: 2, title: "Real-time Updates", description: "See your changes instantly on the website", }, { id: 3, title: "Git Integration", description: "Track content changes with built-in version control", }, { id: 4, title: "Schema Validation", description: "Ensure content structure remains consistent", }, ], cta: { text: "Start Editing", link: "/editor", }, }, "content/schema/example.schema.json": { $schema: "http://json-schema.org/draft-07/schema#", type: "object", required: ["title", "description", "items"], properties: { title: { type: "string", minLength: 1, }, description: { type: "string", }, items: { type: "array", items: { type: "object", required: ["id", "name"], properties: { id: { type: "number", }, name: { type: "string", minLength: 1, }, }, }, }, }, }, }; // Editor component templates - updated with 'use client' const EDITOR_COMPONENT = `'use client'; import { useEffect } from "react"; import dynamic from "next/dynamic"; import { useEditorStore } from "@/store/editorStore"; import { FileTree } from "@/components/editor/FileTree"; import { EditorToolbar } from "@/components/editor/EditorToolbar"; // Dynamically import Monaco editor to avoid SSR issues const MonacoEditor = dynamic( () => import("@monaco-editor/react").then((mod) => mod.default), { ssr: false } ); export function EditorPage() { const { currentFile, content, isLoading, hasChanges, error, setContent, } = useEditorStore(); return ( <div className="flex bg-gray-100 h-screen"> {/* Sidebar */} <div className="bg-white border-r w-64"> <FileTree /> </div> {/* Main content */} <div className="flex flex-col flex-1"> <EditorToolbar /> <div className="relative flex-1"> {isLoading ? ( <div className="absolute inset-0 flex justify-center items-center bg-white bg-opacity-75"> Loading... </div> ) : currentFile ? ( <MonacoEditor height="100%" language="json" theme="vs-light" value={content} onChange={(value) => setContent(value || "")} options={{ minimap: { enabled: false }, formatOnPaste: true, formatOnType: true, automaticLayout: true, }} /> ) : ( <div className="flex justify-center items-center h-full text-gray-500"> Select a file to edit </div> )} {error && ( <div className="right-0 bottom-0 left-0 absolute bg-red-100 p-4 text-red-700"> {error} </div> )} </div> </div> </div> ); }`; // File tree component const FILE_TREE_COMPONENT = `'use client'; import { useEffect, useState, useCallback } from "react"; import { useEditorStore } from "@/store/editorStore"; import { contentService } from "@/services/contentService"; import { gitService } from "@/services/gitService"; interface FileItem { name: string; path: string; status?: string; } export function FileTree() { const { setCurrentFile, currentFile, loadContent, isLoading, hasChanges } = useEditorStore(); const [files, setFiles] = useState<FileItem[]>([]); const loadFiles = useCallback(async () => { try { // Load files from content directory const contentFiles = await contentService.listContentFiles(); // Get Git status for each file const filesWithStatus = await Promise.all( contentFiles.map(async (file) => { const status = await gitService.getFileStatus(file.path); return { ...file, status, }; }) ); setFiles(filesWithStatus); } catch (error) { console.error("Error loading files:", error); } }, []); // Initial load useEffect(() => { loadFiles(); }, [loadFiles]); // Refresh when changes are made useEffect(() => { if (!hasChanges) { loadFiles(); } }, [hasChanges, loadFiles]); function handleFileClick(file: FileItem) { if (isLoading) return; setCurrentFile(file.name); loadContent(file.name); } // Helper to render status icon function getStatusIcon(status?: string) { if (!status || status === "unmodified") return null; if (status === "modified") { return <span className="h-2 w-2 rounded-full bg-yellow-500 inline-block mr-1" title="Modified" />; } if (status === "added") { return <span className="h-2 w-2 rounded-full bg-green-500 inline-block mr-1" title="Added" />; } return <span className="h-2 w-2 rounded-full bg-gray-500 inline-block mr-1" title={status} />; } return ( <div className="p-4"> <h2 className="text-lg font-semibold mb-4">Content Files</h2> <div className="space-y-1"> {files.map((file) => ( <div key={file.name} className={\`flex items-center p-2 rounded hover:bg-gray-100 cursor-pointer \${ currentFile === file.name ? "bg-blue-50 border-l-4 border-blue-500" : "" }\`} onClick={() => handleFileClick(file)} > {getStatusIcon(file.status)} <span className="truncate">{file.name}</span> </div> ))} </div> </div> ); }`; // Editor toolbar component const EDITOR_TOOLBAR_COMPONENT = `'use client'; import { useState } from "react"; import { useEditorStore } from "@/store/editorStore"; import { gitService } from "@/services/gitService"; export function EditorToolbar() { const { currentFile, hasChanges, saveContent, refreshContent } = useEditorStore(); const [commitMessage, setCommitMessage] = useState(""); const [isCommitting, setIsCommitting] = useState(false); const [commitError, setCommitError] = useState(""); async function handleSave() { if (!currentFile) return; await saveContent(); } async function handleCommit() { if (!currentFile || !commitMessage.trim()) return; setIsCommitting(true); setCommitError(""); try { // First save any pending changes await saveContent(); // Then commit to Git await gitService.commitChanges(commitMessage); // Clear commit message after successful commit setCommitMessage(""); // Refresh content to update Git status refreshContent(); } catch (error) { console.error("Error committing changes:", error); setCommitError(error instanceof Error ? error.message : String(error)); } finally { setIsCommitting(false); } } return ( <div className="bg-white border-b p-4 flex items-center justify-between"> <div className="flex items-center space-x-2"> <h1 className="text-xl font-semibold"> {currentFile || "JSON CMS Editor"} </h1> {currentFile && hasChanges && ( <span className="text-yellow-500 text-sm">(unsaved changes)</span> )} </div> <div className="flex items-center space-x-4"> {currentFile && ( <> <button onClick={handleSave} disabled={!hasChanges} className={\`px-3 py-1 rounded \${ hasChanges ? "bg-blue-500 text-white hover:bg-blue-600" : "bg-gray-200 text-gray-500 cursor-not-allowed" }\`} > Save </button> <div className="flex items-center space-x-2"> <input type="text" placeholder="Commit message" value={commitMessage} onChange={(e) => setCommitMessage(e.target.value)} className="border rounded px-2 py-1 w-48" /> <button onClick={handleCommit} disabled={isCommitting || !commitMessage.trim()} className={\`px-3 py-1 rounded \${ isCommitting || !commitMessage.trim() ? "bg-gray-200 text-gray-500 cursor-not-allowed" : "bg-green-500 text-white hover:bg-green-600" }\`} > {isCommitting ? "Committing..." : "Commit"} </button> </div> </> )} </div> {commitError && ( <div className="absolute top-16 right-4 bg-red-100 text-red-700 p-2 rounded"> {commitError} </div> )} </div> ); }`; // Advanced editor store const EDITOR_STORE = `'use client'; import { create } from 'zustand'; import { contentService } from '@/services/contentService'; interface EditorState { currentFile: string | null; content: string; originalContent: string; isLoading: boolean; error: string | null; hasChanges: boolean; setCurrentFile: (file: string | null) => void; setContent: (content: string) => void; loadContent: (filename: string) => Promise<void>; saveContent: () => Promise<void>; refreshContent: () => Promise<void>; } export const useEditorStore = create<EditorState>((set, get) => ({ currentFile: null, content: "", originalContent: "", isLoading: false, error: null, hasChanges: false, setCurrentFile: (file) => set({ currentFile: file }), setContent: (content) => { set(state => ({ content, hasChanges: content !== state.originalContent })); }, loadContent: async (filename) => { set({ isLoading: true, error: null }); try { const content = await contentService.getContent(filename); const contentStr = JSON.stringify(content, null, 2); set({ content: contentStr, originalContent: contentStr, currentFile: filename, hasChanges: false }); } catch (error) { set({ error: error instanceof Error ? error.message : String(error) }); } finally { set({ isLoading: false }); } }, saveContent: async () => { const { currentFile, content } = get(); if (!currentFile) return; set({ isLoading: true, error: null }); try { // Parse JSON to validate and format const parsedContent = JSON.parse(content); // Save content await contentService.saveContent(currentFile, parsedContent); // Update original content to mark as saved set({ originalContent: content, hasChanges: false }); } catch (error) { set({ error: error instanceof Error ? error.message : String(error) }); } finally { set({ isLoading: false }); } }, refreshContent: async () => { const { currentFile } = get(); if (currentFile) { await get().loadContent(currentFile); } } }));`; // Content service that uses API calls instead of direct fs access const SERVICE_TEMPLATE = `/** * ContentService - Handles content operations through API calls */ class ContentService { private apiBase: string; constructor() { // Use relative URL for API endpoints this.apiBase = '/api/content'; } /** * Get a list of all content files */ async listContentFiles() { try { const response = await fetch(this.apiBase); if (!response.ok) throw new Error('Failed to fetch content files'); return await response.json(); } catch (error) { console.error('Error listing content files:', error); return []; } } /** * Read content from a file */ async getContent(filename: string) { try { const response = await fetch(\`\${this.apiBase}?file=\${encodeURIComponent(filename)}\`); if (!response.ok) throw new Error(\`Failed to fetch content: \${filename}\`); return await response.json(); } catch (error) { console.error(\`Error reading content file \${filename}:\`, error); throw new Error(\`Failed to read content file \${filename}\`); } } /** * Write content to a file */ async saveContent(filename: string, content: any) { try { const response = await fetch(this.apiBase, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ filename, content }), }); if (!response.ok) throw new Error('Failed to save content'); return await response.json(); } catch (error) { console.error(\`Error saving content file \${filename}:\`, error); throw new Error(\`Failed to save content file \${filename}\`); } } } export const contentService = new ContentService(); export default contentService; `; // Git service that uses API calls instead of direct Git operations const GIT_SERVICE_TEMPLATE = `/** * GitService - Handles Git operations through API calls */ class GitService { private apiBase: string; constructor() { // Use relative URL for API endpoints this.apiBase = '/api/git'; } /** * Get the status of a specific file */ async getFileStatus(filePath: string): Promise<string> { try { const response = await fetch(\`\${this.apiBase}/status?path=\${encodeURIComponent(filePath)}\`); if (!response.ok) throw new Error('Failed to get file status'); const data = await response.json(); return data.status; } catch (error) { console.error('Error getting file status:', error); return 'unknown'; } } /** * Stage and commit changes */ async commitChanges(message: string): Promise<void> { try { const response = await fetch(\`\${this.apiBase}/commit\`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ message }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Failed to commit changes'); } } catch (error) { console.error('Error committing changes:', error); throw new Error('Failed to commit changes: ' + (error instanceof Error ? error.message : String(error))); } } /** * Push commits to remote */ async pushChanges(): Promise<void> { try { const response = await fetch(\`\${this.apiBase}/push\`, { method: 'POST' }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Failed to push changes'); } } catch (error) { console.error('Error pushing changes:', error); throw new Error('Failed to push changes: ' + (error instanceof Error ? error.message : String(error))); } } /** * Pull changes from remote */ async pullChanges(): Promise<void> { try { const response = await fetch(\`\${this.apiBase}/pull\`, { method: 'POST' }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Failed to pull changes'); } } catch (error) { console.error('Error pulling changes:', error); throw new Error('Failed to pull changes: ' + (error instanceof Error ? error.message : String(error))); } } /** * Get the commit history for a file */ async getFileHistory(filePath: string, maxCount = 10): Promise<any[]> { try { const response = await fetch( \`\${this.apiBase}/history?path=\${encodeURIComponent(filePath)}&count=\${maxCount}\` ); if (!response.ok) throw new Error('Failed to get file history'); const data = await response.json(); return data.history; } catch (error) { console.error('Error getting file history:', error); return []; } } } export const gitService = new GitService(); export default gitService; `; // Server-side implementation for Git API (Pages Router) const GIT_API_PAGES = `import { NextApiRequest, NextApiResponse } from 'next'; import { simpleGit } from 'simple-git'; import path from 'path'; const git = simpleGit({ baseDir: process.cwd() }); export default async function handler(req: NextApiRequest, res: NextApiResponse) { const { action } = req.query; try { switch (action) { case 'status': await handleStatus(req, res); break; case 'commit': await handleCommit(req, res); break; case 'push': await handlePush(req, res); break; case 'pull': await handlePull(req, res); break; case 'history': await handleHistory(req, res); break; default: res.status(400).json({ error: 'Invalid action' }); } } catch (error) { console.error('Git API error:', error); res.status(500).json({ error: 'Git operation failed', message: error instanceof Error ? error.message : String(error) }); } } async function handleStatus(req: NextApiRequest, res: NextApiResponse) { const { path: filePath } = req.query; if (!filePath || typeof filePath !== 'string') { return res.status(400).json({ error: 'File path is required' }); } const status = await git.status(); const relativePath = path.relative(process.cwd(), filePath); let fileStatus = 'unmodified'; if (status.not_added.includes(relativePath)) { fileStatus = 'untracked'; } else if (status.modified.includes(relativePath)) { fileStatus = 'modified'; } else if (status.created.includes(relativePath)) { fileStatus = 'added'; } else if (status.deleted.includes(relativePath)) { fileStatus = 'deleted'; } res.status(200).json({ status: fileStatus }); } async function handleCommit(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } const { message } = req.body; if (!message) { return res.status(400).json({ error: 'Commit message is required' }); } await git.add('.'); const result = await git.commit(message); res.status(200).json({ success: true, result }); } async function handlePush(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } const result = await git.push(); res.status(200).json({ success: true, result }); } async function handlePull(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } const result = await git.pull(); res.status(200).json({ success: true, result }); } async function handleHistory(req: NextApiRequest, res: NextApiResponse) { const { path: filePath, count } = req.query; if (!filePath || typeof filePath !== 'string') { return res.status(400).json({ error: 'File path is required' }); } const maxCount = count ? parseInt(count as string, 10) : 10; const relativePath = path.relative(process.cwd(), filePath); const log = await git.log({ file: relativePath, maxCount }); res.status(200).json({ history: log.all }); }`; // Server-side implementation for Git API (App Router) const GIT_API_APP = `import { NextResponse } from 'next/server'; import { simpleGit } from 'simple-git'; import path from 'path'; const git = simpleGit({ baseDir: process.cwd() }); export async function GET(request: Request) { const url = new URL(request.url); const action = url.pathname.split('/').pop(); try { switch (action) { case 'status': return handleStatus(url); case 'history': return handleHistory(url); default: return NextResponse.json({ error: 'Invalid action' }, { status: 400 }); } } catch (error) { console.error('Git API error:', error); return NextResponse.json({ error: 'Git operation failed', message: error instanceof Error ? error.message : String(error) }, { status: 500 }); } } export async function POST(request: Request) { const url = new URL(request.url); const action = url.pathname.split('/').pop(); try { switch (action) { case 'commit': return handleCommit(request); case 'push': return handlePush(); case 'pull': return handlePull(); default: return NextResponse.json({ error: 'Invalid action' }, { status: 400 }); } } catch (error) { console.error('Git API error:', error); return NextResponse.json({ error: 'Git operation failed', message: error instanceof Error ? error.message : String(error) }, { status: 500 }); } } async function handleStatus(url: URL) { const filePath = url.searchParams.get('path'); if (!filePath) { return NextResponse.json({ error: 'File path is required' }, { status: 400 }); } const status = await git.status(); const relativePath = path.relative(process.cwd(), filePath); let fileStatus = 'unmodified'; if (status.not_added.includes(relativePath)) { fileStatus = 'untracked'; } else if (status.modified.includes(relativePath)) { fileStatus = 'modified'; } else if (status.created.includes(relativePath)) { fileStatus = 'added'; } else if (status.deleted.includes(relativePath)) { fileStatus = 'deleted'; } return NextResponse.json({ status: fileStatus }); } async function handleCommit(request: Request) { const { message } = await request.json(); if (!message) { return NextResponse.json({ error: 'Commit message is required' }, { status: 400 }); } await git.add('.'); const result = await git.commit(message); return NextResponse.json({ success: true, result }); } async function handlePush() { const result = await git.push(); return NextResponse.json({ success: true, result }); } async function handlePull() { const result = await git.pull(); return NextResponse.json({ success: true, result }); } async function handleHistory(url: URL) { const filePath = url.searchParams.get('path'); const countParam = url.searchParams.get('count'); if (!filePath) { return NextResponse.json({ error: 'File path is required' }, { status: 400 }); } const maxCount = countParam ? parseInt(countParam, 10) : 10; const relativePath = path.relative(process.cwd(), filePath); const log = await git.log({ file: relativePath, maxCount }); return NextResponse.json({ history: log.all }); }`; // Server-side implementation for Content API with file system operations (Pages Router) const CONTENT_API_PAGES = `import { NextApiRequest, NextApiResponse } from 'next'; import fs from 'fs'; import path from 'path'; const contentDir = path.join(process.cwd(), 'content'); export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method === 'GET') { const { file } = req.query; if (!file || typeof file !== 'string') { // Return list of files if no filename provided const files = await listContentFiles(); return res.status(200).json(files); } try { const content = await getContent(file); return res.status(200).json(content); } catch (error) { return res.status(404).json({ error: \`Failed to read file: \${file}\`, message: error instanceof Error ? error.message : String(error) }); } } else if (req.method === 'POST') { try { const { filename, content } = req.body; if (!filename || !content) { return res.status(400).json({ error: 'Filename and content are required' }); } const result = await saveContent(filename, content); return res.status(200).json(result); } catch (error) { return res.status(500).json({ error: 'Failed to save content', message: error instanceof Error ? error.message : 'Unknown error' }); } } return res.status(405).json({ error: 'Method not allowed' }); } // Helper function to list content files async function listContentFiles() { try { const files = await fs.promises.readdir(contentDir); return files .filter(file => file.endsWith('.json') && !file.includes('.schema.')) .map(file => ({ name: file, path: path.join(contentDir, file), })); } catch (error) { console.error('Error listing content files:', error); return []; } } // Helper function to read content async function getContent(filename: string) { try { const filePath = path.join(contentDir, filename); const content = await fs.promises.readFile(filePath, 'utf-8'); return JSON.parse(content); } catch (error) { console.error(\`Error reading content file \${filename}:\`, error); throw new Error(\`Failed to read content file \${filename}\`); } } // Helper function to save content async function saveContent(filename: string, content: any) { try { const filePath = path.join(contentDir, filename); await fs.promises.writeFile( filePath, JSON.stringify(content, null, 2), 'utf-8' ); return { success: true, file: filename }; } catch (error) { console.error(\`Error saving content file \${filename}:\`, error); throw new Error(\`Failed to save content file \${filename}\`); } }`; // Server-side implementation for Content API (App Router) const CONTENT_API_APP = `import { NextResponse } from 'next/server'; import fs from 'fs'; import path from 'path'; const contentDir = path.join(process.cwd(), 'content'); export async function GET(request: Request) { const url = new URL(request.url); const filename = url.searchParams.get('file'); if (!filename) { // Return list of files if no filename provided const files = await listContentFiles(); return NextResponse.json(files); } try { const content = await getContent(filename); return NextResponse.json(content); } catch (error) { return NextResponse.json( { error: \`Failed to read file: \${filename}\` }, { status: 404 } ); } } export async function POST(request: Request) { try { const { filename, content } = await request.json(); if (!filename || !content) { return NextResponse.json( { error: 'Filename and content are required' }, { status: 400 } ); } const result = await saveContent(filename, content); return NextResponse.json(result); } catch (error) { return NextResponse.json( { error: 'Failed to save content', message: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 } ); } } // Helper function to list content files async function listContentFiles() { try { const files = await fs.promises.readdir(contentDir); return files .filter(file => file.endsWith('.json') && !file.includes('.schema.')) .map(file => ({ name: file, path: path.join(contentDir, file), })); } catch (error) { console.error('Error listing content files:', error); return []; } } // Helper function to read content async function getContent(filename: string) { try { const filePath = path.join(contentDir, filename); const content = await fs.promises.readFile(filePath, 'utf-8'); return JSON.parse(content); } catch (error) { console.error(\`Error reading content file \${filename}:\`, error); throw new Error(\`Failed to read content file \${filename}\`); } } // Helper function to save content async function saveContent(filename: string, content: any) { try { const filePath = path.join(contentDir, filename); await fs.promises.writeFile( filePath, JSON.stringify(content, null, 2), 'utf-8' ); return { success: true, file: filename }; } catch (error) { console.error(\`Error saving content file \${filename}:\`, error); throw new Error(\`Failed to save content file \${filename}\`); } }`; // Tailwind CSS config const TAILWIND_CONFIG = `module.exports = { content: [ './src/pages/**/*.{js,ts,jsx,tsx,mdx}', './src/components/**/*.{js,ts,jsx,tsx,mdx}', './src/app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: {}, }, plugins: [], }`; // PostCSS config const POSTCSS_CONFIG = `module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }`; // Global CSS with Tailwind const GLOBAL_CSS = `@import "tailwindcss"`; // Update the layout file content const LAYOUT_CONTENT = `import './globals.css'; export const metadata = { title: 'JSON CMS', description: 'A headless CMS for managing JSON content', }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body>{children}</body> </html> ); }`; // Update the _app.tsx content const APP_CONTENT = `import '../styles/globals.css'; import type { AppProps } from 'next/app'; export default function App({ Component, pageProps }: AppProps) { return <Component {...pageProps} />; }`; // Components const CONTENT_DISPLAY_COMPONENT = `'use client'; import { useEffect, useState } from 'react'; import Link from 'next/link'; interface Feature { id: number; title: string; description: string; } interface CTA { text: string; link: string; } interface ContentData { title: string; description: string; features: Feature[]; cta: CTA; } export function ContentDisplay() { const [content, setContent] = useState<ContentData | null>(null); useEffect(() => { fetch('/api/content?file=example.json') .then(res => res.json()) .then(data => setContent(data)) .catch(err => console.error('Error loading content:', err)); }, []); if (!content) { return ( <div className="flex items-center justify-center min-h-[400px]"> <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div> </div> ); } return ( <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> {/* Hero Section */} <div className="text-center mb-16"> <h1 className="text-4xl sm:text-5xl font-bold text-gray-900 mb-6"> {content.title} </h1> <p className="text-xl text-gray-600 mb-8 max-w-3xl mx-auto"> {content.description} </p> <Link href={content.cta.link} className="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 transition-colors" > {content.cta.text} </Link> </div> {/* Features Grid */} <div className="grid grid-cols-1 md:grid-cols-2 gap-8"> {content.features.map((feature) => ( <div key={feature.id} className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow" > <h3 className="text-xl font-semibold text-gray-900 mb-3"> {feature.title} </h3> <p className="text-gray-600"> {feature.description} </p> </div> ))} </div> </div> ); }`; // Add home page content const HOME_PAGE_CONTENT = `'use client'; import { ContentDisplay } from '@/components/home/ContentDisplay'; export default function Home() { return ( <main className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100"> <div className="py-8"> <ContentDisplay /> </div> </main> ); }`; // Update the editor page content const EDITOR_PAGE_CONTENT = `'use client'; import { EditorPage } from '@/components/editor/EditorPage'; export default function Editor() { return <EditorPage />; }`; async function promptForOptions() { const answers = await inquirer_1.default.prompt([ { type: "list", name: "router", message: "Which Next.js router would you like to use?", choices: [ { name: "App Router (recommended)", value: "app", description: "Use the new App Router (app directory)", }, { name: "Pages Router", value: "pages", description: "Use the Pages Router (pages directory)", }, ], default: "app", }, { type: "list", name: "useSrc", message: "Would you like to use the src directory?", choices: [ { name: "Yes (recommended)", value: true, description: "Create a src directory for your code", }, { name: "No", value: false, description: "Keep all code in the root directory", }, ], default: true, }, ]); return { router: answers.router, useSrc: answers.useSrc, }; } async function initProject(options) { const { directory, gitConfig } = options; // Prompt for options if not provided const { router, useSrc } = options.router && options.useSrc !== undefined ? { router: options.router, useSrc: options.useSrc } : await promptForOptions(); console.log("\n✨ Creating a new JSON CMS project with the following configuration:"); console.log(`\n📦 Router: ${router === "app" ? "App Router" : "Pages Router"}`); console.log(`📁 Directory Structure: ${useSrc ? "src directory" : "no src directory"}`); console.log("\n🚀 Let's get started!\n"); // Get absolute path for target directory const targetDir = path_1.default.resolve(directory); const originalDir = process.cwd(); try { // Create Next.js project using create-next-app console.log("Creating Next.js project..."); // For Pages Router, we use traditional flags // For App Router, we include the --app flag let createNextAppCommand = "npx create-next-app@latest"; createNextAppCommand += ` ${directory}`; createNextAppCommand += " --typescript --tailwind --eslint"; if (router === "app") { createNextAppCommand += " --app"; } else { createNextAppCommand += " --no-app"; // Explicitly disable app directory } if (useSrc) { createNextAppCommand += " --src-dir"; } (0, child_process_1.execSync)(createNextAppCommand, { stdio: "inherit" }); // Change to target directory process.chdir(targetDir); // Create additional CMS-specific directories console.log("Adding CMS-specific files..."); const baseDir = useSrc ? "src" : ""; const cmsDirectories = [ "content", "content/schema", `${baseDir}/components/editor`, `${baseDir}/services`, `${baseDir}/store`, ]; // Create CMS directories cmsDirectories.forEach((dir) => { if (!fs_1.default.existsSync(dir)) { fs_1.default.mkdirSync(dir, { recursive: true }); } }); // Create JSON content files Object.entries(TEMPLATE_FILES).forEach(([filePath, content]) => { fs_1.default.writeFileSync(filePath, JSON.stringify(content, null, 2)); }); // Create the EditorPage component and related components fs_1.default.writeFileSync(path_1.default.join(targetDir, `${baseDir}/components/editor/EditorPage.tsx`), EDITOR_COMPONENT); fs_1.default.writeFileSync(path_1.default.join(targetDir, `${baseDir}/components/editor/FileTree.tsx`), FILE_TREE_COMPONENT); fs_1.default.writeFileSync(path_1.default.join(targetDir, `${baseDir}/components/editor/EditorToolbar.tsx`), EDITOR_TOOLBAR_COMPONENT); // Create CMS components and services const componentsToCreate = [ { dir: `${baseDir}/components/editor`, files: [ { name: "EditorPage.tsx", content: EDITOR_COMPONENT }, { name: "FileTree.tsx", content: FILE_TREE_COMPONENT }, { name: "EditorToolbar.tsx", content: EDITOR_TOOLBAR_COMPONENT }, ], }, { dir: `${baseDir}/components/home`, files: [ { name: "ContentDisplay.tsx", content: CONTENT_DISPLAY_COMPONENT }, ], }, ]; // Create component directories and files componentsToCreate.forEach(({ dir, files }) => { fs_1.default.mkdirSync(dir, { recursive: true }); files.forEach(({ name, content }) => { fs_1.default.writeFileSync(path_1.default.join(dir, name), content); }); }); fs_1.default.writeFileSync(`${baseDir}/services/contentService.ts`, SERVICE_TEMPLATE); fs_1.default.writeFileSync(`${baseDir}/services/gitService.ts`, GIT_SERVICE_TEMPLATE); fs_1.default.writeFileSync(`${baseDir}/store/editorStore.ts`, EDITOR_STORE); // Create API routes and pages based on router type if (router === "app") { // Create app directory structure fs_1.default.mkdirSync(`${baseDir}/app`, { recursive: true }); fs_1.default.mkdirSync(`${baseDir}/app/api/content`, { recursive: true }); fs_1.default.mkdirSync(`${baseDir}/app/api/git/[action]`, { recursive: true }); fs_1.default.mkdirSync(`${baseDir}/app/editor`, { recursive: true }); // Create API routes fs_1.default.writeFileSync(`${baseDir}/app/api/content/route.ts`, CONTENT_API_APP); fs_1.default.writeFileSync(`${baseDir}/app/api/git/[action]/route.ts`, GIT_API_APP); // Create app pages fs_1.default.writeFileSync(`${baseDir}/app/globals.css`, GLOBAL_CSS); fs_1.default.writeFileSync(`${baseDir}/app/page.tsx`, HOME_PAGE_CONTENT); fs_1.default.writeFileSync(`${baseDir}/app/layout.tsx`, LAYOUT_CONTENT); fs_1.default.writeFileSync(`${baseDir}/app/editor/page.tsx`, EDITOR_PAGE_CONTENT); } else { // Create pages directory structure fs_1.default.mkdirSync(`${baseDir}/pages`, { recursive: true }); fs_1.default.mkdirSync(`${baseDir}/pages/api`, { recursive: true }); fs_1.default.mkdirSync(`${baseDir}/pages/api/git`, { recursive: true }); fs_1.default.mkdirSync(`${baseDir}/pages/editor`, { recursive: true }); fs_1.default.mkdirSync(`${baseDir}/styles`, { recursive: true }); // Create API routes fs_1.default.writeFileSync(`${baseDir}/pages/api/content.ts`, CONTENT_API_PAGES); fs_1.default.writeFileSync(`${baseDir}/pages/api/git/[action].ts`, GIT_API_PAGES); // Create pages fs_1.default.writeFileSync(`${baseDir}/styles/globals.css`, GLOBAL_CSS); fs_1.default.writeFileSync(`${baseDir}/pages/index.tsx`, HOME_PAGE_CONTENT); fs_1.default.writeFileSync(`${baseDir}/pages/_app.tsx`, APP_CONTENT); fs_1.default.writeFileSync(`${baseDir}/pages/editor/index.tsx`, EDITOR_PAGE_CONTENT); } // Create shared components directory fs_1.default.mkdirSync(`${baseDir}/components/home`, { recursive: true }); fs_1.default.writeFileSync(`${baseDir}/components/home/ContentDisplay.tsx`, CONTENT_DISPLAY_COMPONENT); // Update package.json to add CMS dependencies const packageJson = JSON.parse(fs_1.default.readFileSync("package.json", "utf-8")); const cmsPackages = { "@monaco-editor/react": "^4.6.0", "simple-git": "^3.22.0", zustand: "^4.5.1", }; packageJson.dependencies = { ...packageJson.dependencies, ...cmsPackages, }; fs_1.default.writeFileSync("package.json", JSON.stringify(packageJson, null, 2)); // Install the new dependencies console.log("Installing additional dependencies..."); (0, child_process_1.execSync)("npm install", { stdio: "inherit" }); console.log("\n✨ Project initialized successfully!"); console.log("\nNext steps:"); console.log(`1. cd "${directory}"`); console.log("2. Run 'npm run dev' to start the development server"); console.log("\nThe CMS will be available at: http://localhost:3000/editor"); // Return to original directory process.chdir(originalDir); } catch (error) { // Return to original directory in case of error process.chdir(originalDir); throw new Error(`Failed to initialize project: ${error.message}`); } }