From e57c3f0595e60766fe6c6f4b049021de538c56a3 Mon Sep 17 00:00:00 2001 From: Armano Date: Mon, 30 May 2022 21:24:33 +0200 Subject: [PATCH 1/5] chore(website): parse eslint module config on parse and improve visual editor --- .../src/components/config/ConfigEslint.tsx | 21 +++--- .../components/config/ConfigTypeScript.tsx | 16 +++-- .../website/src/components/config/utils.ts | 68 ++++++++++++------- .../src/components/editor/LoadedEditor.tsx | 19 +++++- packages/website/src/components/types.ts | 5 ++ 5 files changed, 82 insertions(+), 47 deletions(-) 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..28912fdf59d1 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,65 @@ 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 = /^\s*(module\.exports\s*=)?\s*(\{[\s\S]*})\s*$/g; + +export function tryParseEslintModule(value: string): string { + try { + if (moduleRegexp.test(value)) { + const newValue = toJson(parse(value.replace(moduleRegexp, '$2'))); + 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 +93,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 4ef26784f9da..49b7d80b62b8 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -26,6 +26,7 @@ import { import { defaultEslintConfig, defaultTsConfig, + tryParseEslintModule, parseESLintRC, parseTSConfig, } from '../config/utils'; @@ -87,13 +88,16 @@ export const LoadedEditor: React.FC = ({ }, []); 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(() => { @@ -154,9 +158,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/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; +}; From d0480dfa56eede0a57395d48e8f9047f55e3e1ae Mon Sep 17 00:00:00 2001 From: Armano Date: Tue, 31 May 2022 02:43:13 +0200 Subject: [PATCH 2/5] fix: remove semicolon from eslint module config --- packages/website/src/components/config/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/website/src/components/config/utils.ts b/packages/website/src/components/config/utils.ts index 28912fdf59d1..44beb417e1fc 100644 --- a/packages/website/src/components/config/utils.ts +++ b/packages/website/src/components/config/utils.ts @@ -46,7 +46,7 @@ export function parseTSConfig(code?: string): TSConfig { return { compilerOptions: {} }; } -const moduleRegexp = /^\s*(module\.exports\s*=)?\s*(\{[\s\S]*})\s*$/g; +const moduleRegexp = /^\s*(module\.exports\s*=)?\s*(\{[\s\S]*})\s*;?\s*$/g; export function tryParseEslintModule(value: string): string { try { From 3d2d91a34afc999bb44bbdfcb37b839d534d2072 Mon Sep 17 00:00:00 2001 From: Armano Date: Tue, 31 May 2022 16:18:24 +0200 Subject: [PATCH 3/5] fix: ensure that fixers are properly applied to undo stack --- packages/website/src/components/editor/LoadedEditor.tsx | 2 +- packages/website/src/components/linter/utils.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/website/src/components/editor/LoadedEditor.tsx b/packages/website/src/components/editor/LoadedEditor.tsx index 49b7d80b62b8..d126b7c8004e 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -84,7 +84,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(() => { 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), + ]); }, }; }) ?? []; From 6e8ff2f10fdd19ecb6747cfb5b09167e17b13480 Mon Sep 17 00:00:00 2001 From: Armano Date: Tue, 31 May 2022 16:20:14 +0200 Subject: [PATCH 4/5] fix: reuse tsconfig and eslintrc as initial config model content --- packages/website/src/components/editor/LoadedEditor.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/website/src/components/editor/LoadedEditor.tsx b/packages/website/src/components/editor/LoadedEditor.tsx index d126b7c8004e..ddef332fcfcd 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -24,8 +24,6 @@ import { LintCodeAction, } from '../linter/utils'; import { - defaultEslintConfig, - defaultTsConfig, tryParseEslintModule, parseESLintRC, parseTSConfig, @@ -63,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'), ), From c6994530d77a7dc71d4b9c3b070d72a405e27583 Mon Sep 17 00:00:00 2001 From: Armano Date: Sun, 5 Jun 2022 19:30:13 +0200 Subject: [PATCH 5/5] fix(website): [playground] use Function to evaluate pasted config file --- packages/website/src/components/config/utils.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/website/src/components/config/utils.ts b/packages/website/src/components/config/utils.ts index 44beb417e1fc..77d06c66b0be 100644 --- a/packages/website/src/components/config/utils.ts +++ b/packages/website/src/components/config/utils.ts @@ -46,12 +46,22 @@ export function parseTSConfig(code?: string): TSConfig { return { compilerOptions: {} }; } -const moduleRegexp = /^\s*(module\.exports\s*=)?\s*(\{[\s\S]*})\s*;?\s*$/g; +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(parse(value.replace(moduleRegexp, '$2'))); + const newValue = toJson(constrainedScopeEval(value)); if (newValue !== value) { return newValue; }