From 05a15846bc334ad7c2be72408eb260c9f91257a1 Mon Sep 17 00:00:00 2001 From: Armano Date: Wed, 1 Dec 2021 05:27:44 +0100 Subject: [PATCH 01/14] docs(website): initial refactor of AstViewer --- .../src/components/Playground.module.css | 2 +- .../website/src/components/Playground.tsx | 24 +++++---- .../src/components/ast/ASTViewer.module.css | 22 +++++--- .../website/src/components/ast/ASTViewer.tsx | 21 ++++---- .../website/src/components/ast/Elements.tsx | 51 ++++++------------- .../src/components/ast/PropertyName.tsx | 40 ++++++++++----- .../src/components/ast/PropertyValue.tsx | 6 ++- .../website/src/components/ast/selection.ts | 22 +++----- packages/website/src/components/ast/types.ts | 11 ++-- .../src/components/editor/LoadedEditor.tsx | 9 ++-- .../website/src/components/editor/types.ts | 4 +- .../src/components/hooks/useHashState.ts | 21 +------- .../src/components/lib/shallowEqual.ts | 19 +++++++ packages/website/src/components/types.ts | 10 ++++ 14 files changed, 136 insertions(+), 126 deletions(-) create mode 100644 packages/website/src/components/lib/shallowEqual.ts diff --git a/packages/website/src/components/Playground.module.css b/packages/website/src/components/Playground.module.css index 6c5a9fdad71e..f1d78f13c6c4 100644 --- a/packages/website/src/components/Playground.module.css +++ b/packages/website/src/components/Playground.module.css @@ -25,8 +25,8 @@ height: 100%; width: 50%; border: 1px solid var(--ifm-color-emphasis-100); + padding: 0; overflow: auto; - background: var(--ifm-background-surface-color); word-wrap: initial; white-space: nowrap; background: var(--code-editor-bg); diff --git a/packages/website/src/components/Playground.tsx b/packages/website/src/components/Playground.tsx index f0c7903e5f27..885ad1d10384 100644 --- a/packages/website/src/components/Playground.tsx +++ b/packages/website/src/components/Playground.tsx @@ -11,7 +11,9 @@ import OptionsSelector from './OptionsSelector'; import ASTViewer from './ast/ASTViewer'; import { LoadingEditor } from './editor/LoadingEditor'; import { EditorEmbed } from './editor/EditorEmbed'; -import type { RuleDetails } from './types'; +import { shallowEqual } from './lib/shallowEqual'; + +import type { RuleDetails, SelectedRange } from './types'; import type { TSESTree } from '@typescript-eslint/website-eslint'; @@ -30,21 +32,23 @@ function Playground(): JSX.Element { const [ruleNames, setRuleNames] = useState([]); const [isLoading, setIsLoading] = useState(true); const [tsVersions, setTSVersion] = useState([]); - const [selectedNode, setSelectedNode] = useState(null); + const [selectedRange, setSelectedRange] = useState( + null, + ); const [position, setPosition] = useState(null); const updateSelectedNode = useCallback( - (node: TSESTree.Node | null) => { + (value: SelectedRange | null) => { if ( - !node || - !selectedNode || - selectedNode.range[0] !== node.range[0] || - selectedNode.range[1] !== node.range[1] + !value || + !selectedRange || + !shallowEqual(selectedRange.start, value.start) || + !shallowEqual(selectedRange.end, value.end) ) { - setSelectedNode(node); + setSelectedRange(value); } }, - [selectedNode], + [selectedRange], ); return ( @@ -77,7 +81,7 @@ function Playground(): JSX.Element { rules={state.rules} showAST={state.showAST} onASTChange={setAST} - decoration={selectedNode} + decoration={selectedRange} onChange={(code): void => setState({ code: code })} onLoaded={(ruleNames, tsVersions): void => { setRuleNames(ruleNames); diff --git a/packages/website/src/components/ast/ASTViewer.module.css b/packages/website/src/components/ast/ASTViewer.module.css index ebb4b3470dcb..3ec748bb3a9d 100644 --- a/packages/website/src/components/ast/ASTViewer.module.css +++ b/packages/website/src/components/ast/ASTViewer.module.css @@ -1,16 +1,23 @@ +.list, +.subList, +.clickable { + font-family: var(--ifm-font-family-monospace); + background: transparent; + border: none; + padding: 0; + font-weight: normal; + font-size: 13px; + line-height: 18px; + letter-spacing: 0; + font-feature-settings: 'liga' 0, 'calt' 0; +} + .list, .subList { cursor: default; box-sizing: border-box; margin: 0; - list-style: none; padding-left: 1.5rem; - font-family: Consolas, "Courier New", monospace; - font-weight: normal; - font-size: 13px; - font-feature-settings: "liga" 0, "calt" 0; - line-height: 18px; - letter-spacing: 0px; } .nonExpand, @@ -73,6 +80,7 @@ .clickable { cursor: pointer; } + .clickable:hover { text-decoration: underline; } diff --git a/packages/website/src/components/ast/ASTViewer.tsx b/packages/website/src/components/ast/ASTViewer.tsx index 7c234f91a238..f6cb3852dd96 100644 --- a/packages/website/src/components/ast/ASTViewer.tsx +++ b/packages/website/src/components/ast/ASTViewer.tsx @@ -2,17 +2,20 @@ import React, { useEffect, useState } from 'react'; import styles from './ASTViewer.module.css'; import type { TSESTree } from '@typescript-eslint/website-eslint'; -import type { Position } from './types'; +import type { SelectedPosition, SelectedRange } from '../types'; import { ElementObject } from './Elements'; import type Monaco from 'monaco-editor'; +import { isRecord } from './selection'; -function ASTViewer(props: { - ast: TSESTree.Node | string; +export interface ASTViewerProps { + ast: Record | TSESTree.Node | string; position?: Monaco.Position | null; - onSelectNode: (node: TSESTree.Node | null) => void; -}): JSX.Element { - const [selection, setSelection] = useState(() => + onSelectNode: (node: SelectedRange | null) => void; +} + +function ASTViewer(props: ASTViewerProps): JSX.Element { + const [selection, setSelection] = useState(() => props.position ? { line: props.position.lineNumber, @@ -32,9 +35,7 @@ function ASTViewer(props: { ); }, [props.position]); - return typeof props.ast === 'string' ? ( -
{props.ast}
- ) : ( + return isRecord(props.ast) ? (
+ ) : ( +
{props.ast}
); } diff --git a/packages/website/src/components/ast/Elements.tsx b/packages/website/src/components/ast/Elements.tsx index 9dba9eff491a..e92d74e5466b 100644 --- a/packages/website/src/components/ast/Elements.tsx +++ b/packages/website/src/components/ast/Elements.tsx @@ -1,13 +1,6 @@ -import React, { - SyntheticEvent, - useCallback, - useEffect, - useRef, - useState, -} from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import clsx from 'clsx'; -import type { TSESTree } from '@typescript-eslint/website-eslint'; import type { GenericParams } from './types'; import { scrollIntoViewIfNeeded } from '@site/src/components/lib/scroll-into'; @@ -89,7 +82,7 @@ export function ElementArray(props: GenericParams): JSX.Element { } export function ElementObject( - props: GenericParams>, + props: GenericParams>, ): JSX.Element { const [isExpanded, setIsExpanded] = useState(() => { return isInRange(props.selection, props.value); @@ -100,25 +93,17 @@ export function ElementObject( ); const listItem = useRef(null); - const onMouseEnter = useCallback( - (e: SyntheticEvent) => { - if (isEsNode(props.value)) { - props.onSelectNode(props.value as TSESTree.Node); - e.stopPropagation(); - e.preventDefault(); - } - }, - [props.value], - ); + const onMouseEnter = useCallback(() => { + if (isEsNode(props.value)) { + props.onSelectNode(props.value.loc); + } + }, [props.value]); - const onMouseLeave = useCallback( - (_e: SyntheticEvent) => { - if (isEsNode(props.value)) { - props.onSelectNode(null); - } - }, - [props.value], - ); + const onMouseLeave = useCallback(() => { + if (isEsNode(props.value)) { + props.onSelectNode(null); + } + }, [props.value]); useEffect(() => { const selected = isInRange(props.selection, props.value); @@ -148,10 +133,10 @@ export function ElementObject( isExpanded ? '' : styles.open, isSelected ? styles.selected : '', )} - onMouseEnter={onMouseEnter} - onMouseLeave={onMouseLeave} > setIsExpanded(!isExpanded)} @@ -193,16 +178,12 @@ export function ElementItem(props: GenericParams): JSX.Element { onSelectNode={props.onSelectNode} /> ); - } else if ( - typeof props.value === 'object' && - props.value && - props.value.constructor === Object - ) { + } else if (isRecord(props.value)) { return ( } + value={props.value} selection={props.selection} onSelectNode={props.onSelectNode} /> diff --git a/packages/website/src/components/ast/PropertyName.tsx b/packages/website/src/components/ast/PropertyName.tsx index f8768a6c4a2a..fa8614efe6c4 100644 --- a/packages/website/src/components/ast/PropertyName.tsx +++ b/packages/website/src/components/ast/PropertyName.tsx @@ -1,27 +1,39 @@ -import React, { SyntheticEvent } from 'react'; +import React, { MouseEvent } from 'react'; import clsx from 'clsx'; import styles from './ASTViewer.module.css'; -export default function PropertyName(props: { - name?: string; - propName?: string; - onClick?: (e: SyntheticEvent) => void; - onMouseEnter?: (e: SyntheticEvent) => void; -}): JSX.Element { +export interface PropertyNameProps { + readonly name?: string; + readonly propName?: string; + readonly onClick?: (e: MouseEvent) => void; + readonly onMouseEnter?: (e: MouseEvent) => void; + readonly onMouseLeave?: (e: MouseEvent) => void; +} + +export default function PropertyName(props: PropertyNameProps): JSX.Element { return ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions - + <> {props.propName && ( - + )} {props.propName && : } {props.name && ( - + )} - + ); } diff --git a/packages/website/src/components/ast/PropertyValue.tsx b/packages/website/src/components/ast/PropertyValue.tsx index 48f3ba119a68..3a22ca9010c2 100644 --- a/packages/website/src/components/ast/PropertyValue.tsx +++ b/packages/website/src/components/ast/PropertyValue.tsx @@ -1,7 +1,11 @@ import React from 'react'; import styles from './ASTViewer.module.css'; -export default function PropertyValue(props: { value: unknown }): JSX.Element { +export interface PropertyValueProps { + readonly value: unknown; +} + +export default function PropertyValue(props: PropertyValueProps): JSX.Element { if (typeof props.value === 'string') { return ( {JSON.stringify(props.value)} diff --git a/packages/website/src/components/ast/selection.ts b/packages/website/src/components/ast/selection.ts index 9a882d2bf9af..5f6e57efe5d2 100644 --- a/packages/website/src/components/ast/selection.ts +++ b/packages/website/src/components/ast/selection.ts @@ -1,26 +1,20 @@ import type { TSESTree } from '@typescript-eslint/website-eslint'; -import type { Position } from './types'; +import type { SelectedPosition } from '../types'; export const propsToFilter = ['parent', 'comments', 'tokens', 'loc']; export function filterRecord( - values: TSESTree.Node | Record, + values: Record, ): [string, unknown][] { return Object.entries(values).filter( item => !propsToFilter.includes(item[0]), ); } -export function isNode(node: unknown): node is TSESTree.Node { - return Boolean( - typeof node === 'object' && node && 'type' in node && 'loc' in node, - ); -} - export function isWithinNode( - loc: Position, - start: Position, - end: Position, + loc: SelectedPosition, + start: SelectedPosition, + end: SelectedPosition, ): boolean { const canStart = start.line < loc.line || @@ -43,7 +37,7 @@ export function isEsNode( } export function isInRange( - position: Position | null | undefined, + position: SelectedPosition | null | undefined, value: unknown, ): boolean { return Boolean( @@ -54,7 +48,7 @@ export function isInRange( } export function isArrayInRange( - position: Position | null | undefined, + position: SelectedPosition | null | undefined, value: unknown, ): boolean { return Boolean( @@ -65,7 +59,7 @@ export function isArrayInRange( } export function hasChildInRange( - position: Position | null | undefined, + position: SelectedPosition | null | undefined, value: unknown, ): boolean { return Boolean( diff --git a/packages/website/src/components/ast/types.ts b/packages/website/src/components/ast/types.ts index 53a565599ab9..f7eb9de5e08c 100644 --- a/packages/website/src/components/ast/types.ts +++ b/packages/website/src/components/ast/types.ts @@ -1,15 +1,10 @@ -import type { TSESTree } from '@typescript-eslint/website-eslint'; - -export interface Position { - line: number; - column: number; -} +import type { SelectedPosition, SelectedRange } from '../types'; export interface GenericParams { readonly propName?: string; readonly name?: string; readonly value: V; readonly level: string; - readonly selection?: Position | null; - readonly onSelectNode: (node: TSESTree.Node | null) => void; + readonly selection?: SelectedPosition | null; + readonly onSelectNode: (node: SelectedRange | null) => void; } diff --git a/packages/website/src/components/editor/LoadedEditor.tsx b/packages/website/src/components/editor/LoadedEditor.tsx index 309da95ad6fb..5faeb5c96067 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -11,7 +11,6 @@ import { createProvideCodeActions } from './createProvideCodeActions'; export interface LoadedEditorProps extends CommonEditorProps { readonly main: typeof Monaco; - readonly onSelect: (position: Monaco.Position | null) => void; readonly sandboxInstance: SandboxInstance; readonly webLinter: WebLinter; } @@ -149,10 +148,10 @@ export const LoadedEditor: React.FC = ({ ? [ { range: new sandboxInstance.monaco.Range( - decoration.loc.start.line, - decoration.loc.start.column + 1, - decoration.loc.end.line, - decoration.loc.end.column + 1, + decoration.start.line, + decoration.start.column + 1, + decoration.end.line, + decoration.end.column + 1, ), options: { inlineClassName: 'myLineDecoration', diff --git a/packages/website/src/components/editor/types.ts b/packages/website/src/components/editor/types.ts index 6760c57f13d8..308400b15f8e 100644 --- a/packages/website/src/components/editor/types.ts +++ b/packages/website/src/components/editor/types.ts @@ -1,10 +1,10 @@ import type Monaco from 'monaco-editor'; -import type { ConfigModel } from '../types'; +import type { ConfigModel, SelectedRange } from '../types'; import type { TSESTree } from '@typescript-eslint/website-eslint'; export interface CommonEditorProps extends ConfigModel { readonly darkTheme: boolean; - readonly decoration: TSESTree.Node | null; + readonly decoration: SelectedRange | null; readonly onChange: (value: string) => void; readonly onASTChange: (value: string | TSESTree.Program) => void; readonly onSelect: (position: Monaco.Position | null) => void; diff --git a/packages/website/src/components/hooks/useHashState.ts b/packages/website/src/components/hooks/useHashState.ts index d6a8e8cc8e91..b1263ce04ccb 100644 --- a/packages/website/src/components/hooks/useHashState.ts +++ b/packages/website/src/components/hooks/useHashState.ts @@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from 'react'; import type { CompilerFlags, ConfigModel, RulesRecord } from '../types'; import * as lz from 'lzstring.ts'; +import { shallowEqual } from '../lib/shallowEqual'; function writeQueryParam(value: string): string { return lz.LZString.compressToEncodedURIComponent(value); @@ -76,26 +77,6 @@ const writeStateToUrl = (newState: ConfigModel): string => { return ''; }; -function shallowEqual( - object1: Record | ConfigModel | undefined, - object2: Record | ConfigModel | undefined, -): boolean { - if (object1 === object2) { - return true; - } - 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]) { - return false; - } - } - return true; -} - function useHashState( initialState: ConfigModel, ): [ConfigModel, (cfg: Partial) => void] { diff --git a/packages/website/src/components/lib/shallowEqual.ts b/packages/website/src/components/lib/shallowEqual.ts new file mode 100644 index 000000000000..f1e26ede82d5 --- /dev/null +++ b/packages/website/src/components/lib/shallowEqual.ts @@ -0,0 +1,19 @@ +export function shallowEqual( + object1: object | undefined | null, + object2: object | undefined | null, +): boolean { + if (object1 === object2) { + return true; + } + 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]) { + return false; + } + } + return true; +} diff --git a/packages/website/src/components/types.ts b/packages/website/src/components/types.ts index 4d5d032db0fc..53a1f45d9728 100644 --- a/packages/website/src/components/types.ts +++ b/packages/website/src/components/types.ts @@ -44,3 +44,13 @@ export interface ConfigModel { ts: string; showAST?: boolean; } + +export interface SelectedPosition { + line: number; + column: number; +} + +export interface SelectedRange { + start: SelectedPosition; + end: SelectedPosition; +} From 33fe86b053f3338ec8d781284d073e3642de844a Mon Sep 17 00:00:00 2001 From: Armano Date: Wed, 1 Dec 2021 06:31:21 +0100 Subject: [PATCH 02/14] docs(website): add simple ts ast viewer --- packages/website-eslint/src/linter/linter.js | 6 ++++++ packages/website-eslint/src/linter/parser.js | 4 +++- packages/website-eslint/types/index.d.ts | 1 + .../src/components/OptionsSelector.tsx | 17 +++++++++++---- .../website/src/components/Playground.tsx | 21 +++++++++++-------- .../src/components/ast/ASTViewer.module.css | 10 +++++++++ .../website/src/components/ast/ASTViewer.tsx | 2 +- .../website/src/components/ast/Elements.tsx | 11 +++++++--- .../src/components/ast/PropertyValue.tsx | 3 ++- .../website/src/components/ast/selection.ts | 14 ++++++++++--- packages/website/src/components/ast/types.ts | 2 +- .../src/components/editor/LoadedEditor.tsx | 6 ++++-- .../website/src/components/editor/types.ts | 3 ++- .../src/components/hooks/useHashState.ts | 13 +++++++++++- packages/website/src/components/types.ts | 2 +- 15 files changed, 87 insertions(+), 28 deletions(-) diff --git a/packages/website-eslint/src/linter/linter.js b/packages/website-eslint/src/linter/linter.js index 4b798674eeb6..1643dcf2562f 100644 --- a/packages/website-eslint/src/linter/linter.js +++ b/packages/website-eslint/src/linter/linter.js @@ -8,11 +8,13 @@ const PARSER_NAME = '@typescript-eslint/parser'; export function loadLinter() { const linter = new Linter(); let storedAST; + let storedTsAST; linter.defineParser(PARSER_NAME, { parseForESLint(code, options) { const toParse = parseForESLint(code, options); storedAST = toParse.ast; + storedTsAST = toParse.tsAst; return toParse; }, // parse(code: string, options: ParserOptions): ParseForESLintResult['ast'] { // const toParse = parseForESLint(code, options); @@ -39,6 +41,10 @@ export function loadLinter() { return storedAST; }, + getTsAst() { + return storedTsAST; + }, + lint(code, parserOptions, rules) { return linter.verify(code, { parser: PARSER_NAME, diff --git a/packages/website-eslint/src/linter/parser.js b/packages/website-eslint/src/linter/parser.js index 282f67c0da7a..41c0b24baf10 100644 --- a/packages/website-eslint/src/linter/parser.js +++ b/packages/website-eslint/src/linter/parser.js @@ -14,6 +14,7 @@ function parseAndGenerateServices(code, options) { return { ast: estree, + tsAst: ast, services: { hasFullTypeInformation: true, program, @@ -24,7 +25,7 @@ function parseAndGenerateServices(code, options) { } export function parseForESLint(code, parserOptions) { - const { ast, services } = parseAndGenerateServices(code, { + const { ast, tsAst, services } = parseAndGenerateServices(code, { ...parserOptions, jsx: parserOptions.ecmaFeatures?.jsx ?? false, useJSXTextNode: true, @@ -40,6 +41,7 @@ export function parseForESLint(code, parserOptions) { return { ast, + tsAst, services, scopeManager, visitorKeys, diff --git a/packages/website-eslint/types/index.d.ts b/packages/website-eslint/types/index.d.ts index eaa86160cba7..86e0c02eed6c 100644 --- a/packages/website-eslint/types/index.d.ts +++ b/packages/website-eslint/types/index.d.ts @@ -12,6 +12,7 @@ export interface WebLinter { ruleNames: { name: string; description?: string }[]; getAst(): ESLintAST; + getTsAst(): Record; lint( code: string, diff --git a/packages/website/src/components/OptionsSelector.tsx b/packages/website/src/components/OptionsSelector.tsx index 71f83a95902a..4e58b9525b29 100644 --- a/packages/website/src/components/OptionsSelector.tsx +++ b/packages/website/src/components/OptionsSelector.tsx @@ -132,11 +132,20 @@ function OptionsSelector({ /> + diff --git a/packages/website/src/components/Playground.tsx b/packages/website/src/components/Playground.tsx index 885ad1d10384..efc67bdb2787 100644 --- a/packages/website/src/components/Playground.tsx +++ b/packages/website/src/components/Playground.tsx @@ -28,7 +28,8 @@ function Playground(): JSX.Element { tsConfig: {}, }); const { isDarkTheme } = useThemeContext(); - const [ast, setAST] = useState(); + const [esAst, setEsAst] = useState(); + const [tsAst, setTsAST] = useState | string | null>(); const [ruleNames, setRuleNames] = useState([]); const [isLoading, setIsLoading] = useState(true); const [tsVersions, setTSVersion] = useState([]); @@ -80,7 +81,8 @@ function Playground(): JSX.Element { sourceType={state.sourceType} rules={state.rules} showAST={state.showAST} - onASTChange={setAST} + onEsASTChange={setEsAst} + onTsASTChange={setTsAST} decoration={selectedRange} onChange={(code): void => setState({ code: code })} onLoaded={(ruleNames, tsVersions): void => { @@ -93,13 +95,14 @@ function Playground(): JSX.Element { {state.showAST && (
- {ast && ( - - )} + {(tsAst && state.showAST === 'ts' && ) || + (esAst && ( + + ))}
)} diff --git a/packages/website/src/components/ast/ASTViewer.module.css b/packages/website/src/components/ast/ASTViewer.module.css index 3ec748bb3a9d..5ccb2fdceaef 100644 --- a/packages/website/src/components/ast/ASTViewer.module.css +++ b/packages/website/src/components/ast/ASTViewer.module.css @@ -65,6 +65,10 @@ color: #b58900; } +.propClass { + color: #b58900; +} + .propBoolean { color: #b58900; } @@ -75,6 +79,12 @@ .hidden { color: var(--ifm-color-emphasis-400); + max-width: 40%; + overflow: hidden; + display: inline-block; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: bottom; } .clickable { diff --git a/packages/website/src/components/ast/ASTViewer.tsx b/packages/website/src/components/ast/ASTViewer.tsx index f6cb3852dd96..9b70185cd3fe 100644 --- a/packages/website/src/components/ast/ASTViewer.tsx +++ b/packages/website/src/components/ast/ASTViewer.tsx @@ -11,7 +11,7 @@ import { isRecord } from './selection'; export interface ASTViewerProps { ast: Record | TSESTree.Node | string; position?: Monaco.Position | null; - onSelectNode: (node: SelectedRange | null) => void; + onSelectNode?: (node: SelectedRange | null) => void; } function ASTViewer(props: ASTViewerProps): JSX.Element { diff --git a/packages/website/src/components/ast/Elements.tsx b/packages/website/src/components/ast/Elements.tsx index e92d74e5466b..ed0da6d65e7c 100644 --- a/packages/website/src/components/ast/Elements.tsx +++ b/packages/website/src/components/ast/Elements.tsx @@ -94,13 +94,13 @@ export function ElementObject( const listItem = useRef(null); const onMouseEnter = useCallback(() => { - if (isEsNode(props.value)) { + if (props.onSelectNode && isEsNode(props.value)) { props.onSelectNode(props.value.loc); } }, [props.value]); const onMouseLeave = useCallback(() => { - if (isEsNode(props.value)) { + if (props.onSelectNode && isEsNode(props.value)) { props.onSelectNode(null); } }, [props.value]); @@ -138,7 +138,12 @@ export function ElementObject( onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} propName={props.propName} - name={props.value.type as string | undefined} + name={ + // TODO: refactor this + ((props.value.type || String(props.value.kind)) as + | string + | undefined) ?? '_' + } onClick={(): void => setIsExpanded(!isExpanded)} /> {'{'} diff --git a/packages/website/src/components/ast/PropertyValue.tsx b/packages/website/src/components/ast/PropertyValue.tsx index 3a22ca9010c2..23cbe7494e0f 100644 --- a/packages/website/src/components/ast/PropertyValue.tsx +++ b/packages/website/src/components/ast/PropertyValue.tsx @@ -1,5 +1,6 @@ import React from 'react'; import styles from './ASTViewer.module.css'; +import { objType } from './selection'; export interface PropertyValueProps { readonly value: unknown; @@ -25,5 +26,5 @@ export default function PropertyValue(props: PropertyValueProps): JSX.Element {
); } - return {String(props.value)}; + return {objType(props.value)}; } diff --git a/packages/website/src/components/ast/selection.ts b/packages/website/src/components/ast/selection.ts index 5f6e57efe5d2..2093cb4926d2 100644 --- a/packages/website/src/components/ast/selection.ts +++ b/packages/website/src/components/ast/selection.ts @@ -24,10 +24,18 @@ export function isWithinNode( return canStart && canEnd; } +export function objType(obj: unknown): string { + const type = Object.prototype.toString.call(obj).slice(8, -1); + // @ts-expect-error: this is correct check + if (type === 'Object' && obj && typeof obj[Symbol.iterator] === 'function') { + return 'Iterable'; + } + + return type; +} + export function isRecord(value: unknown): value is Record { - return Boolean( - typeof value === 'object' && value && value.constructor === Object, - ); + return objType(value) === 'Object'; } export function isEsNode( diff --git a/packages/website/src/components/ast/types.ts b/packages/website/src/components/ast/types.ts index f7eb9de5e08c..ee675d442d1d 100644 --- a/packages/website/src/components/ast/types.ts +++ b/packages/website/src/components/ast/types.ts @@ -6,5 +6,5 @@ export interface GenericParams { readonly value: V; readonly level: string; readonly selection?: SelectedPosition | null; - readonly onSelectNode: (node: SelectedRange | null) => void; + readonly onSelectNode?: (node: SelectedRange | null) => void; } diff --git a/packages/website/src/components/editor/LoadedEditor.tsx b/packages/website/src/components/editor/LoadedEditor.tsx index 5faeb5c96067..5a2180ba0fdc 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -21,7 +21,8 @@ export const LoadedEditor: React.FC = ({ decoration, jsx, main, - onASTChange, + onEsASTChange, + onTsASTChange, onChange, onSelect, rules, @@ -62,7 +63,8 @@ export const LoadedEditor: React.FC = ({ ); } - onASTChange(fatalMessage ?? webLinter.getAst()); + onEsASTChange(fatalMessage ?? webLinter.getAst()); + onTsASTChange(fatalMessage ?? webLinter.getTsAst()); onSelect(sandboxInstance.editor.getPosition()); }, 500), [code, jsx, sandboxInstance, rules, sourceType, webLinter], diff --git a/packages/website/src/components/editor/types.ts b/packages/website/src/components/editor/types.ts index 308400b15f8e..56048321473f 100644 --- a/packages/website/src/components/editor/types.ts +++ b/packages/website/src/components/editor/types.ts @@ -6,6 +6,7 @@ export interface CommonEditorProps extends ConfigModel { readonly darkTheme: boolean; readonly decoration: SelectedRange | null; readonly onChange: (value: string) => void; - readonly onASTChange: (value: string | TSESTree.Program) => void; + readonly onTsASTChange: (value: string | Record) => void; + readonly onEsASTChange: (value: string | TSESTree.Program) => void; readonly onSelect: (position: Monaco.Position | null) => void; } diff --git a/packages/website/src/components/hooks/useHashState.ts b/packages/website/src/components/hooks/useHashState.ts index b1263ce04ccb..58ffb030bd44 100644 --- a/packages/website/src/components/hooks/useHashState.ts +++ b/packages/website/src/components/hooks/useHashState.ts @@ -15,6 +15,16 @@ function readQueryParam(value: string | null, fallback: string): string { : fallback; } +function readShowAST(value: string | null): 'ts' | 'es' | boolean { + switch (value) { + case 'es': + return 'es'; + case 'ts': + return 'ts'; + } + return Boolean(value); +} + const parseStateFromUrl = (hash: string): ConfigModel | undefined => { if (!hash) { return; @@ -25,7 +35,8 @@ const parseStateFromUrl = (hash: string): ConfigModel | undefined => { return { ts: (searchParams.get('ts') ?? process.env.TS_VERSION).trim(), jsx: searchParams.has('jsx'), - showAST: searchParams.has('showAST'), + showAST: + searchParams.has('showAST') && readShowAST(searchParams.get('showAST')), sourceType: searchParams.has('sourceType') && searchParams.get('sourceType') === 'script' diff --git a/packages/website/src/components/types.ts b/packages/website/src/components/types.ts index 53a1f45d9728..7830baa224c5 100644 --- a/packages/website/src/components/types.ts +++ b/packages/website/src/components/types.ts @@ -42,7 +42,7 @@ export interface ConfigModel { tsConfig?: CompilerFlags; code: string; ts: string; - showAST?: boolean; + showAST?: boolean | 'ts' | 'es'; } export interface SelectedPosition { From db99a86450e3d01543d19872c06f639e0e4e3b5a Mon Sep 17 00:00:00 2001 From: Armano Date: Wed, 1 Dec 2021 17:11:50 +0100 Subject: [PATCH 03/14] docs(website): correct issues with rendering ast --- .../website/src/components/ASTEsViewer.tsx | 15 ++ .../website/src/components/ASTTsViewer.tsx | 19 ++ .../website/src/components/Playground.tsx | 14 +- .../src/components/ast/ASTViewer.module.css | 12 +- .../website/src/components/ast/ASTViewer.tsx | 28 ++- .../website/src/components/ast/Elements.tsx | 205 ++++++------------ .../website/src/components/ast/HiddenItem.tsx | 48 ++++ .../website/src/components/ast/ItemGroup.tsx | 50 +++++ .../src/components/ast/PropertyName.tsx | 11 +- .../src/components/ast/PropertyValue.tsx | 2 +- packages/website/src/components/ast/types.ts | 3 +- .../components/ast/{selection.ts => utils.ts} | 27 ++- 12 files changed, 250 insertions(+), 184 deletions(-) create mode 100644 packages/website/src/components/ASTEsViewer.tsx create mode 100644 packages/website/src/components/ASTTsViewer.tsx create mode 100644 packages/website/src/components/ast/HiddenItem.tsx create mode 100644 packages/website/src/components/ast/ItemGroup.tsx rename packages/website/src/components/ast/{selection.ts => utils.ts} (84%) diff --git a/packages/website/src/components/ASTEsViewer.tsx b/packages/website/src/components/ASTEsViewer.tsx new file mode 100644 index 000000000000..250f6522040c --- /dev/null +++ b/packages/website/src/components/ASTEsViewer.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import ASTViewer, { ASTViewerBaseProps } from './ast/ASTViewer'; +import { isRecord } from './ast/utils'; + +function getTypeName(value: unknown): string | undefined { + if (isRecord(value) && 'type' in value && value.type) { + return String(value.type); + } + return undefined; +} + +export default function ASTEsViewer(props: ASTViewerBaseProps): JSX.Element { + return ; +} diff --git a/packages/website/src/components/ASTTsViewer.tsx b/packages/website/src/components/ASTTsViewer.tsx new file mode 100644 index 000000000000..d9431699fa1e --- /dev/null +++ b/packages/website/src/components/ASTTsViewer.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import ASTViewer, { ASTViewerBaseProps } from './ast/ASTViewer'; +import { isRecord } from './ast/utils'; + +function getTypeName(value: unknown): string | undefined { + if ( + isRecord(value) && + typeof value.kind === 'number' && + window.ts.SyntaxKind[value.kind] + ) { + return String(window.ts.SyntaxKind[value.kind]); + } + return undefined; +} + +export default function ASTTsViewer(props: ASTViewerBaseProps): JSX.Element { + return ; +} diff --git a/packages/website/src/components/Playground.tsx b/packages/website/src/components/Playground.tsx index efc67bdb2787..d324311c28fa 100644 --- a/packages/website/src/components/Playground.tsx +++ b/packages/website/src/components/Playground.tsx @@ -8,11 +8,13 @@ import Loader from './layout/Loader'; import useHashState from './hooks/useHashState'; import OptionsSelector from './OptionsSelector'; -import ASTViewer from './ast/ASTViewer'; import { LoadingEditor } from './editor/LoadingEditor'; import { EditorEmbed } from './editor/EditorEmbed'; import { shallowEqual } from './lib/shallowEqual'; +import ASTEsViewer from './ASTEsViewer'; +import ASTTsViewer from './ASTTsViewer'; + import type { RuleDetails, SelectedRange } from './types'; import type { TSESTree } from '@typescript-eslint/website-eslint'; @@ -95,9 +97,15 @@ function Playground(): JSX.Element { {state.showAST && (
- {(tsAst && state.showAST === 'ts' && ) || + {(tsAst && state.showAST === 'ts' && ( + + )) || (esAst && ( - | TSESTree.Node | string; - position?: Monaco.Position | null; - onSelectNode?: (node: SelectedRange | null) => void; +export interface ASTViewerBaseProps { + readonly ast: Record | TSESTree.Node | string; + readonly position?: Monaco.Position | null; + readonly onSelectNode?: (node: SelectedRange | null) => void; +} + +export interface ASTViewerProps extends ASTViewerBaseProps { + readonly getTypeName: (data: unknown) => string | undefined; } function ASTViewer(props: ASTViewerProps): JSX.Element { - const [selection, setSelection] = useState(() => - props.position - ? { - line: props.position.lineNumber, - column: props.position.column - 1, - } - : null, - ); + const [selection, setSelection] = useState(null); useEffect(() => { setSelection( @@ -37,7 +34,8 @@ function ASTViewer(props: ASTViewerProps): JSX.Element { return isRecord(props.ast) ? (
- ): JSX.Element { - const [isComplex, setIsComplex] = useState(() => - isRecord(props.value), - ); +export function ComplexItem( + props: GenericParams | unknown[]>, +): JSX.Element { const [isExpanded, setIsExpanded] = useState( - () => - isComplex || props.value.some(item => isInRange(props.selection, item)), + () => props.level === 'ast', ); + const [isSelected, setIsSelected] = useState(false); + const [model, setModel] = useState<[string, unknown][]>([]); useEffect(() => { - setIsComplex( - props.value.some(item => typeof item === 'object' && item !== null), + setModel( + Object.entries(props.value).filter( + item => + !propsToFilter.includes(item[0]) && + !item[0].startsWith('_') && + item[1] !== undefined, + ), ); }, [props.value]); - useEffect(() => { - if (isComplex && !isExpanded) { - setIsExpanded(isArrayInRange(props.selection, props.value)); - } - }, [props.value, props.selection]); - - return ( -
- setIsExpanded(!isExpanded)} - /> - [ - {isExpanded ? ( -
- {props.value.map((item, index) => { - return ( - - ); - })} -
- ) : !isComplex ? ( - - {props.value.map((item, index) => ( - - {index > 0 && ', '} - - - ))} - - ) : ( - - {props.value.length} {props.value.length > 1 ? 'elements' : 'element'} - - )} - ] -
+ const onHover = useCallback( + (state: boolean) => { + if (props.onSelectNode && isEsNode(props.value)) { + props.onSelectNode(state ? props.value.loc : null); + } + }, + [props.value], ); -} - -export function ElementObject( - props: GenericParams>, -): JSX.Element { - const [isExpanded, setIsExpanded] = useState(() => { - return isInRange(props.selection, props.value); - }); - const [isSelected, setIsSelected] = useState( - () => - isInRange(props.selection, props.value) && props.value.type !== 'Program', - ); - const listItem = useRef(null); - - const onMouseEnter = useCallback(() => { - if (props.onSelectNode && isEsNode(props.value)) { - props.onSelectNode(props.value.loc); - } - }, [props.value]); - - const onMouseLeave = useCallback(() => { - if (props.onSelectNode && isEsNode(props.value)) { - props.onSelectNode(null); - } - }, [props.value]); useEffect(() => { - const selected = isInRange(props.selection, props.value); + const selected = props.isArray + ? isArrayInRange(props.selection, props.value) + : isInRange(props.selection, props.value); setIsSelected( - selected && - props.value.type !== 'Program' && - !hasChildInRange(props.selection, props.value), + props.level !== 'ast' && + selected && + !hasChildInRange(props.selection, model), ); if (selected && !isExpanded) { - setIsExpanded(isInRange(props.selection, props.value)); + setIsExpanded(selected); } - }, [props.selection, props.value]); - - useEffect(() => { - if (listItem.current && isSelected) { - scrollIntoViewIfNeeded(listItem.current); - } - }, [isSelected, listItem]); + }, [model, props.selection, props.value, props.isArray]); return ( -
setIsExpanded(!isExpanded)} > - setIsExpanded(!isExpanded)} - /> - {'{'} + {props.isArray ? '[' : '{'} {isExpanded ? (
- {filterRecord(props.value).map((item, index) => ( + {model.map((item, index) => ( ))}
) : ( - - {filterRecord(props.value) - .map(item => item[0]) - .join(', ')} - + )} - {'}'} -
+ {props.isArray ? ']' : '}'} + ); } export function ElementItem(props: GenericParams): JSX.Element { - if (Array.isArray(props.value)) { + const isArray = Array.isArray(props.value); + if (isArray || isRecord(props.value)) { return ( - - ); - } else if (isRecord(props.value)) { - return ( - ): JSX.Element { ); } return ( -
- {props.name && {props.name}} - {props.name && : } + -
+ ); } diff --git a/packages/website/src/components/ast/HiddenItem.tsx b/packages/website/src/components/ast/HiddenItem.tsx new file mode 100644 index 000000000000..8fed84aac512 --- /dev/null +++ b/packages/website/src/components/ast/HiddenItem.tsx @@ -0,0 +1,48 @@ +import React, { useEffect, useState } from 'react'; +import styles from '@site/src/components/ast/ASTViewer.module.css'; +import { PropertyValue } from '@site/src/components/ast/Elements'; + +export interface HiddenItemProps { + readonly value: [string, unknown][]; + readonly level: string; + readonly isArray?: boolean; +} + +export default function HiddenItem(props: HiddenItemProps): JSX.Element { + const [isComplex, setIsComplex] = useState(true); + + useEffect(() => { + setIsComplex( + Boolean( + props.isArray && + props.value.some( + item => typeof item[1] === 'object' && item[1] !== null, + ), + ), + ); + }, [props.value, props.isArray]); + + return ( + + {props.isArray && !isComplex ? ( + props.value.map((item, index) => ( + + {index > 0 && ', '} + + + )) + ) : props.isArray ? ( + <> + {props.value.length} {props.value.length > 1 ? 'elements' : 'element'} + + ) : ( + props.value.map((item, index) => ( + + {index > 0 && ', '} + {String(item[0])} + + )) + )} + + ); +} diff --git a/packages/website/src/components/ast/ItemGroup.tsx b/packages/website/src/components/ast/ItemGroup.tsx new file mode 100644 index 000000000000..060631b18b96 --- /dev/null +++ b/packages/website/src/components/ast/ItemGroup.tsx @@ -0,0 +1,50 @@ +import React, { MouseEvent, useEffect, useRef } from 'react'; +import { scrollIntoViewIfNeeded } from '@site/src/components/lib/scroll-into'; +import clsx from 'clsx'; +import styles from '@site/src/components/ast/ASTViewer.module.css'; + +import PropertyNameComp from '@site/src/components/ast/PropertyName'; + +const PropertyName = React.memo(PropertyNameComp); + +export interface ItemGroupProps { + readonly propName?: string; + readonly value: unknown; + readonly typeName: (data: unknown) => string | undefined; + readonly isSelected?: boolean; + readonly isExpanded?: boolean; + readonly canExpand?: boolean; + readonly onClick?: (e: MouseEvent) => void; + readonly onHover?: (e: boolean) => void; + readonly children: JSX.Element | (JSX.Element | false)[]; +} + +export default function ItemGroup(props: ItemGroupProps): JSX.Element { + const listItem = useRef(null); + + useEffect(() => { + if (listItem.current && props.isSelected) { + scrollIntoViewIfNeeded(listItem.current); + } + }, [props.isSelected, listItem]); + + return ( +
+ props.onHover?.(true)} + onMouseLeave={(): void => props.onHover?.(false)} + onClick={props.onClick} + /> + {React.Children.map(props.children, child => child)} +
+ ); +} diff --git a/packages/website/src/components/ast/PropertyName.tsx b/packages/website/src/components/ast/PropertyName.tsx index fa8614efe6c4..444327b51cb1 100644 --- a/packages/website/src/components/ast/PropertyName.tsx +++ b/packages/website/src/components/ast/PropertyName.tsx @@ -3,7 +3,7 @@ import clsx from 'clsx'; import styles from './ASTViewer.module.css'; export interface PropertyNameProps { - readonly name?: string; + readonly typeName?: string; readonly propName?: string; readonly onClick?: (e: MouseEvent) => void; readonly onMouseEnter?: (e: MouseEvent) => void; @@ -18,22 +18,23 @@ export default function PropertyName(props: PropertyNameProps): JSX.Element { onMouseEnter={props.onMouseEnter} onMouseLeave={props.onMouseLeave} onClick={props.onClick} - className={clsx(styles.propName, styles.clickable)} + className={clsx(styles.propName, props.onClick && styles.clickable)} > {props.propName} )} {props.propName && : } - {props.name && ( + {props.typeName && ( )} + {props.typeName && } ); } diff --git a/packages/website/src/components/ast/PropertyValue.tsx b/packages/website/src/components/ast/PropertyValue.tsx index 23cbe7494e0f..51f7e70ea438 100644 --- a/packages/website/src/components/ast/PropertyValue.tsx +++ b/packages/website/src/components/ast/PropertyValue.tsx @@ -1,6 +1,6 @@ import React from 'react'; import styles from './ASTViewer.module.css'; -import { objType } from './selection'; +import { objType } from './utils'; export interface PropertyValueProps { readonly value: unknown; diff --git a/packages/website/src/components/ast/types.ts b/packages/website/src/components/ast/types.ts index ee675d442d1d..61fddb587528 100644 --- a/packages/website/src/components/ast/types.ts +++ b/packages/website/src/components/ast/types.ts @@ -2,9 +2,10 @@ import type { SelectedPosition, SelectedRange } from '../types'; export interface GenericParams { readonly propName?: string; - readonly name?: string; readonly value: V; readonly level: string; readonly selection?: SelectedPosition | null; readonly onSelectNode?: (node: SelectedRange | null) => void; + readonly getTypeName: (data: unknown) => string | undefined; + readonly isArray?: boolean; } diff --git a/packages/website/src/components/ast/selection.ts b/packages/website/src/components/ast/utils.ts similarity index 84% rename from packages/website/src/components/ast/selection.ts rename to packages/website/src/components/ast/utils.ts index 2093cb4926d2..32b0642c04e0 100644 --- a/packages/website/src/components/ast/selection.ts +++ b/packages/website/src/components/ast/utils.ts @@ -1,15 +1,19 @@ import type { TSESTree } from '@typescript-eslint/website-eslint'; import type { SelectedPosition } from '../types'; -export const propsToFilter = ['parent', 'comments', 'tokens', 'loc']; - -export function filterRecord( - values: Record, -): [string, unknown][] { - return Object.entries(values).filter( - item => !propsToFilter.includes(item[0]), - ); -} +export const propsToFilter = [ + 'parent', + 'comments', + 'tokens', + // 'loc', + 'jsDoc', + 'lineMap', + 'externalModuleIndicator', + 'bindDiagnostics', + 'modifierFlagsCache', + 'transformFlags', + 'resolvedModules', +]; export function isWithinNode( loc: SelectedPosition, @@ -68,12 +72,11 @@ export function isArrayInRange( export function hasChildInRange( position: SelectedPosition | null | undefined, - value: unknown, + value: [string, unknown][], ): boolean { return Boolean( position && - isEsNode(value) && - filterRecord(value).some( + value.some( ([, item]) => isInRange(position, item) || isArrayInRange(position, item), ), From 3b496eb20a08e1d1a3b4999f0c9ebefef87de76e Mon Sep 17 00:00:00 2001 From: Armano Date: Thu, 2 Dec 2021 09:33:43 +0100 Subject: [PATCH 04/14] docs(website): filter out SyntaxKind --- .../website/src/components/ASTTsViewer.tsx | 27 ++++++++++++++----- .../src/components/OptionsSelector.tsx | 4 +-- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/website/src/components/ASTTsViewer.tsx b/packages/website/src/components/ASTTsViewer.tsx index d9431699fa1e..6f31b9787a1d 100644 --- a/packages/website/src/components/ASTTsViewer.tsx +++ b/packages/website/src/components/ASTTsViewer.tsx @@ -3,13 +3,28 @@ import React from 'react'; import ASTViewer, { ASTViewerBaseProps } from './ast/ASTViewer'; import { isRecord } from './ast/utils'; +const cache: Record> = {}; + +export function getSyntaxKind(): Record { + if (!cache[window.ts.version]) { + const result: Record = {}; + const keys = Object.keys(window.ts.SyntaxKind).filter(k => + isNaN(parseInt(k, 10)), + ); + for (const name of keys) { + const value = Number(window.ts.SyntaxKind[name]); + if (!(value in result)) { + result[value] = name; + } + } + cache[window.ts.version] = result; + } + return cache[window.ts.version]; +} + function getTypeName(value: unknown): string | undefined { - if ( - isRecord(value) && - typeof value.kind === 'number' && - window.ts.SyntaxKind[value.kind] - ) { - return String(window.ts.SyntaxKind[value.kind]); + if (isRecord(value) && typeof value.kind === 'number') { + return getSyntaxKind()[value.kind]; } return undefined; } diff --git a/packages/website/src/components/OptionsSelector.tsx b/packages/website/src/components/OptionsSelector.tsx index 4e58b9525b29..c9baf694d7ea 100644 --- a/packages/website/src/components/OptionsSelector.tsx +++ b/packages/website/src/components/OptionsSelector.tsx @@ -132,7 +132,7 @@ function OptionsSelector({ />