diff --git a/packages/website/src/components/OptionsSelector.tsx b/packages/website/src/components/OptionsSelector.tsx index 3e3305d886da..f7e2f88f75e7 100644 --- a/packages/website/src/components/OptionsSelector.tsx +++ b/packages/website/src/components/OptionsSelector.tsx @@ -7,7 +7,7 @@ import IconExternalLink from '@theme/Icon/ExternalLink'; import React, { useCallback } from 'react'; import { useClipboard } from '../hooks/useClipboard'; -import Checkbox from './inputs/Checkbox'; +import { fileTypes } from './config'; import Dropdown from './inputs/Dropdown'; import Tooltip from './inputs/Tooltip'; import ActionLabel from './layout/ActionLabel'; @@ -74,11 +74,12 @@ function OptionsSelectorContent({ {process.env.TS_ESLINT_VERSION} - - setState({ jsx: e })} + + setState({ fileType })} + options={fileTypes} /> diff --git a/packages/website/src/components/Playground.tsx b/packages/website/src/components/Playground.tsx index 79c5c9c3f89e..12e393682b72 100644 --- a/packages/website/src/components/Playground.tsx +++ b/packages/website/src/components/Playground.tsx @@ -29,7 +29,7 @@ import type { function Playground(): JSX.Element { const [state, setState] = useHashState({ - jsx: false, + fileType: '.tsx', showAST: false, sourceType: 'module', code: '', @@ -131,7 +131,7 @@ function Playground(): JSX.Element { = ({ tsconfig, eslintrc, selectedRange, - jsx, + fileType, onEsASTChange, onScopeChange, onTsASTChange, @@ -84,11 +84,11 @@ export const LoadedEditor: React.FC = ({ ]); useEffect(() => { - const newPath = jsx ? '/input.tsx' : '/input.ts'; + const newPath = `/input${fileType}`; if (tabs.code.uri.path !== newPath) { const newModel = sandboxInstance.monaco.editor.createModel( tabs.code.getValue(), - 'typescript', + undefined, sandboxInstance.monaco.Uri.file(newPath), ); newModel.updateOptions({ tabSize: 2, insertSpaces: true }); @@ -98,22 +98,15 @@ export const LoadedEditor: React.FC = ({ tabs.code.dispose(); tabs.code = newModel; } - }, [ - jsx, - sandboxInstance.editor, - sandboxInstance.monaco.Uri, - sandboxInstance.monaco.editor, - tabs, - ]); + }, [fileType, sandboxInstance.editor, sandboxInstance.monaco, tabs]); useEffect(() => { const config = createCompilerOptions( - jsx, parseTSConfig(tsconfig).compilerOptions, ); webLinter.updateCompilerOptions(config); sandboxInstance.setCompilerSettings(config); - }, [jsx, sandboxInstance, tsconfig, webLinter]); + }, [sandboxInstance, tsconfig, webLinter]); useEffect(() => { webLinter.updateRules(parseESLintRC(eslintrc).rules); @@ -128,10 +121,10 @@ export const LoadedEditor: React.FC = ({ const lintEditor = debounce(() => { console.info('[Editor] linting triggered'); - webLinter.updateParserOptions(jsx, sourceType); + webLinter.updateParserOptions(sourceType); try { - const messages = webLinter.lint(code); + const messages = webLinter.lint(code, tabs.code.uri.path); const markers = parseLintResults(messages, codeActions, ruleId => sandboxInstance.monaco.Uri.parse( @@ -164,7 +157,7 @@ export const LoadedEditor: React.FC = ({ lintEditor(); }, [ code, - jsx, + fileType, tsconfig, eslintrc, sourceType, @@ -239,7 +232,10 @@ export const LoadedEditor: React.FC = ({ run(editor) { const editorModel = editor.getModel(); if (editorModel) { - const fixed = webLinter.fix(editor.getValue()); + const fixed = webLinter.fix( + editor.getValue(), + editorModel.uri.path, + ); if (fixed.fixed) { editorModel.pushEditOperations( null, diff --git a/packages/website/src/components/editor/config.ts b/packages/website/src/components/editor/config.ts index aee01fb11f07..1882238595ce 100644 --- a/packages/website/src/components/editor/config.ts +++ b/packages/website/src/components/editor/config.ts @@ -4,7 +4,6 @@ import type Monaco from 'monaco-editor'; import { getTypescriptOptions } from '../config/utils'; export function createCompilerOptions( - jsx = false, tsConfig: Record = {}, ): Monaco.languages.typescript.CompilerOptions { const config = window.ts.convertCompilerOptionsFromJson( @@ -12,8 +11,9 @@ export function createCompilerOptions( // ts and monaco has different type as monaco types are not changing base on ts version target: 'esnext', module: 'esnext', + jsx: 'preserve', ...tsConfig, - jsx: jsx ? 'preserve' : undefined, + allowJs: true, lib: Array.isArray(tsConfig.lib) ? tsConfig.lib : undefined, moduleResolution: undefined, plugins: undefined, diff --git a/packages/website/src/components/editor/useSandboxServices.ts b/packages/website/src/components/editor/useSandboxServices.ts index 3007856e54ac..6bcc9d01b885 100644 --- a/packages/website/src/components/editor/useSandboxServices.ts +++ b/packages/website/src/components/editor/useSandboxServices.ts @@ -33,22 +33,14 @@ export const useSandboxServices = ( ): Error | SandboxServices | undefined => { const { onLoaded } = props; const [services, setServices] = useState(); - const [loadedTs, setLoadedTs] = useState(props.ts); const { colorMode } = useColorMode(); - useEffect(() => { - if (props.ts !== loadedTs) { - window.location.reload(); - } - }, [props.ts, loadedTs]); - useEffect(() => { let sandboxInstance: SandboxInstance | undefined; - setLoadedTs(props.ts); sandboxSingleton(props.ts) .then(async ({ main, sandboxFactory, lintUtils }) => { - const compilerOptions = createCompilerOptions(props.jsx); + const compilerOptions = createCompilerOptions(); const sandboxConfig: Partial = { text: props.code, @@ -128,7 +120,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/hooks/useHashState.ts b/packages/website/src/components/hooks/useHashState.ts index d8a7a44b5ce9..2c80d7a3ab23 100644 --- a/packages/website/src/components/hooks/useHashState.ts +++ b/packages/website/src/components/hooks/useHashState.ts @@ -1,31 +1,39 @@ -import { toJsonConfig } from '@site/src/components/config/utils'; +import { useHistory } from '@docusaurus/router'; import * as lz from 'lz-string'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useState } from 'react'; +import { fileTypes } from '../config'; +import { toJson } from '../config/utils'; import { hasOwnProperty } from '../lib/has-own-property'; -import { shallowEqual } from '../lib/shallowEqual'; -import type { ConfigModel } from '../types'; +import type { ConfigFileType, ConfigModel, ConfigShowAst } from '../types'; -function writeQueryParam(value: string): string { - return lz.compressToEncodedURIComponent(value); +function writeQueryParam(value: string | null): string { + return (value && lz.compressToEncodedURIComponent(value)) ?? ''; } function readQueryParam(value: string | null, fallback: string): string { - return value - ? lz.decompressFromEncodedURIComponent(value) ?? fallback - : fallback; + return (value && lz.decompressFromEncodedURIComponent(value)) ?? fallback; } -function readShowAST(value: string | null): 'ts' | 'scope' | 'es' | boolean { +function readShowAST(value: string | null): ConfigShowAst { switch (value) { case 'es': - return 'es'; case 'ts': - return 'ts'; case 'scope': - return 'scope'; + return value; } - return Boolean(value); + return value ? 'es' : false; +} + +function readFileType(value: string | null): ConfigFileType { + if (value && (fileTypes as string[]).includes(value)) { + return value as ConfigFileType; + } + return '.ts'; +} + +function toJsonConfig(cfg: unknown, prop: string): string { + return toJson({ [prop]: cfg }); } function readLegacyParam( @@ -40,7 +48,7 @@ function readLegacyParam( return undefined; } -const parseStateFromUrl = (hash: string): ConfigModel | undefined => { +const parseStateFromUrl = (hash: string): Partial | undefined => { if (!hash) { return; } @@ -65,20 +73,22 @@ const parseStateFromUrl = (hash: string): ConfigModel | undefined => { ); } + const fileType = + searchParams.get('jsx') === 'true' + ? '.tsx' + : readFileType(searchParams.get('fileType')); + + const code = searchParams.has('code') + ? readQueryParam(searchParams.get('code'), '') + : ''; + return { - // @ts-expect-error: process.env.TS_VERSION - ts: (searchParams.get('ts') ?? process.env.TS_VERSION).trim(), - jsx: searchParams.has('jsx'), - showAST: - searchParams.has('showAST') && readShowAST(searchParams.get('showAST')), + ts: searchParams.get('ts') ?? process.env.TS_VERSION!, + showAST: readShowAST(searchParams.get('showAST')), sourceType: - searchParams.has('sourceType') && - searchParams.get('sourceType') === 'script' - ? 'script' - : 'module', - code: searchParams.has('code') - ? readQueryParam(searchParams.get('code'), '') - : '', + searchParams.get('sourceType') === 'script' ? 'script' : 'module', + code, + fileType, eslintrc: eslintrc ?? '', tsconfig: tsconfig ?? '', }; @@ -88,28 +98,27 @@ const parseStateFromUrl = (hash: string): ConfigModel | undefined => { return undefined; }; -const writeStateToUrl = (newState: ConfigModel): string => { +const writeStateToUrl = (newState: ConfigModel): string | undefined => { try { - return Object.entries({ - ts: newState.ts.trim(), - jsx: newState.jsx, - sourceType: newState.sourceType, - showAST: newState.showAST, - code: newState.code ? writeQueryParam(newState.code) : undefined, - eslintrc: newState.eslintrc - ? writeQueryParam(newState.eslintrc) - : undefined, - tsconfig: newState.tsconfig - ? writeQueryParam(newState.tsconfig) - : undefined, - }) - .filter(item => item[1]) - .map(item => `${encodeURIComponent(item[0])}=${item[1]}`) - .join('&'); + const searchParams = new URLSearchParams(); + searchParams.set('ts', newState.ts.trim()); + if (newState.sourceType === 'script') { + searchParams.set('sourceType', newState.sourceType); + } + if (newState.showAST) { + searchParams.set('showAST', newState.showAST); + } + if (newState.fileType) { + searchParams.set('fileType', newState.fileType); + } + searchParams.set('code', writeQueryParam(newState.code)); + searchParams.set('eslintrc', writeQueryParam(newState.eslintrc)); + searchParams.set('tsconfig', writeQueryParam(newState.tsconfig)); + return searchParams.toString(); } catch (e) { console.warn(e); } - return ''; + return undefined; }; const retrieveStateFromLocalStorage = (): Partial | undefined => { @@ -131,17 +140,15 @@ const retrieveStateFromLocalStorage = (): Partial | undefined => { state.ts = ts; } } - if (hasOwnProperty('jsx', config)) { - const jsx = config.jsx; - if (typeof jsx === 'boolean') { - state.jsx = jsx; + if (hasOwnProperty('fileType', config)) { + const fileType = config.fileType; + if (fileType === 'true') { + state.fileType = readFileType(fileType); } } if (hasOwnProperty('showAST', config)) { const showAST = config.showAST; - if (typeof showAST === 'boolean') { - state.showAST = showAST; - } else if (typeof showAST === 'string') { + if (typeof showAST === 'string') { state.showAST = readShowAST(showAST); } } @@ -156,7 +163,8 @@ const retrieveStateFromLocalStorage = (): Partial | undefined => { const writeStateToLocalStorage = (newState: ConfigModel): void => { const config: Partial = { ts: newState.ts, - jsx: newState.jsx, + fileType: newState.fileType, + sourceType: newState.sourceType, showAST: newState.showAST, }; window.localStorage.setItem('config', JSON.stringify(config)); @@ -165,66 +173,36 @@ const writeStateToLocalStorage = (newState: ConfigModel): void => { function useHashState( initialState: ConfigModel, ): [ConfigModel, (cfg: Partial) => void] { - const [hash, setHash] = useState(window.location.hash.slice(1)); + const history = useHistory(); const [state, setState] = useState(() => ({ ...initialState, ...retrieveStateFromLocalStorage(), ...parseStateFromUrl(window.location.hash.slice(1)), })); - const [tmpState, setTmpState] = useState>(() => ({ - ...initialState, - ...retrieveStateFromLocalStorage(), - ...parseStateFromUrl(window.location.hash.slice(1)), - })); - - useEffect(() => { - const newHash = window.location.hash.slice(1); - if (newHash !== hash) { - const newState = parseStateFromUrl(newHash); - if (newState) { - setState(newState); - setTmpState(newState); - } - } - }, [hash]); - - useEffect(() => { - const newState = { ...state, ...tmpState }; - if (!shallowEqual(newState, state)) { - writeStateToLocalStorage(newState); - const newHash = writeStateToUrl(newState); - setState(newState); - setHash(newHash); - - if (window.location.hash.slice(1) !== newHash) { - window.history.pushState( - undefined, - document.title, - `${window.location.pathname}#${newHash}`, - ); - } - } - }, [tmpState, state]); - - const onHashChange = (): void => { - const newHash = window.location.hash; - console.info('[State] hash change detected', newHash); - setHash(newHash); - }; - - useEffect(() => { - window.addEventListener('popstate', onHashChange); - return (): void => { - window.removeEventListener('popstate', onHashChange); - }; - }, []); - - const _setState = useCallback((cfg: Partial) => { - console.info('[State] updating config diff', cfg); - setTmpState(cfg); - }, []); - return [state, _setState]; + const updateState = useCallback( + (cfg: Partial) => { + console.info('[State] updating config diff', cfg); + setState(oldState => { + const newState = { ...oldState, ...cfg }; + + writeStateToLocalStorage(newState); + + history.replace({ + ...history.location, + hash: writeStateToUrl(newState), + }); + + if (cfg.ts) { + window.location.reload(); + } + return newState; + }); + }, + [setState, history], + ); + + return [state, updateState]; } export default useHashState; diff --git a/packages/website/src/components/linter/WebLinter.ts b/packages/website/src/components/linter/WebLinter.ts index e8a92a1003c3..2218460ba170 100644 --- a/packages/website/src/components/linter/WebLinter.ts +++ b/packages/website/src/components/linter/WebLinter.ts @@ -36,9 +36,10 @@ export class WebLinter { private compilerOptions: CompilerOptions; private readonly parserOptions: ParserOptions = { ecmaFeatures: { - jsx: false, + jsx: true, globalReturn: false, }, + comment: true, ecmaVersion: 'latest', project: ['./tsconfig.json'], sourceType: 'module', @@ -85,20 +86,24 @@ export class WebLinter { }; } - lint(code: string): TSESLint.Linter.LintMessage[] { - return this.linter.verify(code, this.eslintConfig); + lint(code: string, filename: string): TSESLint.Linter.LintMessage[] { + return this.linter.verify(code, this.eslintConfig, { + filename: filename, + }); } - fix(code: string): TSESLint.Linter.FixReport { - return this.linter.verifyAndFix(code, this.eslintConfig, { fix: true }); + fix(code: string, filename: string): TSESLint.Linter.FixReport { + return this.linter.verifyAndFix(code, this.eslintConfig, { + filename: filename, + fix: true, + }); } updateRules(rules: TSESLint.Linter.RulesRecord): void { this.rules = rules; } - updateParserOptions(jsx?: boolean, sourceType?: TSESLint.SourceType): void { - this.parserOptions.ecmaFeatures!.jsx = jsx ?? false; + updateParserOptions(sourceType?: TSESLint.SourceType): void { this.parserOptions.sourceType = sourceType ?? 'module'; } @@ -110,14 +115,13 @@ export class WebLinter { code: string, eslintOptions: ParserOptions = {}, ): TSESLint.Linter.ESLintParseResult { - const isJsx = eslintOptions?.ecmaFeatures?.jsx ?? false; - const fileName = isJsx ? '/demo.tsx' : '/demo.ts'; + const fileName = eslintOptions.filePath ?? '/input.ts'; this.storedAST = undefined; this.storedTsAST = undefined; this.storedScope = undefined; - this.host.writeFile(fileName, code, false); + this.host.writeFile(fileName, code || '\n', false); const program = window.ts.createProgram({ rootNames: [fileName], @@ -129,7 +133,7 @@ export class WebLinter { const { estree: ast, astMaps } = this.lintUtils.astConverter( tsAst, - { ...parseSettings, code, codeFullText: code, jsx: isJsx }, + { ...parseSettings, code, codeFullText: code }, true, ); diff --git a/packages/website/src/components/linter/config.ts b/packages/website/src/components/linter/config.ts index 93466f9451ab..5cf9b5cccd9c 100644 --- a/packages/website/src/components/linter/config.ts +++ b/packages/website/src/components/linter/config.ts @@ -13,7 +13,7 @@ export const parseSettings: ParseSettings = { EXPERIMENTAL_useSourceOfProjectReferenceRedirect: false, extraFileExtensions: [], filePath: '', - jsx: false, + jsx: true, loc: true, log: console.log, preserveNodeMaps: true, diff --git a/packages/website/src/components/types.ts b/packages/website/src/components/types.ts index 2e532e90a973..84156fe4924e 100644 --- a/packages/website/src/components/types.ts +++ b/packages/website/src/components/types.ts @@ -1,4 +1,5 @@ import type { TSESLint } from '@typescript-eslint/utils'; +import type * as ts from 'typescript'; export type CompilerFlags = Record; @@ -14,14 +15,18 @@ export interface RuleDetails { export type TabType = 'code' | 'tsconfig' | 'eslintrc'; +export type ConfigFileType = `${ts.Extension}`; + +export type ConfigShowAst = false | 'es' | 'ts' | 'scope'; + export interface ConfigModel { - jsx?: boolean; + fileType?: ConfigFileType; sourceType?: SourceType; eslintrc: string; tsconfig: string; code: string; ts: string; - showAST?: boolean | 'ts' | 'es' | 'scope'; + showAST?: ConfigShowAst; } export type SelectedRange = [number, number];