| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- import type { UITree } from "@json-render/core";
- import { collectUsedComponents, serializeProps } from "@json-render/codegen";
- import { componentTemplates } from "./templates";
- export interface ExportedFile {
- path: string;
- content: string;
- }
- export interface GeneratorOptions {
- projectName?: string;
- data?: Record<string, unknown>;
- }
- /**
- * Generate a complete Next.js project from a UI tree
- */
- export function generateNextJSProject(
- tree: UITree,
- options: GeneratorOptions = {},
- ): ExportedFile[] {
- const { projectName = "generated-dashboard", data = {} } = options;
- const files: ExportedFile[] = [];
- // Collect what we need
- const usedComponents = collectUsedComponents(tree);
- // 1. Generate package.json
- files.push({
- path: "package.json",
- content: generatePackageJson(projectName),
- });
- // 2. Generate next.config.js
- files.push({
- path: "next.config.js",
- content: `/** @type {import('next').NextConfig} */
- module.exports = {
- reactStrictMode: true,
- };
- `,
- });
- // 3. Generate tsconfig.json
- files.push({
- path: "tsconfig.json",
- content: JSON.stringify(
- {
- compilerOptions: {
- target: "ES2017",
- lib: ["dom", "dom.iterable", "esnext"],
- allowJs: true,
- skipLibCheck: true,
- strict: true,
- noEmit: true,
- esModuleInterop: true,
- module: "esnext",
- moduleResolution: "bundler",
- resolveJsonModule: true,
- isolatedModules: true,
- jsx: "preserve",
- incremental: true,
- plugins: [{ name: "next" }],
- paths: { "@/*": ["./*"] },
- },
- include: [
- "next-env.d.ts",
- "**/*.ts",
- "**/*.tsx",
- ".next/types/**/*.ts",
- ],
- exclude: ["node_modules"],
- },
- null,
- 2,
- ),
- });
- // 4. Generate globals.css
- files.push({
- path: "app/globals.css",
- content: generateGlobalsCss(),
- });
- // 5. Generate layout.tsx
- files.push({
- path: "app/layout.tsx",
- content: generateLayout(projectName),
- });
- // 6. Generate component files (only the ones that are used)
- for (const componentName of usedComponents) {
- const template = componentTemplates[componentName];
- if (template) {
- files.push({
- path: `components/ui/${componentName.toLowerCase()}.tsx`,
- content: template,
- });
- }
- }
- // 7. Generate components/ui/index.ts
- files.push({
- path: "components/ui/index.ts",
- content: generateComponentIndex(usedComponents),
- });
- // 8. Generate the main page with the tree
- files.push({
- path: "app/page.tsx",
- content: generateMainPage(tree, usedComponents, data),
- });
- // 9. Generate README
- files.push({
- path: "README.md",
- content: generateReadme(projectName),
- });
- return files;
- }
- function generatePackageJson(name: string): string {
- return JSON.stringify(
- {
- name,
- version: "0.1.0",
- private: true,
- scripts: {
- dev: "next dev",
- build: "next build",
- start: "next start",
- lint: "next lint",
- },
- dependencies: {
- next: "^14.2.0",
- react: "^18.3.0",
- "react-dom": "^18.3.0",
- },
- devDependencies: {
- "@types/node": "^20.0.0",
- "@types/react": "^18.3.0",
- "@types/react-dom": "^18.3.0",
- typescript: "^5.4.0",
- },
- },
- null,
- 2,
- );
- }
- function generateGlobalsCss(): string {
- return `* {
- box-sizing: border-box;
- }
- :root {
- --background: #000;
- --foreground: #fafafa;
- --card: #0a0a0a;
- --border: #262626;
- --muted: #a3a3a3;
- --radius: 8px;
- }
- html,
- body {
- margin: 0;
- padding: 0;
- font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background-color: var(--background);
- color: var(--foreground);
- -webkit-font-smoothing: antialiased;
- }
- button {
- font-family: inherit;
- cursor: pointer;
- }
- input,
- select,
- textarea {
- font-family: inherit;
- }
- ::selection {
- background: var(--foreground);
- color: var(--background);
- }
- `;
- }
- function generateLayout(projectName: string): string {
- return `import type { Metadata } from "next";
- import "./globals.css";
- export const metadata: Metadata = {
- title: "${projectName}",
- description: "Generated dashboard",
- };
- export default function RootLayout({
- children,
- }: {
- children: React.ReactNode;
- }) {
- return (
- <html lang="en">
- <body>{children}</body>
- </html>
- );
- }
- `;
- }
- function generateComponentIndex(components: Set<string>): string {
- const exports = Array.from(components)
- .sort()
- .map((name) => `export { ${name} } from "./${name.toLowerCase()}";`)
- .join("\n");
- return exports + "\n";
- }
- function generateMainPage(
- tree: UITree,
- components: Set<string>,
- data: Record<string, unknown>,
- ): string {
- const imports = Array.from(components).sort().join(", ");
- const jsx = generateJSX(tree, tree.root, 4);
- const dataStr = JSON.stringify(data, null, 2).replace(/\n/g, "\n ");
- return `"use client";
- import { ${imports} } from "@/components/ui";
- const data = ${dataStr};
- export default function Page() {
- return (
- <div style={{ maxWidth: 960, margin: "0 auto", padding: "48px 24px" }}>
- ${jsx}
- </div>
- );
- }
- `;
- }
- function generateJSX(tree: UITree, key: string, indent: number): string {
- const element = tree.elements[key];
- if (!element) return "";
- const spaces = " ".repeat(indent);
- const componentName = element.type;
- // Filter out null/undefined props and convert data paths to data references
- const propsObj: Record<string, unknown> = {};
- for (const [k, v] of Object.entries(element.props)) {
- if (v === null || v === undefined) continue;
- // Convert *Path props to actual data values
- if (
- typeof v === "string" &&
- (k.endsWith("Path") || k === "bindPath" || k === "dataPath")
- ) {
- // Keep as a special marker for the component
- propsObj[k] = v;
- } else {
- propsObj[k] = v;
- }
- }
- // Add data prop for components that need it
- const needsData =
- Object.keys(propsObj).some(
- (k) => k.endsWith("Path") || k === "bindPath" || k === "dataPath",
- ) || ["Chart", "Table", "Metric", "List"].includes(componentName);
- const propsStr = serializeProps(propsObj);
- const dataAttr = needsData ? " data={data}" : "";
- const hasChildren = element.children && element.children.length > 0;
- if (!hasChildren) {
- if (propsStr || dataAttr) {
- return `${spaces}<${componentName}${dataAttr}${propsStr ? " " + propsStr : ""} />`;
- }
- return `${spaces}<${componentName} />`;
- }
- const lines: string[] = [];
- if (propsStr || dataAttr) {
- lines.push(
- `${spaces}<${componentName}${dataAttr}${propsStr ? " " + propsStr : ""}>`,
- );
- } else {
- lines.push(`${spaces}<${componentName}>`);
- }
- for (const childKey of element.children!) {
- lines.push(generateJSX(tree, childKey, indent + 2));
- }
- lines.push(`${spaces}</${componentName}>`);
- return lines.join("\n");
- }
- function generateReadme(projectName: string): string {
- return `# ${projectName}
- This dashboard was generated from a json-render UI tree.
- ## Getting Started
- \`\`\`bash
- npm install
- npm run dev
- \`\`\`
- Open [http://localhost:3000](http://localhost:3000) to view the dashboard.
- ## Project Structure
- - \`app/page.tsx\` - Main dashboard page
- - \`components/ui/\` - UI components
- - \`app/globals.css\` - Global styles
- `;
- }
|