Skip to content

Commit

Permalink
fix(codemirror): remove inline props (#477)
Browse files Browse the repository at this point in the history
  • Loading branch information
danilowoz authored May 27, 2022
1 parent b70370f commit b3d9c63
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 3 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,9 @@ jobs:
packages/*/node_modules
key: modules-${{ hashFiles('yarn.lock') }}

- name: Setup | Install dependencies
run: yarn install --frozen-lockfile

- name: Lint | Eslint
run: yarn run lint

Expand All @@ -267,5 +270,8 @@ jobs:
packages/*/node_modules
key: modules-${{ hashFiles('yarn.lock') }}

- name: Setup | Install dependencies
run: yarn install --frozen-lockfile

- name: Format
run: yarn run format:check
36 changes: 34 additions & 2 deletions sandpack-react/src/components/CodeEditor/CodeEditor.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
import type { Story } from "@storybook/react";
import * as React from "react";
Expand All @@ -7,14 +9,44 @@ import { SandpackProvider } from "../../contexts/sandpackContext";
import { SandpackThemeProvider } from "../../contexts/themeContext";
import { SandpackPreview } from "../Preview";

import type { CodeEditorProps } from "./index";
import { SandpackCodeEditor } from "./index";
import { useSandpackLint } from "./eslint";

import type { CodeEditorProps } from "./";
import { SandpackCodeEditor } from "./";

export default {
title: "components/Code Editor",
component: SandpackCodeEditor,
};

export const EslintIntegration: React.FC = () => {
const { lintErrors, lintExtensions } = useSandpackLint();

return (
<SandpackProvider
files={{
"/App.js": `export default function App() {
if(true) {
useState()
}
return <h1>Hello World</h1>
}`,
}}
template="react"
>
<SandpackThemeProvider>
<SandpackCodeEditor
extensions={lintExtensions}
extensionsKeymap={[]}
id="extensions"
/>

{JSON.stringify(lintErrors)}
</SandpackThemeProvider>
</SandpackProvider>
);
};

export const Component: Story<CodeEditorProps> = (args) => (
<SandpackProvider
customSetup={{
Expand Down
13 changes: 12 additions & 1 deletion sandpack-react/src/components/CodeEditor/CodeMirror.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {
EditorState as SandpackEditorState,
SandpackInitMode,
} from "../../types";
import { shallowEqual } from "../../utils/array";
import { classNames } from "../../utils/classNames";
import { getFileName } from "../../utils/stringUtils";

Expand Down Expand Up @@ -143,6 +144,9 @@ export const CodeMirror = React.forwardRef<CodeMirrorRef, CodeMirrorProps>(
const { listen } = useSandpack();
const ariaId = useGeneratedId(id);

const prevExtension = React.useRef<Extension[]>([]);
const prevExtensionKeymap = React.useRef<Array<readonly KeyBinding[]>>([]);

const { isIntersecting } = useIntersectionObserver(wrapper, {
rootMargin: "600px 0px",
threshold: 0.2,
Expand Down Expand Up @@ -323,7 +327,11 @@ export const CodeMirror = React.forwardRef<CodeMirrorRef, CodeMirrorProps>(
function applyExtensions() {
const view = cmView.current;

if (view) {
const dependenciesAreDiff =
!shallowEqual(extensions, prevExtension.current) ||
!shallowEqual(extensionsKeymap, prevExtensionKeymap.current);

if (view && dependenciesAreDiff) {
view.dispatch({
effects: StateEffect.appendConfig.of(extensions),
});
Expand All @@ -333,6 +341,9 @@ export const CodeMirror = React.forwardRef<CodeMirrorRef, CodeMirrorProps>(
keymap.of([...extensionsKeymap] as unknown as KeyBinding[])
),
});

prevExtension.current = extensions;
prevExtensionKeymap.current = extensionsKeymap;
}
},
[extensions, extensionsKeymap]
Expand Down
121 changes: 121 additions & 0 deletions sandpack-react/src/components/CodeEditor/eslint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable import/order */
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import type { EditorView } from "@codemirror/view";
import type { Diagnostic } from "@codemirror/lint";
import type { Text } from "@codemirror/text";
import { Linter } from "eslint/lib/linter/linter";
import React from "react";

const getCodeMirrorPosition = (
doc: Text,
{ line, column }: { line: number; column?: number }
): number => {
return doc.line(line).from + (column ?? 0) - 1;
};

const linter = new Linter();

// HACK! Eslint requires 'esquery' using `require`, but there's no commonjs interop.
// because of this it tries to run `esquery.parse()`, while there's only `esquery.default.parse()`.
// This hack places the functions in the right place.
const esquery = require("esquery");
esquery.parse = esquery.default?.parse;
esquery.matches = esquery.default?.matches;

const reactRules = require("eslint-plugin-react-hooks").rules;
linter.defineRules({
"react-hooks/rules-of-hooks": reactRules["rules-of-hooks"],
"react-hooks/exhaustive-deps": reactRules["exhaustive-deps"],
});

const options = {
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
ecmaFeatures: { jsx: true },
},
rules: {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
},
};

const runESLint = (
doc: Text
): { errors: any[]; codeMirrorErrors: Diagnostic[] } => {
const codeString = doc.toString();
const errors = linter.verify(codeString, options) as any[];

const severity = {
1: "warning",
2: "error",
};

const codeMirrorErrors = errors
.map((error) => {
if (!error) return undefined;

const from = getCodeMirrorPosition(doc, {
line: error.line,
column: error.column,
});

const to = getCodeMirrorPosition(doc, {
line: error.endLine ?? error.line,
column: error.endColumn ?? error.column,
});

return {
ruleId: error.ruleId,
from,
to,
severity: severity[error.severity],
message: error.message,
};
})
.filter(Boolean) as Diagnostic[];

return {
codeMirrorErrors,
errors: errors.map((item) => {
return {
...item,
severity: severity[item.severity],
};
}),
};
};

type LintDiagnostic = Array<{
line: number;
column: number;
severity: "warning" | "error";
message: string;
}>;

export const useSandpackLint = (): any => {
const [lintErrors, setLintErrors] = React.useState<LintDiagnostic>([]);
const [lintExtensions, setLintExtensions] = React.useState<any>([]);

React.useEffect(() => {
const loadLinter = async (): Promise<any> => {
const { linter } = await import("@codemirror/lint");
const onLint = linter(async (props: EditorView) => {
const editorState = props.state.doc;
const { errors, codeMirrorErrors } = runESLint(editorState);
// Ignore parsing or internal linter errors.
const isReactRuleError = (error: any): any => error.ruleId != null;
setLintErrors(errors.filter(isReactRuleError));
return codeMirrorErrors.filter(isReactRuleError);
});
setLintExtensions([onLint]);
};

loadLinter();
}, []);

return { lintErrors, lintExtensions };
};
19 changes: 19 additions & 0 deletions sandpack-react/src/utils/array.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { shallowEqual } from "./array";

describe(shallowEqual, () => {
it("should be different: when the first array is longer than the second one", () => {
expect(shallowEqual(["1", "2"], ["1"])).toBe(false);
});

it("should be different: when the first array is shorter than the second one", () => {
expect(shallowEqual(["1"], ["1", "2"])).toBe(false);
});

it("should be different: when an item is different", () => {
expect(shallowEqual(["1", "2"], ["1", "3"])).toBe(false);
});

it("should be equal: when both are an empty array", () => {
expect(shallowEqual([], [])).toBe(true);
});
});
17 changes: 17 additions & 0 deletions sandpack-react/src/utils/array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

export const shallowEqual = (a: any[], b: any[]): boolean => {
if (a.length !== b.length) return false;

let result = true;

for (let index = 0; index < a.length; index++) {
if (a[index] !== b[index]) {
result = false;

break;
}
}

return result;
};

0 comments on commit b3d9c63

Please sign in to comment.