From 4c9bea608a584e49a016332402a57d979eb80ce2 Mon Sep 17 00:00:00 2001 From: Armano Date: Thu, 30 Mar 2023 19:33:23 +0200 Subject: [PATCH 1/3] chore(website): [playground] add support for extends in eslint config --- packages/website-eslint/package.json | 1 + packages/website-eslint/src/index.js | 26 +++- packages/website-eslint/types/eslint-js.d.ts | 6 + .../src/components/editor/LoadedEditor.tsx | 15 ++- .../website/src/components/editor/config.ts | 61 +++++---- .../components/editor/useSandboxServices.ts | 2 +- .../src/components/linter/WebLinter.ts | 121 +++++++++++------- .../website/src/components/linter/config.ts | 17 +++ packages/website/src/components/types.ts | 5 +- 9 files changed, 169 insertions(+), 85 deletions(-) create mode 100644 packages/website-eslint/types/eslint-js.d.ts diff --git a/packages/website-eslint/package.json b/packages/website-eslint/package.json index f98341c5701d..15e31a5d5763 100644 --- a/packages/website-eslint/package.json +++ b/packages/website-eslint/package.json @@ -31,6 +31,7 @@ "@typescript-eslint/types": "5.56.0", "@typescript-eslint/utils": "5.56.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..8e780dd4920f 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,27 @@ 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 in eslintJs.configs) { + configs[`eslint:${name}`] = eslintJs.configs[name]; +} + +for (const name in plugin.configs) { + const value = plugin.configs[name]; + 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 edfb49929c51..0fcb5bd696f6 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -116,7 +116,7 @@ export const LoadedEditor: React.FC = ({ }, [jsx, sandboxInstance, tsconfig, webLinter]); useEffect(() => { - webLinter.updateRules(parseESLintRC(eslintrc).rules); + webLinter.updateEslintConfig(parseESLintRC(eslintrc)); }, [eslintrc, webLinter]); useEffect(() => { @@ -131,11 +131,11 @@ export const LoadedEditor: React.FC = ({ webLinter.updateParserOptions(jsx, sourceType); try { - const messages = webLinter.lint(code); + const messages = webLinter.lint(code, tabs.code.uri.path); const markers = parseLintResults(messages, codeActions, ruleId => sandboxInstance.monaco.Uri.parse( - webLinter.rulesUrl.get(ruleId) ?? '', + webLinter.rulesMap.get(ruleId)?.url ?? '', ), ); @@ -190,7 +190,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 @@ -237,7 +237,10 @@ export const LoadedEditor: React.FC = ({ run(editor) { const editorModel = editor.getModel(); if (editorModel) { - const fixed = webLinter.fix(editor.getValue()); + const fixed = webLinter.fix( + editor.getValue(), + editorModel.uri.path, + ); if (fixed.fixed) { editorModel.pushEditOperations( null, @@ -292,7 +295,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 aee01fb11f07..a82651eab703 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( jsx = false, @@ -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 3007856e54ac..2eae7275f127 100644 --- a/packages/website/src/components/editor/useSandboxServices.ts +++ b/packages/website/src/components/editor/useSandboxServices.ts @@ -93,7 +93,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 e8a92a1003c3..d854705d3f6a 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,19 @@ 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'; + +import type { EslintRC } from '../types'; +import { createVirtualCompilerHost } from './CompilerHost'; +import { eslintConfig, PARSER_NAME, parseSettings } from './config'; + +export interface RuleDetails { + name: string; + description?: string; + url?: string; +} -const PARSER_NAME = '@typescript-eslint/parser'; +export type RulesMap = Map; export interface LintUtils { createLinter: () => TSESLint.Linter; @@ -24,36 +27,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: false, - globalReturn: false, - }, - 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: RulesMap = new Map(); + public readonly configs: Record = {}; constructor( - system: System, - compilerOptions: CompilerOptions, + system: ts.System, + compilerOptions: ts.CompilerOptions, lintUtils: LintUtils, ) { this.compilerOptions = compilerOptions; @@ -68,41 +63,45 @@ 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): TSESLint.Linter.LintMessage[] { - return this.linter.verify(code, this.eslintConfig); + lint(code: string, filename: string): TSESLint.Linter.LintMessage[] { + return this.linter.verify(code, this.eslintConfig, { + filename, + }); } - fix(code: string): TSESLint.Linter.FixReport { - return this.linter.verifyAndFix(code, this.eslintConfig, { fix: true }); + fix(code: string, filename: string): TSESLint.Linter.FixReport { + return this.linter.verifyAndFix(code, this.eslintConfig, { + fix: true, + filename, + }); } - updateRules(rules: TSESLint.Linter.RulesRecord): void { - this.rules = rules; + updateEslintConfig(config: EslintRC): void { + const resolvedConfig = this.resolveEslintConfig(config); + this.eslintConfig.rules = resolvedConfig.rules; + // TODO: overrides are not fully supported yet + this.eslintConfig.overrides = resolvedConfig.overrides; } updateParserOptions(jsx?: boolean, sourceType?: TSESLint.SourceType): void { - this.parserOptions.ecmaFeatures!.jsx = jsx ?? false; - this.parserOptions.sourceType = sourceType ?? 'module'; + this.eslintConfig.parserOptions ??= {}; + this.eslintConfig.parserOptions.ecmaFeatures ??= {}; + this.eslintConfig.parserOptions.ecmaFeatures.jsx = jsx ?? false; + this.eslintConfig.parserOptions.sourceType = sourceType ?? 'module'; } - updateCompilerOptions(options: CompilerOptions = {}): void { + updateCompilerOptions(options: ts.CompilerOptions = {}): void { this.compilerOptions = options; } @@ -157,4 +156,36 @@ 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 (resolved.overrides) { + Object.assign(config.overrides, resolved.overrides); + } + } + } + } + if (cfg.rules) { + Object.assign(config.rules, cfg.rules); + } + if (cfg.overrides) { + Object.assign(config.overrides, cfg.overrides); + } + return config; + } } diff --git a/packages/website/src/components/linter/config.ts b/packages/website/src/components/linter/config.ts index 93466f9451ab..5adf16114556 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 b63d9bca307b..b771c5a4440c 100644 --- a/packages/website/src/components/types.ts +++ b/packages/website/src/components/types.ts @@ -7,10 +7,7 @@ export type SourceType = TSESLint.SourceType; export type RulesRecord = TSESLint.Linter.RulesRecord; export type RuleEntry = TSESLint.Linter.RuleEntry; -export interface RuleDetails { - name: string; - description?: string; -} +export type { RuleDetails } from './linter/WebLinter'; export type TabType = 'code' | 'tsconfig' | 'eslintrc'; From d859a5000547f5974118ab607b45bc3df12095fa Mon Sep 17 00:00:00 2001 From: Armano Date: Sun, 2 Apr 2023 23:51:07 +0200 Subject: [PATCH 2/3] fix: remove overrides --- packages/website/src/components/linter/WebLinter.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/website/src/components/linter/WebLinter.ts b/packages/website/src/components/linter/WebLinter.ts index bb70e0a46e5c..f789a7417173 100644 --- a/packages/website/src/components/linter/WebLinter.ts +++ b/packages/website/src/components/linter/WebLinter.ts @@ -82,8 +82,6 @@ export class WebLinter { updateEslintConfig(config: EslintRC): void { const resolvedConfig = this.resolveEslintConfig(config); this.eslintConfig.rules = resolvedConfig.rules; - // TODO: overrides are not fully supported yet - this.eslintConfig.overrides = resolvedConfig.overrides; } updateParserOptions(sourceType?: TSESLint.SourceType): void { @@ -163,18 +161,12 @@ export class WebLinter { if (resolved.rules) { Object.assign(config.rules, resolved.rules); } - if (resolved.overrides) { - Object.assign(config.overrides, resolved.overrides); - } } } } if (cfg.rules) { Object.assign(config.rules, cfg.rules); } - if (cfg.overrides) { - Object.assign(config.overrides, cfg.overrides); - } return config; } } From 4ec8d97ece6fc94a4715b14dafa4d86b522ff345 Mon Sep 17 00:00:00 2001 From: Armano Date: Mon, 3 Apr 2023 00:13:15 +0200 Subject: [PATCH 3/3] fix: change `for in` with `for of Object.entries` --- packages/website-eslint/src/index.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/website-eslint/src/index.js b/packages/website-eslint/src/index.js index 8e780dd4920f..53483c7bb93c 100644 --- a/packages/website-eslint/src/index.js +++ b/packages/website-eslint/src/index.js @@ -34,12 +34,11 @@ exports.createLinter = function () { /** @type {Record} */ const configs = {}; -for (const name in eslintJs.configs) { - configs[`eslint:${name}`] = eslintJs.configs[name]; +for (const [name, value] of Object.entries(eslintJs.configs)) { + configs[`eslint:${name}`] = value; } -for (const name in plugin.configs) { - const value = plugin.configs[name]; +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/'),