diff --git a/packages/website/src/components/config/ConfigEslint.tsx b/packages/website/src/components/config/ConfigEslint.tsx index 3ffc57cb35ff..a25046b71103 100644 --- a/packages/website/src/components/config/ConfigEslint.tsx +++ b/packages/website/src/components/config/ConfigEslint.tsx @@ -1,14 +1,9 @@ import React, { useCallback, useEffect, useState } from 'react'; import ConfigEditor, { ConfigOptionsType } from './ConfigEditor'; -import type { - RulesRecord, - RuleDetails, - RuleEntry, - ConfigModel, -} from '../types'; +import type { RuleDetails, RuleEntry, ConfigModel, EslintRC } from '../types'; import { shallowEqual } from '../lib/shallowEqual'; -import { parseESLintRC, toJsonConfig } from '@site/src/components/config/utils'; +import { parseESLintRC, toJson } from './utils'; export interface ConfigEslintProps { readonly isOpen: boolean; @@ -33,11 +28,11 @@ function checkOptions(rule: [string, unknown]): rule is [string, RuleEntry] { function ConfigEslint(props: ConfigEslintProps): JSX.Element { const [options, updateOptions] = useState([]); - const [configObject, updateConfigObject] = useState({}); + const [configObject, updateConfigObject] = useState(); useEffect(() => { if (props.isOpen) { - updateConfigObject(props.config ? parseESLintRC(props.config) : {}); + updateConfigObject(parseESLintRC(props.config)); } }, [props.isOpen, props.config]); @@ -77,8 +72,10 @@ function ConfigEslint(props: ConfigEslintProps): JSX.Element { ) .filter(checkOptions), ); - if (!shallowEqual(cfg, configObject)) { - props.onClose({ eslintrc: toJsonConfig(cfg, 'rules') }); + if (!shallowEqual(cfg, configObject?.rules)) { + props.onClose({ + eslintrc: toJson({ ...(configObject ?? {}), rules: cfg }), + }); } else { props.onClose(); } @@ -90,7 +87,7 @@ function ConfigEslint(props: ConfigEslintProps): JSX.Element { diff --git a/packages/website/src/components/config/ConfigTypeScript.tsx b/packages/website/src/components/config/ConfigTypeScript.tsx index 78f547088b66..8d64045ac4b8 100644 --- a/packages/website/src/components/config/ConfigTypeScript.tsx +++ b/packages/website/src/components/config/ConfigTypeScript.tsx @@ -1,9 +1,9 @@ import React, { useCallback, useEffect, useState } from 'react'; import ConfigEditor, { ConfigOptionsType } from './ConfigEditor'; -import type { CompilerFlags, ConfigModel } from '../types'; +import type { ConfigModel, TSConfig } from '../types'; import { shallowEqual } from '../lib/shallowEqual'; -import { getTypescriptOptions, parseTSConfig, toJsonConfig } from './utils'; +import { getTypescriptOptions, parseTSConfig, toJson } from './utils'; interface ConfigTypeScriptProps { readonly isOpen: boolean; @@ -17,11 +17,11 @@ function checkOptions(item: [string, unknown]): item is [string, boolean] { function ConfigTypeScript(props: ConfigTypeScriptProps): JSX.Element { const [tsConfigOptions, updateOptions] = useState([]); - const [configObject, updateConfigObject] = useState({}); + const [configObject, updateConfigObject] = useState(); useEffect(() => { if (props.isOpen) { - updateConfigObject(props.config ? parseTSConfig(props.config) : {}); + updateConfigObject(parseTSConfig(props.config)); } }, [props.isOpen, props.config]); @@ -54,8 +54,10 @@ function ConfigTypeScript(props: ConfigTypeScriptProps): JSX.Element { const cfg = Object.fromEntries( Object.entries(newConfig).filter(checkOptions), ); - if (!shallowEqual(cfg, configObject)) { - props.onClose({ tsconfig: toJsonConfig(cfg, 'compilerOptions') }); + if (!shallowEqual(cfg, configObject?.compilerOptions)) { + props.onClose({ + tsconfig: toJson({ ...(configObject ?? {}), compilerOptions: cfg }), + }); } else { props.onClose(); } @@ -67,7 +69,7 @@ function ConfigTypeScript(props: ConfigTypeScriptProps): JSX.Element { diff --git a/packages/website/src/components/config/utils.ts b/packages/website/src/components/config/utils.ts index 9ee188b6152e..77d06c66b0be 100644 --- a/packages/website/src/components/config/utils.ts +++ b/packages/website/src/components/config/utils.ts @@ -1,4 +1,4 @@ -import type { CompilerFlags, RulesRecord } from '@site/src/components/types'; +import type { EslintRC, TSConfig } from '@site/src/components/types'; import { parse } from 'json5'; import { isRecord } from '@site/src/components/ast/utils'; @@ -10,48 +10,75 @@ export interface OptionDeclarations { description?: { message: string }; } -export function parseESLintRC(code?: string): RulesRecord { +export function parseESLintRC(code?: string): EslintRC { if (code) { try { const parsed: unknown = parse(code); - if (isRecord(parsed) && 'rules' in parsed && isRecord(parsed.rules)) { - return parsed.rules as RulesRecord; + if (isRecord(parsed)) { + if ('rules' in parsed && isRecord(parsed.rules)) { + return parsed as EslintRC; + } + return { ...parsed, rules: {} }; } } catch (e) { // eslint-disable-next-line no-console console.error(e); } } - return {}; + return { rules: {} }; } -export function parseTSConfig(code?: string): CompilerFlags { +export function parseTSConfig(code?: string): TSConfig { if (code) { try { const parsed: unknown = parse(code); - if ( - isRecord(parsed) && - 'compilerOptions' in parsed && - isRecord(parsed.compilerOptions) - ) { - return parsed.compilerOptions as CompilerFlags; + if (isRecord(parsed)) { + if ('compilerOptions' in parsed && isRecord(parsed.compilerOptions)) { + return parsed as TSConfig; + } + return { ...parsed, compilerOptions: {} }; } } catch (e) { // eslint-disable-next-line no-console console.error(e); } } - return {}; + return { compilerOptions: {} }; +} + +const moduleRegexp = /(module\.exports\s*=)/g; + +function constrainedScopeEval(obj: string): unknown { + // eslint-disable-next-line @typescript-eslint/no-implied-eval + return new Function(` + "use strict"; + var module = { exports: {} }; + (${obj}); + return module.exports + `)(); +} + +export function tryParseEslintModule(value: string): string { + try { + if (moduleRegexp.test(value)) { + const newValue = toJson(constrainedScopeEval(value)); + if (newValue !== value) { + return newValue; + } + } + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } + return value; +} + +export function toJson(cfg: unknown): string { + return JSON.stringify(cfg, null, 2); } export function toJsonConfig(cfg: unknown, prop: string): string { - return JSON.stringify( - { - [prop]: cfg, - }, - null, - 2, - ); + return toJson({ [prop]: cfg }); } export function getTypescriptOptions(): OptionDeclarations[] { @@ -76,11 +103,12 @@ export function getTypescriptOptions(): OptionDeclarations[] { ); } -export const defaultTsConfig = toJsonConfig( - { +export const defaultTsConfig = toJson({ + compilerOptions: { strictNullChecks: true, }, - 'compilerOptions', -); +}); -export const defaultEslintConfig = toJsonConfig({}, 'rules'); +export const defaultEslintConfig = toJson({ + rules: {}, +}); diff --git a/packages/website/src/components/editor/LoadedEditor.tsx b/packages/website/src/components/editor/LoadedEditor.tsx index 0269c62e7f5f..bdc0138f7ac2 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -24,8 +24,7 @@ import { LintCodeAction, } from '../linter/utils'; import { - defaultEslintConfig, - defaultTsConfig, + tryParseEslintModule, parseESLintRC, parseTSConfig, } from '../config/utils'; @@ -62,12 +61,12 @@ export const LoadedEditor: React.FC = ({ const tabsDefault = { code: sandboxInstance.editor.getModel()!, tsconfig: sandboxInstance.monaco.editor.createModel( - defaultTsConfig, + tsconfig, 'json', sandboxInstance.monaco.Uri.file('./tsconfig.json'), ), eslintrc: sandboxInstance.monaco.editor.createModel( - defaultEslintConfig, + eslintrc, 'json', sandboxInstance.monaco.Uri.file('./.eslintrc'), ), @@ -83,7 +82,7 @@ export const LoadedEditor: React.FC = ({ const markers = sandboxInstance.monaco.editor.getModelMarkers({ resource: model.uri, }); - onMarkersChange(parseMarkers(markers, codeActions, model)); + onMarkersChange(parseMarkers(markers, codeActions, sandboxInstance.editor)); }, []); useEffect(() => { @@ -95,20 +94,25 @@ export const LoadedEditor: React.FC = ({ sandboxInstance.monaco.Uri.file(newPath), ); newModel.updateOptions({ tabSize: 2, insertSpaces: true }); - sandboxInstance.editor.setModel(newModel); + if (tabs.code.isAttachedToEditor()) { + sandboxInstance.editor.setModel(newModel); + } tabs.code.dispose(); tabs.code = newModel; } }, [jsx]); useEffect(() => { - const config = createCompilerOptions(jsx, parseTSConfig(tsconfig)); + const config = createCompilerOptions( + jsx, + parseTSConfig(tsconfig).compilerOptions, + ); webLinter.updateCompilerOptions(config); sandboxInstance.setCompilerSettings(config); }, [jsx, tsconfig]); useEffect(() => { - webLinter.updateRules(parseESLintRC(eslintrc)); + webLinter.updateRules(parseESLintRC(eslintrc).rules); }, [eslintrc]); useEffect(() => { @@ -169,9 +173,18 @@ export const LoadedEditor: React.FC = ({ 'typescript', createProvideCodeActions(codeActions), ), + sandboxInstance.editor.onDidPaste(() => { + if (tabs.eslintrc.isAttachedToEditor()) { + const value = tabs.eslintrc.getValue(); + const newValue = tryParseEslintModule(value); + if (newValue !== value) { + tabs.eslintrc.setValue(newValue); + } + } + }), sandboxInstance.editor.onDidChangeCursorPosition( debounce(() => { - if (sandboxInstance.editor.getModel() === tabs.code) { + if (tabs.code.isAttachedToEditor()) { const position = sandboxInstance.editor.getPosition(); if (position) { // eslint-disable-next-line no-console diff --git a/packages/website/src/components/linter/utils.ts b/packages/website/src/components/linter/utils.ts index 7c7056a62821..d51d86b0867a 100644 --- a/packages/website/src/components/linter/utils.ts +++ b/packages/website/src/components/linter/utils.ts @@ -48,7 +48,7 @@ export function createEditOperation( export function parseMarkers( markers: Monaco.editor.IMarker[], fixes: Map, - model: Monaco.editor.ITextModel, + editor: Monaco.editor.IStandaloneCodeEditor, ): ErrorItem[] { return markers.map(marker => { const code = @@ -60,7 +60,9 @@ export function parseMarkers( return { message: item.message, fix(): void { - model.applyEdits([createEditOperation(model, item)]); + editor.executeEdits('eslint', [ + createEditOperation(editor.getModel()!, item), + ]); }, }; }) ?? []; diff --git a/packages/website/src/components/types.ts b/packages/website/src/components/types.ts index c517353a66bf..ef6cf70dcad4 100644 --- a/packages/website/src/components/types.ts +++ b/packages/website/src/components/types.ts @@ -42,3 +42,8 @@ export interface ErrorItem { hasFixers: boolean; fixers: { message: string; fix(): void }[]; } + +export type EslintRC = Record & { rules: RulesRecord }; +export type TSConfig = Record & { + compilerOptions: CompilerFlags; +};