vzcode 1.30.1 → 1.32.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 (38) hide show
  1. package/dist/assets/{index-zzK6rRiK.css → index-DTtcN2MP.css} +1 -1
  2. package/dist/assets/index-sreAszZh.js +240 -0
  3. package/dist/assets/worker-BKfYWklR.js +136 -0
  4. package/dist/assets/{index-Dd8gk_1S.js → worker-COyDRdqW.js} +109 -103
  5. package/dist/assets/{worker-CaP6zYz7.js → worker-CWsWXMyk.js} +70 -70
  6. package/dist/index.html +2 -2
  7. package/package.json +42 -36
  8. package/src/client/AIAssist/startAIAssist.ts +0 -24
  9. package/src/client/App/index.tsx +14 -7
  10. package/src/client/CodeEditor/getOrCreateEditor.ts +80 -56
  11. package/src/client/CodeEditor/index.tsx +56 -30
  12. package/src/client/CodeEditor/typescriptExtension/worker.ts +32 -0
  13. package/src/client/RunCodeWidget/index.tsx +0 -2
  14. package/src/client/VZCodeContext.tsx +8 -25
  15. package/src/client/VZMiddle.tsx +11 -5
  16. package/src/client/VZSidebar/Search.tsx +11 -0
  17. package/src/client/VZSidebar/index.tsx +18 -11
  18. package/src/client/VZSidebar/styles.scss +27 -7
  19. package/src/client/VZSidebar/useDragAndDrop.tsx +237 -35
  20. package/src/client/main.jsx +0 -2
  21. package/src/client/useESLint/index.ts +72 -0
  22. package/src/client/useESLint/worker.ts +113 -0
  23. package/src/client/useFileCRUD.ts +0 -2
  24. package/src/server/featureFlags.js +4 -6
  25. package/src/server/setupEnv.js +0 -10
  26. package/dist/assets/index-BOiH3_fC.js +0 -234
  27. package/src/client/CodeEditor/typeScriptCompletions.ts +0 -134
  28. package/src/client/CodeEditor/typeScriptLinter.ts +0 -47
  29. package/src/client/useTypeScript/index.ts +0 -48
  30. package/src/client/useTypeScript/worker/constants.ts +0 -94
  31. package/src/client/useTypeScript/worker/getTSFileName.ts +0 -4
  32. package/src/client/useTypeScript/worker/handleMessageAutocompleteRequest.ts +0 -63
  33. package/src/client/useTypeScript/worker/handleMessageLintRequest.ts +0 -84
  34. package/src/client/useTypeScript/worker/handleMessageTranspileRequest.ts +0 -20
  35. package/src/client/useTypeScript/worker/handleMessageUpdateContent.ts +0 -42
  36. package/src/client/useTypeScript/worker/index.ts +0 -110
  37. package/src/client/useTypeScript/worker/isTS.ts +0 -3
  38. package/src/client/useTypeScript/worker/requestTypes.ts +0 -31
@@ -34,7 +34,6 @@ import { useKeyboardShortcuts } from './useKeyboardShortcuts';
34
34
  import { useOpenDirectories } from './useOpenDirectories';
35
35
  import { usePrettier } from './usePrettier';
36
36
  import { useRunCode } from './useRunCode';
37
- import { useTypeScript } from './useTypeScript';
38
37
  import { useURLSync } from './useURLSync';
39
38
  import { createInitialState, vzReducer } from './vzReducer';
40
39
  import { findPane } from './vzReducer/findPane';
@@ -110,8 +109,6 @@ export type VZCodeContextValue = {
110
109
 
111
110
  errorMessage: string | null;
112
111
 
113
- typeScriptWorker: Worker | null;
114
-
115
112
  search: SearchResults;
116
113
  isSearchOpen: boolean;
117
114
  setIsSearchOpen: (isSearchOpen: boolean) => void;
@@ -185,7 +182,6 @@ export const VZCodeProvider = ({
185
182
  localPresence,
186
183
  docPresence,
187
184
  prettierWorker,
188
- typeScriptWorker,
189
185
  initialUsername,
190
186
  children,
191
187
  codeError = null,
@@ -204,18 +200,17 @@ export const VZCodeProvider = ({
204
200
  localPresence: any;
205
201
  docPresence: any;
206
202
  prettierWorker: Worker;
207
- typeScriptWorker: Worker;
208
203
  initialUsername: Username;
209
204
  children: React.ReactNode;
210
205
  codeError?: string | null;
211
- connected: boolean;
212
- pending: boolean;
213
- liveKitToken: string | undefined;
214
- setLiveKitToken: (state: string) => void;
215
- liveKitRoomName: string | undefined;
216
- setLiveKitRoom: (state: string) => void;
217
- liveKitConnection: boolean;
218
- setLiveKitConnection: (state: boolean) => void;
206
+ connected?: boolean;
207
+ pending?: boolean;
208
+ liveKitToken?: string | undefined;
209
+ setLiveKitToken?: (state: string) => void;
210
+ liveKitRoomName?: string | undefined;
211
+ setLiveKitRoom?: (state: string) => void;
212
+ liveKitConnection?: boolean;
213
+ setLiveKitConnection?: (state: boolean) => void;
219
214
  }) => {
220
215
  // Auto-run Pretter after local changes.
221
216
  const { prettierError, runPrettierRef } = usePrettier({
@@ -240,16 +235,6 @@ export const VZCodeProvider = ({
240
235
  ? prettierError
241
236
  : codeError;
242
237
 
243
- // Set up the TypeScript Language Server worker.
244
- // This acts as a central "brain" for features powered
245
- // by the TypeScript Language Server including:
246
- // * Completions
247
- // * Linting
248
- useTypeScript({
249
- content,
250
- typeScriptWorker,
251
- });
252
-
253
238
  // Set up the reducer that manages much of the application state.
254
239
  // See https://react.dev/reference/react/useReducer
255
240
  const [state, dispatch] = useReducer(
@@ -473,8 +458,6 @@ export const VZCodeProvider = ({
473
458
 
474
459
  errorMessage,
475
460
 
476
- typeScriptWorker,
477
-
478
461
  isCreateFileModalOpen,
479
462
  handleOpenCreateFileModal,
480
463
  handleCloseCreateFileModal,
@@ -8,6 +8,10 @@ import { AIAssistWidget } from './AIAssist/AIAssistWidget';
8
8
  import { VZCodeContext } from './VZCodeContext';
9
9
  import { RunCodeWidget } from './RunCodeWidget';
10
10
  import { InteractRule } from '@replit/codemirror-interact';
11
+ import { EditorView } from '@codemirror/view';
12
+ import { Diagnostic } from '@codemirror/lint';
13
+ import { VizContent } from '@vizhub/viz-types';
14
+ import { LeafPane } from '../types';
11
15
 
12
16
  // TODO modify this to handle the SplitPane type
13
17
  // Recursive structure?
@@ -17,13 +21,13 @@ const PaneView = ({
17
21
  pane,
18
22
  content,
19
23
  customInteractRules,
20
- allowGlobals,
21
24
  enableAIAssist,
22
25
  aiAssistEndpoint,
23
26
  aiAssistOptions,
24
27
  aiAssistTooltipText,
25
28
  aiAssistClickOverride,
26
29
  aiCopilotEndpoint,
30
+ esLintSource,
27
31
  }) => {
28
32
  // This prevents the CodeEditor from rendering
29
33
  // during SSR.
@@ -49,8 +53,8 @@ const PaneView = ({
49
53
  {isClient && content && pane.activeFileId && (
50
54
  <CodeEditor
51
55
  customInteractRules={customInteractRules}
52
- allowGlobals={allowGlobals}
53
56
  aiCopilotEndpoint={aiCopilotEndpoint}
57
+ esLintSource={esLintSource}
54
58
  />
55
59
  )}
56
60
  {isClient &&
@@ -86,7 +90,7 @@ export const VZMiddle = ({
86
90
  aiAssistClickOverride,
87
91
  aiCopilotEndpoint,
88
92
  customInteractRules,
89
- allowGlobals = false,
93
+ esLintSource,
90
94
  }: {
91
95
  enableAIAssist?: boolean;
92
96
  aiAssistEndpoint?: string;
@@ -95,7 +99,9 @@ export const VZMiddle = ({
95
99
  aiAssistClickOverride?: () => void;
96
100
  aiCopilotEndpoint?: string;
97
101
  customInteractRules?: Array<InteractRule>;
98
- allowGlobals?: boolean;
102
+ esLintSource: (
103
+ view: EditorView,
104
+ ) => Promise<readonly Diagnostic[]>;
99
105
  }) => {
100
106
  const { codeEditorWidth } = useContext(
101
107
  SplitPaneResizeContext,
@@ -126,13 +132,13 @@ export const VZMiddle = ({
126
132
  pane={pane}
127
133
  content={content}
128
134
  customInteractRules={customInteractRules}
129
- allowGlobals={allowGlobals}
130
135
  enableAIAssist={enableAIAssist}
131
136
  aiAssistEndpoint={aiAssistEndpoint}
132
137
  aiAssistOptions={aiAssistOptions}
133
138
  aiAssistTooltipText={aiAssistTooltipText}
134
139
  aiAssistClickOverride={aiAssistClickOverride}
135
140
  aiCopilotEndpoint={aiCopilotEndpoint}
141
+ esLintSource={esLintSource}
136
142
  />
137
143
  <CodeErrorOverlay
138
144
  errorMessage={errorMessage}
@@ -64,6 +64,7 @@ export const Search = () => {
64
64
  setSearchFocusedIndex,
65
65
  shareDBDoc,
66
66
  editorCache,
67
+ openTab,
67
68
  } = useContext(VZCodeContext);
68
69
  const {
69
70
  pattern,
@@ -227,6 +228,9 @@ export const Search = () => {
227
228
  case 'Enter':
228
229
  case ' ':
229
230
  const fileId: VizFileId = files[focusedIndex][0];
231
+
232
+ openTab({ fileId, isTransient: true });
233
+
230
234
  setActiveFileId(fileId);
231
235
 
232
236
  if (focusedChildIndex !== null) {
@@ -441,6 +445,13 @@ export const Search = () => {
441
445
  childIndex,
442
446
  );
443
447
 
448
+ openTab({
449
+ fileId,
450
+ isTransient: true,
451
+ });
452
+
453
+ setActiveFileId(fileId);
454
+
444
455
  const cacheKey =
445
456
  editorCacheKey(
446
457
  fileId,
@@ -32,9 +32,9 @@ import { SplitPaneResizeContext } from '../SplitPaneResizeContext';
32
32
  import { VZCodeContext } from '../VZCodeContext';
33
33
  import { Listing } from './Listing';
34
34
  import { Search } from './Search';
35
- import './styles.scss';
36
35
  import { useDragAndDrop } from './useDragAndDrop';
37
36
  import { enableLiveKit } from '../featureFlags';
37
+ import './styles.scss';
38
38
 
39
39
  // TODO turn this UI back on when we are actually detecting
40
40
  // the connection status.
@@ -252,10 +252,6 @@ export const VZSidebar = ({
252
252
  <div
253
253
  className="vz-sidebar"
254
254
  style={{ width: sidebarWidth + 'px' }}
255
- onDragEnter={handleDragEnter}
256
- onDragOver={handleDragOver}
257
- onDragLeave={handleDragLeave}
258
- onDrop={handleDrop}
259
255
  >
260
256
  <div
261
257
  className="full-box"
@@ -426,10 +422,6 @@ export const VZSidebar = ({
426
422
  id="mic-icon"
427
423
  className="icon-button icon-button-dark"
428
424
  onClick={() => {
429
- console.log(
430
- 'clicking',
431
- liveKitConnection,
432
- );
433
425
  setVoiceChatModalOpen(true);
434
426
  }}
435
427
  >
@@ -438,11 +430,23 @@ export const VZSidebar = ({
438
430
  </OverlayTrigger>
439
431
  )}
440
432
  </div>
441
- <div className="files" id="sidebar-view-container">
433
+ <div
434
+ className="files"
435
+ id="sidebar-view-container"
436
+ onDragEnter={handleDragEnter}
437
+ onDragOver={handleDragOver}
438
+ onDragLeave={handleDragLeave}
439
+ onDrop={handleDrop}
440
+ >
442
441
  {!isSearchOpen ? (
443
442
  <div className="sidebar-files">
444
443
  {isDragOver ? (
445
- <div className="empty">
444
+ <div className="empty drag-over">
445
+ <div className="empty-text">
446
+ It looks like you don't have any files
447
+ yet! Click the "Create file" button
448
+ above to create your first file.
449
+ </div>
446
450
  <div className="empty-text">
447
451
  Drop files here!
448
452
  </div>
@@ -470,6 +474,9 @@ export const VZSidebar = ({
470
474
  yet! Click the "Create file" button
471
475
  above to create your first file.
472
476
  </div>
477
+ <div className="empty-text">
478
+ Drop files here!
479
+ </div>
473
480
  </div>
474
481
  )}
475
482
  </div>
@@ -228,21 +228,41 @@
228
228
  flex: 1;
229
229
  flex-direction: column;
230
230
  align-items: center;
231
- justify-content: space-evenly;
231
+ justify-content: center;
232
232
  margin: 10px;
233
233
  padding: 20px;
234
- border: 1px dashed rgba(255, 255, 255, 0.5); /* White dashed outline */
234
+ border: 1px dashed rgba(255, 255, 255, 0.5);
235
235
  border-radius: 10px;
236
-
237
- /*
238
- * This fixes some issues where the drop zone for files
239
- * was flickering when dragging files over the sidebar.
240
- */
236
+ min-height: 200px;
237
+ position: relative;
241
238
  pointer-events: none;
242
239
 
243
240
  .empty-text {
244
241
  font-size: 16px;
245
242
  font-weight: 500;
243
+ pointer-events: none;
244
+ position: absolute;
245
+ text-align: center;
246
+ left: 50%;
247
+ top: 50%;
248
+ transform: translate(-50%, -50%);
249
+ width: 80%;
250
+ opacity: 1;
251
+ transition: opacity 0.2s ease;
252
+
253
+ &:not(:first-child) {
254
+ opacity: 0;
255
+ }
256
+ }
257
+
258
+ &.drag-over {
259
+ .empty-text {
260
+ opacity: 0;
261
+
262
+ &:last-child {
263
+ opacity: 1;
264
+ }
265
+ }
246
266
  }
247
267
  }
248
268
 
@@ -2,22 +2,42 @@ import { useState, useContext, useCallback } from 'react';
2
2
  import { VZCodeContext } from '../VZCodeContext';
3
3
  import './styles.scss';
4
4
 
5
- const debug = false;
5
+ interface FileSystemEntry {
6
+ isFile: boolean;
7
+ isDirectory: boolean;
8
+ name: string;
9
+ createReader: () => FileSystemDirectoryReader;
10
+ file: (callback: (file: File) => void) => void;
11
+ }
12
+
13
+ interface FileSystemDirectoryReader {
14
+ readEntries: (
15
+ callback: (entries: FileSystemEntry[]) => void,
16
+ ) => void;
17
+ }
18
+
19
+ const DEBUG = false;
6
20
 
7
21
  export const useDragAndDrop = () => {
8
22
  const [isDragOver, setIsDragOver] = useState(false);
23
+ const [isProcessing, setIsProcessing] = useState(false);
9
24
  const { createFile } = useContext(VZCodeContext);
10
25
 
11
- const handleDragEnter = useCallback((event) => {
12
- event.preventDefault();
13
- event.stopPropagation();
14
- setIsDragOver(true);
15
- }, []);
26
+ const handleDragEnter = useCallback(
27
+ (event: React.DragEvent) => {
28
+ event.preventDefault();
29
+ event.stopPropagation();
30
+ DEBUG && console.log('[useDragAndDrop] Drag Enter');
31
+ setIsDragOver(true);
32
+ },
33
+ [],
34
+ );
16
35
 
17
36
  const handleDragOver = useCallback(
18
- (event) => {
37
+ (event: React.DragEvent) => {
19
38
  event.preventDefault();
20
39
  event.stopPropagation();
40
+ DEBUG && console.log('[useDragAndDrop] Drag Over');
21
41
  if (!isDragOver) setIsDragOver(true);
22
42
  },
23
43
  [isDragOver],
@@ -27,52 +47,234 @@ export const useDragAndDrop = () => {
27
47
  (event: React.DragEvent<HTMLDivElement>) => {
28
48
  event.preventDefault();
29
49
  event.stopPropagation();
50
+ DEBUG && console.log('[useDragAndDrop] Drag Leave');
30
51
  setIsDragOver(false);
31
52
  },
32
53
  [],
33
54
  );
34
55
 
56
+ const isTextFile = (file: File): boolean => {
57
+ // Add more text file types as needed
58
+ const textTypes = [
59
+ 'text/',
60
+ 'application/json',
61
+ 'application/javascript',
62
+ 'application/typescript',
63
+ 'application/xml',
64
+ 'application/x-httpd-php',
65
+ ];
66
+ const isText =
67
+ textTypes.some((type) =>
68
+ file.type.startsWith(type),
69
+ ) ||
70
+ file.name.match(
71
+ /\.(txt|js|jsx|ts|tsx|md|css|scss|html|xml|json|php|py|rb|java|c|cpp|h|hpp)$/i,
72
+ ) !== null;
73
+
74
+ DEBUG &&
75
+ console.log(
76
+ `[useDragAndDrop] File ${file.name} is${isText ? '' : ' not'} a text file`,
77
+ );
78
+ return isText;
79
+ };
80
+
81
+ const readFileAsText = (file: File): Promise<string> => {
82
+ return new Promise((resolve, reject) => {
83
+ DEBUG &&
84
+ console.log(
85
+ `[useDragAndDrop] Reading file: ${file.name}`,
86
+ );
87
+
88
+ if (!isTextFile(file)) {
89
+ DEBUG &&
90
+ console.log(
91
+ `[useDragAndDrop] Rejected non-text file: ${file.name}`,
92
+ );
93
+ reject(
94
+ new Error(`File ${file.name} is not a text file`),
95
+ );
96
+ return;
97
+ }
98
+
99
+ const reader = new FileReader();
100
+ reader.onload = (e) => {
101
+ if (e.target?.result) {
102
+ DEBUG &&
103
+ console.log(
104
+ `[useDragAndDrop] Successfully read file: ${file.name}`,
105
+ );
106
+ resolve(e.target.result as string);
107
+ } else {
108
+ DEBUG &&
109
+ console.log(
110
+ `[useDragAndDrop] Failed to read file: ${file.name}`,
111
+ );
112
+ reject(
113
+ new Error(`Failed to read file ${file.name}`),
114
+ );
115
+ }
116
+ };
117
+ reader.onerror = () => {
118
+ DEBUG &&
119
+ console.log(
120
+ `[useDragAndDrop] Error reading file: ${file.name}`,
121
+ );
122
+ reject(
123
+ new Error(`Error reading file ${file.name}`),
124
+ );
125
+ };
126
+ reader.readAsText(file);
127
+ });
128
+ };
129
+
130
+ const processEntry = async (
131
+ entry: FileSystemEntry,
132
+ path: string,
133
+ ): Promise<void> => {
134
+ try {
135
+ DEBUG &&
136
+ console.log(
137
+ `[useDragAndDrop] Processing entry: ${entry.name} at path: ${path}`,
138
+ );
139
+
140
+ if (entry.isFile) {
141
+ DEBUG &&
142
+ console.log(
143
+ `[useDragAndDrop] Processing file: ${entry.name}`,
144
+ );
145
+ entry.file(async (file) => {
146
+ try {
147
+ const content = await readFileAsText(file);
148
+ DEBUG &&
149
+ console.log(
150
+ `[useDragAndDrop] Creating file: ${path}${file.name}`,
151
+ );
152
+ createFile(`${path}${file.name}`, content);
153
+ } catch (error) {
154
+ console.error(
155
+ `Error processing file ${file.name}:`,
156
+ error,
157
+ );
158
+ }
159
+ });
160
+ } else if (entry.isDirectory) {
161
+ DEBUG &&
162
+ console.log(
163
+ `[useDragAndDrop] Processing directory: ${entry.name}`,
164
+ );
165
+ return new Promise<void>((resolve) => {
166
+ const dirReader = entry.createReader();
167
+ const readEntries = () => {
168
+ dirReader.readEntries(async (entries) => {
169
+ if (entries.length === 0) {
170
+ DEBUG &&
171
+ console.log(
172
+ `[useDragAndDrop] Finished reading directory: ${entry.name}`,
173
+ );
174
+ resolve();
175
+ return;
176
+ }
177
+
178
+ const newPath = `${path}${entry.name}/`;
179
+ DEBUG &&
180
+ console.log(
181
+ `[useDragAndDrop] Reading ${entries.length} entries in directory: ${newPath}`,
182
+ );
183
+ await Promise.all(
184
+ entries.map((entry) =>
185
+ processEntry(entry, newPath),
186
+ ),
187
+ );
188
+ readEntries(); // Continue reading if there are more entries
189
+ });
190
+ };
191
+ readEntries();
192
+ });
193
+ }
194
+ } catch (error) {
195
+ console.error(
196
+ `Error processing entry ${entry.name}:`,
197
+ error,
198
+ );
199
+ }
200
+ };
201
+
35
202
  const handleDrop = useCallback(
36
- (event: React.DragEvent<HTMLDivElement>) => {
203
+ async (event: React.DragEvent<HTMLDivElement>) => {
37
204
  event.preventDefault();
38
205
  event.stopPropagation();
206
+ DEBUG &&
207
+ console.log('[useDragAndDrop] Drop event started');
39
208
  setIsDragOver(false);
209
+ setIsProcessing(true);
40
210
 
41
- const items = event.dataTransfer.items;
42
-
43
- const processEntry = (entry, path) => {
44
- if (entry.isFile) {
45
- entry.file((file) => {
46
- const reader = new FileReader();
47
- reader.onload = (e) => {
48
- const content = e.target.result as string;
49
- createFile(`${path}${file.name}`, content);
50
- };
51
- reader.readAsText(file);
52
- });
53
- } else if (entry.isDirectory) {
54
- const dirReader = entry.createReader();
55
- dirReader.readEntries((entries) => {
56
- const newPath = `${path}${entry.name}/`;
57
- for (const entry of entries) {
58
- processEntry(entry, newPath);
211
+ try {
212
+ const items = event.dataTransfer.items;
213
+ DEBUG &&
214
+ console.log(
215
+ `[useDragAndDrop] Processing ${items.length} dropped items`,
216
+ );
217
+ const entries: FileSystemEntry[] = [];
218
+
219
+ for (const item of Array.from(items)) {
220
+ // Try webkit first, then fallback to other methods
221
+ const entry =
222
+ item.webkitGetAsEntry?.() ||
223
+ (item as any).getAsEntry?.() ||
224
+ (item as any).createEntry?.();
225
+
226
+ if (entry) {
227
+ DEBUG &&
228
+ console.log(
229
+ `[useDragAndDrop] Got entry for: ${entry.name}`,
230
+ );
231
+ entries.push(entry as FileSystemEntry);
232
+ } else if (item.kind === 'file') {
233
+ const file = item.getAsFile();
234
+ if (file) {
235
+ DEBUG &&
236
+ console.log(
237
+ `[useDragAndDrop] Processing dropped file: ${file.name}`,
238
+ );
239
+ try {
240
+ const content = await readFileAsText(file);
241
+ createFile(file.name, content);
242
+ } catch (error) {
243
+ console.error(
244
+ `Error processing file ${file.name}:`,
245
+ error,
246
+ );
247
+ }
59
248
  }
60
- });
249
+ }
61
250
  }
62
- };
63
- const itemsArray = Array.from(items);
64
- itemsArray.forEach((item) => {
65
- const entry = item.webkitGetAsEntry();
66
- if (entry) {
67
- processEntry(entry, '');
68
- }
69
- });
251
+
252
+ DEBUG &&
253
+ console.log(
254
+ `[useDragAndDrop] Processing ${entries.length} entries`,
255
+ );
256
+ await Promise.all(
257
+ entries.map((entry) => processEntry(entry, '')),
258
+ );
259
+ } catch (error) {
260
+ console.error(
261
+ 'Error processing dropped items:',
262
+ error,
263
+ );
264
+ } finally {
265
+ DEBUG &&
266
+ console.log(
267
+ '[useDragAndDrop] Drop processing completed',
268
+ );
269
+ setIsProcessing(false);
270
+ }
70
271
  },
71
272
  [createFile],
72
273
  );
73
274
 
74
275
  return {
75
276
  isDragOver,
277
+ isProcessing,
76
278
  handleDragEnter,
77
279
  handleDragOver,
78
280
  handleDragLeave,
@@ -4,9 +4,7 @@ import App from './App';
4
4
  import { BrowserRouter } from 'react-router-dom';
5
5
 
6
6
  ReactDOM.createRoot(document.getElementById('root')).render(
7
- // <React.StrictMode>
8
7
  <BrowserRouter>
9
8
  <App />
10
9
  </BrowserRouter>,
11
- // </React.StrictMode>,
12
10
  );
@@ -0,0 +1,72 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { EditorView } from '@codemirror/view';
3
+ import { Diagnostic } from '@codemirror/lint';
4
+ // @ts-ignore - Worker import
5
+ import ESLintWorker from './worker?worker';
6
+ import { enableESLint } from '../../server/featureFlags';
7
+ import { fileNameStateField } from '../CodeEditor/getOrCreateEditor';
8
+
9
+ let requestCounter = 0;
10
+ const pendingRequests = new Map();
11
+
12
+ export const useESLint = () => {
13
+ const [worker, setWorker] = useState(null);
14
+
15
+ // Initialize the work in an effect so it does not run during SSR.
16
+ useEffect(() => {
17
+ setWorker(new ESLintWorker());
18
+ }, []);
19
+
20
+ useEffect(() => {
21
+ // Wait for the worker to be initialized
22
+ if (!worker) return;
23
+
24
+ const handleMessage = (event: MessageEvent) => {
25
+ const { diagnostics, requestId, error } = event.data;
26
+
27
+ if (error) {
28
+ console.error('[ESLint] Error:', error);
29
+ }
30
+
31
+ const resolve = pendingRequests.get(requestId);
32
+ if (resolve) {
33
+ resolve(diagnostics);
34
+ pendingRequests.delete(requestId);
35
+ }
36
+ };
37
+
38
+ worker.addEventListener('message', handleMessage);
39
+
40
+ return () => {
41
+ worker.removeEventListener('message', handleMessage);
42
+ worker.terminate();
43
+ };
44
+ }, [worker]);
45
+
46
+ const esLintSource = useCallback(
47
+ async (
48
+ view: EditorView,
49
+ ): Promise<readonly Diagnostic[]> => {
50
+ if (!enableESLint) return [];
51
+
52
+ // Retrieve the file name from the editor's state
53
+ const fileName = view.state.field(fileNameStateField);
54
+
55
+ try {
56
+ const code = view.state.doc.toString();
57
+ const requestId = requestCounter++;
58
+
59
+ return new Promise<Diagnostic[]>((resolve) => {
60
+ pendingRequests.set(requestId, resolve);
61
+ worker.postMessage({ code, requestId, fileName });
62
+ });
63
+ } catch (e) {
64
+ console.error('[ESLint] Error in linter:', e);
65
+ return [];
66
+ }
67
+ },
68
+ [worker],
69
+ );
70
+
71
+ return { esLintSource };
72
+ };