Browse Source

[Beta] Adds eslint integration on Sandpack (#4665)

* added sandpack linter and installed latest sandpacl

* integrate eslint into Sandpack

* Format the linting errors, disable preview on lint error, have only two react hooks

* fixes build

* split eslint-integration

* fix tooltip text color, error rename to 'Lint Error', show single lint error

* NIT

* Just enable it

* Delete eslint.md

Co-authored-by: Strek <ssharishkumar@gmail.com>
Co-authored-by: dan <dan.abramov@gmail.com>
main
Danilo Woznica 3 years ago
committed by GitHub
parent
commit
fc88516abf
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      beta/package.json
  2. 6
      beta/src/components/MDX/Sandpack/CustomPreset.tsx
  3. 22
      beta/src/components/MDX/Sandpack/Error.tsx
  4. 18
      beta/src/components/MDX/Sandpack/Preview.tsx
  5. 85
      beta/src/components/MDX/Sandpack/eslint-integration.tsx
  6. 28
      beta/src/components/MDX/Sandpack/utils.ts
  7. 13
      beta/src/styles/sandpack.css
  8. 55
      beta/yarn.lock

2
beta/package.json

@ -22,7 +22,7 @@
"check-all": "npm-run-all prettier lint:fix tsc" "check-all": "npm-run-all prettier lint:fix tsc"
}, },
"dependencies": { "dependencies": {
"@codesandbox/sandpack-react": "v0.14.3-experimental.1", "@codesandbox/sandpack-react": "v0.19.8-experimental.0",
"@docsearch/css": "3.0.0-alpha.41", "@docsearch/css": "3.0.0-alpha.41",
"@docsearch/react": "3.0.0-alpha.41", "@docsearch/react": "3.0.0-alpha.41",
"@headlessui/react": "^1.3.0", "@headlessui/react": "^1.3.0",

6
beta/src/components/MDX/Sandpack/CustomPreset.tsx

@ -1,7 +1,6 @@
/* /*
* Copyright (c) Facebook, Inc. and its affiliates. * Copyright (c) Facebook, Inc. and its affiliates.
*/ */
import React from 'react'; import React from 'react';
// @ts-ignore // @ts-ignore
import {flushSync} from 'react-dom'; import {flushSync} from 'react-dom';
@ -13,13 +12,13 @@ import {
SandpackReactDevTools, SandpackReactDevTools,
} from '@codesandbox/sandpack-react'; } from '@codesandbox/sandpack-react';
import scrollIntoView from 'scroll-into-view-if-needed'; import scrollIntoView from 'scroll-into-view-if-needed';
import cn from 'classnames'; import cn from 'classnames';
import {IconChevron} from 'components/Icon/IconChevron'; import {IconChevron} from 'components/Icon/IconChevron';
import {NavigationBar} from './NavigationBar'; import {NavigationBar} from './NavigationBar';
import {Preview} from './Preview'; import {Preview} from './Preview';
import {CustomTheme} from './Themes'; import {CustomTheme} from './Themes';
import {useSandpackLint} from './utils';
export function CustomPreset({ export function CustomPreset({
isSingleFile, isSingleFile,
@ -32,6 +31,7 @@ export function CustomPreset({
devToolsLoaded: boolean; devToolsLoaded: boolean;
onDevToolsLoad: () => void; onDevToolsLoad: () => void;
}) { }) {
const {lintErrors, onLint} = useSandpackLint();
const lineCountRef = React.useRef<{[key: string]: number}>({}); const lineCountRef = React.useRef<{[key: string]: number}>({});
const containerRef = React.useRef<HTMLDivElement>(null); const containerRef = React.useRef<HTMLDivElement>(null);
const {sandpack} = useSandpack(); const {sandpack} = useSandpack();
@ -64,10 +64,12 @@ export function CustomPreset({
showInlineErrors showInlineErrors
showTabs={false} showTabs={false}
showRunButton={false} showRunButton={false}
extensions={[onLint]}
/> />
<Preview <Preview
className="order-last xl:order-2" className="order-last xl:order-2"
isExpanded={isExpanded} isExpanded={isExpanded}
lintErrors={lintErrors}
/> />
{isExpandable && ( {isExpandable && (
<button <button

22
beta/src/components/MDX/Sandpack/Error.tsx

@ -24,3 +24,25 @@ export function Error({error}: {error: ErrorType}) {
</div> </div>
); );
} }
export function LintError({
error: {line, column, message},
}: {
error: {
line: number;
column: number;
message: string;
};
}) {
return (
<div
className={
'bg-white border-2 border-orange-40 border- border-red-40 rounded-lg p-6'
}>
<h2 className="text-red-40 text-xl mb-4">Lint Error</h2>
<pre className="text-secondary whitespace-pre-wrap break-words">
{line}:{column} - {message}
</pre>
</div>
);
}

18
beta/src/components/MDX/Sandpack/Preview.tsx

@ -7,13 +7,15 @@ import * as React from 'react';
import {useSandpack, LoadingOverlay} from '@codesandbox/sandpack-react'; import {useSandpack, LoadingOverlay} from '@codesandbox/sandpack-react';
import cn from 'classnames'; import cn from 'classnames';
import {Error} from './Error'; import {Error, LintError} from './Error';
import {computeViewportSize, generateRandomId} from './utils'; import {computeViewportSize, generateRandomId} from './utils';
import type {LintDiagnostic} from './utils';
type CustomPreviewProps = { type CustomPreviewProps = {
className?: string; className?: string;
customStyle?: Record<string, unknown>; customStyle?: Record<string, unknown>;
isExpanded: boolean; isExpanded: boolean;
lintErrors: LintDiagnostic;
}; };
function useDebounced(value: any): any { function useDebounced(value: any): any {
@ -32,6 +34,7 @@ export function Preview({
customStyle, customStyle,
isExpanded, isExpanded,
className, className,
lintErrors,
}: CustomPreviewProps) { }: CustomPreviewProps) {
const {sandpack, listen} = useSandpack(); const {sandpack, listen} = useSandpack();
const [isReady, setIsReady] = React.useState(false); const [isReady, setIsReady] = React.useState(false);
@ -107,7 +110,7 @@ export function Preview({
maxHeight: undefined, maxHeight: undefined,
} }
: null; : null;
const hideContent = !isReady || error; const hideContent = !isReady || error || lintErrors.length;
// WARNING: // WARNING:
// The layout and styling here is convoluted and really easy to break. // The layout and styling here is convoluted and really easy to break.
@ -186,6 +189,17 @@ export function Preview({
clientId={clientId.current} clientId={clientId.current}
loading={!isReady && iframeComputedHeight === null} loading={!isReady && iframeComputedHeight === null}
/> />
{/*
* TODO: properly style the errors
*/}
{lintErrors.length > 0 && !error && (
<div className={cn('p-2', isExpanded ? 'sticky top-8' : null)}>
<div style={{zIndex: 99}}>
<LintError error={lintErrors[0]} />
</div>
</div>
)}
</div> </div>
</div> </div>
); );

85
beta/src/components/MDX/Sandpack/eslint-integration.tsx

@ -0,0 +1,85 @@
// @ts-nocheck
import {Linter} from 'eslint/lib/linter/linter';
import type {Diagnostic} from '@codemirror/lint';
import type {Text} from '@codemirror/text';
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',
},
};
export const lintDiagnostic = (
doc: Text
): {errors: any[]; codeMirrorPayload: Diagnostic[]} => {
const codeString = doc.toString();
const errors = linter.verify(codeString, options) as any[];
const severity = {
1: 'warning',
2: 'error',
};
const codeMirrorPayload = 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 {
from,
to,
severity: severity[error.severity],
message: error.message,
};
})
.filter(Boolean) as Diagnostic[];
return {
codeMirrorPayload,
errors: errors.map((item) => {
return {
...item,
severity: severity[item.severity],
};
}),
};
};

28
beta/src/components/MDX/Sandpack/utils.ts

@ -1,6 +1,10 @@
/* /*
* Copyright (c) Facebook, Inc. and its affiliates. * Copyright (c) Facebook, Inc. and its affiliates.
*/ */
import {useState} from 'react';
import {lintDiagnostic} from './eslint-integration';
import {linter} from '@codemirror/lint';
import type {EditorView} from '@codemirror/view';
import type {SandpackFile} from '@codesandbox/sandpack-react'; import type {SandpackFile} from '@codesandbox/sandpack-react';
export type ViewportSizePreset = export type ViewportSizePreset =
| 'iPhone X' | 'iPhone X'
@ -95,3 +99,27 @@ export const createFileMap = (codeSnippets: any) => {
{} {}
); );
}; };
export type LintDiagnostic = {
line: number;
column: number;
severity: 'warning' | 'error';
message: string;
}[];
export const useSandpackLint = () => {
const [lintErrors, setDiagnostic] = useState<LintDiagnostic>([]);
const onLint = linter((props: EditorView) => {
const editorState = props.state.doc;
return import('./eslint-integration').then((module) => {
const {errors} = module.lintDiagnostic(editorState);
setDiagnostic(errors);
return module.lintDiagnostic(editorState).codeMirrorPayload;
});
});
return {lintErrors, onLint};
};

13
beta/src/styles/sandpack.css

@ -104,6 +104,19 @@ html.dark .sp-tabs .sp-tab-button[data-active='true'] {
line-height: 20px; line-height: 20px;
color: #ff3d3d; color: #ff3d3d;
} }
.sp-code-editor .cm-tooltip {
border: 0;
max-width: 200px;
}
html.dark .sp-code-editor .cm-diagnostic {
color: var(--sp-colors-bg-default);
}
.sp-code-editor .cm-diagnostic-error {
@apply border-red-40;
}
.sp-code-editor .cm-diagnostic-warning {
border-left: 5px solid orange;
}
/* /*
* These are manually adjusted to match the final * These are manually adjusted to match the final

55
beta/yarn.lock

@ -547,18 +547,18 @@
style-mod "^4.0.0" style-mod "^4.0.0"
w3c-keyname "^2.2.4" w3c-keyname "^2.2.4"
"@codesandbox/sandpack-client@^0.14.3-experimental.0": "@codesandbox/sandpack-client@^0.19.8-experimental.0":
version "0.14.3-experimental.0" version "0.19.8-experimental.0"
resolved "https://registry.yarnpkg.com/@codesandbox/sandpack-client/-/sandpack-client-0.14.3-experimental.0.tgz#1e4643e5d635528623edc8d45de582784aea4d1d" resolved "https://registry.yarnpkg.com/@codesandbox/sandpack-client/-/sandpack-client-0.19.8-experimental.0.tgz#044afb1efce0356e18b6d644960694c817012a2a"
integrity sha512-7KWOV4mPcQQ0TUheCZV0AC0R51DrtfQysvYCvD8iYJh3f59qBFp8JA3OT0Ffgu27ej/AV5kuXHvFOCGvUD/LwA== integrity sha512-u9/mqJ/k+dv/R2bHuVYoW3wXx5D3vLGnqbRKsGYIBDY/9xV7W3gFGY9ZgbcAbiw64qBAgiId1tzgaIwNcxNEsA==
dependencies: dependencies:
codesandbox-import-utils "^1.2.3" codesandbox-import-utils "^1.2.3"
lodash.isequal "^4.5.0" lodash.isequal "^4.5.0"
"@codesandbox/sandpack-react@v0.14.3-experimental.1": "@codesandbox/sandpack-react@v0.19.8-experimental.0":
version "0.14.3-experimental.1" version "0.19.8-experimental.0"
resolved "https://registry.yarnpkg.com/@codesandbox/sandpack-react/-/sandpack-react-0.14.3-experimental.1.tgz#574eb822449089c77b29e0b8bc7ac1d8ee20d0be" resolved "https://registry.yarnpkg.com/@codesandbox/sandpack-react/-/sandpack-react-0.19.8-experimental.0.tgz#0a86971be53f5d177d61796d4e048998c2dcc271"
integrity sha512-5jFVLJKunVPUgZi105p+nxjjTJN+w/MwO3abW/Ob17jp/UT6o+m1x5XhiJVpc832/jwWGrDGKukFMbOtk35CuQ== integrity sha512-LyEqxlH1LoHLrNKK/Ge0rumLriOiS4Hhy8M9NPgx0f4wCJjQQ9NW8lHzkOwCo3NonWOq5ZsIXSVQAqiWxFz1Bg==
dependencies: dependencies:
"@code-hike/classer" "^0.0.0-aa6efee" "@code-hike/classer" "^0.0.0-aa6efee"
"@codemirror/closebrackets" "^0.19.0" "@codemirror/closebrackets" "^0.19.0"
@ -574,11 +574,11 @@
"@codemirror/matchbrackets" "^0.19.3" "@codemirror/matchbrackets" "^0.19.3"
"@codemirror/state" "^0.19.6" "@codemirror/state" "^0.19.6"
"@codemirror/view" "^0.19.32" "@codemirror/view" "^0.19.32"
"@codesandbox/sandpack-client" "^0.14.3-experimental.0" "@codesandbox/sandpack-client" "^0.19.8-experimental.0"
"@react-hook/intersection-observer" "^3.1.1" "@react-hook/intersection-observer" "^3.1.1"
codesandbox-import-util-types "^2.2.3" codesandbox-import-util-types "^2.2.3"
codesandbox-import-utils "^2.2.3"
lodash.isequal "^4.5.0" lodash.isequal "^4.5.0"
lz-string "^1.4.4"
react-devtools-inline "4.22.1" react-devtools-inline "4.22.1"
react-is "^17.0.2" react-is "^17.0.2"
@ -1326,7 +1326,7 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
binaryextensions@2, binaryextensions@^2.1.2: binaryextensions@2:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.3.0.tgz#1d269cbf7e6243ea886aa41453c3651ccbe13c22" resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.3.0.tgz#1d269cbf7e6243ea886aa41453c3651ccbe13c22"
integrity sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg== integrity sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==
@ -1504,15 +1504,6 @@ codesandbox-import-utils@^1.2.3:
istextorbinary "2.2.1" istextorbinary "2.2.1"
lz-string "^1.4.4" lz-string "^1.4.4"
codesandbox-import-utils@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/codesandbox-import-utils/-/codesandbox-import-utils-2.2.3.tgz#f7b4801245b381cb8c90fe245e336624e19b6c84"
integrity sha512-ymtmcgZKU27U+nM2qUb21aO8Ut/u2S9s6KorOgG81weP+NA0UZkaHKlaRqbLJ9h4i/4FLvwmEXYAnTjNmp6ogg==
dependencies:
codesandbox-import-util-types "^2.2.3"
istextorbinary "^2.2.1"
lz-string "^1.4.4"
collapse-white-space@^1.0.2: collapse-white-space@^1.0.2:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287"
@ -1782,14 +1773,6 @@ editions@^1.3.3:
resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b" resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b"
integrity sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg== integrity sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==
editions@^2.2.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/editions/-/editions-2.3.1.tgz#3bc9962f1978e801312fbd0aebfed63b49bfe698"
integrity sha512-ptGvkwTvGdGfC0hfhKg0MT+TRLRKGtUiWGBInxOm5pz7ssADezahjCUaYuZ8Dr+C05FW0AECIIPt4WBxVINEhA==
dependencies:
errlop "^2.0.0"
semver "^6.3.0"
electron-to-chromium@^1.4.17: electron-to-chromium@^1.4.17:
version "1.4.51" version "1.4.51"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.51.tgz#a432f5a5d983ace79278a33057300cf949627e63" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.51.tgz#a432f5a5d983ace79278a33057300cf949627e63"
@ -1817,11 +1800,6 @@ enquirer@^2.3.5:
dependencies: dependencies:
ansi-colors "^4.1.1" ansi-colors "^4.1.1"
errlop@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/errlop/-/errlop-2.2.0.tgz#1ff383f8f917ae328bebb802d6ca69666a42d21b"
integrity sha512-e64Qj9+4aZzjzzFpZC7p5kmm/ccCrbLhAJplhsDXQFs87XTsXwOpH4s1Io2s90Tau/8r2j9f4l/thhDevRjzxw==
error-ex@^1.3.1: error-ex@^1.3.1:
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
@ -2839,15 +2817,6 @@ istextorbinary@2.2.1:
editions "^1.3.3" editions "^1.3.3"
textextensions "2" textextensions "2"
istextorbinary@^2.2.1:
version "2.6.0"
resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.6.0.tgz#60776315fb0fa3999add276c02c69557b9ca28ab"
integrity sha512-+XRlFseT8B3L9KyjxxLjfXSLMuErKDsd8DBNrsaxoViABMEZlOSCstwmw0qpoFX3+U6yWU1yhLudAe6/lETGGA==
dependencies:
binaryextensions "^2.1.2"
editions "^2.2.0"
textextensions "^2.5.0"
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@ -4742,7 +4711,7 @@ text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
textextensions@2, textextensions@^2.5.0: textextensions@2:
version "2.6.0" version "2.6.0"
resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.6.0.tgz#d7e4ab13fe54e32e08873be40d51b74229b00fc4" resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.6.0.tgz#d7e4ab13fe54e32e08873be40d51b74229b00fc4"
integrity sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ== integrity sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ==

Loading…
Cancel
Save