Skip to content

chore(website): [playground] parse eslint module config on paste and improve visual editor #5112

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 8 commits into from
Jun 10, 2022
21 changes: 9 additions & 12 deletions packages/website/src/components/config/ConfigEslint.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -33,11 +28,11 @@ function checkOptions(rule: [string, unknown]): rule is [string, RuleEntry] {

function ConfigEslint(props: ConfigEslintProps): JSX.Element {
const [options, updateOptions] = useState<ConfigOptionsType[]>([]);
const [configObject, updateConfigObject] = useState<RulesRecord>({});
const [configObject, updateConfigObject] = useState<EslintRC>();

useEffect(() => {
if (props.isOpen) {
updateConfigObject(props.config ? parseESLintRC(props.config) : {});
updateConfigObject(parseESLintRC(props.config));
}
}, [props.isOpen, props.config]);

Expand Down Expand Up @@ -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();
}
Expand All @@ -90,7 +87,7 @@ function ConfigEslint(props: ConfigEslintProps): JSX.Element {
<ConfigEditor
header="Eslint Config"
options={options}
values={configObject ?? {}}
values={configObject?.rules ?? {}}
isOpen={props.isOpen}
onClose={onClose}
/>
Expand Down
16 changes: 9 additions & 7 deletions packages/website/src/components/config/ConfigTypeScript.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,11 +17,11 @@ function checkOptions(item: [string, unknown]): item is [string, boolean] {

function ConfigTypeScript(props: ConfigTypeScriptProps): JSX.Element {
const [tsConfigOptions, updateOptions] = useState<ConfigOptionsType[]>([]);
const [configObject, updateConfigObject] = useState<CompilerFlags>({});
const [configObject, updateConfigObject] = useState<TSConfig>();

useEffect(() => {
if (props.isOpen) {
updateConfigObject(props.config ? parseTSConfig(props.config) : {});
updateConfigObject(parseTSConfig(props.config));
}
}, [props.isOpen, props.config]);

Expand Down Expand Up @@ -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();
}
Expand All @@ -67,7 +69,7 @@ function ConfigTypeScript(props: ConfigTypeScriptProps): JSX.Element {
<ConfigEditor
header="TypeScript Config"
options={tsConfigOptions}
values={configObject ?? {}}
values={configObject?.compilerOptions ?? {}}
isOpen={props.isOpen}
onClose={onClose}
/>
Expand Down
78 changes: 53 additions & 25 deletions packages/website/src/components/config/utils.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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[] {
Expand All @@ -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: {},
});
31 changes: 22 additions & 9 deletions packages/website/src/components/editor/LoadedEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ import {
LintCodeAction,
} from '../linter/utils';
import {
defaultEslintConfig,
defaultTsConfig,
tryParseEslintModule,
parseESLintRC,
parseTSConfig,
} from '../config/utils';
Expand Down Expand Up @@ -62,12 +61,12 @@ export const LoadedEditor: React.FC<LoadedEditorProps> = ({
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'),
),
Expand All @@ -83,7 +82,7 @@ export const LoadedEditor: React.FC<LoadedEditorProps> = ({
const markers = sandboxInstance.monaco.editor.getModelMarkers({
resource: model.uri,
});
onMarkersChange(parseMarkers(markers, codeActions, model));
onMarkersChange(parseMarkers(markers, codeActions, sandboxInstance.editor));
}, []);

useEffect(() => {
Expand All @@ -95,20 +94,25 @@ export const LoadedEditor: React.FC<LoadedEditorProps> = ({
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(() => {
Expand Down Expand Up @@ -169,9 +173,18 @@ export const LoadedEditor: React.FC<LoadedEditorProps> = ({
'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
Expand Down
6 changes: 4 additions & 2 deletions packages/website/src/components/linter/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function createEditOperation(
export function parseMarkers(
markers: Monaco.editor.IMarker[],
fixes: Map<string, LintCodeAction[]>,
model: Monaco.editor.ITextModel,
editor: Monaco.editor.IStandaloneCodeEditor,
): ErrorItem[] {
return markers.map(marker => {
const code =
Expand All @@ -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),
]);
},
};
}) ?? [];
Expand Down
5 changes: 5 additions & 0 deletions packages/website/src/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,8 @@ export interface ErrorItem {
hasFixers: boolean;
fixers: { message: string; fix(): void }[];
}

export type EslintRC = Record<string, unknown> & { rules: RulesRecord };
export type TSConfig = Record<string, unknown> & {
compilerOptions: CompilerFlags;
};