diff --git a/packages/website-eslint/package.json b/packages/website-eslint/package.json index 61b37ca8ff9d..530ad946cb96 100644 --- a/packages/website-eslint/package.json +++ b/packages/website-eslint/package.json @@ -31,6 +31,7 @@ "@typescript-eslint/types": "5.57.0", "@typescript-eslint/utils": "5.57.0", "eslint": "*", + "@eslint/js": "8.36.0", "esbuild": "~0.17.12", "esquery": "*", "semver": "^7.3.7" diff --git a/packages/website-eslint/src/index.js b/packages/website-eslint/src/index.js index c00a82466473..53483c7bb93c 100644 --- a/packages/website-eslint/src/index.js +++ b/packages/website-eslint/src/index.js @@ -5,7 +5,8 @@ This saves us having to mock unnecessary things and reduces our bundle size. */ // @ts-check -import { rules } from '@typescript-eslint/eslint-plugin'; +import eslintJs from '@eslint/js'; +import * as plugin from '@typescript-eslint/eslint-plugin'; import { analyze } from '@typescript-eslint/scope-manager'; import { astConverter, @@ -24,8 +25,26 @@ exports.esquery = esquery; exports.createLinter = function () { const linter = new Linter(); - for (const name in rules) { - linter.defineRule(`@typescript-eslint/${name}`, rules[name]); + for (const name in plugin.rules) { + linter.defineRule(`@typescript-eslint/${name}`, plugin.rules[name]); } return linter; }; + +/** @type {Record} */ +const configs = {}; + +for (const [name, value] of Object.entries(eslintJs.configs)) { + configs[`eslint:${name}`] = value; +} + +for (const [name, value] of Object.entries(plugin.configs)) { + if (value.extends && Array.isArray(value.extends)) { + value.extends = value.extends.map(name => + name.replace(/^\.\/configs\//, 'plugin:@typescript-eslint/'), + ); + } + configs[`plugin:@typescript-eslint/${name}`] = value; +} + +exports.configs = configs; diff --git a/packages/website-eslint/types/eslint-js.d.ts b/packages/website-eslint/types/eslint-js.d.ts new file mode 100644 index 000000000000..561e55b673af --- /dev/null +++ b/packages/website-eslint/types/eslint-js.d.ts @@ -0,0 +1,6 @@ +declare module '@eslint/js' { + declare const configs: Record; + export = { + configs, + }; +} diff --git a/packages/website/src/components/editor/LoadedEditor.tsx b/packages/website/src/components/editor/LoadedEditor.tsx index 402bcab84ba9..e4fae79f61d1 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -109,7 +109,7 @@ export const LoadedEditor: React.FC = ({ }, [sandboxInstance, tsconfig, webLinter]); useEffect(() => { - webLinter.updateRules(parseESLintRC(eslintrc).rules); + webLinter.updateEslintConfig(parseESLintRC(eslintrc)); }, [eslintrc, webLinter]); useEffect(() => { @@ -128,7 +128,7 @@ export const LoadedEditor: React.FC = ({ const markers = parseLintResults(messages, codeActions, ruleId => sandboxInstance.monaco.Uri.parse( - webLinter.rulesUrl.get(ruleId) ?? '', + webLinter.rulesMap.get(ruleId)?.url ?? '', ), ); @@ -185,7 +185,7 @@ export const LoadedEditor: React.FC = ({ { uri: sandboxInstance.monaco.Uri.file('eslint-schema.json').toString(), // id of the first schema fileMatch: [tabs.eslintrc.uri.toString()], // associate with our model - schema: getEslintSchema(webLinter.ruleNames), + schema: getEslintSchema(webLinter), }, { uri: sandboxInstance.monaco.Uri.file('ts-schema.json').toString(), // id of the first schema @@ -290,7 +290,7 @@ export const LoadedEditor: React.FC = ({ tabs.eslintrc, tabs.tsconfig, updateMarkers, - webLinter.ruleNames, + webLinter.rulesMap, ]); const resize = useMemo(() => { diff --git a/packages/website/src/components/editor/config.ts b/packages/website/src/components/editor/config.ts index 1882238595ce..be5c58a2f960 100644 --- a/packages/website/src/components/editor/config.ts +++ b/packages/website/src/components/editor/config.ts @@ -2,6 +2,7 @@ import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'; import type Monaco from 'monaco-editor'; import { getTypescriptOptions } from '../config/utils'; +import type { WebLinter } from '../linter/WebLinter'; export function createCompilerOptions( tsConfig: Record = {}, @@ -34,37 +35,45 @@ export function createCompilerOptions( return options; } -export function getEslintSchema( - rules: { name: string; description?: string }[], -): JSONSchema4 { - const properties = rules.reduce>( - (rules, item) => { - rules[item.name] = { - description: item.description, +export function getEslintSchema(linter: WebLinter): JSONSchema4 { + const properties: Record = {}; + + for (const [, item] of linter.rulesMap) { + properties[item.name] = { + description: `${item.description}\n ${item.url}`, + title: item.name.startsWith('@typescript') ? 'Rules' : 'Core rules', + default: 'off', + oneOf: [ + { + type: ['string', 'number'], + enum: ['off', 'warn', 'error', 0, 1, 2], + }, + { + type: 'array', + items: [ + { + type: ['string', 'number'], + enum: ['off', 'warn', 'error', 0, 1, 2], + }, + ], + }, + ], + }; + } + + return { + type: 'object', + properties: { + extends: { oneOf: [ - { - type: ['string', 'number'], - enum: ['off', 'warn', 'error', 0, 1, 2], - }, + { type: 'string' }, { type: 'array', - items: [ - { - type: ['string', 'number'], - enum: ['off', 'warn', 'error', 0, 1, 2], - }, - ], + items: { type: 'string', enum: Object.keys(linter.configs) }, + uniqueItems: true, }, ], - }; - return rules; - }, - {}, - ); - - return { - type: 'object', - properties: { + }, rules: { type: 'object', properties: properties, diff --git a/packages/website/src/components/editor/useSandboxServices.ts b/packages/website/src/components/editor/useSandboxServices.ts index 6bcc9d01b885..eeb65747d6a2 100644 --- a/packages/website/src/components/editor/useSandboxServices.ts +++ b/packages/website/src/components/editor/useSandboxServices.ts @@ -85,7 +85,7 @@ export const useSandboxServices = ( const webLinter = new WebLinter(system, compilerOptions, lintUtils); onLoaded( - webLinter.ruleNames, + Array.from(webLinter.rulesMap.values()), Array.from( new Set([...sandboxInstance.supportedVersions, window.ts.version]), ) diff --git a/packages/website/src/components/linter/WebLinter.ts b/packages/website/src/components/linter/WebLinter.ts index 2218460ba170..f789a7417173 100644 --- a/packages/website/src/components/linter/WebLinter.ts +++ b/packages/website/src/components/linter/WebLinter.ts @@ -1,5 +1,3 @@ -import { createVirtualCompilerHost } from '@site/src/components/linter/CompilerHost'; -import { parseSettings } from '@site/src/components/linter/config'; import type { analyze } from '@typescript-eslint/scope-manager'; import type { ParserOptions } from '@typescript-eslint/types'; import type { @@ -8,14 +6,11 @@ import type { } from '@typescript-eslint/typescript-estree/use-at-your-own-risk'; import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import type esquery from 'esquery'; -import type { - CompilerHost, - CompilerOptions, - SourceFile, - System, -} from 'typescript'; +import type * as ts from 'typescript'; -const PARSER_NAME = '@typescript-eslint/parser'; +import type { EslintRC, RuleDetails } from '../types'; +import { createVirtualCompilerHost } from './CompilerHost'; +import { eslintConfig, PARSER_NAME, parseSettings } from './config'; export interface LintUtils { createLinter: () => TSESLint.Linter; @@ -24,37 +19,28 @@ export interface LintUtils { astConverter: typeof astConverter; getScriptKind: typeof getScriptKind; esquery: typeof esquery; + configs: Record; } export class WebLinter { - private readonly host: CompilerHost; + private readonly host: ts.CompilerHost; public storedAST?: TSESTree.Program; - public storedTsAST?: SourceFile; + public storedTsAST?: ts.SourceFile; public storedScope?: Record; - private compilerOptions: CompilerOptions; - private readonly parserOptions: ParserOptions = { - ecmaFeatures: { - jsx: true, - globalReturn: false, - }, - comment: true, - ecmaVersion: 'latest', - project: ['./tsconfig.json'], - sourceType: 'module', - }; + private compilerOptions: ts.CompilerOptions; + private eslintConfig = eslintConfig; private linter: TSESLint.Linter; private lintUtils: LintUtils; - private rules: TSESLint.Linter.RulesRecord = {}; - public readonly ruleNames: { name: string; description?: string }[] = []; - public readonly rulesUrl = new Map(); + public readonly rulesMap = new Map(); + public readonly configs: Record = {}; constructor( - system: System, - compilerOptions: CompilerOptions, + system: ts.System, + compilerOptions: ts.CompilerOptions, lintUtils: LintUtils, ) { this.compilerOptions = compilerOptions; @@ -69,23 +55,17 @@ export class WebLinter { }, }); + this.configs = lintUtils.configs; + this.linter.getRules().forEach((item, name) => { - this.ruleNames.push({ + this.rulesMap.set(name, { name: name, description: item.meta?.docs?.description, + url: item.meta?.docs?.url, }); - this.rulesUrl.set(name, item.meta?.docs?.url); }); } - get eslintConfig(): TSESLint.Linter.Config { - return { - parser: PARSER_NAME, - parserOptions: this.parserOptions, - rules: this.rules, - }; - } - lint(code: string, filename: string): TSESLint.Linter.LintMessage[] { return this.linter.verify(code, this.eslintConfig, { filename: filename, @@ -99,15 +79,17 @@ export class WebLinter { }); } - updateRules(rules: TSESLint.Linter.RulesRecord): void { - this.rules = rules; + updateEslintConfig(config: EslintRC): void { + const resolvedConfig = this.resolveEslintConfig(config); + this.eslintConfig.rules = resolvedConfig.rules; } updateParserOptions(sourceType?: TSESLint.SourceType): void { - this.parserOptions.sourceType = sourceType ?? 'module'; + this.eslintConfig.parserOptions ??= {}; + this.eslintConfig.parserOptions.sourceType = sourceType ?? 'module'; } - updateCompilerOptions(options: CompilerOptions = {}): void { + updateCompilerOptions(options: ts.CompilerOptions = {}): void { this.compilerOptions = options; } @@ -161,4 +143,30 @@ export class WebLinter { visitorKeys: this.lintUtils.visitorKeys, }; } + + private resolveEslintConfig( + cfg: Partial, + ): TSESLint.Linter.Config { + const config = { + rules: {}, + overrides: [], + }; + if (cfg.extends) { + const cfgExtends = Array.isArray(cfg.extends) + ? cfg.extends + : [cfg.extends]; + for (const extendsName of cfgExtends) { + if (typeof extendsName === 'string' && extendsName in this.configs) { + const resolved = this.resolveEslintConfig(this.configs[extendsName]); + if (resolved.rules) { + Object.assign(config.rules, resolved.rules); + } + } + } + } + if (cfg.rules) { + Object.assign(config.rules, cfg.rules); + } + return config; + } } diff --git a/packages/website/src/components/linter/config.ts b/packages/website/src/components/linter/config.ts index 5cf9b5cccd9c..641adc873c1b 100644 --- a/packages/website/src/components/linter/config.ts +++ b/packages/website/src/components/linter/config.ts @@ -1,4 +1,7 @@ import type { ParseSettings } from '@typescript-eslint/typescript-estree/use-at-your-own-risk'; +import type { TSESLint } from '@typescript-eslint/utils'; + +export const PARSER_NAME = '@typescript-eslint/parser'; export const parseSettings: ParseSettings = { allowInvalidAST: false, @@ -26,3 +29,17 @@ export const parseSettings: ParseSettings = { tsconfigMatchCache: new Map(), tsconfigRootDir: '/', }; + +export const eslintConfig: TSESLint.Linter.Config = { + parser: PARSER_NAME, + parserOptions: { + ecmaFeatures: { + jsx: false, + globalReturn: false, + }, + ecmaVersion: 'latest', + project: ['./tsconfig.json'], + sourceType: 'module', + }, + rules: {}, +}; diff --git a/packages/website/src/components/types.ts b/packages/website/src/components/types.ts index 84156fe4924e..8a128da07b14 100644 --- a/packages/website/src/components/types.ts +++ b/packages/website/src/components/types.ts @@ -11,6 +11,7 @@ export type RuleEntry = TSESLint.Linter.RuleEntry; export interface RuleDetails { name: string; description?: string; + url?: string; } export type TabType = 'code' | 'tsconfig' | 'eslintrc';