From 20ad0e8ca9600d29772e011abeb5933c62fc55cd Mon Sep 17 00:00:00 2001 From: dan Date: Mon, 23 May 2022 17:14:21 +0100 Subject: [PATCH] [Beta] Lazy-load linter code (#4675) * [Beta] Lazy-load linter code * Split utils into separate files --- .../components/MDX/Sandpack/CustomPreset.tsx | 6 +- beta/src/components/MDX/Sandpack/Preview.tsx | 7 +- .../components/MDX/Sandpack/SandpackRoot.tsx | 2 +- .../MDX/Sandpack/computeViewportSize.ts | 46 +++++++ .../components/MDX/Sandpack/createFileMap.ts | 53 ++++++++ beta/src/components/MDX/Sandpack/index.tsx | 2 +- .../{eslint-integration.tsx => runESLint.tsx} | 2 +- .../MDX/Sandpack/useSandpackLint.tsx | 35 +++++ beta/src/components/MDX/Sandpack/utils.ts | 124 ------------------ 9 files changed, 145 insertions(+), 132 deletions(-) create mode 100644 beta/src/components/MDX/Sandpack/computeViewportSize.ts create mode 100644 beta/src/components/MDX/Sandpack/createFileMap.ts rename beta/src/components/MDX/Sandpack/{eslint-integration.tsx => runESLint.tsx} (98%) create mode 100644 beta/src/components/MDX/Sandpack/useSandpackLint.tsx delete mode 100644 beta/src/components/MDX/Sandpack/utils.ts diff --git a/beta/src/components/MDX/Sandpack/CustomPreset.tsx b/beta/src/components/MDX/Sandpack/CustomPreset.tsx index a7b401b4f..4f250af8d 100644 --- a/beta/src/components/MDX/Sandpack/CustomPreset.tsx +++ b/beta/src/components/MDX/Sandpack/CustomPreset.tsx @@ -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(null); const {sandpack} = useSandpack(); @@ -64,7 +64,7 @@ export function CustomPreset({ showInlineErrors showTabs={false} showRunButton={false} - extensions={[onLint]} + extensions={lintExtensions} /> + Math.floor(Math.random() * 10000).toString(); type CustomPreviewProps = { className?: string; diff --git a/beta/src/components/MDX/Sandpack/SandpackRoot.tsx b/beta/src/components/MDX/Sandpack/SandpackRoot.tsx index 444776e1b..a4df485bd 100644 --- a/beta/src/components/MDX/Sandpack/SandpackRoot.tsx +++ b/beta/src/components/MDX/Sandpack/SandpackRoot.tsx @@ -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'; diff --git a/beta/src/components/MDX/Sandpack/computeViewportSize.ts b/beta/src/components/MDX/Sandpack/computeViewportSize.ts new file mode 100644 index 000000000..81007d1bd --- /dev/null +++ b/beta/src/components/MDX/Sandpack/computeViewportSize.ts @@ -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; +}; diff --git a/beta/src/components/MDX/Sandpack/createFileMap.ts b/beta/src/components/MDX/Sandpack/createFileMap.ts new file mode 100644 index 000000000..670161008 --- /dev/null +++ b/beta/src/components/MDX/Sandpack/createFileMap.ts @@ -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, 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; + }, + {} + ); +}; diff --git a/beta/src/components/MDX/Sandpack/index.tsx b/beta/src/components/MDX/Sandpack/index.tsx index 104aad55b..ad3a2eba9 100644 --- a/beta/src/components/MDX/Sandpack/index.tsx +++ b/beta/src/components/MDX/Sandpack/index.tsx @@ -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}); diff --git a/beta/src/components/MDX/Sandpack/eslint-integration.tsx b/beta/src/components/MDX/Sandpack/runESLint.tsx similarity index 98% rename from beta/src/components/MDX/Sandpack/eslint-integration.tsx rename to beta/src/components/MDX/Sandpack/runESLint.tsx index f85d0905c..8f1ae4924 100644 --- a/beta/src/components/MDX/Sandpack/eslint-integration.tsx +++ b/beta/src/components/MDX/Sandpack/runESLint.tsx @@ -39,7 +39,7 @@ const options = { }, }; -export const lintDiagnostic = ( +export const runESLint = ( doc: Text ): {errors: any[]; codeMirrorPayload: Diagnostic[]} => { const codeString = doc.toString(); diff --git a/beta/src/components/MDX/Sandpack/useSandpackLint.tsx b/beta/src/components/MDX/Sandpack/useSandpackLint.tsx new file mode 100644 index 000000000..7b095aa93 --- /dev/null +++ b/beta/src/components/MDX/Sandpack/useSandpackLint.tsx @@ -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([]); + + // 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]}; +}; diff --git a/beta/src/components/MDX/Sandpack/utils.ts b/beta/src/components/MDX/Sandpack/utils.ts deleted file mode 100644 index bc8c88a4d..000000000 --- a/beta/src/components/MDX/Sandpack/utils.ts +++ /dev/null @@ -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, 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([]); - - 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}; -};