generator.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. import type { UITree } from "@json-render/core";
  2. import { collectUsedComponents, serializeProps } from "@json-render/codegen";
  3. import { componentTemplates } from "./templates";
  4. export interface ExportedFile {
  5. path: string;
  6. content: string;
  7. }
  8. export interface GeneratorOptions {
  9. projectName?: string;
  10. data?: Record<string, unknown>;
  11. }
  12. /**
  13. * Generate a complete Next.js project from a UI tree
  14. */
  15. export function generateNextJSProject(
  16. tree: UITree,
  17. options: GeneratorOptions = {},
  18. ): ExportedFile[] {
  19. const { projectName = "generated-dashboard", data = {} } = options;
  20. const files: ExportedFile[] = [];
  21. // Collect what we need
  22. const usedComponents = collectUsedComponents(tree);
  23. // 1. Generate package.json
  24. files.push({
  25. path: "package.json",
  26. content: generatePackageJson(projectName),
  27. });
  28. // 2. Generate next.config.js
  29. files.push({
  30. path: "next.config.js",
  31. content: `/** @type {import('next').NextConfig} */
  32. module.exports = {
  33. reactStrictMode: true,
  34. };
  35. `,
  36. });
  37. // 3. Generate tsconfig.json
  38. files.push({
  39. path: "tsconfig.json",
  40. content: JSON.stringify(
  41. {
  42. compilerOptions: {
  43. target: "ES2017",
  44. lib: ["dom", "dom.iterable", "esnext"],
  45. allowJs: true,
  46. skipLibCheck: true,
  47. strict: true,
  48. noEmit: true,
  49. esModuleInterop: true,
  50. module: "esnext",
  51. moduleResolution: "bundler",
  52. resolveJsonModule: true,
  53. isolatedModules: true,
  54. jsx: "preserve",
  55. incremental: true,
  56. plugins: [{ name: "next" }],
  57. paths: { "@/*": ["./*"] },
  58. },
  59. include: [
  60. "next-env.d.ts",
  61. "**/*.ts",
  62. "**/*.tsx",
  63. ".next/types/**/*.ts",
  64. ],
  65. exclude: ["node_modules"],
  66. },
  67. null,
  68. 2,
  69. ),
  70. });
  71. // 4. Generate globals.css
  72. files.push({
  73. path: "app/globals.css",
  74. content: generateGlobalsCss(),
  75. });
  76. // 5. Generate layout.tsx
  77. files.push({
  78. path: "app/layout.tsx",
  79. content: generateLayout(projectName),
  80. });
  81. // 6. Generate component files (only the ones that are used)
  82. for (const componentName of usedComponents) {
  83. const template = componentTemplates[componentName];
  84. if (template) {
  85. files.push({
  86. path: `components/ui/${componentName.toLowerCase()}.tsx`,
  87. content: template,
  88. });
  89. }
  90. }
  91. // 7. Generate components/ui/index.ts
  92. files.push({
  93. path: "components/ui/index.ts",
  94. content: generateComponentIndex(usedComponents),
  95. });
  96. // 8. Generate the main page with the tree
  97. files.push({
  98. path: "app/page.tsx",
  99. content: generateMainPage(tree, usedComponents, data),
  100. });
  101. // 9. Generate README
  102. files.push({
  103. path: "README.md",
  104. content: generateReadme(projectName),
  105. });
  106. return files;
  107. }
  108. function generatePackageJson(name: string): string {
  109. return JSON.stringify(
  110. {
  111. name,
  112. version: "0.1.0",
  113. private: true,
  114. scripts: {
  115. dev: "next dev",
  116. build: "next build",
  117. start: "next start",
  118. lint: "next lint",
  119. },
  120. dependencies: {
  121. next: "^14.2.0",
  122. react: "^18.3.0",
  123. "react-dom": "^18.3.0",
  124. },
  125. devDependencies: {
  126. "@types/node": "^20.0.0",
  127. "@types/react": "^18.3.0",
  128. "@types/react-dom": "^18.3.0",
  129. typescript: "^5.4.0",
  130. },
  131. },
  132. null,
  133. 2,
  134. );
  135. }
  136. function generateGlobalsCss(): string {
  137. return `* {
  138. box-sizing: border-box;
  139. }
  140. :root {
  141. --background: #000;
  142. --foreground: #fafafa;
  143. --card: #0a0a0a;
  144. --border: #262626;
  145. --muted: #a3a3a3;
  146. --radius: 8px;
  147. }
  148. html,
  149. body {
  150. margin: 0;
  151. padding: 0;
  152. font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  153. background-color: var(--background);
  154. color: var(--foreground);
  155. -webkit-font-smoothing: antialiased;
  156. }
  157. button {
  158. font-family: inherit;
  159. cursor: pointer;
  160. }
  161. input,
  162. select,
  163. textarea {
  164. font-family: inherit;
  165. }
  166. ::selection {
  167. background: var(--foreground);
  168. color: var(--background);
  169. }
  170. `;
  171. }
  172. function generateLayout(projectName: string): string {
  173. return `import type { Metadata } from "next";
  174. import "./globals.css";
  175. export const metadata: Metadata = {
  176. title: "${projectName}",
  177. description: "Generated dashboard",
  178. };
  179. export default function RootLayout({
  180. children,
  181. }: {
  182. children: React.ReactNode;
  183. }) {
  184. return (
  185. <html lang="en">
  186. <body>{children}</body>
  187. </html>
  188. );
  189. }
  190. `;
  191. }
  192. function generateComponentIndex(components: Set<string>): string {
  193. const exports = Array.from(components)
  194. .sort()
  195. .map((name) => `export { ${name} } from "./${name.toLowerCase()}";`)
  196. .join("\n");
  197. return exports + "\n";
  198. }
  199. function generateMainPage(
  200. tree: UITree,
  201. components: Set<string>,
  202. data: Record<string, unknown>,
  203. ): string {
  204. const imports = Array.from(components).sort().join(", ");
  205. const jsx = generateJSX(tree, tree.root, 4);
  206. const dataStr = JSON.stringify(data, null, 2).replace(/\n/g, "\n ");
  207. return `"use client";
  208. import { ${imports} } from "@/components/ui";
  209. const data = ${dataStr};
  210. export default function Page() {
  211. return (
  212. <div style={{ maxWidth: 960, margin: "0 auto", padding: "48px 24px" }}>
  213. ${jsx}
  214. </div>
  215. );
  216. }
  217. `;
  218. }
  219. function generateJSX(tree: UITree, key: string, indent: number): string {
  220. const element = tree.elements[key];
  221. if (!element) return "";
  222. const spaces = " ".repeat(indent);
  223. const componentName = element.type;
  224. // Filter out null/undefined props and convert data paths to data references
  225. const propsObj: Record<string, unknown> = {};
  226. for (const [k, v] of Object.entries(element.props)) {
  227. if (v === null || v === undefined) continue;
  228. // Convert *Path props to actual data values
  229. if (
  230. typeof v === "string" &&
  231. (k.endsWith("Path") || k === "bindPath" || k === "dataPath")
  232. ) {
  233. // Keep as a special marker for the component
  234. propsObj[k] = v;
  235. } else {
  236. propsObj[k] = v;
  237. }
  238. }
  239. // Add data prop for components that need it
  240. const needsData =
  241. Object.keys(propsObj).some(
  242. (k) => k.endsWith("Path") || k === "bindPath" || k === "dataPath",
  243. ) || ["Chart", "Table", "Metric", "List"].includes(componentName);
  244. const propsStr = serializeProps(propsObj);
  245. const dataAttr = needsData ? " data={data}" : "";
  246. const hasChildren = element.children && element.children.length > 0;
  247. if (!hasChildren) {
  248. if (propsStr || dataAttr) {
  249. return `${spaces}<${componentName}${dataAttr}${propsStr ? " " + propsStr : ""} />`;
  250. }
  251. return `${spaces}<${componentName} />`;
  252. }
  253. const lines: string[] = [];
  254. if (propsStr || dataAttr) {
  255. lines.push(
  256. `${spaces}<${componentName}${dataAttr}${propsStr ? " " + propsStr : ""}>`,
  257. );
  258. } else {
  259. lines.push(`${spaces}<${componentName}>`);
  260. }
  261. for (const childKey of element.children!) {
  262. lines.push(generateJSX(tree, childKey, indent + 2));
  263. }
  264. lines.push(`${spaces}</${componentName}>`);
  265. return lines.join("\n");
  266. }
  267. function generateReadme(projectName: string): string {
  268. return `# ${projectName}
  269. This dashboard was generated from a json-render UI tree.
  270. ## Getting Started
  271. \`\`\`bash
  272. npm install
  273. npm run dev
  274. \`\`\`
  275. Open [http://localhost:3000](http://localhost:3000) to view the dashboard.
  276. ## Project Structure
  277. - \`app/page.tsx\` - Main dashboard page
  278. - \`components/ui/\` - UI components
  279. - \`app/globals.css\` - Global styles
  280. `;
  281. }