From dc3ddde6813e995a6d7e9818296f6882fa4b5c2a Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 20 Aug 2024 13:56:15 -0400 Subject: [PATCH] chore: enable eslint-plugin-perfectionist on website packages --- eslint.config.mjs | 2 +- .../src/components/ast/selectedRange.ts | 7 +- .../website/src/components/ast/tsUtils.ts | 26 ++++---- packages/website/src/components/ast/utils.ts | 65 ++++++++++--------- .../editor/createProvideCodeActions.ts | 7 +- .../editor/createProvideTwoslashInlay.ts | 8 +-- .../src/components/editor/loadSandbox.ts | 8 +-- .../website/src/components/editor/types.ts | 4 +- .../components/editor/useSandboxServices.ts | 40 ++++++------ .../src/components/hooks/useHashState.ts | 23 +++---- .../components/lib/createCompilerOptions.ts | 10 +-- .../src/components/lib/createEventsBinder.ts | 8 +-- .../website/src/components/lib/jsonSchema.ts | 46 ++++++------- .../website/src/components/lib/markdown.ts | 7 +- .../website/src/components/lib/parseConfig.ts | 3 +- .../website/src/components/lib/scroll-into.ts | 6 +- .../website/src/components/linter/bridge.ts | 3 +- .../website/src/components/linter/config.ts | 2 +- .../src/components/linter/createLinter.ts | 39 +++++------ .../src/components/linter/createParser.ts | 23 +++---- .../website/src/components/linter/types.ts | 20 +++--- .../website/src/components/linter/utils.ts | 40 ++++++------ packages/website/src/components/options.ts | 27 ++++---- packages/website/src/components/types.ts | 34 +++++----- packages/website/src/globals.d.ts | 6 +- packages/website/src/hooks/useBool.ts | 3 +- .../website/src/hooks/useHistorySelector.ts | 3 +- packages/website/src/hooks/useRulesMeta.ts | 3 +- 28 files changed, 243 insertions(+), 230 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 60b302d8cb50..c5faa02b831f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -578,7 +578,7 @@ export default tseslint.config( }, { extends: [perfectionistPlugin.configs['recommended-alphabetical']], - files: ['packages/utils/src/**/*.ts'], + files: ['packages/utils/src/**/*.ts', 'packages/website*/src/**/*.ts'], rules: { 'perfectionist/sort-classes': [ 'error', diff --git a/packages/website/src/components/ast/selectedRange.ts b/packages/website/src/components/ast/selectedRange.ts index a92da6a801b0..573d9c8166c8 100644 --- a/packages/website/src/components/ast/selectedRange.ts +++ b/packages/website/src/components/ast/selectedRange.ts @@ -1,4 +1,5 @@ import type { ParentNodeType } from './types'; + import { filterProperties, isESNode, isRecord, isTSNode } from './utils'; function isInRange(offset: number, value: object): boolean { @@ -70,7 +71,7 @@ function findInObject( export function findSelectionPath( node: object, cursorPosition: number, -): { path: string[]; node: object | null } { +): { node: object | null; path: string[] } { const nodePath = ['ast']; const visited = new Set(); let currentNode: unknown = node; @@ -86,8 +87,8 @@ export function findSelectionPath( currentNode = result.value; nodePath.push(...result.key); } else { - return { path: nodePath, node: currentNode }; + return { node: currentNode, path: nodePath }; } } - return { path: nodePath, node: null }; + return { node: null, path: nodePath }; } diff --git a/packages/website/src/components/ast/tsUtils.ts b/packages/website/src/components/ast/tsUtils.ts index e75905bd72fe..70d8f63fbd4d 100644 --- a/packages/website/src/components/ast/tsUtils.ts +++ b/packages/website/src/components/ast/tsUtils.ts @@ -1,15 +1,15 @@ interface TsParsedEnums { - SyntaxKind: Record; - NodeFlags: Record; - TokenFlags: Record; + LanguageVariant: Record; ModifierFlags: Record; + NodeFlags: Record; ObjectFlags: Record; - SymbolFlags: Record; - TypeFlags: Record; ScriptKind: Record; - TransformFlags: Record; ScriptTarget: Record; - LanguageVariant: Record; + SymbolFlags: Record; + SyntaxKind: Record; + TokenFlags: Record; + TransformFlags: Record; + TypeFlags: Record; } /** @@ -39,16 +39,16 @@ let tsEnumCache: TsParsedEnums | undefined; */ function getTsEnum(type: keyof TsParsedEnums): Record { tsEnumCache ??= { - SyntaxKind: extractEnum(window.ts.SyntaxKind), - NodeFlags: extractEnum(window.ts.NodeFlags), - TokenFlags: extractEnum(window.ts.TokenFlags), + LanguageVariant: extractEnum(window.ts.LanguageVariant), ModifierFlags: extractEnum(window.ts.ModifierFlags), + NodeFlags: extractEnum(window.ts.NodeFlags), ObjectFlags: extractEnum(window.ts.ObjectFlags), - SymbolFlags: extractEnum(window.ts.SymbolFlags), - TypeFlags: extractEnum(window.ts.TypeFlags), ScriptKind: extractEnum(window.ts.ScriptKind), ScriptTarget: extractEnum(window.ts.ScriptTarget), - LanguageVariant: extractEnum(window.ts.LanguageVariant), + SymbolFlags: extractEnum(window.ts.SymbolFlags), + SyntaxKind: extractEnum(window.ts.SyntaxKind), + TokenFlags: extractEnum(window.ts.TokenFlags), + TypeFlags: extractEnum(window.ts.TypeFlags), // @ts-expect-error: non public API // eslint-disable-next-line @typescript-eslint/no-unsafe-argument TransformFlags: extractEnum(window.ts.TransformFlags), diff --git a/packages/website/src/components/ast/utils.ts b/packages/website/src/components/ast/utils.ts index 9812bc6309a3..b85cff3cc4cc 100644 --- a/packages/website/src/components/ast/utils.ts +++ b/packages/website/src/components/ast/utils.ts @@ -1,9 +1,10 @@ import type { TSESTree } from '@typescript-eslint/utils'; import type * as ts from 'typescript'; -import { tsEnumFlagToString, tsEnumToString } from './tsUtils'; import type { ParentNodeType } from './types'; +import { tsEnumFlagToString, tsEnumToString } from './tsUtils'; + export function objType(obj: unknown): string { const type = Object.prototype.toString.call(obj).slice(8, -1); if (type === 'Object' && obj && typeof obj[Symbol.iterator] === 'function') { @@ -79,26 +80,26 @@ export function getTypeName( switch (valueType) { case 'esNode': return String(value.type); - case 'tsNode': - return tsEnumToString('SyntaxKind', Number(value.kind)); - case 'scopeManager': - return 'ScopeManager'; case 'scope': return `${ucFirst(String(value.type))}Scope$${String(value.$id)}`; case 'scopeDefinition': return `Definition#${String(value.type)}$${String(value.$id)}`; - case 'scopeVariable': - return `Variable#${String(value.name)}$${String(value.$id)}`; + case 'scopeManager': + return 'ScopeManager'; case 'scopeReference': return `Reference#${String( isRecord(value.identifier) ? value.identifier.name : 'unknown', )}$${String(value.$id)}`; - case 'tsType': - return '[Type]'; - case 'tsSymbol': - return `Symbol(${String(value.escapedName)})`; + case 'scopeVariable': + return `Variable#${String(value.name)}$${String(value.$id)}`; + case 'tsNode': + return tsEnumToString('SyntaxKind', Number(value.kind)); case 'tsSignature': return '[Signature]'; + case 'tsSymbol': + return `Symbol(${String(value.escapedName)})`; + case 'tsType': + return '[Type]'; } return undefined; } @@ -114,23 +115,23 @@ export function getTooltipLabel( switch (propName) { case 'flags': return tsEnumFlagToString('NodeFlags', value); - case 'numericLiteralFlags': - return tsEnumFlagToString('TokenFlags', value); - case 'modifierFlagsCache': - return tsEnumFlagToString('ModifierFlags', value); - case 'scriptKind': - return `ScriptKind.${tsEnumToString('ScriptKind', value)}`; - case 'transformFlags': - return tsEnumFlagToString('TransformFlags', value); case 'kind': return `SyntaxKind.${tsEnumToString('SyntaxKind', value)}`; - case 'languageVersion': - return `ScriptTarget.${tsEnumToString('ScriptTarget', value)}`; case 'languageVariant': return `LanguageVariant.${tsEnumToString( 'LanguageVariant', value, )}`; + case 'languageVersion': + return `ScriptTarget.${tsEnumToString('ScriptTarget', value)}`; + case 'modifierFlagsCache': + return tsEnumFlagToString('ModifierFlags', value); + case 'numericLiteralFlags': + return tsEnumFlagToString('TokenFlags', value); + case 'scriptKind': + return `ScriptKind.${tsEnumToString('ScriptKind', value)}`; + case 'transformFlags': + return tsEnumFlagToString('TransformFlags', value); } break; } @@ -170,22 +171,11 @@ export function getRange( switch (valueType) { case 'esNode': return getValidRange(value.range); - case 'tsNode': - return getValidRange([value.pos, value.end]); case 'scope': if (isRecord(value.block)) { return getValidRange(value.block.range); } break; - case 'scopeVariable': - if ( - Array.isArray(value.identifiers) && - value.identifiers.length > 0 && - isRecord(value.identifiers[0]) - ) { - return getValidRange(value.identifiers[0].range); - } - break; case 'scopeDefinition': if (isRecord(value.node)) { return getValidRange(value.node.range); @@ -196,6 +186,17 @@ export function getRange( return getValidRange(value.identifier.range); } break; + case 'scopeVariable': + if ( + Array.isArray(value.identifiers) && + value.identifiers.length > 0 && + isRecord(value.identifiers[0]) + ) { + return getValidRange(value.identifiers[0].range); + } + break; + case 'tsNode': + return getValidRange([value.pos, value.end]); } } return undefined; diff --git a/packages/website/src/components/editor/createProvideCodeActions.ts b/packages/website/src/components/editor/createProvideCodeActions.ts index a5eae9f849a9..51333336d03e 100644 --- a/packages/website/src/components/editor/createProvideCodeActions.ts +++ b/packages/website/src/components/editor/createProvideCodeActions.ts @@ -1,6 +1,7 @@ import type Monaco from 'monaco-editor'; import type { LintCodeAction } from '../linter/utils'; + import { createEditOperation, createURI } from '../linter/utils'; export function createProvideCodeActions( @@ -26,10 +27,7 @@ export function createProvideCodeActions( 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: [ { @@ -41,6 +39,9 @@ export function createProvideCodeActions( }, ], }, + isPreferred: message.isPreferred, + kind: 'quickfix', + title: message.message + (message.code ? ` (${message.code})` : ''), }); } } diff --git a/packages/website/src/components/editor/createProvideTwoslashInlay.ts b/packages/website/src/components/editor/createProvideTwoslashInlay.ts index 04036bbe86b5..e775e80e519a 100644 --- a/packages/website/src/components/editor/createProvideTwoslashInlay.ts +++ b/packages/website/src/components/editor/createProvideTwoslashInlay.ts @@ -30,10 +30,10 @@ export function createTwoslashInlayProvider( const worker = await sandbox.getWorkerProcess(); if (model.isDisposed() || cancel.isCancellationRequested) { return { - hints: [], dispose(): void { /* nop */ }, + hints: [], }; } @@ -50,10 +50,10 @@ export function createTwoslashInlayProvider( } return { - hints: results, dispose(): void { /* nop */ }, + hints: results, }; async function resolveInlayHint( @@ -85,12 +85,12 @@ export function createTwoslashInlayProvider( return { kind: sandbox.monaco.languages.InlayHintKind.Type, + label: text, + paddingLeft: true, position: new sandbox.monaco.Position( endPos.lineNumber, endPos.column + 1, ), - label: text, - paddingLeft: true, }; } }, diff --git a/packages/website/src/components/editor/loadSandbox.ts b/packages/website/src/components/editor/loadSandbox.ts index f7798a4093eb..84e4793a47a7 100644 --- a/packages/website/src/components/editor/loadSandbox.ts +++ b/packages/website/src/components/editor/loadSandbox.ts @@ -7,9 +7,9 @@ type Monaco = typeof MonacoEditor; type Sandbox = typeof SandboxFactory; export interface SandboxModel { + lintUtils: WebLinterModule; main: Monaco; sandboxFactory: Sandbox; - lintUtils: WebLinterModule; } function loadSandbox(tsVersion: string): Promise { @@ -23,9 +23,9 @@ function loadSandbox(tsVersion: string): Promise { // 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', + sandbox: 'https://www.typescriptlang.org/js/sandbox', + vs: `https://typescript.azureedge.net/cdn/${tsVersion}/monaco/min/vs`, }, // This is something you need for monaco to work ignoreDuplicateModules: ['vs/editor/editor.main'], @@ -35,7 +35,7 @@ function loadSandbox(tsVersion: string): Promise { window.require<[Monaco, Sandbox, WebLinterModule]>( ['vs/editor/editor.main', 'sandbox/index', 'linter/index'], (main, sandboxFactory, lintUtils) => { - resolve({ main, sandboxFactory, lintUtils }); + resolve({ lintUtils, main, sandboxFactory }); }, () => { reject( diff --git a/packages/website/src/components/editor/types.ts b/packages/website/src/components/editor/types.ts index e8933ce19f42..3b8b41a67944 100644 --- a/packages/website/src/components/editor/types.ts +++ b/packages/website/src/components/editor/types.ts @@ -3,9 +3,9 @@ import type { ConfigModel, ErrorGroup, SelectedRange, TabType } from '../types'; export interface CommonEditorProps extends ConfigModel { readonly activeTab: TabType; - readonly selectedRange?: SelectedRange; + readonly onASTChange: (value: UpdateModel | undefined) => void; readonly onChange: (cfg: Partial) => void; - readonly onASTChange: (value: undefined | UpdateModel) => void; readonly onMarkersChange: (value: ErrorGroup[]) => void; readonly onSelect: (position?: number) => void; + readonly selectedRange?: SelectedRange; } diff --git a/packages/website/src/components/editor/useSandboxServices.ts b/packages/website/src/components/editor/useSandboxServices.ts index 7e2eab3ef2a3..fac8e1838fa0 100644 --- a/packages/website/src/components/editor/useSandboxServices.ts +++ b/packages/website/src/components/editor/useSandboxServices.ts @@ -1,21 +1,23 @@ -import { useColorMode } from '@docusaurus/theme-common'; import type * as Monaco from 'monaco-editor'; + +import { useColorMode } from '@docusaurus/theme-common'; import { useEffect, useState } from 'react'; import semverSatisfies from 'semver/functions/satisfies'; +import type { createTypeScriptSandbox } from '../../vendor/sandbox'; +import type { CreateLinter } from '../linter/createLinter'; +import type { PlaygroundSystem } from '../linter/types'; +import type { RuleDetails } from '../types'; +import type { CommonEditorProps } from './types'; + // eslint-disable-next-line @typescript-eslint/internal/no-relative-paths-to-internal-packages import rootPackageJson from '../../../../../package.json'; -import type { createTypeScriptSandbox } from '../../vendor/sandbox'; import { createCompilerOptions } from '../lib/createCompilerOptions'; import { createFileSystem } from '../linter/bridge'; -import type { CreateLinter } from '../linter/createLinter'; import { createLinter } from '../linter/createLinter'; -import type { PlaygroundSystem } from '../linter/types'; -import type { RuleDetails } from '../types'; import { createTwoslashInlayProvider } from './createProvideTwoslashInlay'; import { editorEmbedId } from './EditorEmbed'; import { sandboxSingleton } from './loadSandbox'; -import type { CommonEditorProps } from './types'; export interface SandboxServicesProps { readonly onLoaded: ( @@ -44,28 +46,28 @@ export const useSandboxServices = ( let sandboxInstance: SandboxInstance | undefined; sandboxSingleton(props.ts) - .then(async ({ main, sandboxFactory, lintUtils }) => { + .then(async ({ lintUtils, main, sandboxFactory }) => { const compilerOptions = createCompilerOptions(); sandboxInstance = sandboxFactory.createTypeScriptSandbox( { - text: props.code, + acquireTypes: true, + compilerOptions: + compilerOptions as Monaco.languages.typescript.CompilerOptions, + domID: editorEmbedId, monacoSettings: { - minimap: { enabled: false }, - fontSize: 13, - wordWrap: 'off', - scrollBeyondLastLine: false, - smoothScrolling: true, autoIndent: 'full', + fontSize: 13, formatOnPaste: true, formatOnType: true, - wrappingIndent: 'same', hover: { above: false }, + minimap: { enabled: false }, + scrollBeyondLastLine: false, + smoothScrolling: true, + wordWrap: 'off', + wrappingIndent: 'same', }, - acquireTypes: true, - compilerOptions: - compilerOptions as Monaco.languages.typescript.CompilerOptions, - domID: editorEmbedId, + text: props.code, }, main, window.ts, @@ -129,9 +131,9 @@ export const useSandboxServices = ( ); setServices({ + sandboxInstance, system, webLinter, - sandboxInstance, }); }) .catch((err: unknown) => { diff --git a/packages/website/src/components/hooks/useHashState.ts b/packages/website/src/components/hooks/useHashState.ts index b8553d3cb158..593b4769c57c 100644 --- a/packages/website/src/components/hooks/useHashState.ts +++ b/packages/website/src/components/hooks/useHashState.ts @@ -2,11 +2,12 @@ import { useHistory } from '@docusaurus/router'; import * as lz from 'lz-string'; import { useCallback, useState } from 'react'; +import type { ConfigFileType, ConfigModel, ConfigShowAst } from '../types'; + import { hasOwnProperty } from '../lib/has-own-property'; import { toJsonConfig } from '../lib/json'; import { shallowEqual } from '../lib/shallowEqual'; import { fileTypes } from '../options'; -import type { ConfigFileType, ConfigModel, ConfigShowAst } from '../types'; function writeQueryParam(value: string | null): string { return (value && lz.compressToEncodedURIComponent(value)) ?? ''; @@ -19,8 +20,8 @@ function readQueryParam(value: string | null, fallback: string): string { function readShowAST(value: string | null): ConfigShowAst { switch (value) { case 'es': - case 'ts': case 'scope': + case 'ts': case 'types': return value; } @@ -91,16 +92,16 @@ const parseStateFromUrl = ( : ''; return { - ts: searchParams.get('ts') ?? process.env.TS_VERSION, + code, + eslintrc: eslintrc ?? initialState.eslintrc, + esQuery, + fileType, showAST: readShowAST(searchParams.get('showAST')), + showTokens: searchParams.get('tokens') === 'true', sourceType: searchParams.get('sourceType') === 'script' ? 'script' : 'module', - code, - fileType, - eslintrc: eslintrc ?? initialState.eslintrc, + ts: searchParams.get('ts') ?? process.env.TS_VERSION, tsconfig: tsconfig ?? initialState.tsconfig, - showTokens: searchParams.get('tokens') === 'true', - esQuery, }; } catch (e) { console.warn(e); @@ -180,11 +181,11 @@ const retrieveStateFromLocalStorage = (): Partial | undefined => { const writeStateToLocalStorage = (newState: ConfigModel): void => { const config: Partial = { - ts: newState.ts, fileType: newState.fileType, - sourceType: newState.sourceType, - showAST: newState.showAST, scroll: newState.scroll, + showAST: newState.showAST, + sourceType: newState.sourceType, + ts: newState.ts, }; window.localStorage.setItem('config', JSON.stringify(config)); }; diff --git a/packages/website/src/components/lib/createCompilerOptions.ts b/packages/website/src/components/lib/createCompilerOptions.ts index 33540cc1b3e5..9223920f981f 100644 --- a/packages/website/src/components/lib/createCompilerOptions.ts +++ b/packages/website/src/components/lib/createCompilerOptions.ts @@ -8,18 +8,18 @@ export function createCompilerOptions( ): ts.CompilerOptions { const config = window.ts.convertCompilerOptionsFromJson( { - target: 'esnext', - module: 'esnext', jsx: 'preserve', + module: 'esnext', + target: 'esnext', ...tsConfig, allowJs: true, + baseUrl: undefined, lib: Array.isArray(tsConfig.lib) ? tsConfig.lib : undefined, + moduleDetection: undefined, moduleResolution: undefined, + paths: undefined, plugins: undefined, typeRoots: undefined, - paths: undefined, - moduleDetection: undefined, - baseUrl: undefined, }, '/tsconfig.json', ); diff --git a/packages/website/src/components/lib/createEventsBinder.ts b/packages/website/src/components/lib/createEventsBinder.ts index 6b5bfaecbee1..4d3e3f3d12ae 100644 --- a/packages/website/src/components/lib/createEventsBinder.ts +++ b/packages/website/src/components/lib/createEventsBinder.ts @@ -1,19 +1,19 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function createEventsBinder void>(): { - trigger: (...args: Parameters) => void; register: (cb: T) => () => void; + trigger: (...args: Parameters) => void; } { const events = new Set(); return { - trigger(...args: Parameters): void { - events.forEach(cb => cb(...args)); - }, register(cb: T): () => void { events.add(cb); return (): void => { events.delete(cb); }; }, + trigger(...args: Parameters): void { + events.forEach(cb => cb(...args)); + }, }; } diff --git a/packages/website/src/components/lib/jsonSchema.ts b/packages/website/src/components/lib/jsonSchema.ts index f8870f61b241..7b63eec715bd 100644 --- a/packages/website/src/components/lib/jsonSchema.ts +++ b/packages/website/src/components/lib/jsonSchema.ts @@ -4,8 +4,8 @@ import type * as ts from 'typescript'; import type { CreateLinter } from '../linter/createLinter'; const defaultRuleSchema: JSONSchema4 = { - type: ['string', 'number'], enum: ['off', 'warn', 'error', 0, 1, 2], + type: ['string', 'number'], }; // https://github.com/microsoft/TypeScript/issues/17002 @@ -31,10 +31,10 @@ export function getRuleJsonSchemaWithErrorLevel( defaultRuleSchemaCopy.$defs = ruleSchema[0].$defs; } return { - type: 'array', + additionalItems: false, items: [defaultRuleSchemaCopy, ...ruleSchema], minItems: 1, - additionalItems: false, + type: 'array', }; } if ('items' in ruleSchema) { @@ -42,22 +42,22 @@ export function getRuleJsonSchemaWithErrorLevel( if (isArray(ruleSchema.items)) { return { ...ruleSchema, - type: 'array', + additionalItems: false, items: [defaultRuleSchema, ...ruleSchema.items], maxItems: ruleSchema.maxItems ? ruleSchema.maxItems + 1 : undefined, minItems: ruleSchema.minItems ? ruleSchema.minItems + 1 : 1, - additionalItems: false, + type: 'array', }; } // example: naming-convention rule if (typeof ruleSchema.items === 'object') { return { ...ruleSchema, - type: 'array', + additionalItems: ruleSchema.items, items: [defaultRuleSchema], maxItems: ruleSchema.maxItems ? ruleSchema.maxItems + 1 : undefined, minItems: ruleSchema.minItems ? ruleSchema.minItems + 1 : 1, - additionalItems: ruleSchema.items, + type: 'array', }; } } @@ -84,10 +84,10 @@ export function getRuleJsonSchemaWithErrorLevel( console.error('unsupported rule schema', name, ruleSchema); } return { - type: 'array', + additionalItems: false, items: [defaultRuleSchema], minItems: 1, - additionalItems: false, + type: 'array', }; } @@ -103,38 +103,38 @@ export function getEslintJsonSchema( for (const [, item] of linter.rules) { properties[item.name] = { - description: `${item.description}\n ${item.url}`, - title: item.name.startsWith('@typescript') ? 'Rules' : 'Core rules', default: 'off', + description: `${item.description}\n ${item.url}`, oneOf: [defaultRuleSchema, { $ref: createRef(item.name) }], + title: item.name.startsWith('@typescript') ? 'Rules' : 'Core rules', }; } return { - type: 'object', properties: { extends: { oneOf: [ { type: 'string' }, { + items: { enum: linter.configs, type: 'string' }, type: 'array', - items: { type: 'string', enum: linter.configs }, uniqueItems: true, }, ], }, rules: { - type: 'object', - properties, additionalProperties: false, + properties, + type: 'object', }, }, + type: 'object', }; } export interface DescribedOptionDeclaration extends ts.OptionDeclarations { - description: NonNullable; category: NonNullable; + description: NonNullable; } /** @@ -179,35 +179,35 @@ export function getTypescriptJsonSchema(): JSONSchema4 { const properties = getTypescriptOptions().reduce((options, item) => { if (item.type === 'boolean') { options[item.name] = { - type: 'boolean', description: item.description.message, + type: 'boolean', }; } else if (item.type === 'list' && item.element?.type instanceof Map) { options[item.name] = { - type: 'array', + description: item.description.message, items: { - type: 'string', enum: Array.from(item.element.type.keys()), + type: 'string', }, - description: item.description.message, + type: 'array', }; } else if (item.type instanceof Map) { options[item.name] = { - type: 'string', description: item.description.message, enum: Array.from(item.type.keys()), + type: 'string', }; } return options; }, {}); return { - type: 'object', properties: { compilerOptions: { - type: 'object', properties, + type: 'object', }, }, + type: 'object', }; } diff --git a/packages/website/src/components/lib/markdown.ts b/packages/website/src/components/lib/markdown.ts index 7e9faaccc02b..f654ba1111a2 100644 --- a/packages/website/src/components/lib/markdown.ts +++ b/packages/website/src/components/lib/markdown.ts @@ -1,4 +1,5 @@ import type { ConfigModel } from '../types'; + import { parseESLintRC } from './parseConfig'; function createSummary( @@ -61,12 +62,12 @@ export function createMarkdownParams(state: ConfigModel): string { : 'rule name here'; const params = { + 'eslint-config': `module.exports = ${state.eslintrc}`, labels: 'bug,package: eslint-plugin,triage', - template: '01-bug-report-plugin.yaml', - title: `Bug: [${onlyRuleName}] `, 'playground-link': document.location.toString(), 'repro-code': state.code, - 'eslint-config': `module.exports = ${state.eslintrc}`, + template: '01-bug-report-plugin.yaml', + title: `Bug: [${onlyRuleName}] `, 'typescript-config': state.tsconfig, versions: generateVersionsTable(state.ts), }; diff --git a/packages/website/src/components/lib/parseConfig.ts b/packages/website/src/components/lib/parseConfig.ts index c38c553be134..2915e9f350db 100644 --- a/packages/website/src/components/lib/parseConfig.ts +++ b/packages/website/src/components/lib/parseConfig.ts @@ -1,5 +1,6 @@ -import { isRecord } from '../ast/utils'; import type { EslintRC, TSConfig } from '../types'; + +import { isRecord } from '../ast/utils'; import { ensureObject, parseJSONObject, toJson } from './json'; /** diff --git a/packages/website/src/components/lib/scroll-into.ts b/packages/website/src/components/lib/scroll-into.ts index d21dca1d6af4..0eb0b5500d15 100644 --- a/packages/website/src/components/lib/scroll-into.ts +++ b/packages/website/src/components/lib/scroll-into.ts @@ -7,27 +7,27 @@ export function scrollIntoViewIfNeeded(target: Element): void { const isAbove = rect.bottom > window.innerHeight; if ((isAbove && isBelow) || rect.height > window.innerHeight) { target.scrollIntoView({ + behavior: 'smooth', block: 'start', inline: 'start', - behavior: 'smooth', }); return; } // Target is outside the viewport from the bottom if (isAbove) { target.scrollIntoView({ + behavior: 'smooth', block: 'center', inline: 'center', - behavior: 'smooth', }); return; } // Target is outside the view from the top if (isBelow) { target.scrollIntoView({ + behavior: 'smooth', block: 'center', inline: 'center', - behavior: 'smooth', }); return; } diff --git a/packages/website/src/components/linter/bridge.ts b/packages/website/src/components/linter/bridge.ts index a553b7d7f283..414873484c1a 100644 --- a/packages/website/src/components/linter/bridge.ts +++ b/packages/website/src/components/linter/bridge.ts @@ -1,9 +1,10 @@ import type * as tsvfs from '@site/src/vendor/typescript-vfs'; import type * as ts from 'typescript'; -import { debounce } from '../lib/debounce'; import type { ConfigModel } from '../types'; import type { PlaygroundSystem } from './types'; + +import { debounce } from '../lib/debounce'; import { getPathRegExp } from './utils'; export function createFileSystem( diff --git a/packages/website/src/components/linter/config.ts b/packages/website/src/components/linter/config.ts index b1fa526b9d97..c23c4a536c54 100644 --- a/packages/website/src/components/linter/config.ts +++ b/packages/website/src/components/linter/config.ts @@ -36,8 +36,8 @@ export const defaultEslintConfig: ClassicConfig.Config = { parser: PARSER_NAME, parserOptions: { ecmaFeatures: { - jsx: true, globalReturn: false, + jsx: true, }, ecmaVersion: 'latest', project: ['./tsconfig.json'], diff --git a/packages/website/src/components/linter/createLinter.ts b/packages/website/src/components/linter/createLinter.ts index ea0a1abc0b5a..0d3eea31794d 100644 --- a/packages/website/src/components/linter/createLinter.ts +++ b/packages/website/src/components/linter/createLinter.ts @@ -7,11 +7,6 @@ import type { } from '@typescript-eslint/utils/ts-eslint'; import type * as ts from 'typescript'; -import { createCompilerOptions } from '../lib/createCompilerOptions'; -import { createEventsBinder } from '../lib/createEventsBinder'; -import { parseESLintRC, parseTSConfig } from '../lib/parseConfig'; -import { defaultEslintConfig, PARSER_NAME } from './config'; -import { createParser } from './createParser'; import type { LinterOnLint, LinterOnParse, @@ -19,21 +14,27 @@ import type { WebLinterModule, } from './types'; +import { createCompilerOptions } from '../lib/createCompilerOptions'; +import { createEventsBinder } from '../lib/createEventsBinder'; +import { parseESLintRC, parseTSConfig } from '../lib/parseConfig'; +import { defaultEslintConfig, PARSER_NAME } from './config'; +import { createParser } from './createParser'; + export interface CreateLinter { + configs: string[]; + onLint(cb: LinterOnLint): () => void; + onParse(cb: LinterOnParse): () => void; rules: Map< string, { - name: string; description?: string; - url?: string; + name: string; schema: JSONSchema.JSONSchema4 | readonly JSONSchema.JSONSchema4[]; + url?: string; } >; - configs: string[]; triggerFix(filename: string): Linter.FixReport | undefined; triggerLint(filename: string): void; - onLint(cb: LinterOnLint): () => void; - onParse(cb: LinterOnParse): () => void; updateParserOptions(sourceType?: SourceType): void; } @@ -66,10 +67,10 @@ export function createLinter( linter.getRules().forEach((item, name) => { rules.set(name, { - name, description: item.meta?.docs?.description, - url: item.meta?.docs?.url, + name, schema: item.meta?.schema ?? [], + url: item.meta?.docs?.url, }); }); @@ -81,13 +82,13 @@ export function createLinter( onLint.trigger(filename, messages); } catch (e) { const lintMessage: Linter.LintMessage = { - source: 'eslint', - ruleId: '', - severity: 2, - nodeType: '', column: 1, line: 1, message: String(e instanceof Error ? e.stack : e), + nodeType: '', + ruleId: '', + severity: 2, + source: 'eslint', }; if (typeof e === 'object' && e && 'currentNode' in e) { const node = e.currentNode as TSESTree.Node; @@ -181,12 +182,12 @@ export function createLinter( applyTSConfig('/tsconfig.json'); return { - rules, configs: Array.from(configs.keys()), + onLint: onLint.register, + onParse: onParse.register, + rules, triggerFix, triggerLint, updateParserOptions, - onParse: onParse.register, - onLint: onLint.register, }; } diff --git a/packages/website/src/components/linter/createParser.ts b/packages/website/src/components/linter/createParser.ts index 08e3c17fd0ee..8be91ed929c2 100644 --- a/packages/website/src/components/linter/createParser.ts +++ b/packages/website/src/components/linter/createParser.ts @@ -3,7 +3,6 @@ import type { ParserOptions } from '@typescript-eslint/types'; import type { Parser } from '@typescript-eslint/utils/ts-eslint'; import type * as ts from 'typescript'; -import { defaultParseSettings } from './config'; import type { ParseSettings, PlaygroundSystem, @@ -11,15 +10,17 @@ import type { WebLinterModule, } from './types'; +import { defaultParseSettings } from './config'; + export function createParser( system: PlaygroundSystem, compilerOptions: ts.CompilerOptions, onUpdate: (filename: string, model: UpdateModel) => void, utils: WebLinterModule, vfs: typeof tsvfs, -): Parser.ParserModule & { +): { updateConfig: (compilerOptions: ts.CompilerOptions) => void; -} { +} & Parser.ParserModule { const registeredFiles = new Set(); const createEnv = ( @@ -36,9 +37,6 @@ export function createParser( let compilerHost = createEnv(compilerOptions); return { - updateConfig(compilerOptions): void { - compilerHost = createEnv(compilerOptions); - }, parseForESLint: ( text: string, options: ParserOptions = {}, @@ -82,20 +80,19 @@ export function createParser( onUpdate(filePath, { storedAST: converted.estree, - storedTsAST: tsAst, storedScope: scopeManager, + storedTsAST: tsAst, typeChecker: checker, }); return { ast: converted.estree, + scopeManager, services: { - program, emitDecoratorMetadata: compilerOptions.emitDecoratorMetadata ?? false, + esTreeNodeToTSNodeMap: converted.astMaps.esTreeNodeToTSNodeMap, experimentalDecorators: compilerOptions.experimentalDecorators ?? false, - esTreeNodeToTSNodeMap: converted.astMaps.esTreeNodeToTSNodeMap, - tsNodeToESTreeNodeMap: converted.astMaps.tsNodeToESTreeNodeMap, getSymbolAtLocation: node => checker.getSymbolAtLocation( converted.astMaps.esTreeNodeToTSNodeMap.get(node), @@ -104,10 +101,14 @@ export function createParser( checker.getTypeAtLocation( converted.astMaps.esTreeNodeToTSNodeMap.get(node), ), + program, + tsNodeToESTreeNodeMap: converted.astMaps.tsNodeToESTreeNodeMap, }, - scopeManager, visitorKeys: utils.visitorKeys, }; }, + updateConfig(compilerOptions): void { + compilerHost = createEnv(compilerOptions); + }, }; } diff --git a/packages/website/src/components/linter/types.ts b/packages/website/src/components/linter/types.ts index 8491640b4b22..6e8a046733b7 100644 --- a/packages/website/src/components/linter/types.ts +++ b/packages/website/src/components/linter/types.ts @@ -13,27 +13,25 @@ export type { ParseSettings } from '@typescript-eslint/typescript-estree/use-at- export interface UpdateModel { storedAST?: TSESTree.Program; - storedTsAST?: ts.Node; storedScope?: ScopeManager; + storedTsAST?: ts.Node; typeChecker?: ts.TypeChecker; } export interface WebLinterModule { - createLinter: () => Linter; analyze: typeof analyze; - visitorKeys: SourceCode.VisitorKeys; astConverter: typeof astConverter; - esquery: typeof esquery; configs: Record; + createLinter: () => Linter; + esquery: typeof esquery; + visitorKeys: SourceCode.VisitorKeys; } -export type PlaygroundSystem = Required< - Pick -> & - ts.System & { - removeFile: (fileName: string) => void; - searchFiles: (path: string) => string[]; - }; +export type PlaygroundSystem = { + removeFile: (fileName: string) => void; + searchFiles: (path: string) => string[]; +} & Required> & + ts.System; export type LinterOnLint = ( fileName: string, diff --git a/packages/website/src/components/linter/utils.ts b/packages/website/src/components/linter/utils.ts index 89f12e0cdeea..e4006d5d90b7 100644 --- a/packages/website/src/components/linter/utils.ts +++ b/packages/website/src/components/linter/utils.ts @@ -4,13 +4,13 @@ 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; }; + isPreferred: boolean; + message: string; } export function ensurePositiveInt( @@ -38,19 +38,19 @@ export function createEditOperation( 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, + endLineNumber: end.lineNumber, + startColumn: start.column, + startLineNumber: start.lineNumber, }, + text: action.fix.text, }; } function normalizeCode(code: Monaco.editor.IMarker['code']): { - value: string; target?: string; + value: string; } { if (!code) { return { value: '' }; @@ -59,8 +59,8 @@ function normalizeCode(code: Monaco.editor.IMarker['code']): { return { value: code }; } return { - value: code.value, target: code.target.toString(), + value: code.value, }; } @@ -76,14 +76,14 @@ export function parseMarkers( const fixers = fixes.get(uri)?.map(item => ({ - message: item.message, - isPreferred: item.isPreferred, fix(): void { const model = editor.getModel(); if (model) { editor.executeEdits('eslint', [createEditOperation(model, item)]); } }, + isPreferred: item.isPreferred, + message: item.message, })) ?? []; const group = @@ -96,18 +96,18 @@ export function parseMarkers( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition result[group] ||= { group, - uri: code.target, items: [], + uri: code.target, }; result[group].items.push({ + fixer: fixers.find(item => item.isPreferred), + location: `${marker.startLineNumber}:${marker.startColumn} - ${marker.endLineNumber}:${marker.endColumn}`, 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), }); } @@ -133,38 +133,38 @@ export function parseLintResults( const marker: Monaco.editor.IMarkerData = { code: message.ruleId ? { - value: message.ruleId, target: ruleUri(message.ruleId), + value: message.ruleId, } : 'Internal error', + endColumn, + endLineNumber, + message: message.message, severity: message.severity === 2 ? 8 // MarkerSeverity.Error : 4, // MarkerSeverity.Warning source: 'ESLint', - message: message.message, - startLineNumber, startColumn, - endLineNumber, - endColumn, + startLineNumber, }; const markerUri = createURI(marker); const fixes: LintCodeAction[] = []; if (message.fix) { fixes.push({ - message: `Fix this ${message.ruleId ?? 'unknown'} problem`, fix: message.fix, isPreferred: true, + message: `Fix this ${message.ruleId ?? 'unknown'} problem`, }); } if (message.suggestions) { for (const suggestion of message.suggestions) { fixes.push({ - message: suggestion.desc, code: message.ruleId, fix: suggestion.fix, isPreferred: false, + message: suggestion.desc, }); } } diff --git a/packages/website/src/components/options.ts b/packages/website/src/components/options.ts index 3e5bb8c90a4e..08711de17a4b 100644 --- a/packages/website/src/components/options.ts +++ b/packages/website/src/components/options.ts @@ -1,12 +1,13 @@ -import { toJson } from './lib/json'; import type { ConfigFileType, ConfigModel, ConfigShowAst } from './types'; -export const detailTabs: { value: ConfigShowAst; label: string }[] = [ - { value: false, label: 'Errors' }, - { value: 'es', label: 'ESTree' }, - { value: 'ts', label: 'TypeScript' }, - { value: 'scope', label: 'Scope' }, - { value: 'types', label: 'Types' }, +import { toJson } from './lib/json'; + +export const detailTabs: { label: string; value: ConfigShowAst }[] = [ + { label: 'Errors', value: false }, + { label: 'ESTree', value: 'es' }, + { label: 'TypeScript', value: 'ts' }, + { label: 'Scope', value: 'scope' }, + { label: 'Types', value: 'types' }, ]; /** @@ -29,19 +30,19 @@ export const fileTypes: ConfigFileType[] = [ * It's used as a fallback when the URL doesn't contain any config */ export const defaultConfig: ConfigModel = { + code: '', + eslintrc: toJson({ + rules: {}, + }), fileType: '.tsx', + scroll: true, showAST: false, + showTokens: false, sourceType: 'module', - code: '', ts: process.env.TS_VERSION, tsconfig: toJson({ compilerOptions: { strictNullChecks: true, }, }), - eslintrc: toJson({ - rules: {}, - }), - scroll: true, - showTokens: false, }; diff --git a/packages/website/src/components/types.ts b/packages/website/src/components/types.ts index 9d1efa9b0abb..181e1e5de39b 100644 --- a/packages/website/src/components/types.ts +++ b/packages/website/src/components/types.ts @@ -9,8 +9,8 @@ export type SourceType = TSESLint.SourceType; export type RulesRecord = TSESLint.Linter.RulesRecord; export interface RuleDetails { - name: string; description?: string; + name: string; url?: string; } @@ -21,38 +21,38 @@ export type ConfigFileType = `${ts.Extension}`; export type ConfigShowAst = 'es' | 'scope' | 'ts' | 'types' | false; export interface ConfigModel { - fileType?: ConfigFileType; - sourceType?: SourceType; - eslintrc: string; - tsconfig: string; code: string; - ts: string; - showAST?: ConfigShowAst; - scroll?: boolean; - showTokens?: boolean; + eslintrc: string; esQuery?: { - selector: ESQuery.Selector; filter?: string; + selector: ESQuery.Selector; }; + fileType?: ConfigFileType; + scroll?: boolean; + showAST?: ConfigShowAst; + showTokens?: boolean; + sourceType?: SourceType; + ts: string; + tsconfig: string; } export type SelectedRange = [number, number]; export interface ErrorItem { - message: string; + fixer?: { fix(): void; message: string }; location: string; + message: string; severity: number; - suggestions: { message: string; fix(): void }[]; - fixer?: { message: string; fix(): void }; + suggestions: { fix(): void; message: string }[]; } export interface ErrorGroup { group: string; - uri?: string; items: ErrorItem[]; + uri?: string; } -export type EslintRC = Record & { rules: RulesRecord }; -export type TSConfig = Record & { +export type EslintRC = { rules: RulesRecord } & Record; +export type TSConfig = { compilerOptions: CompilerFlags; -}; +} & Record; diff --git a/packages/website/src/globals.d.ts b/packages/website/src/globals.d.ts index 966cee026906..7bd068a9a13c 100644 --- a/packages/website/src/globals.d.ts +++ b/packages/website/src/globals.d.ts @@ -12,16 +12,16 @@ declare global { error?: (e: Error) => void, ): void; config: (arg: { - paths?: Record; ignoreDuplicateModules?: string[]; + paths?: Record; }) => void; } interface Window { - ts: typeof ts; - require: WindowRequire; esquery: typeof esquery; + require: WindowRequire; system: unknown; + ts: typeof ts; visitorKeys: Record; } } diff --git a/packages/website/src/hooks/useBool.ts b/packages/website/src/hooks/useBool.ts index b06c9c40e5a4..8ef2e086f422 100644 --- a/packages/website/src/hooks/useBool.ts +++ b/packages/website/src/hooks/useBool.ts @@ -1,8 +1,9 @@ import type { Dispatch, SetStateAction } from 'react'; + import { useCallback, useState } from 'react'; export function useBool( - initialState: boolean | (() => boolean), + initialState: (() => boolean) | boolean, ): [boolean, () => void, Dispatch>] { const [value, setValue] = useState(initialState); diff --git a/packages/website/src/hooks/useHistorySelector.ts b/packages/website/src/hooks/useHistorySelector.ts index 31745005ac11..c180fef9ea59 100644 --- a/packages/website/src/hooks/useHistorySelector.ts +++ b/packages/website/src/hooks/useHistorySelector.ts @@ -1,5 +1,6 @@ -import { useHistory } from '@docusaurus/router'; import type * as H from 'history'; + +import { useHistory } from '@docusaurus/router'; import { useSyncExternalStore } from 'react'; export type HistorySelector = (history: H.History) => T; diff --git a/packages/website/src/hooks/useRulesMeta.ts b/packages/website/src/hooks/useRulesMeta.ts index fc6ca0d230d0..06005684dace 100644 --- a/packages/website/src/hooks/useRulesMeta.ts +++ b/packages/website/src/hooks/useRulesMeta.ts @@ -1,6 +1,7 @@ -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import type { RulesMeta } from '@site/rulesMeta'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; + export function useRulesMeta(): RulesMeta { const { siteConfig: { customFields },