Skip to content

Add expression caching, virtual scrolling, and developer tooling#283

Merged
hotlong merged 11 commits intomainfrom
copilot/implement-virtual-scrolling
Jan 30, 2026
Merged

Add expression caching, virtual scrolling, and developer tooling#283
hotlong merged 11 commits intomainfrom
copilot/implement-virtual-scrolling

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jan 30, 2026

Implements performance optimizations for large datasets and developer experience improvements: expression caching to eliminate redundant parsing, virtual scrolling for efficient list rendering, schema validation helpers, and CLI tooling.

Expression Caching

Added ExpressionCache with LFU eviction (default: 1000 entries) integrated into ExpressionEvaluator. Convenience functions now share a global cache instance.

// Before: Expression parsed on every render
evaluator.evaluate('${data.amount > 1000}');

// After: Cached and reused
const cache = new ExpressionCache();
const compiled = cache.compile('data.amount > 1000', ['data']);
compiled.fn({ amount: 1500 }); // true

Impact: Eliminates redundant Function constructor calls for frequently evaluated expressions.

Virtual Scrolling

  • plugin-grid: New VirtualGrid component using @tanstack/react-virtual renders only visible rows. Configurable height, row height, overscan, and custom cell renderers.
  • plugin-aggrid: Optimized with rowBuffer: 10 and debounceVerticalScrollbar for datasets >1000 rows. AG Grid's built-in virtualization documented.

Impact: Handles 10,000+ items without DOM bloat.

Schema Validation

Exported validateSchema() (throws) and safeValidateSchema() (returns result object) from @object-ui/types/zod:

import { safeValidateSchema } from '@object-ui/types/zod';

const result = safeValidateSchema({ type: 'button', label: 'Click' });
if (!result.success) console.error(result.error);

CLI Commands

  • objectui validate <schema> - Validates JSON/YAML schemas with formatted error output
  • objectui create plugin <name> - Scaffolds plugin structure via @object-ui/create-plugin
  • objectui analyze - Reports bundle size breakdown and performance recommendations
  • objectui generate --from <source> - Placeholder for OpenAPI/Prisma codegen

Files Changed

  • packages/core/src/evaluator/ExpressionCache.ts (new)
  • packages/core/src/evaluator/ExpressionEvaluator.ts (shared cache)
  • packages/plugin-grid/src/VirtualGrid.tsx (new)
  • packages/plugin-aggrid/src/AgGridImpl.tsx (virtual scroll config)
  • packages/types/src/zod/index.zod.ts (validation exports)
  • packages/cli/src/commands/{validate,create-plugin,analyze}.ts (new)

Dependencies

  • Added @tanstack/react-virtual@^3.11.3 to plugin-grid
Original prompt

3.3 性能优化

A. 虚拟滚动

现状: 大数据集性能问题
优化方案: 在 plugin-grid 和 plugin-aggrid 中实现虚拟滚动

import { useVirtualizer } from '@tanstack/react-virtual';

export function VirtualGrid({ data, rowHeight = 40 }) {
const parentRef = useRef(null);

const virtualizer = useVirtualizer({
count: data.length,
getScrollElement: () => parentRef.current,
estimateSize: () => rowHeight,
overscan: 5
});

return (


<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map(item => (
<div key={item.key} style={{ transform: translateY(${item.start}px) }}>


))}


);
}
B. 表达式引擎优化

现状: 每次渲染都解析表达式
优化方案: 表达式缓存与预编译

// packages/core/src/evaluator/ExpressionCache.ts
class ExpressionCache {
private cache = new Map<string, CompiledExpression>();

compile(expr: string): CompiledExpression {
if (this.cache.has(expr)) {
return this.cache.get(expr)!;
}
const compiled = compileExpression(expr);
this.cache.set(expr, compiled);
return compiled;
}
}
3.4 开发体验优化

A. Schema 验证增强

优化方案: 实时 Schema 验证与 IDE 提示

// packages/types/src/zod/index.zod.ts
import { z } from 'zod';

export const ComponentSchemaZod = z.object({
type: z.string(),
props: z.record(z.any()).optional(),
children: z.lazy(() => z.array(ComponentSchemaZod)).optional()
});

// 导出验证函数
export function validateSchema(schema: unknown) {
return ComponentSchemaZod.parse(schema);
}
B. CLI 增强

新功能:

1. Schema 验证

objectui validate app.json

2. Schema 生成器(从 OpenAPI/Prisma)

objectui generate --from openapi.yaml --output schemas/

3. 插件脚手架

objectui create plugin my-plugin

4. 性能分析

objectui analyze --bundle-size --render-performance


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@vercel
Copy link
Copy Markdown

vercel Bot commented Jan 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectui Ready Ready Preview, Comment Jan 30, 2026 3:27pm
objectui-components Ready Ready Preview, Comment Jan 30, 2026 3:27pm
objectui-crm-app Error Error Jan 30, 2026 3:27pm

Request Review

Copilot AI and others added 2 commits January 30, 2026 14:27
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…dd shared cache

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Add virtual scrolling optimization for large data sets Add expression caching, virtual scrolling, and developer tooling Jan 30, 2026
Copilot AI requested a review from hotlong January 30, 2026 14:49
// 2. Strict mode enabled ("use strict")
// 3. Limited scope (only varNames variables available)
// 4. No access to global objects (process, window, etc.)
return new Function(...varNames, `"use strict"; return (${expression});`) as CompiledExpression;

Check warning

Code scanning / CodeQL

Unsafe code constructed from library input Medium

This string concatenation which depends on
library input
is later
interpreted as code
.
This string concatenation which depends on
library input
is later
interpreted as code
.
This string concatenation which depends on
library input
is later
interpreted as code
.
This string concatenation which depends on
library input
is later
interpreted as code
.
This string concatenation which depends on
library input
is later
interpreted as code
.
This string concatenation which depends on
library input
is later
interpreted as code
.
This string concatenation which depends on
library input
is later
interpreted as code
.
This string concatenation which depends on
library input
is later
interpreted as code
.
This string concatenation which depends on
library input
is later
interpreted as code
.
This string concatenation which depends on
library input
is later
interpreted as code
.
This string concatenation which depends on
library input
is later
interpreted as code
.
This string concatenation which depends on library input is later
interpreted as code
.
This string concatenation which depends on library input is later
interpreted as code
.
This string concatenation which depends on library input is later
interpreted as code
.
This string concatenation which depends on library input is later
interpreted as code
.
Copilot AI and others added 2 commits January 30, 2026 15:03
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

Package Size Gzipped
components (index.js) 1760.07KB 416.42KB
core (index.js) 0.56KB 0.29KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 16.91KB 4.46KB
fields (index.js) 75.69KB 15.58KB
layout (index.js) 12.63KB 3.91KB
plugin-aggrid (AgGridImpl-Bh0lwHIN.js) 5.27KB 1.92KB
plugin-aggrid (ObjectAgGridImpl-BnF7XF8L.js) 11.44KB 3.52KB
plugin-aggrid (index-BOMcxDJV.js) 19.13KB 4.88KB
plugin-aggrid (index.js) 0.22KB 0.16KB
plugin-calendar (index.js) 33.12KB 8.29KB
plugin-charts (AdvancedChartImpl-DPXnchtJ.js) 69.51KB 16.23KB
plugin-charts (BarChart-RKJxvg5Y.js) 535.74KB 134.11KB
plugin-charts (ChartImpl-DZGXB6YY.js) 8.78KB 3.11KB
plugin-charts (index-A3NiI95J.js) 12.59KB 3.90KB
plugin-charts (index.js) 0.21KB 0.16KB
plugin-chatbot (index.js) 18.36KB 5.21KB
plugin-dashboard (index.js) 11.92KB 3.81KB
plugin-editor (MonacoImpl-B7ZgZJJG.js) 18.15KB 5.59KB
plugin-editor (index-Dl3HAAqu.js) 10.07KB 3.31KB
plugin-editor (index.js) 0.19KB 0.15KB
plugin-form (index.js) 14.43KB 4.64KB
plugin-gantt (index.js) 18.00KB 5.26KB
plugin-grid (index.js) 40.44KB 11.21KB
plugin-kanban (KanbanImpl-CUWM-JC-.js) 76.50KB 20.46KB
plugin-kanban (index-BV3FWhCb.js) 11.86KB 3.67KB
plugin-kanban (index.js) 0.18KB 0.15KB
plugin-map (index.js) 16.75KB 5.11KB
plugin-markdown (MarkdownImpl-BRkYjVWf.js) 256.79KB 64.50KB
plugin-markdown (index-D_CdfEXQ.js) 9.59KB 3.16KB
plugin-markdown (index.js) 0.19KB 0.15KB
plugin-timeline (index.js) 23.90KB 5.95KB
plugin-view (index.js) 16.64KB 4.92KB
react (LazyPluginLoader.js) 1.10KB 0.58KB
react (SchemaRenderer.js) 2.58KB 1.04KB
react (index.js) 0.39KB 0.25KB
react (index.test.js) 0.34KB 0.26KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 0.20KB 0.18KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (disclosure.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (index.js) 0.34KB 0.25KB
types (layout.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (theme.js) 0.20KB 0.18KB
types (views.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

2 similar comments
@github-actions
Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

Package Size Gzipped
components (index.js) 1760.07KB 416.42KB
core (index.js) 0.56KB 0.29KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 16.91KB 4.46KB
fields (index.js) 75.69KB 15.58KB
layout (index.js) 12.63KB 3.91KB
plugin-aggrid (AgGridImpl-Bh0lwHIN.js) 5.27KB 1.92KB
plugin-aggrid (ObjectAgGridImpl-BnF7XF8L.js) 11.44KB 3.52KB
plugin-aggrid (index-BOMcxDJV.js) 19.13KB 4.88KB
plugin-aggrid (index.js) 0.22KB 0.16KB
plugin-calendar (index.js) 33.12KB 8.29KB
plugin-charts (AdvancedChartImpl-DPXnchtJ.js) 69.51KB 16.23KB
plugin-charts (BarChart-RKJxvg5Y.js) 535.74KB 134.11KB
plugin-charts (ChartImpl-DZGXB6YY.js) 8.78KB 3.11KB
plugin-charts (index-A3NiI95J.js) 12.59KB 3.90KB
plugin-charts (index.js) 0.21KB 0.16KB
plugin-chatbot (index.js) 18.36KB 5.21KB
plugin-dashboard (index.js) 11.92KB 3.81KB
plugin-editor (MonacoImpl-B7ZgZJJG.js) 18.15KB 5.59KB
plugin-editor (index-Dl3HAAqu.js) 10.07KB 3.31KB
plugin-editor (index.js) 0.19KB 0.15KB
plugin-form (index.js) 14.43KB 4.64KB
plugin-gantt (index.js) 18.00KB 5.26KB
plugin-grid (index.js) 40.44KB 11.21KB
plugin-kanban (KanbanImpl-CUWM-JC-.js) 76.50KB 20.46KB
plugin-kanban (index-BV3FWhCb.js) 11.86KB 3.67KB
plugin-kanban (index.js) 0.18KB 0.15KB
plugin-map (index.js) 16.75KB 5.11KB
plugin-markdown (MarkdownImpl-BRkYjVWf.js) 256.79KB 64.50KB
plugin-markdown (index-D_CdfEXQ.js) 9.59KB 3.16KB
plugin-markdown (index.js) 0.19KB 0.15KB
plugin-timeline (index.js) 23.90KB 5.95KB
plugin-view (index.js) 16.64KB 4.92KB
react (LazyPluginLoader.js) 1.10KB 0.58KB
react (SchemaRenderer.js) 2.58KB 1.04KB
react (index.js) 0.39KB 0.25KB
react (index.test.js) 0.34KB 0.26KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 0.20KB 0.18KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (disclosure.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (index.js) 0.34KB 0.25KB
types (layout.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (theme.js) 0.20KB 0.18KB
types (views.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@github-actions
Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

Package Size Gzipped
components (index.js) 1760.07KB 416.42KB
core (index.js) 0.56KB 0.29KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 16.91KB 4.46KB
fields (index.js) 75.69KB 15.58KB
layout (index.js) 12.63KB 3.91KB
plugin-aggrid (AgGridImpl-Bh0lwHIN.js) 5.27KB 1.92KB
plugin-aggrid (ObjectAgGridImpl-BnF7XF8L.js) 11.44KB 3.52KB
plugin-aggrid (index-BOMcxDJV.js) 19.13KB 4.88KB
plugin-aggrid (index.js) 0.22KB 0.16KB
plugin-calendar (index.js) 33.12KB 8.29KB
plugin-charts (AdvancedChartImpl-DPXnchtJ.js) 69.51KB 16.23KB
plugin-charts (BarChart-RKJxvg5Y.js) 535.74KB 134.11KB
plugin-charts (ChartImpl-DZGXB6YY.js) 8.78KB 3.11KB
plugin-charts (index-A3NiI95J.js) 12.59KB 3.90KB
plugin-charts (index.js) 0.21KB 0.16KB
plugin-chatbot (index.js) 18.36KB 5.21KB
plugin-dashboard (index.js) 11.92KB 3.81KB
plugin-editor (MonacoImpl-B7ZgZJJG.js) 18.15KB 5.59KB
plugin-editor (index-Dl3HAAqu.js) 10.07KB 3.31KB
plugin-editor (index.js) 0.19KB 0.15KB
plugin-form (index.js) 14.43KB 4.64KB
plugin-gantt (index.js) 18.00KB 5.26KB
plugin-grid (index.js) 40.44KB 11.21KB
plugin-kanban (KanbanImpl-CUWM-JC-.js) 76.50KB 20.46KB
plugin-kanban (index-BV3FWhCb.js) 11.86KB 3.67KB
plugin-kanban (index.js) 0.18KB 0.15KB
plugin-map (index.js) 16.75KB 5.11KB
plugin-markdown (MarkdownImpl-BRkYjVWf.js) 256.79KB 64.50KB
plugin-markdown (index-D_CdfEXQ.js) 9.59KB 3.16KB
plugin-markdown (index.js) 0.19KB 0.15KB
plugin-timeline (index.js) 23.90KB 5.95KB
plugin-view (index.js) 16.64KB 4.92KB
react (LazyPluginLoader.js) 1.10KB 0.58KB
react (SchemaRenderer.js) 2.58KB 1.04KB
react (index.js) 0.39KB 0.25KB
react (index.test.js) 0.34KB 0.26KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 0.20KB 0.18KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (disclosure.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (index.js) 0.34KB 0.25KB
types (layout.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (theme.js) 0.20KB 0.18KB
types (views.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@github-actions
Copy link
Copy Markdown
Contributor

✅ All checks passed!

  • ✅ Type check passed
  • ✅ Tests passed
  • ✅ Lint check completed

@github-actions
Copy link
Copy Markdown
Contributor

✅ All checks passed!

  • ✅ Type check passed
  • ✅ Tests passed
  • ✅ Lint check completed

1 similar comment
@github-actions
Copy link
Copy Markdown
Contributor

✅ All checks passed!

  • ✅ Type check passed
  • ✅ Tests passed
  • ✅ Lint check completed

@hotlong hotlong marked this pull request as ready for review January 30, 2026 15:37
Copilot AI review requested due to automatic review settings January 30, 2026 15:37
@hotlong hotlong merged commit b95b792 into main Jan 30, 2026
16 of 17 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements performance optimizations and developer tooling enhancements for ObjectUI, focusing on expression caching for reduced parsing overhead, virtual scrolling for large datasets, schema validation utilities, and CLI commands for development workflows.

Changes:

  • Expression Caching: Added ExpressionCache class with hybrid LFU/LRU eviction policy (default 1000 entries), integrated into ExpressionEvaluator with a global cache for convenience functions
  • Virtual Scrolling: Implemented VirtualGrid component using @tanstack/react-virtual for rendering only visible rows, and optimized AG Grid with automatic rowBuffer and debounceVerticalScrollbar settings for datasets >1000 rows
  • Schema Validation: Exported validateSchema() and safeValidateSchema() helper functions from @object-ui/types/zod for convenient schema validation
  • CLI Tooling: Added objectui validate, objectui create plugin, and objectui analyze commands with formatted output and error handling

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
packages/core/src/evaluator/ExpressionCache.ts New cache implementation with LFU eviction and metadata tracking for compiled expressions
packages/core/src/evaluator/ExpressionEvaluator.ts Integrated cache into evaluator, added cache statistics methods, shared global cache for convenience functions
packages/core/src/evaluator/tests/ExpressionCache.test.ts Comprehensive tests for cache functionality including eviction, hit counting, and statistics
packages/core/src/evaluator/index.ts Export ExpressionCache for external use
packages/plugin-grid/src/VirtualGrid.tsx New virtual scrolling grid component with configurable row height, overscan, and custom cell renderers
packages/plugin-grid/src/VirtualGrid.test.tsx Basic existence tests (minimal coverage)
packages/plugin-grid/src/index.tsx Export VirtualGrid component and types
packages/plugin-grid/package.json Add @tanstack/react-virtual dependency
packages/plugin-aggrid/src/VirtualScrolling.ts Documentation file explaining AG Grid's built-in virtual scrolling
packages/plugin-aggrid/src/AgGridImpl.tsx Add automatic virtual scrolling optimizations based on dataset size
packages/types/src/zod/index.zod.ts Add validateSchema and safeValidateSchema helper functions
packages/cli/src/commands/validate.ts New command to validate schema files with formatted error output
packages/cli/src/commands/create-plugin.ts New command to scaffold plugin structure
packages/cli/src/commands/analyze.ts New command for bundle size analysis and performance recommendations
packages/cli/src/cli.ts Integrate new commands into CLI interface
packages/cli/package.json Add @object-ui/types dependency
pnpm-lock.yaml Add @tanstack/react-virtual@3.13.18 and update dependency graph
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +245 to +246
rowBuffer: gridOptions.rowBuffer ?? 10,
debounceVerticalScrollbar: gridOptions.debounceVerticalScrollbar ?? (rowData.length > 1000),
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debounceVerticalScrollbar setting is automatically enabled for datasets larger than 1000 rows (line 246), but this is applied to gridOptions which the user can override. This creates an inconsistency where:

  1. If gridOptions.debounceVerticalScrollbar is undefined, it automatically becomes true for large datasets
  2. If gridOptions.debounceVerticalScrollbar is explicitly set to false, it stays false (due to the ?? operator)
  3. If gridOptions.debounceVerticalScrollbar is explicitly set to true, it stays true

The spread operator on line 227 (...gridOptions) means that if a user passes gridOptions: { debounceVerticalScrollbar: false }, it will be overridden on line 246. This is actually correct behavior (auto-optimization unless explicitly disabled), but it should be documented.

Consider adding a comment explaining this behavior, or documenting in the component's JSDoc that automatic optimizations can be disabled by explicitly setting these options to false in gridOptions.

Copilot uses AI. Check for mistakes.
>
{columns.map((column, index) => (
<div
key={index}
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The column headers are using array index as the key (line 96), which is an anti-pattern in React. While this works when the columns array is static, it can cause issues if:

  1. The columns array order changes dynamically
  2. Columns are added or removed
  3. The grid needs to update with a new columns configuration

Using index as a key can lead to incorrect component reuse and potential rendering bugs. Instead, consider using a more stable identifier:

key={column.accessorKey || column.header || index}

This ensures that if the same column appears in a different position, React can properly track and update it. The accessorKey is the most stable identifier since it's unique per column.

Suggested change
key={index}
key={column.accessorKey || column.header || index}

Copilot uses AI. Check for mistakes.
"@object-ui/fields": "workspace:*",
"@object-ui/react": "workspace:*",
"@object-ui/types": "workspace:*",
"@tanstack/react-virtual": "^3.11.3",
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The installed version of @tanstack/react-virtual is 3.13.18, but the package.json specifies ^3.11.3. While this works due to semver, there's a discrepancy between what was requested in the PR description ("Added @tanstack/react-virtual@^3.11.3") and what was actually installed (3.13.18).

This is not necessarily a problem, but for clarity and predictability, consider:

  1. Updating package.json to use the exact version that was tested: "@tanstack/react-virtual": "^3.13.18", OR
  2. Ensuring tests pass with the minimum specified version (3.11.3)

This ensures other developers installing from the package.json get the same version used during development.

Copilot uses AI. Check for mistakes.
import { ComponentRegistry } from '@object-ui/core';
import { ObjectGrid } from './ObjectGrid';
import { VirtualGrid } from './VirtualGrid';

Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The VirtualGrid component is not documented in the README. The plugin-grid README should be updated to include information about when and how to use VirtualGrid versus the standard ObjectGrid.

Based on the PR description, VirtualGrid is specifically designed for handling 10,000+ items without DOM bloat. Users need guidance on:

  1. When to use VirtualGrid vs ObjectGrid
  2. How to use VirtualGrid with schema-based rendering
  3. Example configurations
  4. Performance characteristics and limitations

This is especially important since VirtualGrid is a new feature that addresses a specific performance use case.

Suggested change
/**
* ObjectGrid vs VirtualGrid
*
* - {@link ObjectGrid}:
* - General-purpose data grid integrated with the ObjectUI schema renderer.
* - Best suited for small-to-medium datasets (typically up to a few thousand rows),
* where rendering all rows in the DOM at once is acceptable.
* - Prioritizes feature richness and simplicity over extreme-scale performance.
*
* - {@link VirtualGrid}:
* - Specialized grid implementation designed for very large datasets
* (10,000+ items) without causing DOM bloat.
* - Only the visible rows (plus a small overscan window) are mounted in the DOM,
* dramatically reducing memory usage and layout/paint cost.
* - Recommended when:
* - You need to render tens of thousands of records in a single grid.
* - Scroll performance degrades with {@link ObjectGrid}.
* - You care more about scroll smoothness and initial render time than about
* having every row present in the DOM simultaneously.
*
* Schema-based usage
* -------------------
* This plugin registers the `'object-grid'` view type in the {@link ComponentRegistry}.
* In schema-driven UIs, you typically render an ObjectGrid like:
*
* ```jsonc
* {
* "type": "object-grid",
* "id": "users-grid",
* "props": {
* "columns": [
* { "key": "id", "title": "ID" },
* { "key": "name", "title": "Name" }
* ]
* }
* }
* ```
*
* {@link VirtualGrid} exposes `VirtualGridProps` and `VirtualGridColumn` for direct
* use when you need manual, code-level control over a virtualized grid instance.
* A typical configuration in React/TypeScript may look like:
*
* ```tsx
* import { VirtualGrid, VirtualGridColumn } from '@object-ui/plugin-grid';
*
* const columns: VirtualGridColumn[] = [
* { key: 'id', title: 'ID', width: 80 },
* { key: 'name', title: 'Name', flex: 1 }
* ];
*
* <VirtualGrid
* columns={columns}
* data={hugeDataArray} // 10k+ items
* rowHeight={32}
* overscan={8}
* />
* ```
*
* Performance characteristics & limitations
* ----------------------------------------
* - Virtualized rendering:
* - Only a subset of rows is rendered at any time.
* - Greatly improves performance for very large datasets.
* - Layout assumptions:
* - Typically assumes a fixed or predictable row height (`rowHeight`).
* - Highly dynamic row heights may reduce virtualization accuracy.
* - Feature parity:
* - Some advanced features that depend on the full DOM (e.g., complex
* row-measurement-based layouts) may not be available or may behave
* differently compared to {@link ObjectGrid}.
*
* Exported surface
* ----------------
* - {@link ObjectGrid} & {@link ObjectGridProps}:
* - Use when you want a standard, schema-friendly data grid for typical
* dataset sizes.
* - {@link VirtualGrid}, {@link VirtualGridProps}, {@link VirtualGridColumn}:
* - Use when you explicitly need a virtualized grid for 10,000+ items and
* want to avoid DOM bloat, with the understanding of the constraints above.
*/

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +74

export const VIRTUAL_SCROLLING_DOCS = {
enabled: true,
automatic: true,
recommendedSettings: {
rowBuffer: 10,
rowHeight: 40,
debounceVerticalScrollbar: true,
animateRows: false,
},
};
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The VirtualScrolling.ts file exports a VIRTUAL_SCROLLING_DOCS constant that is never used in the codebase. This appears to be documentation-only code that exists purely to document AG Grid's virtual scrolling capabilities.

Consider either:

  1. Removing this file and moving the documentation to a markdown file (e.g., docs/virtual-scrolling.md) if it's only for reference
  2. Actually using this constant somewhere in the code (e.g., to display help text or recommendations to users)
  3. Exporting this from the plugin-aggrid index.ts if it's meant to be public API

Unused exported constants can confuse developers about the intended usage and may indicate incomplete implementation.

Suggested change
export const VIRTUAL_SCROLLING_DOCS = {
enabled: true,
automatic: true,
recommendedSettings: {
rowBuffer: 10,
rowHeight: 40,
debounceVerticalScrollbar: true,
animateRows: false,
},
};

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +44
files.push({
path: fullPath.replace(distDir + '/', ''),
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path manipulation on line 44 uses string concatenation with a hardcoded forward slash, which may not work correctly on Windows:

path: fullPath.replace(distDir + '/', '')

On Windows, paths use backslashes, so this replacement may fail. Use path.relative() instead for cross-platform compatibility:

import { relative } from 'path';
// ...
path: relative(distDir, fullPath)

This ensures the code works correctly on all operating systems.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +183
return (
<div className={className}>
{/* Header */}
<div
className={`grid border-b sticky top-0 bg-background z-10 ${headerClassName}`}
style={{
gridTemplateColumns: columns
.map((col) => col.width || '1fr')
.join(' '),
}}
>
{columns.map((column, index) => (
<div
key={index}
className={`px-4 py-2 font-semibold text-sm ${
column.align === 'center'
? 'text-center'
: column.align === 'right'
? 'text-right'
: 'text-left'
}`}
>
{column.header}
</div>
))}
</div>

{/* Virtual scrolling container */}
<div
ref={parentRef}
className="overflow-auto"
style={{
height: typeof height === 'number' ? `${height}px` : height,
contain: 'strict'
}}
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{items.map((virtualRow) => {
const row = data[virtualRow.index];
const rowClasses =
typeof rowClassName === 'function'
? rowClassName(row, virtualRow.index)
: rowClassName || '';

return (
<div
key={virtualRow.key}
className={`grid border-b hover:bg-muted/50 cursor-pointer ${rowClasses}`}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
gridTemplateColumns: columns
.map((col) => col.width || '1fr')
.join(' '),
}}
onClick={() => onRowClick?.(row, virtualRow.index)}
>
{columns.map((column, colIndex) => {
const value = row[column.accessorKey];
const cellContent = column.cell
? column.cell(value, row)
: value;

return (
<div
key={colIndex}
className={`px-4 py-2 text-sm flex items-center ${
column.align === 'center'
? 'text-center justify-center'
: column.align === 'right'
? 'text-right justify-end'
: 'text-left justify-start'
}`}
>
{cellContent}
</div>
);
})}
</div>
);
})}
</div>
</div>

{/* Footer info */}
<div className="px-4 py-2 text-xs text-muted-foreground border-t">
Showing {items.length} of {data.length} rows (virtual scrolling enabled)
</div>
</div>
);
};
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The VirtualGrid component lacks accessibility attributes for screen readers. The grid structure should include appropriate ARIA roles and labels to be accessible to users with disabilities.

Consider adding:

  1. role="grid" on the main container
  2. role="row" on each row element
  3. role="columnheader" on header cells
  4. role="gridcell" on data cells
  5. aria-label or aria-labelledby to identify the grid
  6. aria-rowcount and aria-rowindex for virtual scrolling awareness
  7. aria-colcount and aria-colindex for column identification

Example:

<div role="grid" aria-label="Data grid" aria-rowcount={data.length} className={className}>
  <div role="row" className="...">
    {columns.map((column) => (
      <div role="columnheader" aria-colindex={index + 1}>
        {column.header}
      </div>
    ))}
  </div>
  {/* ... rows with role="row" and cells with role="gridcell" */}
</div>

This is especially important for a data grid component that users may rely on for navigating tabular data.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +22
describe('VirtualGrid', () => {
it('should be exported', () => {
expect(VirtualGrid).toBeDefined();
expect(typeof VirtualGrid).toBe('function');
});

it('should have the correct display name', () => {
// Verify it's a React component
expect(VirtualGrid.name).toBe('VirtualGrid');
});
});
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The VirtualGrid component lacks proper test coverage. The existing tests only verify that the component is exported and has the correct name, but do not test any actual functionality.

Based on the test patterns found in the codebase (packages/components/src/tests/test-utils.tsx and packages/react/src/tests/LazyPluginLoader.test.tsx), this component should have tests that:

  1. Render the component with sample data and columns
  2. Verify that virtual scrolling is working (only visible rows are rendered)
  3. Test row click handlers
  4. Test custom cell renderers
  5. Verify accessibility attributes

This is particularly important because VirtualGrid is a new, performance-critical component for handling large datasets.

Copilot uses AI. Check for mistakes.
Comment on lines +130 to +148
private evictLFU(): void {
let oldestKey: string | null = null;
let oldestTime = Infinity;
let lowestHits = Infinity;

// Find the entry with lowest hit count, or oldest if tied
for (const [key, metadata] of this.cache.entries()) {
if (metadata.hitCount < lowestHits ||
(metadata.hitCount === lowestHits && metadata.compiledAt < oldestTime)) {
oldestKey = key;
oldestTime = metadata.compiledAt;
lowestHits = metadata.hitCount;
}
}

if (oldestKey) {
this.cache.delete(oldestKey);
}
}
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description claims "LFU eviction" but the implementation is actually a hybrid LFU/LRU (Least Frequently Used with Least Recently Used as tiebreaker). The evictLFU method finds the entry with the lowest hitCount, and uses compiledAt as a tiebreaker when multiple entries have the same hitCount.

While this is a reasonable eviction strategy, the terminology is imprecise. Consider either:

  1. Renaming the method to evictLFUWithLRUTiebreaker or evictHybrid for clarity
  2. Updating the PR description and documentation to reflect that this is a hybrid approach, not pure LFU
  3. Documenting this behavior in the class-level JSDoc comment

The current implementation is correct, but the naming could be more accurate to avoid confusion.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +51
// Try JSON first, then YAML
try {
schema = JSON.parse(fileContent);
console.log(chalk.gray(`Reading schema from: ${schemaPath} (detected as JSON)`));
} catch {
schema = loadYaml(fileContent);
console.log(chalk.gray(`Reading schema from: ${schemaPath} (detected as YAML)`));
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The auto-detection of file format (lines 45-52) has a potential issue: if the file is neither valid JSON nor valid YAML, the YAML parser error will be thrown and caught by the outer try-catch, resulting in a generic "Error reading or parsing schema file" message.

This could confuse users who provide an invalid file, as they won't know if the issue is with JSON or YAML parsing. Consider adding a more specific error message when both parsers fail:

try {
  schema = JSON.parse(fileContent);
  console.log(chalk.gray(`Reading schema from: ${schemaPath} (detected as JSON)`));
} catch (jsonError) {
  try {
    schema = loadYaml(fileContent);
    console.log(chalk.gray(`Reading schema from: ${schemaPath} (detected as YAML)`));
  } catch (yamlError) {
    throw new Error(`Failed to parse file as JSON or YAML. JSON error: ${jsonError.message}. YAML error: ${yamlError.message}`);
  }
}

This provides clearer feedback when a file cannot be parsed in either format.

Suggested change
// Try JSON first, then YAML
try {
schema = JSON.parse(fileContent);
console.log(chalk.gray(`Reading schema from: ${schemaPath} (detected as JSON)`));
} catch {
schema = loadYaml(fileContent);
console.log(chalk.gray(`Reading schema from: ${schemaPath} (detected as YAML)`));
// Try JSON first, then YAML with detailed error reporting
try {
schema = JSON.parse(fileContent);
console.log(chalk.gray(`Reading schema from: ${schemaPath} (detected as JSON)`));
} catch (jsonError) {
try {
schema = loadYaml(fileContent);
console.log(chalk.gray(`Reading schema from: ${schemaPath} (detected as YAML)`));
} catch (yamlError) {
const jsonMessage =
jsonError instanceof Error ? jsonError.message : String(jsonError);
const yamlMessage =
yamlError instanceof Error ? yamlError.message : String(yamlError);
throw new Error(
`Failed to parse file as JSON or YAML. JSON error: ${jsonMessage}. YAML error: ${yamlMessage}`
);
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants