vzcode 1.62.0 → 2.2.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.
- package/dist/assets/{index-Br76WttW.css → index-BM8tDh6M.css} +1 -1
- package/dist/assets/{index-vbF7Aejt.js → index-CpxqGhXj.js} +163 -163
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/client/Icons/MicOffSVG.tsx +35 -0
- package/src/client/Icons/index.tsx +2 -0
- package/src/client/VZCodeContext/types.ts +1 -0
- package/src/client/VZCodeContext/useVZCodeState.ts +2 -0
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +58 -15
- package/src/client/VZSidebar/AIChat/MessageList.tsx +23 -0
- package/src/client/VZSidebar/AIChat/useSpeechRecognition.ts +117 -0
- package/src/client/VZSidebar/DeleteConfirmationModal.tsx +15 -4
- package/src/client/VZSidebar/VisualEditor.tsx +120 -133
- package/src/client/VZSidebar/aiCopyPaste.ts +1 -1
- package/src/client/VZSidebar/index.tsx +42 -0
- package/src/client/VZSidebar/styles.scss +15 -0
- package/src/client/useFileCRUD.ts +14 -0
- package/src/server/aiChatHandler/aiEditing.ts +50 -1
- package/src/server/prettier.ts +115 -0
|
@@ -12,13 +12,36 @@ import { EmptyState } from './EmptyState';
|
|
|
12
12
|
|
|
13
13
|
const CONFIG_FILE_NAME = 'config.json';
|
|
14
14
|
|
|
15
|
+
// Helper function to calculate percentage for progress fill
|
|
16
|
+
const calculatePercentage = (
|
|
17
|
+
value: number,
|
|
18
|
+
min: number,
|
|
19
|
+
max: number,
|
|
20
|
+
) => {
|
|
21
|
+
return ((value - min) / (max - min)) * 100;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Helper function to format values nicely
|
|
25
|
+
const formatValue = (
|
|
26
|
+
value: number,
|
|
27
|
+
min: number,
|
|
28
|
+
max: number,
|
|
29
|
+
) => {
|
|
30
|
+
// Determine decimal places based on the range
|
|
31
|
+
const range = max - min;
|
|
32
|
+
const decimalPlaces =
|
|
33
|
+
range < 10 ? 2 : range < 100 ? 1 : 0;
|
|
34
|
+
return Number(value).toFixed(decimalPlaces);
|
|
35
|
+
};
|
|
36
|
+
|
|
15
37
|
export const VisualEditor = () => {
|
|
16
|
-
const {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
38
|
+
const { files, submitOperation, iframeRef } =
|
|
39
|
+
useContext(VZCodeContext);
|
|
40
|
+
|
|
41
|
+
// Local state to track slider values during user interaction
|
|
42
|
+
const [localValues, setLocalValues] = useState<{
|
|
43
|
+
[key: string]: number;
|
|
44
|
+
}>({});
|
|
22
45
|
|
|
23
46
|
let configFileId: VizFileId | null = null;
|
|
24
47
|
for (const fileId in files) {
|
|
@@ -70,22 +93,23 @@ export const VisualEditor = () => {
|
|
|
70
93
|
);
|
|
71
94
|
}
|
|
72
95
|
|
|
73
|
-
const
|
|
74
|
-
(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
//TODO: test race condition in which someone is editing the config file as another user uses the visual editor
|
|
96
|
+
const onSliderChange = useCallback(
|
|
97
|
+
(property: string) =>
|
|
98
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
99
|
+
const newValue = parseFloat(
|
|
100
|
+
event.currentTarget.value,
|
|
101
|
+
);
|
|
80
102
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
103
|
+
// Update local state immediately for responsive UI
|
|
104
|
+
setLocalValues((prev) => ({
|
|
105
|
+
...prev,
|
|
106
|
+
[property]: newValue,
|
|
107
|
+
}));
|
|
85
108
|
|
|
109
|
+
// Update config.json
|
|
86
110
|
const newConfigData = {
|
|
87
111
|
...configData,
|
|
88
|
-
[property]:
|
|
112
|
+
[property]: newValue,
|
|
89
113
|
};
|
|
90
114
|
|
|
91
115
|
submitOperation((document: VizContent) => ({
|
|
@@ -98,140 +122,104 @@ export const VisualEditor = () => {
|
|
|
98
122
|
},
|
|
99
123
|
},
|
|
100
124
|
}));
|
|
101
|
-
|
|
102
|
-
iframeRef.current.contentWindow.postMessage({
|
|
103
|
-
[property]: newValueOfConsistentType,
|
|
104
|
-
});
|
|
105
125
|
},
|
|
106
|
-
[configData, files, configFileId],
|
|
126
|
+
[configData, files, configFileId, setLocalValues],
|
|
107
127
|
);
|
|
108
128
|
|
|
109
129
|
const visualEditorWidgets: VisualEditorConfigEntry[] =
|
|
110
130
|
configData.visualEditorWidgets;
|
|
111
131
|
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
string
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
[key: string]: HTMLInputElement | null;
|
|
124
|
-
}>({});
|
|
125
|
-
|
|
126
|
-
// Helper function to format values nicely
|
|
127
|
-
const formatValue = (
|
|
128
|
-
value: number,
|
|
129
|
-
min: number,
|
|
130
|
-
max: number,
|
|
131
|
-
) => {
|
|
132
|
-
// Determine decimal places based on the range
|
|
133
|
-
const range = max - min;
|
|
134
|
-
const decimalPlaces =
|
|
135
|
-
range < 10 ? 2 : range < 100 ? 1 : 0;
|
|
136
|
-
return Number(value).toFixed(decimalPlaces);
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
// Helper function to calculate percentage for progress fill
|
|
140
|
-
const calculatePercentage = (
|
|
141
|
-
value: number,
|
|
142
|
-
min: number,
|
|
143
|
-
max: number,
|
|
144
|
-
) => {
|
|
145
|
-
return ((value - min) / (max - min)) * 100;
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
// Enhanced input handler with animations
|
|
149
|
-
const onInputUpdateEnhanced = useCallback(
|
|
150
|
-
(
|
|
151
|
-
property: string,
|
|
152
|
-
previousValue: any,
|
|
153
|
-
widgetConfig: VisualEditorConfigEntry,
|
|
154
|
-
): React.FormEventHandler<HTMLInputElement> =>
|
|
155
|
-
(event) => {
|
|
156
|
-
const newValueOfConsistentType =
|
|
157
|
-
typeof previousValue === 'number'
|
|
158
|
-
? parseFloat(event.currentTarget.value)
|
|
159
|
-
: event.currentTarget.value;
|
|
132
|
+
// Sync local values with config data when it changes (including remote updates)
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
const newLocalValues: { [key: string]: number } = {};
|
|
135
|
+
visualEditorWidgets.forEach((widget) => {
|
|
136
|
+
if (widget.type === 'number') {
|
|
137
|
+
newLocalValues[widget.property] =
|
|
138
|
+
configData[widget.property];
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
setLocalValues(newLocalValues);
|
|
142
|
+
}, [configData, visualEditorWidgets]);
|
|
160
143
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
[property]: newValueOfConsistentType,
|
|
164
|
-
};
|
|
144
|
+
// Track previous config state to detect changes from any source (remote clients, text editor, etc.)
|
|
145
|
+
const previousConfigRef = useRef<any>(null);
|
|
165
146
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
name: 'config.json',
|
|
172
|
-
text: JSON.stringify(newConfigData, null, 2),
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
}));
|
|
147
|
+
// Detect config.json changes and send updates to iframe
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (!configFileId || !files || !files[configFileId]) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
176
152
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
153
|
+
let newConfigData;
|
|
154
|
+
try {
|
|
155
|
+
newConfigData = JSON.parse(files[configFileId].text);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
// If config is invalid JSON, we can't process changes
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
180
160
|
|
|
181
|
-
|
|
182
|
-
const sliderElement = sliderRefs.current[property];
|
|
183
|
-
if (sliderElement) {
|
|
184
|
-
sliderElement.classList.add('value-changed');
|
|
185
|
-
setTimeout(() => {
|
|
186
|
-
sliderElement.classList.remove('value-changed');
|
|
187
|
-
}, 300);
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
[configData, files, configFileId],
|
|
191
|
-
);
|
|
161
|
+
const previousConfig = previousConfigRef.current;
|
|
192
162
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
setIsDragging(property);
|
|
196
|
-
setActiveSlider(property);
|
|
197
|
-
};
|
|
163
|
+
// Update the ref with the new config
|
|
164
|
+
previousConfigRef.current = newConfigData;
|
|
198
165
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
166
|
+
// Skip processing if this is the first time or if there's no previous config
|
|
167
|
+
if (!previousConfig) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
202
170
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
};
|
|
171
|
+
// Find changed top-level properties
|
|
172
|
+
const changedProperties: { [key: string]: any } = {};
|
|
206
173
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
174
|
+
// Check all properties in the new config
|
|
175
|
+
for (const key in newConfigData) {
|
|
176
|
+
if (newConfigData[key] !== previousConfig[key]) {
|
|
177
|
+
// Deep comparison for objects to detect actual changes
|
|
178
|
+
if (
|
|
179
|
+
typeof newConfigData[key] === 'object' &&
|
|
180
|
+
typeof previousConfig[key] === 'object'
|
|
181
|
+
) {
|
|
182
|
+
if (
|
|
183
|
+
JSON.stringify(newConfigData[key]) !==
|
|
184
|
+
JSON.stringify(previousConfig[key])
|
|
185
|
+
) {
|
|
186
|
+
changedProperties[key] = newConfigData[key];
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
changedProperties[key] = newConfigData[key];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
211
193
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
194
|
+
// Check for deleted properties (properties that existed before but don't exist now)
|
|
195
|
+
for (const key in previousConfig) {
|
|
196
|
+
if (!(key in newConfigData)) {
|
|
197
|
+
changedProperties[key] = undefined;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
217
200
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
201
|
+
// Send changed properties to iframe if any changes were detected
|
|
202
|
+
if (Object.keys(changedProperties).length > 0) {
|
|
203
|
+
try {
|
|
204
|
+
iframeRef.current.contentWindow.postMessage(
|
|
205
|
+
changedProperties,
|
|
206
|
+
);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error(
|
|
209
|
+
'Failed to send config changes to iframe:',
|
|
210
|
+
error,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}, [files, configFileId, iframeRef]);
|
|
229
215
|
|
|
230
216
|
return (
|
|
231
217
|
<div className="visual-editor">
|
|
232
218
|
{visualEditorWidgets.map((widgetConfig, index) => {
|
|
233
219
|
if (widgetConfig.type === 'number') {
|
|
220
|
+
// Use local value if available, otherwise fall back to config value
|
|
234
221
|
const currentValue =
|
|
222
|
+
localValues[widgetConfig.property] ??
|
|
235
223
|
configData[widgetConfig.property];
|
|
236
224
|
const percentage = calculatePercentage(
|
|
237
225
|
currentValue,
|
|
@@ -267,11 +255,10 @@ export const VisualEditor = () => {
|
|
|
267
255
|
min={widgetConfig.min}
|
|
268
256
|
max={widgetConfig.max}
|
|
269
257
|
step="any"
|
|
270
|
-
|
|
258
|
+
value={currentValue}
|
|
259
|
+
onChange={onSliderChange(
|
|
271
260
|
widgetConfig.property,
|
|
272
|
-
configData[widgetConfig.property],
|
|
273
261
|
)}
|
|
274
|
-
defaultValue={currentValue}
|
|
275
262
|
/>
|
|
276
263
|
<div
|
|
277
264
|
className="slider-track-fill"
|
|
@@ -35,6 +35,7 @@ import { VZCodeContext } from '../VZCodeContext';
|
|
|
35
35
|
import { AIChat } from './AIChat';
|
|
36
36
|
import { Listing } from './Listing';
|
|
37
37
|
import { Search } from './Search';
|
|
38
|
+
import { DeleteConfirmationModal } from './DeleteConfirmationModal';
|
|
38
39
|
import { useDragAndDrop } from './useDragAndDrop';
|
|
39
40
|
import { createAICopyPasteHandlers } from './aiCopyPaste';
|
|
40
41
|
import {
|
|
@@ -157,6 +158,7 @@ export const VZSidebar = ({
|
|
|
157
158
|
updatePresenceIndicator,
|
|
158
159
|
setVoiceChatModalOpen,
|
|
159
160
|
submitOperation,
|
|
161
|
+
deleteAllFiles,
|
|
160
162
|
} = useContext(VZCodeContext);
|
|
161
163
|
|
|
162
164
|
const fileTree = useMemo(
|
|
@@ -178,6 +180,10 @@ export const VZSidebar = ({
|
|
|
178
180
|
const [exportButtonText, setExportButtonText] =
|
|
179
181
|
useState('Export to ZIP');
|
|
180
182
|
|
|
183
|
+
// State for delete all confirmation modal
|
|
184
|
+
const [showDeleteAllModal, setShowDeleteAllModal] =
|
|
185
|
+
useState(false);
|
|
186
|
+
|
|
181
187
|
// Create AI copy/paste handlers
|
|
182
188
|
const {
|
|
183
189
|
handleCopyForAI,
|
|
@@ -195,6 +201,20 @@ export const VZSidebar = ({
|
|
|
195
201
|
[files, submitOperation],
|
|
196
202
|
);
|
|
197
203
|
|
|
204
|
+
// Delete all handlers
|
|
205
|
+
const handleDeleteAllClick = useCallback(() => {
|
|
206
|
+
setShowDeleteAllModal(true);
|
|
207
|
+
}, []);
|
|
208
|
+
|
|
209
|
+
const handleDeleteAllModalClose = useCallback(() => {
|
|
210
|
+
setShowDeleteAllModal(false);
|
|
211
|
+
}, []);
|
|
212
|
+
|
|
213
|
+
const handleDeleteAllConfirm = useCallback(() => {
|
|
214
|
+
setShowDeleteAllModal(false);
|
|
215
|
+
deleteAllFiles();
|
|
216
|
+
}, [deleteAllFiles]);
|
|
217
|
+
|
|
198
218
|
const { sidebarWidth, setSidebarView } = useContext(
|
|
199
219
|
SplitPaneResizeContext,
|
|
200
220
|
);
|
|
@@ -653,6 +673,17 @@ export const VZSidebar = ({
|
|
|
653
673
|
<i className="bi bi-clipboard-plus"></i>
|
|
654
674
|
{pasteButtonText}
|
|
655
675
|
</button>
|
|
676
|
+
|
|
677
|
+
{filesExist && (
|
|
678
|
+
<button
|
|
679
|
+
className="ai-button delete-all-button"
|
|
680
|
+
onClick={handleDeleteAllClick}
|
|
681
|
+
title="Delete all files"
|
|
682
|
+
>
|
|
683
|
+
<i className="bi bi-trash"></i>
|
|
684
|
+
delete all
|
|
685
|
+
</button>
|
|
686
|
+
)}
|
|
656
687
|
</div>
|
|
657
688
|
)}
|
|
658
689
|
{enableConnectionStatus && (
|
|
@@ -681,6 +712,17 @@ export const VZSidebar = ({
|
|
|
681
712
|
</div>
|
|
682
713
|
</div>
|
|
683
714
|
)}
|
|
715
|
+
|
|
716
|
+
{/* Delete All Files Confirmation Modal */}
|
|
717
|
+
{showDeleteAllModal && (
|
|
718
|
+
<DeleteConfirmationModal
|
|
719
|
+
show={showDeleteAllModal}
|
|
720
|
+
onClose={handleDeleteAllModalClose}
|
|
721
|
+
onConfirm={handleDeleteAllConfirm}
|
|
722
|
+
isDirectory={false}
|
|
723
|
+
name="all files"
|
|
724
|
+
/>
|
|
725
|
+
)}
|
|
684
726
|
</div>
|
|
685
727
|
);
|
|
686
728
|
};
|
|
@@ -357,6 +357,21 @@
|
|
|
357
357
|
border-color: var(--vh-color-success-04);
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
|
+
|
|
361
|
+
&.delete-all-button {
|
|
362
|
+
border-color: var(--vh-color-neutral-02);
|
|
363
|
+
color: var(--vh-color-neutral-03);
|
|
364
|
+
font-size: 11px;
|
|
365
|
+
font-weight: 400;
|
|
366
|
+
opacity: 0.7;
|
|
367
|
+
|
|
368
|
+
&:hover {
|
|
369
|
+
background: var(--vh-color-hover-dark);
|
|
370
|
+
border-color: var(--vh-color-danger-03);
|
|
371
|
+
color: var(--vh-color-danger-04);
|
|
372
|
+
opacity: 1;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
360
375
|
}
|
|
361
376
|
}
|
|
362
377
|
|
|
@@ -203,6 +203,19 @@ export const useFileCRUD = ({
|
|
|
203
203
|
[submitOperation, closeTabs],
|
|
204
204
|
);
|
|
205
205
|
|
|
206
|
+
// Deletes all files
|
|
207
|
+
const deleteAllFiles = useCallback(() => {
|
|
208
|
+
const tabsToClose: Array<VizFileId> = [];
|
|
209
|
+
submitOperation((document: VizContent) => {
|
|
210
|
+
// Collect all file IDs for closing tabs
|
|
211
|
+
for (const fileId in document.files) {
|
|
212
|
+
tabsToClose.push(fileId);
|
|
213
|
+
}
|
|
214
|
+
return { ...document, files: {} };
|
|
215
|
+
});
|
|
216
|
+
closeTabs(tabsToClose);
|
|
217
|
+
}, [submitOperation, closeTabs]);
|
|
218
|
+
|
|
206
219
|
return {
|
|
207
220
|
createFile,
|
|
208
221
|
renameFile,
|
|
@@ -210,5 +223,6 @@ export const useFileCRUD = ({
|
|
|
210
223
|
createDirectory,
|
|
211
224
|
renameDirectory,
|
|
212
225
|
deleteDirectory,
|
|
226
|
+
deleteAllFiles,
|
|
213
227
|
};
|
|
214
228
|
};
|
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
createFilesSnapshot,
|
|
8
8
|
generateFilesUnifiedDiff,
|
|
9
9
|
} from '../../utils/fileDiff.js';
|
|
10
|
+
import { formatFiles } from '../prettier.js';
|
|
11
|
+
import { createSubmitOperation } from '../../submitOperation.js';
|
|
12
|
+
import { VizContent } from '@vizhub/viz-types';
|
|
10
13
|
|
|
11
14
|
// Dev flag for waiting 1 second before starting the LLM function.
|
|
12
15
|
// Useful for debugging and testing purposes, e.g. checking the typing indicator.
|
|
@@ -83,9 +86,55 @@ export const performAIEditing = async ({
|
|
|
83
86
|
// Call the LLM function which will handle streaming and incremental file updates
|
|
84
87
|
const result = await llmFunction(fullPrompt);
|
|
85
88
|
|
|
89
|
+
// 3. Run Prettier on changed files before running code
|
|
90
|
+
const afterLLMFiles = createFilesSnapshot(
|
|
91
|
+
shareDBDoc.data.files,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Find files that were changed by the AI
|
|
95
|
+
const changedFileIds = Object.keys(afterLLMFiles).filter(
|
|
96
|
+
(fileId) =>
|
|
97
|
+
beforeFiles[fileId]?.text !==
|
|
98
|
+
afterLLMFiles[fileId]?.text,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (changedFileIds.length > 0) {
|
|
102
|
+
// Format the changed files
|
|
103
|
+
const formattedFiles = await formatFiles(
|
|
104
|
+
shareDBDoc.data.files,
|
|
105
|
+
changedFileIds,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Apply formatted versions to shareDBDoc if formatting succeeded
|
|
109
|
+
if (Object.keys(formattedFiles).length > 0) {
|
|
110
|
+
const submitOperation =
|
|
111
|
+
createSubmitOperation(shareDBDoc);
|
|
112
|
+
submitOperation((document: VizContent) => {
|
|
113
|
+
const updatedFiles = { ...document.files };
|
|
114
|
+
|
|
115
|
+
// Apply each formatted file
|
|
116
|
+
Object.entries(formattedFiles).forEach(
|
|
117
|
+
([fileId, formattedText]) => {
|
|
118
|
+
if (updatedFiles[fileId]) {
|
|
119
|
+
updatedFiles[fileId] = {
|
|
120
|
+
...updatedFiles[fileId],
|
|
121
|
+
text: formattedText,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
...document,
|
|
129
|
+
files: updatedFiles,
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
86
135
|
runCode();
|
|
87
136
|
|
|
88
|
-
//
|
|
137
|
+
// 4. Capture the state of files after editing and formatting, then generate diff
|
|
89
138
|
const afterFiles = createFilesSnapshot(
|
|
90
139
|
shareDBDoc.data.files,
|
|
91
140
|
);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { format } from 'prettier/standalone';
|
|
2
|
+
import * as prettierPluginBabel from 'prettier/plugins/babel';
|
|
3
|
+
import * as prettierPluginEstree from 'prettier/plugins/estree';
|
|
4
|
+
import * as prettierPluginHtml from 'prettier/plugins/html';
|
|
5
|
+
import * as prettierPluginMarkdown from 'prettier/plugins/markdown';
|
|
6
|
+
import * as prettierPluginCSS from 'prettier/plugins/postcss';
|
|
7
|
+
import * as prettierPluginTypescript from 'prettier/plugins/typescript';
|
|
8
|
+
import { VizFiles, VizFileId } from '@vizhub/viz-types';
|
|
9
|
+
|
|
10
|
+
// Parser mappings - matches client-side implementation
|
|
11
|
+
const parsers = {
|
|
12
|
+
js: 'babel',
|
|
13
|
+
jsx: 'babel',
|
|
14
|
+
mjs: 'babel',
|
|
15
|
+
cjs: 'babel',
|
|
16
|
+
ts: 'typescript',
|
|
17
|
+
tsx: 'typescript',
|
|
18
|
+
css: 'css',
|
|
19
|
+
html: 'html',
|
|
20
|
+
json: 'json',
|
|
21
|
+
json5: 'json5',
|
|
22
|
+
md: 'markdown',
|
|
23
|
+
markdown: 'markdown',
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
// Prettier plugins - matches client-side implementation
|
|
27
|
+
const plugins = [
|
|
28
|
+
prettierPluginBabel,
|
|
29
|
+
prettierPluginEstree,
|
|
30
|
+
prettierPluginHtml,
|
|
31
|
+
prettierPluginMarkdown,
|
|
32
|
+
prettierPluginTypescript,
|
|
33
|
+
prettierPluginCSS,
|
|
34
|
+
] as any;
|
|
35
|
+
|
|
36
|
+
// Prettier options - matches client-side implementation and .prettierrc
|
|
37
|
+
const prettierOptions = {
|
|
38
|
+
proseWrap: 'always' as const,
|
|
39
|
+
singleQuote: true,
|
|
40
|
+
printWidth: 60,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extracts file extension from filename
|
|
45
|
+
* Example: 'foo.js' => 'js'
|
|
46
|
+
*/
|
|
47
|
+
const getFileExtension = (fileName: string): string => {
|
|
48
|
+
const match = fileName.match(/\.([^.]+)$/);
|
|
49
|
+
return match ? match[1] : '';
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Formats a single file using Prettier
|
|
54
|
+
*/
|
|
55
|
+
export const formatFile = async (
|
|
56
|
+
fileText: string,
|
|
57
|
+
fileName: string,
|
|
58
|
+
): Promise<string | null> => {
|
|
59
|
+
const fileExtension = getFileExtension(fileName);
|
|
60
|
+
const parser =
|
|
61
|
+
parsers[fileExtension as keyof typeof parsers];
|
|
62
|
+
|
|
63
|
+
// If no parser is found, return null (no formatting)
|
|
64
|
+
if (!parser) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const formatted = await format(fileText, {
|
|
70
|
+
parser,
|
|
71
|
+
plugins,
|
|
72
|
+
...prettierOptions,
|
|
73
|
+
});
|
|
74
|
+
return formatted;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
// Log error but don't throw - return original text
|
|
77
|
+
console.error(
|
|
78
|
+
`Prettier formatting error for ${fileName}:`,
|
|
79
|
+
error,
|
|
80
|
+
);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Formats multiple files using Prettier
|
|
87
|
+
* Only formats files that have supported extensions
|
|
88
|
+
* Returns a map of fileId -> formatted text for successfully formatted files
|
|
89
|
+
*/
|
|
90
|
+
export const formatFiles = async (
|
|
91
|
+
files: VizFiles,
|
|
92
|
+
fileIds?: VizFileId[],
|
|
93
|
+
): Promise<{ [fileId: VizFileId]: string }> => {
|
|
94
|
+
const results: { [fileId: VizFileId]: string } = {};
|
|
95
|
+
const targetFileIds = fileIds || Object.keys(files);
|
|
96
|
+
|
|
97
|
+
// Process files in parallel for better performance
|
|
98
|
+
const formatPromises = targetFileIds.map(
|
|
99
|
+
async (fileId) => {
|
|
100
|
+
const file = files[fileId];
|
|
101
|
+
if (!file) return;
|
|
102
|
+
|
|
103
|
+
const formatted = await formatFile(
|
|
104
|
+
file.text,
|
|
105
|
+
file.name,
|
|
106
|
+
);
|
|
107
|
+
if (formatted !== null && formatted !== file.text) {
|
|
108
|
+
results[fileId] = formatted;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
await Promise.all(formatPromises);
|
|
114
|
+
return results;
|
|
115
|
+
};
|