diff --git a/packages/website/src/components/editor/LoadedEditor.tsx b/packages/website/src/components/editor/LoadedEditor.tsx index 994561e7c318..f8c88d3d5f0a 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -8,6 +8,7 @@ import { createCompilerOptions } from '../lib/createCompilerOptions'; import { debounce } from '../lib/debounce'; import { getEslintJsonSchema, + getRuleJsonSchemaWithErrorLevel, getTypescriptJsonSchema, } from '../lib/jsonSchema'; import { parseTSConfig, tryParseEslintModule } from '../lib/parseConfig'; @@ -147,16 +148,23 @@ export const LoadedEditor: React.FC = ({ }, [webLinter, onEsASTChange, onScopeChange, onTsASTChange]); useEffect(() => { + const createRuleUri = (name: string): string => + monaco.Uri.parse(`/rules/${name.replace('@', '')}.json`).toString(); + // configure the JSON language support with schemas and schema associations monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ validate: true, enableSchemaRequest: false, allowComments: true, schemas: [ + ...Array.from(webLinter.rules.values()).map(rule => ({ + uri: createRuleUri(rule.name), + schema: getRuleJsonSchemaWithErrorLevel(rule.name, rule.schema), + })), { uri: monaco.Uri.file('eslint-schema.json').toString(), // id of the first schema fileMatch: ['/.eslintrc'], // associate with our model - schema: getEslintJsonSchema(webLinter), + schema: getEslintJsonSchema(webLinter, createRuleUri), }, { uri: monaco.Uri.file('ts-schema.json').toString(), // id of the first schema diff --git a/packages/website/src/components/editor/useSandboxServices.ts b/packages/website/src/components/editor/useSandboxServices.ts index 21a7f51ea471..027e12d918cf 100644 --- a/packages/website/src/components/editor/useSandboxServices.ts +++ b/packages/website/src/components/editor/useSandboxServices.ts @@ -125,6 +125,7 @@ export const useSandboxServices = ( }; // colorMode and jsx can't be reactive here because we don't want to force a recreation // updating of colorMode and jsx is handled in LoadedEditor + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return services; diff --git a/packages/website/src/components/lib/jsonSchema.ts b/packages/website/src/components/lib/jsonSchema.ts index b43b6ad72cb9..db11292c0847 100644 --- a/packages/website/src/components/lib/jsonSchema.ts +++ b/packages/website/src/components/lib/jsonSchema.ts @@ -3,11 +3,98 @@ 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], +}; + +/** + * Add the error level to the rule schema items + * + * if you encounter issues with rule schema validation you can check the schema by using the following code in the console: + * monaco.languages.json.jsonDefaults.diagnosticsOptions.schemas.find(item => item.uri.includes('typescript-eslint/consistent-type-imports')) + * monaco.languages.json.jsonDefaults.diagnosticsOptions.schemas.find(item => item.uri.includes('no-unused-labels')) + * monaco.languages.json.jsonDefaults.diagnosticsOptions.schemas.filter(item => item.schema.type === 'array') + */ +export function getRuleJsonSchemaWithErrorLevel( + name: string, + ruleSchema: JSONSchema4 | JSONSchema4[], +): JSONSchema4 { + if (Array.isArray(ruleSchema)) { + return { + type: 'array', + items: [defaultRuleSchema, ...ruleSchema], + minItems: 1, + additionalItems: false, + }; + } + // TODO: delete this once we update schemas + // example: ban-ts-comment + if (Array.isArray(ruleSchema.prefixItems)) { + const { prefixItems, ...rest } = ruleSchema; + return { + ...rest, + items: [defaultRuleSchema, ...(prefixItems as JSONSchema4[])], + maxItems: ruleSchema.maxItems ? ruleSchema.maxItems + 1 : undefined, + minItems: ruleSchema.minItems ? ruleSchema.minItems + 1 : 1, + additionalItems: false, + }; + } + // example: explicit-member-accessibility + if (Array.isArray(ruleSchema.items)) { + return { + ...ruleSchema, + items: [defaultRuleSchema, ...ruleSchema.items], + maxItems: ruleSchema.maxItems ? ruleSchema.maxItems + 1 : undefined, + minItems: ruleSchema.minItems ? ruleSchema.minItems + 1 : 1, + additionalItems: false, + }; + } + if (typeof ruleSchema.items === 'object' && ruleSchema.items) { + // example: naming-convention rule + return { + ...ruleSchema, + items: [defaultRuleSchema], + additionalItems: ruleSchema.items, + }; + } + // example eqeqeq + if (Array.isArray(ruleSchema.anyOf)) { + return { + ...ruleSchema, + anyOf: ruleSchema.anyOf.map(item => + getRuleJsonSchemaWithErrorLevel(name, item), + ), + }; + } + // example logical-assignment-operators + if (Array.isArray(ruleSchema.oneOf)) { + return { + ...ruleSchema, + oneOf: ruleSchema.oneOf.map(item => + getRuleJsonSchemaWithErrorLevel(name, item), + ), + }; + } + if (typeof ruleSchema !== 'object' || Object.keys(ruleSchema).length) { + console.error('unsupported rule schema', name, ruleSchema); + } + return { + type: 'array', + items: [defaultRuleSchema], + minItems: 1, + additionalItems: false, + }; +} + /** * Get the JSON schema for the eslint config * Currently we only support the rules and extends */ -export function getEslintJsonSchema(linter: CreateLinter): JSONSchema4 { +export function getEslintJsonSchema( + linter: CreateLinter, + createRef: (name: string) => string, +): JSONSchema4 { const properties: Record = {}; for (const [, item] of linter.rules) { @@ -15,21 +102,7 @@ export function getEslintJsonSchema(linter: CreateLinter): JSONSchema4 { 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], - }, - ], - }, - ], + oneOf: [defaultRuleSchema, { $ref: createRef(item.name) }], }; } diff --git a/packages/website/src/components/linter/createLinter.ts b/packages/website/src/components/linter/createLinter.ts index 813fd10eaf24..30ba20fe1a43 100644 --- a/packages/website/src/components/linter/createLinter.ts +++ b/packages/website/src/components/linter/createLinter.ts @@ -1,5 +1,5 @@ import type * as tsvfs from '@site/src/vendor/typescript-vfs'; -import type { TSESLint } from '@typescript-eslint/utils'; +import type { JSONSchema, TSESLint } from '@typescript-eslint/utils'; import type * as ts from 'typescript'; import { createCompilerOptions } from '../lib/createCompilerOptions'; @@ -15,7 +15,15 @@ import type { } from './types'; export interface CreateLinter { - rules: Map; + rules: Map< + string, + { + name: string; + description?: string; + url?: string; + schema: JSONSchema.JSONSchema4; + } + >; configs: string[]; triggerFix(filename: string): TSESLint.Linter.FixReport | undefined; triggerLint(filename: string): void; @@ -56,6 +64,7 @@ export function createLinter( name: name, description: item.meta?.docs?.description, url: item.meta?.docs?.url, + schema: item.meta?.schema ?? [], }); }); diff --git a/packages/website/src/components/linter/utils.ts b/packages/website/src/components/linter/utils.ts index e2a4b3ed1c72..474e28cca8fb 100644 --- a/packages/website/src/components/linter/utils.ts +++ b/packages/website/src/components/linter/utils.ts @@ -102,8 +102,9 @@ export function parseMarkers( result[group].items.push({ message: - (marker.owner !== 'eslint' && code ? `${code.value}: ` : '') + - marker.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),