Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update website to use function components everywhere #814

Merged
merged 1 commit into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion website/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
module.exports = {
extends: ["plugin:react-hooks/recommended"],
parserOptions: {
project: `${__dirname}/tsconfig.json`,
},
}
rules: {
"react-hooks/exhaustive-deps": "error",
},
};
7 changes: 5 additions & 2 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
"monaco-editor": "^0.33.0",
"monaco-editor-webpack-plugin": "^7.0.1",
"process": "^0.11.10",
"react": "^16.11.0",
"react": "^18.2.0",
"react-dev-utils": "^12.0.0-next.47",
"react-dom": "^16.11.0",
"react-dom": "^18.2.0",
"react-hot-loader": "^4.12.16",
"react-monaco-editor": "^0.48.0",
"react-virtualized-auto-sizer": "^1.0.6",
Expand Down Expand Up @@ -78,5 +78,8 @@
"jsx",
"node"
]
},
"devDependencies": {
"eslint-plugin-react-hooks": "^4.6.0"
}
}
322 changes: 145 additions & 177 deletions website/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {css, StyleSheet} from "aphrodite";
import {Component} from "react";
import {useEffect, useRef, useState} from "react";
import {hot} from "react-hot-loader/root";

import CompareOptionsBox from "./CompareOptionsBox";
Expand All @@ -9,211 +9,179 @@ import {
DEFAULT_COMPARE_OPTIONS,
DEFAULT_OPTIONS,
type CompareOptions,
type HydratedOptions,
INITIAL_CODE,
} from "./Constants";
import DebugOptionsBox from "./DebugOptionsBox";
import EditorWrapper from "./EditorWrapper";
import SucraseOptionsBox from "./SucraseOptionsBox";
import {loadHashState, saveHashState} from "./URLHashState";
import {type BaseHashState, loadHashState, saveHashState} from "./URLHashState";
import * as WorkerClient from "./WorkerClient";
import {type StateUpdate} from "./WorkerClient";

interface State {
code: string;
sucraseOptions: HydratedOptions;
compareOptions: CompareOptions;
debugOptions: DebugOptions;
sucraseCode: string;
sucraseTimeMs: number | null | "LOADING";
babelCode: string;
babelTimeMs: number | null | "LOADING";
typeScriptCode: string;
typeScriptTimeMs: number | null | "LOADING";
tokensStr: string;
sourceMapStr: string;
showMore: boolean;
babelLoaded: boolean;
typeScriptLoaded: boolean;
}

class App extends Component<unknown, State> {
constructor(props: unknown) {
super(props);
this.state = {
code: INITIAL_CODE,
sucraseOptions: DEFAULT_OPTIONS,
compareOptions: DEFAULT_COMPARE_OPTIONS,
debugOptions: DEFAULT_DEBUG_OPTIONS,
sucraseCode: "",
sucraseTimeMs: null,
babelCode: "",
babelTimeMs: null,
typeScriptCode: "",
typeScriptTimeMs: null,
tokensStr: "",
sourceMapStr: "",
showMore: false,
babelLoaded: false,
typeScriptLoaded: false,
};
const hashState = loadHashState();
if (hashState) {
this.state = {...this.state, ...hashState};
function App(): JSX.Element {
const cachedHashState = useRef<BaseHashState | null | "NOT_LOADED">("NOT_LOADED");
function hashState(): BaseHashState | null {
if (cachedHashState.current === "NOT_LOADED") {
cachedHashState.current = loadHashState();
}
return cachedHashState.current;
}

componentDidMount(): void {
WorkerClient.subscribe({
const [code, setCode] = useState(hashState()?.code ?? INITIAL_CODE);
const [sucraseOptions, setSucraseOptions] = useState(
hashState()?.sucraseOptions ?? DEFAULT_OPTIONS,
);
const [compareOptions, setCompareOptions] = useState(
hashState()?.compareOptions ?? DEFAULT_COMPARE_OPTIONS,
);
const [debugOptions, setDebugOptions] = useState(
hashState()?.debugOptions ?? DEFAULT_DEBUG_OPTIONS,
);
const [sucraseCode, setSucraseCode] = useState("");
const [sucraseTimeMs, setSucraseTimeMs] = useState<number | null | "LOADING">(null);
const [babelCode, setBabelCode] = useState("");
const [babelTimeMs, setBabelTimeMs] = useState<number | null | "LOADING">(null);
const [typeScriptCode, setTypeScriptCode] = useState("");
const [typeScriptTimeMs, setTypeScriptTimeMs] = useState<number | null | "LOADING">(null);
const [tokensStr, setTokensStr] = useState("");
const [sourceMapStr, setSourceMapStr] = useState("");
const [babelLoaded, setBabelLoaded] = useState(false);
const [typeScriptLoaded, setTypeScriptLoaded] = useState(false);

useEffect(() => {
WorkerClient.updateHandlers({
updateState: (stateUpdate) => {
this.setState((state) => ({...state, ...stateUpdate}));
const setters: {
[k in keyof StateUpdate]-?: (newValue: Exclude<StateUpdate[k], undefined>) => void;
} = {
sucraseCode: setSucraseCode,
babelCode: setBabelCode,
typeScriptCode: setTypeScriptCode,
tokensStr: setTokensStr,
sourceMapStr: setSourceMapStr,
sucraseTimeMs: setSucraseTimeMs,
babelTimeMs: setBabelTimeMs,
typeScriptTimeMs: setTypeScriptTimeMs,
babelLoaded: setBabelLoaded,
typeScriptLoaded: setTypeScriptLoaded,
};
// The above mapping ensures we list all properties in StateUpdate with the right types.
// Use escape hatches for actually setting the properties.
for (const [key, setter] of Object.entries(setters)) {
if (stateUpdate[key as keyof StateUpdate] !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(setter as any)(stateUpdate[key as keyof StateUpdate]);
}
}
},
handleCompressedCode: (compressedCode) => {
saveHashState({
code: this.state.code,
code,
compressedCode,
sucraseOptions: this.state.sucraseOptions,
compareOptions: this.state.compareOptions,
debugOptions: this.state.debugOptions,
sucraseOptions,
compareOptions,
debugOptions,
});
},
});
this.postConfigToWorker();
}

componentDidUpdate(prevProps: unknown, prevState: State): void {
if (
this.state.code !== prevState.code ||
this.state.sucraseOptions !== prevState.sucraseOptions ||
this.state.compareOptions !== prevState.compareOptions ||
this.state.debugOptions !== prevState.debugOptions ||
this.state.babelLoaded !== prevState.babelLoaded ||
this.state.typeScriptLoaded !== prevState.typeScriptLoaded
) {
this.postConfigToWorker();
}
}
}, [code, sucraseOptions, compareOptions, debugOptions]);

postConfigToWorker(): void {
this.setState({sucraseTimeMs: "LOADING", babelTimeMs: "LOADING", typeScriptTimeMs: "LOADING"});
WorkerClient.updateConfig({
code: this.state.code,
sucraseOptions: this.state.sucraseOptions,
compareOptions: this.state.compareOptions,
debugOptions: this.state.debugOptions,
});
}
// On any change to code, config, or loading state, kick off a worker task to re-calculate.
useEffect(() => {
setSucraseTimeMs("LOADING");
setBabelTimeMs("LOADING");
setTypeScriptTimeMs("LOADING");
WorkerClient.updateConfig({code, sucraseOptions, compareOptions, debugOptions});
}, [code, sucraseOptions, compareOptions, debugOptions, babelLoaded, typeScriptLoaded]);

_handleCodeChange = (newCode: string): void => {
this.setState({
code: newCode,
});
};
return (
<div className={css(styles.app)}>
<span className={css(styles.title)}>Sucrase</span>
<span className={css(styles.subtitle)}>
<span>Super-fast Babel alternative</span>
{" | "}
<a className={css(styles.link)} href="https://github.com/alangpierce/sucrase">
GitHub
</a>
</span>
<div className={css(styles.options)}>
<SucraseOptionsBox
options={sucraseOptions}
onUpdateOptions={(newSucraseOptions) => {
setSucraseOptions(newSucraseOptions);
}}
/>
<CompareOptionsBox
compareOptions={compareOptions}
onUpdateCompareOptions={(newCompareOptions: CompareOptions) => {
setCompareOptions(newCompareOptions);
}}
/>
<DebugOptionsBox
debugOptions={debugOptions}
onUpdateDebugOptions={(newDebugOptions: DebugOptions) => {
setDebugOptions(newDebugOptions);
}}
/>
</div>

render(): JSX.Element {
const {
sucraseCode,
sucraseTimeMs,
babelCode,
babelTimeMs,
typeScriptCode,
typeScriptTimeMs,
tokensStr,
sourceMapStr,
} = this.state;
return (
<div className={css(styles.app)}>
<span className={css(styles.title)}>Sucrase</span>
<span className={css(styles.subtitle)}>
<span>Super-fast Babel alternative</span>
{" | "}
<a className={css(styles.link)} href="https://github.com/alangpierce/sucrase">
GitHub
</a>
</span>
<div className={css(styles.options)}>
<SucraseOptionsBox
options={this.state.sucraseOptions}
onUpdateOptions={(sucraseOptions) => {
this.setState({sucraseOptions});
}}
/>
<CompareOptionsBox
compareOptions={this.state.compareOptions}
onUpdateCompareOptions={(compareOptions: CompareOptions) => {
this.setState({compareOptions});
}}
<div className={css(styles.editors)}>
<EditorWrapper label="Your code" code={code} onChange={setCode} babelLoaded={babelLoaded} />
<EditorWrapper
label="Transformed with Sucrase"
code={sucraseCode}
timeMs={sucraseTimeMs}
isReadOnly={true}
babelLoaded={babelLoaded}
/>
{compareOptions.compareWithBabel && (
<EditorWrapper
label="Transformed with Babel"
code={babelCode}
timeMs={babelTimeMs}
isReadOnly={true}
babelLoaded={babelLoaded}
/>
<DebugOptionsBox
debugOptions={this.state.debugOptions}
onUpdateDebugOptions={(debugOptions: DebugOptions) => {
this.setState({debugOptions});
}}
)}
{compareOptions.compareWithTypeScript && (
<EditorWrapper
label="Transformed with TypeScript"
code={typeScriptCode}
timeMs={typeScriptTimeMs}
isReadOnly={true}
babelLoaded={babelLoaded}
/>
</div>

<div className={css(styles.editors)}>
)}
{debugOptions.showTokens && (
<EditorWrapper
label="Your code"
code={this.state.code}
onChange={this._handleCodeChange}
babelLoaded={this.state.babelLoaded}
label="Tokens"
code={tokensStr}
isReadOnly={true}
isPlaintext={true}
options={{
lineNumbers: (n) => (n > 1 ? String(n - 2) : ""),
}}
babelLoaded={babelLoaded}
/>
)}
{debugOptions.showSourceMap && (
<EditorWrapper
label="Transformed with Sucrase"
code={sucraseCode}
timeMs={sucraseTimeMs}
label="Source Map"
code={sourceMapStr}
isReadOnly={true}
babelLoaded={this.state.babelLoaded}
isPlaintext={true}
babelLoaded={babelLoaded}
/>
{this.state.compareOptions.compareWithBabel && (
<EditorWrapper
label="Transformed with Babel"
code={babelCode}
timeMs={babelTimeMs}
isReadOnly={true}
babelLoaded={this.state.babelLoaded}
/>
)}
{this.state.compareOptions.compareWithTypeScript && (
<EditorWrapper
label="Transformed with TypeScript"
code={typeScriptCode}
timeMs={typeScriptTimeMs}
isReadOnly={true}
babelLoaded={this.state.babelLoaded}
/>
)}
{this.state.debugOptions.showTokens && (
<EditorWrapper
label="Tokens"
code={tokensStr}
isReadOnly={true}
isPlaintext={true}
options={{
lineNumbers: (n) => (n > 1 ? String(n - 2) : ""),
}}
babelLoaded={this.state.babelLoaded}
/>
)}
{this.state.debugOptions.showSourceMap && (
<EditorWrapper
label="Source Map"
code={sourceMapStr}
isReadOnly={true}
isPlaintext={true}
babelLoaded={this.state.babelLoaded}
/>
)}
</div>
<span className={css(styles.footer)}>
<a className={css(styles.link)} href="https://www.npmjs.com/package/sucrase">
sucrase
</a>{" "}
{process.env.SUCRASE_VERSION}
</span>
)}
</div>
);
}
<span className={css(styles.footer)}>
<a className={css(styles.link)} href="https://www.npmjs.com/package/sucrase">
sucrase
</a>{" "}
{process.env.SUCRASE_VERSION}
</span>
</div>
);
}

export default hot(App);
Expand Down