cmssy-cli 0.21.0 → 0.25.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.
Files changed (72) hide show
  1. package/config.d.ts +1 -1
  2. package/dist/cli.js +131 -30
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/build.d.ts +1 -0
  5. package/dist/commands/build.d.ts.map +1 -1
  6. package/dist/commands/build.js +56 -12
  7. package/dist/commands/build.js.map +1 -1
  8. package/dist/commands/create.d.ts.map +1 -1
  9. package/dist/commands/create.js +22 -2
  10. package/dist/commands/create.js.map +1 -1
  11. package/dist/commands/dev.d.ts.map +1 -1
  12. package/dist/commands/dev.js +652 -410
  13. package/dist/commands/dev.js.map +1 -1
  14. package/dist/commands/init.d.ts.map +1 -1
  15. package/dist/commands/init.js +3 -1
  16. package/dist/commands/init.js.map +1 -1
  17. package/dist/commands/migrate.d.ts.map +1 -1
  18. package/dist/commands/migrate.js +3 -1
  19. package/dist/commands/migrate.js.map +1 -1
  20. package/dist/commands/publish.js +31 -4
  21. package/dist/commands/publish.js.map +1 -1
  22. package/dist/dev-ui/app.js +166 -19
  23. package/dist/dev-ui/index.html +138 -0
  24. package/dist/dev-ui-react/App.tsx +164 -0
  25. package/dist/dev-ui-react/__tests__/previewData.test.ts +193 -0
  26. package/dist/dev-ui-react/components/BlocksList.tsx +232 -0
  27. package/dist/dev-ui-react/components/Editor.tsx +469 -0
  28. package/dist/dev-ui-react/components/Preview.tsx +146 -0
  29. package/dist/dev-ui-react/hooks/useBlocks.ts +80 -0
  30. package/dist/dev-ui-react/index.html +13 -0
  31. package/dist/dev-ui-react/main.tsx +8 -0
  32. package/dist/dev-ui-react/styles.css +856 -0
  33. package/dist/dev-ui-react/types.ts +45 -0
  34. package/dist/types/block-config.d.ts +100 -2
  35. package/dist/types/block-config.d.ts.map +1 -1
  36. package/dist/types/block-config.js +6 -1
  37. package/dist/types/block-config.js.map +1 -1
  38. package/dist/utils/block-config.js +3 -3
  39. package/dist/utils/block-config.js.map +1 -1
  40. package/dist/utils/blocks-meta-cache.d.ts +28 -0
  41. package/dist/utils/blocks-meta-cache.d.ts.map +1 -0
  42. package/dist/utils/blocks-meta-cache.js +72 -0
  43. package/dist/utils/blocks-meta-cache.js.map +1 -0
  44. package/dist/utils/builder.d.ts +3 -0
  45. package/dist/utils/builder.d.ts.map +1 -1
  46. package/dist/utils/builder.js +17 -14
  47. package/dist/utils/builder.js.map +1 -1
  48. package/dist/utils/field-schema.d.ts +2 -0
  49. package/dist/utils/field-schema.d.ts.map +1 -1
  50. package/dist/utils/field-schema.js +21 -4
  51. package/dist/utils/field-schema.js.map +1 -1
  52. package/dist/utils/scanner.d.ts +5 -3
  53. package/dist/utils/scanner.d.ts.map +1 -1
  54. package/dist/utils/scanner.js +23 -16
  55. package/dist/utils/scanner.js.map +1 -1
  56. package/dist/utils/type-generator.d.ts +7 -1
  57. package/dist/utils/type-generator.d.ts.map +1 -1
  58. package/dist/utils/type-generator.js +58 -41
  59. package/dist/utils/type-generator.js.map +1 -1
  60. package/package.json +8 -3
  61. package/dist/commands/deploy.d.ts +0 -9
  62. package/dist/commands/deploy.d.ts.map +0 -1
  63. package/dist/commands/deploy.js +0 -226
  64. package/dist/commands/deploy.js.map +0 -1
  65. package/dist/commands/push.d.ts +0 -9
  66. package/dist/commands/push.d.ts.map +0 -1
  67. package/dist/commands/push.js +0 -199
  68. package/dist/commands/push.js.map +0 -1
  69. package/dist/utils/blockforge-config.d.ts +0 -19
  70. package/dist/utils/blockforge-config.d.ts.map +0 -1
  71. package/dist/utils/blockforge-config.js +0 -19
  72. package/dist/utils/blockforge-config.js.map +0 -1
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Test for preview data merge behavior
3
+ * Run with: npx vitest run src/dev-ui-react/__tests__/previewData.test.ts
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+
8
+ // Simulating the merge logic from App.tsx
9
+ function mergeForSave(
10
+ configDataRef: Record<string, unknown>,
11
+ previewData: Record<string, unknown>
12
+ ): Record<string, unknown> {
13
+ return { ...configDataRef, ...previewData };
14
+ }
15
+
16
+ // Simulating mergeDefaultsWithPreview from dev.ts
17
+ function mergeDefaultsWithPreview(
18
+ schema: Record<string, any>,
19
+ previewData: Record<string, unknown>
20
+ ): Record<string, unknown> {
21
+ const merged: Record<string, unknown> = { ...previewData };
22
+
23
+ for (const [key, field] of Object.entries(schema)) {
24
+ if (merged[key] === undefined || merged[key] === null) {
25
+ if (field.defaultValue !== undefined) {
26
+ merged[key] = field.defaultValue;
27
+ } else if (field.type === 'repeater') {
28
+ merged[key] = [];
29
+ }
30
+ }
31
+
32
+ if (field.type === 'repeater' && field.schema && Array.isArray(merged[key])) {
33
+ merged[key] = (merged[key] as any[]).map((item: any) => {
34
+ const mergedItem: Record<string, unknown> = { ...item };
35
+ for (const [nestedKey, nestedField] of Object.entries(field.schema as Record<string, any>)) {
36
+ if (mergedItem[nestedKey] === undefined && nestedField.defaultValue !== undefined) {
37
+ mergedItem[nestedKey] = nestedField.defaultValue;
38
+ }
39
+ }
40
+ return mergedItem;
41
+ });
42
+ }
43
+
44
+ if (field.type === 'media' && typeof merged[key] === 'string') {
45
+ merged[key] = { url: merged[key], alt: '' };
46
+ }
47
+ }
48
+
49
+ return merged;
50
+ }
51
+
52
+ describe('Preview Data Management', () => {
53
+ const mockSchema = {
54
+ badge: { type: 'singleLine', label: 'Badge', defaultValue: 'About Us' },
55
+ heading: { type: 'singleLine', label: 'Heading', defaultValue: 'Default Heading' },
56
+ values: {
57
+ type: 'repeater',
58
+ label: 'Values',
59
+ schema: {
60
+ title: { type: 'singleLine', defaultValue: 'Title' },
61
+ description: { type: 'multiLine', defaultValue: 'Description' },
62
+ },
63
+ defaultValue: [
64
+ { title: 'Value 1', description: 'Desc 1' },
65
+ { title: 'Value 2', description: 'Desc 2' },
66
+ ],
67
+ },
68
+ imageUrl: { type: 'media', label: 'Image', defaultValue: 'https://example.com/img.jpg' },
69
+ };
70
+
71
+ const completePreviewData = {
72
+ badge: 'About Us',
73
+ heading: 'Building the future',
74
+ values: [
75
+ { title: 'Innovation', description: 'We innovate' },
76
+ { title: 'Quality', description: 'We deliver quality' },
77
+ ],
78
+ imageUrl: { url: 'https://example.com/team.jpg', alt: 'Team' },
79
+ };
80
+
81
+ describe('mergeDefaultsWithPreview', () => {
82
+ it('should merge defaultValues into empty previewData', () => {
83
+ const result = mergeDefaultsWithPreview(mockSchema, {});
84
+
85
+ expect(result.badge).toBe('About Us');
86
+ expect(result.heading).toBe('Default Heading');
87
+ expect(result.values).toEqual([
88
+ { title: 'Value 1', description: 'Desc 1' },
89
+ { title: 'Value 2', description: 'Desc 2' },
90
+ ]);
91
+ expect(result.imageUrl).toEqual({ url: 'https://example.com/img.jpg', alt: '' });
92
+ });
93
+
94
+ it('should preserve existing previewData values over defaults', () => {
95
+ const result = mergeDefaultsWithPreview(mockSchema, { badge: 'Custom Badge' });
96
+
97
+ expect(result.badge).toBe('Custom Badge');
98
+ expect(result.heading).toBe('Default Heading'); // From default
99
+ });
100
+
101
+ it('should handle repeater arrays correctly', () => {
102
+ const result = mergeDefaultsWithPreview(mockSchema, {
103
+ values: [{ title: 'Custom' }], // Missing description
104
+ });
105
+
106
+ expect(result.values).toEqual([
107
+ { title: 'Custom', description: 'Description' }, // description from default
108
+ ]);
109
+ });
110
+
111
+ it('should convert string media to object', () => {
112
+ const result = mergeDefaultsWithPreview(mockSchema, {
113
+ imageUrl: 'https://custom.com/img.png',
114
+ });
115
+
116
+ expect(result.imageUrl).toEqual({ url: 'https://custom.com/img.png', alt: '' });
117
+ });
118
+ });
119
+
120
+ describe('mergeForSave', () => {
121
+ it('should merge configDataRef with previewData, previewData wins', () => {
122
+ const configDataRef = { ...completePreviewData };
123
+ const previewData = { badge: 'New Badge' };
124
+
125
+ const result = mergeForSave(configDataRef, previewData);
126
+
127
+ expect(result.badge).toBe('New Badge'); // User edit
128
+ expect(result.heading).toBe('Building the future'); // From config
129
+ expect(result.values).toEqual(completePreviewData.values); // From config
130
+ expect(result.imageUrl).toEqual(completePreviewData.imageUrl); // From config
131
+ });
132
+
133
+ it('should preserve all fields when user edits one field', () => {
134
+ const configDataRef = { ...completePreviewData };
135
+ const previewData = { ...completePreviewData, badge: 'Edited Badge' };
136
+
137
+ const result = mergeForSave(configDataRef, previewData);
138
+
139
+ expect(Object.keys(result)).toEqual(Object.keys(completePreviewData));
140
+ expect(result.badge).toBe('Edited Badge');
141
+ expect(result.values).toEqual(completePreviewData.values);
142
+ });
143
+
144
+ it('should NOT lose data when configDataRef is empty (BUG SCENARIO)', () => {
145
+ // This is the bug: if configDataRef is empty, we lose all data
146
+ const configDataRef = {}; // Bug: ref was not set
147
+ const previewData = { badge: 'New Badge' };
148
+
149
+ const result = mergeForSave(configDataRef, previewData);
150
+
151
+ // This will fail - demonstrating the bug
152
+ // If configDataRef is empty, we only get previewData
153
+ expect(Object.keys(result)).toEqual(['badge']);
154
+ });
155
+
156
+ it('should work correctly when configDataRef has complete data', () => {
157
+ const configDataRef = { ...completePreviewData };
158
+ const previewData = { badge: 'New Badge' };
159
+
160
+ const result = mergeForSave(configDataRef, previewData);
161
+
162
+ // With proper configDataRef, all data is preserved
163
+ expect(Object.keys(result).sort()).toEqual(Object.keys(completePreviewData).sort());
164
+ });
165
+ });
166
+
167
+ describe('Full flow simulation', () => {
168
+ it('should simulate complete user edit flow', () => {
169
+ // 1. Server loads preview.json and merges with schema defaults
170
+ const previewJsonContent = { heading: 'Only heading' };
171
+ const serverMerged = mergeDefaultsWithPreview(mockSchema, previewJsonContent);
172
+
173
+ expect(serverMerged.badge).toBe('About Us'); // From default
174
+ expect(serverMerged.heading).toBe('Only heading'); // From preview.json
175
+ expect(serverMerged.values).toBeDefined(); // From default
176
+
177
+ // 2. Frontend receives merged data and stores in configDataRef
178
+ const configDataRef = { ...serverMerged };
179
+
180
+ // 3. User edits badge
181
+ const previewData = { ...serverMerged, badge: 'Edited Badge' };
182
+
183
+ // 4. Save merges configDataRef with previewData
184
+ const dataToSave = mergeForSave(configDataRef, previewData);
185
+
186
+ // 5. All fields should be preserved
187
+ expect(dataToSave.badge).toBe('Edited Badge');
188
+ expect(dataToSave.heading).toBe('Only heading');
189
+ expect(dataToSave.values).toEqual(serverMerged.values);
190
+ expect(Object.keys(dataToSave).length).toBeGreaterThanOrEqual(4);
191
+ });
192
+ });
193
+ });
@@ -0,0 +1,232 @@
1
+ import { useState, useMemo } from 'react';
2
+ import { Block, Filters } from '../types';
3
+
4
+ interface BlocksListProps {
5
+ blocks: Block[];
6
+ selectedBlock: Block | null;
7
+ onSelectBlock: (block: Block) => void;
8
+ loading: boolean;
9
+ }
10
+
11
+ export function BlocksList({ blocks, selectedBlock, onSelectBlock, loading }: BlocksListProps) {
12
+ const [filters, setFilters] = useState<Filters>({
13
+ search: '',
14
+ type: 'all',
15
+ category: '',
16
+ tags: [],
17
+ });
18
+ const [collapsed, setCollapsed] = useState(false);
19
+
20
+ // Extract unique categories and tags
21
+ const { categories, tags } = useMemo(() => {
22
+ const cats = new Set<string>();
23
+ const tgs = new Set<string>();
24
+
25
+ blocks.forEach((block) => {
26
+ if (block.category) cats.add(block.category);
27
+ block.tags?.forEach((tag) => tgs.add(tag));
28
+ });
29
+
30
+ return {
31
+ categories: Array.from(cats).sort(),
32
+ tags: Array.from(tgs).sort(),
33
+ };
34
+ }, [blocks]);
35
+
36
+ // Filter blocks
37
+ const filteredBlocks = useMemo(() => {
38
+ return blocks.filter((block) => {
39
+ // Search filter
40
+ if (filters.search) {
41
+ const search = filters.search.toLowerCase();
42
+ const matches =
43
+ block.name.toLowerCase().includes(search) ||
44
+ block.displayName.toLowerCase().includes(search) ||
45
+ block.description?.toLowerCase().includes(search);
46
+ if (!matches) return false;
47
+ }
48
+
49
+ // Type filter
50
+ if (filters.type !== 'all' && block.type !== filters.type) {
51
+ return false;
52
+ }
53
+
54
+ // Category filter
55
+ if (filters.category && block.category !== filters.category) {
56
+ return false;
57
+ }
58
+
59
+ // Tags filter
60
+ if (filters.tags.length > 0) {
61
+ const hasTag = filters.tags.some((tag) => block.tags?.includes(tag));
62
+ if (!hasTag) return false;
63
+ }
64
+
65
+ return true;
66
+ });
67
+ }, [blocks, filters]);
68
+
69
+ // Group by category
70
+ const groupedBlocks = useMemo(() => {
71
+ const grouped: Record<string, Block[]> = {};
72
+
73
+ filteredBlocks.forEach((block) => {
74
+ const cat = block.category || 'Uncategorized';
75
+ if (!grouped[cat]) grouped[cat] = [];
76
+ grouped[cat].push(block);
77
+ });
78
+
79
+ return Object.entries(grouped).sort(([a], [b]) => {
80
+ if (a === 'Uncategorized') return 1;
81
+ if (b === 'Uncategorized') return -1;
82
+ return a.localeCompare(b);
83
+ });
84
+ }, [filteredBlocks]);
85
+
86
+ const hasActiveFilters =
87
+ filters.search || filters.type !== 'all' || filters.category || filters.tags.length > 0;
88
+
89
+ const clearFilters = () => {
90
+ setFilters({ search: '', type: 'all', category: '', tags: [] });
91
+ };
92
+
93
+ if (collapsed) {
94
+ return (
95
+ <div className="blocks-panel collapsed">
96
+ <div className="blocks-header">
97
+ <button className="panel-toggle" onClick={() => setCollapsed(false)} title="Expand">
98
+ <span className="toggle-icon">☰</span>
99
+ </button>
100
+ </div>
101
+ </div>
102
+ );
103
+ }
104
+
105
+ return (
106
+ <div className="blocks-panel">
107
+ <div className="blocks-header">
108
+ <div className="blocks-header-content">
109
+ <h1>Blocks</h1>
110
+ <p>
111
+ {hasActiveFilters
112
+ ? `${filteredBlocks.length} of ${blocks.length} items`
113
+ : `${blocks.length} items`}
114
+ </p>
115
+ </div>
116
+ <button className="panel-toggle" onClick={() => setCollapsed(true)} title="Collapse">
117
+ <span className="toggle-icon">☰</span>
118
+ </button>
119
+ </div>
120
+
121
+ <div className="blocks-filters">
122
+ <input
123
+ type="search"
124
+ className="search-input"
125
+ placeholder="Search blocks..."
126
+ value={filters.search}
127
+ onChange={(e) => setFilters((f) => ({ ...f, search: e.target.value }))}
128
+ />
129
+
130
+ <div className="filter-tabs">
131
+ {(['all', 'block', 'template'] as const).map((type) => (
132
+ <button
133
+ key={type}
134
+ className={`filter-tab ${filters.type === type ? 'active' : ''}`}
135
+ onClick={() => setFilters((f) => ({ ...f, type }))}
136
+ >
137
+ {type === 'all' ? 'All' : type === 'block' ? 'Blocks' : 'Templates'}
138
+ </button>
139
+ ))}
140
+ </div>
141
+
142
+ <select
143
+ className="filter-select"
144
+ value={filters.category}
145
+ onChange={(e) => setFilters((f) => ({ ...f, category: e.target.value }))}
146
+ >
147
+ <option value="">All Categories</option>
148
+ {categories.map((cat) => (
149
+ <option key={cat} value={cat}>
150
+ {cat}
151
+ </option>
152
+ ))}
153
+ </select>
154
+
155
+ {tags.length > 0 && (
156
+ <div className="tags-filter">
157
+ {tags.map((tag) => (
158
+ <button
159
+ key={tag}
160
+ className={`tag-chip ${filters.tags.includes(tag) ? 'active' : ''}`}
161
+ onClick={() =>
162
+ setFilters((f) => ({
163
+ ...f,
164
+ tags: f.tags.includes(tag)
165
+ ? f.tags.filter((t) => t !== tag)
166
+ : [...f.tags, tag],
167
+ }))
168
+ }
169
+ >
170
+ {tag}
171
+ </button>
172
+ ))}
173
+ {hasActiveFilters && (
174
+ <button className="clear-filters" onClick={clearFilters}>
175
+ Clear all
176
+ </button>
177
+ )}
178
+ </div>
179
+ )}
180
+ </div>
181
+
182
+ <div className="blocks-list">
183
+ {loading ? (
184
+ <div className="loading">
185
+ <div className="spinner" />
186
+ <span>Loading blocks...</span>
187
+ </div>
188
+ ) : filteredBlocks.length === 0 ? (
189
+ <div className="no-results">
190
+ <div className="no-results-icon">🔍</div>
191
+ <div>No items match your filters</div>
192
+ {hasActiveFilters && (
193
+ <button className="btn-clear" onClick={clearFilters}>
194
+ Clear filters
195
+ </button>
196
+ )}
197
+ </div>
198
+ ) : (
199
+ groupedBlocks.map(([category, categoryBlocks]) => (
200
+ <div key={category} className="block-category">
201
+ <div className="category-header">
202
+ {category}
203
+ <span className="category-count">{categoryBlocks.length}</span>
204
+ </div>
205
+ {categoryBlocks.map((block) => (
206
+ <div
207
+ key={block.name}
208
+ className={`block-item ${selectedBlock?.name === block.name ? 'active' : ''}`}
209
+ onClick={() => onSelectBlock(block)}
210
+ >
211
+ <div className="block-item-header">
212
+ <div className="block-item-name">
213
+ {block.displayName}
214
+ {block.type === 'template' && (
215
+ <span className="type-badge template">Template</span>
216
+ )}
217
+ </div>
218
+ <span className="version-badge">v{block.version}</span>
219
+ </div>
220
+ <div className="block-item-footer">
221
+ <span className="block-item-type">{block.category || 'Block'}</span>
222
+ <span className="status-badge status-local">Local</span>
223
+ </div>
224
+ </div>
225
+ ))}
226
+ </div>
227
+ ))
228
+ )}
229
+ </div>
230
+ </div>
231
+ );
232
+ }