vzcode 1.31.0 → 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.
package/dist/index.html CHANGED
@@ -20,7 +20,7 @@
20
20
  href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"
21
21
  rel="stylesheet"
22
22
  />
23
- <script type="module" crossorigin src="/assets/index-D01yzCWj.js"></script>
23
+ <script type="module" crossorigin src="/assets/index-sreAszZh.js"></script>
24
24
  <link rel="stylesheet" crossorigin href="/assets/index-DTtcN2MP.css">
25
25
  </head>
26
26
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "1.31.0",
3
+ "version": "1.32.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -107,72 +107,79 @@
107
107
  "@codemirror/autocomplete": "^6.18.6",
108
108
  "@codemirror/lang-css": "^6.3.1",
109
109
  "@codemirror/lang-html": "^6.4.9",
110
- "@codemirror/lang-javascript": "^6.2.3",
110
+ "@codemirror/lang-javascript": "^6.2.4",
111
111
  "@codemirror/lang-json": "^6.0.1",
112
112
  "@codemirror/lang-markdown": "^6.3.2",
113
+ "@codemirror/lint": "^6.8.5",
113
114
  "@codemirror/state": "^6.5.2",
114
115
  "@codemirror/theme-one-dark": "^6.1.2",
115
- "@codemirror/view": "^6.36.5",
116
- "@langchain/core": "^0.3.43",
117
- "@langchain/openai": "^0.5.4",
116
+ "@codemirror/view": "^6.37.1",
117
+ "@langchain/core": "^0.3.57",
118
+ "@langchain/openai": "^0.5.12",
118
119
  "@lezer/highlight": "^1.2.1",
119
- "@livekit/components-react": "^2.9.0",
120
+ "@livekit/components-react": "^2.9.9",
120
121
  "@replit/codemirror-indentation-markers": "^6.5.3",
121
122
  "@replit/codemirror-interact": "^6.3.1",
122
123
  "@replit/codemirror-lang-svelte": "^6.0.0",
123
124
  "@replit/codemirror-vscode-keymap": "^6.0.2",
124
125
  "@teamwork/websocket-json-stream": "^2.0.0",
125
- "@uiw/codemirror-theme-abcdef": "^4.23.10",
126
- "@uiw/codemirror-theme-dracula": "^4.23.10",
127
- "@uiw/codemirror-theme-eclipse": "^4.23.10",
128
- "@uiw/codemirror-theme-github": "^4.23.10",
129
- "@uiw/codemirror-theme-material": "^4.23.10",
130
- "@uiw/codemirror-theme-nord": "^4.23.10",
131
- "@uiw/codemirror-theme-okaidia": "^4.23.10",
132
- "@uiw/codemirror-theme-xcode": "^4.23.10",
133
- "@uiw/codemirror-themes": "^4.23.10",
126
+ "@typescript/vfs": "^1.6.1",
127
+ "@uiw/codemirror-theme-abcdef": "^4.23.12",
128
+ "@uiw/codemirror-theme-dracula": "^4.23.12",
129
+ "@uiw/codemirror-theme-eclipse": "^4.23.12",
130
+ "@uiw/codemirror-theme-github": "^4.23.12",
131
+ "@uiw/codemirror-theme-material": "^4.23.12",
132
+ "@uiw/codemirror-theme-nord": "^4.23.12",
133
+ "@uiw/codemirror-theme-okaidia": "^4.23.12",
134
+ "@uiw/codemirror-theme-xcode": "^4.23.12",
135
+ "@uiw/codemirror-themes": "^4.23.12",
136
+ "@valtown/codemirror-ts": "^2.3.1",
134
137
  "@vizhub/viz-types": "^0.1.0",
135
138
  "body-parser": "^2.2.0",
136
139
  "codemirror": "^6.0.1",
137
140
  "codemirror-copilot": "^0.0.7",
138
141
  "codemirror-ot": "^4.4.0",
139
142
  "color-hash": "^2.0.2",
143
+ "comlink": "^4.4.2",
140
144
  "d3-array": "^3.2.4",
141
145
  "diff-match-patch": "^1.0.5",
142
- "dotenv": "^16.4.7",
146
+ "dotenv": "^16.5.0",
147
+ "eslint": "^9.28.0",
148
+ "eslint-linter-browserify": "^9.28.0",
143
149
  "express": "^5.1.0",
144
- "ignore": "^7.0.3",
150
+ "globals": "^16.2.0",
151
+ "ignore": "^7.0.5",
145
152
  "json0-ot-diff": "^1.1.2",
146
- "livekit-server-sdk": "^2.11.0",
153
+ "livekit-server-sdk": "^2.13.0",
147
154
  "ngrok": "^5.0.0-beta.2",
148
- "npm": "^11.2.0",
149
- "open": "^10.1.0",
150
- "prettier-plugin-svelte": "^3.3.3",
155
+ "npm": "^11.4.1",
156
+ "open": "^10.1.2",
157
+ "prettier-plugin-svelte": "^3.4.0",
151
158
  "react": "^18",
152
- "react-bootstrap": "^2.10.9",
159
+ "react-bootstrap": "^2.10.10",
153
160
  "react-dom": "^18",
154
- "react-router-dom": "^7.5.0",
155
- "sharedb": "^5.2.1",
156
- "sharedb-client-browser": "^5.1.1",
161
+ "react-router-dom": "^7.6.2",
162
+ "sharedb": "^5.2.2",
163
+ "sharedb-client-browser": "^5.2.2",
157
164
  "uuid": "^11.1.0",
158
165
  "vizhub-ui": "^3.23.0",
159
- "ws": "^8.18.1"
166
+ "ws": "^8.18.2"
160
167
  },
161
168
  "devDependencies": {
162
169
  "@types/react": "^18",
163
170
  "@types/react-dom": "^18",
164
- "@vitejs/plugin-react": "^4.3.4",
171
+ "@vitejs/plugin-react": "^4.5.1",
165
172
  "concurrently": "^9.1.2",
166
173
  "cross-env": "^7.0.3",
167
- "npm-check-updates": "^17.1.16",
174
+ "npm-check-updates": "^18.0.1",
168
175
  "prettier": "^3.5.3",
169
- "sass": "^1.86.3",
176
+ "sass": "^1.89.1",
170
177
  "typescript": "^5.8.3",
171
- "vite": "^6.2.5",
172
- "vitest": "^3.1.1"
178
+ "vite": "^6.3.5",
179
+ "vitest": "^3.2.2"
173
180
  },
174
181
  "optionalDependencies": {
175
- "@rollup/rollup-darwin-arm64": "^4.39.0",
176
- "@rollup/rollup-win32-x64-msvc": "^4.39.0"
182
+ "@rollup/rollup-darwin-arm64": "^4.42.0",
183
+ "@rollup/rollup-win32-x64-msvc": "^4.42.0"
177
184
  }
178
185
  }
@@ -25,6 +25,7 @@ import {
25
25
  } from '../usernameLocalStorage';
26
26
  import './style.scss';
27
27
  import { useShareDB } from './useShareDB';
28
+ import { useESLint } from '../useESLint';
28
29
 
29
30
  // Instantiate the Prettier worker.
30
31
  const prettierWorker = new PrettierWorker();
@@ -75,6 +76,7 @@ function App() {
75
76
  useState(v4()); //default room name will be an arbitrary uuid
76
77
  const [liveKitConnection, setLiveKitConnection] =
77
78
  useState(false);
79
+ const { esLintSource } = useESLint();
78
80
  // Get the initial username from localStorage.
79
81
  const initialUsername: Username = useInitialUsername();
80
82
 
@@ -129,6 +131,7 @@ function App() {
129
131
  aiCopilotEndpoint={
130
132
  enableCopilot ? '/ai-copilot' : null
131
133
  }
134
+ esLintSource={esLintSource}
132
135
  />
133
136
  {enableRightPanel ? <VZRight /> : null}
134
137
  <VZResizer side="left" />
@@ -2,6 +2,7 @@ import { EditorView } from 'codemirror';
2
2
  import {
3
3
  Compartment,
4
4
  EditorState,
5
+ StateField,
5
6
  } from '@codemirror/state';
6
7
  import { javascript } from '@codemirror/lang-javascript';
7
8
  import { svelte } from '@replit/codemirror-lang-svelte';
@@ -15,7 +16,11 @@ import { autocompletion } from '@codemirror/autocomplete';
15
16
  import { indentationMarkers } from '@replit/codemirror-indentation-markers';
16
17
  // import { showMinimap } from '@replit/codemirror-minimap';
17
18
  import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
18
- import { Diagnostic, linter } from '@codemirror/lint';
19
+ import {
20
+ Diagnostic,
21
+ linter,
22
+ lintGutter,
23
+ } from '@codemirror/lint';
19
24
 
20
25
  import { json1Presence, textUnicode } from '../../ot';
21
26
  import {
@@ -41,17 +46,54 @@ import { ThemeLabel, themeOptionsByLabel } from '../themes';
41
46
  import { keymap } from '@codemirror/view';
42
47
  import { basicSetup } from './basicSetup';
43
48
  import { InteractRule } from '@replit/codemirror-interact';
44
- import rainbowBrackets from '../CodeEditor/rainbowBrackets';
49
+ import rainbowBrackets from './rainbowBrackets';
45
50
  import { cssLanguage } from '@codemirror/lang-css';
46
51
  import { javascriptLanguage } from '@codemirror/lang-javascript';
47
52
  import { copilot } from './Copilot';
53
+ import { type WorkerShape } from '@valtown/codemirror-ts/src/worker';
54
+ import * as Comlink from 'comlink';
55
+ import {
56
+ tsAutocompleteWorker,
57
+ tsFacetWorker,
58
+ tsHoverWorker,
59
+ tsLinterWorker,
60
+ tsSyncWorker,
61
+ } from '@valtown/codemirror-ts';
48
62
 
49
63
  const DEBUG = false;
50
64
 
65
+ // Define a StateField to store the file name.
66
+ // This should be defined at the module level if it's to be imported by other modules.
67
+ export const fileNameStateField = StateField.define<string>(
68
+ {
69
+ create: () => '', // Default initial value
70
+ update: (value, tr) => value, // Typically set once at creation for a given editor instance
71
+ },
72
+ );
73
+
51
74
  // Enables TypeScript +JSX support in CodeMirror.
52
75
  const tsx = () =>
53
76
  javascript({ jsx: true, typescript: true });
54
77
 
78
+ // Lazy initialization of worker
79
+ let worker: Comlink.Remote<WorkerShape> | null = null;
80
+ const initializeWorker = async () => {
81
+ if (worker === null) {
82
+ const innerWorker = new Worker(
83
+ new URL(
84
+ './typescriptExtension/worker.ts',
85
+ import.meta.url,
86
+ ),
87
+ {
88
+ type: 'module',
89
+ },
90
+ );
91
+ worker = Comlink.wrap<WorkerShape>(innerWorker);
92
+ await worker.initialize();
93
+ }
94
+ return worker;
95
+ };
96
+
55
97
  const htmlConfig = {
56
98
  matchClosingTags: true,
57
99
  selfClosingTags: false,
@@ -110,7 +152,7 @@ interface ExtendedEditorCacheValue
110
152
 
111
153
  // Gets or creates an `editorCache` entry for the given file id.
112
154
  // Looks in `editorCache` first, and if not found, creates a new editor.
113
- export const getOrCreateEditor = ({
155
+ export const getOrCreateEditor = async ({
114
156
  paneId = 'root',
115
157
  fileId,
116
158
  shareDBDoc,
@@ -126,6 +168,7 @@ export const getOrCreateEditor = ({
126
168
  enableAutoFollowRef,
127
169
  openTab,
128
170
  aiCopilotEndpoint,
171
+ esLintSource,
129
172
  rainbowBracketsEnabled = true,
130
173
  }: {
131
174
  // TODO pass this in from the outside
@@ -162,8 +205,11 @@ export const getOrCreateEditor = ({
162
205
  enableAutoFollowRef: React.MutableRefObject<boolean>;
163
206
  openTab: (tabState: TabState) => void;
164
207
  aiCopilotEndpoint?: string;
208
+ esLintSource: (
209
+ view: EditorView,
210
+ ) => Promise<readonly Diagnostic[]>;
165
211
  rainbowBracketsEnabled?: boolean; // New parameter type
166
- }): ExtendedEditorCacheValue => {
212
+ }): Promise<ExtendedEditorCacheValue> => {
167
213
  // Cache hit
168
214
 
169
215
  const cacheKey = editorCacheKey(fileId, paneId);
@@ -194,6 +240,9 @@ export const getOrCreateEditor = ({
194
240
  // const extensions = [autocompletion(), html(htmlConfig)]
195
241
  const extensions = [];
196
242
 
243
+ // Initialize the fileNameStateField with the actual file name
244
+ extensions.push(fileNameStateField.init(() => name));
245
+
197
246
  // This plugin implements multiplayer editing,
198
247
  // real-time synchronozation of changes across clients.
199
248
  // Does not deal with showing others' cursors.
@@ -244,6 +293,16 @@ export const getOrCreateEditor = ({
244
293
  // https://github.com/vizhub-core/vzcode/issues/134
245
294
  extensions.push(basicSetup);
246
295
 
296
+ if (esLintSource) {
297
+ extensions.push(lintGutter()); // Show lint icons in the gutter
298
+ extensions.push(
299
+ linter(esLintSource, {
300
+ // You can configure linter options here, e.g., delay
301
+ delay: 750,
302
+ }),
303
+ );
304
+ }
305
+
247
306
  // This supports dynamic changing of the theme.
248
307
  extensions.push(
249
308
  themeCompartment.of(themeOptionsByLabel[theme].value),
@@ -332,6 +391,23 @@ export const getOrCreateEditor = ({
332
391
  }),
333
392
  );
334
393
 
394
+ if (name.endsWith('ts') || name.endsWith('tsx')) {
395
+ // Initialize worker if needed
396
+ const tsWorker = await initializeWorker();
397
+
398
+ extensions.push(
399
+ ...[
400
+ tsFacetWorker.of({ worker: tsWorker, path: name }),
401
+ tsSyncWorker(),
402
+ tsLinterWorker(),
403
+ autocompletion({
404
+ override: [tsAutocompleteWorker()],
405
+ }),
406
+ tsHoverWorker(),
407
+ ],
408
+ );
409
+ }
410
+
335
411
  // Show the minimap
336
412
  // See https://github.com/replit/codemirror-minimap#usage
337
413
  // This extension has poor performance, so it's disabled for now.
@@ -2,15 +2,17 @@ import {
2
2
  useRef,
3
3
  useLayoutEffect,
4
4
  useCallback,
5
- useMemo,
6
5
  useContext,
7
6
  useEffect,
7
+ useState,
8
8
  } from 'react';
9
9
  import { Username } from '../../types';
10
10
  import { EditorCacheValue } from '../useEditorCache';
11
11
  import { getOrCreateEditor } from './getOrCreateEditor';
12
12
  import { VZCodeContext } from '../VZCodeContext';
13
13
  import { InteractRule } from '@replit/codemirror-interact';
14
+ import { EditorView } from 'codemirror'; // Import EditorView
15
+ import { Diagnostic } from '@codemirror/lint'; // Import Diagnostic
14
16
  import './style.scss';
15
17
 
16
18
  // The path in the ShareDB document where the files live.
@@ -19,9 +21,13 @@ const filesPath = ['files'];
19
21
  export const CodeEditor = ({
20
22
  customInteractRules,
21
23
  aiCopilotEndpoint,
24
+ esLintSource,
22
25
  }: {
23
26
  customInteractRules?: Array<InteractRule>;
24
27
  aiCopilotEndpoint?: string;
28
+ esLintSource: (
29
+ view: EditorView,
30
+ ) => Promise<readonly Diagnostic[]>;
25
31
  }) => {
26
32
  const {
27
33
  activePane,
@@ -78,11 +84,17 @@ export const CodeEditor = ({
78
84
  enableAutoFollowRef.current = enableAutoFollow;
79
85
  }, [enableAutoFollow]);
80
86
 
87
+ // State to hold the editor cache value
88
+ const [editorCacheValue, setEditorCacheValue] =
89
+ useState<EditorCacheValue | null>(null);
90
+
81
91
  // Get the editor corresponding to the active file.
82
92
  // Looks in `editorCache` first, and if not found, creates a new editor.
83
- const editorCacheValue: EditorCacheValue = useMemo(
84
- () =>
85
- getOrCreateEditor({
93
+ useEffect(() => {
94
+ let isMounted = true;
95
+
96
+ const initEditor = async () => {
97
+ const editor = await getOrCreateEditor({
86
98
  fileId: activePane.activeFileId,
87
99
  shareDBDoc,
88
100
  content,
@@ -97,20 +109,32 @@ export const CodeEditor = ({
97
109
  enableAutoFollowRef,
98
110
  openTab,
99
111
  aiCopilotEndpoint,
100
- }),
101
- [
102
- activePane,
103
- shareDBDoc,
104
- filesPath,
105
- localPresence,
106
- docPresence,
107
- theme,
108
- onInteract,
109
- editorCache,
110
- usernameRef,
111
- aiCopilotEndpoint,
112
- ],
113
- );
112
+ esLintSource,
113
+ });
114
+
115
+ if (isMounted) {
116
+ setEditorCacheValue(editor);
117
+ }
118
+ };
119
+
120
+ initEditor();
121
+
122
+ return () => {
123
+ isMounted = false;
124
+ };
125
+ }, [
126
+ activePane,
127
+ shareDBDoc,
128
+ filesPath,
129
+ localPresence,
130
+ docPresence,
131
+ theme,
132
+ onInteract,
133
+ editorCache,
134
+ usernameRef,
135
+ aiCopilotEndpoint,
136
+ esLintSource,
137
+ ]);
114
138
 
115
139
  // Every time the active file switches from one file to another,
116
140
  // the editor corresponding to the old file is removed from the DOM,
@@ -119,6 +143,7 @@ export const CodeEditor = ({
119
143
  // Guard against cases where page is still loading.
120
144
  if (!codeEditorRef.current) return;
121
145
  if (!content) return;
146
+ if (!editorCacheValue) return; // Guard against null editorCacheValue
122
147
  // if (openSplitPane) return;
123
148
 
124
149
  // Add the editor and apply the prior scroll position to the DOM.
@@ -132,17 +157,24 @@ export const CodeEditor = ({
132
157
  return () => {
133
158
  // Remove the old editor from the DOM and store the current scroll position.
134
159
  // This happens every time `activeFileId` changes.
135
- editorCacheValue.scrollPosition =
136
- editorCacheValue.editor.scrollDOM.scrollTop;
137
- codeEditorRef.current.removeChild(
138
- editorCacheValue.editor.dom,
139
- );
160
+ if (
161
+ editorCacheValue &&
162
+ codeEditorRef.current?.contains(
163
+ editorCacheValue.editor.dom,
164
+ )
165
+ ) {
166
+ editorCacheValue.scrollPosition =
167
+ editorCacheValue.editor.scrollDOM.scrollTop;
168
+ codeEditorRef.current.removeChild(
169
+ editorCacheValue.editor.dom,
170
+ );
171
+ }
140
172
  };
141
173
  }, [shareDBDoc, editorCacheValue]);
142
174
 
143
175
  // Focus the editor
144
176
  useEffect(() => {
145
- if (editorWantsFocus) {
177
+ if (editorWantsFocus && editorCacheValue) {
146
178
  editorCacheValue.editor.focus();
147
179
  editorNoLongerWantsFocus();
148
180
  }
@@ -0,0 +1,32 @@
1
+ import {
2
+ createDefaultMapFromCDN,
3
+ createSystem,
4
+ createVirtualTypeScriptEnvironment,
5
+ } from '@typescript/vfs';
6
+ import ts from 'typescript';
7
+ import * as Comlink from 'comlink';
8
+
9
+ // When importing this module, typescript says that it "cannot find the module or its corresponding type declarations"
10
+ // However, when this file is used as a web worker, no error occurs.
11
+ // As a result, this error is suppressed to avoid future confusion.
12
+ // @ts-expect-error
13
+ import { createWorker } from '@valtown/codemirror-ts/worker';
14
+
15
+ Comlink.expose(
16
+ createWorker(async function () {
17
+ const fsMap = await createDefaultMapFromCDN(
18
+ { target: ts.ScriptTarget.ES2022 },
19
+ '3.7.3',
20
+ false,
21
+ ts,
22
+ );
23
+ const system = createSystem(fsMap);
24
+ const compilerOpts = {};
25
+ return createVirtualTypeScriptEnvironment(
26
+ system,
27
+ [],
28
+ ts,
29
+ compilerOpts,
30
+ );
31
+ }),
32
+ );
@@ -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?
@@ -23,6 +27,7 @@ const PaneView = ({
23
27
  aiAssistTooltipText,
24
28
  aiAssistClickOverride,
25
29
  aiCopilotEndpoint,
30
+ esLintSource,
26
31
  }) => {
27
32
  // This prevents the CodeEditor from rendering
28
33
  // during SSR.
@@ -49,6 +54,7 @@ const PaneView = ({
49
54
  <CodeEditor
50
55
  customInteractRules={customInteractRules}
51
56
  aiCopilotEndpoint={aiCopilotEndpoint}
57
+ esLintSource={esLintSource}
52
58
  />
53
59
  )}
54
60
  {isClient &&
@@ -84,6 +90,7 @@ export const VZMiddle = ({
84
90
  aiAssistClickOverride,
85
91
  aiCopilotEndpoint,
86
92
  customInteractRules,
93
+ esLintSource,
87
94
  }: {
88
95
  enableAIAssist?: boolean;
89
96
  aiAssistEndpoint?: string;
@@ -92,6 +99,9 @@ export const VZMiddle = ({
92
99
  aiAssistClickOverride?: () => void;
93
100
  aiCopilotEndpoint?: string;
94
101
  customInteractRules?: Array<InteractRule>;
102
+ esLintSource: (
103
+ view: EditorView,
104
+ ) => Promise<readonly Diagnostic[]>;
95
105
  }) => {
96
106
  const { codeEditorWidth } = useContext(
97
107
  SplitPaneResizeContext,
@@ -128,6 +138,7 @@ export const VZMiddle = ({
128
138
  aiAssistTooltipText={aiAssistTooltipText}
129
139
  aiAssistClickOverride={aiAssistClickOverride}
130
140
  aiCopilotEndpoint={aiCopilotEndpoint}
141
+ esLintSource={esLintSource}
131
142
  />
132
143
  <CodeErrorOverlay
133
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,
@@ -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
+ };