Skip to content

chore(website): [playground] add support for extends in eslint config #6796

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/website-eslint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
25 changes: 22 additions & 3 deletions packages/website-eslint/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<string, unknown>} */
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;
6 changes: 6 additions & 0 deletions packages/website-eslint/types/eslint-js.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
declare module '@eslint/js' {
declare const configs: Record<string, unknown>;
export = {
configs,
};
}
8 changes: 4 additions & 4 deletions packages/website/src/components/editor/LoadedEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export const LoadedEditor: React.FC<LoadedEditorProps> = ({
}, [sandboxInstance, tsconfig, webLinter]);

useEffect(() => {
webLinter.updateRules(parseESLintRC(eslintrc).rules);
webLinter.updateEslintConfig(parseESLintRC(eslintrc));
}, [eslintrc, webLinter]);

useEffect(() => {
Expand All @@ -128,7 +128,7 @@ export const LoadedEditor: React.FC<LoadedEditorProps> = ({

const markers = parseLintResults(messages, codeActions, ruleId =>
sandboxInstance.monaco.Uri.parse(
webLinter.rulesUrl.get(ruleId) ?? '',
webLinter.rulesMap.get(ruleId)?.url ?? '',
),
);

Expand Down Expand Up @@ -185,7 +185,7 @@ export const LoadedEditor: React.FC<LoadedEditorProps> = ({
{
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
Expand Down Expand Up @@ -290,7 +290,7 @@ export const LoadedEditor: React.FC<LoadedEditorProps> = ({
tabs.eslintrc,
tabs.tsconfig,
updateMarkers,
webLinter.ruleNames,
webLinter.rulesMap,
]);

const resize = useMemo(() => {
Expand Down
61 changes: 35 additions & 26 deletions packages/website/src/components/editor/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = {},
Expand Down Expand Up @@ -34,37 +35,45 @@ export function createCompilerOptions(
return options;
}

export function getEslintSchema(
rules: { name: string; description?: string }[],
): JSONSchema4 {
const properties = rules.reduce<Record<string, JSONSchema4>>(
(rules, item) => {
rules[item.name] = {
description: item.description,
export function getEslintSchema(linter: WebLinter): JSONSchema4 {
const properties: Record<string, JSONSchema4> = {};

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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
)
Expand Down
90 changes: 49 additions & 41 deletions packages/website/src/components/linter/WebLinter.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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;
Expand All @@ -24,37 +19,28 @@ export interface LintUtils {
astConverter: typeof astConverter;
getScriptKind: typeof getScriptKind;
esquery: typeof esquery;
configs: Record<string, TSESLint.Linter.Config>;
}

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<string, unknown>;

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<string, string | undefined>();
public readonly rulesMap = new Map<string, RuleDetails>();
public readonly configs: Record<string, TSESLint.Linter.Config> = {};

constructor(
system: System,
compilerOptions: CompilerOptions,
system: ts.System,
compilerOptions: ts.CompilerOptions,
lintUtils: LintUtils,
) {
this.compilerOptions = compilerOptions;
Expand All @@ -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,
Expand All @@ -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;
}

Expand Down Expand Up @@ -161,4 +143,30 @@ export class WebLinter {
visitorKeys: this.lintUtils.visitorKeys,
};
}

private resolveEslintConfig(
cfg: Partial<TSESLint.Linter.Config>,
): 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;
}
}
17 changes: 17 additions & 0 deletions packages/website/src/components/linter/config.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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: {},
};
1 change: 1 addition & 0 deletions packages/website/src/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down