mirror of
https://github.com/reactjs/react.dev.git
synced 2026-02-24 04:33:10 +00:00
[Beta] Lazy-load linter code (#4675)
* [Beta] Lazy-load linter code * Split utils into separate files
This commit is contained in:
@@ -18,7 +18,7 @@ import {IconChevron} from 'components/Icon/IconChevron';
|
||||
import {NavigationBar} from './NavigationBar';
|
||||
import {Preview} from './Preview';
|
||||
import {CustomTheme} from './Themes';
|
||||
import {useSandpackLint} from './utils';
|
||||
import {useSandpackLint} from './useSandpackLint';
|
||||
|
||||
export function CustomPreset({
|
||||
isSingleFile,
|
||||
@@ -31,7 +31,7 @@ export function CustomPreset({
|
||||
devToolsLoaded: boolean;
|
||||
onDevToolsLoad: () => void;
|
||||
}) {
|
||||
const {lintErrors, onLint} = useSandpackLint();
|
||||
const {lintErrors, lintExtensions} = useSandpackLint();
|
||||
const lineCountRef = React.useRef<{[key: string]: number}>({});
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const {sandpack} = useSandpack();
|
||||
@@ -64,7 +64,7 @@ export function CustomPreset({
|
||||
showInlineErrors
|
||||
showTabs={false}
|
||||
showRunButton={false}
|
||||
extensions={[onLint]}
|
||||
extensions={lintExtensions}
|
||||
/>
|
||||
<Preview
|
||||
className="order-last xl:order-2"
|
||||
|
||||
@@ -8,8 +8,11 @@ import {useSandpack, LoadingOverlay} from '@codesandbox/sandpack-react';
|
||||
import cn from 'classnames';
|
||||
|
||||
import {Error} from './Error';
|
||||
import {computeViewportSize, generateRandomId} from './utils';
|
||||
import type {LintDiagnostic} from './utils';
|
||||
import {computeViewportSize} from './computeViewportSize';
|
||||
import type {LintDiagnostic} from './useSandpackLint';
|
||||
|
||||
const generateRandomId = (): string =>
|
||||
Math.floor(Math.random() * 10000).toString();
|
||||
|
||||
type CustomPreviewProps = {
|
||||
className?: string;
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as React from 'react';
|
||||
import {SandpackProvider} from '@codesandbox/sandpack-react';
|
||||
import {SandpackLogLevel} from '@codesandbox/sandpack-client';
|
||||
import {CustomPreset} from './CustomPreset';
|
||||
import {createFileMap} from './utils';
|
||||
import {createFileMap} from './createFileMap';
|
||||
|
||||
import type {SandpackSetup} from '@codesandbox/sandpack-react';
|
||||
|
||||
|
||||
46
beta/src/components/MDX/Sandpack/computeViewportSize.ts
Normal file
46
beta/src/components/MDX/Sandpack/computeViewportSize.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
export type ViewportSizePreset =
|
||||
| 'iPhone X'
|
||||
| 'Pixel 2'
|
||||
| 'iPad'
|
||||
| 'Moto G4'
|
||||
| 'Surface Duo';
|
||||
|
||||
export type ViewportSize =
|
||||
| ViewportSizePreset
|
||||
| 'auto'
|
||||
| {width: number; height: number};
|
||||
|
||||
export type ViewportOrientation = 'portrait' | 'landscape';
|
||||
|
||||
const VIEWPORT_SIZE_PRESET_MAP: Record<
|
||||
ViewportSizePreset,
|
||||
{x: number; y: number}
|
||||
> = {
|
||||
'iPhone X': {x: 375, y: 812},
|
||||
iPad: {x: 768, y: 1024},
|
||||
'Pixel 2': {x: 411, y: 731},
|
||||
'Moto G4': {x: 360, y: 640},
|
||||
'Surface Duo': {x: 540, y: 720},
|
||||
};
|
||||
|
||||
export const computeViewportSize = (
|
||||
viewport: ViewportSize,
|
||||
orientation: ViewportOrientation
|
||||
): {width?: number; height?: number} => {
|
||||
if (viewport === 'auto') {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (typeof viewport === 'string') {
|
||||
const {x, y} = VIEWPORT_SIZE_PRESET_MAP[viewport];
|
||||
return orientation === 'portrait'
|
||||
? {width: x, height: y}
|
||||
: {width: y, height: x};
|
||||
}
|
||||
|
||||
return viewport;
|
||||
};
|
||||
53
beta/src/components/MDX/Sandpack/createFileMap.ts
Normal file
53
beta/src/components/MDX/Sandpack/createFileMap.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
import type {SandpackFile} from '@codesandbox/sandpack-react';
|
||||
|
||||
export const createFileMap = (codeSnippets: any) => {
|
||||
return codeSnippets.reduce(
|
||||
(result: Record<string, SandpackFile>, codeSnippet: React.ReactElement) => {
|
||||
if (codeSnippet.props.mdxType !== 'pre') {
|
||||
return result;
|
||||
}
|
||||
const {props} = codeSnippet.props.children;
|
||||
let filePath; // path in the folder structure
|
||||
let fileHidden = false; // if the file is available as a tab
|
||||
let fileActive = false; // if the file tab is shown by default
|
||||
|
||||
if (props.metastring) {
|
||||
const [name, ...params] = props.metastring.split(' ');
|
||||
filePath = '/' + name;
|
||||
if (params.includes('hidden')) {
|
||||
fileHidden = true;
|
||||
}
|
||||
if (params.includes('active')) {
|
||||
fileActive = true;
|
||||
}
|
||||
} else {
|
||||
if (props.className === 'language-js') {
|
||||
filePath = '/App.js';
|
||||
} else if (props.className === 'language-css') {
|
||||
filePath = '/styles.css';
|
||||
} else {
|
||||
throw new Error(
|
||||
`Code block is missing a filename: ${props.children}`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (result[filePath]) {
|
||||
throw new Error(
|
||||
`File ${filePath} was defined multiple times. Each file snippet should have a unique path name`
|
||||
);
|
||||
}
|
||||
result[filePath] = {
|
||||
code: props.children as string,
|
||||
hidden: fileHidden,
|
||||
active: fileActive,
|
||||
};
|
||||
|
||||
return result;
|
||||
},
|
||||
{}
|
||||
);
|
||||
};
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import {createFileMap} from './utils';
|
||||
import {createFileMap} from './createFileMap';
|
||||
|
||||
const SandpackRoot = dynamic(() => import('./SandpackRoot'), {suspense: true});
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ const options = {
|
||||
},
|
||||
};
|
||||
|
||||
export const lintDiagnostic = (
|
||||
export const runESLint = (
|
||||
doc: Text
|
||||
): {errors: any[]; codeMirrorPayload: Diagnostic[]} => {
|
||||
const codeString = doc.toString();
|
||||
35
beta/src/components/MDX/Sandpack/useSandpackLint.tsx
Normal file
35
beta/src/components/MDX/Sandpack/useSandpackLint.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import {useState, useEffect} from 'react';
|
||||
import {linter} from '@codemirror/lint';
|
||||
import type {Diagnostic} from '@codemirror/lint';
|
||||
import type {Text} from '@codemirror/text';
|
||||
import type {EditorView} from '@codemirror/view';
|
||||
|
||||
export type LintDiagnostic = {
|
||||
line: number;
|
||||
column: number;
|
||||
severity: 'warning' | 'error';
|
||||
message: string;
|
||||
}[];
|
||||
|
||||
export const useSandpackLint = () => {
|
||||
const [lintErrors, setLintErrors] = useState<LintDiagnostic>([]);
|
||||
|
||||
// TODO: ideally @codemirror/linter would be code-split too but I don't know how
|
||||
// because Sandpack seems to ignore updates to the "extensions" prop.
|
||||
const onLint = linter(async (props: EditorView) => {
|
||||
const {runESLint} = await import('./runESLint');
|
||||
const editorState = props.state.doc;
|
||||
let {errors, codeMirrorPayload} = runESLint(editorState);
|
||||
// Only show errors from rules, not parsing errors etc
|
||||
setLintErrors(errors.filter((e) => !e.fatal));
|
||||
return codeMirrorPayload;
|
||||
});
|
||||
|
||||
return {lintErrors, lintExtensions: [onLint]};
|
||||
};
|
||||
@@ -1,124 +0,0 @@
|
||||
/*
|
||||
* 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';
|
||||
export type ViewportSizePreset =
|
||||
| 'iPhone X'
|
||||
| 'Pixel 2'
|
||||
| 'iPad'
|
||||
| 'Moto G4'
|
||||
| 'Surface Duo';
|
||||
|
||||
export type ViewportSize =
|
||||
| ViewportSizePreset
|
||||
| 'auto'
|
||||
| {width: number; height: number};
|
||||
|
||||
export type ViewportOrientation = 'portrait' | 'landscape';
|
||||
|
||||
export const generateRandomId = (): string =>
|
||||
Math.floor(Math.random() * 10000).toString();
|
||||
|
||||
const VIEWPORT_SIZE_PRESET_MAP: Record<
|
||||
ViewportSizePreset,
|
||||
{x: number; y: number}
|
||||
> = {
|
||||
'iPhone X': {x: 375, y: 812},
|
||||
iPad: {x: 768, y: 1024},
|
||||
'Pixel 2': {x: 411, y: 731},
|
||||
'Moto G4': {x: 360, y: 640},
|
||||
'Surface Duo': {x: 540, y: 720},
|
||||
};
|
||||
|
||||
export const computeViewportSize = (
|
||||
viewport: ViewportSize,
|
||||
orientation: ViewportOrientation
|
||||
): {width?: number; height?: number} => {
|
||||
if (viewport === 'auto') {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (typeof viewport === 'string') {
|
||||
const {x, y} = VIEWPORT_SIZE_PRESET_MAP[viewport];
|
||||
return orientation === 'portrait'
|
||||
? {width: x, height: y}
|
||||
: {width: y, height: x};
|
||||
}
|
||||
|
||||
return viewport;
|
||||
};
|
||||
|
||||
export const createFileMap = (codeSnippets: any) => {
|
||||
return codeSnippets.reduce(
|
||||
(result: Record<string, SandpackFile>, codeSnippet: React.ReactElement) => {
|
||||
if (codeSnippet.props.mdxType !== 'pre') {
|
||||
return result;
|
||||
}
|
||||
const {props} = codeSnippet.props.children;
|
||||
let filePath; // path in the folder structure
|
||||
let fileHidden = false; // if the file is available as a tab
|
||||
let fileActive = false; // if the file tab is shown by default
|
||||
|
||||
if (props.metastring) {
|
||||
const [name, ...params] = props.metastring.split(' ');
|
||||
filePath = '/' + name;
|
||||
if (params.includes('hidden')) {
|
||||
fileHidden = true;
|
||||
}
|
||||
if (params.includes('active')) {
|
||||
fileActive = true;
|
||||
}
|
||||
} else {
|
||||
if (props.className === 'language-js') {
|
||||
filePath = '/App.js';
|
||||
} else if (props.className === 'language-css') {
|
||||
filePath = '/styles.css';
|
||||
} else {
|
||||
throw new Error(
|
||||
`Code block is missing a filename: ${props.children}`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (result[filePath]) {
|
||||
throw new Error(
|
||||
`File ${filePath} was defined multiple times. Each file snippet should have a unique path name`
|
||||
);
|
||||
}
|
||||
result[filePath] = {
|
||||
code: props.children as string,
|
||||
hidden: fileHidden,
|
||||
active: fileActive,
|
||||
};
|
||||
|
||||
return result;
|
||||
},
|
||||
{}
|
||||
);
|
||||
};
|
||||
|
||||
export type LintDiagnostic = {
|
||||
line: number;
|
||||
column: number;
|
||||
severity: 'warning' | 'error';
|
||||
message: string;
|
||||
}[];
|
||||
|
||||
export const useSandpackLint = () => {
|
||||
const [lintErrors, setLintErrors] = useState<LintDiagnostic>([]);
|
||||
|
||||
const onLint = linter((props: EditorView) => {
|
||||
const editorState = props.state.doc;
|
||||
return import('./eslint-integration').then((module) => {
|
||||
let {errors} = module.lintDiagnostic(editorState);
|
||||
// Only show errors from rules, not parsing errors etc
|
||||
setLintErrors(errors.filter((e) => !e.fatal));
|
||||
return module.lintDiagnostic(editorState).codeMirrorPayload;
|
||||
});
|
||||
});
|
||||
|
||||
return {lintErrors, onLint};
|
||||
};
|
||||
Reference in New Issue
Block a user