Skip to content

Commit 781d1b5

Browse files
author
Milo
authored
Merge pull request solidjs#120 from joshwilsonvu/master
Add ESLint support
2 parents 3761dae + e00c425 commit 781d1b5

File tree

10 files changed

+194
-13
lines changed

10 files changed

+194
-13
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ lib
66

77
.yarn*
88
yarn.lock
9-
package-lock.json
9+
package-lock.json
10+
11+
.DS_Store

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"@solid-primitives/scheduled": "^1.0.0",
6161
"babel-preset-solid": "1.4.8",
6262
"dedent": "^0.7.0",
63+
"eslint-solid-standalone": "^0.1.5",
6364
"jszip": "^3.10.1",
6465
"monaco-editor-textmate": "^3.0.0",
6566
"monaco-textmate": "^3.0.1",

playground/pages/edit.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
33
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
44
import CompilerWorker from '../../src/workers/compiler?worker';
55
import FormatterWorker from '../../src/workers/formatter?worker';
6+
import LinterWorker from '../../src/workers/linter?worker';
67
import onigasm from 'onigasm/lib/onigasm.wasm?url';
78
import { batch, createResource, createSignal, lazy, onCleanup, Show, Suspense } from 'solid-js';
89
import { useMatch, useNavigate, useParams } from '@solidjs/router';
@@ -39,6 +40,7 @@ export const Edit = (props: { horizontal: boolean }) => {
3940
const scratchpad = useMatch(() => '/scratchpad');
4041
const compiler = new CompilerWorker();
4142
const formatter = new FormatterWorker();
43+
const linter = new LinterWorker();
4244

4345
const params = useParams();
4446
const context = useAppContext()!;
@@ -214,6 +216,7 @@ export const Edit = (props: { horizontal: boolean }) => {
214216
<Repl
215217
compiler={compiler}
216218
formatter={formatter}
219+
linter={linter}
217220
isHorizontal={props.horizontal}
218221
dark={context.dark()}
219222
tabs={tabs()}

pnpm-lock.yaml

Lines changed: 26 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rollup.config.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,13 @@ const preppy = {
110110
};
111111

112112
rollup({
113-
input: ['src/index.ts', 'src/workers/compiler.ts', 'src/workers/formatter.ts', 'src/components/repl.tsx'],
113+
input: [
114+
'src/index.ts',
115+
'src/workers/compiler.ts',
116+
'src/workers/formatter.ts',
117+
'src/workers/linter.ts',
118+
'src/components/repl.tsx',
119+
],
114120
external: ['solid-js', 'solid-js/web', 'solid-js/store', 'monaco-editor'],
115121
acornInjectPlugins: [jsx()],
116122
plugins: [

src/components/editor/index.tsx

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1-
import { Component, createEffect, onMount, onCleanup } from 'solid-js';
1+
import { Component, createEffect, onMount, onCleanup, on } from 'solid-js';
22
import { Uri, languages, editor as mEditor, KeyMod, KeyCode } from 'monaco-editor';
33
import { liftOff } from './setupSolid';
44
import { useZoom } from '../../hooks/useZoom';
55
import type { Repl } from 'solid-repl/lib/repl';
6+
import type { LinterWorkerPayload, LinterWorkerResponse } from '../../workers/linter';
7+
import { throttle } from '@solid-primitives/scheduled';
68

79
const Editor: Component<{
810
url: string;
911
disabled?: true;
1012
isDark?: boolean;
1113
withMinimap?: boolean;
1214
formatter?: Worker;
15+
linter?: Worker;
1316
displayErrors?: boolean;
17+
displayLintMessages?: boolean;
1418
onDocChange?: (code: string) => void;
1519
onEditorReady?: Parameters<Repl>[0]['onEditorReady'];
1620
}> = (props) => {
@@ -47,6 +51,33 @@ const Editor: Component<{
4751
},
4852
});
4953
}
54+
if (props.linter) {
55+
const listener = ({ data }: MessageEvent<LinterWorkerResponse>) => {
56+
if (props.displayLintMessages) {
57+
const { event } = data;
58+
if (event === 'LINT') {
59+
const m = model();
60+
m && mEditor.setModelMarkers(m, 'eslint', data.markers);
61+
} else if (event === 'FIX') {
62+
const m = model();
63+
m && mEditor.setModelMarkers(m, 'eslint', data.markers);
64+
data.fixed && model()?.setValue(data.output);
65+
}
66+
}
67+
};
68+
props.linter.addEventListener('message', listener);
69+
onCleanup(() => props.linter?.removeEventListener('message', listener));
70+
}
71+
72+
const runLinter = throttle((code: string) => {
73+
if (props.linter && props.displayLintMessages) {
74+
const payload: LinterWorkerPayload = {
75+
event: 'LINT',
76+
code,
77+
};
78+
props.linter.postMessage(payload);
79+
}
80+
}, 250);
5081

5182
// Initialize Monaco
5283
onMount(() => {
@@ -63,13 +94,39 @@ const Editor: Component<{
6394
},
6495
});
6596

97+
if (props.linter) {
98+
editor.addAction({
99+
id: 'eslint.executeAutofix',
100+
label: 'Fix all auto-fixable problems',
101+
contextMenuGroupId: '1_modification',
102+
contextMenuOrder: 3.5,
103+
run: (ed) => {
104+
const code = ed.getValue();
105+
if (code) {
106+
const payload: LinterWorkerPayload = {
107+
event: 'FIX',
108+
code,
109+
};
110+
props.linter?.postMessage(payload);
111+
}
112+
},
113+
});
114+
}
115+
66116
editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyS, () => {
67-
editor?.getAction('editor.action.formatDocument').run();
68-
editor?.focus();
117+
if (editor) {
118+
// auto-format
119+
editor.getAction('editor.action.formatDocument')?.run();
120+
// auto-fix problems
121+
props.displayLintMessages && editor.getAction('eslint.executeAutofix')?.run();
122+
editor.focus();
123+
}
69124
});
70125

71126
editor.onDidChangeModelContent(() => {
72-
props.onDocChange?.(editor.getValue());
127+
const code = editor.getValue();
128+
props.onDocChange?.(code);
129+
runLinter(code);
73130
});
74131
});
75132
onCleanup(() => editor?.dispose());
@@ -95,6 +152,17 @@ const Editor: Component<{
95152
});
96153
});
97154

155+
createEffect(() => {
156+
if (props.displayLintMessages) {
157+
// run on mount and when displayLintMessages is turned on
158+
runLinter(editor.getValue());
159+
} else {
160+
// reset eslint markers when displayLintMessages is turned off
161+
const m = model();
162+
m && mEditor.setModelMarkers(m, 'eslint', []);
163+
}
164+
});
165+
98166
onMount(() => {
99167
props.onEditorReady?.(editor, { Uri, editor: mEditor });
100168
});

src/components/repl.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const compileMode = {
2222
} as const;
2323

2424
const Repl: ReplProps = (props) => {
25-
const { compiler, formatter } = props;
25+
const { compiler, formatter, linter } = props;
2626
let now: number;
2727

2828
const tabRefs = new Map<string, HTMLSpanElement>();
@@ -166,6 +166,7 @@ const Repl: ReplProps = (props) => {
166166
const [reloadSignal, reload] = createSignal(false, { equals: false });
167167
const [devtoolsOpen, setDevtoolsOpen] = createSignal(true);
168168
const [displayErrors, setDisplayErrors] = createSignal(true);
169+
const [displayLintMessages, setDisplayLintMessages] = createSignal(true);
169170

170171
return (
171172
<div
@@ -251,6 +252,17 @@ const Repl: ReplProps = (props) => {
251252
<span>Display Errors</span>
252253
</label>
253254
</TabItem>
255+
<TabItem class="justify-self-end">
256+
<label class="space-x-2 px-3 py-2 cursor-pointer">
257+
<input
258+
type="checkbox"
259+
name="run-linter"
260+
checked={displayLintMessages()}
261+
onChange={(event) => setDisplayLintMessages(event.currentTarget.checked)}
262+
/>
263+
<span>Run Linter</span>
264+
</label>
265+
</TabItem>
254266
</TabList>
255267

256268
<MonacoTabs tabs={props.tabs} folder={props.id} />
@@ -260,10 +272,12 @@ const Repl: ReplProps = (props) => {
260272
url={`file:///${props.id}/${props.current}`}
261273
onDocChange={() => compile()}
262274
formatter={formatter}
275+
linter={linter}
263276
isDark={props.dark}
264277
withMinimap={false}
265278
onEditorReady={props.onEditorReady}
266279
displayErrors={displayErrors()}
280+
displayLintMessages={displayLintMessages()}
267281
/>
268282
</Show>
269283

src/workers/compiler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ self.addEventListener('message', async ({ data }) => {
123123
try {
124124
if (event === 'BABEL') {
125125
self.postMessage(await babel(tab, compileOpts));
126-
} else {
126+
} else if (event === 'ROLLUP') {
127127
self.postMessage(await compile(tabs, event));
128128
}
129129
} catch (e) {

src/workers/linter.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { verify, verifyAndFix } from 'eslint-solid-standalone';
2+
import type { Linter } from 'eslint-solid-standalone';
3+
import type { editor } from 'monaco-editor';
4+
5+
type RuleSeverityOverrides = Parameters<typeof verify>[1];
6+
export interface LinterWorkerPayload {
7+
event: 'LINT' | 'FIX';
8+
code: string;
9+
ruleSeverityOverrides?: RuleSeverityOverrides;
10+
}
11+
12+
const messagesToMarkers = (messages: Array<Linter.LintMessage>): Array<editor.IMarkerData> => {
13+
if (messages.some((m) => m.fatal)) return []; // no need for any extra highlights on parse errors
14+
return messages.map((m) => ({
15+
startLineNumber: m.line,
16+
endLineNumber: m.endLine ?? m.line,
17+
startColumn: m.column,
18+
endColumn: m.endColumn ?? m.column,
19+
message: `${m.message}\neslint(${m.ruleId})`,
20+
severity: m.severity === 2 ? 8 /* error */ : 4 /* warning */,
21+
}));
22+
};
23+
24+
async function lintResponse(code: string, ruleSeverityOverrides?: RuleSeverityOverrides) {
25+
return {
26+
event: 'LINT' as const,
27+
markers: messagesToMarkers(await verify(code, ruleSeverityOverrides)),
28+
};
29+
}
30+
31+
async function fixResponse(code: string, ruleSeverityOverrides?: RuleSeverityOverrides) {
32+
const fixReport = await verifyAndFix(code, ruleSeverityOverrides);
33+
return {
34+
event: 'FIX' as const,
35+
markers: messagesToMarkers(fixReport.messages),
36+
output: fixReport.output,
37+
fixed: fixReport.fixed,
38+
};
39+
}
40+
41+
function errorResponse(e: any) {
42+
return { event: 'ERROR' as const, error: (e as Error).message };
43+
}
44+
45+
self.addEventListener('message', async ({ data }: MessageEvent<LinterWorkerPayload>) => {
46+
const { event } = data;
47+
try {
48+
if (event === 'LINT') {
49+
const { code, ruleSeverityOverrides } = data;
50+
self.postMessage(await lintResponse(code, ruleSeverityOverrides));
51+
} else if (event === 'FIX') {
52+
const { code, ruleSeverityOverrides } = data;
53+
self.postMessage(await fixResponse(code, ruleSeverityOverrides));
54+
}
55+
} catch (e) {
56+
console.error(e);
57+
self.postMessage(errorResponse(e));
58+
}
59+
});
60+
61+
type LintResponse = Awaited<ReturnType<typeof lintResponse>>;
62+
type FixResponse = Awaited<ReturnType<typeof fixResponse>>;
63+
type ErrorResponse = ReturnType<typeof errorResponse>;
64+
export type LinterWorkerResponse = LintResponse | FixResponse | ErrorResponse;

types/types.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ declare module 'solid-repl' {
1010
declare module 'solid-repl/lib/repl' {
1111
export type Repl = import('solid-js').Component<{
1212
compiler: Worker;
13-
formatter?: Worker;
13+
formatter: Worker;
14+
linter: Worker;
1415
isHorizontal: boolean;
1516
dark: boolean;
1617
tabs: Tab[];

0 commit comments

Comments
 (0)