@object-ui/cli 0.3.0

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.
@@ -0,0 +1,1272 @@
1
+ // src/commands/serve.ts
2
+ import { createServer } from "vite";
3
+ import react from "@vitejs/plugin-react";
4
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
5
+ import { join as join2, resolve, relative } from "path";
6
+ import chalk2 from "chalk";
7
+ import { execSync } from "child_process";
8
+
9
+ // src/utils/app-generator.ts
10
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync } from "fs";
11
+ import { join } from "path";
12
+ import chalk from "chalk";
13
+ import * as yaml from "js-yaml";
14
+ function isSupportedSchemaFile(filename) {
15
+ return filename.endsWith(".json") || filename.endsWith(".yml") || filename.endsWith(".yaml");
16
+ }
17
+ function getBaseFileName(filename) {
18
+ return filename.replace(/\.(json|yml|yaml)$/, "");
19
+ }
20
+ function parseSchemaFile(filePath) {
21
+ const content = readFileSync(filePath, "utf-8");
22
+ if (filePath.endsWith(".json")) {
23
+ return JSON.parse(content);
24
+ } else if (filePath.endsWith(".yml") || filePath.endsWith(".yaml")) {
25
+ return yaml.load(content);
26
+ }
27
+ throw new Error(`Unsupported file format: ${filePath}`);
28
+ }
29
+ function scanPagesDirectory(pagesDir) {
30
+ const routes = [];
31
+ const scanDir = (dir, routePrefix = "") => {
32
+ const entries = readdirSync(dir);
33
+ for (const entry of entries) {
34
+ const fullPath = join(dir, entry);
35
+ const stat = statSync(fullPath);
36
+ if (stat.isDirectory()) {
37
+ const newPrefix = routePrefix + "/" + entry;
38
+ scanDir(fullPath, newPrefix);
39
+ } else if (isSupportedSchemaFile(entry)) {
40
+ const fileName = getBaseFileName(entry);
41
+ let routePath;
42
+ let isDynamic = false;
43
+ let paramName;
44
+ if (fileName === "index") {
45
+ routePath = routePrefix || "/";
46
+ } else if (fileName.startsWith("[") && fileName.endsWith("]")) {
47
+ paramName = fileName.slice(1, -1);
48
+ routePath = routePrefix + "/:" + paramName;
49
+ isDynamic = true;
50
+ } else {
51
+ routePath = routePrefix + "/" + fileName;
52
+ }
53
+ try {
54
+ const schema = parseSchemaFile(fullPath);
55
+ routes.push({
56
+ path: routePath,
57
+ filePath: fullPath,
58
+ schema,
59
+ isDynamic,
60
+ paramName
61
+ });
62
+ } catch (error) {
63
+ console.warn(chalk.yellow(`\u26A0 Warning: Failed to parse ${fullPath}: ${error instanceof Error ? error.message : error}`));
64
+ }
65
+ }
66
+ }
67
+ };
68
+ scanDir(pagesDir);
69
+ routes.sort((a, b) => {
70
+ if (a.isDynamic && !b.isDynamic) return 1;
71
+ if (!a.isDynamic && b.isDynamic) return -1;
72
+ return a.path.localeCompare(b.path);
73
+ });
74
+ return routes;
75
+ }
76
+ function createTempApp(tmpDir, schema) {
77
+ const html = `<!DOCTYPE html>
78
+ <html lang="en">
79
+ <head>
80
+ <meta charset="UTF-8" />
81
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
82
+ <title>Object UI App</title>
83
+ </head>
84
+ <body>
85
+ <div id="root"></div>
86
+ <script type="module" src="/src/main.tsx"></script>
87
+ </body>
88
+ </html>`;
89
+ writeFileSync(join(tmpDir, "index.html"), html);
90
+ const srcDir = join(tmpDir, "src");
91
+ mkdirSync(srcDir, { recursive: true });
92
+ const mainTsx = `import React from 'react';
93
+ import ReactDOM from 'react-dom/client';
94
+ import App from './App';
95
+ import './index.css';
96
+
97
+ ReactDOM.createRoot(document.getElementById('root')!).render(
98
+ <React.StrictMode>
99
+ <App />
100
+ </React.StrictMode>
101
+ );`;
102
+ writeFileSync(join(srcDir, "main.tsx"), mainTsx);
103
+ const appTsx = `import { SchemaRenderer } from '@object-ui/react';
104
+ import '@object-ui/components';
105
+
106
+ const schema = ${JSON.stringify(schema, null, 2)};
107
+
108
+ function App() {
109
+ return <SchemaRenderer schema={schema} />;
110
+ }
111
+
112
+ export default App;`;
113
+ writeFileSync(join(srcDir, "App.tsx"), appTsx);
114
+ const indexCss = `@tailwind base;
115
+ @tailwind components;
116
+ @tailwind utilities;
117
+
118
+ @layer base {
119
+ :root {
120
+ --background: 0 0% 100%;
121
+ --foreground: 222.2 84% 4.9%;
122
+ --card: 0 0% 100%;
123
+ --card-foreground: 222.2 84% 4.9%;
124
+ --popover: 0 0% 100%;
125
+ --popover-foreground: 222.2 84% 4.9%;
126
+ --primary: 222.2 47.4% 11.2%;
127
+ --primary-foreground: 210 40% 98%;
128
+ --secondary: 210 40% 96.1%;
129
+ --secondary-foreground: 222.2 47.4% 11.2%;
130
+ --muted: 210 40% 96.1%;
131
+ --muted-foreground: 215.4 16.3% 46.9%;
132
+ --accent: 210 40% 96.1%;
133
+ --accent-foreground: 222.2 47.4% 11.2%;
134
+ --destructive: 0 84.2% 60.2%;
135
+ --destructive-foreground: 210 40% 98%;
136
+ --border: 214.3 31.8% 91.4%;
137
+ --input: 214.3 31.8% 91.4%;
138
+ --ring: 222.2 84% 4.9%;
139
+ --radius: 0.5rem;
140
+ --chart-1: 12 76% 61%;
141
+ --chart-2: 173 58% 39%;
142
+ --chart-3: 197 37% 24%;
143
+ --chart-4: 43 74% 66%;
144
+ --chart-5: 27 87% 67%;
145
+ }
146
+
147
+ .dark {
148
+ --background: 222.2 84% 4.9%;
149
+ --foreground: 210 40% 98%;
150
+ --card: 222.2 84% 4.9%;
151
+ --card-foreground: 210 40% 98%;
152
+ --popover: 222.2 84% 4.9%;
153
+ --popover-foreground: 210 40% 98%;
154
+ --primary: 210 40% 98%;
155
+ --primary-foreground: 222.2 47.4% 11.2%;
156
+ --secondary: 217.2 32.6% 17.5%;
157
+ --secondary-foreground: 210 40% 98%;
158
+ --muted: 217.2 32.6% 17.5%;
159
+ --muted-foreground: 215 20.2% 65.1%;
160
+ --accent: 217.2 32.6% 17.5%;
161
+ --accent-foreground: 210 40% 98%;
162
+ --destructive: 0 62.8% 30.6%;
163
+ --destructive-foreground: 210 40% 98%;
164
+ --border: 217.2 32.6% 17.5%;
165
+ --input: 217.2 32.6% 17.5%;
166
+ --ring: 212.7 26.8% 83.9%;
167
+ --chart-1: 220 70% 50%;
168
+ --chart-2: 160 60% 45%;
169
+ --chart-3: 30 80% 55%;
170
+ --chart-4: 280 65% 60%;
171
+ --chart-5: 340 75% 55%;
172
+ }
173
+ }
174
+
175
+ @layer base {
176
+ * {
177
+ @apply border-border;
178
+ }
179
+ body {
180
+ @apply bg-background text-foreground;
181
+ }
182
+ }`;
183
+ writeFileSync(join(srcDir, "index.css"), indexCss);
184
+ const tailwindConfig = `/** @type {import('tailwindcss').Config} */
185
+ export default {
186
+ darkMode: ['class'],
187
+ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
188
+ theme: {
189
+ extend: {
190
+ borderRadius: {
191
+ lg: 'var(--radius)',
192
+ md: 'calc(var(--radius) - 2px)',
193
+ sm: 'calc(var(--radius) - 4px)',
194
+ },
195
+ colors: {
196
+ background: 'hsl(var(--background))',
197
+ foreground: 'hsl(var(--foreground))',
198
+ card: {
199
+ DEFAULT: 'hsl(var(--card))',
200
+ foreground: 'hsl(var(--card-foreground))',
201
+ },
202
+ popover: {
203
+ DEFAULT: 'hsl(var(--popover))',
204
+ foreground: 'hsl(var(--popover-foreground))',
205
+ },
206
+ primary: {
207
+ DEFAULT: 'hsl(var(--primary))',
208
+ foreground: 'hsl(var(--primary-foreground))',
209
+ },
210
+ secondary: {
211
+ DEFAULT: 'hsl(var(--secondary))',
212
+ foreground: 'hsl(var(--secondary-foreground))',
213
+ },
214
+ muted: {
215
+ DEFAULT: 'hsl(var(--muted))',
216
+ foreground: 'hsl(var(--muted-foreground))',
217
+ },
218
+ accent: {
219
+ DEFAULT: 'hsl(var(--accent))',
220
+ foreground: 'hsl(var(--accent-foreground))',
221
+ },
222
+ destructive: {
223
+ DEFAULT: 'hsl(var(--destructive))',
224
+ foreground: 'hsl(var(--destructive-foreground))',
225
+ },
226
+ border: 'hsl(var(--border))',
227
+ input: 'hsl(var(--input))',
228
+ ring: 'hsl(var(--ring))',
229
+ chart: {
230
+ 1: 'hsl(var(--chart-1))',
231
+ 2: 'hsl(var(--chart-2))',
232
+ 3: 'hsl(var(--chart-3))',
233
+ 4: 'hsl(var(--chart-4))',
234
+ 5: 'hsl(var(--chart-5))',
235
+ },
236
+ },
237
+ },
238
+ },
239
+ plugins: [],
240
+ };`;
241
+ const cwd = process.cwd();
242
+ const isMonorepo = existsSync(join(cwd, "pnpm-workspace.yaml"));
243
+ const contentPaths = ["'./index.html'", "'./src/**/*.{js,ts,jsx,tsx,json}'"];
244
+ if (isMonorepo) {
245
+ const componentsPath = join(cwd, "packages/components/src/**/*.{ts,tsx}");
246
+ const pluginsPath = join(cwd, "packages/plugin-*/src/**/*.{ts,tsx}");
247
+ contentPaths.push(`'${componentsPath}'`);
248
+ contentPaths.push(`'${pluginsPath}'`);
249
+ }
250
+ const finalTailwindConfig = `/** @type {import('tailwindcss').Config} */
251
+ export default {
252
+ darkMode: ['class'],
253
+ content: [${contentPaths.join(", ")}],
254
+ theme: {
255
+ extend: {
256
+ borderRadius: {
257
+ lg: 'var(--radius)',
258
+ md: 'calc(var(--radius) - 2px)',
259
+ sm: 'calc(var(--radius) - 4px)',
260
+ },
261
+ colors: {
262
+ background: 'hsl(var(--background))',
263
+ foreground: 'hsl(var(--foreground))',
264
+ card: {
265
+ DEFAULT: 'hsl(var(--card))',
266
+ foreground: 'hsl(var(--card-foreground))',
267
+ },
268
+ popover: {
269
+ DEFAULT: 'hsl(var(--popover))',
270
+ foreground: 'hsl(var(--popover-foreground))',
271
+ },
272
+ primary: {
273
+ DEFAULT: 'hsl(var(--primary))',
274
+ foreground: 'hsl(var(--primary-foreground))',
275
+ },
276
+ secondary: {
277
+ DEFAULT: 'hsl(var(--secondary))',
278
+ foreground: 'hsl(var(--secondary-foreground))',
279
+ },
280
+ muted: {
281
+ DEFAULT: 'hsl(var(--muted))',
282
+ foreground: 'hsl(var(--muted-foreground))',
283
+ },
284
+ accent: {
285
+ DEFAULT: 'hsl(var(--accent))',
286
+ foreground: 'hsl(var(--accent-foreground))',
287
+ },
288
+ destructive: {
289
+ DEFAULT: 'hsl(var(--destructive))',
290
+ foreground: 'hsl(var(--destructive-foreground))',
291
+ },
292
+ border: 'hsl(var(--border))',
293
+ input: 'hsl(var(--input))',
294
+ ring: 'hsl(var(--ring))',
295
+ chart: {
296
+ 1: 'hsl(var(--chart-1))',
297
+ 2: 'hsl(var(--chart-2))',
298
+ 3: 'hsl(var(--chart-3))',
299
+ 4: 'hsl(var(--chart-4))',
300
+ 5: 'hsl(var(--chart-5))',
301
+ },
302
+ },
303
+ },
304
+ },
305
+ plugins: [],
306
+ };`;
307
+ writeFileSync(join(tmpDir, "tailwind.config.js"), finalTailwindConfig);
308
+ const finalPostcssConfig = `export default {
309
+ plugins: {
310
+ tailwindcss: {},
311
+ autoprefixer: {},
312
+ },
313
+ };`;
314
+ writeFileSync(join(tmpDir, "postcss.config.js"), finalPostcssConfig);
315
+ const baseDependencies = {
316
+ react: "^18.3.1",
317
+ "react-dom": "^18.3.1",
318
+ "@object-ui/react": "^0.1.0",
319
+ "@object-ui/components": "^0.1.0"
320
+ };
321
+ const baseDevDependencies = {
322
+ "@types/react": "^18.3.12",
323
+ "@types/react-dom": "^18.3.1",
324
+ "@vitejs/plugin-react": "^4.2.1",
325
+ autoprefixer: "^10.4.23",
326
+ postcss: "^8.5.6",
327
+ tailwindcss: "^3.4.19",
328
+ typescript: "~5.7.3",
329
+ vite: "^5.0.0"
330
+ };
331
+ const packageJson = {
332
+ name: "objectui-temp-app",
333
+ private: true,
334
+ type: "module",
335
+ // In monorepo, we use root node_modules, so we don't need dependencies here
336
+ dependencies: isMonorepo ? {} : baseDependencies,
337
+ devDependencies: isMonorepo ? {} : baseDevDependencies
338
+ };
339
+ writeFileSync(join(tmpDir, "package.json"), JSON.stringify(packageJson, null, 2));
340
+ const tsconfig = {
341
+ compilerOptions: {
342
+ target: "ES2020",
343
+ useDefineForClassFields: true,
344
+ lib: ["ES2020", "DOM", "DOM.Iterable"],
345
+ module: "ESNext",
346
+ skipLibCheck: true,
347
+ moduleResolution: "bundler",
348
+ allowImportingTsExtensions: true,
349
+ resolveJsonModule: true,
350
+ isolatedModules: true,
351
+ noEmit: true,
352
+ jsx: "react-jsx",
353
+ strict: true,
354
+ noUnusedLocals: true,
355
+ noUnusedParameters: true,
356
+ noFallthroughCasesInSwitch: true
357
+ },
358
+ include: ["src"]
359
+ };
360
+ writeFileSync(join(tmpDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
361
+ }
362
+ function createTempAppWithRouting(tmpDir, routes, appConfig) {
363
+ const html = `<!DOCTYPE html>
364
+ <html lang="en">
365
+ <head>
366
+ <meta charset="UTF-8" />
367
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
368
+ <title>${appConfig?.title || "Object UI App"}</title>
369
+ </head>
370
+ <body>
371
+ <div id="root"></div>
372
+ <script type="module" src="/src/main.tsx"></script>
373
+ </body>
374
+ </html>`;
375
+ writeFileSync(join(tmpDir, "index.html"), html);
376
+ const srcDir = join(tmpDir, "src");
377
+ mkdirSync(srcDir, { recursive: true });
378
+ const schemasDir = join(srcDir, "schemas");
379
+ mkdirSync(schemasDir, { recursive: true });
380
+ const schemaImports = [];
381
+ const routeComponents = [];
382
+ routes.forEach((route, index) => {
383
+ const schemaVarName = `schema${index}`;
384
+ const schemaFileName = `page${index}.json`;
385
+ writeFileSync(
386
+ join(schemasDir, schemaFileName),
387
+ JSON.stringify(route.schema, null, 2)
388
+ );
389
+ schemaImports.push(`import ${schemaVarName} from './schemas/${schemaFileName}';`);
390
+ routeComponents.push(` <Route path="${route.path}" element={<SchemaRenderer schema={${schemaVarName}} />} />`);
391
+ });
392
+ const mainTsx = `import React from 'react';
393
+ import ReactDOM from 'react-dom/client';
394
+ import App from './App';
395
+ import './index.css';
396
+
397
+ ReactDOM.createRoot(document.getElementById('root')!).render(
398
+ <React.StrictMode>
399
+ <App />
400
+ </React.StrictMode>
401
+ );`;
402
+ writeFileSync(join(srcDir, "main.tsx"), mainTsx);
403
+ let layoutImport = "";
404
+ let layoutWrapperStart = "";
405
+ let layoutWrapperEnd = "";
406
+ if (appConfig) {
407
+ const layoutCode = `
408
+ import { Link, useLocation } from 'react-router-dom';
409
+ import * as LucideIcons from 'lucide-react';
410
+ import {
411
+ cn,
412
+ SidebarProvider,
413
+ Sidebar,
414
+ SidebarContent,
415
+ SidebarFooter,
416
+ SidebarHeader,
417
+ SidebarRail,
418
+ SidebarGroup,
419
+ SidebarGroupContent,
420
+ SidebarGroupLabel,
421
+ SidebarMenu,
422
+ SidebarMenuItem,
423
+ SidebarMenuButton,
424
+ SidebarMenuSub,
425
+ SidebarMenuSubItem,
426
+ SidebarMenuSubButton,
427
+ SidebarInset,
428
+ SidebarTrigger,
429
+ Separator,
430
+ Collapsible,
431
+ CollapsibleTrigger,
432
+ CollapsibleContent
433
+ } from '@object-ui/components';
434
+
435
+ const DynamicIcon = ({ name, className }) => {
436
+ // @ts-ignore
437
+ const Icon = LucideIcons[name];
438
+ if (!Icon) return null;
439
+ return <Icon className={className} />;
440
+ };
441
+
442
+ const AppLayout = ({ app, children }) => {
443
+ const location = useLocation();
444
+ const menu = app.menu || [];
445
+
446
+ return (
447
+ <SidebarProvider>
448
+ <Sidebar collapsible="icon">
449
+ <SidebarHeader>
450
+ <div className="flex items-center gap-2 px-2 py-2">
451
+ <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
452
+ {app.logo && <DynamicIcon name={app.logo} className="size-4" />}
453
+ {!app.logo && <span className="text-xl font-bold">O</span>}
454
+ </div>
455
+ <div className="grid flex-1 text-left text-sm leading-tight">
456
+ <span className="truncate font-semibold">{app.title || "Object UI"}</span>
457
+ <span className="truncate text-xs">Showcase</span>
458
+ </div>
459
+ </div>
460
+ </SidebarHeader>
461
+ <SidebarContent>
462
+ <SidebarGroup>
463
+ <SidebarMenu>
464
+ {menu.map((item, idx) => {
465
+ // Collapsible Item (Children present)
466
+ if (item.children && item.children.length > 0) {
467
+ const isActive = item.children.some(child => child.path === location.pathname);
468
+ return (
469
+ <Collapsible key={idx} asChild defaultOpen={isActive} className="group/collapsible">
470
+ <SidebarMenuItem>
471
+ <CollapsibleTrigger asChild>
472
+ <SidebarMenuButton tooltip={item.label}>
473
+ {item.icon && <DynamicIcon name={item.icon} />}
474
+ <span>{item.label}</span>
475
+ <DynamicIcon name="ChevronRight" className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
476
+ </SidebarMenuButton>
477
+ </CollapsibleTrigger>
478
+ <CollapsibleContent>
479
+ <SidebarMenuSub>
480
+ {item.children.map((child, cIdx) => (
481
+ <SidebarMenuSubItem key={cIdx}>
482
+ <SidebarMenuSubButton asChild isActive={location.pathname === child.path}>
483
+ <Link to={child.path || '#'}>
484
+ <span>{child.label}</span>
485
+ </Link>
486
+ </SidebarMenuSubButton>
487
+ </SidebarMenuSubItem>
488
+ ))}
489
+ </SidebarMenuSub>
490
+ </CollapsibleContent>
491
+ </SidebarMenuItem>
492
+ </Collapsible>
493
+ );
494
+ }
495
+
496
+ // Single Item
497
+ if (item.path) {
498
+ return (
499
+ <SidebarMenuItem key={idx}>
500
+ <SidebarMenuButton asChild isActive={location.pathname === item.path} tooltip={item.label}>
501
+ <Link to={item.path}>
502
+ {item.icon && <DynamicIcon name={item.icon} />}
503
+ <span>{item.label}</span>
504
+ </Link>
505
+ </SidebarMenuButton>
506
+ </SidebarMenuItem>
507
+ );
508
+ }
509
+ return null;
510
+ })}
511
+ </SidebarMenu>
512
+ </SidebarGroup>
513
+ </SidebarContent>
514
+ <SidebarFooter>
515
+ </SidebarFooter>
516
+ <SidebarRail />
517
+ </Sidebar>
518
+
519
+ <SidebarInset>
520
+ <header className="flex h-16 shrink-0 items-center gap-2 border-b bg-background px-4">
521
+ <SidebarTrigger className="-ml-1" />
522
+ <Separator orientation="vertical" className="mr-2 h-4" />
523
+ <div className="flex flex-1 items-center justify-between">
524
+ <span className="font-medium">{app.title}</span>
525
+ </div>
526
+ </header>
527
+ <div className="flex-1 flex flex-col min-h-0 bg-muted/20 p-4">
528
+ <div className="mx-auto max-w-full w-full">
529
+ {children}
530
+ </div>
531
+ </div>
532
+ </SidebarInset>
533
+ </SidebarProvider>
534
+ );
535
+ };
536
+
537
+ export default AppLayout;
538
+ `;
539
+ writeFileSync(join(srcDir, "Layout.tsx"), layoutCode);
540
+ layoutImport = `import AppLayout from './Layout';
541
+ const appConfig = ${JSON.stringify(appConfig)};`;
542
+ layoutWrapperStart = `<AppLayout app={appConfig}>`;
543
+ layoutWrapperEnd = `</AppLayout>`;
544
+ }
545
+ const appTsx = `import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
546
+ import { SchemaRenderer } from '@object-ui/react';
547
+ import '@object-ui/components';
548
+ ${schemaImports.join("\n")}
549
+ ${layoutImport}
550
+
551
+ function App() {
552
+ return (
553
+ <BrowserRouter>
554
+ ${layoutWrapperStart}
555
+ <Routes>
556
+ ${routeComponents.join("\n")}
557
+ </Routes>
558
+ ${layoutWrapperEnd}
559
+ </BrowserRouter>
560
+ );
561
+ }
562
+
563
+ export default App;`;
564
+ writeFileSync(join(srcDir, "App.tsx"), appTsx);
565
+ const indexCss = `@tailwind base;
566
+ @tailwind components;
567
+ @tailwind utilities;
568
+
569
+ @layer base {
570
+ :root {
571
+ --background: 0 0% 100%;
572
+ --foreground: 222.2 84% 4.9%;
573
+ --card: 0 0% 100%;
574
+ --card-foreground: 222.2 84% 4.9%;
575
+ --popover: 0 0% 100%;
576
+ --popover-foreground: 222.2 84% 4.9%;
577
+ --primary: 222.2 47.4% 11.2%;
578
+ --primary-foreground: 210 40% 98%;
579
+ --secondary: 210 40% 96.1%;
580
+ --secondary-foreground: 222.2 47.4% 11.2%;
581
+ --muted: 210 40% 96.1%;
582
+ --muted-foreground: 215.4 16.3% 46.9%;
583
+ --accent: 210 40% 96.1%;
584
+ --accent-foreground: 222.2 47.4% 11.2%;
585
+ --destructive: 0 84.2% 60.2%;
586
+ --destructive-foreground: 210 40% 98%;
587
+ --border: 214.3 31.8% 91.4%;
588
+ --input: 214.3 31.8% 91.4%;
589
+ --ring: 222.2 84% 4.9%;
590
+ --radius: 0.5rem;
591
+ --chart-1: 12 76% 61%;
592
+ --chart-2: 173 58% 39%;
593
+ --chart-3: 197 37% 24%;
594
+ --chart-4: 43 74% 66%;
595
+ --chart-5: 27 87% 67%;
596
+ }
597
+
598
+ .dark {
599
+ --background: 222.2 84% 4.9%;
600
+ --foreground: 210 40% 98%;
601
+ --card: 222.2 84% 4.9%;
602
+ --card-foreground: 210 40% 98%;
603
+ --popover: 222.2 84% 4.9%;
604
+ --popover-foreground: 210 40% 98%;
605
+ --primary: 210 40% 98%;
606
+ --primary-foreground: 222.2 47.4% 11.2%;
607
+ --secondary: 217.2 32.6% 17.5%;
608
+ --secondary-foreground: 210 40% 98%;
609
+ --muted: 217.2 32.6% 17.5%;
610
+ --muted-foreground: 215 20.2% 65.1%;
611
+ --accent: 217.2 32.6% 17.5%;
612
+ --accent-foreground: 210 40% 98%;
613
+ --destructive: 0 62.8% 30.6%;
614
+ --destructive-foreground: 210 40% 98%;
615
+ --border: 217.2 32.6% 17.5%;
616
+ --input: 217.2 32.6% 17.5%;
617
+ --ring: 212.7 26.8% 83.9%;
618
+ --chart-1: 220 70% 50%;
619
+ --chart-2: 160 60% 45%;
620
+ --chart-3: 30 80% 55%;
621
+ --chart-4: 280 65% 60%;
622
+ --chart-5: 340 75% 55%;
623
+ }
624
+ }
625
+
626
+ @layer base {
627
+ * {
628
+ @apply border-border;
629
+ }
630
+ body {
631
+ @apply bg-background text-foreground font-sans antialiased min-h-screen;
632
+ }
633
+ }`;
634
+ writeFileSync(join(srcDir, "index.css"), indexCss);
635
+ const cwd = process.cwd();
636
+ const isMonorepo = existsSync(join(cwd, "pnpm-workspace.yaml"));
637
+ const contentPaths = ["'./index.html'", "'./src/**/*.{js,ts,jsx,tsx,json}'"];
638
+ if (isMonorepo) {
639
+ const componentsPath = join(cwd, "packages/components/src/**/*.{ts,tsx}");
640
+ const pluginsPath = join(cwd, "packages/plugin-*/src/**/*.{ts,tsx}");
641
+ contentPaths.push(`'${componentsPath}'`);
642
+ contentPaths.push(`'${pluginsPath}'`);
643
+ }
644
+ const tailwindConfig = `/** @type {import('tailwindcss').Config} */
645
+ export default {
646
+ darkMode: ['class'],
647
+ content: [${contentPaths.join(", ")}],
648
+ theme: {
649
+ extend: {
650
+ borderRadius: {
651
+ lg: 'var(--radius)',
652
+ md: 'calc(var(--radius) - 2px)',
653
+ sm: 'calc(var(--radius) - 4px)',
654
+ },
655
+ colors: {
656
+ background: 'hsl(var(--background))',
657
+ foreground: 'hsl(var(--foreground))',
658
+ card: {
659
+ DEFAULT: 'hsl(var(--card))',
660
+ foreground: 'hsl(var(--card-foreground))',
661
+ },
662
+ popover: {
663
+ DEFAULT: 'hsl(var(--popover))',
664
+ foreground: 'hsl(var(--popover-foreground))',
665
+ },
666
+ primary: {
667
+ DEFAULT: 'hsl(var(--primary))',
668
+ foreground: 'hsl(var(--primary-foreground))',
669
+ },
670
+ secondary: {
671
+ DEFAULT: 'hsl(var(--secondary))',
672
+ foreground: 'hsl(var(--secondary-foreground))',
673
+ },
674
+ muted: {
675
+ DEFAULT: 'hsl(var(--muted))',
676
+ foreground: 'hsl(var(--muted-foreground))',
677
+ },
678
+ accent: {
679
+ DEFAULT: 'hsl(var(--accent))',
680
+ foreground: 'hsl(var(--accent-foreground))',
681
+ },
682
+ destructive: {
683
+ DEFAULT: 'hsl(var(--destructive))',
684
+ foreground: 'hsl(var(--destructive-foreground))',
685
+ },
686
+ border: 'hsl(var(--border))',
687
+ input: 'hsl(var(--input))',
688
+ ring: 'hsl(var(--ring))',
689
+ chart: {
690
+ 1: 'hsl(var(--chart-1))',
691
+ 2: 'hsl(var(--chart-2))',
692
+ 3: 'hsl(var(--chart-3))',
693
+ 4: 'hsl(var(--chart-4))',
694
+ 5: 'hsl(var(--chart-5))',
695
+ },
696
+ },
697
+ },
698
+ },
699
+ plugins: [],
700
+ };`;
701
+ writeFileSync(join(tmpDir, "tailwind.config.js"), tailwindConfig);
702
+ const postcssConfig = `export default {
703
+ plugins: {
704
+ tailwindcss: {},
705
+ autoprefixer: {},
706
+ },
707
+ };`;
708
+ writeFileSync(join(tmpDir, "postcss.config.js"), postcssConfig);
709
+ const packageJson = {
710
+ name: "objectui-temp-app",
711
+ private: true,
712
+ type: "module",
713
+ dependencies: {
714
+ react: "^18.3.1",
715
+ "react-dom": "^18.3.1",
716
+ "react-router-dom": "^7.12.0",
717
+ "@object-ui/react": "^0.1.0",
718
+ "@object-ui/components": "^0.1.0"
719
+ },
720
+ devDependencies: {
721
+ "@types/react": "^18.3.12",
722
+ "@types/react-dom": "^18.3.1",
723
+ "@vitejs/plugin-react": "^4.2.1",
724
+ autoprefixer: "^10.4.23",
725
+ postcss: "^8.5.6",
726
+ tailwindcss: "^3.4.19",
727
+ typescript: "~5.7.3",
728
+ vite: "^5.0.0"
729
+ }
730
+ };
731
+ writeFileSync(join(tmpDir, "package.json"), JSON.stringify(packageJson, null, 2));
732
+ const tsconfig = {
733
+ compilerOptions: {
734
+ target: "ES2020",
735
+ useDefineForClassFields: true,
736
+ lib: ["ES2020", "DOM", "DOM.Iterable"],
737
+ module: "ESNext",
738
+ skipLibCheck: true,
739
+ moduleResolution: "bundler",
740
+ allowImportingTsExtensions: true,
741
+ resolveJsonModule: true,
742
+ isolatedModules: true,
743
+ noEmit: true,
744
+ jsx: "react-jsx",
745
+ strict: true,
746
+ noUnusedLocals: true,
747
+ noUnusedParameters: true,
748
+ noFallthroughCasesInSwitch: true
749
+ },
750
+ include: ["src"]
751
+ };
752
+ writeFileSync(join(tmpDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
753
+ }
754
+
755
+ // src/commands/serve.ts
756
+ async function serve(schemaPath, options) {
757
+ const cwd = process.cwd();
758
+ const pagesDir = join2(cwd, "pages");
759
+ const hasPagesDir = existsSync2(pagesDir);
760
+ let routes = [];
761
+ let schema = null;
762
+ let useFileSystemRouting = false;
763
+ if (hasPagesDir) {
764
+ console.log(chalk2.blue("\u{1F4C1} Detected pages/ directory - using file-system routing"));
765
+ routes = scanPagesDirectory(pagesDir);
766
+ useFileSystemRouting = true;
767
+ if (routes.length === 0) {
768
+ throw new Error("No schema files found in pages/ directory");
769
+ }
770
+ console.log(chalk2.green(`\u2713 Found ${routes.length} route(s)`));
771
+ routes.forEach((route) => {
772
+ console.log(chalk2.dim(` ${route.path} \u2192 ${relative(cwd, route.filePath)}`));
773
+ });
774
+ } else {
775
+ const fullSchemaPath = resolve(cwd, schemaPath);
776
+ if (!existsSync2(fullSchemaPath)) {
777
+ throw new Error(`Schema file not found: ${schemaPath}
778
+ Run 'objectui init' to create a sample schema.`);
779
+ }
780
+ console.log(chalk2.blue("\u{1F4CB} Loading schema:"), chalk2.cyan(schemaPath));
781
+ try {
782
+ schema = parseSchemaFile(fullSchemaPath);
783
+ } catch (error) {
784
+ throw new Error(`Invalid schema file: ${error instanceof Error ? error.message : error}`);
785
+ }
786
+ }
787
+ const tmpDir = join2(cwd, ".objectui-tmp");
788
+ mkdirSync2(tmpDir, { recursive: true });
789
+ if (useFileSystemRouting) {
790
+ createTempAppWithRouting(tmpDir, routes);
791
+ } else {
792
+ createTempApp(tmpDir, schema);
793
+ }
794
+ console.log(chalk2.blue("\u{1F4E6} Installing dependencies..."));
795
+ console.log(chalk2.dim(" This may take a moment on first run..."));
796
+ try {
797
+ execSync("npm install --silent --prefer-offline", {
798
+ cwd: tmpDir,
799
+ stdio: "inherit"
800
+ });
801
+ console.log(chalk2.green("\u2713 Dependencies installed"));
802
+ } catch {
803
+ throw new Error("Failed to install dependencies. Please check your internet connection and try again.");
804
+ }
805
+ console.log(chalk2.green("\u2713 Schema loaded successfully"));
806
+ console.log(chalk2.blue("\u{1F680} Starting development server...\n"));
807
+ const viteConfig = {
808
+ root: tmpDir,
809
+ server: {
810
+ port: parseInt(options.port),
811
+ host: options.host,
812
+ open: true
813
+ },
814
+ plugins: [react()]
815
+ };
816
+ const server = await createServer(viteConfig);
817
+ await server.listen();
818
+ const { port, host } = server.config.server;
819
+ const protocol = server.config.server.https ? "https" : "http";
820
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
821
+ console.log();
822
+ console.log(chalk2.green("\u2713 Server started successfully!"));
823
+ console.log();
824
+ console.log(chalk2.bold(" Local: ") + chalk2.cyan(`${protocol}://${displayHost}:${port}`));
825
+ console.log();
826
+ console.log(chalk2.dim(" Press Ctrl+C to stop the server"));
827
+ console.log();
828
+ }
829
+
830
+ // src/commands/init.ts
831
+ import { existsSync as existsSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
832
+ import { join as join3 } from "path";
833
+ import chalk3 from "chalk";
834
+ var templates = {
835
+ simple: {
836
+ type: "div",
837
+ className: "min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100",
838
+ body: {
839
+ type: "card",
840
+ className: "w-full max-w-md shadow-lg",
841
+ title: "Welcome to Object UI",
842
+ description: "Start building your application with JSON schemas",
843
+ body: {
844
+ type: "div",
845
+ className: "p-6 space-y-4",
846
+ body: [
847
+ {
848
+ type: "text",
849
+ content: "This is a simple example. Edit app.json to customize your application.",
850
+ className: "text-sm text-muted-foreground"
851
+ },
852
+ {
853
+ type: "button",
854
+ label: "Get Started",
855
+ className: "w-full"
856
+ }
857
+ ]
858
+ }
859
+ }
860
+ },
861
+ form: {
862
+ type: "div",
863
+ className: "min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-50 to-pink-100 p-4",
864
+ body: {
865
+ type: "card",
866
+ className: "w-full max-w-2xl shadow-xl",
867
+ title: "Contact Form",
868
+ description: "Fill out the form below to get in touch",
869
+ body: {
870
+ type: "div",
871
+ className: "p-6 space-y-6",
872
+ body: [
873
+ {
874
+ type: "div",
875
+ className: "grid grid-cols-2 gap-4",
876
+ body: [
877
+ {
878
+ type: "input",
879
+ label: "First Name",
880
+ placeholder: "John",
881
+ required: true
882
+ },
883
+ {
884
+ type: "input",
885
+ label: "Last Name",
886
+ placeholder: "Doe",
887
+ required: true
888
+ }
889
+ ]
890
+ },
891
+ {
892
+ type: "input",
893
+ label: "Email Address",
894
+ inputType: "email",
895
+ placeholder: "john.doe@example.com",
896
+ required: true
897
+ },
898
+ {
899
+ type: "input",
900
+ label: "Phone Number",
901
+ inputType: "tel",
902
+ placeholder: "+1 (555) 000-0000"
903
+ },
904
+ {
905
+ type: "textarea",
906
+ label: "Message",
907
+ placeholder: "Tell us what you need...",
908
+ rows: 4
909
+ },
910
+ {
911
+ type: "div",
912
+ className: "flex gap-3",
913
+ body: [
914
+ {
915
+ type: "button",
916
+ label: "Submit",
917
+ className: "flex-1"
918
+ },
919
+ {
920
+ type: "button",
921
+ label: "Reset",
922
+ variant: "outline",
923
+ className: "flex-1"
924
+ }
925
+ ]
926
+ }
927
+ ]
928
+ }
929
+ }
930
+ },
931
+ dashboard: {
932
+ type: "div",
933
+ className: "min-h-screen bg-muted/10",
934
+ body: [
935
+ {
936
+ type: "div",
937
+ className: "border-b bg-background",
938
+ body: {
939
+ type: "div",
940
+ className: "container mx-auto px-6 py-4",
941
+ body: {
942
+ type: "div",
943
+ className: "flex items-center justify-between",
944
+ body: [
945
+ {
946
+ type: "div",
947
+ className: "text-2xl font-bold",
948
+ body: { type: "text", content: "Dashboard" }
949
+ },
950
+ {
951
+ type: "button",
952
+ label: "New Item",
953
+ size: "sm"
954
+ }
955
+ ]
956
+ }
957
+ }
958
+ },
959
+ {
960
+ type: "div",
961
+ className: "container mx-auto p-6 space-y-6",
962
+ body: [
963
+ {
964
+ type: "div",
965
+ className: "grid gap-4 md:grid-cols-2 lg:grid-cols-4",
966
+ body: [
967
+ {
968
+ type: "card",
969
+ className: "shadow-sm",
970
+ body: [
971
+ {
972
+ type: "div",
973
+ className: "p-6 pb-2",
974
+ body: {
975
+ type: "div",
976
+ className: "text-sm font-medium text-muted-foreground",
977
+ body: { type: "text", content: "Total Revenue" }
978
+ }
979
+ },
980
+ {
981
+ type: "div",
982
+ className: "p-6 pt-0",
983
+ body: [
984
+ {
985
+ type: "div",
986
+ className: "text-2xl font-bold",
987
+ body: { type: "text", content: "$45,231.89" }
988
+ },
989
+ {
990
+ type: "div",
991
+ className: "text-xs text-muted-foreground mt-1",
992
+ body: { type: "text", content: "+20.1% from last month" }
993
+ }
994
+ ]
995
+ }
996
+ ]
997
+ },
998
+ {
999
+ type: "card",
1000
+ className: "shadow-sm",
1001
+ body: [
1002
+ {
1003
+ type: "div",
1004
+ className: "p-6 pb-2",
1005
+ body: {
1006
+ type: "div",
1007
+ className: "text-sm font-medium text-muted-foreground",
1008
+ body: { type: "text", content: "Active Users" }
1009
+ }
1010
+ },
1011
+ {
1012
+ type: "div",
1013
+ className: "p-6 pt-0",
1014
+ body: [
1015
+ {
1016
+ type: "div",
1017
+ className: "text-2xl font-bold",
1018
+ body: { type: "text", content: "+2,350" }
1019
+ },
1020
+ {
1021
+ type: "div",
1022
+ className: "text-xs text-muted-foreground mt-1",
1023
+ body: { type: "text", content: "+180.1% from last month" }
1024
+ }
1025
+ ]
1026
+ }
1027
+ ]
1028
+ },
1029
+ {
1030
+ type: "card",
1031
+ className: "shadow-sm",
1032
+ body: [
1033
+ {
1034
+ type: "div",
1035
+ className: "p-6 pb-2",
1036
+ body: {
1037
+ type: "div",
1038
+ className: "text-sm font-medium text-muted-foreground",
1039
+ body: { type: "text", content: "Sales" }
1040
+ }
1041
+ },
1042
+ {
1043
+ type: "div",
1044
+ className: "p-6 pt-0",
1045
+ body: [
1046
+ {
1047
+ type: "div",
1048
+ className: "text-2xl font-bold",
1049
+ body: { type: "text", content: "+12,234" }
1050
+ },
1051
+ {
1052
+ type: "div",
1053
+ className: "text-xs text-muted-foreground mt-1",
1054
+ body: { type: "text", content: "+19% from last month" }
1055
+ }
1056
+ ]
1057
+ }
1058
+ ]
1059
+ },
1060
+ {
1061
+ type: "card",
1062
+ className: "shadow-sm",
1063
+ body: [
1064
+ {
1065
+ type: "div",
1066
+ className: "p-6 pb-2",
1067
+ body: {
1068
+ type: "div",
1069
+ className: "text-sm font-medium text-muted-foreground",
1070
+ body: { type: "text", content: "Active Now" }
1071
+ }
1072
+ },
1073
+ {
1074
+ type: "div",
1075
+ className: "p-6 pt-0",
1076
+ body: [
1077
+ {
1078
+ type: "div",
1079
+ className: "text-2xl font-bold",
1080
+ body: { type: "text", content: "+573" }
1081
+ },
1082
+ {
1083
+ type: "div",
1084
+ className: "text-xs text-muted-foreground mt-1",
1085
+ body: { type: "text", content: "+201 since last hour" }
1086
+ }
1087
+ ]
1088
+ }
1089
+ ]
1090
+ }
1091
+ ]
1092
+ },
1093
+ {
1094
+ type: "card",
1095
+ className: "shadow-sm",
1096
+ title: "Recent Activity",
1097
+ description: "Your latest updates and notifications",
1098
+ body: {
1099
+ type: "div",
1100
+ className: "p-6 pt-0 space-y-4",
1101
+ body: [
1102
+ {
1103
+ type: "div",
1104
+ className: "flex items-center gap-4 border-b pb-4",
1105
+ body: [
1106
+ {
1107
+ type: "div",
1108
+ className: "flex-1",
1109
+ body: [
1110
+ {
1111
+ type: "div",
1112
+ className: "font-medium",
1113
+ body: { type: "text", content: "New user registration" }
1114
+ },
1115
+ {
1116
+ type: "div",
1117
+ className: "text-sm text-muted-foreground",
1118
+ body: { type: "text", content: "2 minutes ago" }
1119
+ }
1120
+ ]
1121
+ }
1122
+ ]
1123
+ },
1124
+ {
1125
+ type: "div",
1126
+ className: "flex items-center gap-4 border-b pb-4",
1127
+ body: [
1128
+ {
1129
+ type: "div",
1130
+ className: "flex-1",
1131
+ body: [
1132
+ {
1133
+ type: "div",
1134
+ className: "font-medium",
1135
+ body: { type: "text", content: "Payment received" }
1136
+ },
1137
+ {
1138
+ type: "div",
1139
+ className: "text-sm text-muted-foreground",
1140
+ body: { type: "text", content: "15 minutes ago" }
1141
+ }
1142
+ ]
1143
+ }
1144
+ ]
1145
+ },
1146
+ {
1147
+ type: "div",
1148
+ className: "flex items-center gap-4",
1149
+ body: [
1150
+ {
1151
+ type: "div",
1152
+ className: "flex-1",
1153
+ body: [
1154
+ {
1155
+ type: "div",
1156
+ className: "font-medium",
1157
+ body: { type: "text", content: "New order placed" }
1158
+ },
1159
+ {
1160
+ type: "div",
1161
+ className: "text-sm text-muted-foreground",
1162
+ body: { type: "text", content: "1 hour ago" }
1163
+ }
1164
+ ]
1165
+ }
1166
+ ]
1167
+ }
1168
+ ]
1169
+ }
1170
+ }
1171
+ ]
1172
+ }
1173
+ ]
1174
+ }
1175
+ };
1176
+ async function init(name, options) {
1177
+ const cwd = process.cwd();
1178
+ const projectDir = join3(cwd, name);
1179
+ if (existsSync3(projectDir) && name !== ".") {
1180
+ throw new Error(`Directory "${name}" already exists. Please choose a different name.`);
1181
+ }
1182
+ const targetDir = name === "." ? cwd : projectDir;
1183
+ if (name !== ".") {
1184
+ mkdirSync3(projectDir, { recursive: true });
1185
+ }
1186
+ console.log(chalk3.blue("\u{1F3A8} Creating Object UI application..."));
1187
+ console.log(chalk3.dim(` Template: ${options.template}`));
1188
+ console.log();
1189
+ const template = templates[options.template];
1190
+ if (!template) {
1191
+ throw new Error(
1192
+ `Unknown template: ${options.template}
1193
+ Available templates: ${Object.keys(templates).join(", ")}`
1194
+ );
1195
+ }
1196
+ const schemaPath = join3(targetDir, "app.json");
1197
+ writeFileSync2(schemaPath, JSON.stringify(template, null, 2));
1198
+ console.log(chalk3.green("\u2713 Created app.json"));
1199
+ const readme = `# ${name}
1200
+
1201
+ A Object UI application built from JSON schemas.
1202
+
1203
+ ## Getting Started
1204
+
1205
+ 1. Install Object UI CLI globally (if you haven't already):
1206
+ \`\`\`bash
1207
+ npm install -g @object-ui/cli
1208
+ \`\`\`
1209
+
1210
+ 2. Start the development server:
1211
+ \`\`\`bash
1212
+ objectui serve app.json
1213
+ \`\`\`
1214
+
1215
+ 3. Open your browser and visit http://localhost:3000
1216
+
1217
+ ## Customize Your App
1218
+
1219
+ Edit \`app.json\` to customize your application. The dev server will automatically reload when you save changes.
1220
+
1221
+ ## Available Templates
1222
+
1223
+ - **simple**: A minimal getting started template
1224
+ - **form**: A contact form example
1225
+ - **dashboard**: A full dashboard with metrics and activity feed
1226
+
1227
+ ## Learn More
1228
+
1229
+ - [Object UI Documentation](https://www.objectui.org)
1230
+ - [Schema Reference](https://www.objectui.org/docs/protocol/overview)
1231
+ - [Component Library](https://www.objectui.org/docs/api/components)
1232
+
1233
+ ## Commands
1234
+
1235
+ - \`objectui serve [schema]\` - Start development server
1236
+ - \`objectui init [name]\` - Create a new application
1237
+
1238
+ Built with \u2764\uFE0F using [Object UI](https://www.objectui.org)
1239
+ `;
1240
+ writeFileSync2(join3(targetDir, "README.md"), readme);
1241
+ console.log(chalk3.green("\u2713 Created README.md"));
1242
+ const gitignore = `.objectui-tmp
1243
+ node_modules
1244
+ dist
1245
+ .DS_Store
1246
+ *.log
1247
+ `;
1248
+ writeFileSync2(join3(targetDir, ".gitignore"), gitignore);
1249
+ console.log(chalk3.green("\u2713 Created .gitignore"));
1250
+ console.log();
1251
+ console.log(chalk3.green("\u2728 Application created successfully!"));
1252
+ console.log();
1253
+ console.log(chalk3.bold("Next steps:"));
1254
+ console.log();
1255
+ if (name !== ".") {
1256
+ console.log(chalk3.cyan(` cd ${name}`));
1257
+ }
1258
+ console.log(chalk3.cyan(" objectui serve app.json"));
1259
+ console.log();
1260
+ console.log(chalk3.dim(" The development server will start on http://localhost:3000"));
1261
+ console.log();
1262
+ }
1263
+
1264
+ export {
1265
+ parseSchemaFile,
1266
+ scanPagesDirectory,
1267
+ createTempApp,
1268
+ createTempAppWithRouting,
1269
+ serve,
1270
+ init
1271
+ };
1272
+ //# sourceMappingURL=chunk-WOV6EOMH.js.map