Skip to content

Commit b95b792

Browse files
authored
Merge pull request #283 from objectstack-ai/copilot/implement-virtual-scrolling
2 parents 9654ca4 + c55a781 commit b95b792

File tree

17 files changed

+1095
-15
lines changed

17 files changed

+1095
-15
lines changed

packages/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"dependencies": {
4343
"@object-ui/components": "workspace:*",
4444
"@object-ui/react": "workspace:*",
45+
"@object-ui/types": "workspace:*",
4546
"@types/glob": "^9.0.0",
4647
"@vitejs/plugin-react": "^4.2.1",
4748
"chalk": "^5.4.1",

packages/cli/src/cli.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import { doctor } from './commands/doctor.js';
2020
import { add } from './commands/add.js';
2121
import { studio } from './commands/studio.js';
2222
import { check } from './commands/check.js';
23+
import { validate } from './commands/validate.js';
24+
import { createPlugin } from './commands/create-plugin.js';
25+
import { analyze } from './commands/analyze.js';
2326
import { readFileSync } from 'fs';
2427
import { fileURLToPath } from 'url';
2528
import { dirname, join } from 'path';
@@ -148,8 +151,17 @@ program
148151
.description('Generate new resources (objects, pages, plugins)')
149152
.argument('<type>', 'Type of resource to generate (resource/object, page, plugin)')
150153
.argument('<name>', 'Name of the resource')
151-
.action(async (type, name) => {
154+
.option('--from <source>', 'Generate schema from external source (openapi.yaml, prisma.schema)')
155+
.option('--output <dir>', 'Output directory for generated schemas', 'schemas/')
156+
.action(async (type, name, options) => {
152157
try {
158+
// Handle schema generation from external sources
159+
if (options.from) {
160+
console.log(chalk.yellow('\n⚠ Schema generation from external sources (OpenAPI/Prisma) is not yet implemented.'));
161+
console.log(chalk.gray('This feature will be available in a future release.\n'));
162+
process.exit(0);
163+
}
164+
153165
await generate(type, name);
154166
} catch (error) {
155167
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
@@ -206,5 +218,52 @@ program
206218
}
207219
});
208220

221+
program
222+
.command('validate')
223+
.description('Validate a schema file against ObjectUI specifications')
224+
.argument('[schema]', 'Path to schema file (JSON or YAML)', 'app.json')
225+
.action(async (schema) => {
226+
try {
227+
await validate(schema);
228+
} catch (error) {
229+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
230+
process.exit(1);
231+
}
232+
});
233+
234+
program
235+
.command('create')
236+
.description('Create new resources')
237+
.argument('<type>', 'Type of resource to create (plugin)')
238+
.argument('<name>', 'Name of the resource')
239+
.action(async (type, name) => {
240+
try {
241+
if (type === 'plugin') {
242+
await createPlugin(name);
243+
} else {
244+
console.error(chalk.red(`Unknown resource type: ${type}`));
245+
console.log(chalk.gray('Available types: plugin'));
246+
process.exit(1);
247+
}
248+
} catch (error) {
249+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
250+
process.exit(1);
251+
}
252+
});
253+
254+
program
255+
.command('analyze')
256+
.description('Analyze application performance')
257+
.option('--bundle-size', 'Analyze bundle size')
258+
.option('--render-performance', 'Analyze render performance')
259+
.action(async (options) => {
260+
try {
261+
await analyze(options);
262+
} catch (error) {
263+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
264+
process.exit(1);
265+
}
266+
});
267+
209268

210269
program.parse();
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import chalk from 'chalk';
10+
import { existsSync, statSync, readdirSync } from 'fs';
11+
import { resolve, join, extname } from 'path';
12+
13+
interface AnalyzeOptions {
14+
bundleSize?: boolean;
15+
renderPerformance?: boolean;
16+
}
17+
18+
/**
19+
* Analyze bundle size by scanning dist directory
20+
*/
21+
async function analyzeBundleSize() {
22+
console.log(chalk.bold('\n📦 Bundle Size Analysis\n'));
23+
24+
const distDir = resolve(process.cwd(), 'dist');
25+
26+
if (!existsSync(distDir)) {
27+
console.log(chalk.yellow('⚠ No dist directory found. Run build first.'));
28+
return;
29+
}
30+
31+
const files: Array<{ path: string; size: number }> = [];
32+
33+
function scanDirectory(dir: string) {
34+
const items = readdirSync(dir);
35+
36+
for (const item of items) {
37+
const fullPath = join(dir, item);
38+
const stat = statSync(fullPath);
39+
40+
if (stat.isDirectory()) {
41+
scanDirectory(fullPath);
42+
} else {
43+
files.push({
44+
path: fullPath.replace(distDir + '/', ''),
45+
size: stat.size,
46+
});
47+
}
48+
}
49+
}
50+
51+
scanDirectory(distDir);
52+
53+
// Sort by size (largest first)
54+
files.sort((a, b) => b.size - a.size);
55+
56+
// Calculate totals
57+
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
58+
const jsFiles = files.filter(f => extname(f.path) === '.js');
59+
const cssFiles = files.filter(f => extname(f.path) === '.css');
60+
61+
const jsSize = jsFiles.reduce((sum, file) => sum + file.size, 0);
62+
const cssSize = cssFiles.reduce((sum, file) => sum + file.size, 0);
63+
64+
console.log(chalk.bold('Summary:'));
65+
console.log(chalk.gray(' Total Size:'), formatBytes(totalSize));
66+
console.log(chalk.gray(' JavaScript:'), formatBytes(jsSize), chalk.dim(`(${jsFiles.length} files)`));
67+
console.log(chalk.gray(' CSS: '), formatBytes(cssSize), chalk.dim(`(${cssFiles.length} files)`));
68+
console.log(chalk.gray(' Other: '), formatBytes(totalSize - jsSize - cssSize));
69+
70+
console.log(chalk.bold('\nLargest Files:'));
71+
files.slice(0, 10).forEach((file) => {
72+
const sizeStr = formatBytes(file.size).padStart(10);
73+
console.log(chalk.gray(` ${sizeStr}`), file.path);
74+
});
75+
76+
// Bundle size recommendations
77+
console.log(chalk.bold('\n💡 Recommendations:'));
78+
79+
if (totalSize > 1024 * 1024) {
80+
console.log(chalk.yellow(' ⚠ Total bundle size is large (> 1MB)'));
81+
console.log(chalk.gray(' Consider code splitting or lazy loading'));
82+
}
83+
84+
if (jsSize > 500 * 1024) {
85+
console.log(chalk.yellow(' ⚠ JavaScript bundle is large (> 500KB)'));
86+
console.log(chalk.gray(' Consider:'));
87+
console.log(chalk.gray(' - Tree shaking unused code'));
88+
console.log(chalk.gray(' - Lazy loading components'));
89+
console.log(chalk.gray(' - Using dynamic imports'));
90+
}
91+
92+
if (files.length > 100) {
93+
console.log(chalk.yellow(` ⚠ Large number of files (${files.length})`));
94+
console.log(chalk.gray(' Consider bundling or combining files'));
95+
}
96+
97+
console.log('');
98+
}
99+
100+
/**
101+
* Analyze render performance (placeholder for now)
102+
*/
103+
async function analyzeRenderPerformance() {
104+
console.log(chalk.bold('\n⚡ Render Performance Analysis\n'));
105+
106+
console.log(chalk.gray('Performance analysis features:'));
107+
console.log(chalk.gray(' ✓ Expression caching enabled'));
108+
console.log(chalk.gray(' ✓ Component memoization available'));
109+
console.log(chalk.gray(' ✓ Virtual scrolling support for large lists'));
110+
111+
console.log(chalk.bold('\n💡 Performance Tips:'));
112+
console.log(chalk.gray(' • Use virtual scrolling for lists > 100 items'));
113+
console.log(chalk.gray(' • Cache frequently evaluated expressions'));
114+
console.log(chalk.gray(' • Use React.memo for expensive components'));
115+
console.log(chalk.gray(' • Implement pagination for large datasets'));
116+
console.log(chalk.gray(' • Use code splitting for large apps'));
117+
118+
console.log('');
119+
}
120+
121+
/**
122+
* Format bytes to human-readable string
123+
*/
124+
function formatBytes(bytes: number): string {
125+
if (bytes === 0) return '0 B';
126+
127+
const k = 1024;
128+
const sizes = ['B', 'KB', 'MB', 'GB'];
129+
const i = Math.floor(Math.log(bytes) / Math.log(k));
130+
131+
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
132+
}
133+
134+
/**
135+
* Analyze application performance
136+
*
137+
* @param options - Analysis options
138+
*/
139+
export async function analyze(options: AnalyzeOptions = {}) {
140+
console.log(chalk.blue('🔍 ObjectUI Performance Analyzer\n'));
141+
142+
const runAll = !options.bundleSize && !options.renderPerformance;
143+
144+
if (options.bundleSize || runAll) {
145+
await analyzeBundleSize();
146+
}
147+
148+
if (options.renderPerformance || runAll) {
149+
await analyzeRenderPerformance();
150+
}
151+
152+
console.log(chalk.green('✓ Analysis complete!\n'));
153+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import chalk from 'chalk';
10+
import { spawn } from 'child_process';
11+
import { resolve, dirname } from 'path';
12+
import { fileURLToPath } from 'url';
13+
14+
const __filename = fileURLToPath(import.meta.url);
15+
const __dirname = dirname(__filename);
16+
17+
/**
18+
* Create a new plugin using the @object-ui/create-plugin package
19+
*
20+
* @param pluginName - Name of the plugin to create
21+
*/
22+
export async function createPlugin(pluginName: string) {
23+
console.log(chalk.blue('🚀 Creating ObjectUI plugin...\n'));
24+
25+
// Resolve the create-plugin script path
26+
const createPluginScript = resolve(
27+
__dirname,
28+
'../../../create-plugin/dist/index.js'
29+
);
30+
31+
return new Promise<void>((resolve, reject) => {
32+
// Spawn the create-plugin command
33+
const child = spawn('node', [createPluginScript, pluginName], {
34+
stdio: 'inherit',
35+
shell: true,
36+
});
37+
38+
child.on('error', (error) => {
39+
console.error(chalk.red('Failed to create plugin:'), error);
40+
reject(error);
41+
});
42+
43+
child.on('exit', (code) => {
44+
if (code === 0) {
45+
resolve();
46+
} else {
47+
reject(new Error(`create-plugin exited with code ${code}`));
48+
}
49+
});
50+
});
51+
}

0 commit comments

Comments
 (0)