From 5cdebdd86a05422feb23e43e7e4ec1450d902e5d Mon Sep 17 00:00:00 2001 From: Felix Rieseberg Date: Sat, 9 Jun 2018 17:19:32 +0200 Subject: [PATCH] :guardsman: Cleanup code, document it --- package-lock.json | 5 - src/interfaces.ts | 7 ++ src/main.ts | 3 +- src/menu.ts | 3 + src/renderer/app.tsx | 116 +++++++++----------- src/renderer/binary.ts | 60 ++++++++-- src/renderer/components/address-bar.tsx | 70 +++++++++--- src/renderer/components/commands.tsx | 24 ++-- src/renderer/components/editor.tsx | 2 +- src/renderer/components/editors.tsx | 7 +- src/renderer/components/header.tsx | 8 +- src/renderer/components/output.tsx | 19 +++- src/renderer/components/publish-button.tsx | 15 ++- src/renderer/components/runner.tsx | 66 ++++++++--- src/renderer/components/token-dialog.tsx | 2 +- src/renderer/components/version-chooser.tsx | 63 ++++------- src/renderer/content.ts | 20 +++- src/renderer/fetch-types.ts | 22 +++- src/renderer/npm.ts | 15 +++ src/renderer/state.ts | 100 +++++++++++++++++ src/renderer/themes.ts | 3 + src/renderer/versions.ts | 16 +++ src/utils/array-to-stringmap.ts | 7 ++ src/utils/editor-layout.ts | 4 + src/utils/normalize-version.ts | 8 +- src/utils/update-versions.ts | 15 --- 26 files changed, 472 insertions(+), 208 deletions(-) create mode 100644 src/renderer/state.ts delete mode 100644 src/utils/update-versions.ts diff --git a/package-lock.json b/package-lock.json index dd1d836923..143a0d52e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1377,11 +1377,6 @@ "assert-plus": "1.0.0" } }, - "debounce": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.1.0.tgz", - "integrity": "sha512-ZQVKfRVlwRfD150ndzEK8M90ABT+Y/JQKs4Y7U4MXdpuoUkkrr4DwKbVux3YjylA5bUMUj0Nc3pMxPJX6N2QQQ==" - }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", diff --git a/src/interfaces.ts b/src/interfaces.ts index c3dc06435b..55796321e2 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -19,6 +19,13 @@ export interface GitHubVersion { body: string; } +export interface EditorValues { + main: string; + renderer: string; + html: string; + package?: string; +} + export interface ElectronVersion extends GitHubVersion { state: ElectronVersionState; } diff --git a/src/main.ts b/src/main.ts index fa84b117ec..b31ab4dbf7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,8 +2,6 @@ import { app, BrowserWindow } from 'electron'; import { setupMenu } from './menu'; -require('update-electron-app')(); - // Handle creating/removing shortcuts on Windows when installing/uninstalling. if (require('electron-squirrel-startup')) { // eslint-disable-line global-require app.quit(); @@ -11,6 +9,7 @@ if (require('electron-squirrel-startup')) { // eslint-disable-line global-requir app.setName('Electron Fiddle'); + // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. let mainWindow; diff --git a/src/menu.ts b/src/menu.ts index babca93526..69d273df1f 100644 --- a/src/menu.ts +++ b/src/menu.ts @@ -1,6 +1,9 @@ import { app, shell, Menu } from 'electron'; import * as defaultMenu from 'electron-default-menu'; +/** + * Creates the app's window menu. + */ export function setupMenu() { // Get template for default menu const menu = defaultMenu(app, shell); diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index b843b37d37..f459f99d95 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -1,104 +1,81 @@ import * as React from 'react'; import { render } from 'react-dom'; import * as loader from 'monaco-loader'; -import { observable } from 'mobx'; import * as MonacoType from 'monaco-editor'; import { mainTheme } from './themes'; import { Header } from './components/header'; -import { BinaryManager } from './binary'; -import { ElectronVersion, StringMap, OutputEntry } from '../interfaces'; -import { arrayToStringMap } from '../utils/array-to-stringmap'; -import { getKnownVersions } from './versions'; -import { normalizeVersion } from '../utils/normalize-version'; +import { EditorValues } from '../interfaces'; import { editors } from './components/editors'; import { updateEditorLayout } from '../utils/editor-layout'; - -const knownVersions = getKnownVersions(); -const defaultVersion = normalizeVersion(knownVersions[0].tag_name); - -window.ElectronFiddle = { - editors: { - main: null, - renderer: null, - html: null - }, - app: null -}; - -export class AppState { - @observable public gistId: string = ''; - @observable public version: string = defaultVersion; - @observable public githubToken: string | null = null; - @observable public binaryManager: BinaryManager = new BinaryManager(defaultVersion); - @observable public versions: StringMap = arrayToStringMap(knownVersions); - @observable public output: Array = []; - @observable public isConsoleShowing: boolean = false; - @observable public isTokenDialogShowing: boolean = false; - @observable public isUnsaved: boolean = true; - @observable public isMyGist: boolean = false; -} - -const appState = new AppState(); -appState.githubToken = localStorage.getItem('githubToken'); - +import { appState } from './state'; + +/** + * The top-level class controlling the whole app. This is *not* a React component, + * but it does eventually render all components. + * + * @class App + */ class App { - public editors: { - main: MonacoType.editor.IStandaloneCodeEditor | null, - renderer: MonacoType.editor.IStandaloneCodeEditor | null, - html: MonacoType.editor.IStandaloneCodeEditor | null, - } = { - main: null, - renderer: null, - html: null - }; public monaco: typeof MonacoType | null = null; public name = 'test'; public typeDefDisposable = null; - public isScrollbarHidden = false; constructor() { this.getValues = this.getValues.bind(this); - this.setup(); } - public setValues(values: { - html: string; - main: string; - renderer: string; - }) { + /** + * Sets the values on all three editors. + * + * @param {EditorValues} values + */ + public setValues(values: EditorValues): void { const { ElectronFiddle: fiddle } = window; - if (!fiddle.editors.html || !fiddle.editors.main || !fiddle.editors.renderer) { - throw new Error('Editors not ready'); + if (!fiddle) { + throw new Error('Fiddle not ready'); } - fiddle.editors.html.setValue(values.html); - fiddle.editors.main.setValue(values.main); - fiddle.editors.renderer.setValue(values.renderer); + const { main, html, renderer } = fiddle.editors; + + if (html && html.setValue) html.setValue(values.html); + if (main && main.setValue) main.setValue(values.main); + if (renderer && renderer.setValue) renderer.setValue(values.renderer); } - public getValues() { + /** + * Gets the values on all three editors. + * + * @returns {EditorValues} + */ + public getValues(): EditorValues { const { ElectronFiddle: fiddle } = window; - if (!fiddle.editors.html || !fiddle.editors.main || !fiddle.editors.renderer) { - throw new Error('Editors not ready'); + if (!fiddle) { + throw new Error('Fiddle not ready'); } + const { main, html, renderer } = fiddle.editors; + return { - html: fiddle.editors.html.getValue(), - main: fiddle.editors.main.getValue(), - renderer: fiddle.editors.renderer.getValue(), + html: html && html.getValue() ? html.getValue() : '', + main: main && main.getValue() ? main.getValue() : '', + renderer: renderer && renderer.getValue() ? renderer.getValue() : '', package: JSON.stringify({ name: this.name, main: './main.js', - version: '1.0.0' + version: '1.0.0', }) }; } - public async setup() { + /** + * Initial setup call, loading Monaco and kicking off the React + * render process. + */ + public async setup(): Promise { this.monaco = await loader(); this.createThemes(); @@ -114,11 +91,18 @@ class App { this.setupResizeListener(); } - public setupResizeListener() { + /** + * We need to possibly recalculate the layout whenever the window + * is resized. This method sets up the listener. + */ + public setupResizeListener(): void { window.addEventListener('resize', updateEditorLayout); } - public createThemes() { + /** + * We have a custom theme for the Monaco editor. This sets that up. + */ + public createThemes(): void { if (!this.monaco) return; this.monaco.editor.defineTheme('main', mainTheme as any); } diff --git a/src/renderer/binary.ts b/src/renderer/binary.ts index aa3415e923..fd0456f869 100644 --- a/src/renderer/binary.ts +++ b/src/renderer/binary.ts @@ -3,7 +3,6 @@ import * as os from 'os'; import * as fs from 'fs-extra'; import * as path from 'path'; import * as extract from 'extract-zip'; -import { EventEmitter } from 'events'; import { USER_DATA_PATH } from './constants'; import { normalizeVersion } from '../utils/normalize-version'; @@ -11,16 +10,27 @@ import { StringMap } from '../interfaces'; const eDownload = promisify(require('electron-download')); -export class BinaryManager extends EventEmitter { +/** + * The binary manager takes care of downloading Electron versions + * + * @export + * @class BinaryManager + */ +export class BinaryManager { public state: StringMap<'ready' | 'downloading'> = {}; constructor(version: string) { - super(); - this.setup(version); } - public async setup(iVersion: string) { + /** + * General setup, called with a version. Is called during construction + * to ensure that we always have or download at least one version. + * + * @param {string} iVersion + * @returns {Promise} + */ + public async setup(iVersion: string): Promise { const version = normalizeVersion(iVersion); await fs.mkdirp(this.getDownloadPath(version)); @@ -47,7 +57,13 @@ export class BinaryManager extends EventEmitter { this.state[version] = 'ready'; } - public getElectronBinary(version: string) { + /** + * Gets the expected path for the binary of a given Electron version + * + * @param {string} version + * @returns {string} + */ + public getElectronBinaryPath(version: string): string { const platform = os.platform(); const dir = this.getDownloadPath(version); @@ -64,6 +80,11 @@ export class BinaryManager extends EventEmitter { } } + /** + * Returns an array of all versions downloaded to disk + * + * @returns {Promise>} + */ public async getDownloadedVersions(): Promise> { const downloadPath = path.join(USER_DATA_PATH, 'electron-bin'); console.log(`BinaryManager: Checking for downloaded versions`); @@ -85,16 +106,35 @@ export class BinaryManager extends EventEmitter { } } - public getIsDownloaded(version: string) { - const expectedPath = this.getElectronBinary(version); + /** + * Did we already download a given version? + * + * @param {string} version + * @returns {boolean} + */ + public getIsDownloaded(version: string): boolean { + const expectedPath = this.getElectronBinaryPath(version); return fs.existsSync(expectedPath); } - private getDownloadPath(version: string) { + /** + * Gets the expected path for a given Electron version + * + * @param {string} version + * @returns {string} + */ + private getDownloadPath(version: string): string { return path.join(USER_DATA_PATH, 'electron-bin', version); } - private unzip(zipPath: string, extractPath: string) { + /** + * Unzips an electron package so that we can actaully use it. + * + * @param {string} zipPath + * @param {string} extractPath + * @returns {Promise} + */ + private unzip(zipPath: string, extractPath: string): Promise { return new Promise((resolve, reject) => { process.noAsar = true; diff --git a/src/renderer/components/address-bar.tsx b/src/renderer/components/address-bar.tsx index 55a72f40ec..159a344e11 100644 --- a/src/renderer/components/address-bar.tsx +++ b/src/renderer/components/address-bar.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react'; import * as Octokit from '@octokit/rest'; import * as classNames from 'classnames'; -import { AppState } from '../app'; +import { AppState } from '../state'; import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../constants'; import { idFromUrl } from '../../utils/gist'; @@ -11,46 +11,82 @@ export interface AddressBarProps { appState: AppState; } +export interface AddressBarState { + value: string; +} + @observer -export class AddressBar extends React.Component { +export class AddressBar extends React.Component { constructor(props: AddressBarProps) { super(props); this.loadFiddle = this.loadFiddle.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleChange = this.handleChange.bind(this); + + this.state = { + value: this.props.appState.gistId + }; } + /** + * Handle the form's submit event, trying to load whatever + * URL was entered. + * + * @param {React.SyntheticEvent} event + * @memberof AddressBar + */ public async handleSubmit(event: React.SyntheticEvent) { event.preventDefault(); - this.loadFiddle(); + + this.props.appState.gistId = idFromUrl(this.state.value) || this.state.value; + if (this.state.value) { + this.loadFiddle(); + } } + /** + * Handle the change event, which usually just updates the address bar's value + * + * @param {React.ChangeEvent} event + * @memberof AddressBar + */ public async handleChange(event: React.ChangeEvent) { - this.props.appState.gistId = idFromUrl(event.target.value) || event.target.value; + this.setState({ value: idFromUrl(event.target.value) || event.target.value }); } + /** + * Load a fiddle + * + * @returns + * @memberof AddressBar + */ public async loadFiddle() { const { gistId } = this.props.appState; if (!confirm('Are you sure you want to load a new fiddle, all current progress will be lost?')) return; - const octo = new Octokit(); - const gist = await octo.gists.get({ - gist_id: gistId, - id: gistId, - }); + try { + const octo = new Octokit(); + const gist = await octo.gists.get({ + gist_id: gistId, + id: gistId, + }); - window.ElectronFiddle.app.setValues({ - html: gist.data.files[INDEX_HTML_NAME].content, - main: gist.data.files[MAIN_JS_NAME].content, - renderer: gist.data.files[RENDERER_JS_NAME].content, - }); + window.ElectronFiddle.app.setValues({ + html: gist.data.files[INDEX_HTML_NAME].content, + main: gist.data.files[MAIN_JS_NAME].content, + renderer: gist.data.files[RENDERER_JS_NAME].content, + }); + } catch (error) { + console.warn(`Loading fiddle failed`, error); + } } public render() { - const { gistId, isUnsaved } = this.props.appState; - const className = classNames('address-bar', isUnsaved, { empty: !gistId }); + const { isUnsaved } = this.props.appState; + const { value } = this.state; + const className = classNames('address-bar', isUnsaved, { empty: !value }); return (
@@ -59,7 +95,7 @@ export class AddressBar extends React.Component { key='addressbar' type='text' placeholder='...' - value={gistId} + value={value} onChange={this.handleChange} /> diff --git a/src/renderer/components/commands.tsx b/src/renderer/components/commands.tsx index 47680f2bb3..5c18758d1d 100644 --- a/src/renderer/components/commands.tsx +++ b/src/renderer/components/commands.tsx @@ -5,7 +5,7 @@ import { faTerminal, faUser } from '@fortawesome/fontawesome-free-solid'; import { Runner } from './runner'; import { VersionChooser } from './version-chooser'; -import { AppState } from '../app'; +import { AppState } from '../state'; import { AddressBar } from './address-bar'; import { PublishButton } from './publish-button'; @@ -13,27 +13,23 @@ export interface CommandsProps { appState: AppState; } +/** + * The command bar, containing all the buttons doing + * all the things + * + * @class Commands + * @extends {React.Component} + */ @observer export class Commands extends React.Component { constructor(props: CommandsProps) { super(props); - - this.toggleConsole = this.toggleConsole.bind(this); - this.showAuthDialog = this.showAuthDialog.bind(this); - } - - public toggleConsole() { - this.props.appState.isConsoleShowing = !this.props.appState.isConsoleShowing; - } - - public showAuthDialog() { - this.props.appState.isTokenDialogShowing = true; } public render() { const { appState } = this.props; const authButton = !appState.githubToken ? ( - ) : null; @@ -48,7 +44,7 @@ export class Commands extends React.Component { {authButton} - diff --git a/src/renderer/components/editor.tsx b/src/renderer/components/editor.tsx index c3b5cf309b..923a565a35 100644 --- a/src/renderer/components/editor.tsx +++ b/src/renderer/components/editor.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import * as MonacoType from 'monaco-editor'; -import { AppState } from '../app'; +import { AppState } from '../state'; import { getContent } from '../content'; export interface EditorProps { diff --git a/src/renderer/components/editors.tsx b/src/renderer/components/editors.tsx index 62641b4885..3873b658a3 100644 --- a/src/renderer/components/editors.tsx +++ b/src/renderer/components/editors.tsx @@ -2,7 +2,7 @@ import { Mosaic, MosaicWindow } from 'react-mosaic-component'; import * as React from 'react'; import * as MonacoType from 'monaco-editor'; -import { AppState } from '../app'; +import { AppState } from '../state'; import { Editor } from './editor'; import { EditorId } from '../../interfaces'; import { updateEditorLayout } from '../../utils/editor-layout'; @@ -31,6 +31,11 @@ export interface EditorsProps { monaco: typeof MonacoType; } +/** + * This function returns the editors embedded in a window manager. + * + * @param {EditorsProps} props + */ export const editors = (props: EditorsProps) => ( } + */ export class Header extends React.Component { public render() { return ( diff --git a/src/renderer/components/output.tsx b/src/renderer/components/output.tsx index b7b94c280a..0ac7649631 100644 --- a/src/renderer/components/output.tsx +++ b/src/renderer/components/output.tsx @@ -1,20 +1,35 @@ import * as React from 'react'; import { observer } from 'mobx-react'; -import { AppState } from '../app'; +import { AppState } from '../state'; import { OutputEntry } from '../../interfaces'; export interface CommandsProps { appState: AppState; } +/** + * This component represents the "console" that is shown + * whenever a fiddle is launched in Electron. + * + * @class Output + * @extends {React.Component} + */ @observer export class Output extends React.Component { constructor(props: CommandsProps) { super(props); } - public renderEntry(entry: OutputEntry) { + /** + * An individial entry might span multiple lines. To ensure that + * each line has a timestamp, this method might split up entries. + * + * @param {OutputEntry} entry + * @returns {Array} + * @memberof Output + */ + public renderEntry(entry: OutputEntry): Array { const ts = new Date(entry.timestamp).toLocaleTimeString(); const timestamp = {ts}; const lines = entry.text.split(/\r?\n/); diff --git a/src/renderer/components/publish-button.tsx b/src/renderer/components/publish-button.tsx index d8241fae6c..f6e8920e7b 100644 --- a/src/renderer/components/publish-button.tsx +++ b/src/renderer/components/publish-button.tsx @@ -5,7 +5,7 @@ import * as Icon from '@fortawesome/react-fontawesome'; import { faUpload, faSpinner } from '@fortawesome/fontawesome-free-solid'; import * as classNames from 'classnames'; -import { AppState } from '../app'; +import { AppState } from '../state'; import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../constants'; export interface PublishButtonProps { @@ -16,6 +16,13 @@ export interface PublishButtonState { isPublishing: boolean; } +/** + * The "publish" button takes care of logging you in. + * + * @export + * @class PublishButton + * @extends {React.Component} + */ @observer export class PublishButton extends React.Component { constructor(props: PublishButtonProps) { @@ -25,7 +32,11 @@ export class PublishButton extends React.Component { this.setState({ isPublishing: true }); const octo = new Octokit(); diff --git a/src/renderer/components/runner.tsx b/src/renderer/components/runner.tsx index 91eb958e78..842ff0b279 100644 --- a/src/renderer/components/runner.tsx +++ b/src/renderer/components/runner.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; -import * as tmp from 'tmp'; import * as fs from 'fs-extra'; import * as path from 'path'; import { observer } from 'mobx-react'; import { spawn, ChildProcess } from 'child_process'; import { normalizeVersion } from '../../utils/normalize-version'; -import { AppState } from '../app'; +import { AppState } from '../state'; import { findModules, installModules } from '../npm'; +import { EditorValues } from '../../interfaces'; export interface RunnerState { isRunning: boolean; @@ -17,6 +17,13 @@ export interface RunnerProps { appState: AppState; } +/** + * The runner component is responsible for actually launching the fiddle + * with Electron. It also renders the button that does so. + * + * @class Runner + * @extends {React.Component} + */ @observer export class Runner extends React.Component { public child: ChildProcess | null = null; @@ -56,6 +63,9 @@ export class Runner extends React.Component { return ; } + /** + * Stop a currently running Electron fiddle. + */ public async stop() { if (this.child) { this.child.kill(); @@ -65,6 +75,13 @@ export class Runner extends React.Component { } } + /** + * Push output to the application's state. Accepts a buffer or a string as input, + * attaches a timestamp, and pushes into the store. + * + * @param {(string | Buffer)} data + * @returns + */ public pushData(data: string | Buffer) { const strData = data.toString(); if (strData.startsWith('Debugger listening on ws://')) return; @@ -75,7 +92,15 @@ export class Runner extends React.Component { }); } - public async installModules(values: any, dir: string) { + /** + * Analyzes the editor's JavaScript contents for modules + * and installs them. + * + * @param {EditorValues} values + * @param {string} dir + * @returns {Promise} + */ + public async installModules(values: EditorValues, dir: string): Promise { const files = [ values.main, values.renderer ]; const modules: Array = []; @@ -89,40 +114,45 @@ export class Runner extends React.Component { this.pushData(await installModules({ dir }, ...modules)); } - public async run() { + /** + * Actually run the fiddle. + * + * @returns + * @memberof Runner + */ + public async run(): Promise { const values = window.ElectronFiddle.app.getValues(); - const tmpdir = (tmp as any).dirSync(); - const { binaryManager, version } = this.props.appState; + const { binaryManager, version, tmpDir } = this.props.appState; this.props.appState.isConsoleShowing = true; try { - await fs.writeFile(path.join(tmpdir.name, 'index.html'), values.html); - await fs.writeFile(path.join(tmpdir.name, 'main.js'), values.main); - await fs.writeFile(path.join(tmpdir.name, 'renderer.js'), values.renderer); - await fs.writeFile(path.join(tmpdir.name, 'package.json'), values.package); - await this.installModules(values, tmpdir.name); + await fs.writeFile(path.join(tmpDir.name, 'index.html'), values.html); + await fs.writeFile(path.join(tmpDir.name, 'main.js'), values.main); + await fs.writeFile(path.join(tmpDir.name, 'renderer.js'), values.renderer); + await fs.writeFile(path.join(tmpDir.name, 'package.json'), values.package); + await this.installModules(values, tmpDir.name); } catch (error) { - console.error('Could not write files', error); + console.error('Runner: Could not write files', error); } if (!binaryManager.getIsDownloaded(version)) { - console.warn(`Binary ${version} not ready`); + console.warn(`Runner: Binary ${version} not ready`); return; } - const binaryPath = this.props.appState.binaryManager.getElectronBinary(version); - console.log(`Binary ${binaryPath} ready, launching`); + const binaryPath = this.props.appState.binaryManager.getElectronBinaryPath(version); + console.log(`Runner: Binary ${binaryPath} ready, launching`); - this.child = spawn(binaryPath, [ tmpdir.name, '--inspect' ]); + this.child = spawn(binaryPath, [ tmpDir.name, '--inspect' ]); this.setState({ isRunning: true }); - this.pushData('Electron started.'); + this.pushData(`Electron v${version} started.`); this.child.stdout.on('data', this.pushData); this.child.stderr.on('data', this.pushData); this.child.on('close', (code) => { this.pushData(`Electron exited with code ${code.toString()}.`); this.setState({ isRunning: false }); - tmpdir.removeCallback(); + this.child = null; }); } } diff --git a/src/renderer/components/token-dialog.tsx b/src/renderer/components/token-dialog.tsx index 99aeac25f3..209d471a63 100644 --- a/src/renderer/components/token-dialog.tsx +++ b/src/renderer/components/token-dialog.tsx @@ -5,7 +5,7 @@ import * as Icon from '@fortawesome/react-fontawesome'; import { faKey, faSpinner } from '@fortawesome/fontawesome-free-solid'; import * as Octokit from '@octokit/rest'; -import { AppState } from '../app'; +import { AppState } from '../state'; export interface TokenDialogProps { appState: AppState; diff --git a/src/renderer/components/version-chooser.tsx b/src/renderer/components/version-chooser.tsx index 7e533902ad..314817fc36 100644 --- a/src/renderer/components/version-chooser.tsx +++ b/src/renderer/components/version-chooser.tsx @@ -2,9 +2,7 @@ import * as React from 'react'; import * as semver from 'semver'; import { observer } from 'mobx-react'; -import { normalizeVersion } from '../../utils/normalize-version'; -import { AppState } from '../app'; -import { updateEditorTypeDefinitions } from '../fetch-types'; +import { AppState } from '../state'; export interface VersionChooserState { value: string; @@ -14,56 +12,37 @@ export interface VersionChooserProps { appState: AppState; } +/** + * A dropdown allowing the selection of Electron versions. The actual + * download is managed in the state. + * + * @class VersionChooser + * @extends {React.Component} + */ @observer export class VersionChooser extends React.Component { constructor(props: VersionChooserProps) { super(props); this.handleChange = this.handleChange.bind(this); - this.handleVersionChange(this.props.appState.version); - } - - public async updateDownloadedVersionState() { - const { binaryManager } = this.props.appState; - const downloadedVersions = await binaryManager.getDownloadedVersions(); - const updatedVersions = { ...this.props.appState.versions }; - - console.log(`Version Chooser: Updating version state`); - downloadedVersions.forEach((version) => { - if (updatedVersions[version]) { - updatedVersions[version].state = 'ready'; - } - }); - - this.props.appState.versions = updatedVersions; } + /** + * Handle change, which usually means that we'd like update + * the selection version. + * + * @param {React.ChangeEvent} event + */ public handleChange(event: React.ChangeEvent) { - const version = normalizeVersion(event.target.value); - this.handleVersionChange(version); - } - - public handleVersionChange(version: string) { - console.log(`Version Chooser: Switching to v${version}`); - - this.props.appState.version = version; - - // Update TypeScript definitions - updateEditorTypeDefinitions(version); - - // Fetch new binaries, maybe? - if ((this.props.appState.versions[version] || { state: '' }).state === 'ready') return; - - console.log(`Version Chooser: Instructing BinaryManager to fetch v${version}`); - const updatedVersions = { ...this.props.appState.versions }; - updatedVersions[normalizeVersion(version)].state = 'downloading'; - this.props.appState.versions = updatedVersions; - - this.props.appState.binaryManager.setup(version) - .then(() => this.updateDownloadedVersionState()); + this.props.appState.setVersion(event.target.value); } - public renderOptions() { + /** + * Renders the individual options (Electron versions) + * + * @returns {Array} + */ + public renderOptions(): Array { const { versions } = this.props.appState; return Object.keys(versions) diff --git a/src/renderer/content.ts b/src/renderer/content.ts index 6eaa6e2532..7aff66b8e3 100644 --- a/src/renderer/content.ts +++ b/src/renderer/content.ts @@ -3,13 +3,25 @@ import * as path from 'path'; const simpleCache = {}; -export function getContent(name: string) { +/** + * Returns expected content for a given name. Currently synchronous, + * but probably shouldn't be. + * + * @param {string} name + * @returns {string} + */ +export function getContent(name: string): string { if (simpleCache[name]) return simpleCache[name]; - const filePath = path.join(__dirname, '../../static/content', name); - const content = fs.readFileSync(filePath, 'utf-8'); + let content = ''; - simpleCache[name] = content; + try { + const filePath = path.join(__dirname, '../../static/content', name); + content = fs.readFileSync(filePath, 'utf-8'); + simpleCache[name] = content; + } catch (error) { + console.error(`Content: Could not read file content for ${name}`, error); + } return content; } diff --git a/src/renderer/fetch-types.ts b/src/renderer/fetch-types.ts index 4bfead31a2..103f256e8a 100644 --- a/src/renderer/fetch-types.ts +++ b/src/renderer/fetch-types.ts @@ -1,6 +1,7 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import * as MonacoType from 'monaco-editor'; +import { get } from 'lodash'; import { USER_DATA_PATH } from './constants'; @@ -85,19 +86,28 @@ export async function getTypeDefinitions(version: string): Promise updateEditorTypeDefinitions(version, i + 1), 200); + return; + } + const typeDefs = await getTypeDefinitions(version); - if (window.ElectronFiddle.app.typeDefDisposable) { - window.ElectronFiddle.app.typeDefDisposable.dispose(); + if (typeDefDisposable) { + typeDefDisposable.dispose(); } if (typeDefs) { console.log(`Fetch Types: Updating Monaco types with electron.d.ts@${version}`); const disposable = monaco.languages.typescript.javascriptDefaults.addExtraLib(typeDefs); - (window as any).electronFiddle.typeDefDisposable = disposable; + window.ElectronFiddle.app.typeDefDisposable = disposable; } else { - console.log(`Fetch Types: No type definitons for ${version} 😢`); + console.log(`Fetch Types: No type definitions for ${version} 😢`); } } diff --git a/src/renderer/npm.ts b/src/renderer/npm.ts index 7cda8d3899..6188347906 100644 --- a/src/renderer/npm.ts +++ b/src/renderer/npm.ts @@ -5,6 +5,14 @@ export interface InstallModulesOptions { dir: string; } +/** + * Uses a simple regex to find `require()` statements in a string. + * Tries to exclude electron and Node built-ins as well as file-path + * references. + * + * @param {string} input + * @returns {Array} + */ export function findModules(input: string): Array { const matchRequire = /require\(['"]{1}([\w\d\/\-\_]*)['"]{1}\)/; const matched = input.match(matchRequire); @@ -23,6 +31,13 @@ export function findModules(input: string): Array { return result; } +/** + * Installs given modules to a given folder. + * + * @param {InstallModulesOptions} { dir } + * @param {...Array} names + * @returns {Promise} + */ export function installModules({ dir }: InstallModulesOptions, ...names: Array): Promise { return new Promise((resolve, reject) => { const args = ['-S']; diff --git a/src/renderer/state.ts b/src/renderer/state.ts new file mode 100644 index 0000000000..62bd67d10b --- /dev/null +++ b/src/renderer/state.ts @@ -0,0 +1,100 @@ +import { observable, action } from 'mobx'; +import * as tmp from 'tmp'; + +import { BinaryManager } from './binary'; +import { ElectronVersion, StringMap, OutputEntry } from '../interfaces'; +import { arrayToStringMap } from '../utils/array-to-stringmap'; +import { getKnownVersions } from './versions'; +import { normalizeVersion } from '../utils/normalize-version'; +import { updateEditorTypeDefinitions } from './fetch-types'; + +const knownVersions = getKnownVersions(); +const defaultVersion = normalizeVersion(knownVersions[0].tag_name); + +/** + * Editors exist outside of React's world. To make things *a lot* + * easier, we keep them around in a global object. Don't judge us, + * we're really only doing that for the editors. + */ +window.ElectronFiddle = { + editors: { + main: null, + renderer: null, + html: null + }, + app: null +}; + +/** + * The application's state. Exported as a singleton below. + * + * @export + * @class AppState + */ +export class AppState { + @observable public gistId: string = ''; + @observable public version: string = defaultVersion; + @observable public tmpDir: tmp.SynchrounousResult = tmp.dirSync(); + @observable public githubToken: string | null = null; + @observable public binaryManager: BinaryManager = new BinaryManager(defaultVersion); + @observable public versions: StringMap = arrayToStringMap(knownVersions); + @observable public output: Array = []; + @observable public isConsoleShowing: boolean = false; + @observable public isTokenDialogShowing: boolean = false; + @observable public isUnsaved: boolean = true; + @observable public isMyGist: boolean = false; + + @action public toggleConsole() { + this.isConsoleShowing = !this.isConsoleShowing; + } + + @action public toggleAuthDialog() { + this.isTokenDialogShowing = !this.isTokenDialogShowing; + } + + @action public async setVersion(input: string) { + const version = normalizeVersion(input); + console.log(`State: Switching to ${version}`); + + this.version = version; + + // Update TypeScript definitions + updateEditorTypeDefinitions(version); + + // Fetch new binaries, maybe? + if ((this.versions[version] || { state: '' }).state !== 'ready') { + console.log(`State: Instructing BinaryManager to fetch v${version}`); + const updatedVersions = { ...this.versions }; + updatedVersions[normalizeVersion(version)].state = 'downloading'; + this.versions = updatedVersions; + + await this.binaryManager.setup(version); + this.updateDownloadedVersionState(); + } + } + + /* + * Go and check which versions have already been downloaded. + * + * @returns {Promise} + */ + @action public async updateDownloadedVersionState(): Promise { + const downloadedVersions = await this.binaryManager.getDownloadedVersions(); + const updatedVersions = { ...this.versions }; + + console.log(`State: Updating version state`); + downloadedVersions.forEach((version) => { + if (updatedVersions[version]) { + updatedVersions[version].state = 'ready'; + } + }); + + this.versions = updatedVersions; + } +} + +export const appState = new AppState(); +appState.githubToken = localStorage.getItem('githubToken'); +appState.setVersion(appState.version); + +tmp.setGracefulCleanup(); diff --git a/src/renderer/themes.ts b/src/renderer/themes.ts index 6b2648c9ae..50744eae9e 100644 --- a/src/renderer/themes.ts +++ b/src/renderer/themes.ts @@ -1,3 +1,6 @@ +/** + * The Monaco editor theme used by Electron Fiddle. + */ export const mainTheme = { base: 'vs-dark', inherit: true, diff --git a/src/renderer/versions.ts b/src/renderer/versions.ts index f3c6a82f0a..ac316d50a9 100644 --- a/src/renderer/versions.ts +++ b/src/renderer/versions.ts @@ -1,5 +1,11 @@ import { GitHubVersion } from '../interfaces'; +/** + * Retrieves our best guess regarding the latest Electron versions. Tries to + * fetch them from localStorage, then from a static releases.json file. + * + * @returns {Array} + */ export function getKnownVersions(): Array { const fromLs = window.localStorage.getItem('known-electron-versions'); @@ -14,11 +20,21 @@ export function getKnownVersions(): Array { return require('../../static/releases.json'); } +/** + * Saves known versions to localStorage. + * + * @param {Array} versions + */ export function saveKnownVersions(versions: Array) { const stringified = JSON.stringify(versions); window.localStorage.setItem('known-electron-versions', stringified); } +/** + * Fetch the latest known versions directly from GitHub. + * + * @returns {Promise>} + */ export function fetchVersions(): Promise> { return window.fetch('https://api.github.com/repos/electron/electron/releases') .then((response) => response.json()) diff --git a/src/utils/array-to-stringmap.ts b/src/utils/array-to-stringmap.ts index a927dc7935..862b3dc3a9 100644 --- a/src/utils/array-to-stringmap.ts +++ b/src/utils/array-to-stringmap.ts @@ -1,6 +1,13 @@ import { normalizeVersion } from './normalize-version'; import { GitHubVersion, StringMap, ElectronVersion } from '../interfaces'; +/** + * Takes an array of GitHub releases and returns a StringMap of + * Electron releases. + * + * @param {Array} input + * @returns {StringMap} + */ export function arrayToStringMap(input: Array): StringMap { const output = {}; diff --git a/src/utils/editor-layout.ts b/src/utils/editor-layout.ts index d1618a2320..78d17fda97 100644 --- a/src/utils/editor-layout.ts +++ b/src/utils/editor-layout.ts @@ -1,5 +1,9 @@ import { throttle } from 'lodash'; +/** + * Attempts to update the layout of all editors. Exported as + * a debounced version below. + */ function _updateEditorLayout() { const { main, renderer, html } = window.ElectronFiddle.editors; diff --git a/src/utils/normalize-version.ts b/src/utils/normalize-version.ts index d1a9d7ef59..df6ec64b50 100644 --- a/src/utils/normalize-version.ts +++ b/src/utils/normalize-version.ts @@ -1,4 +1,10 @@ -export function normalizeVersion(version: string) { +/** + * Removes a possible leading "v" from a version. + * + * @param {string} version + * @returns {string} + */ +export function normalizeVersion(version: string): string { if (version.startsWith('v')) { return version.slice(1); } else { diff --git a/src/utils/update-versions.ts b/src/utils/update-versions.ts deleted file mode 100644 index 5c58ec314f..0000000000 --- a/src/utils/update-versions.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ElectronVersion, StringMap, ElectronVersionState } from '../interfaces'; - -export function updateVersionState( - versions: StringMap, version: string, state: ElectronVersionState -) { - const updatedVersion = { - ...versions[version], - state - }; - - const updatedVersions = { ...versions }; - updatedVersions[version] = updatedVersion; - - return updatedVersions; -}