diff --git a/.yarn/patches/react-split-pane-npm-0.1.92-93dbf51dff.patch b/.yarn/patches/react-split-pane-npm-0.1.92-93dbf51dff.patch deleted file mode 100644 index 33aeafadbde9..000000000000 --- a/.yarn/patches/react-split-pane-npm-0.1.92-93dbf51dff.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/index.d.ts b/index.d.ts -index d116f54d6da12d24b48e24ff3636c9066059aa58..20a132e2a8a2bf0b023af87699d6b0703c9b1a1a 100644 ---- a/index.d.ts -+++ b/index.d.ts -@@ -6,6 +6,7 @@ export type Split = 'vertical' | 'horizontal'; - - export type SplitPaneProps = { - allowResize?: boolean; -+ children?: React.ReactNode; - className?: string; - primary?: 'first' | 'second'; - minSize?: Size; diff --git a/packages/typescript-estree/src/use-at-your-own-risk.ts b/packages/typescript-estree/src/use-at-your-own-risk.ts index 951d68d649af..1c70fbb3311c 100644 --- a/packages/typescript-estree/src/use-at-your-own-risk.ts +++ b/packages/typescript-estree/src/use-at-your-own-risk.ts @@ -1,5 +1,4 @@ // required by website -export * from './create-program/getScriptKind'; export * from './ast-converter'; export type { ParseSettings } from './parseSettings'; diff --git a/packages/website/package.json b/packages/website/package.json index a61e1e667b43..30230e1ee4aa 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -6,7 +6,7 @@ "build": "docusaurus build", "clear": "docusaurus clear", "format": "prettier --write \"./**/*.{md,mdx,ts,js,tsx,jsx}\" --ignore-path ../../.prettierignore", - "generate-website-dts": "tsx ./tools/generate-website-dts.ts", + "generate-package-versions": "tsx ./tools/generate-package-versions.ts", "stylelint": "stylelint \"src/**/*.css\"", "stylelint:fix": "stylelint \"src/**/*.css\" --fix", "lint": "nx lint", @@ -24,8 +24,10 @@ "@docusaurus/remark-plugin-npm2yarn": "~2.4.1", "@docusaurus/theme-common": "~2.4.1", "@mdx-js/react": "1.6.22", + "@monaco-editor/react": "^4.3.0", "@typescript-eslint/parser": "6.7.2", "@typescript-eslint/website-eslint": "6.7.2", + "@typescript/vfs": "^1.4.0", "clsx": "^2.0.0", "eslint": "*", "json-schema": "^0.4.0", @@ -36,7 +38,7 @@ "prism-react-renderer": "^1.3.3", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-split-pane": "^0.1.92", + "react-resizable-panels": "^0.0.55", "remark-docusaurus-tabs": "^0.2.0", "semver": "^7.5.4", "ts-node": "*", diff --git a/packages/website/src/components/OptionsSelector.tsx b/packages/website/src/components/OptionsSelector.tsx index 00870a850c35..44052beeb275 100644 --- a/packages/website/src/components/OptionsSelector.tsx +++ b/packages/website/src/components/OptionsSelector.tsx @@ -15,19 +15,18 @@ import ActionLabel from './layout/ActionLabel'; import Expander from './layout/Expander'; import InputLabel from './layout/InputLabel'; import { createMarkdown, createMarkdownParams } from './lib/markdown'; -import { fileTypes } from './options'; +import { fileTypes, tsVersions } from './options'; +import styles from './Playground.module.css'; import type { ConfigModel } from './types'; export interface OptionsSelectorParams { readonly state: ConfigModel; readonly setState: (cfg: Partial) => void; - readonly tsVersions: readonly string[]; } function OptionsSelectorContent({ state, setState, - tsVersions, }: OptionsSelectorParams): React.JSX.Element { const [copyLink, copyLinkToClipboard] = useClipboard(() => document.location.toString(), @@ -54,10 +53,9 @@ function OptionsSelectorContent({ setState({ ts })} - options={(tsVersions.length && tsVersions) || [state.ts]} /> {process.env.ESLINT_VERSION} @@ -75,7 +73,7 @@ function OptionsSelectorContent({ setState({ sourceType })} options={['script', 'module']} /> @@ -132,7 +130,12 @@ function OptionsSelector(props: OptionsSelectorParams): React.JSX.Element { /> ); } - return ; + + return ( +
+ +
+ ); } export default OptionsSelector; diff --git a/packages/website/src/components/Playground.module.css b/packages/website/src/components/Playground.module.css index b824d441fafc..b1a67ea21389 100644 --- a/packages/website/src/components/Playground.module.css +++ b/packages/website/src/components/Playground.module.css @@ -3,89 +3,95 @@ --playground-secondary-color: var(--ifm-color-emphasis-100); } -.options { +.playgroundMenu { + width: 100%; background: var(--playground-main-color); overflow: auto; - z-index: 1; } -.sourceCode { - height: 100%; - border: 1px solid var(--playground-secondary-color); +.tabCode { + flex: 1; + overflow: hidden; } -.codeBlocks { - display: flex; - flex: 1 1 0; - flex-direction: row; - height: 100%; - width: calc(100vw - 20rem); +.hidden { + display: none; + visibility: hidden; } -.astViewer { - height: 100%; +.playgroundInfoContainer { width: 100%; - border: 1px solid var(--playground-secondary-color); - padding: 0; + height: 100%; overflow: auto; - word-wrap: initial; - white-space: nowrap; background: var(--code-editor-bg); } -.codeContainer { +.playgroundInfo { + word-wrap: normal; + width: 100%; + position: relative; + padding: 5px 0; +} + +.panelGroup { + height: calc(100vh - 60px) !important; + border-top: 1px solid var(--ifm-color-emphasis-200); +} + +.Panel { display: flex; flex-direction: row; - position: fixed; - width: 100%; - height: calc(100% - var(--ifm-navbar-height)); - top: var(--ifm-navbar-height); - z-index: calc(var(--ifm-z-index-fixed) - 1); + font-size: 2rem; } -.playgroundInfoHeader { - position: sticky; - top: 0; - left: 0; - z-index: 1; +.PanelColumn, +.PanelRow { + display: flex; } -.tabCode { - height: calc(100% - 41px); - overflow: auto; +.PanelColumn { + flex-direction: column; } -.hidden { - display: none; - visibility: hidden; +.PanelRow { + flex-direction: row; } -@media only screen and (width <= 996px) { - .codeContainer { - display: block; - width: 100%; - position: static; - } +.PanelResizeHandle { + --resize-border-color: var(--playground-main-color); + --resize-background-color: var(--ifm-color-emphasis-200); - .codeBlocks { - display: block; - width: 100%; - } + flex: 0 0 11px; + background-clip: padding-box; + display: flex; + justify-content: stretch; + align-items: stretch; + outline: none; + transition: border-color 0.2s linear, background-color 0.2s linear; + background-color: var(--resize-background-color); + border-color: var(--resize-border-color); + border-style: solid; + border-width: 0 5px; +} - .options { - display: none; - } +.PanelResizeHandle[data-panel-group-direction='vertical'] { + flex-direction: column; + border-width: 5px 0; +} - .tabCode { - height: calc(30rem - 3.2rem); - } +.PanelResizeHandle[data-resize-handle-active], +.PanelResizeHandle:hover { + --resize-border-color: var(--ifm-color-emphasis-200); + --resize-background-color: var(--ifm-color-emphasis-300); +} - .astViewer, - .sourceCode { - height: 30rem; +@media (max-width: 500px) { + .PanelResizeHandle { + flex: 0 0 21px; + border-width: 0 10px; } - .astViewer { - height: auto; + .PanelResizeHandle[data-panel-group-direction='vertical'] { + border-width: 10px 0; } } diff --git a/packages/website/src/components/Playground.tsx b/packages/website/src/components/Playground.tsx index ef45fecab317..dabb991a3559 100644 --- a/packages/website/src/components/Playground.tsx +++ b/packages/website/src/components/Playground.tsx @@ -1,193 +1,193 @@ -import clsx from 'clsx'; +import { useWindowSize } from '@docusaurus/theme-common'; import type * as ESQuery from 'esquery'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import type { ImperativePanelHandle } from 'react-resizable-panels'; +import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; import ASTViewer from './ast/ASTViewer'; import ConfigEslint from './config/ConfigEslint'; import ConfigTypeScript from './config/ConfigTypeScript'; -import { EditorEmbed } from './editor/EditorEmbed'; -import { LoadingEditor } from './editor/LoadingEditor'; -import { ErrorsViewer, ErrorViewer } from './ErrorsViewer'; +import LoadingEditor from './editor/LoadingEditor'; +import { ErrorsViewer } from './ErrorsViewer'; import { ESQueryFilter } from './ESQueryFilter'; import useHashState from './hooks/useHashState'; import EditorTabs from './layout/EditorTabs'; -import Loader from './layout/Loader'; -import type { UpdateModel } from './linter/types'; +import { createFileSystem } from './linter/bridge'; +import type { PlaygroundSystem, UpdateModel } from './linter/types'; import { defaultConfig, detailTabs } from './options'; import OptionsSelector from './OptionsSelector'; import styles from './Playground.module.css'; -import ConditionalSplitPane from './SplitPane/ConditionalSplitPane'; import { TypesDetails } from './typeDetails/TypesDetails'; -import type { ErrorGroup, RuleDetails, SelectedRange, TabType } from './types'; +import type { ErrorGroup } from './types'; function Playground(): React.JSX.Element { + const windowSize = useWindowSize(); const [state, setState] = useHashState(defaultConfig); - const [astModel, setAstModel] = useState(); - const [markers, setMarkers] = useState(); - const [ruleNames, setRuleNames] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [tsVersions, setTSVersion] = useState([]); - const [selectedRange, setSelectedRange] = useState(); - const [position, setPosition] = useState(); - const [activeTab, setTab] = useState('code'); - const [esQueryFilter, setEsQueryFilter] = useState(); - const [esQueryError, setEsQueryError] = useState(); + + const [system] = useState(() => createFileSystem(state)); + const [activeFile, setFileName] = useState(`input${state.fileType}`); + const [editorFile, setEditorFile] = useState(`input${state.fileType}`); const [visualEslintRc, setVisualEslintRc] = useState(false); const [visualTSConfig, setVisualTSConfig] = useState(false); + const [errors, setErrors] = useState([]); + const [astModel, setAstModel] = useState(); + const [esQueryFilter, setEsQueryFilter] = useState(); + const [selectedRange, setSelectedRange] = useState<[number, number]>(); + const [cursorPosition, onCursorChange] = useState(); + const playgroundMenuRef = useRef(null); - const onLoaded = useCallback( - (ruleNames: RuleDetails[], tsVersions: readonly string[]): void => { - setRuleNames(ruleNames); - setTSVersion(tsVersions); - setIsLoading(false); - }, - [], - ); - - const activeVisualEditor = !isLoading - ? visualEslintRc && activeTab === 'eslintrc' + const activeVisualEditor = + visualEslintRc && activeFile === '.eslintrc' ? 'eslintrc' - : visualTSConfig && activeTab === 'tsconfig' + : visualTSConfig && activeFile === 'tsconfig.json' ? 'tsconfig' - : undefined - : undefined; + : undefined; - const onVisualEditor = useCallback((tab: TabType): void => { - if (tab === 'tsconfig') { + const onVisualEditor = useCallback((tab: string): void => { + if (tab === 'tsconfig.json') { setVisualTSConfig(val => !val); - } else if (tab === 'eslintrc') { + } else if (tab === '.eslintrc') { setVisualEslintRc(val => !val); } }, []); + useEffect(() => { + const closeable = [ + system.watchFile('/input.*', fileName => { + setState({ code: system.readFile(fileName) }); + }), + system.watchFile('/.eslintrc', fileName => { + setState({ eslintrc: system.readFile(fileName) }); + }), + system.watchFile('/tsconfig.json', fileName => { + setState({ tsconfig: system.readFile(fileName) }); + }), + ]; + return () => { + closeable.forEach(d => d.close()); + }; + }, [setState, system]); + + useEffect(() => { + const newFile = `input${state.fileType}`; + if (newFile !== editorFile) { + if (editorFile === activeFile) { + setFileName(newFile); + } + setEditorFile(newFile); + } + }, [state, system, editorFile, activeFile]); + + useEffect(() => { + if (windowSize === 'mobile') { + playgroundMenuRef.current?.collapse(); + } else if (windowSize === 'desktop') { + playgroundMenuRef.current?.expand(); + } + }, [windowSize, playgroundMenuRef]); + return ( -
-
- + + + + + + -
- + + {(activeVisualEditor === 'eslintrc' && ( + + )) || + (activeVisualEditor === 'tsconfig' && ( + + ))} +
- -
- {isLoading && } + + + +
+
- {(activeVisualEditor === 'eslintrc' && ( - - )) || - (activeVisualEditor === 'tsconfig' && ( - - ))} -
- -
- setState({ showAST: v })} /> + {state.showAST === 'es' && ( + + )}
-
-
- setState({ showAST: v })} +
+ {!state.showAST || !astModel ? ( + + ) : state.showAST === 'types' && astModel.storedTsAST ? ( + - {state.showAST === 'es' && ( - - )} -
- - {(state.showAST === 'es' && esQueryError && ( - - )) || - (state.showAST === 'types' && astModel?.storedTsAST && ( - - )) || - (state.showAST && astModel && ( - - )) || } + )}
- - -
-
+
+
+
+ ); } diff --git a/packages/website/src/components/SplitPane/ConditionalSplitPane.tsx b/packages/website/src/components/SplitPane/ConditionalSplitPane.tsx deleted file mode 100644 index ffaa10cd36ab..000000000000 --- a/packages/website/src/components/SplitPane/ConditionalSplitPane.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useWindowSize } from '@docusaurus/theme-common'; -import clsx from 'clsx'; -import React from 'react'; -import SplitPane, { type SplitPaneProps } from 'react-split-pane'; - -import splitPaneStyles from './SplitPane.module.css'; - -function ConditionalSplitPane({ - children, - ...props -}: SplitPaneProps): React.JSX.Element { - const windowSize = useWindowSize(); - - return windowSize !== 'mobile' ? ( - - {children} - - ) : ( - <>{children} - ); -} - -export default ConditionalSplitPane; diff --git a/packages/website/src/components/SplitPane/SplitPane.module.css b/packages/website/src/components/SplitPane/SplitPane.module.css deleted file mode 100644 index 0c5bb9b16a4c..000000000000 --- a/packages/website/src/components/SplitPane/SplitPane.module.css +++ /dev/null @@ -1,24 +0,0 @@ -.resizer { - background: var(--ifm-color-emphasis-700); - opacity: 0.2; - z-index: 1; - box-sizing: border-box; - background-clip: padding-box; -} - -.resizer:hover { - transition: all 2s ease; -} - -.resizer.vertical { - width: 11px; - margin: 0 -5px; - border-left: 5px solid rgb(255 255 255 / 0%); - border-right: 5px solid rgb(255 255 255 / 0%); - cursor: col-resize; -} - -.resizer.vertical:hover { - border-left: 5px solid var(--ifm-color-emphasis-700); - border-right: 5px solid var(--ifm-color-emphasis-700); -} diff --git a/packages/website/src/components/ast/ASTViewer.tsx b/packages/website/src/components/ast/ASTViewer.tsx index 087464a56f1f..9e37a81db684 100644 --- a/packages/website/src/components/ast/ASTViewer.tsx +++ b/packages/website/src/components/ast/ASTViewer.tsx @@ -7,11 +7,12 @@ import { scrollIntoViewIfNeeded } from '../lib/scroll-into'; import styles from './ASTViewer.module.css'; import DataRender from './DataRenderer'; import { findSelectionPath } from './selectedRange'; -import type { OnHoverNodeFn } from './types'; +import type { OnClickNodeFn, OnHoverNodeFn } from './types'; export interface ASTViewerProps { readonly cursorPosition?: number; readonly onHoverNode?: OnHoverNodeFn; + readonly onClickNode?: OnClickNodeFn; readonly value: unknown; readonly filter?: ESQuery.Selector; readonly enableScrolling?: boolean; @@ -34,6 +35,7 @@ function tryToApplyFilter(value: T, filter?: ESQuery.Selector): T | T[] { function ASTViewer({ cursorPosition, onHoverNode, + onClickNode, value, filter, enableScrolling, @@ -75,6 +77,7 @@ function ASTViewer({ value={model} lastElement={true} selectedPath={selectedPath} + onClickNode={onClickNode} onHover={onHoverNode} showTokens={showTokens} /> diff --git a/packages/website/src/components/ast/DataRenderer.tsx b/packages/website/src/components/ast/DataRenderer.tsx index 8562b1f4ae38..04da575c7297 100644 --- a/packages/website/src/components/ast/DataRenderer.tsx +++ b/packages/website/src/components/ast/DataRenderer.tsx @@ -7,7 +7,7 @@ import styles from './ASTViewer.module.css'; import HiddenItem from './HiddenItem'; import PropertyName from './PropertyName'; import PropertyValue from './PropertyValue'; -import type { OnHoverNodeFn, ParentNodeType } from './types'; +import type { OnClickNodeFn, OnHoverNodeFn, ParentNodeType } from './types'; import { filterProperties, getNodeType, @@ -25,6 +25,7 @@ export interface JsonRenderProps { readonly lastElement?: boolean; readonly level: string; readonly onHover?: OnHoverNodeFn; + readonly onClickNode?: OnClickNodeFn; readonly selectedPath?: string; readonly showTokens?: boolean; } @@ -47,6 +48,7 @@ function RenderExpandableObject({ value, level, onHover, + onClickNode, selectedPath, showTokens, }: ExpandableRenderProps): React.JSX.Element { @@ -103,7 +105,10 @@ function RenderExpandableObject({ {typeName && ( { + onClickNode?.(value); + toggleExpanded(); + }} className={styles.tokenName} onHover={onHoverItem} /> @@ -121,6 +126,7 @@ function RenderExpandableObject({ lastElement={index === lastIndex} level={`${level}.${dataElement[0]}`} onHover={onHover} + onClickNode={onClickNode} selectedPath={selectedPath} nodeType={nodeType} showTokens={showTokens} diff --git a/packages/website/src/components/ast/types.ts b/packages/website/src/components/ast/types.ts index 308a2ba95251..75c240698fab 100644 --- a/packages/website/src/components/ast/types.ts +++ b/packages/website/src/components/ast/types.ts @@ -1,4 +1,5 @@ export type OnHoverNodeFn = (node?: [number, number]) => void; +export type OnClickNodeFn = (node?: unknown) => void; export type ParentNodeType = | 'esNode' diff --git a/packages/website/src/components/config/ConfigEditor.module.css b/packages/website/src/components/config/ConfigEditor.module.css index 940c7a825ee9..8fd9993fa49d 100644 --- a/packages/website/src/components/config/ConfigEditor.module.css +++ b/packages/website/src/components/config/ConfigEditor.module.css @@ -42,7 +42,7 @@ .searchResultContainer { overflow: auto; - height: 50vh; + height: 10000vh; } .searchResultName { diff --git a/packages/website/src/components/config/ConfigEslint.tsx b/packages/website/src/components/config/ConfigEslint.tsx index d32636e61b18..b62601cb1c38 100644 --- a/packages/website/src/components/config/ConfigEslint.tsx +++ b/packages/website/src/components/config/ConfigEslint.tsx @@ -1,63 +1,50 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useSystemFile } from '@site/src/components/hooks/useSystemFile'; +import type { JSONSchema4 } from 'json-schema'; +import React, { useCallback, useMemo } from 'react'; -import { ensureObject, parseJSONObject, toJson } from '../lib/json'; +import { ensureObject } from '../lib/json'; import { shallowEqual } from '../lib/shallowEqual'; -import type { ConfigModel, RuleDetails } from '../types'; -import type { ConfigOptionsField, ConfigOptionsType } from './ConfigEditor'; +import type { PlaygroundSystem } from '../linter/types'; import ConfigEditor from './ConfigEditor'; +import { schemaToConfigOptions } from './utils'; export interface ConfigEslintProps { - readonly onChange: (value: Partial) => void; - readonly ruleOptions: RuleDetails[]; - readonly config?: string; readonly className?: string; + readonly system: PlaygroundSystem; } -function ConfigEslint(props: ConfigEslintProps): React.JSX.Element { - const { config, onChange: onChangeProp, ruleOptions, className } = props; - - const [configObject, updateConfigObject] = useState>( - () => ({}), +function ConfigEslint({ + className, + system, +}: ConfigEslintProps): React.JSX.Element { + const [rawConfig, updateConfigObject] = useSystemFile(system, '/.eslintrc'); + const configObject = useMemo( + () => ensureObject(rawConfig?.rules), + [rawConfig], ); - useEffect(() => { - updateConfigObject(oldConfig => { - const newConfig = ensureObject(parseJSONObject(config).rules); - if (shallowEqual(oldConfig, newConfig)) { - return oldConfig; + const options = useMemo(() => { + const schemaContent = system.readFile('/schema/eslint.schema'); + if (schemaContent) { + const schema = JSON.parse(schemaContent) as JSONSchema4; + if (schema.type === 'object') { + const props = schema.properties?.rules?.properties; + if (props) { + return schemaToConfigOptions(props).reverse(); + } } - return newConfig; - }); - }, [config]); - - const options = useMemo((): ConfigOptionsType[] => { - const mappedRules: ConfigOptionsField[] = ruleOptions.map(item => ({ - key: item.name, - label: item.description, - type: 'boolean', - defaults: ['error', 2, 'warn', 1, ['error'], ['warn'], [2], [1]], - })); + } - return [ - { - heading: 'Rules', - fields: mappedRules.filter(item => item.key.startsWith('@typescript')), - }, - { - heading: 'Core rules', - fields: mappedRules.filter(item => !item.key.startsWith('@typescript')), - }, - ]; - }, [ruleOptions]); + return []; + }, [system]); const onChange = useCallback( (newConfig: Record) => { - const parsed = parseJSONObject(config); - parsed.rules = newConfig; - updateConfigObject(newConfig); - onChangeProp({ eslintrc: toJson(parsed) }); + if (!shallowEqual(newConfig, ensureObject(rawConfig?.rules))) { + updateConfigObject({ ...rawConfig, rules: newConfig }); + } }, - [config, onChangeProp], + [rawConfig, updateConfigObject], ); return ( diff --git a/packages/website/src/components/config/ConfigTypeScript.tsx b/packages/website/src/components/config/ConfigTypeScript.tsx index b367af6fafb7..ce59340f0473 100644 --- a/packages/website/src/components/config/ConfigTypeScript.tsx +++ b/packages/website/src/components/config/ConfigTypeScript.tsx @@ -1,73 +1,53 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useSystemFile } from '@site/src/components/hooks/useSystemFile'; +import type { JSONSchema4 } from 'json-schema'; +import React, { useCallback, useMemo } from 'react'; -import { ensureObject, parseJSONObject, toJson } from '../lib/json'; -import { getTypescriptOptions } from '../lib/jsonSchema'; +import { ensureObject } from '../lib/json'; import { shallowEqual } from '../lib/shallowEqual'; -import type { ConfigModel } from '../types'; -import type { ConfigOptionsType } from './ConfigEditor'; +import type { PlaygroundSystem } from '../linter/types'; import ConfigEditor from './ConfigEditor'; +import { schemaToConfigOptions } from './utils'; -interface ConfigTypeScriptProps { - readonly onChange: (config: Partial) => void; - readonly config?: string; +export interface ConfigTypeScriptProps { readonly className?: string; + readonly system: PlaygroundSystem; } -function ConfigTypeScript(props: ConfigTypeScriptProps): React.JSX.Element { - const { config, onChange: onChangeProp, className } = props; - - const [configObject, updateConfigObject] = useState>( - () => ({}), +function ConfigTypeScript({ + className, + system, +}: ConfigTypeScriptProps): React.JSX.Element { + const [rawConfig, updateConfigObject] = useSystemFile( + system, + '/tsconfig.json', + ); + const configObject = useMemo( + () => ensureObject(rawConfig?.compilerOptions), + [rawConfig], ); - useEffect(() => { - updateConfigObject(oldConfig => { - const newConfig = ensureObject(parseJSONObject(config).compilerOptions); - if (shallowEqual(oldConfig, newConfig)) { - return oldConfig; + const options = useMemo(() => { + const schemaContent = system.readFile('/schema/tsconfig.schema'); + if (schemaContent) { + const schema = JSON.parse(schemaContent) as JSONSchema4; + if (schema.type === 'object') { + const props = schema.properties?.compilerOptions?.properties; + if (props) { + return schemaToConfigOptions(props); + } } - return newConfig; - }); - }, [config]); + } - const options = useMemo((): ConfigOptionsType[] => { - return Object.values( - getTypescriptOptions().reduce>( - (group, item) => { - const category = item.category!.message; - group[category] ??= { - heading: category, - fields: [], - }; - if (item.type === 'boolean') { - group[category].fields.push({ - key: item.name, - type: 'boolean', - label: item.description!.message, - }); - } else if (item.type instanceof Map) { - group[category].fields.push({ - key: item.name, - type: 'string', - label: item.description!.message, - enum: ['', ...Array.from(item.type.keys())], - }); - } - return group; - }, - {}, - ), - ); - }, []); + return []; + }, [system]); const onChange = useCallback( (newConfig: Record) => { - const parsed = parseJSONObject(config); - parsed.compilerOptions = newConfig; - updateConfigObject(newConfig); - onChangeProp({ tsconfig: toJson(parsed) }); + if (!shallowEqual(newConfig, ensureObject(rawConfig?.compilerOptions))) { + updateConfigObject({ ...rawConfig, compilerOptions: newConfig }); + } }, - [config, onChangeProp], + [rawConfig, updateConfigObject], ); return ( diff --git a/packages/website/src/components/config/utils.ts b/packages/website/src/components/config/utils.ts new file mode 100644 index 000000000000..133a1955bc7a --- /dev/null +++ b/packages/website/src/components/config/utils.ts @@ -0,0 +1,52 @@ +import type { JSONSchema4 } from 'json-schema'; + +import type { ConfigOptionsField, ConfigOptionsType } from './ConfigEditor'; + +export function schemaItemToField( + name: string, + item: JSONSchema4, +): ConfigOptionsField | null { + if (item.type === 'boolean') { + return { + key: name, + type: 'boolean', + label: item.description, + }; + } else if (item.type === 'string' && item.enum) { + return { + key: name, + type: 'string', + label: item.description, + enum: ['', ...(item.enum as string[])], + }; + } else if (item.oneOf) { + return { + key: name, + type: 'boolean', + label: item.description, + defaults: ['error', 2, 'warn', 1], + }; + } + return null; +} + +export function schemaToConfigOptions( + options: Record, +): ConfigOptionsType[] { + const result = Object.entries(options).reduce< + Record + >((group, [name, item]) => { + const category = item.title!; + group[category] = group[category] ?? { + heading: category, + fields: [], + }; + const field = schemaItemToField(name, item); + if (field) { + group[category].fields.push(field); + } + return group; + }, {}); + + return Object.values(result); +} diff --git a/packages/website/src/components/editor/EditorEmbed.tsx b/packages/website/src/components/editor/EditorEmbed.tsx deleted file mode 100644 index a791e19001f2..000000000000 --- a/packages/website/src/components/editor/EditorEmbed.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -export const editorEmbedId = 'monaco-editor-embed'; - -export const EditorEmbed: React.FC = () => ( -
-); diff --git a/packages/website/src/components/editor/LoadedEditor.tsx b/packages/website/src/components/editor/LoadedEditor.tsx index 58ff7d3cd305..c0329a704343 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -1,334 +1,150 @@ import { useColorMode } from '@docusaurus/theme-common'; -import type Monaco from 'monaco-editor'; -import type React from 'react'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; - -import { useResizeObserver } from '../hooks/useResizeObserver'; -import { createCompilerOptions } from '../lib/createCompilerOptions'; -import { debounce } from '../lib/debounce'; -import { - getEslintJsonSchema, - getRuleJsonSchemaWithErrorLevel, - getTypescriptJsonSchema, -} from '../lib/jsonSchema'; -import { parseTSConfig, tryParseEslintModule } from '../lib/parseConfig'; -import type { LintCodeAction } from '../linter/utils'; -import { parseLintResults, parseMarkers } from '../linter/utils'; -import type { TabType } from '../types'; -import { createProvideCodeActions } from './createProvideCodeActions'; -import type { CommonEditorProps } from './types'; -import type { SandboxServices } from './useSandboxServices'; - -export type LoadedEditorProps = CommonEditorProps & SandboxServices; - -function applyEdit( - model: Monaco.editor.ITextModel, - editor: Monaco.editor.ICodeEditor, - edit: Monaco.editor.IIdentifiedSingleEditOperation, -): void { - if (model.isAttachedToEditor()) { - editor.executeEdits('eslint', [edit]); - } else { - model.pushEditOperations([], [edit], () => null); - } +import Editor from '@monaco-editor/react'; +import type * as Monaco from 'monaco-editor'; +import React, { useEffect, useRef, useState } from 'react'; + +import { addLibFiles } from '../linter/bridge'; +import { createLinter } from '../linter/createLinter'; +import type { WebLinterModule } from '../linter/types'; +import { defaultEditorOptions } from '../options'; +import { createModels, determineLanguage } from './actions/createModels'; +import { registerActions } from './actions/registerActions'; +import { registerDefaults } from './actions/registerDefaults'; +import { registerEvents } from './actions/registerEvents'; +import { registerLinter } from './actions/registerLinter'; +import type { LintCodeAction } from './actions/utils'; +import { isCodeFile } from './actions/utils'; +import type { LoadingEditorProps } from './LoadingEditor'; + +interface LoadedEditorProps extends LoadingEditorProps { + monaco: typeof Monaco; + utils: WebLinterModule; } -export const LoadedEditor: React.FC = ({ - code, - tsconfig, - eslintrc, - selectedRange, - fileType, - onASTChange, - onMarkersChange, - onChange, - onSelect, - sandboxInstance: { editor, monaco }, - showAST, +export default function LoadedEditor({ + activeFile, system, - sourceType, - webLinter, - activeTab, -}) => { + onValidate, + onUpdate, + onCursorChange, + monaco, + className, + utils, + selectedRange, +}: LoadedEditorProps): JSX.Element { const { colorMode } = useColorMode(); - const [_, setDecorations] = useState([]); - - const codeActions = useRef(new Map()).current; - const [tabs] = useState>(() => { - const tabsDefault = { - code: editor.getModel()!, - tsconfig: monaco.editor.createModel( - tsconfig, - 'json', - monaco.Uri.file('/tsconfig.json'), - ), - eslintrc: monaco.editor.createModel( - eslintrc, - 'json', - monaco.Uri.file('/.eslintrc'), - ), - }; - tabsDefault.code.updateOptions({ tabSize: 2, insertSpaces: true }); - tabsDefault.eslintrc.updateOptions({ tabSize: 2, insertSpaces: true }); - tabsDefault.tsconfig.updateOptions({ tabSize: 2, insertSpaces: true }); - return tabsDefault; - }); - - const updateMarkers = useCallback(() => { - const model = editor.getModel()!; - const markers = monaco.editor.getModelMarkers({ - resource: model.uri, - }); - onMarkersChange(parseMarkers(markers, codeActions, editor)); - }, [codeActions, onMarkersChange, editor, monaco.editor]); - - useEffect(() => { - webLinter.updateParserOptions(sourceType); - }, [webLinter, sourceType]); - - useEffect(() => { - const newPath = `/input${fileType}`; - if (tabs.code.uri.path !== newPath) { - const code = tabs.code.getValue(); - const newModel = monaco.editor.createModel( - code, - undefined, - monaco.Uri.file(newPath), + const editorRef = useRef(); + const [, setDecorations] = useState([]); + const [defaultValue] = useState( + () => system.readFile('/' + activeFile) ?? '', + ); + + useEffect(() => { + const model = monaco.editor.getModel(monaco.Uri.file(activeFile)); + if (model) { + setDecorations(prevDecorations => + model.deltaDecorations( + prevDecorations, + selectedRange + ? [ + { + range: monaco.Range.fromPositions( + model.getPositionAt(selectedRange[0]), + model.getPositionAt(selectedRange[1]), + ), + options: { + inlineClassName: 'myLineDecoration', + stickiness: 1, + }, + }, + ] + : [], + ), ); - newModel.updateOptions({ tabSize: 2, insertSpaces: true }); - if (tabs.code.isAttachedToEditor()) { - editor.setModel(newModel); - } - tabs.code.dispose(); - tabs.code = newModel; - system.writeFile(newPath, code); } - }, [fileType, editor, system, monaco, tabs]); + }, [selectedRange, monaco, activeFile]); useEffect(() => { - const config = createCompilerOptions( - parseTSConfig(tsconfig).compilerOptions, - ); - monaco.languages.typescript.typescriptDefaults.setCompilerOptions( - config as Monaco.languages.typescript.CompilerOptions, - ); - }, [monaco, tsconfig]); - - useEffect(() => { - if (editor.getModel()?.uri.path !== tabs[activeTab].uri.path) { - editor.setModel(tabs[activeTab]); - updateMarkers(); + if (!editorRef.current) { + return; } - }, [activeTab, editor, tabs, updateMarkers]); - - useEffect(() => { - const disposable = webLinter.onLint((uri, messages) => { - const diagnostics = parseLintResults(messages, codeActions, ruleId => - monaco.Uri.parse(webLinter.rules.get(ruleId)?.url ?? ''), - ); - monaco.editor.setModelMarkers( - monaco.editor.getModel(monaco.Uri.file(uri))!, - 'eslint', - diagnostics, - ); - updateMarkers(); - }); - return () => disposable(); - }, [webLinter, monaco, codeActions, updateMarkers]); - - useEffect(() => { - const disposable = webLinter.onParse((uri, model) => { - onASTChange(model); - }); - return () => disposable(); - }, [webLinter, onASTChange]); - - useEffect(() => { - const createRuleUri = (name: string): string => - monaco.Uri.parse(`/rules/${name.replace('@', '')}.json`).toString(); - - // configure the JSON language support with schemas and schema associations - monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ - validate: true, - enableSchemaRequest: false, - allowComments: true, - schemas: [ - ...Array.from(webLinter.rules.values()).map(rule => ({ - uri: createRuleUri(rule.name), - schema: getRuleJsonSchemaWithErrorLevel(rule.name, rule.schema), - })), - { - uri: monaco.Uri.file('eslint-schema.json').toString(), // id of the first schema - fileMatch: ['/.eslintrc'], // associate with our model - schema: getEslintJsonSchema(webLinter, createRuleUri), - }, - { - uri: monaco.Uri.file('ts-schema.json').toString(), // id of the first schema - fileMatch: ['/tsconfig.json'], // associate with our model - schema: getTypescriptJsonSchema(), - }, - ], - }); - }, [monaco, webLinter]); - - useEffect(() => { - const disposable = monaco.languages.registerCodeActionProvider( - 'typescript', - createProvideCodeActions(codeActions), - ); - return () => disposable.dispose(); - }, [codeActions, monaco]); - - useEffect(() => { - const disposable = editor.onDidPaste(() => { - if (tabs.eslintrc.isAttachedToEditor()) { - const value = tabs.eslintrc.getValue(); - const newValue = tryParseEslintModule(value); - if (newValue !== value) { - tabs.eslintrc.setValue(newValue); - } - } - }); - return () => disposable.dispose(); - }, [editor, tabs.eslintrc]); - - useEffect(() => { - const disposable = editor.onDidChangeCursorPosition( - debounce(e => { - if (tabs.code.isAttachedToEditor()) { - const position = tabs.code.getOffsetAt(e.position); - console.info('[Editor] updating cursor', position); - onSelect(position); - } - }, 150), - ); - return () => disposable.dispose(); - }, [onSelect, editor, tabs.code]); - useEffect(() => { - const disposable = editor.addAction({ - id: 'fix-eslint-problems', - label: 'Fix eslint problems', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS], - contextMenuGroupId: 'snippets', - contextMenuOrder: 1.5, - run(editor) { - const editorModel = editor.getModel(); - if (editorModel) { - const fixed = webLinter.triggerFix(editor.getValue()); - if (fixed?.fixed) { - applyEdit(editorModel, editor, { - range: editorModel.getFullModelRange(), - text: fixed.output, - }); - } + const activeUri = monaco.Uri.file(activeFile); + let model = monaco.editor.getModel(activeUri); + if (!model) { + console.log('[Editor] creating new model', activeUri.path); + let code: string | undefined = ''; + if (isCodeFile(activeUri.path)) { + const codeModel = monaco.editor + .getModels() + .find(m => isCodeFile(m.uri.path)); + if (codeModel) { + code = codeModel.getValue(); + console.log('[Editor] destroying model', codeModel.uri.path); + codeModel.dispose(); } - }, - }); - return () => disposable.dispose(); - }, [editor, monaco, webLinter]); - - useEffect(() => { - const closable = [ - system.watchFile('/tsconfig.json', filename => { - onChange({ tsconfig: system.readFile(filename) }); - }), - system.watchFile('/.eslintrc', filename => { - onChange({ eslintrc: system.readFile(filename) }); - }), - system.watchFile('/input.*', filename => { - onChange({ code: system.readFile(filename) }); - }), - ]; - - return () => { - closable.forEach(c => c.close()); - }; - }, [system, onChange]); - - useEffect(() => { - const disposable = editor.onDidChangeModelContent(() => { - const model = editor.getModel(); - if (model) { - system.writeFile(model.uri.path, model.getValue()); + system.writeFile(activeUri.path, code ?? ''); + } else { + code = system.readFile(activeUri.path); } - }); - return () => disposable.dispose(); - }, [editor, system]); - - useEffect(() => { - const disposable = monaco.editor.onDidChangeMarkers(() => { - updateMarkers(); - }); - return () => disposable.dispose(); - }, [monaco.editor, updateMarkers]); - - const resize = useMemo(() => { - return debounce(() => editor.layout(), 1); - }, [editor]); - - const container = editor.getContainerDomNode?.() ?? editor.getDomNode(); - - useResizeObserver(container, () => { - resize(); - }); - - useEffect(() => { - if (!editor.hasTextFocus() && code !== tabs.code.getValue()) { - applyEdit(tabs.code, editor, { - range: tabs.code.getFullModelRange(), - text: code, - }); - } - }, [code, editor, tabs.code]); - - useEffect(() => { - if (!editor.hasTextFocus() && tsconfig !== tabs.tsconfig.getValue()) { - applyEdit(tabs.tsconfig, editor, { - range: tabs.tsconfig.getFullModelRange(), - text: tsconfig, - }); + model = monaco.editor.createModel( + code ?? '', + determineLanguage(activeUri.path), + activeUri, + ); + model.updateOptions({ tabSize: 2, insertSpaces: true }); } - }, [editor, tabs.tsconfig, tsconfig]); - useEffect(() => { - if (!editor.hasTextFocus() && eslintrc !== tabs.eslintrc.getValue()) { - applyEdit(tabs.eslintrc, editor, { - range: tabs.eslintrc.getFullModelRange(), - text: eslintrc, - }); + if (!model.isAttachedToEditor()) { + console.log('[Editor] attaching model', activeUri.path); + editorRef.current.setModel(model); } - }, [eslintrc, editor, tabs.eslintrc]); - - useEffect(() => { - monaco.editor.setTheme(colorMode === 'dark' ? 'vs-dark' : 'vs-light'); - }, [colorMode, monaco]); - useEffect(() => { - setDecorations(prevDecorations => - tabs.code.deltaDecorations( - prevDecorations, - selectedRange && showAST - ? [ - { - range: monaco.Range.fromPositions( - tabs.code.getPositionAt(selectedRange[0]), - tabs.code.getPositionAt(selectedRange[1]), - ), - options: { - inlineClassName: 'myLineDecoration', - stickiness: 1, - }, - }, - ] - : [], - ), - ); - }, [selectedRange, monaco, showAST, tabs.code]); - - useEffect(() => { - webLinter.triggerLint(tabs.code.uri.path); - }, [webLinter, fileType, sourceType, tabs.code]); + monaco.editor.setModelLanguage(model, determineLanguage(activeUri.path)); + }, [system, monaco, editorRef, activeFile]); + + const onEditorDidMount = ( + editor: Monaco.editor.IStandaloneCodeEditor, + ): void => { + editorRef.current = editor; + window.esquery = utils.esquery; + window.system = system; + + // we want to ignore this error + void addLibFiles(system, monaco).then(() => { + const globalActions = new Map>(); + const linter = createLinter(system, utils); + registerDefaults(monaco, linter, system); + createModels(monaco, editor, system); + registerActions(monaco, editor, linter); + registerEvents( + monaco, + editor, + system, + onValidate, + onCursorChange, + globalActions, + ); + registerLinter(monaco, editor, linter, globalActions); - return null; -}; + const model = editor.getModel()!; + model.updateOptions({ tabSize: 2, insertSpaces: true }); + monaco.editor.setModelLanguage(model, determineLanguage(activeFile)); + linter.onParse((_, updateModel) => { + onUpdate(updateModel); + }); + }); + }; + + return ( + + ); +} diff --git a/packages/website/src/components/editor/LoadingEditor.tsx b/packages/website/src/components/editor/LoadingEditor.tsx index 813593ae3b87..0f8f83fe703c 100644 --- a/packages/website/src/components/editor/LoadingEditor.tsx +++ b/packages/website/src/components/editor/LoadingEditor.tsx @@ -1,22 +1,70 @@ -import React from 'react'; +import { loader } from '@monaco-editor/react'; +import type * as Monaco from 'monaco-editor'; +import React, { useEffect, useRef, useState } from 'react'; -import { LoadedEditor } from './LoadedEditor'; -import type { CommonEditorProps } from './types'; -import type { SandboxServicesProps } from './useSandboxServices'; -import { useSandboxServices } from './useSandboxServices'; +import Loader from '../layout/Loader'; +import type { + PlaygroundSystem, + UpdateModel, + WebLinterModule, +} from '../linter/types'; +import type { ErrorGroup } from '../types'; +import LoadedEditor from './LoadedEditor'; +import { loadWebLinterModule } from './loadWebLinterModule'; -export type LoadingEditorProps = CommonEditorProps & SandboxServicesProps; +export interface LoadingEditorProps { + readonly className?: string; + readonly activeFile: string; + readonly tsVersion: string; + readonly system: PlaygroundSystem; + readonly onValidate: (markers: ErrorGroup[]) => void; + readonly onUpdate: (model: UpdateModel) => void; + readonly onCursorChange: (offset: number) => void; + readonly selectedRange?: [number, number]; +} -export const LoadingEditor: React.FC = props => { - const services = useSandboxServices(props); +function LoadingEditor(props: LoadingEditorProps): JSX.Element { + const [isLoading, setLoading] = useState(true); + const monaco = useRef(); + const utils = useRef(); - if (!services) { - return null; - } + useEffect(() => { + loader.config({ + paths: { + vs: `https://typescript.azureedge.net/cdn/${props.tsVersion}/monaco/min/vs`, + }, + }); + + // This has to be executed in proper order + loader + .init() + .then((instance: typeof Monaco) => { + monaco.current = instance; + return loadWebLinterModule(); + }) + .then(instance => { + utils.current = instance; + setLoading(false); + }) + .catch(e => { + console.log('Unable to initialize editor', e); + }); + // this can't be reactive + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - if (services instanceof Error) { - return <>{services.stack}; + if (isLoading || !monaco.current || !utils.current) { + return ; } - return ; -}; + return ( + + ); +} + +export default LoadingEditor; diff --git a/packages/website/src/components/editor/actions/createModels.ts b/packages/website/src/components/editor/actions/createModels.ts new file mode 100644 index 000000000000..3d2a15cd921f --- /dev/null +++ b/packages/website/src/components/editor/actions/createModels.ts @@ -0,0 +1,40 @@ +import type * as Monaco from 'monaco-editor'; + +import type { PlaygroundSystem } from '../../linter/types'; +import { applyEdit } from './utils'; + +export function determineLanguage(file: string): string { + if (/\.[mc]?ts(x)?$/.test(file)) { + return 'typescript'; + } + if (/\.[mc]?js(x)?$/.test(file)) { + return 'javascript'; + } + if (/\.(json|eslintrc)$/.test(file)) { + return 'json'; + } + return 'plaintext'; +} + +export function createModels( + monaco: typeof Monaco, + editor: Monaco.editor.IStandaloneCodeEditor, + system: PlaygroundSystem, +): void { + system.watchFile('/*', fileName => { + if (editor.hasTextFocus()) { + return; + } + + const model = monaco.editor.getModel(monaco.Uri.file(fileName)); + if (model) { + const code = system.readFile(fileName) ?? '\n'; + if (model.getValue() !== code) { + applyEdit(model, editor, { + range: model.getFullModelRange(), + text: code, + }); + } + } + }); +} diff --git a/packages/website/src/components/editor/actions/registerActions.ts b/packages/website/src/components/editor/actions/registerActions.ts new file mode 100644 index 000000000000..7cf30f7cf89e --- /dev/null +++ b/packages/website/src/components/editor/actions/registerActions.ts @@ -0,0 +1,38 @@ +import type * as Monaco from 'monaco-editor'; + +import type { CreateLinter } from '../../linter/createLinter'; +import { applyEdit } from './utils'; + +export function registerActions( + monaco: typeof Monaco, + editor: Monaco.editor.IStandaloneCodeEditor, + linter: CreateLinter, +): Monaco.IDisposable { + const disposable = [ + editor.addAction({ + id: 'fix-eslint-problems', + label: 'Fix eslint problems', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS], + contextMenuGroupId: 'snippets', + contextMenuOrder: 1.5, + run(editor) { + const editorModel = editor.getModel(); + if (editorModel) { + const fixed = linter.triggerFix(editorModel.uri.path); + if (fixed) { + applyEdit(editorModel, editor, { + range: editorModel.getFullModelRange(), + text: fixed.output, + }); + } + } + }, + }), + ]; + + return { + dispose(): void { + disposable.forEach(d => d.dispose()); + }, + }; +} diff --git a/packages/website/src/components/editor/actions/registerDefaults.ts b/packages/website/src/components/editor/actions/registerDefaults.ts new file mode 100644 index 000000000000..ad4f628d5d8c --- /dev/null +++ b/packages/website/src/components/editor/actions/registerDefaults.ts @@ -0,0 +1,44 @@ +import type * as Monaco from 'monaco-editor'; + +import { + getEslintJsonSchema, + getRuleJsonSchemaWithErrorLevel, + getTypescriptJsonSchema, +} from '../../lib/jsonSchema'; +import type { CreateLinter } from '../../linter/createLinter'; +import type { PlaygroundSystem } from '../../linter/types'; + +export function registerDefaults( + monaco: typeof Monaco, + linter: CreateLinter, + system: PlaygroundSystem, +): void { + const createRuleUri = (name: string): string => + monaco.Uri.parse(`/rules/${name.replace('@', '')}.json`).toString(); + + const eslintSchema = getEslintJsonSchema(linter, createRuleUri); + system.writeFile('/schema/eslint.schema', JSON.stringify(eslintSchema)); + const tsconfigSchema = getTypescriptJsonSchema(); + system.writeFile('/schema/tsconfig.schema', JSON.stringify(tsconfigSchema)); + + // configure the JSON language support with schemas and schema associations + monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ + validate: true, + schemas: [ + ...Array.from(linter.rules.values()).map(rule => ({ + uri: createRuleUri(rule.name), + schema: getRuleJsonSchemaWithErrorLevel(rule.name, rule.schema), + })), + { + uri: monaco.Uri.file('/schema/eslint.schema').toString(), + fileMatch: ['.eslintrc'], + schema: eslintSchema, + }, + { + uri: monaco.Uri.file('/schema/tsconfig.schema').toString(), + fileMatch: ['tsconfig.json'], + schema: tsconfigSchema, + }, + ], + }); +} diff --git a/packages/website/src/components/editor/actions/registerEvents.ts b/packages/website/src/components/editor/actions/registerEvents.ts new file mode 100644 index 000000000000..3edefad4946f --- /dev/null +++ b/packages/website/src/components/editor/actions/registerEvents.ts @@ -0,0 +1,130 @@ +import type * as Monaco from 'monaco-editor'; + +import { debounce } from '../../lib/debounce'; +import type { PlaygroundSystem } from '../../linter/types'; +import type { ErrorGroup } from '../../types'; +import type { LintCodeAction } from './utils'; +import { + applyEdit, + createEditAction, + createURI, + isCodeFile, + normalizeMarkerCode, + normalizeMarkerGroup, + tryParseEslintModule, +} from './utils'; + +export function registerEvents( + monaco: typeof Monaco, + editor: Monaco.editor.IStandaloneCodeEditor, + system: PlaygroundSystem, + onValidate: (markers: ErrorGroup[]) => void, + onCursorChange: (offset: number) => void, + globalActions: Map>, +): Monaco.IDisposable { + const updateMarkers = (uri: Monaco.Uri): void => { + const model = monaco.editor.getModel(uri); + if (!model?.isAttachedToEditor()) { + return; + } + const markers = monaco.editor.getModelMarkers({ + resource: uri, + }); + const fileActions = globalActions.get(uri.toString()); + + const result: Record = {}; + for (const marker of markers) { + const group = normalizeMarkerGroup(marker); + const { target, value } = normalizeMarkerCode(marker); + + const fixers = + fileActions?.get(createURI(marker))?.map(item => ({ + message: item.message, + isPreferred: item.isPreferred, + fix(): void { + applyEdit(model, editor, createEditAction(monaco, model, item.fix)); + }, + })) ?? []; + + if (!result[group]) { + result[group] = { + group, + uri: target, + items: [], + }; + } + + result[group].items.push({ + message: + (marker.owner !== 'eslint' ? `${value}: ` : '') + marker.message, + location: `${marker.startLineNumber}:${marker.startColumn} - ${marker.endLineNumber}:${marker.endColumn}`, + severity: marker.severity, + fixer: fixers.find(item => item.isPreferred), + suggestions: fixers.filter(item => !item.isPreferred), + }); + } + + onValidate( + Object.values(result).sort((a, b) => a.group.localeCompare(b.group)), + ); + }; + + const disposable = [ + editor.onDidChangeModelContent(event => { + if (event.isFlush) { + return; + } + const model = editor.getModel(); + if (model) { + system.writeFile(model.uri.path, model.getValue() || '\n'); + } + }), + editor.onDidChangeModel(event => { + if (event.newModelUrl) { + updateMarkers(event.newModelUrl); + } + }), + monaco.editor.onDidChangeMarkers(event => { + const currentModelUri = editor.getModel()?.uri; + if (!currentModelUri) { + return; + } + + event.forEach(uri => { + updateMarkers(uri); + }); + }), + editor.onDidPaste(() => { + const model = editor.getModel(); + if (!model) { + return; + } + if (model.uri.path === '/.eslintrc') { + const newValue = tryParseEslintModule(model.getValue()); + if (newValue) { + applyEdit(model, editor, { + range: model.getFullModelRange(), + text: newValue, + }); + } + } + }), + editor.onDidChangeCursorPosition( + debounce(e => { + if (e.position) { + const model = editor.getModel(); + if (model && isCodeFile(model.uri.path)) { + console.info('[Editor] updating cursor', e.position); + onCursorChange(model.getOffsetAt(e.position)); + } + } + }, 150), + ), + ]; + + return { + dispose(): void { + disposable.forEach(item => item.dispose()); + }, + }; +} diff --git a/packages/website/src/components/editor/actions/registerLinter.ts b/packages/website/src/components/editor/actions/registerLinter.ts new file mode 100644 index 000000000000..e397049b74b9 --- /dev/null +++ b/packages/website/src/components/editor/actions/registerLinter.ts @@ -0,0 +1,148 @@ +import type * as Monaco from 'monaco-editor'; + +import type { CreateLinter } from '../../linter/createLinter'; +import type { LintCodeAction } from './utils'; +import { createEditOperation, createURI } from './utils'; + +export function registerLinter( + monaco: typeof Monaco, + editor: Monaco.editor.IStandaloneCodeEditor, + linter: CreateLinter, + globalActions: Map>, +): Monaco.IDisposable { + const computeMarkerCode = ( + ruleId: string | null, + ): Monaco.editor.IMarkerData['code'] => { + if (!ruleId) { + return 'FATAL'; + } + const ruleURI = linter.rules.get(ruleId); + if (!ruleURI?.url) { + return ruleId; + } + return { + value: ruleId, + target: monaco.Uri.parse(ruleURI.url), + }; + }; + + const disposable = [ + linter.onLint((fileName, messages): void => { + const uri = monaco.Uri.file(fileName); + const model = monaco.editor.getModel(uri); + + if (!model) { + return; + } + const actions = new Map(); + + const markers = messages.map(message => { + const fixes: LintCodeAction[] = []; + const marker: Monaco.editor.IMarkerData = { + code: computeMarkerCode(message.ruleId), + severity: + message.severity === 2 + ? monaco.MarkerSeverity.Error + : monaco.MarkerSeverity.Warning, + source: 'ESLint', + message: message.message, + startLineNumber: message.line ?? 1, + startColumn: message.column ?? 1, + endLineNumber: message.endLine ?? 1, + endColumn: message.endColumn ?? 2, + }; + + if (message.fix) { + fixes.push({ + message: `Fix this ${message.ruleId ?? 'unknown'} problem`, + isPreferred: true, + fix: message.fix, + }); + } + if (message.suggestions) { + for (const suggestion of message.suggestions) { + fixes.push({ + message: suggestion.desc, + code: message.ruleId, + isPreferred: false, + fix: suggestion.fix, + }); + } + } + if (fixes.length > 0) { + actions.set(createURI(marker), fixes); + } + + return marker; + }); + + globalActions.set(model.uri.toString(), actions); + + monaco.editor.setModelMarkers(model, 'eslint', markers); + }), + monaco.languages.registerCodeActionProvider( + 'typescript', + { + provideCodeActions( + model, + _range, + context, + _token, + ): Monaco.languages.ProviderResult { + const fileActions = globalActions.get(model.uri.toString()); + if (!fileActions) { + return { + actions: [], + dispose(): void { + // noop + }, + }; + } + + const actions: Monaco.languages.CodeAction[] = []; + + for (const marker of context.markers) { + const messages = fileActions.get(createURI(marker)) ?? []; + for (const message of messages) { + const editOperation = createEditOperation(model, message); + actions.push({ + title: + message.message + (message.code ? ` (${message.code})` : ''), + diagnostics: [marker], + kind: 'quickfix', + isPreferred: message.isPreferred, + edit: { + edits: [ + { + resource: model.uri, + // monaco for ts >= 4.8 + textEdit: editOperation, + // @ts-expect-error monaco for ts < 4.8 + edit: editOperation, + }, + ], + }, + }); + } + } + + return { + actions, + dispose(): void { + // noop + }, + }; + }, + }, + { + providedCodeActionKinds: ['quickfix'], + }, + ), + ]; + + return { + dispose(): void { + disposable.forEach(item => item.dispose()); + }, + }; +} diff --git a/packages/website/src/components/editor/actions/utils.ts b/packages/website/src/components/editor/actions/utils.ts new file mode 100644 index 000000000000..f9037a50b243 --- /dev/null +++ b/packages/website/src/components/editor/actions/utils.ts @@ -0,0 +1,128 @@ +import type { TSESLint } from '@typescript-eslint/utils'; +import type * as Monaco from 'monaco-editor'; + +import { toJson } from '../../lib/json'; + +export interface LintCodeAction { + message: string; + code?: string | null; + isPreferred: boolean; + fix: { + range: Readonly<[number, number]>; + text: string; + }; +} + +export function createURI(marker: Monaco.editor.IMarkerData): string { + return `[${[ + marker.startLineNumber, + marker.startColumn, + marker.startColumn, + marker.endLineNumber, + marker.endColumn, + (typeof marker.code === 'string' ? marker.code : marker.code?.value) ?? '', + ].join('|')}]`; +} + +export function createEditOperation( + model: Monaco.editor.ITextModel, + action: LintCodeAction, +): Monaco.languages.TextEdit { + const start = model.getPositionAt(action.fix.range[0]); + const end = model.getPositionAt(action.fix.range[1]); + return { + text: action.fix.text, + range: { + startLineNumber: start.lineNumber, + startColumn: start.column, + endLineNumber: end.lineNumber, + endColumn: end.column, + }, + }; +} + +export function applyEdit( + model: Monaco.editor.ITextModel, + editor: Monaco.editor.ICodeEditor, + edit: Monaco.editor.IIdentifiedSingleEditOperation, +): void { + if (model.isAttachedToEditor()) { + editor.executeEdits('eslint', [edit]); + } else { + model.pushEditOperations([], [edit], () => null); + } +} + +export function createEditAction( + monaco: typeof Monaco, + model: Monaco.editor.ITextModel, + fix: TSESLint.RuleFix, +): Monaco.editor.IIdentifiedSingleEditOperation { + return { + text: fix.text, + range: monaco.Range.fromPositions( + model.getPositionAt(fix.range[0]), + model.getPositionAt(fix.range[1]), + ), + }; +} + +export function normalizeMarkerCode(marker: Monaco.editor.IMarker): { + value: string; + target?: string; +} { + if (!marker.code) { + return { value: '' }; + } + if (typeof marker.code === 'string') { + return { value: marker.code }; + } + return { + value: marker.code.value, + target: marker.code.target.toString(), + }; +} + +export function normalizeMarkerGroup(marker: Monaco.editor.IMarker): string { + if (marker.owner === 'eslint' && marker.code) { + if (typeof marker.code === 'string') { + return marker.code; + } + return marker.code.value; + } + return marker.owner === 'typescript' + ? 'TypeScript' + : marker.owner === 'javascript' + ? 'JavaScript' + : marker.owner; +} + +const moduleRegexp = /(module\.exports\s*=)/g; + +function constrainedScopeEval(obj: string): unknown { + // eslint-disable-next-line @typescript-eslint/no-implied-eval + return new Function(` + "use strict"; + var module = { exports: {} }; + (${obj}); + return module.exports + `)(); +} + +export function tryParseEslintModule(value: string): string | null { + try { + if (moduleRegexp.test(value)) { + const newValue = toJson(constrainedScopeEval(value)); + if (newValue !== value) { + return newValue; + } + } + } catch (e) { + console.error(e); + } + return null; +} + +export function isCodeFile(fileName: string): boolean { + return /^\/input\.[cm]?(tsx?|jsx?|d\.ts)$/.test(fileName); +} diff --git a/packages/website/src/components/editor/createProvideCodeActions.ts b/packages/website/src/components/editor/createProvideCodeActions.ts deleted file mode 100644 index a5eae9f849a9..000000000000 --- a/packages/website/src/components/editor/createProvideCodeActions.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type Monaco from 'monaco-editor'; - -import type { LintCodeAction } from '../linter/utils'; -import { createEditOperation, createURI } from '../linter/utils'; - -export function createProvideCodeActions( - fixes: Map, -): Monaco.languages.CodeActionProvider { - return { - provideCodeActions( - model, - _range, - context, - ): Monaco.languages.ProviderResult { - if (context.only !== 'quickfix') { - return { - actions: [], - dispose(): void { - /* nop */ - }, - }; - } - const actions: Monaco.languages.CodeAction[] = []; - for (const marker of context.markers) { - const messages = fixes.get(createURI(marker)) ?? []; - for (const message of messages) { - const editOperation = createEditOperation(model, message); - actions.push({ - title: message.message + (message.code ? ` (${message.code})` : ''), - diagnostics: [marker], - kind: 'quickfix', - isPreferred: message.isPreferred, - edit: { - edits: [ - { - resource: model.uri, - // monaco for ts >= 4.8 - textEdit: editOperation, - // @ts-expect-error monaco for ts < 4.8 - edit: editOperation, - }, - ], - }, - }); - } - } - return { - actions, - dispose(): void { - /* nop */ - }, - }; - }, - }; -} diff --git a/packages/website/src/components/editor/loadSandbox.ts b/packages/website/src/components/editor/loadSandbox.ts deleted file mode 100644 index f7798a4093eb..000000000000 --- a/packages/website/src/components/editor/loadSandbox.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type MonacoEditor from 'monaco-editor'; - -import type * as SandboxFactory from '../../vendor/sandbox'; -import type { WebLinterModule } from '../linter/types'; - -type Monaco = typeof MonacoEditor; -type Sandbox = typeof SandboxFactory; - -export interface SandboxModel { - main: Monaco; - sandboxFactory: Sandbox; - lintUtils: WebLinterModule; -} - -function loadSandbox(tsVersion: string): Promise { - return new Promise((resolve, reject): void => { - const getLoaderScript = document.createElement('script'); - getLoaderScript.src = 'https://www.typescriptlang.org/js/vs.loader.js'; - getLoaderScript.async = true; - getLoaderScript.onload = (): void => { - // For the monaco version you can use unpkg or the TypeScript web infra CDN - // You can see the available releases for TypeScript here: - // https://typescript.azureedge.net/indexes/releases.json - window.require.config({ - paths: { - vs: `https://typescript.azureedge.net/cdn/${tsVersion}/monaco/min/vs`, - sandbox: 'https://www.typescriptlang.org/js/sandbox', - linter: '/sandbox', - }, - // This is something you need for monaco to work - ignoreDuplicateModules: ['vs/editor/editor.main'], - }); - - // Grab a copy of monaco, TypeScript and the sandbox - window.require<[Monaco, Sandbox, WebLinterModule]>( - ['vs/editor/editor.main', 'sandbox/index', 'linter/index'], - (main, sandboxFactory, lintUtils) => { - resolve({ main, sandboxFactory, lintUtils }); - }, - () => { - reject( - new Error('Could not get all the dependencies of sandbox set up!'), - ); - }, - ); - }; - document.body.appendChild(getLoaderScript); - }); -} - -let instance: Promise | undefined; - -export const sandboxSingleton = (version: string): Promise => { - if (instance) { - return instance; - } - return (instance = loadSandbox(version)); -}; diff --git a/packages/website/src/components/editor/loadWebLinterModule.ts b/packages/website/src/components/editor/loadWebLinterModule.ts new file mode 100644 index 000000000000..f7b2dff7884d --- /dev/null +++ b/packages/website/src/components/editor/loadWebLinterModule.ts @@ -0,0 +1,12 @@ +import type { WebLinterModule } from '../linter/types'; + +export function loadWebLinterModule(): Promise { + return new Promise(resolve => { + window.require<[WebLinterModule]>( + [document.location.origin + '/sandbox/index.js'], + webLinterModules => { + resolve(webLinterModules); + }, + ); + }); +} diff --git a/packages/website/src/components/editor/types.ts b/packages/website/src/components/editor/types.ts deleted file mode 100644 index e8933ce19f42..000000000000 --- a/packages/website/src/components/editor/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { UpdateModel } from '../linter/types'; -import type { ConfigModel, ErrorGroup, SelectedRange, TabType } from '../types'; - -export interface CommonEditorProps extends ConfigModel { - readonly activeTab: TabType; - readonly selectedRange?: SelectedRange; - readonly onChange: (cfg: Partial) => void; - readonly onASTChange: (value: undefined | UpdateModel) => void; - readonly onMarkersChange: (value: ErrorGroup[]) => void; - readonly onSelect: (position?: number) => void; -} diff --git a/packages/website/src/components/editor/useSandboxServices.ts b/packages/website/src/components/editor/useSandboxServices.ts deleted file mode 100644 index 997cc1cc6058..000000000000 --- a/packages/website/src/components/editor/useSandboxServices.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { useColorMode } from '@docusaurus/theme-common'; -import type * as Monaco from 'monaco-editor'; -import { useEffect, useState } from 'react'; -import semverSatisfies from 'semver/functions/satisfies'; - -import rootPackageJson from '../../../../../package.json'; -import type { createTypeScriptSandbox } from '../../vendor/sandbox'; -import { createCompilerOptions } from '../lib/createCompilerOptions'; -import { createFileSystem } from '../linter/bridge'; -import { type CreateLinter, createLinter } from '../linter/createLinter'; -import type { PlaygroundSystem } from '../linter/types'; -import type { RuleDetails } from '../types'; -import { editorEmbedId } from './EditorEmbed'; -import { sandboxSingleton } from './loadSandbox'; -import type { CommonEditorProps } from './types'; - -export interface SandboxServicesProps { - readonly onLoaded: ( - ruleDetails: RuleDetails[], - tsVersions: readonly string[], - ) => void; - readonly ts: string; -} - -export type SandboxInstance = ReturnType; - -export interface SandboxServices { - sandboxInstance: SandboxInstance; - system: PlaygroundSystem; - webLinter: CreateLinter; -} - -export const useSandboxServices = ( - props: CommonEditorProps & SandboxServicesProps, -): Error | SandboxServices | undefined => { - const { onLoaded } = props; - const [services, setServices] = useState(); - const { colorMode } = useColorMode(); - - useEffect(() => { - let sandboxInstance: SandboxInstance | undefined; - - sandboxSingleton(props.ts) - .then(async ({ main, sandboxFactory, lintUtils }) => { - const compilerOptions = createCompilerOptions(); - - sandboxInstance = sandboxFactory.createTypeScriptSandbox( - { - text: props.code, - monacoSettings: { - minimap: { enabled: false }, - fontSize: 13, - wordWrap: 'off', - scrollBeyondLastLine: false, - smoothScrolling: true, - autoIndent: 'full', - formatOnPaste: true, - formatOnType: true, - wrappingIndent: 'same', - hover: { above: false }, - }, - acquireTypes: false, - compilerOptions: - compilerOptions as Monaco.languages.typescript.CompilerOptions, - domID: editorEmbedId, - }, - main, - window.ts, - ); - sandboxInstance.monaco.editor.setTheme( - colorMode === 'dark' ? 'vs-dark' : 'vs-light', - ); - - const system = createFileSystem(props, sandboxInstance.tsvfs); - - const worker = await sandboxInstance.getWorkerProcess(); - if (worker.getLibFiles) { - const libs = await worker.getLibFiles(); - for (const [key, value] of Object.entries(libs)) { - system.writeFile('/' + key, value); - } - } - - window.system = system; - window.esquery = lintUtils.esquery; - - const webLinter = createLinter( - system, - lintUtils, - sandboxInstance.tsvfs, - ); - - onLoaded( - Array.from(webLinter.rules.values()), - Array.from( - new Set([...sandboxInstance.supportedVersions, window.ts.version]), - ) - .filter(item => - semverSatisfies(item, rootPackageJson.devDependencies.typescript), - ) - .sort((a, b) => b.localeCompare(a)), - ); - - setServices({ - system, - webLinter, - sandboxInstance, - }); - }) - .catch(setServices); - - return (): void => { - if (!sandboxInstance) { - return; - } - - const editorModel = sandboxInstance.editor.getModel()!; - sandboxInstance.monaco.editor.setModelMarkers( - editorModel, - sandboxInstance.editor.getId(), - [], - ); - sandboxInstance.editor.dispose(); - editorModel.dispose(); - const models = sandboxInstance.monaco.editor.getModels(); - for (const model of models) { - model.dispose(); - } - }; - // colorMode and jsx can't be reactive here because we don't want to force a recreation - // updating of colorMode and jsx is handled in LoadedEditor - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return services; -}; diff --git a/packages/website/src/components/hooks/useSystemFile.ts b/packages/website/src/components/hooks/useSystemFile.ts new file mode 100644 index 000000000000..29fc06f9b90d --- /dev/null +++ b/packages/website/src/components/hooks/useSystemFile.ts @@ -0,0 +1,37 @@ +import { useCallback, useEffect, useState } from 'react'; + +import { parseJSONObject, toJson } from '../lib/json'; +import type { PlaygroundSystem } from '../linter/types'; + +function readJsonFile(system: PlaygroundSystem, fileName: string): T { + const tsconfig = system.readFile(fileName); + return parseJSONObject(tsconfig) as T; +} + +export function useSystemFile>( + system: PlaygroundSystem, + fileName: string, +): [T, (value: T) => void] { + const [json, setJson] = useState(() => readJsonFile(system, fileName)); + + useEffect(() => { + const watcher = system.watchFile(fileName, fileName => { + try { + setJson(readJsonFile(system, fileName)); + } catch (e) { + // suppress errors + } + }); + return () => watcher.close(); + }, [system, fileName]); + + const updateJson = useCallback( + (value: T) => { + setJson(value); + system.writeFile(fileName, toJson(value)); + }, + [system, fileName], + ); + + return [json, updateJson]; +} diff --git a/packages/website/src/components/inputs/Tooltip.module.css b/packages/website/src/components/inputs/Tooltip.module.css index 74d247e544c4..ec6d1140463f 100644 --- a/packages/website/src/components/inputs/Tooltip.module.css +++ b/packages/website/src/components/inputs/Tooltip.module.css @@ -27,7 +27,7 @@ word-wrap: break-word; z-index: 10; min-width: 6.25rem; - max-width: 25rem; + max-width: 30rem; visibility: hidden; } diff --git a/packages/website/src/components/lib/createEventsBinder.ts b/packages/website/src/components/lib/createEventsBinder.ts index 6b5bfaecbee1..50ddba6c1d74 100644 --- a/packages/website/src/components/lib/createEventsBinder.ts +++ b/packages/website/src/components/lib/createEventsBinder.ts @@ -1,7 +1,7 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function createEventsBinder void>(): { trigger: (...args: Parameters) => void; - register: (cb: T) => () => void; + register: (cb: T) => { dispose(): void }; } { const events = new Set(); @@ -9,10 +9,12 @@ export function createEventsBinder void>(): { trigger(...args: Parameters): void { events.forEach(cb => cb(...args)); }, - register(cb: T): () => void { + register(cb: T): { dispose(): void } { events.add(cb); - return (): void => { - events.delete(cb); + return { + dispose(): void { + events.delete(cb); + }, }; }, }; diff --git a/packages/website/src/components/lib/markdown.ts b/packages/website/src/components/lib/markdown.ts index 682378c5a5a6..0801ea9f7af7 100644 --- a/packages/website/src/components/lib/markdown.ts +++ b/packages/website/src/components/lib/markdown.ts @@ -53,7 +53,7 @@ export function createMarkdown(state: ConfigModel): string { */ export function createMarkdownParams(state: ConfigModel): string { const { rules } = parseESLintRC(state.eslintrc); - const ruleKeys = Object.keys(rules); + const ruleKeys = Object.keys(rules ?? {}); const onlyRuleName = ruleKeys.length === 1 diff --git a/packages/website/src/components/lib/shallowEqual.ts b/packages/website/src/components/lib/shallowEqual.ts index fc6909430cf8..54a8a0aa860d 100644 --- a/packages/website/src/components/lib/shallowEqual.ts +++ b/packages/website/src/components/lib/shallowEqual.ts @@ -8,13 +8,16 @@ export function shallowEqual( if (object1 === object2) { return true; } + if (!object2 || !object1) { + return false; + } const keys1 = Object.keys(object1 ?? {}); const keys2 = Object.keys(object2 ?? {}); if (keys1.length !== keys2.length) { return false; } for (const key of keys1) { - if (object1![key] !== object2![key]) { + if (object1[key] !== object2[key]) { return false; } } diff --git a/packages/website/src/components/linter/bridge.ts b/packages/website/src/components/linter/bridge.ts index 4a4d52e44637..c0c618cbaeac 100644 --- a/packages/website/src/components/linter/bridge.ts +++ b/packages/website/src/components/linter/bridge.ts @@ -1,14 +1,28 @@ -import type * as tsvfs from '@site/src/vendor/typescript-vfs'; +import { createSystem } from '@typescript/vfs'; +import type * as Monaco from 'monaco-editor'; import type * as ts from 'typescript'; import { debounce } from '../lib/debounce'; +import type { PlaygroundSystem } from '../linter/types'; import type { ConfigModel } from '../types'; -import type { PlaygroundSystem } from './types'; -export function createFileSystem( - config: Pick, - vfs: typeof tsvfs, -): PlaygroundSystem { +export async function addLibFiles( + system: PlaygroundSystem, + monaco: typeof Monaco, +): Promise { + const worker = await monaco.languages.typescript.getTypeScriptWorker(); + const workerInstance = await worker(); + if (workerInstance.getLibFiles) { + const libs = await workerInstance.getLibFiles(); + if (libs) { + for (const [name, content] of Object.entries(libs)) { + system.writeFile('/' + name, content); + } + } + } +} + +export function createFileSystem(config: ConfigModel): PlaygroundSystem { const files = new Map(); files.set(`/.eslintrc`, config.eslintrc); files.set(`/tsconfig.json`, config.tsconfig); @@ -16,7 +30,7 @@ export function createFileSystem( const fileWatcherCallbacks = new Map>(); - const system = vfs.createSystem(files) as PlaygroundSystem; + const system = createSystem(files) as PlaygroundSystem; system.watchFile = ( path, diff --git a/packages/website/src/components/linter/createLinter.ts b/packages/website/src/components/linter/createLinter.ts index 2f0c17253297..7b38479cfaad 100644 --- a/packages/website/src/components/linter/createLinter.ts +++ b/packages/website/src/components/linter/createLinter.ts @@ -1,4 +1,3 @@ -import type * as tsvfs from '@site/src/vendor/typescript-vfs'; import type { JSONSchema, TSESLint, TSESTree } from '@typescript-eslint/utils'; import type * as ts from 'typescript'; @@ -27,15 +26,14 @@ export interface CreateLinter { configs: string[]; triggerFix(filename: string): TSESLint.Linter.FixReport | undefined; triggerLint(filename: string): void; - onLint(cb: LinterOnLint): () => void; - onParse(cb: LinterOnParse): () => void; + onLint(cb: LinterOnLint): { dispose(): void }; + onParse(cb: LinterOnParse): { dispose(): void }; updateParserOptions(sourceType?: TSESLint.SourceType): void; } export function createLinter( system: PlaygroundSystem, webLinterModule: WebLinterModule, - vfs: typeof tsvfs, ): CreateLinter { const rules: CreateLinter['rules'] = new Map(); const configs = new Map(Object.entries(webLinterModule.configs)); @@ -54,7 +52,6 @@ export function createLinter( onParse.trigger(filename, model); }, webLinterModule, - vfs, ); linter.defineParser(PARSER_NAME, parser); diff --git a/packages/website/src/components/linter/createParser.ts b/packages/website/src/components/linter/createParser.ts index 1c0d9258a8f2..35a3f33a6813 100644 --- a/packages/website/src/components/linter/createParser.ts +++ b/packages/website/src/components/linter/createParser.ts @@ -1,4 +1,5 @@ -import type * as tsvfs from '@site/src/vendor/typescript-vfs'; +import type { VirtualTypeScriptEnvironment } from '@typescript/vfs'; +import { createVirtualTypeScriptEnvironment } from '@typescript/vfs'; import type { ParserOptions } from '@typescript-eslint/types'; import type { TSESLint } from '@typescript-eslint/utils'; import type * as ts from 'typescript'; @@ -16,7 +17,6 @@ export function createParser( compilerOptions: ts.CompilerOptions, onUpdate: (filename: string, model: UpdateModel) => void, utils: WebLinterModule, - vfs: typeof tsvfs, ): TSESLint.Linter.ParserModule & { updateConfig: (compilerOptions: ts.CompilerOptions) => void; } { @@ -24,8 +24,8 @@ export function createParser( const createEnv = ( compilerOptions: ts.CompilerOptions, - ): tsvfs.VirtualTypeScriptEnvironment => { - return vfs.createVirtualTypeScriptEnvironment( + ): VirtualTypeScriptEnvironment => { + return createVirtualTypeScriptEnvironment( system, Array.from(registeredFiles), window.ts, diff --git a/packages/website/src/components/linter/utils.ts b/packages/website/src/components/linter/utils.ts deleted file mode 100644 index 2f6f59f9c147..000000000000 --- a/packages/website/src/components/linter/utils.ts +++ /dev/null @@ -1,179 +0,0 @@ -import type { TSESLint } from '@typescript-eslint/utils'; -import type Monaco from 'monaco-editor'; - -import type { ErrorGroup } from '../types'; - -export interface LintCodeAction { - message: string; - code?: string | null; - isPreferred: boolean; - fix: { - range: Readonly<[number, number]>; - text: string; - }; -} - -export function ensurePositiveInt( - value: number | undefined, - defaultValue: number, -): number { - return Math.max(1, (value ?? defaultValue) | 0); -} - -export function createURI(marker: Monaco.editor.IMarkerData): string { - return `[${[ - marker.startLineNumber, - marker.startColumn, - marker.startColumn, - marker.endLineNumber, - marker.endColumn, - (typeof marker.code === 'string' ? marker.code : marker.code?.value) ?? '', - ].join('|')}]`; -} - -export function createEditOperation( - model: Monaco.editor.ITextModel, - action: LintCodeAction, -): { range: Monaco.IRange; text: string } { - const start = model.getPositionAt(action.fix.range[0]); - const end = model.getPositionAt(action.fix.range[1]); - return { - text: action.fix.text, - range: { - startLineNumber: start.lineNumber, - startColumn: start.column, - endLineNumber: end.lineNumber, - endColumn: end.column, - }, - }; -} - -function normalizeCode(code: Monaco.editor.IMarker['code']): { - value: string; - target?: string; -} { - if (!code) { - return { value: '' }; - } - if (typeof code === 'string') { - return { value: code }; - } - return { - value: code.value, - target: code.target.toString(), - }; -} - -export function parseMarkers( - markers: Monaco.editor.IMarker[], - fixes: Map, - editor: Monaco.editor.IStandaloneCodeEditor, -): ErrorGroup[] { - const result: Record = {}; - for (const marker of markers) { - const code = normalizeCode(marker.code); - const uri = createURI(marker); - - const fixers = - fixes.get(uri)?.map(item => ({ - message: item.message, - isPreferred: item.isPreferred, - fix(): void { - editor.executeEdits('eslint', [ - createEditOperation(editor.getModel()!, item), - ]); - }, - })) ?? []; - - const group = - marker.owner === 'eslint' - ? code.value - : marker.owner === 'typescript' - ? 'TypeScript' - : marker.owner; - - if (!result[group]) { - result[group] = { - group: group, - uri: code.target, - items: [], - }; - } - - result[group].items.push({ - message: - (marker.owner !== 'eslint' && marker.owner !== 'json' && code.value - ? `${code.value}: ` - : '') + marker.message, - location: `${marker.startLineNumber}:${marker.startColumn} - ${marker.endLineNumber}:${marker.endColumn}`, - severity: marker.severity, - fixer: fixers.find(item => item.isPreferred), - suggestions: fixers.filter(item => !item.isPreferred), - }); - } - - return Object.values(result).sort((a, b) => a.group.localeCompare(b.group)); -} - -export function parseLintResults( - messages: TSESLint.Linter.LintMessage[], - codeActions: Map, - ruleUri: (ruleId: string) => Monaco.Uri, -): Monaco.editor.IMarkerData[] { - const markers: Monaco.editor.IMarkerData[] = []; - - codeActions.clear(); - - for (const message of messages) { - const startLineNumber = ensurePositiveInt(message.line, 1); - const startColumn = ensurePositiveInt(message.column, 1); - const endLineNumber = ensurePositiveInt(message.endLine, startLineNumber); - const endColumn = ensurePositiveInt(message.endColumn, startColumn + 1); - - const marker: Monaco.editor.IMarkerData = { - code: message.ruleId - ? { - value: message.ruleId, - target: ruleUri(message.ruleId), - } - : 'Internal error', - severity: - message.severity === 2 - ? 8 // MarkerSeverity.Error - : 4, // MarkerSeverity.Warning - source: 'ESLint', - message: message.message, - startLineNumber, - startColumn, - endLineNumber, - endColumn, - }; - const markerUri = createURI(marker); - - const fixes: LintCodeAction[] = []; - if (message.fix) { - fixes.push({ - message: `Fix this ${message.ruleId ?? 'unknown'} problem`, - fix: message.fix, - isPreferred: true, - }); - } - if (message.suggestions) { - for (const suggestion of message.suggestions) { - fixes.push({ - message: suggestion.desc, - code: message.ruleId, - fix: suggestion.fix, - isPreferred: false, - }); - } - } - if (fixes.length > 0) { - codeActions.set(markerUri, fixes); - } - - markers.push(marker); - } - - return markers; -} diff --git a/packages/website/src/components/options.ts b/packages/website/src/components/options.ts index f52e7784bc37..040b1898da29 100644 --- a/packages/website/src/components/options.ts +++ b/packages/website/src/components/options.ts @@ -1,4 +1,7 @@ +import type * as Monaco from 'monaco-editor'; + import { toJson } from './lib/json'; +import versions from './packageVersions.json'; import type { ConfigFileType, ConfigModel, ConfigShowAst } from './types'; export const detailTabs: { value: ConfigShowAst; label: string }[] = [ @@ -45,3 +48,25 @@ export const defaultConfig: ConfigModel = { scroll: true, showTokens: false, }; + +export const tsVersions: string[] = [...versions.typescript]; + +if (!tsVersions.includes(process.env.TS_VERSION!)) { + tsVersions.unshift(process.env.TS_VERSION!); +} + +export const defaultEditorOptions: Monaco.editor.IStandaloneEditorConstructionOptions = + { + minimap: { + enabled: false, + }, + fontSize: 13, + wordWrap: 'off', + scrollBeyondLastLine: false, + smoothScrolling: true, + autoIndent: 'full', + formatOnPaste: true, + formatOnType: true, + wrappingIndent: 'same', + hover: { above: false }, + }; diff --git a/packages/website/src/components/packageVersions.json b/packages/website/src/components/packageVersions.json new file mode 100644 index 000000000000..24d762d8631e --- /dev/null +++ b/packages/website/src/components/packageVersions.json @@ -0,0 +1,12 @@ +{ + "typescript": [ + "5.0.4", + "4.9.5", + "4.8.4", + "4.7.4", + "4.6.4", + "4.5.5", + "4.4.4", + "4.3.5" + ] +} diff --git a/packages/website/src/components/typeDetails/TypeInfo.tsx b/packages/website/src/components/typeDetails/TypeInfo.tsx index 87084650d124..a7c0a85e3582 100644 --- a/packages/website/src/components/typeDetails/TypeInfo.tsx +++ b/packages/website/src/components/typeDetails/TypeInfo.tsx @@ -1,14 +1,16 @@ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import type * as ts from 'typescript'; import ASTViewer from '../ast/ASTViewer'; import astStyles from '../ast/ASTViewer.module.css'; import type { OnHoverNodeFn } from '../ast/types'; +import { isRecord, isTSNode } from '../ast/utils'; export interface TypeInfoProps { readonly value: ts.Node; readonly typeChecker?: ts.TypeChecker; readonly onHoverNode?: OnHoverNodeFn; + readonly onSelect: (value: ts.Node) => void; } interface InfoModel { @@ -65,6 +67,7 @@ export function TypeInfo({ value, typeChecker, onHoverNode, + onSelect, }: TypeInfoProps): React.JSX.Element { const computed = useMemo(() => { if (!typeChecker || !value) { @@ -99,6 +102,16 @@ export function TypeInfo({ return info; }, [value, typeChecker]); + const onSelectNode = useCallback( + (selection: unknown) => { + if (isRecord(selection) && isTSNode(selection) && value !== selection) { + onSelect(selection); + onHoverNode?.(undefined); + } + }, + [onSelect, onHoverNode, value], + ); + if (!typeChecker || !computed) { return
TypeChecker not available
; } @@ -107,7 +120,11 @@ export function TypeInfo({
<>

Node

- + -
- -
- {selectedNode && ( -
- + +
+
+
+ + {selectedNode && ( + +
+ +
+
)} - + ); } diff --git a/packages/website/src/components/types.ts b/packages/website/src/components/types.ts index 4b99af65c887..43a167207a9c 100644 --- a/packages/website/src/components/types.ts +++ b/packages/website/src/components/types.ts @@ -47,7 +47,7 @@ export interface ErrorGroup { items: ErrorItem[]; } -export type EslintRC = Record & { rules: RulesRecord }; +export type EslintRC = TSESLint.Linter.Config; export type TSConfig = Record & { compilerOptions: CompilerFlags; }; diff --git a/packages/website/src/vendor/sandbox.d.ts b/packages/website/src/vendor/sandbox.d.ts deleted file mode 100644 index 9a43757ba3db..000000000000 --- a/packages/website/src/vendor/sandbox.d.ts +++ /dev/null @@ -1,314 +0,0 @@ -/********************************************** - * DO NOT MODIFY THIS FILE MANUALLY * - * * - * THIS FILE HAS BEEN FETCHED FROM THE * - * TYPESCRIPT PLAYGROUND SOURCE CODE. * - * * - * YOU CAN REGENERATE THESE FILES USING * - * yarn generate-website-dts * - **********************************************/ - -import type * as ts from 'typescript'; -import type * as MonacoEditor from 'monaco-editor'; -import type TypeScriptWorker = MonacoEditor.languages.typescript.TypeScriptWorker; -import type lzstring from 'lz-string'; -import type * as tsvfs from './typescript-vfs'; -type CompilerOptions = MonacoEditor.languages.typescript.CompilerOptions; -type Monaco = typeof MonacoEditor; -/** - * These are settings for the playground which are the equivalent to props in React - * any changes to it should require a new setup of the playground - */ -export type SandboxConfig = { - /** The default source code for the playground */ - text: string; - /** @deprecated */ - useJavaScript?: boolean; - /** The default file for the playground */ - filetype: 'js' | 'ts' | 'd.ts'; - /** Compiler options which are automatically just forwarded on */ - compilerOptions: CompilerOptions; - /** Optional monaco settings overrides */ - monacoSettings?: MonacoEditor.editor.IEditorOptions; - /** Acquire types via type acquisition */ - acquireTypes: boolean; - /** Support twoslash compiler options */ - supportTwoslashCompilerOptions: boolean; - /** Get the text via query params and local storage, useful when the editor is the main experience */ - suppressAutomaticallyGettingDefaultText?: true; - /** Suppress setting compiler options from the compiler flags from query params */ - suppressAutomaticallyGettingCompilerFlags?: true; - /** Optional path to TypeScript worker wrapper class script, see https://github.com/microsoft/monaco-typescript/pull/65 */ - customTypeScriptWorkerPath?: string; - /** Logging system */ - logger: { - log: (...args: any[]) => void; - error: (...args: any[]) => void; - groupCollapsed: (...args: any[]) => void; - groupEnd: (...args: any[]) => void; - }; -} & ( - | { - domID: string; - } - | { - elementToAppend: HTMLElement; - } -); -/** The default settings which we apply a partial over */ -export declare function defaultPlaygroundSettings(): { - /** The default source code for the playground */ - text: string; - /** @deprecated */ - useJavaScript?: boolean | undefined; - /** The default file for the playground */ - filetype: 'js' | 'ts' | 'd.ts'; - /** Compiler options which are automatically just forwarded on */ - compilerOptions: MonacoEditor.languages.typescript.CompilerOptions; - /** Optional monaco settings overrides */ - monacoSettings?: MonacoEditor.editor.IEditorOptions | undefined; - /** Acquire types via type acquisition */ - acquireTypes: boolean; - /** Support twoslash compiler options */ - supportTwoslashCompilerOptions: boolean; - /** Get the text via query params and local storage, useful when the editor is the main experience */ - suppressAutomaticallyGettingDefaultText?: true | undefined; - /** Suppress setting compiler options from the compiler flags from query params */ - suppressAutomaticallyGettingCompilerFlags?: true | undefined; - /** Optional path to TypeScript worker wrapper class script, see https://github.com/microsoft/monaco-typescript/pull/65 */ - customTypeScriptWorkerPath?: string | undefined; - /** Logging system */ - logger: { - log: (...args: any[]) => void; - error: (...args: any[]) => void; - groupCollapsed: (...args: any[]) => void; - groupEnd: (...args: any[]) => void; - }; -} & { - domID: string; -}; -/** Creates a sandbox editor, and returns a set of useful functions and the editor */ -export declare const createTypeScriptSandbox: ( - partialConfig: Partial, - monaco: Monaco, - ts: typeof ts, -) => { - /** The same config you passed in */ - config: { - text: string; - useJavaScript?: boolean | undefined; - filetype: 'js' | 'ts' | 'd.ts'; - compilerOptions: CompilerOptions; - monacoSettings?: MonacoEditor.editor.IEditorOptions | undefined; - acquireTypes: boolean; - supportTwoslashCompilerOptions: boolean; - suppressAutomaticallyGettingDefaultText?: true | undefined; - suppressAutomaticallyGettingCompilerFlags?: true | undefined; - customTypeScriptWorkerPath?: string | undefined; - logger: { - log: (...args: any[]) => void; - error: (...args: any[]) => void; - groupCollapsed: (...args: any[]) => void; - groupEnd: (...args: any[]) => void; - }; - domID: string; - }; - /** A list of TypeScript versions you can use with the TypeScript sandbox */ - supportedVersions: readonly [ - '5.2.1-rc', - '5.2.0-beta', - '5.1.6', - '5.0.4', - '4.9.5', - '4.8.4', - '4.7.4', - '4.6.4', - '4.5.5', - '4.4.4', - '4.3.5', - '4.2.3', - '4.1.5', - '4.0.5', - '3.9.7', - '3.8.3', - '3.7.5', - '3.6.3', - '3.5.1', - '3.3.3', - '3.1.6', - '3.0.1', - '2.8.1', - '2.7.2', - '2.4.1', - ]; - /** The monaco editor instance */ - editor: MonacoEditor.editor.IStandaloneCodeEditor; - /** Either "typescript" or "javascript" depending on your config */ - language: string; - /** The outer monaco module, the result of require("monaco-editor") */ - monaco: typeof MonacoEditor; - /** Gets a monaco-typescript worker, this will give you access to a language server. Note: prefer this for language server work because it happens on a webworker . */ - getWorkerProcess: () => Promise; - /** A copy of require("@typescript/vfs") this can be used to quickly set up an in-memory compiler runs for ASTs, or to get complex language server results (anything above has to be serialized when passed)*/ - tsvfs: typeof tsvfs; - /** Get all the different emitted files after TypeScript is run */ - getEmitResult: () => Promise; - /** Gets just the JavaScript for your sandbox, will transpile if in TS only */ - getRunnableJS: () => Promise; - /** Gets the DTS output of the main code in the editor */ - getDTSForCode: () => Promise; - /** The monaco-editor dom node, used for showing/hiding the editor */ - getDomNode: () => HTMLElement; - /** The model is an object which monaco uses to keep track of text in the editor. Use this to directly modify the text in the editor */ - getModel: () => MonacoEditor.editor.ITextModel; - /** Gets the text of the main model, which is the text in the editor */ - getText: () => string; - /** Shortcut for setting the model's text content which would update the editor */ - setText: (text: string) => void; - /** Gets the AST of the current text in monaco - uses `createTSProgram`, so the performance caveat applies there too */ - getAST: () => Promise; - /** The module you get from require("typescript") */ - ts: typeof ts; - /** Create a new Program, a TypeScript data model which represents the entire project. As well as some of the - * primitive objects you would normally need to do work with the files. - * - * The first time this is called it has to download all the DTS files which is needed for an exact compiler run. Which - * at max is about 1.5MB - after that subsequent downloads of dts lib files come from localStorage. - * - * Try to use this sparingly as it can be computationally expensive, at the minimum you should be using the debounced setup. - * - * Accepts an optional fsMap which you can use to add any files, or overwrite the default file. - * - * TODO: It would be good to create an easy way to have a single program instance which is updated for you - * when the monaco model changes. - */ - setupTSVFS: (fsMapAdditions?: Map) => Promise<{ - program: ts.Program; - system: ts.System; - host: { - compilerHost: ts.CompilerHost; - updateFile: (sourceFile: ts.SourceFile) => boolean; - }; - fsMap: Map; - }>; - /** Uses the above call setupTSVFS, but only returns the program */ - createTSProgram: () => Promise; - /** The Sandbox's default compiler options */ - compilerDefaults: { - [x: string]: MonacoEditor.languages.typescript.CompilerOptionsValue; - allowJs?: boolean | undefined; - allowSyntheticDefaultImports?: boolean | undefined; - allowUmdGlobalAccess?: boolean | undefined; - allowUnreachableCode?: boolean | undefined; - allowUnusedLabels?: boolean | undefined; - alwaysStrict?: boolean | undefined; - baseUrl?: string | undefined; - charset?: string | undefined; - checkJs?: boolean | undefined; - declaration?: boolean | undefined; - declarationMap?: boolean | undefined; - emitDeclarationOnly?: boolean | undefined; - declarationDir?: string | undefined; - disableSizeLimit?: boolean | undefined; - disableSourceOfProjectReferenceRedirect?: boolean | undefined; - downlevelIteration?: boolean | undefined; - emitBOM?: boolean | undefined; - emitDecoratorMetadata?: boolean | undefined; - experimentalDecorators?: boolean | undefined; - forceConsistentCasingInFileNames?: boolean | undefined; - importHelpers?: boolean | undefined; - inlineSourceMap?: boolean | undefined; - inlineSources?: boolean | undefined; - isolatedModules?: boolean | undefined; - jsx?: MonacoEditor.languages.typescript.JsxEmit | undefined; - keyofStringsOnly?: boolean | undefined; - lib?: string[] | undefined; - locale?: string | undefined; - mapRoot?: string | undefined; - maxNodeModuleJsDepth?: number | undefined; - module?: MonacoEditor.languages.typescript.ModuleKind | undefined; - moduleResolution?: - | MonacoEditor.languages.typescript.ModuleResolutionKind - | undefined; - newLine?: MonacoEditor.languages.typescript.NewLineKind | undefined; - noEmit?: boolean | undefined; - noEmitHelpers?: boolean | undefined; - noEmitOnError?: boolean | undefined; - noErrorTruncation?: boolean | undefined; - noFallthroughCasesInSwitch?: boolean | undefined; - noImplicitAny?: boolean | undefined; - noImplicitReturns?: boolean | undefined; - noImplicitThis?: boolean | undefined; - noStrictGenericChecks?: boolean | undefined; - noUnusedLocals?: boolean | undefined; - noUnusedParameters?: boolean | undefined; - noImplicitUseStrict?: boolean | undefined; - noLib?: boolean | undefined; - noResolve?: boolean | undefined; - out?: string | undefined; - outDir?: string | undefined; - outFile?: string | undefined; - paths?: MonacoEditor.languages.typescript.MapLike | undefined; - preserveConstEnums?: boolean | undefined; - preserveSymlinks?: boolean | undefined; - project?: string | undefined; - reactNamespace?: string | undefined; - jsxFactory?: string | undefined; - composite?: boolean | undefined; - removeComments?: boolean | undefined; - rootDir?: string | undefined; - rootDirs?: string[] | undefined; - skipLibCheck?: boolean | undefined; - skipDefaultLibCheck?: boolean | undefined; - sourceMap?: boolean | undefined; - sourceRoot?: string | undefined; - strict?: boolean | undefined; - strictFunctionTypes?: boolean | undefined; - strictBindCallApply?: boolean | undefined; - strictNullChecks?: boolean | undefined; - strictPropertyInitialization?: boolean | undefined; - stripInternal?: boolean | undefined; - suppressExcessPropertyErrors?: boolean | undefined; - suppressImplicitAnyIndexErrors?: boolean | undefined; - target?: MonacoEditor.languages.typescript.ScriptTarget | undefined; - traceResolution?: boolean | undefined; - resolveJsonModule?: boolean | undefined; - types?: string[] | undefined; - typeRoots?: string[] | undefined; - esModuleInterop?: boolean | undefined; - useDefineForClassFields?: boolean | undefined; - }; - /** The Sandbox's current compiler options */ - getCompilerOptions: () => MonacoEditor.languages.typescript.CompilerOptions; - /** Replace the Sandbox's compiler options */ - setCompilerSettings: (opts: CompilerOptions) => void; - /** Overwrite the Sandbox's compiler options */ - updateCompilerSetting: (key: keyof CompilerOptions, value: any) => void; - /** Update a single compiler option in the SAndbox */ - updateCompilerSettings: (opts: CompilerOptions) => void; - /** A way to get callbacks when compiler settings have changed */ - setDidUpdateCompilerSettings: (func: (opts: CompilerOptions) => void) => void; - /** A copy of lzstring, which is used to archive/unarchive code */ - lzstring: typeof lzstring; - /** Returns compiler options found in the params of the current page */ - createURLQueryWithCompilerOptions: ( - _sandbox: any, - paramOverrides?: any, - ) => string; - /** - * @deprecated Use `getTwoSlashCompilerOptions` instead. - * - * Returns compiler options in the source code using twoslash notation - */ - getTwoSlashComplierOptions: (code: string) => any; - /** Returns compiler options in the source code using twoslash notation */ - getTwoSlashCompilerOptions: (code: string) => any; - /** Gets to the current monaco-language, this is how you talk to the background webworkers */ - languageServiceDefaults: MonacoEditor.languages.typescript.LanguageServiceDefaults; - /** The path which represents the current file using the current compiler options */ - filepath: string; - /** Adds a file to the vfs used by the editor */ - addLibraryToRuntime: (code: string, _path: string) => void; -}; -export type Sandbox = ReturnType; -export {}; diff --git a/packages/website/src/vendor/typescript-vfs.d.ts b/packages/website/src/vendor/typescript-vfs.d.ts deleted file mode 100644 index e6c7df7b4391..000000000000 --- a/packages/website/src/vendor/typescript-vfs.d.ts +++ /dev/null @@ -1,145 +0,0 @@ -/********************************************** - * DO NOT MODIFY THIS FILE MANUALLY * - * * - * THIS FILE HAS BEEN FETCHED FROM THE * - * TYPESCRIPT PLAYGROUND SOURCE CODE. * - * * - * YOU CAN REGENERATE THESE FILES USING * - * yarn generate-website-dts * - **********************************************/ - -import type * as ts from 'typescript'; -type System = ts.System; -type CompilerOptions = ts.CompilerOptions; -type CustomTransformers = ts.CustomTransformers; -type LanguageServiceHost = ts.LanguageServiceHost; -type CompilerHost = ts.CompilerHost; -type SourceFile = ts.SourceFile; -type TS = typeof ts; -export interface VirtualTypeScriptEnvironment { - sys: System; - languageService: ts.LanguageService; - getSourceFile: (fileName: string) => ts.SourceFile | undefined; - createFile: (fileName: string, content: string) => void; - updateFile: ( - fileName: string, - content: string, - replaceTextSpan?: ts.TextSpan, - ) => void; -} -/** - * Makes a virtual copy of the TypeScript environment. This is the main API you want to be using with - * @typescript/vfs. A lot of the other exposed functions are used by this function to get set up. - * - * @param sys an object which conforms to the TS Sys (a shim over read/write access to the fs) - * @param rootFiles a list of files which are considered inside the project - * @param ts a copy pf the TypeScript module - * @param compilerOptions the options for this compiler run - * @param customTransformers custom transformers for this compiler run - */ -export declare function createVirtualTypeScriptEnvironment( - sys: System, - rootFiles: string[], - ts: TS, - compilerOptions?: CompilerOptions, - customTransformers?: CustomTransformers, -): VirtualTypeScriptEnvironment; -/** - * Grab the list of lib files for a particular target, will return a bit more than necessary (by including - * the dom) but that's OK, we're really working with the constraint that you can't get a list of files - * when running in a browser. - * - * @param target The compiler settings target baseline - * @param ts A copy of the TypeScript module - */ -export declare const knownLibFilesForCompilerOptions: ( - compilerOptions: CompilerOptions, - ts: TS, -) => string[]; -/** - * Sets up a Map with lib contents by grabbing the necessary files from - * the local copy of typescript via the file system. - * - * The first two args are un-used, but kept around so as to not cause a - * semver major bump for no gain to module users. - */ -export declare const createDefaultMapFromNodeModules: ( - _compilerOptions: CompilerOptions, - _ts?: typeof ts, - tsLibDirectory?: string, -) => Map; -/** - * Adds recursively files from the FS into the map based on the folder - */ -export declare const addAllFilesFromFolder: ( - map: Map, - workingDir: string, -) => void; -/** Adds all files from node_modules/@types into the FS Map */ -export declare const addFilesForTypesIntoFolder: ( - map: Map, -) => void; -/** - * Create a virtual FS Map with the lib files from a particular TypeScript - * version based on the target, Always includes dom ATM. - * - * @param options The compiler target, which dictates the libs to set up - * @param version the versions of TypeScript which are supported - * @param cache should the values be stored in local storage - * @param ts a copy of the typescript import - * @param lzstring an optional copy of the lz-string import - * @param fetcher an optional replacement for the global fetch function (tests mainly) - * @param storer an optional replacement for the localStorage global (tests mainly) - */ -export declare const createDefaultMapFromCDN: ( - options: CompilerOptions, - version: string, - cache: boolean, - ts: TS, - lzstring?: typeof import('lz-string'), - fetcher?: typeof fetch, - storer?: typeof localStorage, -) => Promise>; -/** - * Creates an in-memory System object which can be used in a TypeScript program, this - * is what provides read/write aspects of the virtual fs - */ -export declare function createSystem(files: Map): System; -/** - * Creates a file-system backed System object which can be used in a TypeScript program, you provide - * a set of virtual files which are prioritised over the FS versions, then a path to the root of your - * project (basically the folder your node_modules lives) - */ -export declare function createFSBackedSystem( - files: Map, - _projectRoot: string, - ts: TS, - tsLibDirectory?: string, -): System; -/** - * Creates an in-memory CompilerHost -which is essentially an extra wrapper to System - * which works with TypeScript objects - returns both a compiler host, and a way to add new SourceFile - * instances to the in-memory file system. - */ -export declare function createVirtualCompilerHost( - sys: System, - compilerOptions: CompilerOptions, - ts: TS, -): { - compilerHost: CompilerHost; - updateFile: (sourceFile: SourceFile) => boolean; -}; -/** - * Creates an object which can host a language service against the virtual file-system - */ -export declare function createVirtualLanguageServiceHost( - sys: System, - rootFiles: string[], - compilerOptions: CompilerOptions, - ts: TS, - customTransformers?: CustomTransformers, -): { - languageServiceHost: LanguageServiceHost; - updateFile: (sourceFile: ts.SourceFile) => void; -}; -export {}; diff --git a/packages/website/tools/generate-package-versions.ts b/packages/website/tools/generate-package-versions.ts new file mode 100644 index 000000000000..0f599bad2d5a --- /dev/null +++ b/packages/website/tools/generate-package-versions.ts @@ -0,0 +1,135 @@ +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; + +import fetch from 'cross-fetch'; +import * as semver from 'semver'; + +import rootPackageJson from '../../../package.json'; + +interface FetchObject { + package: string; + downloads: Record; +} + +/** + * Get the latest version of each major version + * @param versions + */ +function getLatestMajorVersions(versions: [string, number][]): string[] { + // + const latestMajorVersions = new Map(); + + for (const [version] of versions) { + const major = semver.major(version); + + const latest = latestMajorVersions.get(major); + + if (!latest) { + latestMajorVersions.set(major, version); + } else if (semver.gt(version, latest)) { + latestMajorVersions.set(major, version); + } + } + + return Array.from(latestMajorVersions.values()); +} + +/** + * Get the 10 most popular versions + * @param versions + */ +function getMostPopularVersions(versions: [string, number][]): string[] { + return versions + .sort(([, a], [, b]) => (a === b ? 0 : a < b ? 1 : -1)) + .slice(0, 15) + .map(([version]) => version); +} + +/** + * Group versions by major and minor version + * @param versions + */ +function groupByVersion(versions: [string, number][]): [string, number][] { + const groups = new Map(); + + for (const [version, downloads] of versions) { + const parsed = semver.parse(version); + if (!parsed) { + continue; + } + const name = `${parsed.major}.${parsed.minor}`; + const group = groups.get(name); + if (!group) { + groups.set(name, [version, downloads]); + } else { + if (semver.gt(version, group[0])) { + group[0] = version; + } + group[1] += downloads; + groups.set(name, group); + } + } + + return Array.from(groups.values()); +} + +function sortAndFilter( + downloads: Record, + allowList: string, +): string[] { + const versions = Object.entries(downloads).filter( + ([version]) => + !version.includes('-') && semver.satisfies(version, allowList), + ); + + const grouped = groupByVersion(versions); + + const popular = new Set([ + ...getMostPopularVersions(grouped), + ...getLatestMajorVersions(grouped), + ]); + + return Array.from(popular).sort(semver.rcompare); +} + +async function getPackageStats( + packageName: string, + allowList: string, +): Promise { + const encodedPackageName = encodeURIComponent(packageName); + const response = await fetch( + `https://api.npmjs.org/versions/${encodedPackageName}/last-week`, + { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }, + ); + const result = (await response.json()) as FetchObject; + return sortAndFilter(result.downloads, allowList); +} + +async function main(): Promise { + const packages = await Promise.all([ + getPackageStats('typescript', rootPackageJson.devDependencies.typescript), + // getPackageStats('@typescript-eslint/eslint-plugin', '^5.0.0'), + // getPackageStats('eslint', '^6.0.0 || ^7.0.0 || ^8.0.0'), + ]); + + const result = { + typescript: packages[0], + // 'eslint-plugin': packages[1], + // eslint: packages[2], + }; + + const fileUrl = path.join( + __dirname, + '../src/components/packageVersions.json', + ); + + await fs.writeFile(fileUrl, JSON.stringify(result, null, 2) + '\n', 'utf8'); +} + +main().catch(error => { + console.error(error); + process.exitCode = 1; +}); diff --git a/packages/website/tools/generate-website-dts.ts b/packages/website/tools/generate-website-dts.ts deleted file mode 100644 index 6f7525b9f0da..000000000000 --- a/packages/website/tools/generate-website-dts.ts +++ /dev/null @@ -1,108 +0,0 @@ -import * as fs from 'node:fs/promises'; -import * as path from 'node:path'; - -import fetch from 'cross-fetch'; -import makeDir from 'make-dir'; -import prettier from 'prettier'; -import { rimraf } from 'rimraf'; - -const BASE_HOST = 'https://www.staging-typescript.org'; - -const banner = [ - '/**********************************************', - ' * DO NOT MODIFY THIS FILE MANUALLY *', - ' * *', - ' * THIS FILE HAS BEEN FETCHED FROM THE *', - ' * TYPESCRIPT PLAYGROUND SOURCE CODE. *', - ' * *', - ' * YOU CAN REGENERATE THESE FILES USING *', - ' * yarn generate-website-dts *', - ' **********************************************/', -]; - -async function getFileAndStoreLocally( - url: string, - path: string, - editFunc: (arg: string) => string = (text: string): string => text, -): Promise { - console.log('Fetching', url); - const response = await fetch(BASE_HOST + url, { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }); - - const config = await prettier.resolveConfig(path); - - let contents = await response.text(); - contents = [...banner, '', editFunc(contents)].join('\n'); - contents = prettier.format(contents, { - parser: 'typescript', - ...config, - }); - - await fs.writeFile(path, contents, 'utf8'); -} - -function replaceImports(text: string, from: string, to: string): string { - const regex = new RegExp(`from ["']${from}["']`, 'g'); - const regex2 = new RegExp(`import\\(["']${from}["']\\)`, 'g'); - return text.replace(regex, `from '${to}'`).replace(regex2, `import('${to}')`); -} - -function injectImports(text: string, from: string, safeName: string): string { - const regex = new RegExp(`import\\(["']${from}["']\\)`, 'g'); - if (regex.test(text)) { - return ( - `import type * as ${safeName} from '${from}';\n` + - text.replace(regex, safeName) - ); - } - return text; -} - -function processFiles(text: string): string { - let result = text; - result = injectImports(result, 'monaco-editor', 'MonacoEditor'); - result = injectImports(result, 'typescript', 'ts'); - result = replaceImports(result, './vendor/lzstring.min', 'lz-string'); - result = replaceImports( - result, - './vendor/typescript-vfs', - './typescript-vfs', - ); - // replace the import of the worker with the type - result = result.replace( - /import\s*\{\s*TypeScriptWorker\s*}\s*from\s*['"].\/tsWorker['"];/, - 'import TypeScriptWorker = MonacoEditor.languages.typescript.TypeScriptWorker;', - ); - // replace all imports with import type - result = result.replace(/^import\s+(?!type)/gm, 'import type '); - return result; -} - -async function main(): Promise { - const vendor = path.join(__dirname, '..', 'src', 'vendor'); - - console.log('Cleaning...'); - await rimraf(vendor); - await makeDir(vendor); - - // TS-VFS - await getFileAndStoreLocally( - '/js/sandbox/vendor/typescript-vfs.d.ts', - path.join(vendor, 'typescript-vfs.d.ts'), - processFiles, - ); - - // Sandbox - await getFileAndStoreLocally( - '/js/sandbox/index.d.ts', - path.join(vendor, 'sandbox.d.ts'), - processFiles, - ); -} - -main().catch(error => { - console.error(error); - process.exitCode = 1; -}); diff --git a/packages/website/typings/typescript.d.ts b/packages/website/typings/typescript.d.ts index 30af30c4ae78..463d96e25797 100644 --- a/packages/website/typings/typescript.d.ts +++ b/packages/website/typings/typescript.d.ts @@ -14,6 +14,7 @@ declare module 'typescript' { type?: unknown; category?: { message: string }; description?: { message: string }; + isCommandLineOnly?: boolean; element?: { type: unknown; }; diff --git a/patches/typescript+5.2.1-rc.patch b/patches/typescript+5.2.1-rc.patch deleted file mode 100644 index 765339eee26d..000000000000 --- a/patches/typescript+5.2.1-rc.patch +++ /dev/null @@ -1,84 +0,0 @@ -diff --git a/node_modules/typescript/lib/typescript.d.ts b/node_modules/typescript/lib/typescript.d.ts -index ead6d07..75b1757 100644 ---- a/node_modules/typescript/lib/typescript.d.ts -+++ b/node_modules/typescript/lib/typescript.d.ts -@@ -371,8 +371,8 @@ declare namespace ts { - JSDocFunctionType = 324, - JSDocVariadicType = 325, - JSDocNamepathType = 326, -+ /** @deprecated This was only added in 4.7 */ - JSDoc = 327, -- /** @deprecated Use SyntaxKind.JSDoc */ - JSDocComment = 327, - JSDocText = 328, - JSDocTypeLiteral = 329, -@@ -738,6 +738,8 @@ declare namespace ts { - readonly name: PropertyName; - readonly questionToken?: QuestionToken; - readonly type?: TypeNode; -+ /** @deprecated removed in 5.0 but we want to keep it for backwards compatibility checks! */ -+ readonly initializer?: Expression | undefined; - } - interface PropertyDeclaration extends ClassElement, JSDocContainer { - readonly kind: SyntaxKind.PropertyDeclaration; -@@ -763,6 +765,10 @@ declare namespace ts { - readonly parent: ObjectLiteralExpression; - readonly name: PropertyName; - readonly initializer: Expression; -+ /** @deprecated removed in 5.0 but we want to keep it for backwards compatibility checks! */ -+ readonly questionToken?: QuestionToken | undefined; -+ /** @deprecated removed in 5.0 but we want to keep it for backwards compatibility checks! */ -+ readonly exclamationToken?: ExclamationToken | undefined; - } - interface ShorthandPropertyAssignment extends ObjectLiteralElement, JSDocContainer { - readonly kind: SyntaxKind.ShorthandPropertyAssignment; -@@ -770,6 +776,12 @@ declare namespace ts { - readonly name: Identifier; - readonly equalsToken?: EqualsToken; - readonly objectAssignmentInitializer?: Expression; -+ /** @deprecated removed in 5.0 but we want to keep it for backwards compatibility checks! */ -+ readonly modifiers?: NodeArray | undefined; -+ /** @deprecated removed in 5.0 but we want to keep it for backwards compatibility checks! */ -+ readonly questionToken?: QuestionToken | undefined; -+ /** @deprecated removed in 5.0 but we want to keep it for backwards compatibility checks! */ -+ readonly exclamationToken?: ExclamationToken | undefined; - } - interface SpreadAssignment extends ObjectLiteralElement, JSDocContainer { - readonly kind: SyntaxKind.SpreadAssignment; -@@ -892,6 +904,8 @@ declare namespace ts { - } - interface FunctionTypeNode extends FunctionOrConstructorTypeNodeBase, LocalsContainer { - readonly kind: SyntaxKind.FunctionType; -+ /** @deprecated removed in 5.0 but we want to keep it for backwards compatibility checks! */ -+ readonly modifiers?: NodeArray | undefined; - } - interface ConstructorTypeNode extends FunctionOrConstructorTypeNodeBase, LocalsContainer { - readonly kind: SyntaxKind.ConstructorType; -@@ -4584,7 +4598,13 @@ declare namespace ts { - function symbolName(symbol: Symbol): string; - function getNameOfJSDocTypedef(declaration: JSDocTypedefTag): Identifier | PrivateIdentifier | undefined; - function getNameOfDeclaration(declaration: Declaration | Expression | undefined): DeclarationName | undefined; -+ /** -+ * @deprecated don't use this directly as it does not exist pre-4.8; instead use getDecorators from `@typescript-eslint/type-utils`. -+ */ - function getDecorators(node: HasDecorators): readonly Decorator[] | undefined; -+ /** -+ * @deprecated don't use this directly as it does not exist pre-4.8; instead use getModifiers from `@typescript-eslint/type-utils`. -+ */ - function getModifiers(node: HasModifiers): readonly Modifier[] | undefined; - /** - * Gets the JSDoc parameter tags for the node if present. -@@ -5110,7 +5130,13 @@ declare namespace ts { - function isModuleName(node: Node): node is ModuleName; - function isBinaryOperatorToken(node: Node): node is BinaryOperatorToken; - function setTextRange(range: T, location: TextRange | undefined): T; -+ /** -+ * @deprecated don't use this directly as it does not exist pre-4.8; instead use getModifiers from `@typescript-eslint/type-utils`. -+ */ - function canHaveModifiers(node: Node): node is HasModifiers; -+ /** -+ * @deprecated don't use this directly as it does not exist pre-4.8; instead use getDecorators from `@typescript-eslint/type-utils`. -+ */ - function canHaveDecorators(node: Node): node is HasDecorators; - /** - * Invokes a callback for each child of the given node. The 'cbNode' callback is invoked for all child nodes diff --git a/yarn.lock b/yarn.lock index 5f43be971cbb..227f11010913 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3874,6 +3874,30 @@ __metadata: languageName: node linkType: hard +"@monaco-editor/loader@npm:^1.3.3": + version: 1.3.3 + resolution: "@monaco-editor/loader@npm:1.3.3" + dependencies: + state-local: ^1.0.6 + peerDependencies: + monaco-editor: ">= 0.21.0 < 1" + checksum: 037dd4758651cb623482398fba884c0ddec1ed40502185f1fa417e54f47485291e236eb13bcdffb7bca292edd97ca8e3c51c2c3505e17a3184f4c6d11016fcac + languageName: node + linkType: hard + +"@monaco-editor/react@npm:^4.3.0": + version: 4.5.2 + resolution: "@monaco-editor/react@npm:4.5.2" + dependencies: + "@monaco-editor/loader": ^1.3.3 + peerDependencies: + monaco-editor: ">= 0.25.0 < 1" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: b739b9012729daaa2acc6bccc100cc6e96770b4a5d3779360775b7c69e9b7a333152d639e554e28395302c5c914ad692951a930ffdb2f21916f3cdbe487916c2 + languageName: node + linkType: hard + "@netlify/open-api@npm:^2.19.1": version: 2.19.1 resolution: "@netlify/open-api@npm:2.19.1" @@ -6163,6 +6187,15 @@ __metadata: languageName: unknown linkType: soft +"@typescript/vfs@npm:^1.4.0": + version: 1.5.0 + resolution: "@typescript/vfs@npm:1.5.0" + dependencies: + debug: ^4.1.1 + checksum: 09916e2fc567fe993fd81a34a48f72183b4c84d73a6bfdd8d4ca60ecd28de80fbb509b050f30513913b86270dc2abc759cffb4c0fd1068f7dd53104a1b0b7e27 + languageName: node + linkType: hard + "@webassemblyjs/ast@npm:1.11.5, @webassemblyjs/ast@npm:^1.11.5": version: 1.11.5 resolution: "@webassemblyjs/ast@npm:1.11.5" @@ -16961,7 +16994,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.5.4, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": +"prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -17295,6 +17328,16 @@ __metadata: languageName: node linkType: hard +"react-resizable-panels@npm:^0.0.55": + version: 0.0.55 + resolution: "react-resizable-panels@npm:0.0.55" + peerDependencies: + react: ^16.14.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 + checksum: a7cb938403b57d2489638eb2cf93fddcb420fbd51b8af2b959881ada83ba052f79f51610d650019e577f0e6018542e9195e4bfe45e4e884b4821744c45837646 + languageName: node + linkType: hard + "react-router-config@npm:^5.1.1": version: 5.1.1 resolution: "react-router-config@npm:5.1.1" @@ -17344,43 +17387,6 @@ __metadata: languageName: node linkType: hard -"react-split-pane@npm:0.1.92": - version: 0.1.92 - resolution: "react-split-pane@npm:0.1.92" - dependencies: - prop-types: ^15.7.2 - react-lifecycles-compat: ^3.0.4 - react-style-proptype: ^3.2.2 - peerDependencies: - react: ^16.0.0-0 - react-dom: ^16.0.0-0 - checksum: 4890f172636baeb4bb197195e8d896297ef8eec3c9887331e4a3854da6cf66b2f7758132c3e05f4b38eed595710f678ae1399cb0ca82e9811db06ad95410eee6 - languageName: node - linkType: hard - -"react-split-pane@patch:react-split-pane@npm%3A0.1.92#./.yarn/patches/react-split-pane-npm-0.1.92-93dbf51dff.patch::locator=%40typescript-eslint%2Ftypescript-eslint%40workspace%3A.": - version: 0.1.92 - resolution: "react-split-pane@patch:react-split-pane@npm%3A0.1.92#./.yarn/patches/react-split-pane-npm-0.1.92-93dbf51dff.patch::version=0.1.92&hash=9eba81&locator=%40typescript-eslint%2Ftypescript-eslint%40workspace%3A." - dependencies: - prop-types: ^15.7.2 - react-lifecycles-compat: ^3.0.4 - react-style-proptype: ^3.2.2 - peerDependencies: - react: ^16.0.0-0 - react-dom: ^16.0.0-0 - checksum: e05f6773bb687e6f3fffdb4bac48bc655b41b825ff8eb6ee8a39346d48fa8044be6f8d5832b1353700db65311712e561253cdeb822a074779afbd0754b3703ae - languageName: node - linkType: hard - -"react-style-proptype@npm:^3.2.2": - version: 3.2.2 - resolution: "react-style-proptype@npm:3.2.2" - dependencies: - prop-types: ^15.5.4 - checksum: f0e646e1488a18849a2a0fcff459a3769869d62be5729ff578dc6d37d0c0617706075efe09a15ff9372ba35419e31af5b0df154bc9487d1381cdc80d3dab6d7c - languageName: node - linkType: hard - "react-textarea-autosize@npm:^8.3.2": version: 8.3.3 resolution: "react-textarea-autosize@npm:8.3.3" @@ -18868,6 +18874,13 @@ __metadata: languageName: node linkType: hard +"state-local@npm:^1.0.6": + version: 1.0.7 + resolution: "state-local@npm:1.0.7" + checksum: d1afcf1429e7e6eb08685b3a94be8797db847369316d4776fd51f3962b15b984dacc7f8e401ad20968e5798c9565b4b377afedf4e4c4d60fe7495e1cbe14a251 + languageName: node + linkType: hard + "state-toggle@npm:^1.0.0": version: 1.0.3 resolution: "state-toggle@npm:1.0.3" @@ -20782,6 +20795,7 @@ __metadata: "@docusaurus/remark-plugin-npm2yarn": ~2.4.1 "@docusaurus/theme-common": ~2.4.1 "@mdx-js/react": 1.6.22 + "@monaco-editor/react": ^4.3.0 "@playwright/test": ^1.36.0 "@types/react": "*" "@types/react-helmet": ^6.1.6 @@ -20791,6 +20805,7 @@ __metadata: "@typescript-eslint/rule-schema-to-typescript-types": 6.7.2 "@typescript-eslint/types": 6.7.2 "@typescript-eslint/website-eslint": 6.7.2 + "@typescript/vfs": ^1.4.0 clsx: ^2.0.0 copy-webpack-plugin: ^11.0.0 cross-fetch: "*" @@ -20807,7 +20822,7 @@ __metadata: raw-loader: ^4.0.2 react: ^18.2.0 react-dom: ^18.2.0 - react-split-pane: ^0.1.92 + react-resizable-panels: ^0.0.55 remark-docusaurus-tabs: ^0.2.0 rimraf: "*" semver: ^7.5.4