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];