From a9028d52ee4605d81de8f899026b197c8d6d5b3d Mon Sep 17 00:00:00 2001 From: Armano Date: Fri, 31 Mar 2023 18:26:50 +0200 Subject: [PATCH 1/7] chore(website): [playground] use tsvfs as for linting code --- .eslintrc.js | 2 +- .../src/components/editor/LoadedEditor.tsx | 426 ++++++++---------- .../website/src/components/editor/config.ts | 9 +- .../src/components/editor/loadSandbox.ts | 6 +- .../components/editor/useSandboxServices.ts | 75 +-- .../src/components/lib/createEventHelper.ts | 19 + .../src/components/linter/CompilerHost.ts | 38 -- .../src/components/linter/WebLinter.ts | 160 ------- .../website/src/components/linter/bridge.ts | 82 ++++ .../website/src/components/linter/config.ts | 19 +- .../src/components/linter/createLinter.ts | 122 +++++ .../src/components/linter/createParser.ts | 108 +++++ .../website/src/components/linter/types.ts | 48 ++ 13 files changed, 635 insertions(+), 479 deletions(-) create mode 100644 packages/website/src/components/lib/createEventHelper.ts delete mode 100644 packages/website/src/components/linter/CompilerHost.ts delete mode 100644 packages/website/src/components/linter/WebLinter.ts create mode 100644 packages/website/src/components/linter/bridge.ts create mode 100644 packages/website/src/components/linter/createLinter.ts create mode 100644 packages/website/src/components/linter/createParser.ts create mode 100644 packages/website/src/components/linter/types.ts diff --git a/.eslintrc.js b/.eslintrc.js index 66097b17f060..395191ed87de 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -393,7 +393,7 @@ module.exports = { 'import/no-default-export': 'off', 'react/jsx-no-target-blank': 'off', 'react/no-unescaped-entities': 'off', - 'react-hooks/exhaustive-deps': 'off', // TODO: enable it later + 'react-hooks/exhaustive-deps': 'warn', // TODO: enable it later }, settings: { react: { diff --git a/packages/website/src/components/editor/LoadedEditor.tsx b/packages/website/src/components/editor/LoadedEditor.tsx index edfb49929c51..c16eefa49c20 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -3,16 +3,11 @@ import type Monaco from 'monaco-editor'; import type React from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { - parseESLintRC, - parseTSConfig, - tryParseEslintModule, -} from '../config/utils'; +import { parseTSConfig, tryParseEslintModule } from '../config/utils'; import { useResizeObserver } from '../hooks/useResizeObserver'; import { debounce } from '../lib/debounce'; import type { LintCodeAction } from '../linter/utils'; import { parseLintResults, parseMarkers } from '../linter/utils'; -import type { WebLinter } from '../linter/WebLinter'; import type { TabType } from '../types'; import { createCompilerOptions, @@ -21,11 +16,20 @@ import { } from './config'; import { createProvideCodeActions } from './createProvideCodeActions'; import type { CommonEditorProps } from './types'; -import type { SandboxInstance } from './useSandboxServices'; +import type { SandboxServices } from './useSandboxServices'; + +export type LoadedEditorProps = CommonEditorProps & SandboxServices; -export interface LoadedEditorProps extends CommonEditorProps { - readonly sandboxInstance: SandboxInstance; - readonly webLinter: WebLinter; +function applyEdit( + model: Monaco.editor.ITextModel, + editor: Monaco.editor.ICodeEditor, + edit: Monaco.editor.IIdentifiedSingleEditOperation, +): void { + if (model.isAttachedToEditor()) { + editor.executeEdits('eslint', [edit]); + } else { + model.pushEditOperations([], [edit], () => null); + } } export const LoadedEditor: React.FC = ({ @@ -40,10 +44,11 @@ export const LoadedEditor: React.FC = ({ onMarkersChange, onChange, onSelect, - sandboxInstance, + sandboxInstance: { editor, monaco }, showAST, - sourceType, + system, webLinter, + sourceType, activeTab, }) => { const { colorMode } = useColorMode(); @@ -52,16 +57,16 @@ export const LoadedEditor: React.FC = ({ const codeActions = useRef(new Map()).current; const [tabs] = useState>(() => { const tabsDefault = { - code: sandboxInstance.editor.getModel()!, - tsconfig: sandboxInstance.monaco.editor.createModel( + code: editor.getModel()!, + tsconfig: monaco.editor.createModel( tsconfig, 'json', - sandboxInstance.monaco.Uri.file('/tsconfig.json'), + monaco.Uri.file('/tsconfig.json'), ), - eslintrc: sandboxInstance.monaco.editor.createModel( + eslintrc: monaco.editor.createModel( eslintrc, 'json', - sandboxInstance.monaco.Uri.file('/.eslintrc'), + monaco.Uri.file('/.eslintrc'), ), }; tabsDefault.code.updateOptions({ tabSize: 2, insertSpaces: true }); @@ -71,289 +76,230 @@ export const LoadedEditor: React.FC = ({ }); const updateMarkers = useCallback(() => { - const model = sandboxInstance.editor.getModel()!; - const markers = sandboxInstance.monaco.editor.getModelMarkers({ + const model = editor.getModel()!; + const markers = monaco.editor.getModelMarkers({ resource: model.uri, }); - onMarkersChange(parseMarkers(markers, codeActions, sandboxInstance.editor)); - }, [ - codeActions, - onMarkersChange, - sandboxInstance.editor, - sandboxInstance.monaco.editor, - ]); + onMarkersChange(parseMarkers(markers, codeActions, editor)); + }, [codeActions, onMarkersChange, editor, monaco.editor]); useEffect(() => { - const newPath = jsx ? '/input.tsx' : '/input.ts'; + webLinter.updateParserOptions(jsx, sourceType); + }, [webLinter, jsx, sourceType]); + + useEffect(() => { + const newPath = `/input.${jsx ? 'tsx' : 'ts'}`; if (tabs.code.uri.path !== newPath) { - const newModel = sandboxInstance.monaco.editor.createModel( - tabs.code.getValue(), + const code = tabs.code.getValue(); + const newModel = monaco.editor.createModel( + code, 'typescript', - sandboxInstance.monaco.Uri.file(newPath), + monaco.Uri.file(newPath), ); newModel.updateOptions({ tabSize: 2, insertSpaces: true }); if (tabs.code.isAttachedToEditor()) { - sandboxInstance.editor.setModel(newModel); + editor.setModel(newModel); } tabs.code.dispose(); tabs.code = newModel; + system.writeFile(newPath, code); } - }, [ - jsx, - sandboxInstance.editor, - sandboxInstance.monaco.Uri, - sandboxInstance.monaco.editor, - tabs, - ]); + }, [jsx, editor, system, monaco, tabs]); useEffect(() => { const config = createCompilerOptions( - jsx, parseTSConfig(tsconfig).compilerOptions, ); - webLinter.updateCompilerOptions(config); - sandboxInstance.setCompilerSettings(config); - }, [jsx, sandboxInstance, tsconfig, webLinter]); + // @ts-expect-error - monaco and ts compilerOptions are not the same + monaco.languages.typescript.typescriptDefaults.setCompilerOptions(config); + }, [monaco, tsconfig]); useEffect(() => { - webLinter.updateRules(parseESLintRC(eslintrc).rules); - }, [eslintrc, webLinter]); + if (editor.getModel()?.uri.path !== tabs[activeTab].uri.path) { + editor.setModel(tabs[activeTab]); + updateMarkers(); + } + }, [activeTab, editor, tabs, updateMarkers]); useEffect(() => { - sandboxInstance.editor.setModel(tabs[activeTab]); - updateMarkers(); - }, [activeTab, sandboxInstance.editor, tabs, updateMarkers]); + const disposable = webLinter.onLint((uri, messages) => { + const diagnostics = parseLintResults(messages, codeActions, ruleId => + monaco.Uri.parse(webLinter.rules.get(ruleId)?.url ?? ''), + ); + monaco.editor.setModelMarkers( + monaco.editor.getModel(monaco.Uri.file(uri))!, + 'eslint', + diagnostics, + ); + updateMarkers(); + }); + return () => disposable(); + }, [webLinter, monaco, codeActions, updateMarkers]); useEffect(() => { - const lintEditor = debounce(() => { - console.info('[Editor] linting triggered'); - - webLinter.updateParserOptions(jsx, sourceType); - - try { - const messages = webLinter.lint(code); - - const markers = parseLintResults(messages, codeActions, ruleId => - sandboxInstance.monaco.Uri.parse( - webLinter.rulesUrl.get(ruleId) ?? '', - ), - ); - - sandboxInstance.monaco.editor.setModelMarkers( - tabs.code, - 'eslint', - markers, - ); - - // fallback when event is not preset, ts < 4.0.5 - if (!sandboxInstance.monaco.editor.onDidChangeMarkers) { - updateMarkers(); - } - } catch (e) { - onMarkersChange(e as Error); - } - - onEsASTChange(webLinter.storedAST); - onTsASTChange(webLinter.storedTsAST); - onScopeChange(webLinter.storedScope); - onSelect(sandboxInstance.editor.getPosition()); - }, 500); - - lintEditor(); - }, [ - code, - jsx, - tsconfig, - eslintrc, - sourceType, - webLinter, - onEsASTChange, - onTsASTChange, - onScopeChange, - onSelect, - sandboxInstance.editor, - sandboxInstance.monaco.editor, - sandboxInstance.monaco.Uri, - codeActions, - tabs.code, - updateMarkers, - onMarkersChange, - ]); + const disposable = webLinter.onParse((uri, model) => { + onEsASTChange(model.storedAST); + onScopeChange(model.storedScope as Record | undefined); + onTsASTChange(model.storedTsAST); + }); + return () => disposable(); + }, [webLinter, onEsASTChange, onScopeChange, onTsASTChange]); useEffect(() => { // configure the JSON language support with schemas and schema associations - sandboxInstance.monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ + monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ validate: true, enableSchemaRequest: false, allowComments: true, schemas: [ { - 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), + uri: monaco.Uri.file('eslint-schema.json').toString(), // id of the first schema + fileMatch: ['/.eslintrc'], // associate with our model + schema: getEslintSchema(Array.from(webLinter.rules.values())), }, { - uri: sandboxInstance.monaco.Uri.file('ts-schema.json').toString(), // id of the first schema - fileMatch: [tabs.tsconfig.uri.toString()], // associate with our model + uri: monaco.Uri.file('ts-schema.json').toString(), // id of the first schema + fileMatch: ['/tsconfig.json'], // associate with our model schema: getTsConfigSchema(), }, ], }); + }, [monaco, webLinter.rules]); - const subscriptions = [ - sandboxInstance.monaco.languages.registerCodeActionProvider( - '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); - } + useEffect(() => { + const disposable = monaco.languages.registerCodeActionProvider( + 'typescript', + createProvideCodeActions(codeActions), + ); + return () => disposable.dispose(); + }, [codeActions, monaco]); + + useEffect(() => { + const disposable = 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 (tabs.code.isAttachedToEditor()) { - const position = sandboxInstance.editor.getPosition(); - if (position) { - console.info('[Editor] updating cursor', position); - onSelect(position); - } + } + }); + return () => disposable.dispose(); + }, [editor, tabs.eslintrc]); + + useEffect(() => { + const disposable = editor.onDidChangeCursorPosition( + debounce(() => { + if (tabs.code.isAttachedToEditor()) { + const position = editor.getPosition(); + if (position) { + console.info('[Editor] updating cursor', position); + onSelect(position); } - }, 150), - ), - sandboxInstance.editor.addAction({ - id: 'fix-eslint-problems', - label: 'Fix eslint problems', - keybindings: [ - sandboxInstance.monaco.KeyMod.CtrlCmd | - sandboxInstance.monaco.KeyCode.KeyS, - ], - contextMenuGroupId: 'snippets', - contextMenuOrder: 1.5, - run(editor) { - const editorModel = editor.getModel(); - if (editorModel) { - const fixed = webLinter.fix(editor.getValue()); - if (fixed.fixed) { - editorModel.pushEditOperations( - null, - [ - { - range: editorModel.getFullModelRange(), - text: fixed.output, - }, - ], - () => null, - ); - } + } + }, 150), + ); + return () => disposable.dispose(); + }, [onSelect, editor, tabs.code]); + + useEffect(() => { + const disposable = editor.addAction({ + id: 'fix-eslint-problems', + label: 'Fix eslint problems', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS], + contextMenuGroupId: 'snippets', + contextMenuOrder: 1.5, + run(editor) { + const editorModel = editor.getModel(); + if (editorModel) { + const fixed = webLinter.triggerFix(editor.getValue()); + if (fixed?.fixed) { + applyEdit(editorModel, editor, { + range: editorModel.getFullModelRange(), + text: fixed.output, + }); } - }, + } + }, + }); + return () => disposable.dispose(); + }, [editor, monaco, webLinter]); + + useEffect(() => { + const closable = [ + system.watchFile('/tsconfig.json', filename => { + onChange({ tsconfig: system.readFile(filename) }); }), - tabs.eslintrc.onDidChangeContent( - debounce(() => { - onChange({ eslintrc: tabs.eslintrc.getValue() }); - }, 500), - ), - tabs.tsconfig.onDidChangeContent( - debounce(() => { - onChange({ tsconfig: tabs.tsconfig.getValue() }); - }, 500), - ), - tabs.code.onDidChangeContent( - debounce(() => { - onChange({ code: tabs.code.getValue() }); - }, 500), - ), - // may not be defined in ts < 4.0.5 - sandboxInstance.monaco.editor.onDidChangeMarkers?.(() => { - updateMarkers(); + system.watchFile('/.eslintrc', filename => { + onChange({ eslintrc: system.readFile(filename) }); + }), + system.watchFile('/input.*', filename => { + onChange({ code: system.readFile(filename) }); }), ]; - return (): void => { - for (const subscription of subscriptions) { - if (subscription) { - subscription.dispose(); - } - } + return () => { + closable.forEach(c => c.close()); }; - }, [ - codeActions, - onChange, - onSelect, - sandboxInstance.editor, - sandboxInstance.monaco.editor, - sandboxInstance.monaco.languages.json.jsonDefaults, - tabs.code, - tabs.eslintrc, - tabs.tsconfig, - updateMarkers, - webLinter.ruleNames, - ]); + }, [system, onChange]); + + useEffect(() => { + const disposable = editor.onDidChangeModelContent(() => { + const model = editor.getModel(); + if (model) { + system.writeFile(model.uri.path, model.getValue()); + } + }); + return () => disposable.dispose(); + }, [editor, system]); + + useEffect(() => { + const disposable = monaco.editor.onDidChangeMarkers(() => { + updateMarkers(); + }); + return () => disposable.dispose(); + }, [monaco.editor, updateMarkers]); const resize = useMemo(() => { - return debounce(() => sandboxInstance.editor.layout(), 1); - }, [sandboxInstance]); + return debounce(() => editor.layout(), 1); + }, [editor]); - const container = - sandboxInstance.editor.getContainerDomNode?.() ?? - sandboxInstance.editor.getDomNode(); + const container = editor.getContainerDomNode?.() ?? editor.getDomNode(); useResizeObserver(container, () => { resize(); }); useEffect(() => { - if ( - !sandboxInstance.editor.hasTextFocus() && - code !== tabs.code.getValue() - ) { - tabs.code.applyEdits([ - { - range: tabs.code.getFullModelRange(), - text: code, - }, - ]); + if (!editor.hasTextFocus() && code !== tabs.code.getValue()) { + applyEdit(tabs.code, editor, { + range: tabs.code.getFullModelRange(), + text: code, + }); } - }, [sandboxInstance, code, tabs.code]); + }, [code, editor, tabs.code]); useEffect(() => { - if ( - !sandboxInstance.editor.hasTextFocus() && - tsconfig !== tabs.tsconfig.getValue() - ) { - tabs.tsconfig.applyEdits([ - { - range: tabs.tsconfig.getFullModelRange(), - text: tsconfig, - }, - ]); + if (!editor.hasTextFocus() && tsconfig !== tabs.tsconfig.getValue()) { + applyEdit(tabs.tsconfig, editor, { + range: tabs.tsconfig.getFullModelRange(), + text: tsconfig, + }); } - }, [sandboxInstance, tabs.tsconfig, tsconfig]); + }, [editor, tabs.tsconfig, tsconfig]); useEffect(() => { - if ( - !sandboxInstance.editor.hasTextFocus() && - eslintrc !== tabs.eslintrc.getValue() - ) { - tabs.eslintrc.applyEdits([ - { - range: tabs.eslintrc.getFullModelRange(), - text: eslintrc, - }, - ]); + if (!editor.hasTextFocus() && eslintrc !== tabs.eslintrc.getValue()) { + applyEdit(tabs.eslintrc, editor, { + range: tabs.eslintrc.getFullModelRange(), + text: eslintrc, + }); } - }, [sandboxInstance, eslintrc, tabs.eslintrc]); + }, [eslintrc, editor, tabs.eslintrc]); useEffect(() => { - sandboxInstance.monaco.editor.setTheme( - colorMode === 'dark' ? 'vs-dark' : 'vs-light', - ); - }, [colorMode, sandboxInstance]); + monaco.editor.setTheme(colorMode === 'dark' ? 'vs-dark' : 'vs-light'); + }, [colorMode, monaco]); useEffect(() => { setDecorations(prevDecorations => @@ -362,7 +308,7 @@ export const LoadedEditor: React.FC = ({ decoration && showAST ? [ { - range: new sandboxInstance.monaco.Range( + range: new monaco.Range( decoration.start.line, decoration.start.column + 1, decoration.end.line, @@ -377,7 +323,13 @@ export const LoadedEditor: React.FC = ({ : [], ), ); - }, [decoration, sandboxInstance, showAST, tabs.code]); + }, [decoration, monaco, showAST, tabs.code]); + + useEffect(() => { + if (activeTab === 'code') { + webLinter.triggerLint(tabs.code.uri.path); + } + }, [webLinter, jsx, sourceType, activeTab, tabs.code]); return null; }; diff --git a/packages/website/src/components/editor/config.ts b/packages/website/src/components/editor/config.ts index aee01fb11f07..722a62611aba 100644 --- a/packages/website/src/components/editor/config.ts +++ b/packages/website/src/components/editor/config.ts @@ -1,19 +1,18 @@ import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'; -import type Monaco from 'monaco-editor'; +import type * as ts from 'typescript'; import { getTypescriptOptions } from '../config/utils'; export function createCompilerOptions( - jsx = false, tsConfig: Record = {}, -): Monaco.languages.typescript.CompilerOptions { +): ts.CompilerOptions { const config = window.ts.convertCompilerOptionsFromJson( { // ts and monaco has different type as monaco types are not changing base on ts version target: 'esnext', module: 'esnext', ...tsConfig, - jsx: jsx ? 'preserve' : undefined, + jsx: 'preserve', lib: Array.isArray(tsConfig.lib) ? tsConfig.lib : undefined, moduleResolution: undefined, plugins: undefined, @@ -25,7 +24,7 @@ export function createCompilerOptions( '/tsconfig.json', ); - const options = config.options as Monaco.languages.typescript.CompilerOptions; + const options = config.options; if (!options.lib) { options.lib = [window.ts.getDefaultLibFileName(options)]; diff --git a/packages/website/src/components/editor/loadSandbox.ts b/packages/website/src/components/editor/loadSandbox.ts index 51b8b64295a0..f7798a4093eb 100644 --- a/packages/website/src/components/editor/loadSandbox.ts +++ b/packages/website/src/components/editor/loadSandbox.ts @@ -1,7 +1,7 @@ import type MonacoEditor from 'monaco-editor'; import type * as SandboxFactory from '../../vendor/sandbox'; -import type { LintUtils } from '../linter/WebLinter'; +import type { WebLinterModule } from '../linter/types'; type Monaco = typeof MonacoEditor; type Sandbox = typeof SandboxFactory; @@ -9,7 +9,7 @@ type Sandbox = typeof SandboxFactory; export interface SandboxModel { main: Monaco; sandboxFactory: Sandbox; - lintUtils: LintUtils; + lintUtils: WebLinterModule; } function loadSandbox(tsVersion: string): Promise { @@ -32,7 +32,7 @@ function loadSandbox(tsVersion: string): Promise { }); // Grab a copy of monaco, TypeScript and the sandbox - window.require<[Monaco, Sandbox, LintUtils]>( + window.require<[Monaco, Sandbox, WebLinterModule]>( ['vs/editor/editor.main', 'sandbox/index', 'linter/index'], (main, sandboxFactory, lintUtils) => { resolve({ main, sandboxFactory, lintUtils }); diff --git a/packages/website/src/components/editor/useSandboxServices.ts b/packages/website/src/components/editor/useSandboxServices.ts index 3007856e54ac..880e5278dcda 100644 --- a/packages/website/src/components/editor/useSandboxServices.ts +++ b/packages/website/src/components/editor/useSandboxServices.ts @@ -1,13 +1,13 @@ import { useColorMode } from '@docusaurus/theme-common'; +import type * as MonacoEditor from 'monaco-editor'; import { useEffect, useState } from 'react'; -import type { - createTypeScriptSandbox, - SandboxConfig, -} from '../../vendor/sandbox'; -import { WebLinter } from '../linter/WebLinter'; +import type { createTypeScriptSandbox } from '../../vendor/sandbox'; +import { createCompilerOptions } from '../editor/config'; +import { createFileSystem } from '../linter/bridge'; +import { createLinter } from '../linter/createLinter'; +import type { PlaygroundSystem, WebLinter } from '../linter/types'; import type { RuleDetails } from '../types'; -import { createCompilerOptions } from './config'; import { editorEmbedId } from './EditorEmbed'; import { sandboxSingleton } from './loadSandbox'; import type { CommonEditorProps } from './types'; @@ -25,6 +25,7 @@ export type SandboxInstance = ReturnType; export interface SandboxServices { sandboxInstance: SandboxInstance; + system: PlaygroundSystem; webLinter: WebLinter; } @@ -48,29 +49,28 @@ export const useSandboxServices = ( sandboxSingleton(props.ts) .then(async ({ main, sandboxFactory, lintUtils }) => { - const compilerOptions = createCompilerOptions(props.jsx); - - const sandboxConfig: Partial = { - text: props.code, - monacoSettings: { - minimap: { enabled: false }, - fontSize: 13, - wordWrap: 'off', - scrollBeyondLastLine: false, - smoothScrolling: true, - autoIndent: 'full', - formatOnPaste: true, - formatOnType: true, - wrappingIndent: 'same', - hover: { above: false }, - }, - acquireTypes: false, - compilerOptions: compilerOptions, - domID: editorEmbedId, - }; + const compilerOptions = + createCompilerOptions() as MonacoEditor.languages.typescript.CompilerOptions; sandboxInstance = sandboxFactory.createTypeScriptSandbox( - sandboxConfig, + { + text: props.code, + monacoSettings: { + minimap: { enabled: false }, + fontSize: 13, + wordWrap: 'off', + scrollBeyondLastLine: false, + smoothScrolling: true, + autoIndent: 'full', + formatOnPaste: true, + formatOnType: true, + wrappingIndent: 'same', + hover: { above: false }, + }, + acquireTypes: false, + compilerOptions: compilerOptions, + domID: editorEmbedId, + }, main, window.ts, ); @@ -78,22 +78,28 @@ export const useSandboxServices = ( colorMode === 'dark' ? 'vs-dark' : 'vs-light', ); - const libEntries = new Map(); + const system = createFileSystem(props, sandboxInstance.tsvfs); + const worker = await sandboxInstance.getWorkerProcess(); if (worker.getLibFiles) { const libs = await worker.getLibFiles(); for (const [key, value] of Object.entries(libs)) { - libEntries.set('/' + key, value); + system.writeFile('/' + key, value); } } - const system = sandboxInstance.tsvfs.createSystem(libEntries); + // @ts-expect-error - we're adding these to the window for debugging purposes + window.system = system; window.esquery = lintUtils.esquery; - const webLinter = new WebLinter(system, compilerOptions, lintUtils); + const webLinter = createLinter( + system, + lintUtils, + sandboxInstance.tsvfs, + ); onLoaded( - webLinter.ruleNames, + Array.from(webLinter.rules.values()), Array.from( new Set([...sandboxInstance.supportedVersions, window.ts.version]), ) @@ -102,8 +108,9 @@ export const useSandboxServices = ( ); setServices({ - sandboxInstance, + system, webLinter, + sandboxInstance, }); }) .catch(setServices); @@ -128,7 +135,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 - }, [props.ts, onLoaded]); + }, []); return services; }; diff --git a/packages/website/src/components/lib/createEventHelper.ts b/packages/website/src/components/lib/createEventHelper.ts new file mode 100644 index 000000000000..17e93028b0d1 --- /dev/null +++ b/packages/website/src/components/lib/createEventHelper.ts @@ -0,0 +1,19 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function createEventHelper void>(): { + trigger: (...args: Parameters) => void; + register: (cb: T) => () => void; +} { + const events = new Set(); + + return { + trigger(...args: Parameters): void { + events.forEach(cb => cb(...args)); + }, + register(cb: T): () => void { + events.add(cb); + return (): void => { + events.delete(cb); + }; + }, + }; +} diff --git a/packages/website/src/components/linter/CompilerHost.ts b/packages/website/src/components/linter/CompilerHost.ts deleted file mode 100644 index 22fd9fa83c67..000000000000 --- a/packages/website/src/components/linter/CompilerHost.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { CompilerHost, SourceFile, System } from 'typescript'; - -import type { LintUtils } from './WebLinter'; - -/** - * Creates an in-memory CompilerHost -which is essentially an extra wrapper to System - * which works with TypeScript objects - returns both a compiler host, and a way to add new SourceFile - * instances to the in-memory file system. - * - * based on typescript-vfs - * @see https://github.com/microsoft/TypeScript-Website/blob/d2613c0e57ae1be2f3a76e94b006819a1fc73d5e/packages/typescript-vfs/src/index.ts#L480 - */ -export function createVirtualCompilerHost( - sys: System, - lintUtils: LintUtils, -): CompilerHost { - return { - ...sys, - getCanonicalFileName: (fileName: string) => fileName, - getDefaultLibFileName: options => - '/' + window.ts.getDefaultLibFileName(options), - getNewLine: () => sys.newLine, - getSourceFile(fileName, languageVersionOrOptions): SourceFile | undefined { - if (this.fileExists(fileName)) { - const file = this.readFile(fileName) ?? ''; - return window.ts.createSourceFile( - fileName, - file, - languageVersionOrOptions, - true, - lintUtils.getScriptKind(fileName, false), - ); - } - return undefined; - }, - useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames, - }; -} diff --git a/packages/website/src/components/linter/WebLinter.ts b/packages/website/src/components/linter/WebLinter.ts deleted file mode 100644 index e8a92a1003c3..000000000000 --- a/packages/website/src/components/linter/WebLinter.ts +++ /dev/null @@ -1,160 +0,0 @@ -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 { - astConverter, - getScriptKind, -} 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'; - -const PARSER_NAME = '@typescript-eslint/parser'; - -export interface LintUtils { - createLinter: () => TSESLint.Linter; - analyze: typeof analyze; - visitorKeys: TSESLint.SourceCode.VisitorKeys; - astConverter: typeof astConverter; - getScriptKind: typeof getScriptKind; - esquery: typeof esquery; -} - -export class WebLinter { - private readonly host: CompilerHost; - - public storedAST?: TSESTree.Program; - public storedTsAST?: SourceFile; - public storedScope?: Record; - - private compilerOptions: CompilerOptions; - private readonly parserOptions: ParserOptions = { - ecmaFeatures: { - jsx: false, - globalReturn: false, - }, - ecmaVersion: 'latest', - project: ['./tsconfig.json'], - sourceType: 'module', - }; - - private linter: TSESLint.Linter; - private lintUtils: LintUtils; - private rules: TSESLint.Linter.RulesRecord = {}; - - public readonly ruleNames: { name: string; description?: string }[] = []; - public readonly rulesUrl = new Map(); - - constructor( - system: System, - compilerOptions: CompilerOptions, - lintUtils: LintUtils, - ) { - this.compilerOptions = compilerOptions; - this.lintUtils = lintUtils; - this.linter = lintUtils.createLinter(); - - this.host = createVirtualCompilerHost(system, lintUtils); - - this.linter.defineParser(PARSER_NAME, { - parseForESLint: (text, options?: ParserOptions) => { - return this.eslintParse(text, options); - }, - }); - - this.linter.getRules().forEach((item, name) => { - this.ruleNames.push({ - name: name, - description: item.meta?.docs?.description, - }); - 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): TSESLint.Linter.LintMessage[] { - return this.linter.verify(code, this.eslintConfig); - } - - fix(code: string): TSESLint.Linter.FixReport { - return this.linter.verifyAndFix(code, this.eslintConfig, { fix: true }); - } - - updateRules(rules: TSESLint.Linter.RulesRecord): void { - this.rules = rules; - } - - updateParserOptions(jsx?: boolean, sourceType?: TSESLint.SourceType): void { - this.parserOptions.ecmaFeatures!.jsx = jsx ?? false; - this.parserOptions.sourceType = sourceType ?? 'module'; - } - - updateCompilerOptions(options: CompilerOptions = {}): void { - this.compilerOptions = options; - } - - eslintParse( - code: string, - eslintOptions: ParserOptions = {}, - ): TSESLint.Linter.ESLintParseResult { - const isJsx = eslintOptions?.ecmaFeatures?.jsx ?? false; - const fileName = isJsx ? '/demo.tsx' : '/demo.ts'; - - this.storedAST = undefined; - this.storedTsAST = undefined; - this.storedScope = undefined; - - this.host.writeFile(fileName, code, false); - - const program = window.ts.createProgram({ - rootNames: [fileName], - options: this.compilerOptions, - host: this.host, - }); - const tsAst = program.getSourceFile(fileName)!; - const checker = program.getTypeChecker(); - - const { estree: ast, astMaps } = this.lintUtils.astConverter( - tsAst, - { ...parseSettings, code, codeFullText: code, jsx: isJsx }, - true, - ); - - const scopeManager = this.lintUtils.analyze(ast, { - globalReturn: eslintOptions.ecmaFeatures?.globalReturn ?? false, - sourceType: eslintOptions.sourceType ?? 'script', - }); - - this.storedAST = ast; - this.storedTsAST = tsAst; - this.storedScope = scopeManager as unknown as Record; - - return { - ast, - services: { - program, - esTreeNodeToTSNodeMap: astMaps.esTreeNodeToTSNodeMap, - tsNodeToESTreeNodeMap: astMaps.tsNodeToESTreeNodeMap, - getSymbolAtLocation: node => - checker.getSymbolAtLocation(astMaps.esTreeNodeToTSNodeMap.get(node)), - getTypeAtLocation: node => - checker.getTypeAtLocation(astMaps.esTreeNodeToTSNodeMap.get(node)), - }, - scopeManager, - visitorKeys: this.lintUtils.visitorKeys, - }; - } -} diff --git a/packages/website/src/components/linter/bridge.ts b/packages/website/src/components/linter/bridge.ts new file mode 100644 index 000000000000..ce04df99e59e --- /dev/null +++ b/packages/website/src/components/linter/bridge.ts @@ -0,0 +1,82 @@ +import type * as tsvfs from '@site/src/vendor/typescript-vfs'; +import type * as ts from 'typescript'; + +import { debounce } from '../lib/debounce'; +import type { ConfigModel } from '../types'; +import type { PlaygroundSystem } from './types'; + +export function createFileSystem( + config: Pick, + vfs: typeof tsvfs, +): PlaygroundSystem { + const files = new Map(); + files.set(`/.eslintrc`, config.eslintrc); + files.set(`/tsconfig.json`, config.tsconfig); + files.set(`/input.${config.jsx ? 'tsx' : 'ts'}`, config.code); + + const fileWatcherCallbacks = new Map>(); + + const system = vfs.createSystem(files) as PlaygroundSystem; + + system.watchFile = ( + path, + callback, + pollingInterval = 500, + ): ts.FileWatcher => { + const cb = pollingInterval ? debounce(callback, pollingInterval) : callback; + + const escapedPath = path.replace(/\./g, '\\.').replace(/\*/g, '[^/]+'); + const expPath = new RegExp(`^${escapedPath}$`, ''); + + let handle = fileWatcherCallbacks.get(expPath); + if (!handle) { + handle = new Set(); + fileWatcherCallbacks.set(expPath, handle); + } + handle.add(cb); + + return { + close: (): void => { + const handle = fileWatcherCallbacks.get(expPath); + if (handle) { + handle.delete(cb); + } + }, + }; + }; + + const triggerCallbacks = ( + path: string, + type: ts.FileWatcherEventKind, + ): void => { + fileWatcherCallbacks.forEach((callbacks, key) => { + if (key.test(path)) { + callbacks.forEach(cb => cb(path, type)); + } + }); + }; + + system.deleteFile = (fileName): void => { + files.delete(fileName); + triggerCallbacks(fileName, 1); + }; + + system.writeFile = (fileName, contents): void => { + if (!contents) { + contents = ''; + } + const file = files.get(fileName); + if (file === contents) { + // do not trigger callbacks if the file has not changed + return; + } + files.set(fileName, contents); + triggerCallbacks(fileName, file ? 2 : 0); + }; + + system.removeFile = (fileName): void => { + files.delete(fileName); + }; + + return system; +} diff --git a/packages/website/src/components/linter/config.ts b/packages/website/src/components/linter/config.ts index 93466f9451ab..021a4c91b749 100644 --- a/packages/website/src/components/linter/config.ts +++ b/packages/website/src/components/linter/config.ts @@ -1,6 +1,7 @@ import type { ParseSettings } from '@typescript-eslint/typescript-estree/use-at-your-own-risk'; +import type { TSESLint } from '@typescript-eslint/utils'; -export const parseSettings: ParseSettings = { +export const defaultParseSettings: ParseSettings = { allowInvalidAST: false, code: '', codeFullText: '', @@ -26,3 +27,19 @@ export const parseSettings: ParseSettings = { tsconfigMatchCache: new Map(), tsconfigRootDir: '/', }; + +export const PARSER_NAME = '@typescript-eslint/parser'; + +export const defaultEslintConfig: TSESLint.Linter.Config = { + parser: PARSER_NAME, + parserOptions: { + ecmaFeatures: { + jsx: false, + globalReturn: false, + }, + ecmaVersion: 'latest', + project: ['./tsconfig.json'], + sourceType: 'module', + }, + rules: {}, +}; diff --git a/packages/website/src/components/linter/createLinter.ts b/packages/website/src/components/linter/createLinter.ts new file mode 100644 index 000000000000..22ea5c9e2244 --- /dev/null +++ b/packages/website/src/components/linter/createLinter.ts @@ -0,0 +1,122 @@ +import type * as tsvfs from '@site/src/vendor/typescript-vfs'; +import type { TSESLint } from '@typescript-eslint/utils'; +import type * as ts from 'typescript'; + +import { parseESLintRC, parseTSConfig } from '../config/utils'; +import { createCompilerOptions } from '../editor/config'; +import { createEventHelper } from '../lib/createEventHelper'; +import { defaultEslintConfig, PARSER_NAME } from './config'; +import { createParser } from './createParser'; +import type { + LinterOnLint, + LinterOnParse, + PlaygroundSystem, + WebLinter, + WebLinterModule, +} from './types'; + +export function createLinter( + system: PlaygroundSystem, + webLinterModule: WebLinterModule, + vfs: typeof tsvfs, +): WebLinter { + const rules: WebLinter['rules'] = new Map(); + let compilerOptions: ts.CompilerOptions = {}; + const eslintConfig = { ...defaultEslintConfig }; + + const onLint = createEventHelper(); + const onParse = createEventHelper(); + + const linter = webLinterModule.createLinter(); + + const parser = createParser( + system, + compilerOptions, + (filename, model): void => { + onParse.trigger(filename, model); + }, + webLinterModule, + vfs, + ); + + linter.defineParser(PARSER_NAME, parser); + + linter.getRules().forEach((item, name) => { + rules.set(name, { + name: name, + description: item.meta?.docs?.description, + url: item.meta?.docs?.url, + }); + }); + + const triggerLint = (filename: string): void => { + console.info('[Editor] linting triggered for file', filename); + const code = system.readFile(filename) ?? '\n'; + if (code != null) { + const messages = linter.verify(code, eslintConfig, filename); + onLint.trigger(filename, messages); + } + }; + + const triggerFix = ( + filename: string, + ): TSESLint.Linter.FixReport | undefined => { + const code = system.readFile(filename); + if (code) { + return linter.verifyAndFix(code, eslintConfig, { + filename: filename, + fix: true, + }); + } + return undefined; + }; + + const updateParserOptions = ( + jsx?: boolean, + sourceType?: TSESLint.SourceType, + ): void => { + eslintConfig.parserOptions ??= {}; + eslintConfig.parserOptions.sourceType = sourceType ?? 'module'; + eslintConfig.parserOptions.ecmaFeatures ??= {}; + eslintConfig.parserOptions.ecmaFeatures.jsx = jsx ?? false; + }; + + const applyEslintConfig = (fileName: string): void => { + try { + const file = system.readFile(fileName) ?? '{}'; + const parsed = parseESLintRC(file); + eslintConfig.rules = parsed.rules; + console.log('[Editor] Updating', fileName, eslintConfig); + } catch (e) { + console.error(e); + } + }; + + const applyTSConfig = (fileName: string): void => { + try { + const file = system.readFile(fileName) ?? '{}'; + const parsed = parseTSConfig(file).compilerOptions; + compilerOptions = createCompilerOptions(parsed); + console.log('[Editor] Updating', fileName, compilerOptions); + parser.updateConfig(compilerOptions); + } catch (e) { + console.error(e); + } + }; + + system.watchFile('/input.*', triggerLint); + system.watchFile('/.eslintrc', applyEslintConfig); + system.watchFile('/tsconfig.json', applyTSConfig); + + applyEslintConfig('/.eslintrc'); + applyTSConfig('/tsconfig.json'); + + return { + rules, + triggerFix, + triggerLint, + updateParserOptions, + onParse: onParse.register, + onLint: onLint.register, + }; +} diff --git a/packages/website/src/components/linter/createParser.ts b/packages/website/src/components/linter/createParser.ts new file mode 100644 index 000000000000..1c0d9258a8f2 --- /dev/null +++ b/packages/website/src/components/linter/createParser.ts @@ -0,0 +1,108 @@ +import type * as tsvfs from '@site/src/vendor/typescript-vfs'; +import type { ParserOptions } from '@typescript-eslint/types'; +import type { TSESLint } from '@typescript-eslint/utils'; +import type * as ts from 'typescript'; + +import { defaultParseSettings } from './config'; +import type { + ParseSettings, + PlaygroundSystem, + UpdateModel, + WebLinterModule, +} from './types'; + +export function createParser( + system: PlaygroundSystem, + compilerOptions: ts.CompilerOptions, + onUpdate: (filename: string, model: UpdateModel) => void, + utils: WebLinterModule, + vfs: typeof tsvfs, +): TSESLint.Linter.ParserModule & { + updateConfig: (compilerOptions: ts.CompilerOptions) => void; +} { + const registeredFiles = new Set(); + + const createEnv = ( + compilerOptions: ts.CompilerOptions, + ): tsvfs.VirtualTypeScriptEnvironment => { + return vfs.createVirtualTypeScriptEnvironment( + system, + Array.from(registeredFiles), + window.ts, + compilerOptions, + ); + }; + + let compilerHost = createEnv(compilerOptions); + + return { + updateConfig(compilerOptions): void { + compilerHost = createEnv(compilerOptions); + }, + parseForESLint: ( + text: string, + options: ParserOptions = {}, + ): TSESLint.Linter.ESLintParseResult => { + const filePath = options.filePath ?? '/input.ts'; + + // if text is empty use empty line to avoid error + const code = text || '\n'; + + if (registeredFiles.has(filePath)) { + compilerHost.updateFile(filePath, code); + } else { + registeredFiles.add(filePath); + compilerHost.createFile(filePath, code); + } + + const parseSettings: ParseSettings = { + ...defaultParseSettings, + code: code, + codeFullText: code, + filePath: filePath, + }; + + const program = compilerHost.languageService.getProgram(); + if (!program) { + throw new Error('Failed to get program'); + } + + const tsAst = program.getSourceFile(filePath)!; + + const converted = utils.astConverter(tsAst, parseSettings, true); + + const scopeManager = utils.analyze(converted.estree, { + globalReturn: options.ecmaFeatures?.globalReturn ?? false, + sourceType: options.sourceType ?? 'module', + }); + + const checker = program.getTypeChecker(); + + onUpdate(filePath, { + storedAST: converted.estree, + storedTsAST: tsAst, + storedScope: scopeManager, + typeChecker: checker, + }); + + return { + ast: converted.estree, + services: { + program, + esTreeNodeToTSNodeMap: converted.astMaps.esTreeNodeToTSNodeMap, + tsNodeToESTreeNodeMap: converted.astMaps.tsNodeToESTreeNodeMap, + getSymbolAtLocation: node => + checker.getSymbolAtLocation( + converted.astMaps.esTreeNodeToTSNodeMap.get(node), + ), + getTypeAtLocation: node => + checker.getTypeAtLocation( + converted.astMaps.esTreeNodeToTSNodeMap.get(node), + ), + }, + scopeManager, + visitorKeys: utils.visitorKeys, + }; + }, + }; +} diff --git a/packages/website/src/components/linter/types.ts b/packages/website/src/components/linter/types.ts new file mode 100644 index 000000000000..88db4f2ec429 --- /dev/null +++ b/packages/website/src/components/linter/types.ts @@ -0,0 +1,48 @@ +import type { analyze, ScopeManager } from '@typescript-eslint/scope-manager'; +import type { astConverter } 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 * as ts from 'typescript'; + +export type { ParseSettings } from '@typescript-eslint/typescript-estree/use-at-your-own-risk'; + +export interface UpdateModel { + storedAST?: TSESTree.Program; + storedTsAST?: ts.SourceFile; + storedScope?: ScopeManager; + typeChecker?: ts.TypeChecker; +} + +export interface WebLinterModule { + createLinter: () => TSESLint.Linter; + analyze: typeof analyze; + visitorKeys: TSESLint.SourceCode.VisitorKeys; + astConverter: typeof astConverter; + esquery: typeof esquery; +} + +export type PlaygroundSystem = ts.System & + Required> & { + removeFile: (fileName: string) => void; + }; + +export type RulesMap = Map< + string, + { name: string; description?: string; url?: string } +>; + +export type LinterOnLint = ( + fileName: string, + messages: TSESLint.Linter.LintMessage[], +) => void; + +export type LinterOnParse = (fileName: string, model: UpdateModel) => void; + +export interface WebLinter { + rules: RulesMap; + triggerFix(filename: string): TSESLint.Linter.FixReport | undefined; + triggerLint(filename: string): void; + onLint(cb: LinterOnLint): () => void; + onParse(cb: LinterOnParse): () => void; + updateParserOptions(jsx?: boolean, sourceType?: TSESLint.SourceType): void; +} From a655909c4798813c98f84b4bf825c87aae452560 Mon Sep 17 00:00:00 2001 From: Armano Date: Sun, 2 Apr 2023 22:19:20 +0200 Subject: [PATCH 2/7] fix: correct issue after merge do not trigger linting on tab change, --- packages/website/src/components/editor/LoadedEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/website/src/components/editor/LoadedEditor.tsx b/packages/website/src/components/editor/LoadedEditor.tsx index 75862ca38449..d6aa027537d0 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -323,7 +323,7 @@ export const LoadedEditor: React.FC = ({ useEffect(() => { webLinter.triggerLint(tabs.code.uri.path); - }, [webLinter, fileType, sourceType, activeTab, tabs.code]); + }, [webLinter, fileType, sourceType, tabs.code]); return null; }; From 0adaa655582592be6ee5729044a8a584ad221c28 Mon Sep 17 00:00:00 2001 From: Armano Date: Mon, 3 Apr 2023 13:11:10 +0200 Subject: [PATCH 3/7] fix: refactor linter back to class --- .../src/components/OptionsSelector.tsx | 2 +- .../website/src/components/Playground.tsx | 2 +- .../src/components/ast/PropertyName.tsx | 31 ++-- .../src/components/editor/LoadedEditor.tsx | 4 +- .../website/src/components/editor/config.ts | 2 +- .../components/editor/useSandboxServices.ts | 8 +- .../src/components/hooks/useHashState.ts | 2 +- .../src/components/linter/WebLinter.ts | 142 +++++++++++++++++ .../src/components/linter/createLinter.ts | 143 ------------------ .../website/src/components/linter/types.ts | 10 -- 10 files changed, 170 insertions(+), 176 deletions(-) create mode 100644 packages/website/src/components/linter/WebLinter.ts delete mode 100644 packages/website/src/components/linter/createLinter.ts diff --git a/packages/website/src/components/OptionsSelector.tsx b/packages/website/src/components/OptionsSelector.tsx index 0b83812cfcdb..24adc49093f2 100644 --- a/packages/website/src/components/OptionsSelector.tsx +++ b/packages/website/src/components/OptionsSelector.tsx @@ -7,13 +7,13 @@ import IconExternalLink from '@theme/Icon/ExternalLink'; import React, { useCallback } from 'react'; import { useClipboard } from '../hooks/useClipboard'; -import { fileTypes } from './options'; import Dropdown from './inputs/Dropdown'; import Tooltip from './inputs/Tooltip'; import ActionLabel from './layout/ActionLabel'; import Expander from './layout/Expander'; import InputLabel from './layout/InputLabel'; import { createMarkdown, createMarkdownParams } from './lib/markdown'; +import { fileTypes } from './options'; import type { ConfigModel } from './types'; export interface OptionsSelectorParams { diff --git a/packages/website/src/components/Playground.tsx b/packages/website/src/components/Playground.tsx index 3f1b5cf75cc2..50dbb080cf90 100644 --- a/packages/website/src/components/Playground.tsx +++ b/packages/website/src/components/Playground.tsx @@ -5,7 +5,6 @@ import React, { useCallback, useState } from 'react'; import type { SourceFile } from 'typescript'; import ASTViewer from './ast/ASTViewer'; -import { defaultEslintConfig, defaultTsConfig, detailTabs } from './options'; import ConfigEslint from './config/ConfigEslint'; import ConfigTypeScript from './config/ConfigTypeScript'; import { EditorEmbed } from './editor/EditorEmbed'; @@ -15,6 +14,7 @@ import { ESQueryFilter } from './ESQueryFilter'; import useHashState from './hooks/useHashState'; import EditorTabs from './layout/EditorTabs'; import Loader from './layout/Loader'; +import { defaultEslintConfig, defaultTsConfig, detailTabs } from './options'; import OptionsSelector from './OptionsSelector'; import styles from './Playground.module.css'; import ConditionalSplitPane from './SplitPane/ConditionalSplitPane'; diff --git a/packages/website/src/components/ast/PropertyName.tsx b/packages/website/src/components/ast/PropertyName.tsx index d89b71cd0056..71a606996a90 100644 --- a/packages/website/src/components/ast/PropertyName.tsx +++ b/packages/website/src/components/ast/PropertyName.tsx @@ -9,45 +9,50 @@ export interface PropertyNameProps { readonly className?: string; } -export default function PropertyName(props: PropertyNameProps): JSX.Element { +export default function PropertyName({ + onClick: onClickProp, + onHover: onHoverProp, + className, + value, +}: PropertyNameProps): JSX.Element { const onClick = useCallback( (e: MouseEvent) => { e.preventDefault(); - props.onClick?.(); + onClickProp?.(); }, - [props.onClick], + [onClickProp], ); const onMouseEnter = useCallback(() => { - props.onHover?.(true); - }, [props.onHover]); + onHoverProp?.(true); + }, [onHoverProp]); const onMouseLeave = useCallback(() => { - props.onHover?.(false); - }, [props.onHover]); + onHoverProp?.(false); + }, [onHoverProp]); const onKeyDown = useCallback( (e: KeyboardEvent) => { if (e.code === 'Space') { e.preventDefault(); - props.onClick?.(); + onClickProp?.(); } }, - [props.onClick], + [onClickProp], ); return ( - {props.value} + {value} ); } diff --git a/packages/website/src/components/editor/LoadedEditor.tsx b/packages/website/src/components/editor/LoadedEditor.tsx index 15f94a11cc24..501b54a748c9 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -122,7 +122,7 @@ export const LoadedEditor: React.FC = ({ }, [activeTab, editor, tabs, updateMarkers]); useEffect(() => { - const disposable = webLinter.onLint((uri, messages) => { + const disposable = webLinter.onLint.register((uri, messages) => { const diagnostics = parseLintResults(messages, codeActions, ruleId => monaco.Uri.parse(webLinter.rules.get(ruleId)?.url ?? ''), ); @@ -137,7 +137,7 @@ export const LoadedEditor: React.FC = ({ }, [webLinter, monaco, codeActions, updateMarkers]); useEffect(() => { - const disposable = webLinter.onParse((uri, model) => { + const disposable = webLinter.onParse.register((uri, model) => { onEsASTChange(model.storedAST); onScopeChange(model.storedScope as Record | undefined); onTsASTChange(model.storedTsAST); diff --git a/packages/website/src/components/editor/config.ts b/packages/website/src/components/editor/config.ts index 103f89653c55..0f0c20a32429 100644 --- a/packages/website/src/components/editor/config.ts +++ b/packages/website/src/components/editor/config.ts @@ -2,7 +2,7 @@ import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'; import type * as ts from 'typescript'; import { getTypescriptOptions } from '../config/utils'; -import type { WebLinter } from '../linter/types'; +import type { WebLinter } from '../linter/WebLinter'; export function createCompilerOptions( tsConfig: Record = {}, diff --git a/packages/website/src/components/editor/useSandboxServices.ts b/packages/website/src/components/editor/useSandboxServices.ts index 5efe059a56f4..64c99f1fd99b 100644 --- a/packages/website/src/components/editor/useSandboxServices.ts +++ b/packages/website/src/components/editor/useSandboxServices.ts @@ -3,11 +3,11 @@ import type * as MonacoEditor from 'monaco-editor'; import { useEffect, useState } from 'react'; import type { createTypeScriptSandbox } from '../../vendor/sandbox'; -import { createCompilerOptions } from './config'; import { createFileSystem } from '../linter/bridge'; -import { createLinter } from '../linter/createLinter'; -import type { PlaygroundSystem, WebLinter } from '../linter/types'; +import type { PlaygroundSystem } from '../linter/types'; +import { WebLinter } from '../linter/WebLinter'; import type { RuleDetails } from '../types'; +import { createCompilerOptions } from './config'; import { editorEmbedId } from './EditorEmbed'; import { sandboxSingleton } from './loadSandbox'; import type { CommonEditorProps } from './types'; @@ -83,7 +83,7 @@ export const useSandboxServices = ( window.system = system; window.esquery = lintUtils.esquery; - const webLinter = createLinter( + const webLinter = new WebLinter( system, lintUtils, sandboxInstance.tsvfs, diff --git a/packages/website/src/components/hooks/useHashState.ts b/packages/website/src/components/hooks/useHashState.ts index 26dc810030cf..bf3c83b767e4 100644 --- a/packages/website/src/components/hooks/useHashState.ts +++ b/packages/website/src/components/hooks/useHashState.ts @@ -2,9 +2,9 @@ import { useHistory } from '@docusaurus/router'; import * as lz from 'lz-string'; import { useCallback, useState } from 'react'; -import { fileTypes } from '../options'; import { toJson } from '../config/utils'; import { hasOwnProperty } from '../lib/has-own-property'; +import { fileTypes } from '../options'; import type { ConfigFileType, ConfigModel, ConfigShowAst } from '../types'; function writeQueryParam(value: string | null): string { diff --git a/packages/website/src/components/linter/WebLinter.ts b/packages/website/src/components/linter/WebLinter.ts new file mode 100644 index 000000000000..1c07098a6d86 --- /dev/null +++ b/packages/website/src/components/linter/WebLinter.ts @@ -0,0 +1,142 @@ +import type * as tsvfs from '@site/src/vendor/typescript-vfs'; +import type { TSESLint } from '@typescript-eslint/utils'; +import type * as ts from 'typescript'; + +import { parseESLintRC, parseTSConfig } from '../config/utils'; +import { createCompilerOptions } from '../editor/config'; +import { createEventHelper } from '../lib/createEventHelper'; +import { defaultEslintConfig, PARSER_NAME } from './config'; +import { createParser } from './createParser'; +import type { + LinterOnLint, + LinterOnParse, + PlaygroundSystem, + WebLinterModule, +} from './types'; + +export class WebLinter { + readonly onLint = createEventHelper(); + readonly onParse = createEventHelper(); + readonly rules = new Map< + string, + { name: string; description?: string; url?: string } + >(); + readonly configs: string[] = []; + + readonly #configMap = new Map(); + #compilerOptions: ts.CompilerOptions = {}; + #eslintConfig: TSESLint.Linter.Config = { ...defaultEslintConfig }; + readonly #linter: TSESLint.Linter; + readonly #parser: ReturnType; + readonly #system: PlaygroundSystem; + + constructor( + system: PlaygroundSystem, + webLinterModule: WebLinterModule, + vfs: typeof tsvfs, + ) { + this.#configMap = new Map(Object.entries(webLinterModule.configs)); + this.#linter = webLinterModule.createLinter(); + this.#system = system; + this.configs = Array.from(this.#configMap.keys()); + + this.#parser = createParser( + system, + this.#compilerOptions, + (filename, model): void => { + this.onParse.trigger(filename, model); + }, + webLinterModule, + vfs, + ); + + this.#linter.defineParser(PARSER_NAME, this.#parser); + + this.#linter.getRules().forEach((item, name) => { + this.rules.set(name, { + name: name, + description: item.meta?.docs?.description, + url: item.meta?.docs?.url, + }); + }); + + system.watchFile('/input.*', this.triggerLint); + system.watchFile('/.eslintrc', this.#applyEslintConfig); + system.watchFile('/tsconfig.json', this.#applyTSConfig); + + this.#applyEslintConfig('/.eslintrc'); + this.#applyTSConfig('/tsconfig.json'); + } + + triggerLint(filename: string): void { + console.info('[Editor] linting triggered for file', filename); + const code = this.#system.readFile(filename) ?? '\n'; + if (code != null) { + const messages = this.#linter.verify(code, this.#eslintConfig, filename); + this.onLint.trigger(filename, messages); + } + } + + triggerFix(filename: string): TSESLint.Linter.FixReport | undefined { + const code = this.#system.readFile(filename); + if (code) { + return this.#linter.verifyAndFix(code, this.#eslintConfig, { + filename: filename, + fix: true, + }); + } + return undefined; + } + + updateParserOptions(sourceType?: TSESLint.SourceType): void { + this.#eslintConfig.parserOptions ??= {}; + this.#eslintConfig.parserOptions.sourceType = sourceType ?? 'module'; + } + + #resolveEslintConfig( + cfg: Partial, + ): TSESLint.Linter.Config { + const config = { rules: {} }; + if (cfg.extends) { + const cfgExtends = Array.isArray(cfg.extends) + ? cfg.extends + : [cfg.extends]; + for (const extendsName of cfgExtends) { + const maybeConfig = this.#configMap.get(extendsName); + if (maybeConfig) { + const resolved = this.#resolveEslintConfig(maybeConfig); + if (resolved.rules) { + Object.assign(config.rules, resolved.rules); + } + } + } + } + if (cfg.rules) { + Object.assign(config.rules, cfg.rules); + } + return config; + } + + #applyEslintConfig(fileName: string): void { + try { + const file = this.#system.readFile(fileName) ?? '{}'; + const parsed = this.#resolveEslintConfig(parseESLintRC(file)); + this.#eslintConfig.rules = parsed.rules; + console.log('[Editor] Updating', fileName, this.#eslintConfig); + } catch (e) { + console.error(e); + } + } + + #applyTSConfig(fileName: string): void { + try { + const file = this.#system.readFile(fileName) ?? '{}'; + const parsed = parseTSConfig(file).compilerOptions; + this.#compilerOptions = createCompilerOptions(parsed); + console.log('[Editor] Updating', fileName, this.#compilerOptions); + this.#parser.updateConfig(this.#compilerOptions); + } catch (e) { + console.error(e); + } + } +} diff --git a/packages/website/src/components/linter/createLinter.ts b/packages/website/src/components/linter/createLinter.ts deleted file mode 100644 index 05b019b9723e..000000000000 --- a/packages/website/src/components/linter/createLinter.ts +++ /dev/null @@ -1,143 +0,0 @@ -import type * as tsvfs from '@site/src/vendor/typescript-vfs'; -import type { TSESLint } from '@typescript-eslint/utils'; -import type * as ts from 'typescript'; - -import { parseESLintRC, parseTSConfig } from '../config/utils'; -import { createCompilerOptions } from '../editor/config'; -import { createEventHelper } from '../lib/createEventHelper'; -import { defaultEslintConfig, PARSER_NAME } from './config'; -import { createParser } from './createParser'; -import type { - LinterOnLint, - LinterOnParse, - PlaygroundSystem, - WebLinter, - WebLinterModule, -} from './types'; - -export function createLinter( - system: PlaygroundSystem, - webLinterModule: WebLinterModule, - vfs: typeof tsvfs, -): WebLinter { - const rules: WebLinter['rules'] = new Map(); - const configs = new Map(Object.entries(webLinterModule.configs)); - let compilerOptions: ts.CompilerOptions = {}; - const eslintConfig = { ...defaultEslintConfig }; - - const onLint = createEventHelper(); - const onParse = createEventHelper(); - - const linter = webLinterModule.createLinter(); - - const parser = createParser( - system, - compilerOptions, - (filename, model): void => { - onParse.trigger(filename, model); - }, - webLinterModule, - vfs, - ); - - linter.defineParser(PARSER_NAME, parser); - - linter.getRules().forEach((item, name) => { - rules.set(name, { - name: name, - description: item.meta?.docs?.description, - url: item.meta?.docs?.url, - }); - }); - - const triggerLint = (filename: string): void => { - console.info('[Editor] linting triggered for file', filename); - const code = system.readFile(filename) ?? '\n'; - if (code != null) { - const messages = linter.verify(code, eslintConfig, filename); - onLint.trigger(filename, messages); - } - }; - - const triggerFix = ( - filename: string, - ): TSESLint.Linter.FixReport | undefined => { - const code = system.readFile(filename); - if (code) { - return linter.verifyAndFix(code, eslintConfig, { - filename: filename, - fix: true, - }); - } - return undefined; - }; - - const updateParserOptions = (sourceType?: TSESLint.SourceType): void => { - eslintConfig.parserOptions ??= {}; - eslintConfig.parserOptions.sourceType = sourceType ?? 'module'; - }; - - const resolveEslintConfig = ( - cfg: Partial, - ): TSESLint.Linter.Config => { - const config = { rules: {} }; - if (cfg.extends) { - const cfgExtends = Array.isArray(cfg.extends) - ? cfg.extends - : [cfg.extends]; - for (const extendsName of cfgExtends) { - const maybeConfig = configs.get(extendsName); - if (maybeConfig) { - const resolved = resolveEslintConfig(maybeConfig); - if (resolved.rules) { - Object.assign(config.rules, resolved.rules); - } - } - } - } - if (cfg.rules) { - Object.assign(config.rules, cfg.rules); - } - return config; - }; - - const applyEslintConfig = (fileName: string): void => { - try { - const file = system.readFile(fileName) ?? '{}'; - const parsed = resolveEslintConfig(parseESLintRC(file)); - eslintConfig.rules = parsed.rules; - console.log('[Editor] Updating', fileName, eslintConfig); - } catch (e) { - console.error(e); - } - }; - - const applyTSConfig = (fileName: string): void => { - try { - const file = system.readFile(fileName) ?? '{}'; - const parsed = parseTSConfig(file).compilerOptions; - compilerOptions = createCompilerOptions(parsed); - console.log('[Editor] Updating', fileName, compilerOptions); - parser.updateConfig(compilerOptions); - } catch (e) { - console.error(e); - } - }; - - system.watchFile('/input.*', triggerLint); - system.watchFile('/.eslintrc', applyEslintConfig); - system.watchFile('/tsconfig.json', applyTSConfig); - - applyEslintConfig('/.eslintrc'); - applyTSConfig('/tsconfig.json'); - - return { - rules, - configs: Array.from(configs.keys()), - triggerFix, - triggerLint, - updateParserOptions, - onParse: onParse.register, - onLint: onLint.register, - }; -} diff --git a/packages/website/src/components/linter/types.ts b/packages/website/src/components/linter/types.ts index bd72c1eaf6ab..3623eb649ff7 100644 --- a/packages/website/src/components/linter/types.ts +++ b/packages/website/src/components/linter/types.ts @@ -33,13 +33,3 @@ export type LinterOnLint = ( ) => void; export type LinterOnParse = (fileName: string, model: UpdateModel) => void; - -export interface WebLinter { - rules: Map; - configs: string[]; - triggerFix(filename: string): TSESLint.Linter.FixReport | undefined; - triggerLint(filename: string): void; - onLint(cb: LinterOnLint): () => void; - onParse(cb: LinterOnParse): () => void; - updateParserOptions(sourceType?: TSESLint.SourceType): void; -} From 684833ae1f054eca0d05a8beb9724c1ce87c3fec Mon Sep 17 00:00:00 2001 From: Armano Date: Mon, 3 Apr 2023 16:04:34 +0200 Subject: [PATCH 4/7] fix: apply changes from code review --- .../components/config/ConfigTypeScript.tsx | 3 +- .../website/src/components/config/utils.ts | 42 ------------ .../src/components/editor/LoadedEditor.tsx | 16 ++--- .../components/editor/useSandboxServices.ts | 3 +- .../components/lib/createCompilerOptions.ts | 32 +++++++++ ...teEventHelper.ts => createEventsBinder.ts} | 2 +- .../{editor/config.ts => lib/jsonSchema.ts} | 67 +++++++++---------- .../src/components/linter/WebLinter.ts | 8 +-- .../website/src/components/linter/bridge.ts | 5 +- packages/website/src/globals.d.ts | 1 + packages/website/typings/typescript.d.ts | 12 ++++ 11 files changed, 95 insertions(+), 96 deletions(-) create mode 100644 packages/website/src/components/lib/createCompilerOptions.ts rename packages/website/src/components/lib/{createEventHelper.ts => createEventsBinder.ts} (84%) rename packages/website/src/components/{editor/config.ts => lib/jsonSchema.ts} (69%) diff --git a/packages/website/src/components/config/ConfigTypeScript.tsx b/packages/website/src/components/config/ConfigTypeScript.tsx index 40cd634ffb17..b5892277d8f4 100644 --- a/packages/website/src/components/config/ConfigTypeScript.tsx +++ b/packages/website/src/components/config/ConfigTypeScript.tsx @@ -1,10 +1,11 @@ import React, { useCallback, useEffect, useState } from 'react'; +import { getTypescriptOptions } from '../lib/jsonSchema'; import { shallowEqual } from '../lib/shallowEqual'; import type { ConfigModel, TSConfig } from '../types'; import type { ConfigOptionsType } from './ConfigEditor'; import ConfigEditor from './ConfigEditor'; -import { getTypescriptOptions, parseTSConfig, toJson } from './utils'; +import { parseTSConfig, toJson } from './utils'; interface ConfigTypeScriptProps { readonly isOpen: boolean; diff --git a/packages/website/src/components/config/utils.ts b/packages/website/src/components/config/utils.ts index 8c4092164ade..936b7435eb0f 100644 --- a/packages/website/src/components/config/utils.ts +++ b/packages/website/src/components/config/utils.ts @@ -2,16 +2,6 @@ import { isRecord } from '@site/src/components/ast/utils'; import type { EslintRC, TSConfig } from '@site/src/components/types'; import json5 from 'json5'; -export interface OptionDeclarations { - name: string; - type?: unknown; - category?: { message: string }; - description?: { message: string }; - element?: { - type: unknown; - }; -} - export function parseESLintRC(code?: string): EslintRC { if (code) { try { @@ -78,35 +68,3 @@ export function tryParseEslintModule(value: string): string { export function toJson(cfg: unknown): string { return JSON.stringify(cfg, null, 2); } - -export function getTypescriptOptions(): OptionDeclarations[] { - const allowedCategories = [ - 'Command-line Options', - 'Projects', - 'Compiler Diagnostics', - 'Editor Support', - 'Output Formatting', - 'Watch and Build Modes', - 'Source Map Options', - ]; - - const filteredNames = [ - 'moduleResolution', - 'moduleDetection', - 'plugins', - 'typeRoots', - 'jsx', - ]; - - // @ts-expect-error: definition is not fully correct - return (window.ts.optionDeclarations as OptionDeclarations[]).filter( - item => - (item.type === 'boolean' || - item.type === 'list' || - item.type instanceof Map) && - item.description && - item.category && - !allowedCategories.includes(item.category.message) && - !filteredNames.includes(item.name), - ); -} diff --git a/packages/website/src/components/editor/LoadedEditor.tsx b/packages/website/src/components/editor/LoadedEditor.tsx index 501b54a748c9..6da67570fc1f 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -5,15 +5,15 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { parseTSConfig, tryParseEslintModule } from '../config/utils'; import { useResizeObserver } from '../hooks/useResizeObserver'; +import { createCompilerOptions } from '../lib/createCompilerOptions'; import { debounce } from '../lib/debounce'; +import { + getEslintJsonSchema, + getTypescriptJsonSchema, +} from '../lib/jsonSchema'; import type { LintCodeAction } from '../linter/utils'; import { parseLintResults, parseMarkers } from '../linter/utils'; import type { TabType } from '../types'; -import { - createCompilerOptions, - getEslintSchema, - getTsConfigSchema, -} from './config'; import { createProvideCodeActions } from './createProvideCodeActions'; import type { CommonEditorProps } from './types'; import type { SandboxServices } from './useSandboxServices'; @@ -155,16 +155,16 @@ export const LoadedEditor: React.FC = ({ { uri: monaco.Uri.file('eslint-schema.json').toString(), // id of the first schema fileMatch: ['/.eslintrc'], // associate with our model - schema: getEslintSchema(webLinter), + schema: getEslintJsonSchema(webLinter), }, { uri: monaco.Uri.file('ts-schema.json').toString(), // id of the first schema fileMatch: ['/tsconfig.json'], // associate with our model - schema: getTsConfigSchema(), + schema: getTypescriptJsonSchema(), }, ], }); - }, [monaco, webLinter.rules]); + }, [monaco, webLinter]); useEffect(() => { const disposable = monaco.languages.registerCodeActionProvider( diff --git a/packages/website/src/components/editor/useSandboxServices.ts b/packages/website/src/components/editor/useSandboxServices.ts index 64c99f1fd99b..6578eefe0928 100644 --- a/packages/website/src/components/editor/useSandboxServices.ts +++ b/packages/website/src/components/editor/useSandboxServices.ts @@ -3,11 +3,11 @@ import type * as MonacoEditor from 'monaco-editor'; import { useEffect, useState } from 'react'; import type { createTypeScriptSandbox } from '../../vendor/sandbox'; +import { createCompilerOptions } from '../lib/createCompilerOptions'; import { createFileSystem } from '../linter/bridge'; import type { PlaygroundSystem } from '../linter/types'; import { WebLinter } from '../linter/WebLinter'; import type { RuleDetails } from '../types'; -import { createCompilerOptions } from './config'; import { editorEmbedId } from './EditorEmbed'; import { sandboxSingleton } from './loadSandbox'; import type { CommonEditorProps } from './types'; @@ -79,7 +79,6 @@ export const useSandboxServices = ( } } - // @ts-expect-error - we're adding these to the window for debugging purposes window.system = system; window.esquery = lintUtils.esquery; diff --git a/packages/website/src/components/lib/createCompilerOptions.ts b/packages/website/src/components/lib/createCompilerOptions.ts new file mode 100644 index 000000000000..9415a56bcd7b --- /dev/null +++ b/packages/website/src/components/lib/createCompilerOptions.ts @@ -0,0 +1,32 @@ +import type * as ts from 'typescript'; + +export function createCompilerOptions( + tsConfig: Record = {}, +): ts.CompilerOptions { + const config = window.ts.convertCompilerOptionsFromJson( + { + // ts and monaco has different type as monaco types are not changing base on ts version + target: 'esnext', + module: 'esnext', + jsx: 'preserve', + ...tsConfig, + allowJs: true, + lib: Array.isArray(tsConfig.lib) ? tsConfig.lib : undefined, + moduleResolution: undefined, + plugins: undefined, + typeRoots: undefined, + paths: undefined, + moduleDetection: undefined, + baseUrl: undefined, + }, + '/tsconfig.json', + ); + + const options = config.options; + + if (!options.lib) { + options.lib = [window.ts.getDefaultLibFileName(options)]; + } + + return options; +} diff --git a/packages/website/src/components/lib/createEventHelper.ts b/packages/website/src/components/lib/createEventsBinder.ts similarity index 84% rename from packages/website/src/components/lib/createEventHelper.ts rename to packages/website/src/components/lib/createEventsBinder.ts index 17e93028b0d1..6b5bfaecbee1 100644 --- a/packages/website/src/components/lib/createEventHelper.ts +++ b/packages/website/src/components/lib/createEventsBinder.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createEventHelper void>(): { +export function createEventsBinder void>(): { trigger: (...args: Parameters) => void; register: (cb: T) => () => void; } { diff --git a/packages/website/src/components/editor/config.ts b/packages/website/src/components/lib/jsonSchema.ts similarity index 69% rename from packages/website/src/components/editor/config.ts rename to packages/website/src/components/lib/jsonSchema.ts index 0f0c20a32429..b369d42e5c2c 100644 --- a/packages/website/src/components/editor/config.ts +++ b/packages/website/src/components/lib/jsonSchema.ts @@ -1,41 +1,9 @@ import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'; import type * as ts from 'typescript'; -import { getTypescriptOptions } from '../config/utils'; import type { WebLinter } from '../linter/WebLinter'; -export function createCompilerOptions( - tsConfig: Record = {}, -): ts.CompilerOptions { - const config = window.ts.convertCompilerOptionsFromJson( - { - // ts and monaco has different type as monaco types are not changing base on ts version - target: 'esnext', - module: 'esnext', - jsx: 'preserve', - ...tsConfig, - allowJs: true, - lib: Array.isArray(tsConfig.lib) ? tsConfig.lib : undefined, - moduleResolution: undefined, - plugins: undefined, - typeRoots: undefined, - paths: undefined, - moduleDetection: undefined, - baseUrl: undefined, - }, - '/tsconfig.json', - ); - - const options = config.options; - - if (!options.lib) { - options.lib = [window.ts.getDefaultLibFileName(options)]; - } - - return options; -} - -export function getEslintSchema(linter: WebLinter): JSONSchema4 { +export function getEslintJsonSchema(linter: WebLinter): JSONSchema4 { const properties: Record = {}; for (const [, item] of linter.rules) { @@ -83,7 +51,38 @@ export function getEslintSchema(linter: WebLinter): JSONSchema4 { }; } -export function getTsConfigSchema(): JSONSchema4 { +export function getTypescriptOptions(): ts.OptionDeclarations[] { + const allowedCategories = [ + 'Command-line Options', + 'Projects', + 'Compiler Diagnostics', + 'Editor Support', + 'Output Formatting', + 'Watch and Build Modes', + 'Source Map Options', + ]; + + const filteredNames = [ + 'moduleResolution', + 'moduleDetection', + 'plugins', + 'typeRoots', + 'jsx', + ]; + + return window.ts.optionDeclarations.filter( + item => + (item.type === 'boolean' || + item.type === 'list' || + item.type instanceof Map) && + item.description && + item.category && + !allowedCategories.includes(item.category.message) && + !filteredNames.includes(item.name), + ); +} + +export function getTypescriptJsonSchema(): JSONSchema4 { const properties = getTypescriptOptions().reduce((options, item) => { if (item.type === 'boolean') { options[item.name] = { diff --git a/packages/website/src/components/linter/WebLinter.ts b/packages/website/src/components/linter/WebLinter.ts index 1c07098a6d86..427710a0c0bb 100644 --- a/packages/website/src/components/linter/WebLinter.ts +++ b/packages/website/src/components/linter/WebLinter.ts @@ -3,8 +3,8 @@ import type { TSESLint } from '@typescript-eslint/utils'; import type * as ts from 'typescript'; import { parseESLintRC, parseTSConfig } from '../config/utils'; -import { createCompilerOptions } from '../editor/config'; -import { createEventHelper } from '../lib/createEventHelper'; +import { createCompilerOptions } from '../lib/createCompilerOptions'; +import { createEventsBinder } from '../lib/createEventsBinder'; import { defaultEslintConfig, PARSER_NAME } from './config'; import { createParser } from './createParser'; import type { @@ -15,8 +15,8 @@ import type { } from './types'; export class WebLinter { - readonly onLint = createEventHelper(); - readonly onParse = createEventHelper(); + readonly onLint = createEventsBinder(); + readonly onParse = createEventsBinder(); readonly rules = new Map< string, { name: string; description?: string; url?: string } diff --git a/packages/website/src/components/linter/bridge.ts b/packages/website/src/components/linter/bridge.ts index b17d516ca0e2..b0614fc4329d 100644 --- a/packages/website/src/components/linter/bridge.ts +++ b/packages/website/src/components/linter/bridge.ts @@ -37,10 +37,7 @@ export function createFileSystem( return { close: (): void => { - const handle = fileWatcherCallbacks.get(expPath); - if (handle) { - handle.delete(cb); - } + fileWatcherCallbacks.get(expPath)?.delete(cb); }, }; }; diff --git a/packages/website/src/globals.d.ts b/packages/website/src/globals.d.ts index 898d3649bee2..0f4c2463b060 100644 --- a/packages/website/src/globals.d.ts +++ b/packages/website/src/globals.d.ts @@ -18,5 +18,6 @@ declare global { ts: typeof ts; require: WindowRequire; esquery: typeof esquery; + system: unknown; } } diff --git a/packages/website/typings/typescript.d.ts b/packages/website/typings/typescript.d.ts index 7239e4ddcb5f..96ffc2a4d3cd 100644 --- a/packages/website/typings/typescript.d.ts +++ b/packages/website/typings/typescript.d.ts @@ -10,4 +10,16 @@ declare module 'typescript' { * The value is the file name */ const libMap: StringMap; + + interface OptionDeclarations { + name: string; + type?: unknown; + category?: { message: string }; + description?: { message: string }; + element?: { + type: unknown; + }; + } + + const optionDeclarations: OptionDeclarations[]; } From 6f897b5fe39427ea0499bfdc8aa689e0bd466b7c Mon Sep 17 00:00:00 2001 From: Armano Date: Mon, 3 Apr 2023 16:24:22 +0200 Subject: [PATCH 5/7] fix: revert conversion from createLinter to class --- .../src/components/editor/LoadedEditor.tsx | 4 +- .../components/editor/useSandboxServices.ts | 4 +- .../src/components/linter/WebLinter.ts | 172 +++++++++--------- 3 files changed, 95 insertions(+), 85 deletions(-) diff --git a/packages/website/src/components/editor/LoadedEditor.tsx b/packages/website/src/components/editor/LoadedEditor.tsx index 6da67570fc1f..f1c15e67471f 100644 --- a/packages/website/src/components/editor/LoadedEditor.tsx +++ b/packages/website/src/components/editor/LoadedEditor.tsx @@ -122,7 +122,7 @@ export const LoadedEditor: React.FC = ({ }, [activeTab, editor, tabs, updateMarkers]); useEffect(() => { - const disposable = webLinter.onLint.register((uri, messages) => { + const disposable = webLinter.onLint((uri, messages) => { const diagnostics = parseLintResults(messages, codeActions, ruleId => monaco.Uri.parse(webLinter.rules.get(ruleId)?.url ?? ''), ); @@ -137,7 +137,7 @@ export const LoadedEditor: React.FC = ({ }, [webLinter, monaco, codeActions, updateMarkers]); useEffect(() => { - const disposable = webLinter.onParse.register((uri, model) => { + const disposable = webLinter.onParse((uri, model) => { onEsASTChange(model.storedAST); onScopeChange(model.storedScope as Record | undefined); onTsASTChange(model.storedTsAST); diff --git a/packages/website/src/components/editor/useSandboxServices.ts b/packages/website/src/components/editor/useSandboxServices.ts index 6578eefe0928..9cb74ca1edcd 100644 --- a/packages/website/src/components/editor/useSandboxServices.ts +++ b/packages/website/src/components/editor/useSandboxServices.ts @@ -6,7 +6,7 @@ import type { createTypeScriptSandbox } from '../../vendor/sandbox'; import { createCompilerOptions } from '../lib/createCompilerOptions'; import { createFileSystem } from '../linter/bridge'; import type { PlaygroundSystem } from '../linter/types'; -import { WebLinter } from '../linter/WebLinter'; +import { createLinter, type WebLinter } from '../linter/WebLinter'; import type { RuleDetails } from '../types'; import { editorEmbedId } from './EditorEmbed'; import { sandboxSingleton } from './loadSandbox'; @@ -82,7 +82,7 @@ export const useSandboxServices = ( window.system = system; window.esquery = lintUtils.esquery; - const webLinter = new WebLinter( + const webLinter = createLinter( system, lintUtils, sandboxInstance.tsvfs, diff --git a/packages/website/src/components/linter/WebLinter.ts b/packages/website/src/components/linter/WebLinter.ts index 427710a0c0bb..01f96e8301d6 100644 --- a/packages/website/src/components/linter/WebLinter.ts +++ b/packages/website/src/components/linter/WebLinter.ts @@ -14,97 +14,90 @@ import type { WebLinterModule, } from './types'; -export class WebLinter { - readonly onLint = createEventsBinder(); - readonly onParse = createEventsBinder(); - readonly rules = new Map< - string, - { name: string; description?: string; url?: string } - >(); - readonly configs: string[] = []; - - readonly #configMap = new Map(); - #compilerOptions: ts.CompilerOptions = {}; - #eslintConfig: TSESLint.Linter.Config = { ...defaultEslintConfig }; - readonly #linter: TSESLint.Linter; - readonly #parser: ReturnType; - readonly #system: PlaygroundSystem; - - constructor( - system: PlaygroundSystem, - webLinterModule: WebLinterModule, - vfs: typeof tsvfs, - ) { - this.#configMap = new Map(Object.entries(webLinterModule.configs)); - this.#linter = webLinterModule.createLinter(); - this.#system = system; - this.configs = Array.from(this.#configMap.keys()); - - this.#parser = createParser( - system, - this.#compilerOptions, - (filename, model): void => { - this.onParse.trigger(filename, model); - }, - webLinterModule, - vfs, - ); - - this.#linter.defineParser(PARSER_NAME, this.#parser); - - this.#linter.getRules().forEach((item, name) => { - this.rules.set(name, { - name: name, - description: item.meta?.docs?.description, - url: item.meta?.docs?.url, - }); - }); +export interface WebLinter { + rules: Map; + configs: string[]; + triggerFix(filename: string): TSESLint.Linter.FixReport | undefined; + triggerLint(filename: string): void; + onLint(cb: LinterOnLint): () => void; + onParse(cb: LinterOnParse): () => void; + updateParserOptions(sourceType?: TSESLint.SourceType): void; +} + +export function createLinter( + system: PlaygroundSystem, + webLinterModule: WebLinterModule, + vfs: typeof tsvfs, +): WebLinter { + const rules: WebLinter['rules'] = new Map(); + const configs = new Map(Object.entries(webLinterModule.configs)); + let compilerOptions: ts.CompilerOptions = {}; + const eslintConfig = { ...defaultEslintConfig }; + + const onLint = createEventsBinder(); + const onParse = createEventsBinder(); + + const linter = webLinterModule.createLinter(); - system.watchFile('/input.*', this.triggerLint); - system.watchFile('/.eslintrc', this.#applyEslintConfig); - system.watchFile('/tsconfig.json', this.#applyTSConfig); + const parser = createParser( + system, + compilerOptions, + (filename, model): void => { + onParse.trigger(filename, model); + }, + webLinterModule, + vfs, + ); - this.#applyEslintConfig('/.eslintrc'); - this.#applyTSConfig('/tsconfig.json'); - } + linter.defineParser(PARSER_NAME, parser); - triggerLint(filename: string): void { + linter.getRules().forEach((item, name) => { + rules.set(name, { + name: name, + description: item.meta?.docs?.description, + url: item.meta?.docs?.url, + }); + }); + + const triggerLint = (filename: string): void => { console.info('[Editor] linting triggered for file', filename); - const code = this.#system.readFile(filename) ?? '\n'; + const code = system.readFile(filename) ?? '\n'; if (code != null) { - const messages = this.#linter.verify(code, this.#eslintConfig, filename); - this.onLint.trigger(filename, messages); + const messages = linter.verify(code, eslintConfig, filename); + onLint.trigger(filename, messages); } - } + }; - triggerFix(filename: string): TSESLint.Linter.FixReport | undefined { - const code = this.#system.readFile(filename); + const triggerFix = ( + filename: string, + ): TSESLint.Linter.FixReport | undefined => { + const code = system.readFile(filename); if (code) { - return this.#linter.verifyAndFix(code, this.#eslintConfig, { + return linter.verifyAndFix(code, eslintConfig, { filename: filename, fix: true, }); } return undefined; - } + }; - updateParserOptions(sourceType?: TSESLint.SourceType): void { - this.#eslintConfig.parserOptions ??= {}; - this.#eslintConfig.parserOptions.sourceType = sourceType ?? 'module'; - } + const updateParserOptions = (sourceType?: TSESLint.SourceType): void => { + eslintConfig.parserOptions ??= {}; + eslintConfig.parserOptions.sourceType = sourceType ?? 'module'; + }; - #resolveEslintConfig( + const resolveEslintConfig = ( cfg: Partial, - ): TSESLint.Linter.Config { + ): TSESLint.Linter.Config => { const config = { rules: {} }; if (cfg.extends) { const cfgExtends = Array.isArray(cfg.extends) ? cfg.extends : [cfg.extends]; for (const extendsName of cfgExtends) { - const maybeConfig = this.#configMap.get(extendsName); + const maybeConfig = configs.get(extendsName); if (maybeConfig) { - const resolved = this.#resolveEslintConfig(maybeConfig); + const resolved = resolveEslintConfig(maybeConfig); if (resolved.rules) { Object.assign(config.rules, resolved.rules); } @@ -115,28 +108,45 @@ export class WebLinter { Object.assign(config.rules, cfg.rules); } return config; - } + }; - #applyEslintConfig(fileName: string): void { + const applyEslintConfig = (fileName: string): void => { try { - const file = this.#system.readFile(fileName) ?? '{}'; - const parsed = this.#resolveEslintConfig(parseESLintRC(file)); - this.#eslintConfig.rules = parsed.rules; - console.log('[Editor] Updating', fileName, this.#eslintConfig); + const file = system.readFile(fileName) ?? '{}'; + const parsed = resolveEslintConfig(parseESLintRC(file)); + eslintConfig.rules = parsed.rules; + console.log('[Editor] Updating', fileName, eslintConfig); } catch (e) { console.error(e); } - } + }; - #applyTSConfig(fileName: string): void { + const applyTSConfig = (fileName: string): void => { try { - const file = this.#system.readFile(fileName) ?? '{}'; + const file = system.readFile(fileName) ?? '{}'; const parsed = parseTSConfig(file).compilerOptions; - this.#compilerOptions = createCompilerOptions(parsed); - console.log('[Editor] Updating', fileName, this.#compilerOptions); - this.#parser.updateConfig(this.#compilerOptions); + compilerOptions = createCompilerOptions(parsed); + console.log('[Editor] Updating', fileName, compilerOptions); + parser.updateConfig(compilerOptions); } catch (e) { console.error(e); } - } + }; + + system.watchFile('/input.*', triggerLint); + system.watchFile('/.eslintrc', applyEslintConfig); + system.watchFile('/tsconfig.json', applyTSConfig); + + applyEslintConfig('/.eslintrc'); + applyTSConfig('/tsconfig.json'); + + return { + rules, + configs: Array.from(configs.keys()), + triggerFix, + triggerLint, + updateParserOptions, + onParse: onParse.register, + onLint: onLint.register, + }; } From 223b9477738195f5ad341864592a2229c5123f62 Mon Sep 17 00:00:00 2001 From: Armano Date: Mon, 3 Apr 2023 16:31:40 +0200 Subject: [PATCH 6/7] fix: revert conversion from createLinter to class part 2 --- .../website/src/components/editor/useSandboxServices.ts | 4 ++-- packages/website/src/components/lib/jsonSchema.ts | 4 ++-- .../src/components/linter/{WebLinter.ts => createLinter.ts} | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) rename packages/website/src/components/linter/{WebLinter.ts => createLinter.ts} (97%) diff --git a/packages/website/src/components/editor/useSandboxServices.ts b/packages/website/src/components/editor/useSandboxServices.ts index 9cb74ca1edcd..f815ff5256cd 100644 --- a/packages/website/src/components/editor/useSandboxServices.ts +++ b/packages/website/src/components/editor/useSandboxServices.ts @@ -6,7 +6,7 @@ import type { createTypeScriptSandbox } from '../../vendor/sandbox'; import { createCompilerOptions } from '../lib/createCompilerOptions'; import { createFileSystem } from '../linter/bridge'; import type { PlaygroundSystem } from '../linter/types'; -import { createLinter, type WebLinter } from '../linter/WebLinter'; +import { createLinter, type CreateLinter } from '../linter/createLinter'; import type { RuleDetails } from '../types'; import { editorEmbedId } from './EditorEmbed'; import { sandboxSingleton } from './loadSandbox'; @@ -25,7 +25,7 @@ export type SandboxInstance = ReturnType; export interface SandboxServices { sandboxInstance: SandboxInstance; system: PlaygroundSystem; - webLinter: WebLinter; + webLinter: CreateLinter; } export const useSandboxServices = ( diff --git a/packages/website/src/components/lib/jsonSchema.ts b/packages/website/src/components/lib/jsonSchema.ts index b369d42e5c2c..ee561c114954 100644 --- a/packages/website/src/components/lib/jsonSchema.ts +++ b/packages/website/src/components/lib/jsonSchema.ts @@ -1,9 +1,9 @@ import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'; import type * as ts from 'typescript'; -import type { WebLinter } from '../linter/WebLinter'; +import type { CreateLinter } from '../linter/createLinter'; -export function getEslintJsonSchema(linter: WebLinter): JSONSchema4 { +export function getEslintJsonSchema(linter: CreateLinter): JSONSchema4 { const properties: Record = {}; for (const [, item] of linter.rules) { diff --git a/packages/website/src/components/linter/WebLinter.ts b/packages/website/src/components/linter/createLinter.ts similarity index 97% rename from packages/website/src/components/linter/WebLinter.ts rename to packages/website/src/components/linter/createLinter.ts index 01f96e8301d6..f4dc5b492679 100644 --- a/packages/website/src/components/linter/WebLinter.ts +++ b/packages/website/src/components/linter/createLinter.ts @@ -14,7 +14,7 @@ import type { WebLinterModule, } from './types'; -export interface WebLinter { +export interface CreateLinter { rules: Map; configs: string[]; triggerFix(filename: string): TSESLint.Linter.FixReport | undefined; @@ -28,8 +28,8 @@ export function createLinter( system: PlaygroundSystem, webLinterModule: WebLinterModule, vfs: typeof tsvfs, -): WebLinter { - const rules: WebLinter['rules'] = new Map(); +): CreateLinter { + const rules: CreateLinter['rules'] = new Map(); const configs = new Map(Object.entries(webLinterModule.configs)); let compilerOptions: ts.CompilerOptions = {}; const eslintConfig = { ...defaultEslintConfig }; From 403d8a1301bd426b0935f4ac152bcd1307b21662 Mon Sep 17 00:00:00 2001 From: Armano Date: Mon, 3 Apr 2023 17:40:45 +0200 Subject: [PATCH 7/7] fix: correct linting issue --- packages/website/src/components/editor/useSandboxServices.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/website/src/components/editor/useSandboxServices.ts b/packages/website/src/components/editor/useSandboxServices.ts index f815ff5256cd..cbfcf0afba0b 100644 --- a/packages/website/src/components/editor/useSandboxServices.ts +++ b/packages/website/src/components/editor/useSandboxServices.ts @@ -5,8 +5,8 @@ import { useEffect, useState } from 'react'; import type { createTypeScriptSandbox } from '../../vendor/sandbox'; import { createCompilerOptions } from '../lib/createCompilerOptions'; import { createFileSystem } from '../linter/bridge'; +import { type CreateLinter, createLinter } from '../linter/createLinter'; import type { PlaygroundSystem } from '../linter/types'; -import { createLinter, type CreateLinter } from '../linter/createLinter'; import type { RuleDetails } from '../types'; import { editorEmbedId } from './EditorEmbed'; import { sandboxSingleton } from './loadSandbox'; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy