From 652376fa410a117db0b6860d4e85d054d57fcfd7 Mon Sep 17 00:00:00 2001 From: armano2 Date: Sun, 22 May 2022 17:21:35 +0200 Subject: [PATCH 1/5] chore(website): rewrite web linter to typescript - reuse Linter instance - reuse virtual CompilerHost and use utilize typescript-vfs - update ecmaVersion to latest - correct minor issue in ESLintParseResult --- packages/utils/src/ts-eslint/Linter.ts | 4 +- .../website-eslint/src/linter/CompilerHost.js | 92 ------------- packages/website-eslint/src/linter/linter.js | 73 +---------- packages/website-eslint/src/linter/parser.js | 62 --------- packages/website-eslint/types/index.d.ts | 43 ++----- .../src/components/ASTViewerESTree.tsx | 2 +- .../website/src/components/Playground.tsx | 8 +- .../ast/serializer/serializerESTree.ts | 2 +- .../ast/serializer/serializerScope.ts | 2 +- .../src/components/config/ConfigEslint.tsx | 3 +- .../src/components/editor/LoadedEditor.tsx | 10 +- .../editor/createProvideCodeActions.ts | 4 +- .../src/components/editor/loadSandbox.ts | 10 +- .../website/src/components/editor/types.ts | 2 +- .../components/editor/useSandboxServices.ts | 18 +-- .../src/components/linter/CompilerHost.ts | 63 +++++++++ .../src/components/linter/WebLinter.ts | 121 ++++++++++++++++++ .../src/components/linter/config.ts} | 4 +- .../components/{editor => linter}/lintCode.ts | 9 +- .../components/{editor => linter}/utils.ts | 0 packages/website/src/components/types.ts | 10 +- 21 files changed, 244 insertions(+), 298 deletions(-) delete mode 100644 packages/website-eslint/src/linter/CompilerHost.js delete mode 100644 packages/website-eslint/src/linter/parser.js create mode 100644 packages/website/src/components/linter/CompilerHost.ts create mode 100644 packages/website/src/components/linter/WebLinter.ts rename packages/{website-eslint/src/linter/config.js => website/src/components/linter/config.ts} (81%) rename packages/website/src/components/{editor => linter}/lintCode.ts (91%) rename packages/website/src/components/{editor => linter}/utils.ts (100%) diff --git a/packages/utils/src/ts-eslint/Linter.ts b/packages/utils/src/ts-eslint/Linter.ts index 77c338f5a75..c2a8e67fe3a 100644 --- a/packages/utils/src/ts-eslint/Linter.ts +++ b/packages/utils/src/ts-eslint/Linter.ts @@ -76,7 +76,7 @@ declare class LinterBase { /** * Performs multiple autofix passes over the text until as many fixes as possible have been applied. - * @param text The source text to apply fixes to. + * @param code The source text to apply fixes to. * @param config The ESLint config object to use. * @param options The ESLint options object to use. * @returns The result of the fix operation as returned from the SourceCodeFixer. @@ -316,7 +316,7 @@ namespace Linter { export interface ESLintParseResult { ast: TSESTree.Program; - parserServices?: ParserServices; + services?: ParserServices; scopeManager?: Scope.ScopeManager; visitorKeys?: SourceCode.VisitorKeys; } diff --git a/packages/website-eslint/src/linter/CompilerHost.js b/packages/website-eslint/src/linter/CompilerHost.js deleted file mode 100644 index f48c77109c9..00000000000 --- a/packages/website-eslint/src/linter/CompilerHost.js +++ /dev/null @@ -1,92 +0,0 @@ -import { - getDefaultLibFileName, - ScriptKind, - createSourceFile, - ScriptTarget, -} from 'typescript'; - -function getScriptKind(isJsx, filePath) { - const extension = (/(\.[a-z]+)$/.exec(filePath)?.[0] || '').toLowerCase(); - - switch (extension) { - case '.ts': - return ScriptKind.TS; - case '.tsx': - return ScriptKind.TSX; - case '.js': - return ScriptKind.JS; - - case '.jsx': - return ScriptKind.JSX; - - case '.json': - return ScriptKind.JSON; - - default: - // unknown extension, force typescript to ignore the file extension, and respect the user's setting - return isJsx ? ScriptKind.TSX : ScriptKind.TS; - } -} - -export class CompilerHost { - constructor(libs, isJsx) { - this.files = []; - this.isJsx = isJsx || false; - - if (libs) { - for (const [key, value] of libs) { - this.files[key] = value; - } - } - } - - fileExists(name) { - return !!this.files[name]; - } - - getCanonicalFileName(name) { - return name; - } - - getCurrentDirectory() { - return '/'; - } - - getDirectories() { - return []; - } - - getDefaultLibFileName(options) { - return '/' + getDefaultLibFileName(options); - } - - getNewLine() { - return '\n'; - } - - useCaseSensitiveFileNames() { - return true; - } - - writeFile() { - return null; - } - - readFile(name) { - if (this.fileExists(name)) { - return this.files[name]; - } else { - return ''; // fallback, in case if file is not available - } - } - - getSourceFile(name) { - return createSourceFile( - name, - this.readFile(name), - ScriptTarget.Latest, - /* setParentNodes */ true, - getScriptKind(this.isJsx, name), - ); - } -} diff --git a/packages/website-eslint/src/linter/linter.js b/packages/website-eslint/src/linter/linter.js index 06a4db0d3fb..34f2c1900db 100644 --- a/packages/website-eslint/src/linter/linter.js +++ b/packages/website-eslint/src/linter/linter.js @@ -1,74 +1,15 @@ import 'vs/language/typescript/tsWorker'; -import { parseForESLint } from './parser'; import { Linter } from 'eslint'; import rules from '@typescript-eslint/eslint-plugin/dist/rules'; -const PARSER_NAME = '@typescript-eslint/parser'; - -export function loadLinter(libs, options) { +export function createLinter() { const linter = new Linter(); - let storedAST; - let storedTsAST; - let storedScope; - - let compilerOptions = options; - - linter.defineParser(PARSER_NAME, { - parseForESLint(code, eslintOptions) { - const toParse = parseForESLint( - code, - eslintOptions, - compilerOptions, - libs, - ); - storedAST = toParse.ast; - storedTsAST = toParse.tsAst; - storedScope = toParse.scopeManager; - return toParse; - }, - // parse(code: string, options: ParserOptions): ParseForESLintResult['ast'] { - // const toParse = parseForESLint(code, options); - // storedAST = toParse.ast; - // return toParse.ast; - // }, - }); - - for (const name of Object.keys(rules)) { + for (const name in rules) { linter.defineRule(`@typescript-eslint/${name}`, rules[name]); } - - const ruleNames = Array.from(linter.getRules()).map(value => { - return { - name: value[0], - description: value[1]?.meta?.docs?.description, - }; - }); - - return { - ruleNames: ruleNames, - - updateOptions(options) { - compilerOptions = options || {}; - }, - - getScope() { - return storedScope; - }, - - getAst() { - return storedAST; - }, - - getTsAst() { - return storedTsAST; - }, - - lint(code, parserOptions, rules) { - return linter.verify(code, { - parser: PARSER_NAME, - parserOptions, - rules, - }); - }, - }; + return linter; } + +export { analyze } from '@typescript-eslint/scope-manager/dist/analyze'; +export { visitorKeys } from '@typescript-eslint/visitor-keys/dist/visitor-keys'; +export { astConverter } from '@typescript-eslint/typescript-estree/dist/ast-converter'; diff --git a/packages/website-eslint/src/linter/parser.js b/packages/website-eslint/src/linter/parser.js deleted file mode 100644 index fc637fe65b7..00000000000 --- a/packages/website-eslint/src/linter/parser.js +++ /dev/null @@ -1,62 +0,0 @@ -import { analyze } from '@typescript-eslint/scope-manager/dist/analyze'; -import { visitorKeys } from '@typescript-eslint/visitor-keys/dist/visitor-keys'; -import { astConverter } from '@typescript-eslint/typescript-estree/dist/ast-converter'; -import { extra } from './config.js'; -import { CompilerHost } from './CompilerHost'; -import { createProgram } from 'typescript'; - -export function createASTProgram(code, isJsx, compilerOptions, libs) { - const fileName = isJsx ? '/demo.tsx' : '/demo.ts'; - const compilerHost = new CompilerHost(libs, isJsx); - - compilerHost.files[fileName] = code; - const program = createProgram( - Object.keys(compilerHost.files), - compilerOptions, - compilerHost, - ); - const ast = program.getSourceFile(fileName); - return { - ast, - program, - }; -} - -export function parseForESLint(code, eslintOptions, compilerOptions, libs) { - const isJsx = eslintOptions.ecmaFeatures?.jsx ?? false; - - const { ast: tsAst, program } = createASTProgram( - code, - isJsx, - compilerOptions, - libs, - ); - - const { estree: ast, astMaps } = astConverter( - tsAst, - { ...extra, code, jsx: isJsx }, - true, - ); - - const services = { - hasFullTypeInformation: true, - program, - esTreeNodeToTSNodeMap: astMaps.esTreeNodeToTSNodeMap, - tsNodeToESTreeNodeMap: astMaps.tsNodeToESTreeNodeMap, - }; - - const scopeManager = analyze(ast, { - ecmaVersion: - eslintOptions.ecmaVersion === 'latest' ? 1e8 : eslintOptions.ecmaVersion, - globalReturn: eslintOptions.ecmaFeatures?.globalReturn ?? false, - sourceType: eslintOptions.sourceType ?? 'script', - }); - - 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 aa5a2a43ed7..d0ee27a6280 100644 --- a/packages/website-eslint/types/index.d.ts +++ b/packages/website-eslint/types/index.d.ts @@ -1,38 +1,11 @@ -import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; -import type { ParserOptions } from '@typescript-eslint/types'; -import type { SourceFile, CompilerOptions } from 'typescript'; +import type { TSESLint } from '@typescript-eslint/utils'; -export type LintMessage = TSESLint.Linter.LintMessage; -export type RuleFix = TSESLint.RuleFix; -export type RulesRecord = TSESLint.Linter.RulesRecord; -export type RuleEntry = TSESLint.Linter.RuleEntry; +import { analyze } from '@typescript-eslint/scope-manager/dist/analyze'; +import { astConverter } from '@typescript-eslint/typescript-estree/dist/ast-converter'; -export interface WebLinter { - ruleNames: { name: string; description?: string }[]; - - getAst(): TSESTree.Program; - getTsAst(): SourceFile; - getScope(): Record; - updateOptions(options?: Record): void; - - lint( - code: string, - parserOptions: ParserOptions, - rules?: RulesRecord, - ): LintMessage[]; +export interface LintUtils { + createLinter: () => TSESLint.Linter; + analyze: typeof analyze; + visitorKeys: TSESLint.SourceCode.VisitorKeys; + astConverter: typeof astConverter; } - -export interface LinterLoader { - loadLinter( - libMap: Map, - compilerOptions: CompilerOptions, - ): WebLinter; -} - -export type { - DebugLevel, - EcmaVersion, - ParserOptions, - SourceType, - TSESTree, -} from '@typescript-eslint/types'; diff --git a/packages/website/src/components/ASTViewerESTree.tsx b/packages/website/src/components/ASTViewerESTree.tsx index 7d46efdfb11..7dfcef130cf 100644 --- a/packages/website/src/components/ASTViewerESTree.tsx +++ b/packages/website/src/components/ASTViewerESTree.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import ASTViewer from './ast/ASTViewer'; import type { ASTViewerBaseProps, ASTViewerModelMap } from './ast/types'; -import type { TSESTree } from '@typescript-eslint/website-eslint'; +import type { TSESTree } from '@typescript-eslint/utils'; import { serialize } from './ast/serializer/serializer'; import { createESTreeSerializer } from './ast/serializer/serializerESTree'; diff --git a/packages/website/src/components/Playground.tsx b/packages/website/src/components/Playground.tsx index 6f28f21d36e..d602e457482 100644 --- a/packages/website/src/components/Playground.tsx +++ b/packages/website/src/components/Playground.tsx @@ -17,7 +17,7 @@ import ASTViewerTS from './ASTViewerTS'; import type { RuleDetails, SelectedRange } from './types'; -import type { TSESTree } from '@typescript-eslint/website-eslint'; +import type { TSESTree } from '@typescript-eslint/utils'; import type { SourceFile } from 'typescript'; import ASTViewerScope from '@site/src/components/ASTViewerScope'; @@ -44,11 +44,11 @@ function Playground(): JSX.Element { showAST: false, sourceType: 'module', code: '', - ts: process.env.TS_VERSION, + ts: process.env.TS_VERSION!, rules: {}, tsConfig: {}, }); - const { isDarkTheme } = useColorMode(); + const { colorMode } = useColorMode(); const [esAst, setEsAst] = useState(); const [tsAst, setTsAST] = useState(); const [scope, setScope] = useState | string | null>(); @@ -83,7 +83,7 @@ function Playground(): JSX.Element { jsx={state.jsx} code={state.code} tsConfig={state.tsConfig} - darkTheme={isDarkTheme} + darkTheme={colorMode === 'dark'} sourceType={state.sourceType} rules={state.rules} showAST={state.showAST} diff --git a/packages/website/src/components/ast/serializer/serializerESTree.ts b/packages/website/src/components/ast/serializer/serializerESTree.ts index 7f467757c8a..76aabfd024b 100644 --- a/packages/website/src/components/ast/serializer/serializerESTree.ts +++ b/packages/website/src/components/ast/serializer/serializerESTree.ts @@ -1,6 +1,6 @@ import type { ASTViewerModel, Serializer } from '../types'; +import type { TSESTree } from '@typescript-eslint/utils'; import { isRecord } from '../utils'; -import type { TSESTree } from '@typescript-eslint/website-eslint'; export const propsToFilter = ['parent', 'comments', 'tokens']; diff --git a/packages/website/src/components/ast/serializer/serializerScope.ts b/packages/website/src/components/ast/serializer/serializerScope.ts index 176f49f92c5..b000aba7db1 100644 --- a/packages/website/src/components/ast/serializer/serializerScope.ts +++ b/packages/website/src/components/ast/serializer/serializerScope.ts @@ -1,5 +1,5 @@ import type { ASTViewerModel, Serializer, SelectedRange } from '../types'; -import type { TSESTree } from '@typescript-eslint/website-eslint'; +import type { TSESTree } from '@typescript-eslint/utils'; import { isRecord } from '../utils'; function isESTreeNode( diff --git a/packages/website/src/components/config/ConfigEslint.tsx b/packages/website/src/components/config/ConfigEslint.tsx index cb64ae0a46a..f72e02f342c 100644 --- a/packages/website/src/components/config/ConfigEslint.tsx +++ b/packages/website/src/components/config/ConfigEslint.tsx @@ -1,8 +1,7 @@ import React, { useCallback, useEffect, useState } from 'react'; -import type { RulesRecord, RuleEntry } from '@typescript-eslint/website-eslint'; import ConfigEditor, { ConfigOptionsType } from './ConfigEditor'; -import type { RuleDetails } from '../types'; +import type { RuleDetails, RulesRecord, RuleEntry } from '../types'; import { shallowEqual } from '../lib/shallowEqual'; export interface ModalEslintProps { diff --git a/packages/website/src/components/editor/LoadedEditor.tsx b/packages/website/src/components/editor/LoadedEditor.tsx index 9e4baa04da5..d6433bfaee5 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -1,12 +1,12 @@ import React, { useMemo } from 'react'; import type Monaco from 'monaco-editor'; import { useEffect, useRef, useState } from 'react'; -import type { WebLinter } from '@typescript-eslint/website-eslint'; import type { SandboxInstance } from './useSandboxServices'; import type { CommonEditorProps } from './types'; +import type { WebLinter } from '../linter/WebLinter'; import { debounce } from '../lib/debounce'; -import { lintCode, LintCodeAction } from './lintCode'; +import { lintCode, LintCodeAction } from '../linter/lintCode'; import { createProvideCodeActions } from './createProvideCodeActions'; export interface LoadedEditorProps extends CommonEditorProps { @@ -83,9 +83,9 @@ export const LoadedEditor: React.FC = ({ ); } - onEsASTChange(fatalMessage ?? webLinter.getAst()); - onTsASTChange(fatalMessage ?? webLinter.getTsAst()); - onScopeChange(fatalMessage ?? webLinter.getScope()); + onEsASTChange(fatalMessage ?? webLinter.storedAST ?? ''); + onTsASTChange(fatalMessage ?? webLinter.storedTsAST ?? ''); + onScopeChange(fatalMessage ?? webLinter.storedScope ?? ''); onSelect(sandboxInstance.editor.getPosition()); }, 500), [code, jsx, sandboxInstance, rules, sourceType, tsConfig, webLinter], diff --git a/packages/website/src/components/editor/createProvideCodeActions.ts b/packages/website/src/components/editor/createProvideCodeActions.ts index b0930c19b32..1e9dfaca389 100644 --- a/packages/website/src/components/editor/createProvideCodeActions.ts +++ b/packages/website/src/components/editor/createProvideCodeActions.ts @@ -1,6 +1,6 @@ import type Monaco from 'monaco-editor'; -import { createURI } from './utils'; -import type { LintCodeAction } from './lintCode'; +import type { LintCodeAction } from '../linter/lintCode'; +import { createURI } from '../linter/utils'; export function createProvideCodeActions( fixes: Map, diff --git a/packages/website/src/components/editor/loadSandbox.ts b/packages/website/src/components/editor/loadSandbox.ts index d25d9248bb6..ac342d8e290 100644 --- a/packages/website/src/components/editor/loadSandbox.ts +++ b/packages/website/src/components/editor/loadSandbox.ts @@ -1,6 +1,6 @@ import type * as TsWorker from '../../vendor/tsWorker'; import type * as SandboxFactory from '../../vendor/sandbox'; -import type { LinterLoader } from '@typescript-eslint/website-eslint'; +import type { LintUtils } from '@typescript-eslint/website-eslint'; type Monaco = typeof import('monaco-editor'); type TS = typeof import('typescript'); @@ -10,7 +10,7 @@ declare global { main: Monaco, tsWorker: typeof TsWorker, sandboxFactory: typeof SandboxFactory, - linter: LinterLoader, + lintUtils: LintUtils, ) => void; interface WindowRequire { (files: string[], cb: WindowRequireCb): void; @@ -31,7 +31,7 @@ export interface SandboxModel { tsWorker: typeof TsWorker; sandboxFactory: typeof SandboxFactory; ts: TS; - linter: LinterLoader; + lintUtils: LintUtils; } function loadSandbox(tsVersion: string): Promise { @@ -61,7 +61,7 @@ function loadSandbox(tsVersion: string): Promise { 'sandbox/index', 'linter/index', ], - (main, tsWorker, sandboxFactory, linter) => { + (main, tsWorker, sandboxFactory, lintUtils) => { const isOK = main && window.ts && sandboxFactory; if (isOK) { resolve({ @@ -69,7 +69,7 @@ function loadSandbox(tsVersion: string): Promise { tsWorker, sandboxFactory, ts: window.ts, - linter, + lintUtils, }); } else { reject( diff --git a/packages/website/src/components/editor/types.ts b/packages/website/src/components/editor/types.ts index edcbcf842d3..894a4fde605 100644 --- a/packages/website/src/components/editor/types.ts +++ b/packages/website/src/components/editor/types.ts @@ -1,6 +1,6 @@ import type Monaco from 'monaco-editor'; import type { ConfigModel, SelectedRange } from '../types'; -import type { TSESTree } from '@typescript-eslint/website-eslint'; +import type { TSESTree } from '@typescript-eslint/utils'; import type { SourceFile } from 'typescript'; export interface CommonEditorProps extends ConfigModel { diff --git a/packages/website/src/components/editor/useSandboxServices.ts b/packages/website/src/components/editor/useSandboxServices.ts index 3fe7d7e1ddc..698f136a9c3 100644 --- a/packages/website/src/components/editor/useSandboxServices.ts +++ b/packages/website/src/components/editor/useSandboxServices.ts @@ -1,13 +1,14 @@ import { useEffect, useState } from 'react'; import type Monaco from 'monaco-editor'; -import type { LintMessage, WebLinter } from '@typescript-eslint/website-eslint'; +import type { TSESLint } from '@typescript-eslint/utils'; import type { RuleDetails } from '../types'; import type { createTypeScriptSandbox, SandboxConfig, } from '../../vendor/sandbox'; +import { WebLinter } from '../linter/WebLinter'; import { sandboxSingleton } from './loadSandbox'; import { editorEmbedId } from './EditorEmbed'; import { useColorMode } from '@docusaurus/theme-common'; @@ -24,7 +25,7 @@ export interface SandboxServicesProps { export type SandboxInstance = ReturnType; export interface SandboxServices { - fixes: Map; + fixes: Map; main: typeof Monaco; sandboxInstance: SandboxInstance; webLinter: WebLinter; @@ -35,7 +36,7 @@ export const useSandboxServices = ( ): Error | SandboxServices | undefined => { const [services, setServices] = useState(); const [loadedTs, setLoadedTs] = useState(props.ts); - const { isDarkTheme } = useColorMode(); + const { colorMode } = useColorMode(); useEffect(() => { if (props.ts !== loadedTs) { @@ -44,17 +45,17 @@ export const useSandboxServices = ( }, [props.ts, loadedTs]); useEffect(() => { - const fixes = new Map(); + const fixes = new Map(); let sandboxInstance: SandboxInstance | undefined; setLoadedTs(props.ts); sandboxSingleton(props.ts) - .then(async ({ main, sandboxFactory, ts, linter }) => { + .then(async ({ main, sandboxFactory, ts, lintUtils }) => { const compilerOptions: Monaco.languages.typescript.CompilerOptions = { noResolve: true, target: main.languages.typescript.ScriptTarget.ESNext, jsx: props.jsx ? main.languages.typescript.JsxEmit.React : undefined, - lib: ['esnext'], + lib: ['es2021', 'esnext'], module: main.languages.typescript.ModuleKind.ESNext, }; @@ -77,7 +78,7 @@ export const useSandboxServices = ( ts, ); sandboxInstance.monaco.editor.setTheme( - isDarkTheme ? 'vs-dark' : 'vs-light', + colorMode === 'dark' ? 'vs-dark' : 'vs-light', ); const libMap = await sandboxInstance.tsvfs.createDefaultMapFromCDN( @@ -86,8 +87,9 @@ export const useSandboxServices = ( true, window.ts, ); + const system = sandboxInstance.tsvfs.createSystem(libMap); - const webLinter = linter.loadLinter(libMap, compilerOptions); + const webLinter = new WebLinter(system, compilerOptions, lintUtils); props.onLoaded(webLinter.ruleNames, sandboxInstance.supportedVersions); diff --git a/packages/website/src/components/linter/CompilerHost.ts b/packages/website/src/components/linter/CompilerHost.ts new file mode 100644 index 00000000000..a96b1ac2e0a --- /dev/null +++ b/packages/website/src/components/linter/CompilerHost.ts @@ -0,0 +1,63 @@ +import type { ScriptKind, System, SourceFile, CompilerHost } from 'typescript'; + +function getScriptKind( + ts: typeof import('typescript'), + filePath: string, +): ScriptKind { + const extension = (/(\.[a-z]+)$/.exec(filePath)?.[0] ?? '').toLowerCase(); + + switch (extension) { + case '.ts': + return ts.ScriptKind.TS; + case '.tsx': + return ts.ScriptKind.TSX; + + case '.js': + return ts.ScriptKind.JS; + case '.jsx': + return ts.ScriptKind.JSX; + + case '.json': + return ts.ScriptKind.JSON; + + default: + // unknown extension, force typescript to ignore the file extension, and respect the user's setting + return ts.ScriptKind.TS; + } +} + +/** + * 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. + * + * based on typescript-vfs + * @see https://github.com/microsoft/TypeScript-Website/blob/d2613c0e57ae1be2f3a76e94b006819a1fc73d5e/packages/typescript-vfs/src/index.ts#L480 + */ +export function createVirtualCompilerHost( + sys: System, + ts: typeof import('typescript'), +): CompilerHost { + return { + ...sys, + getCanonicalFileName: (fileName: string) => fileName, + getDefaultLibFileName: options => '/' + ts.getDefaultLibFileName(options), + getCurrentDirectory: () => '/', + getDirectories: () => [], + getNewLine: () => sys.newLine, + getSourceFile(fileName, languageVersionOrOptions): SourceFile | undefined { + if (this.fileExists(fileName)) { + const file = this.readFile(fileName)!; + return ts.createSourceFile( + fileName, + file, + languageVersionOrOptions, + true, + getScriptKind(ts, fileName), + ); + } + return undefined; + }, + useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames, + }; +} diff --git a/packages/website/src/components/linter/WebLinter.ts b/packages/website/src/components/linter/WebLinter.ts new file mode 100644 index 00000000000..bf8061285e8 --- /dev/null +++ b/packages/website/src/components/linter/WebLinter.ts @@ -0,0 +1,121 @@ +import type { + CompilerOptions, + SourceFile, + CompilerHost, + System, +} from 'typescript'; +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import type { ParserServices } from '@typescript-eslint/utils/dist/ts-estree'; +import type { ParserOptions } from '@typescript-eslint/types'; +import type { LintUtils } from '@typescript-eslint/website-eslint'; + +import { createVirtualCompilerHost } from '@site/src/components/linter/CompilerHost'; +import { extra } from '@site/src/components/linter/config'; + +const PARSER_NAME = '@typescript-eslint/parser'; + +export class WebLinter { + private readonly host: CompilerHost; + + public storedAST?: TSESTree.Program; + public storedTsAST?: SourceFile; + public storedScope?: Record; + + private compilerOptions: CompilerOptions; + private linter: TSESLint.Linter; + private lintUtils: LintUtils; + + public ruleNames: { name: string; description?: string }[]; + + constructor( + system: System, + compilerOptions: CompilerOptions, + lintUtils: LintUtils, + ) { + this.compilerOptions = compilerOptions; + this.lintUtils = lintUtils; + this.linter = lintUtils.createLinter(); + + this.host = createVirtualCompilerHost(system, window.ts); + + this.linter.defineParser(PARSER_NAME, { + parseForESLint: (text, options?: ParserOptions) => { + return this.eslintParse(text, compilerOptions, options); + }, + }); + + this.ruleNames = Array.from(this.linter.getRules()).map(value => { + return { + name: value[0], + description: value[1]?.meta?.docs?.description, + }; + }); + } + + lint( + code: string, + parserOptions: ParserOptions, + rules: TSESLint.Linter.RulesRecord, + ): TSESLint.Linter.LintMessage[] { + return this.linter.verify(code, { + parser: PARSER_NAME, + parserOptions, + rules, + }); + } + + updateOptions(options: CompilerOptions = {}): void { + this.compilerOptions = options; + } + + eslintParse( + code: string, + compilerOptions: CompilerOptions, + eslintOptions: ParserOptions = {}, + ): TSESLint.Linter.ESLintParseResult { + const isJsx = eslintOptions?.ecmaFeatures?.jsx ?? false; + const fileName = isJsx ? '/demo.tsx' : '/demo.ts'; + + this.host.writeFile(fileName, code, false); + + const program = window.ts.createProgram( + [fileName], + compilerOptions, + this.host, + ); + const tsAst = program.getSourceFile(fileName)!; + + const { estree: ast, astMaps } = this.lintUtils.astConverter( + tsAst, + { ...extra, code, jsx: isJsx }, + true, + ); + + const scopeManager = this.lintUtils.analyze(ast, { + ecmaVersion: + eslintOptions.ecmaVersion === 'latest' + ? 1e8 + : eslintOptions.ecmaVersion, + globalReturn: eslintOptions.ecmaFeatures?.globalReturn ?? false, + sourceType: eslintOptions.sourceType ?? 'script', + }); + + this.storedAST = ast; + this.storedTsAST = tsAst; + this.storedScope = scopeManager as unknown as Record; + + const services: ParserServices = { + hasFullTypeInformation: true, + program, + esTreeNodeToTSNodeMap: astMaps.esTreeNodeToTSNodeMap, + tsNodeToESTreeNodeMap: astMaps.tsNodeToESTreeNodeMap, + }; + + return { + ast, + services, + scopeManager, + visitorKeys: this.lintUtils.visitorKeys, + }; + } +} diff --git a/packages/website-eslint/src/linter/config.js b/packages/website/src/components/linter/config.ts similarity index 81% rename from packages/website-eslint/src/linter/config.js rename to packages/website/src/components/linter/config.ts index 06e9ec28b3e..f4a33eda679 100644 --- a/packages/website-eslint/src/linter/config.js +++ b/packages/website/src/components/linter/config.ts @@ -1,4 +1,6 @@ -export const extra = { +import type { Extra } from '@typescript-eslint/typescript-estree/dist/parser-options'; + +export const extra: Extra = { code: '', comment: true, comments: [], diff --git a/packages/website/src/components/editor/lintCode.ts b/packages/website/src/components/linter/lintCode.ts similarity index 91% rename from packages/website/src/components/editor/lintCode.ts rename to packages/website/src/components/linter/lintCode.ts index b601a6cdcb3..33f6c216bf3 100644 --- a/packages/website/src/components/editor/lintCode.ts +++ b/packages/website/src/components/linter/lintCode.ts @@ -1,5 +1,6 @@ -import type { RulesRecord, WebLinter } from '@typescript-eslint/website-eslint'; +import type { TSESLint } from '@typescript-eslint/utils'; import type Monaco from 'monaco-editor'; +import type { WebLinter } from './WebLinter'; import { createURI, ensurePositiveInt } from './utils'; export interface LintCodeAction { @@ -15,7 +16,7 @@ export type LintCodeActionGroup = [string, LintCodeAction]; export function lintCode( linter: WebLinter, code: string, - rules: RulesRecord | undefined, + rules: TSESLint.Linter.RulesRecord | undefined, jsx?: boolean, sourceType?: 'module' | 'script', ): [Monaco.editor.IMarkerData[], string | undefined, LintCodeActionGroup[]] { @@ -26,11 +27,11 @@ export function lintCode( jsx: jsx ?? false, globalReturn: false, }, - ecmaVersion: 2020, + ecmaVersion: 'latest', project: ['./tsconfig.json'], sourceType: sourceType ?? 'module', }, - rules, + rules ?? {}, ); const markers: Monaco.editor.IMarkerData[] = []; let fatalMessage: string | undefined = undefined; diff --git a/packages/website/src/components/editor/utils.ts b/packages/website/src/components/linter/utils.ts similarity index 100% rename from packages/website/src/components/editor/utils.ts rename to packages/website/src/components/linter/utils.ts diff --git a/packages/website/src/components/types.ts b/packages/website/src/components/types.ts index bb774a436f4..51cd4e98b40 100644 --- a/packages/website/src/components/types.ts +++ b/packages/website/src/components/types.ts @@ -1,13 +1,11 @@ -import type { - ParserOptions, - RulesRecord, -} from '@typescript-eslint/website-eslint'; +import type { TSESLint } from '@typescript-eslint/utils'; export type CompilerFlags = Record; -export type SourceType = ParserOptions['sourceType']; +export type SourceType = TSESLint.SourceType; -export type { RulesRecord } from '@typescript-eslint/website-eslint'; +export type RulesRecord = TSESLint.Linter.RulesRecord; +export type RuleEntry = TSESLint.Linter.RuleEntry; export interface RuleDetails { name: string; From f7d33b47e6b1e77741f5d68d6833d7a6aefc5f34 Mon Sep 17 00:00:00 2001 From: armano2 Date: Sun, 22 May 2022 17:42:00 +0200 Subject: [PATCH 2/5] chore(website): correct issues with ts ^4.7-beta --- packages/website/src/components/ast/serializer/serializerTS.ts | 1 + packages/website/src/components/linter/CompilerHost.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/website/src/components/ast/serializer/serializerTS.ts b/packages/website/src/components/ast/serializer/serializerTS.ts index b89d6e0e1a4..66cffe82b92 100644 --- a/packages/website/src/components/ast/serializer/serializerTS.ts +++ b/packages/website/src/components/ast/serializer/serializerTS.ts @@ -20,6 +20,7 @@ export const propsToFilter = [ 'jsDocComment', 'lineMap', 'externalModuleIndicator', + 'setExternalModuleIndicator', 'bindDiagnostics', 'transformFlags', 'resolvedModules', diff --git a/packages/website/src/components/linter/CompilerHost.ts b/packages/website/src/components/linter/CompilerHost.ts index a96b1ac2e0a..0421df7c48c 100644 --- a/packages/website/src/components/linter/CompilerHost.ts +++ b/packages/website/src/components/linter/CompilerHost.ts @@ -47,7 +47,7 @@ export function createVirtualCompilerHost( getNewLine: () => sys.newLine, getSourceFile(fileName, languageVersionOrOptions): SourceFile | undefined { if (this.fileExists(fileName)) { - const file = this.readFile(fileName)!; + const file = this.readFile(fileName) ?? ''; return ts.createSourceFile( fileName, file, From 298347d8d3e0da83b9627714dbc7cab923063036 Mon Sep 17 00:00:00 2001 From: armano2 Date: Sun, 22 May 2022 18:07:03 +0200 Subject: [PATCH 3/5] chore(website): linting issues --- packages/website/src/components/linter/CompilerHost.ts | 2 -- packages/website/src/components/linter/config.ts | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/website/src/components/linter/CompilerHost.ts b/packages/website/src/components/linter/CompilerHost.ts index 0421df7c48c..8a882644ba9 100644 --- a/packages/website/src/components/linter/CompilerHost.ts +++ b/packages/website/src/components/linter/CompilerHost.ts @@ -42,8 +42,6 @@ export function createVirtualCompilerHost( ...sys, getCanonicalFileName: (fileName: string) => fileName, getDefaultLibFileName: options => '/' + ts.getDefaultLibFileName(options), - getCurrentDirectory: () => '/', - getDirectories: () => [], getNewLine: () => sys.newLine, getSourceFile(fileName, languageVersionOrOptions): SourceFile | undefined { if (this.fileExists(fileName)) { diff --git a/packages/website/src/components/linter/config.ts b/packages/website/src/components/linter/config.ts index f4a33eda679..cad38a3e28f 100644 --- a/packages/website/src/components/linter/config.ts +++ b/packages/website/src/components/linter/config.ts @@ -12,6 +12,7 @@ export const extra: Extra = { filePath: '', jsx: false, loc: true, + // eslint-disable-next-line no-console log: console.log, preserveNodeMaps: true, projects: [], From 863b054491c299b6a28216cfd38d36b295ffc1a8 Mon Sep 17 00:00:00 2001 From: Armano Date: Wed, 25 May 2022 00:43:49 +0200 Subject: [PATCH 4/5] chore(website): reuse getScriptKind from typescript-estree --- packages/website-eslint/src/linter/linter.js | 1 + packages/website-eslint/types/index.d.ts | 2 + .../src/components/linter/CompilerHost.ts | 38 ++++--------------- .../src/components/linter/WebLinter.ts | 2 +- 4 files changed, 12 insertions(+), 31 deletions(-) diff --git a/packages/website-eslint/src/linter/linter.js b/packages/website-eslint/src/linter/linter.js index 34f2c1900db..2eb7878391c 100644 --- a/packages/website-eslint/src/linter/linter.js +++ b/packages/website-eslint/src/linter/linter.js @@ -13,3 +13,4 @@ export function createLinter() { export { analyze } from '@typescript-eslint/scope-manager/dist/analyze'; export { visitorKeys } from '@typescript-eslint/visitor-keys/dist/visitor-keys'; export { astConverter } from '@typescript-eslint/typescript-estree/dist/ast-converter'; +export { getScriptKind } from '@typescript-eslint/typescript-estree/dist/create-program/shared'; diff --git a/packages/website-eslint/types/index.d.ts b/packages/website-eslint/types/index.d.ts index d0ee27a6280..44701962150 100644 --- a/packages/website-eslint/types/index.d.ts +++ b/packages/website-eslint/types/index.d.ts @@ -2,10 +2,12 @@ import type { TSESLint } from '@typescript-eslint/utils'; import { analyze } from '@typescript-eslint/scope-manager/dist/analyze'; import { astConverter } from '@typescript-eslint/typescript-estree/dist/ast-converter'; +import { getScriptKind } from '@typescript-eslint/typescript-estree/dist/create-program/shared'; export interface LintUtils { createLinter: () => TSESLint.Linter; analyze: typeof analyze; visitorKeys: TSESLint.SourceCode.VisitorKeys; astConverter: typeof astConverter; + getScriptKind: typeof getScriptKind; } diff --git a/packages/website/src/components/linter/CompilerHost.ts b/packages/website/src/components/linter/CompilerHost.ts index 8a882644ba9..89a56c37ddc 100644 --- a/packages/website/src/components/linter/CompilerHost.ts +++ b/packages/website/src/components/linter/CompilerHost.ts @@ -1,30 +1,7 @@ -import type { ScriptKind, System, SourceFile, CompilerHost } from 'typescript'; +import type { System, SourceFile, CompilerHost } from 'typescript'; +import type { LintUtils } from '@typescript-eslint/website-eslint'; -function getScriptKind( - ts: typeof import('typescript'), - filePath: string, -): ScriptKind { - const extension = (/(\.[a-z]+)$/.exec(filePath)?.[0] ?? '').toLowerCase(); - - switch (extension) { - case '.ts': - return ts.ScriptKind.TS; - case '.tsx': - return ts.ScriptKind.TSX; - - case '.js': - return ts.ScriptKind.JS; - case '.jsx': - return ts.ScriptKind.JSX; - - case '.json': - return ts.ScriptKind.JSON; - - default: - // unknown extension, force typescript to ignore the file extension, and respect the user's setting - return ts.ScriptKind.TS; - } -} +import { extra } from './config'; /** * Creates an in-memory CompilerHost -which is essentially an extra wrapper to System @@ -36,22 +13,23 @@ function getScriptKind( */ export function createVirtualCompilerHost( sys: System, - ts: typeof import('typescript'), + lintUtils: LintUtils, ): CompilerHost { return { ...sys, getCanonicalFileName: (fileName: string) => fileName, - getDefaultLibFileName: options => '/' + ts.getDefaultLibFileName(options), + getDefaultLibFileName: options => + '/' + window.ts.getDefaultLibFileName(options), getNewLine: () => sys.newLine, getSourceFile(fileName, languageVersionOrOptions): SourceFile | undefined { if (this.fileExists(fileName)) { const file = this.readFile(fileName) ?? ''; - return ts.createSourceFile( + return window.ts.createSourceFile( fileName, file, languageVersionOrOptions, true, - getScriptKind(ts, fileName), + lintUtils.getScriptKind(extra, fileName), ); } return undefined; diff --git a/packages/website/src/components/linter/WebLinter.ts b/packages/website/src/components/linter/WebLinter.ts index bf8061285e8..d3c045eaf42 100644 --- a/packages/website/src/components/linter/WebLinter.ts +++ b/packages/website/src/components/linter/WebLinter.ts @@ -36,7 +36,7 @@ export class WebLinter { this.lintUtils = lintUtils; this.linter = lintUtils.createLinter(); - this.host = createVirtualCompilerHost(system, window.ts); + this.host = createVirtualCompilerHost(system, lintUtils); this.linter.defineParser(PARSER_NAME, { parseForESLint: (text, options?: ParserOptions) => { From a46048a0efdee1c51608690af755723833258d14 Mon Sep 17 00:00:00 2001 From: Armano Date: Wed, 25 May 2022 01:28:31 +0200 Subject: [PATCH 5/5] chore(website): update link to getScriptKind base on changes from #5027 --- packages/website-eslint/src/linter/linter.js | 2 +- packages/website-eslint/types/index.d.ts | 2 +- packages/website/src/components/linter/CompilerHost.ts | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/website-eslint/src/linter/linter.js b/packages/website-eslint/src/linter/linter.js index 2eb7878391c..fede1540f98 100644 --- a/packages/website-eslint/src/linter/linter.js +++ b/packages/website-eslint/src/linter/linter.js @@ -13,4 +13,4 @@ export function createLinter() { export { analyze } from '@typescript-eslint/scope-manager/dist/analyze'; export { visitorKeys } from '@typescript-eslint/visitor-keys/dist/visitor-keys'; export { astConverter } from '@typescript-eslint/typescript-estree/dist/ast-converter'; -export { getScriptKind } from '@typescript-eslint/typescript-estree/dist/create-program/shared'; +export { getScriptKind } from '@typescript-eslint/typescript-estree/dist/create-program/getScriptKind'; diff --git a/packages/website-eslint/types/index.d.ts b/packages/website-eslint/types/index.d.ts index 44701962150..7673f6d10a2 100644 --- a/packages/website-eslint/types/index.d.ts +++ b/packages/website-eslint/types/index.d.ts @@ -2,7 +2,7 @@ import type { TSESLint } from '@typescript-eslint/utils'; import { analyze } from '@typescript-eslint/scope-manager/dist/analyze'; import { astConverter } from '@typescript-eslint/typescript-estree/dist/ast-converter'; -import { getScriptKind } from '@typescript-eslint/typescript-estree/dist/create-program/shared'; +import { getScriptKind } from '@typescript-eslint/typescript-estree/dist/create-program/getScriptKind'; export interface LintUtils { createLinter: () => TSESLint.Linter; diff --git a/packages/website/src/components/linter/CompilerHost.ts b/packages/website/src/components/linter/CompilerHost.ts index 89a56c37ddc..ddf360a2a25 100644 --- a/packages/website/src/components/linter/CompilerHost.ts +++ b/packages/website/src/components/linter/CompilerHost.ts @@ -1,8 +1,6 @@ import type { System, SourceFile, CompilerHost } from 'typescript'; import type { LintUtils } from '@typescript-eslint/website-eslint'; -import { extra } from './config'; - /** * 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 @@ -29,7 +27,7 @@ export function createVirtualCompilerHost( file, languageVersionOrOptions, true, - lintUtils.getScriptKind(extra, fileName), + lintUtils.getScriptKind(fileName, false), ); } return undefined;