Repository: electron/fiddle
Pull request #620 (2021-03-25 22:28:50)

refactor: remove fancy import
Removes fancyImport. Mostly this results in fs-extra and path being imported at the top level. There was discussion on the Slack channel that we might want to check the performance hit of top-loading vs deferred loading. The thing is, we were top-loading anyway to get the type so that we could cast the fancyImport to the right type. For example you would see

```js
import * as fsType from 'fs-extra';
...
async function doSomethingWithFsExtra() {
  const fs = await fancyImport<typeof fsType>('fs-extra');
  ...
}
```

... so we were already taking the top-level import hit in nearly every case and this PR doesn't change that. The one exception to this is top-importing namor, whose require/import tree is its own 4 files + a few more for its one crypto dependency, about 10 files in total.

PR as discussed with @felixrieseberg and @erickzhao.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/38263743/badge)](https://coveralls.io/builds/38263743)

Coverage increased (+0.003%) to 93.311% when pulling **9ad403720b98fca1b2dfd62a60a6d77425cb7afe on refactor/remove-fancyImport** into **34017cd4be99cf3f26ea2b644d0c9d16c5f266ed on master**.


Patch:
diff --git a/src/main/files.ts b/src/main/files.ts
index 2afe8c67..3a146c17 100644
--- a/src/main/files.ts
+++ b/src/main/files.ts
@@ -3,13 +3,7 @@ import * as fs from 'fs-extra';
 import * as path from 'path';
 
 import { IpcEvents } from '../ipc-events';
-import {
-  INDEX_HTML_NAME,
-  MAIN_JS_NAME,
-  PACKAGE_NAME,
-  PRELOAD_JS_NAME,
-  RENDERER_JS_NAME,
-} from '../shared-constants';
+import { FILENAME_KEYS } from '../shared-constants';
 import { ipcMainManager } from './ipc';
 
 /**
@@ -69,13 +63,9 @@ export async function showSaveDialog(event?: IpcEvents, as?: string) {
  * @returns {Promise<boolean>}
  */
 async function ensureSaveTargetEmpty(filePath: string): Promise<boolean> {
-  const targetPaths = [
-    path.join(filePath, INDEX_HTML_NAME),
-    path.join(filePath, RENDERER_JS_NAME),
-    path.join(filePath, MAIN_JS_NAME),
-    path.join(filePath, PACKAGE_NAME),
-    path.join(filePath, PRELOAD_JS_NAME),
-  ];
+  const targetPaths = Object.keys(FILENAME_KEYS).map((filename) =>
+    path.join(filePath, filename),
+  );
 
   let noFilesOrOverwriteGranted = true;
 
diff --git a/src/renderer/components/commands-action-button.tsx b/src/renderer/components/commands-action-button.tsx
index a2d468ac..ac91ab57 100644
--- a/src/renderer/components/commands-action-button.tsx
+++ b/src/renderer/components/commands-action-button.tsx
@@ -10,6 +10,7 @@ import {
 } from '@blueprintjs/core';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import * as path from 'path';
 
 import { when } from 'mobx';
 import {
@@ -19,13 +20,7 @@ import {
   GistActionType,
 } from '../../interfaces';
 import { IpcEvents } from '../../ipc-events';
-import {
-  INDEX_HTML_NAME,
-  MAIN_JS_NAME,
-  PRELOAD_JS_NAME,
-  RENDERER_JS_NAME,
-  STYLES_CSS_NAME,
-} from '../../shared-constants';
+import { FILENAME_KEYS } from '../../shared-constants';
 import { getOctokit } from '../../utils/octokit';
 import { EMPTY_EDITOR_CONTENT } from '../constants';
 import { ipcRendererManager } from '../ipc';
@@ -434,22 +429,13 @@ export class GistActionButton extends React.Component<
   };
 
   private gistFilesList = (values: EditorValues) => {
-    return {
-      [INDEX_HTML_NAME]: {
-        content: values.html || EMPTY_EDITOR_CONTENT.html,
-      },
-      [MAIN_JS_NAME]: {
-        content: values.main || EMPTY_EDITOR_CONTENT.js,
-      },
-      [RENDERER_JS_NAME]: {
-        content: values.renderer || EMPTY_EDITOR_CONTENT.js,
-      },
-      [PRELOAD_JS_NAME]: {
-        content: values.preload || EMPTY_EDITOR_CONTENT.js,
-      },
-      [STYLES_CSS_NAME]: {
-        content: values.css || EMPTY_EDITOR_CONTENT.css,
-      },
-    };
+    const getSuffix = (name: string) => path.parse(name).ext.slice(1);
+    const filesList = {};
+    for (const [filename, editorId] of Object.entries(FILENAME_KEYS)) {
+      filesList[filename] = {
+        content: values[editorId] || EMPTY_EDITOR_CONTENT[getSuffix(filename)],
+      };
+    }
+    return filesList;
   };
 }
diff --git a/src/renderer/components/commands-bisect.tsx b/src/renderer/components/commands-bisect.tsx
index fc748298..f02c9aef 100644
--- a/src/renderer/components/commands-bisect.tsx
+++ b/src/renderer/components/commands-bisect.tsx
@@ -87,6 +87,7 @@ export class BisectHandler extends React.Component<BisectHandlerProps> {
         <Button
           icon="git-branch"
           text="Bisect"
+          disabled={appState.isAutoBisecting}
           onClick={appState.toggleBisectDialog}
         />
       );
diff --git a/src/renderer/components/commands-version-chooser.tsx b/src/renderer/components/commands-version-chooser.tsx
index f485a8d3..27d11f4c 100644
--- a/src/renderer/components/commands-version-chooser.tsx
+++ b/src/renderer/components/commands-version-chooser.tsx
@@ -14,7 +14,12 @@ export interface VersionChooserProps {
  * download is managed in the state.
  */
 export const VersionChooser = observer((props: VersionChooserProps) => {
-  const { currentElectronVersion, Bisector, setVersion } = props.appState;
+  const {
+    Bisector,
+    currentElectronVersion,
+    isAutoBisecting,
+    setVersion,
+  } = props.appState;
 
   return (
     <ButtonGroup>
@@ -22,7 +27,7 @@ export const VersionChooser = observer((props: VersionChooserProps) => {
         appState={props.appState}
         onVersionSelect={({ version }) => setVersion(version)}
         currentVersion={currentElectronVersion}
-        disabled={!!Bisector}
+        disabled={!!Bisector || isAutoBisecting}
       />
     </ButtonGroup>
   );
diff --git a/src/renderer/components/dialog-bisect.tsx b/src/renderer/components/dialog-bisect.tsx
index a98ddf5d..12647cc4 100644
--- a/src/renderer/components/dialog-bisect.tsx
+++ b/src/renderer/components/dialog-bisect.tsx
@@ -33,6 +33,7 @@ export class BisectDialog extends React.Component<
     super(props);
 
     this.onSubmit = this.onSubmit.bind(this);
+    this.onAuto = this.onAuto.bind(this);
     this.onClose = this.onClose.bind(this);
     this.onBeginSelect = this.onBeginSelect.bind(this);
     this.onEndSelect = this.onEndSelect.bind(this);
@@ -55,25 +56,35 @@ export class BisectDialog extends React.Component<
     this.setState({ endIndex: this.state.allVersions.indexOf(version) });
   }
 
+  getBisectRange(): RunnableVersion[] {
+    const { endIndex, startIndex, allVersions } = this.state;
+    return endIndex !== undefined && startIndex !== undefined
+      ? allVersions.slice(endIndex, startIndex + 1).reverse()
+      : [];
+  }
+
   /**
    * Handles the submission of the dialog
    *
    * @returns {Promise<void>}
    */
   public async onSubmit(): Promise<void> {
-    const { endIndex, startIndex, allVersions } = this.state;
-    const { appState } = this.props;
-
-    if (endIndex === undefined || startIndex === undefined) {
-      return;
+    const range = this.getBisectRange();
+    if (range.length > 1) {
+      const { appState } = this.props;
+      appState.Bisector = new Bisector(range);
+      const initialBisectPivot = appState.Bisector.getCurrentVersion().version;
+      appState.setVersion(initialBisectPivot);
+      this.onClose();
     }
+  }
 
-    const bisectRange = allVersions.slice(endIndex, startIndex + 1).reverse();
-
-    appState.Bisector = new Bisector(bisectRange);
-    const initialBisectPivot = appState.Bisector.getCurrentVersion().version;
-    appState.setVersion(initialBisectPivot);
-    this.onClose();
+  public async onAuto(): Promise<void> {
+    const range = this.getBisectRange();
+    if (range.length > 1) {
+      window.ElectronFiddle.app.runner.autobisect(range);
+      this.onClose();
+    }
   }
 
   /**
@@ -97,6 +108,13 @@ export class BisectDialog extends React.Component<
     return this.state.startIndex > this.state.endIndex;
   }
 
+  /**
+   * Can we autobisect?
+   */
+  get canAuto(): boolean {
+    return this.canSubmit;
+  }
+
   /**
    * Renders the buttons
    */
@@ -109,6 +127,13 @@ export class BisectDialog extends React.Component<
         onClick={this.onSubmit}
         text="Begin"
       />,
+      <Button
+        icon="lab-test"
+        key="auto"
+        disabled={!this.canAuto}
+        onClick={this.onAuto}
+        text="Auto"
+      />,
       <Button icon="cross" key="cancel" onClick={this.onClose} text="Cancel" />,
     ];
   }
diff --git a/src/renderer/components/editors.tsx b/src/renderer/components/editors.tsx
index 16709482..32c99f1e 100644
--- a/src/renderer/components/editors.tsx
+++ b/src/renderer/components/editors.tsx
@@ -2,6 +2,13 @@ import { reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as MonacoType from 'monaco-editor';
 import * as React from 'react';
+import {
+  INDEX_HTML_NAME,
+  MAIN_JS_NAME,
+  PRELOAD_JS_NAME,
+  RENDERER_JS_NAME,
+  STYLES_CSS_NAME,
+} from '../../shared-constants';
 import {
   Mosaic,
   MosaicBranch,
@@ -37,12 +44,12 @@ const defaultMonacoOptions: MonacoType.editor.IEditorOptions = {
 };
 
 export const TITLE_MAP: Record<MosaicId, string> = {
-  main: 'Main Process (main.js)',
-  renderer: 'Renderer Process (renderer.js)',
-  preload: 'Preload (preload.js)',
-  html: 'HTML (index.html)',
-  css: 'Stylesheet (styles.css)',
-  docsDemo: 'Docs & Demos',
+  [EditorId.main]: `Main Process (${MAIN_JS_NAME})`,
+  [EditorId.renderer]: `Renderer Process (${RENDERER_JS_NAME})`,
+  [EditorId.preload]: `Preload (${PRELOAD_JS_NAME})`,
+  [EditorId.html]: `HTML (${INDEX_HTML_NAME})`,
+  [EditorId.css]: `Stylesheet (${STYLES_CSS_NAME})`,
+  [PanelId.docsDemo]: 'Docs & Demos',
 };
 
 export interface EditorsProps {
diff --git a/src/renderer/components/settings-general-appearance.tsx b/src/renderer/components/settings-general-appearance.tsx
index 621977fd..12b1f524 100644
--- a/src/renderer/components/settings-general-appearance.tsx
+++ b/src/renderer/components/settings-general-appearance.tsx
@@ -5,16 +5,16 @@ import {
   FormGroup,
   MenuItem,
 } from '@blueprintjs/core';
+import * as React from 'react';
+import * as fs from 'fs-extra';
+import * as namor from 'namor';
+import * as path from 'path';
 import { ItemPredicate, ItemRenderer, Select } from '@blueprintjs/select';
-import { shell } from 'electron';
-import * as fsType from 'fs-extra';
-import { reaction } from 'mobx';
 import { observer } from 'mobx-react';
-import * as path from 'path';
-import * as React from 'react';
+import { reaction } from 'mobx';
+import { shell } from 'electron';
 
 import { highlightText } from '../../utils/highlight-text';
-import { fancyImport } from '../../utils/import';
 import { AppState } from '../state';
 import { getAvailableThemes, getTheme, THEMES_PATH } from '../themes';
 import { LoadedFiddleTheme } from '../themes-defaults';
@@ -137,11 +137,9 @@ export class AppearanceSettings extends React.Component<
    */
   public async createNewThemeFromCurrent(): Promise<boolean> {
     const { appState } = this.props;
-    const fs = await fancyImport<typeof fsType>('fs-extra');
     const theme = await getTheme(appState.theme);
 
     try {
-      const namor = await fancyImport<any>('namor');
       const name = namor.generate({ words: 2, numbers: 0 });
       const themePath = path.join(THEMES_PATH, `${name}.json`);
 
@@ -175,8 +173,6 @@ export class AppearanceSettings extends React.Component<
    * @returns {Promise<boolean>}
    */
   public async openThemeFolder(): Promise<boolean> {
-    const fs = await fancyImport<typeof fsType>('fs-extra');
-
     try {
       await fs.ensureDir(THEMES_PATH);
       await shell.showItemInFolder(THEMES_PATH);
diff --git a/src/renderer/components/version-select.tsx b/src/renderer/components/version-select.tsx
index d142b13d..7b056937 100644
--- a/src/renderer/components/version-select.tsx
+++ b/src/renderer/components/version-select.tsx
@@ -42,11 +42,16 @@ export function getItemLabel({ source, state, name }: RunnableVersion): string {
  * @returns
  */
 export function getItemIcon({ state }: RunnableVersion) {
-  return state === 'ready'
-    ? 'saved'
-    : state === 'downloading'
-    ? 'cloud-download'
-    : 'cloud';
+  switch (state) {
+    case VersionState.unknown:
+      return 'cloud';
+    case VersionState.ready:
+      return 'saved';
+    case VersionState.downloading:
+      return 'cloud-download';
+    case VersionState.unzipping:
+      return 'compressed';
+  }
 }
 
 /**
diff --git a/src/renderer/content.ts b/src/renderer/content.ts
index 51466768..1ea21cc0 100644
--- a/src/renderer/content.ts
+++ b/src/renderer/content.ts
@@ -1,10 +1,9 @@
 import { EditorId, EditorValues, VersionSource } from '../interfaces';
 import { USER_DATA_PATH } from './constants';
-import { fancyImport } from '../utils/import';
 import { getElectronVersions } from './versions';
 import { readFiddle } from '../utils/read-fiddle';
 
-import * as fsType from 'fs-extra';
+import * as fs from 'fs-extra';
 import * as path from 'path';
 import * as semver from 'semver';
 import decompress from 'decompress';
@@ -33,7 +32,6 @@ async function prepareTemplate(branch: string): Promise<string> {
 
   try {
     // if we don't have it, download it
-    const fs = await fancyImport<typeof fsType>('fs-extra');
     if (!fs.existsSync(folder)) {
       console.log(`Content: ${branch} downloading template`);
       const url = `https://github.com/electron/electron-quick-start/archive/${branch}.zip`;
diff --git a/src/renderer/fetch-types.ts b/src/renderer/fetch-types.ts
index 231bcdc9..28a9fbb6 100644
--- a/src/renderer/fetch-types.ts
+++ b/src/renderer/fetch-types.ts
@@ -1,10 +1,9 @@
-import * as fsType from 'fs-extra';
 import * as MonacoType from 'monaco-editor';
+import * as fs from 'fs-extra';
 import * as path from 'path';
 
 import { RunnableVersion, VersionSource } from '../interfaces';
 import { callIn } from '../utils/call-in';
-import { fancyImport } from '../utils/import';
 import { normalizeVersion } from '../utils/normalize-version';
 import { USER_DATA_PATH } from './constants';
 
@@ -42,18 +41,10 @@ export async function fetchTypeDefinitions(version: string): Promise<string> {
  *
  * @param version
  */
-export async function removeTypeDefsForVersion(version: string) {
-  const fs = await fancyImport<typeof fsType>('fs-extra');
+export function removeTypeDefsForVersion(version: string): Promise<void> {
   const _version = normalizeVersion(version);
   const typeDefsDir = path.dirname(getOfflineTypeDefinitionPath(_version));
-
-  if (fs.existsSync(typeDefsDir)) {
-    try {
-      await fs.remove(typeDefsDir);
-    } catch (error) {
-      throw error;
-    }
-  }
+  return fs.remove(typeDefsDir);
 }
 
 /**
@@ -72,10 +63,7 @@ export function getOfflineTypeDefinitionPath(version: string): string {
  * @param {string} version
  * @returns {boolean}
  */
-export async function getOfflineTypeDefinitions(
-  version: string,
-): Promise<boolean> {
-  const fs = await fancyImport<typeof fsType>('fs-extra');
+export function getOfflineTypeDefinitions(version: string): boolean {
   return fs.existsSync(getOfflineTypeDefinitionPath(version));
 }
 
@@ -89,11 +77,10 @@ export async function getOfflineTypeDefinitions(
 export async function getDownloadedVersionTypeDefs(
   version: RunnableVersion,
 ): Promise<string | null> {
-  const fs = await fancyImport<typeof fsType>('fs-extra');
   await fs.mkdirp(definitionPath);
   const offlinePath = getOfflineTypeDefinitionPath(version.version);
 
-  if (await getOfflineTypeDefinitions(version.version)) {
+  if (getOfflineTypeDefinitions(version.version)) {
     try {
       return await fs.readFile(offlinePath, 'utf-8');
     } catch (error) {
@@ -117,7 +104,6 @@ export async function getDownloadedVersionTypeDefs(
 
 export async function getLocalVersionTypeDefs(version: RunnableVersion) {
   if (version.source === VersionSource.local && !!version.localPath) {
-    const fs = await fancyImport<typeof fsType>('fs-extra');
     const typesPath = getLocalTypePathForVersion(version);
     if (!!typesPath && fs.existsSync(typesPath)) {
       return fs.readFile(typesPath, 'utf-8');
diff --git a/src/renderer/file-manager.ts b/src/renderer/file-manager.ts
index 7551b6b9..22b4454f 100644
--- a/src/renderer/file-manager.ts
+++ b/src/renderer/file-manager.ts
@@ -1,18 +1,10 @@
-import * as fsType from 'fs-extra';
+import * as fs from 'fs-extra';
 import * as path from 'path';
 
 import { Files, FileTransform } from '../interfaces';
 import { IpcEvents } from '../ipc-events';
-import {
-  INDEX_HTML_NAME,
-  MAIN_JS_NAME,
-  PACKAGE_NAME,
-  PRELOAD_JS_NAME,
-  RENDERER_JS_NAME,
-  STYLES_CSS_NAME,
-} from '../shared-constants';
+import { FILENAME_KEYS, PACKAGE_NAME } from '../shared-constants';
 import { DEFAULT_OPTIONS, PackageJsonOptions } from '../utils/get-package';
-import { fancyImport } from '../utils/import';
 import { readFiddle } from '../utils/read-fiddle';
 import { ipcRendererManager } from './ipc';
 import { AppState } from './state';
@@ -133,13 +125,11 @@ export class FileManager {
   ): Promise<Files> {
     const pOptions = typeof options === 'object' ? options : DEFAULT_OPTIONS;
     const values = await window.ElectronFiddle.app.getEditorValues(pOptions);
-    let output: Files = new Map();
 
-    output.set(RENDERER_JS_NAME, values.renderer);
-    output.set(MAIN_JS_NAME, values.main);
-    output.set(INDEX_HTML_NAME, values.html);
-    output.set(PRELOAD_JS_NAME, values.preload);
-    output.set(STYLES_CSS_NAME, values.css);
+    let output: Files = new Map();
+    for (const [filename, editorId] of Object.entries(FILENAME_KEYS)) {
+      output.set(filename, values[editorId]);
+    }
     output.set(PACKAGE_NAME, values.package!);
 
     for (const transform of transforms) {
@@ -165,8 +155,6 @@ export class FileManager {
    */
   public async cleanup(dir?: string): Promise<boolean> {
     if (dir) {
-      const fs = await fancyImport<typeof fsType>('fs-extra');
-
       if (fs.existsSync(dir)) {
         try {
           await fs.remove(dir);
@@ -192,7 +180,6 @@ export class FileManager {
     options: PackageJsonOptions,
     ...transforms: Array<FileTransform>
   ): Promise<string> {
-    const fs = await fancyImport<typeof fsType>('fs-extra');
     const tmp = await import('tmp');
     const files = await this.getFiles(options, ...transforms);
     const dir = tmp.dirSync();
@@ -220,7 +207,6 @@ export class FileManager {
    */
   private async saveFile(filePath: string, content: string): Promise<void> {
     try {
-      const fs = await fancyImport<typeof fsType>('fs-extra');
       return await fs.outputFile(filePath, content, { encoding: 'utf-8' });
     } catch (error) {
       console.log(`FileManager: Could not save ${filePath}`, error);
@@ -238,7 +224,6 @@ export class FileManager {
    */
   private async removeFile(filePath: string): Promise<void> {
     try {
-      const fs = await fancyImport<typeof fsType>('fs-extra');
       return await fs.remove(filePath);
     } catch (error) {
       console.log(`FileManager: Could not remove ${filePath}`, error);
diff --git a/src/renderer/remote-loader.ts b/src/renderer/remote-loader.ts
index d61a8c68..78300f64 100644
--- a/src/renderer/remote-loader.ts
+++ b/src/renderer/remote-loader.ts
@@ -1,14 +1,7 @@
 import { Octokit } from '@octokit/rest';
 import { when } from 'mobx';
 import { EditorValues, GenericDialogType } from '../interfaces';
-import {
-  INDEX_HTML_NAME,
-  MAIN_JS_NAME,
-  PRELOAD_JS_NAME,
-  RENDERER_JS_NAME,
-  STYLES_CSS_NAME,
-  FILENAME_KEYS,
-} from '../shared-constants';
+import { FILENAME_KEYS } from '../shared-constants';
 import { getOctokit } from '../utils/octokit';
 import { sortedElectronMap } from '../utils/sorted-electron-map';
 import { ELECTRON_ORG, ELECTRON_REPO } from './constants';
@@ -136,16 +129,11 @@ export class RemoteLoader {
       const octo = await getOctokit(this.appState);
       const gist = await octo.gists.get({ gist_id: gistId });
 
-      return this.handleLoadingSuccess(
-        {
-          html: this.getContentOrEmpty(gist, INDEX_HTML_NAME),
-          main: this.getContentOrEmpty(gist, MAIN_JS_NAME),
-          renderer: this.getContentOrEmpty(gist, RENDERER_JS_NAME),
-          preload: this.getContentOrEmpty(gist, PRELOAD_JS_NAME),
-          css: this.getContentOrEmpty(gist, STYLES_CSS_NAME),
-        },
-        gistId,
-      );
+      const values: Partial<EditorValues> = {};
+      for (const [filename, editorId] of Object.entries(FILENAME_KEYS)) {
+        values[editorId] = this.getContentOrEmpty(gist, filename);
+      }
+      return this.handleLoadingSuccess(values, gistId);
     } catch (error) {
       return this.handleLoadingFailed(error);
     }
diff --git a/src/renderer/runner.ts b/src/renderer/runner.ts
index d3d12046..f8ad6ea6 100644
--- a/src/renderer/runner.ts
+++ b/src/renderer/runner.ts
@@ -60,7 +60,21 @@ export class Runner {
    * @returns {Promise<RunResult>}
    * @memberof Runner
    */
-  public async autobisect(
+  public autobisect(versions: Array<RunnableVersion>): Promise<RunResult> {
+    const { appState } = this;
+    appState.isAutoBisecting = true;
+    const done = () => (appState.isAutoBisecting = false);
+    return this.autobisectImpl(versions).finally(done);
+  }
+
+  /**
+   * Bisect the current fiddle across the specified versions.
+   *
+   * @param {Array<RunnableVersion>} versions - versions to bisect
+   * @returns {Promise<RunResult>}
+   * @memberof Runner
+   */
+  public async autobisectImpl(
     versions: Array<RunnableVersion>,
   ): Promise<RunResult> {
     const { appState } = this;
diff --git a/src/renderer/state.ts b/src/renderer/state.ts
index 915f62b0..e8a15fd4 100644
--- a/src/renderer/state.ts
+++ b/src/renderer/state.ts
@@ -1,4 +1,4 @@
-import * as fsType from 'fs-extra';
+import * as fs from 'fs-extra';
 import { action, autorun, computed, observable, toJS, when } from 'mobx';
 import { MosaicNode } from 'react-mosaic-component';
 
@@ -27,7 +27,6 @@ import {
   getVisibleMosaics,
 } from '../utils/editors-mosaic-arrangement';
 import { getName } from '../utils/get-title';
-import { fancyImport } from '../utils/import';
 import { normalizeVersion } from '../utils/normalize-version';
 import { isEditorBackup, isEditorId, isPanelId } from '../utils/type-checks';
 import {
@@ -142,11 +141,12 @@ export class AppState {
   public mosaicArrangement: MosaicNode<MosaicId> | null = DEFAULT_MOSAIC_ARRANGEMENT;
   @observable public templateName: string | undefined;
   @observable public currentDocsDemoPage: DocsDemoPage = DocsDemoPage.DEFAULT;
-  @observable public localTypeWatcher: fsType.FSWatcher | undefined;
+  @observable public localTypeWatcher: fs.FSWatcher | undefined;
   @observable public Bisector: Bisector | undefined;
 
   @observable public activeGistAction: GistActionState = GistActionState.none;
   @observable public isRunning = false;
+  @observable public isAutoBisecting = false;
   @observable public isInstallingModules = false;
   @observable public isUnsaved: boolean;
   @observable public isUpdatingElectronVersions = false;
@@ -545,7 +545,6 @@ export class AppState {
     const versionObject = this.versions[version];
 
     if (versionObject.source === VersionSource.local) {
-      const fs = await fancyImport<typeof fsType>('fs-extra');
       const typePath = getLocalTypePathForVersion(versionObject);
       console.info(
         `TypeDefs: Watching file for local version ${version} at path ${typePath}`,
diff --git a/src/renderer/templates.ts b/src/renderer/templates.ts
index 40bed9e9..10fce0ea 100644
--- a/src/renderer/templates.ts
+++ b/src/renderer/templates.ts
@@ -1,6 +1,5 @@
-import * as pathType from 'path';
+import * as path from 'path';
 import { EditorValues } from '../interfaces';
-import { fancyImport } from '../utils/import';
 import { readFiddle } from '../utils/read-fiddle';
 
 /**
@@ -9,8 +8,7 @@ import { readFiddle } from '../utils/read-fiddle';
  * @param {string} name
  * @returns {Promise<EditorValues>}
  */
-export async function getTemplateValues(name: string): Promise<EditorValues> {
-  const path = await fancyImport<typeof pathType>('path');
+export function getTemplateValues(name: string): Promise<EditorValues> {
   const folder = path.join(
     __dirname,
     '../../static/show-me',
diff --git a/src/renderer/themes.ts b/src/renderer/themes.ts
index 1e725b76..56298327 100644
--- a/src/renderer/themes.ts
+++ b/src/renderer/themes.ts
@@ -1,8 +1,7 @@
-import * as fsType from 'fs-extra';
+import * as fs from 'fs-extra';
 import * as MonacoType from 'monaco-editor';
 import * as path from 'path';
 
-import { fancyImport } from '../utils/import';
 import { CONFIG_PATH } from './constants';
 import {
   defaultDark,
@@ -47,7 +46,6 @@ export async function readThemeFile(
   if (!name || name === DefaultThemes.DARK) return defaultDark as any;
   if (name === DefaultThemes.LIGHT) return defaultLight as any;
 
-  const fs = await fancyImport<typeof fsType>('fs-extra');
   const file = name.endsWith('.json') ? name : `${name}.json`;
   const themePath = path.join(THEMES_PATH, file);
 
@@ -70,7 +68,6 @@ export async function readThemeFile(
  * @returns {Promise<Array<FiddleTheme>>}
  */
 export async function getAvailableThemes(): Promise<Array<LoadedFiddleTheme>> {
-  const fs = await fancyImport<typeof fsType>('fs-extra');
   const themes: Array<LoadedFiddleTheme> = [
     defaultDark as any,
     defaultLight as any,
diff --git a/src/utils/get-package.ts b/src/utils/get-package.ts
index 3f8b673a..a233b6c4 100644
--- a/src/utils/get-package.ts
+++ b/src/utils/get-package.ts
@@ -1,4 +1,5 @@
 import { EditorValues } from '../interfaces';
+import { MAIN_JS_NAME } from '../shared-constants';
 import { findModulesInEditors } from '../renderer/npm';
 import { AppState } from '../renderer/state';
 import { getUsername } from './get-username';
@@ -50,7 +51,7 @@ export async function getPackageJson(
       productName: name,
       description: 'My Electron application description',
       keywords: [],
-      main: './main.js',
+      main: `./${MAIN_JS_NAME}`,
       version: '1.0.0',
       author: getUsername(),
       scripts: {
diff --git a/src/utils/import.ts b/src/utils/import.ts
deleted file mode 100644
index ad55e509..00000000
--- a/src/utils/import.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-const moduleMap: Record<string, any> = {};
-
-/**
- * This fancy import mostly helps us to easily mock asynchronous
- * imports of modules.
- *
- * @export
- * @template T
- * @param {string} p
- * @returns {Promise<T>}
- */
-export async function fancyImport<T>(p: string): Promise<T> {
-  if (!moduleMap[p]) {
-    moduleMap[p] = await import(p);
-  }
-
-  return moduleMap[p];
-}
diff --git a/src/utils/read-fiddle.ts b/src/utils/read-fiddle.ts
index eb6aa053..76d591b0 100644
--- a/src/utils/read-fiddle.ts
+++ b/src/utils/read-fiddle.ts
@@ -1,8 +1,7 @@
 import { EditorValues } from '../interfaces';
 import { FILENAME_KEYS } from '../shared-constants';
-import { fancyImport } from './import';
 
-import * as fsType from 'fs-extra';
+import * as fs from 'fs-extra';
 import * as path from 'path';
 
 /**
@@ -22,7 +21,6 @@ export async function readFiddle(folder: string): Promise<EditorValues> {
 
   const hits: string[] = [];
   const misses = new Set(Object.keys(FILENAME_KEYS));
-  const fs = await fancyImport<typeof fsType>('fs-extra');
 
   const tryRead = (basename: string) => {
     try {


Test patch:
diff --git a/tests/renderer/binary-spec.ts b/tests/renderer/binary-spec.ts
index c11e45c3..8d7c4ad3 100644
--- a/tests/renderer/binary-spec.ts
+++ b/tests/renderer/binary-spec.ts
@@ -17,14 +17,6 @@ jest.mock('../../src/renderer/ipc', () => ({}));
 jest.mock('../../src/renderer/constants', () => ({
   USER_DATA_PATH: 'user/data/',
 }));
-jest.mock('../../src/utils/import', () => ({
-  fancyImport: async (p: string) => {
-    if (p === 'extract-zip') {
-      return { default: require('extract-zip') };
-    }
-    throw new Error(`Unexpected module '${p}'`);
-  },
-}));
 jest.mock('../../src/renderer/fetch-types', () => ({
   removeTypeDefsForVersion: jest.fn(),
 }));
diff --git a/tests/renderer/components/__snapshots__/commands-version-chooser-spec.tsx.snap b/tests/renderer/components/__snapshots__/commands-version-chooser-spec.tsx.snap
index 84940e53..3a6a3cb0 100644
--- a/tests/renderer/components/__snapshots__/commands-version-chooser-spec.tsx.snap
+++ b/tests/renderer/components/__snapshots__/commands-version-chooser-spec.tsx.snap
@@ -104,7 +104,6 @@ exports[`VersionSelect component renders 1`] = `
         "version": "2.0.2",
       }
     }
-    disabled={false}
     onVersionSelect={[Function]}
   />
 </Blueprint3.ButtonGroup>
diff --git a/tests/renderer/components/__snapshots__/dialog-bisect-spec.tsx.snap b/tests/renderer/components/__snapshots__/dialog-bisect-spec.tsx.snap
index 015df037..38059a6c 100644
--- a/tests/renderer/components/__snapshots__/dialog-bisect-spec.tsx.snap
+++ b/tests/renderer/components/__snapshots__/dialog-bisect-spec.tsx.snap
@@ -221,6 +221,13 @@ exports[`BisectDialog component renders 1`] = `
         onClick={[Function]}
         text="Begin"
       />
+      <Blueprint3.Button
+        disabled={false}
+        icon="lab-test"
+        key="auto"
+        onClick={[Function]}
+        text="Auto"
+      />
       <Blueprint3.Button
         icon="cross"
         key="cancel"
@@ -439,6 +446,13 @@ exports[`BisectDialog component renders 2`] = `
         onClick={[Function]}
         text="Begin"
       />
+      <Blueprint3.Button
+        disabled={true}
+        icon="lab-test"
+        key="auto"
+        onClick={[Function]}
+        text="Auto"
+      />
       <Blueprint3.Button
         icon="cross"
         key="cancel"
@@ -664,6 +678,13 @@ exports[`BisectDialog component renders 3`] = `
         onClick={[Function]}
         text="Begin"
       />
+      <Blueprint3.Button
+        disabled={true}
+        icon="lab-test"
+        key="auto"
+        onClick={[Function]}
+        text="Auto"
+      />
       <Blueprint3.Button
         icon="cross"
         key="cancel"
@@ -896,6 +917,13 @@ exports[`BisectDialog component renders 4`] = `
         onClick={[Function]}
         text="Begin"
       />
+      <Blueprint3.Button
+        disabled={true}
+        icon="lab-test"
+        key="auto"
+        onClick={[Function]}
+        text="Auto"
+      />
       <Blueprint3.Button
         icon="cross"
         key="cancel"
@@ -1129,6 +1157,13 @@ exports[`BisectDialog component renders 5`] = `
         onClick={[Function]}
         text="Begin"
       />
+      <Blueprint3.Button
+        disabled={true}
+        icon="lab-test"
+        key="auto"
+        onClick={[Function]}
+        text="Auto"
+      />
       <Blueprint3.Button
         icon="cross"
         key="cancel"
diff --git a/tests/renderer/components/dialog-bisect-spec.tsx b/tests/renderer/components/dialog-bisect-spec.tsx
index b8f59204..f1994de1 100644
--- a/tests/renderer/components/dialog-bisect-spec.tsx
+++ b/tests/renderer/components/dialog-bisect-spec.tsx
@@ -1,6 +1,10 @@
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { VersionSource, VersionState } from '../../../src/interfaces';
+import {
+  RunResult,
+  VersionSource,
+  VersionState,
+} from '../../../src/interfaces';
 import { Bisector } from '../../../src/renderer/bisect';
 import { BisectDialog } from '../../../src/renderer/components/dialog-bisect';
 import { ElectronReleaseChannel } from '../../../src/renderer/versions';
@@ -137,6 +141,54 @@ describe('BisectDialog component', () => {
     });
   });
 
+  describe('onAuto()', () => {
+    it('initiates autobisect', async () => {
+      // setup: dialog state
+      const wrapper = shallow(<BisectDialog appState={store} />);
+      wrapper.setState({
+        allVersions: generateVersionRange(5),
+        endIndex: 0,
+        startIndex: 4,
+      });
+
+      // setup: the spy
+      const spy = jest
+        .spyOn(window.ElectronFiddle.app.runner, 'autobisect')
+        .mockResolvedValueOnce(RunResult.SUCCESS);
+
+      // click the 'auto' button
+      const instance1: BisectDialog = wrapper.instance() as any;
+      await instance1.onAuto();
+
+      // check the results
+      expect(spy).toHaveBeenCalled();
+
+      // cleanup
+      spy.mockRestore();
+    });
+
+    it('does nothing if endIndex or startIndex are falsy', async () => {
+      const wrapper = shallow(<BisectDialog appState={store} />);
+
+      wrapper.setState({
+        startIndex: undefined,
+        endIndex: 0,
+      });
+      const instance1: BisectDialog = wrapper.instance() as any;
+      await instance1.onAuto();
+      expect(Bisector).not.toHaveBeenCalled();
+
+      wrapper.setState({
+        startIndex: 4,
+        endIndex: undefined,
+      });
+
+      const instance2: BisectDialog = wrapper.instance() as any;
+      await instance2.onAuto();
+      expect(Bisector).not.toHaveBeenCalled();
+    });
+  });
+
   describe('items disabled', () => {
     let instance: BisectDialog;
 
diff --git a/tests/renderer/components/settings-credits-spec.tsx b/tests/renderer/components/settings-credits-spec.tsx
index ddb2e30e..4ae6f5a9 100644
--- a/tests/renderer/components/settings-credits-spec.tsx
+++ b/tests/renderer/components/settings-credits-spec.tsx
@@ -1,7 +1,7 @@
+import * as React from 'react';
 import * as electron from 'electron';
-import { shallow } from 'enzyme';
 import * as fs from 'fs-extra';
-import * as React from 'react';
+import { shallow } from 'enzyme';
 
 import { CreditsSettings } from '../../../src/renderer/components/settings-credits';
 
@@ -9,10 +9,6 @@ jest.mock('fs-extra', () => ({
   readJSON: jest.fn(),
 }));
 
-jest.mock('../../../src/utils/import', () => ({
-  fancyImport: async (p: string) => require(p),
-}));
-
 describe('CreditsSettings component', () => {
   const mockContributors = [
     {
diff --git a/tests/renderer/components/settings-general-appearance-spec.tsx b/tests/renderer/components/settings-general-appearance-spec.tsx
index 0183e9ab..cf12aeb4 100644
--- a/tests/renderer/components/settings-general-appearance-spec.tsx
+++ b/tests/renderer/components/settings-general-appearance-spec.tsx
@@ -2,6 +2,7 @@ import { IItemRendererProps } from '@blueprintjs/select';
 import { shell } from 'electron';
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import * as fs from 'fs-extra';
 
 import {
   AppearanceSettings,
@@ -22,9 +23,6 @@ const doNothingFunc = () => {
 };
 
 jest.mock('fs-extra');
-jest.mock('../../../src/utils/import', () => ({
-  fancyImport: async (p: string) => require(p),
-}));
 
 jest.mock('../../../src/renderer/themes', () => ({
   THEMES_PATH: '~/.electron-fiddle/themes',
@@ -135,7 +133,6 @@ describe('AppearanceSettings component', () => {
 
   describe('createNewThemeFromCurrent()', () => {
     it('creates a new file from the current theme', async () => {
-      const fs = require('fs-extra');
       const wrapper = shallow(
         <AppearanceSettings
           appState={store}
@@ -148,7 +145,7 @@ describe('AppearanceSettings component', () => {
       expect(shell.showItemInFolder).toHaveBeenCalled();
       expect(fs.outputJSON).toHaveBeenCalled();
 
-      const args = fs.outputJSON.mock.calls[0];
+      const args = (fs.outputJSON as jest.Mock).mock.calls[0];
       expect(args[0].includes(`.electron-fiddle`)).toBe(true);
       expect(args[1].name).toBeDefined();
       expect(args[1].name === 'defaultDark').toBe(false);
@@ -157,7 +154,6 @@ describe('AppearanceSettings component', () => {
     });
 
     it('adds the newly created theme to the Themes dropdown', async () => {
-      const fs = require('fs-extra');
       const arr: Array<FiddleTheme> = [];
       (getAvailableThemes as jest.Mock).mockResolvedValue(arr);
       (fs.outputJSON as jest.Mock).mockImplementation(
diff --git a/tests/renderer/components/version-select-spec.tsx b/tests/renderer/components/version-select-spec.tsx
index bf359e49..394ba10e 100644
--- a/tests/renderer/components/version-select-spec.tsx
+++ b/tests/renderer/components/version-select-spec.tsx
@@ -16,7 +16,7 @@ import {
 import { ElectronReleaseChannel } from '../../../src/renderer/versions';
 import { mockVersions } from '../../mocks/electron-versions';
 
-const { ready, unknown, downloading } = VersionState;
+const { downloading, ready, unknown, unzipping } = VersionState;
 const { remote, local } = VersionSource;
 
 describe('VersionSelect component', () => {
@@ -133,14 +133,15 @@ describe('VersionSelect component', () => {
 
   describe('getItemIcon()', () => {
     it('returns the correct icon', () => {
-      const vDownloaded = { ...mockVersion1, state: ready };
-      expect(getItemIcon(vDownloaded)).toBe('saved');
-
-      const vDownloading = { ...mockVersion1, state: downloading };
-      expect(getItemIcon(vDownloading)).toBe('cloud-download');
-
-      const vUnknown = { ...mockVersion1, state: unknown };
-      expect(getItemIcon(vUnknown)).toBe('cloud');
+      const icons: Array<{ state: VersionState; expected: string }> = [
+        { state: downloading, expected: 'cloud-download' },
+        { state: ready, expected: 'saved' },
+        { state: unknown, expected: 'cloud' },
+        { state: unzipping, expected: 'compressed' },
+      ];
+      icons.forEach(({ state, expected }) => {
+        expect(getItemIcon({ ...mockVersion1, state })).toBe(expected);
+      });
     });
   });
 
diff --git a/tests/renderer/fetch-types-spec.ts b/tests/renderer/fetch-types-spec.ts
index 22720e56..0b3f8db2 100644
--- a/tests/renderer/fetch-types-spec.ts
+++ b/tests/renderer/fetch-types-spec.ts
@@ -1,3 +1,4 @@
+import * as fs from 'fs-extra';
 import {
   RunnableVersion,
   VersionSource,
@@ -24,13 +25,6 @@ jest.mock('extract-zip', () => {
 jest.mock('../../src/renderer/constants', () => ({
   USER_DATA_PATH: 'user/data/',
 }));
-jest.mock('../../src/utils/import', () => ({
-  fancyImport: async (p: string) => {
-    if (p === 'fs-extra') {
-      return require('fs-extra');
-    }
-  },
-}));
 
 describe('fetch-types', () => {
   const getMockResponse = (text: string) => ({
@@ -67,11 +61,10 @@ describe('fetch-types', () => {
   });
 
   describe('getOfflineTypeDefinitions()', () => {
-    it('returns true if they exist', async () => {
-      const fs = require('fs-extra');
-      fs.existsSync.mockReturnValueOnce(true);
+    it('returns true if they exist', () => {
+      (fs.existsSync as jest.Mock).mockReturnValueOnce(true);
 
-      const result = await getOfflineTypeDefinitions('3.0.0');
+      const result = getOfflineTypeDefinitions('3.0.0');
       const expected = path.join(
         'user/data/electron-typedef/3.0.0/electron.d.ts',
       );
@@ -80,11 +73,10 @@ describe('fetch-types', () => {
       expect(fs.existsSync).toHaveBeenCalledWith<any>(expected);
     });
 
-    it('returns false if they exist', async () => {
-      const fs = require('fs-extra');
-      fs.existsSync.mockReturnValueOnce(false);
+    it('returns false if they exist', () => {
+      (fs.existsSync as jest.Mock).mockReturnValueOnce(false);
 
-      const result = await getOfflineTypeDefinitions('3.0.0');
+      const result = getOfflineTypeDefinitions('3.0.0');
       const expected = path.join(
         'user/data/electron-typedef/3.0.0/electron.d.ts',
       );
@@ -102,8 +94,7 @@ describe('fetch-types', () => {
     };
 
     it('returns type definitions (and goes online if they do not exist)', async () => {
-      const fs = require('fs-extra');
-      fs.existsSync.mockReturnValueOnce(false);
+      (fs.existsSync as jest.Mock).mockReturnValueOnce(false);
       mockFetch();
 
       const result = await getDownloadedVersionTypeDefs(version);
@@ -117,8 +108,7 @@ describe('fetch-types', () => {
     });
 
     it('returns null if going online failed', async () => {
-      const fs = require('fs-extra');
-      fs.existsSync.mockReturnValueOnce(false);
+      (fs.existsSync as jest.Mock).mockReturnValueOnce(false);
       (window.fetch as jest.Mock).mockRejectedValue({});
 
       const result = await getDownloadedVersionTypeDefs(version);
@@ -132,8 +122,7 @@ describe('fetch-types', () => {
     });
 
     it('tries to save them to disk if downloaded', async () => {
-      const fs = require('fs-extra');
-      fs.existsSync.mockReturnValueOnce(false);
+      (fs.existsSync as jest.Mock).mockReturnValueOnce(false);
       mockFetch();
 
       await getDownloadedVersionTypeDefs(version);
@@ -148,10 +137,9 @@ describe('fetch-types', () => {
     });
 
     it('returns them even if saving them failed', async () => {
-      const fs = require('fs-extra');
       const def = `it's me, the type definition`;
-      fs.existsSync.mockReturnValueOnce(false);
-      fs.outputFile.mockImplementationOnce(() => Promise.reject());
+      (fs.existsSync as jest.Mock).mockReturnValueOnce(false);
+      (fs.outputFile as jest.Mock).mockRejectedValue(0);
       mockFetch();
 
       const result = await getDownloadedVersionTypeDefs(version);
@@ -164,10 +152,9 @@ describe('fetch-types', () => {
     });
 
     it('tries to read them from disk if downloaded', async () => {
-      const fs = require('fs-extra');
       const def = `it's me, the type definition`;
-      fs.existsSync.mockReturnValueOnce(true);
-      fs.readFile.mockImplementationOnce(() => Promise.resolve(def));
+      (fs.existsSync as jest.Mock).mockReturnValueOnce(true);
+      (fs.readFile as jest.Mock).mockResolvedValue(def);
       mockFetch();
 
       const result = await getDownloadedVersionTypeDefs(version);
@@ -180,9 +167,8 @@ describe('fetch-types', () => {
     });
 
     it('returns null if downloaded but not readable', async () => {
-      const fs = require('fs-extra');
-      fs.existsSync.mockReturnValueOnce(true);
-      fs.readFile.mockImplementationOnce(() => Promise.reject());
+      (fs.existsSync as jest.Mock).mockReturnValueOnce(true);
+      (fs.readFile as jest.Mock).mockRejectedValue(null);
       mockFetch();
 
       const result = await getDownloadedVersionTypeDefs(version);
@@ -207,10 +193,11 @@ describe('fetch-types', () => {
     });
 
     it('fetches a local path if localPath exists', async () => {
-      const fs = require('fs-extra');
       const def = `it's me, the type definition`;
-      fs.existsSync.mockReturnValueOnce(true);
-      fs.readFile.mockImplementationOnce(() => Promise.resolve(def));
+      (fs.existsSync as jest.Mock).mockReturnValueOnce(true);
+      (fs.readFile as jest.Mock).mockImplementationOnce(() =>
+        Promise.resolve(def),
+      );
       const result = await getLocalVersionTypeDefs(version);
       expect(result).toEqual(def);
     });
@@ -327,8 +314,7 @@ describe('fetch-types', () => {
 
   describe('removeTypeDefsForVersion()', () => {
     it("removes a version's typedefs", async () => {
-      const fs = require('fs-extra');
-      (fs.existsSync as jest.Mock<any>).mockReturnValue(true);
+      (fs.existsSync as jest.Mock).mockReturnValue(true);
       await removeTypeDefsForVersion('v3.0.0');
       expect(fs.remove).toHaveBeenCalledWith<any>(
         path.join(USER_DATA_PATH, 'electron-typedef', '3.0.0'),
@@ -336,9 +322,8 @@ describe('fetch-types', () => {
     });
 
     it('throws upon fs failure', async () => {
-      const fs = require('fs-extra');
-      (fs.existsSync as jest.Mock<any>).mockReturnValue(true);
-      (fs.remove as jest.Mock<any>).mockImplementation(() => {
+      (fs.existsSync as jest.Mock).mockReturnValue(true);
+      (fs.remove as jest.Mock).mockImplementation(() => {
         throw new Error('Bwap bwap');
       });
 
diff --git a/tests/renderer/file-manager-spec.ts b/tests/renderer/file-manager-spec.ts
index 2eda8cf4..b64656d0 100644
--- a/tests/renderer/file-manager-spec.ts
+++ b/tests/renderer/file-manager-spec.ts
@@ -1,3 +1,5 @@
+import * as fs from 'fs-extra';
+
 import { Files } from '../../src/interfaces';
 import { IpcEvents } from '../../src/ipc-events';
 import { FileManager } from '../../src/renderer/file-manager';
@@ -20,10 +22,6 @@ jest.mock('../../src/renderer/templates', () => ({
   }),
 }));
 
-jest.mock('../../src/utils/import', () => ({
-  fancyImport: async (p: string) => require(p),
-}));
-
 describe('FileManager', () => {
   let fm: FileManager;
 
@@ -58,7 +56,6 @@ describe('FileManager', () => {
     });
 
     it('writes empty strings if readFile throws an error', async () => {
-      const fs = require('fs-extra');
       (fs.readFile as jest.Mock).mockImplementation(() => {
         throw new Error('bwap');
       });
@@ -99,23 +96,18 @@ describe('FileManager', () => {
 
   describe('saveFiddle()', () => {
     it('saves all non-empty files in Fiddle', async () => {
-      const fs = require('fs-extra');
-
       await fm.saveFiddle('/fake/path');
 
       expect(fs.outputFile).toHaveBeenCalledTimes(5);
     });
 
     it('removes a file that is newly empty', async () => {
-      const fs = require('fs-extra');
-
       await fm.saveFiddle('/fake/path');
 
       expect(fs.remove).toHaveBeenCalledTimes(1);
     });
 
     it('handles an error (output)', async () => {
-      const fs = require('fs-extra');
       (fs.outputFile as jest.Mock).mockImplementation(() => {
         throw new Error('bwap');
       });
@@ -127,7 +119,6 @@ describe('FileManager', () => {
     });
 
     it('handles an error (remove)', async () => {
-      const fs = require('fs-extra');
       (fs.remove as jest.Mock).mockImplementation(() => {
         throw new Error('bwap');
       });
@@ -161,7 +152,6 @@ describe('FileManager', () => {
 
   describe('saveToTemp()', () => {
     it('saves as a local fiddle', async () => {
-      const fs = require('fs-extra');
       const tmp = require('tmp');
 
       await fm.saveToTemp({
@@ -174,7 +164,6 @@ describe('FileManager', () => {
     });
 
     it('throws an error', async () => {
-      const fs = require('fs-extra');
       (fs.outputFile as jest.Mock).mockImplementation(() => {
         throw new Error('bwap');
       });
@@ -221,8 +210,7 @@ describe('FileManager', () => {
 
   describe('cleanup()', () => {
     it('attempts to remove a directory if it exists', async () => {
-      const fs = require('fs-extra');
-      fs.existsSync.mockReturnValueOnce(true);
+      (fs.existsSync as jest.Mock).mockReturnValueOnce(true);
 
       const result = await fm.cleanup('/fake/dir');
 
@@ -231,8 +219,7 @@ describe('FileManager', () => {
     });
 
     it('does not attempt to remove a directory if it does not exists', async () => {
-      const fs = require('fs-extra');
-      fs.existsSync.mockReturnValueOnce(false);
+      (fs.existsSync as jest.Mock).mockReturnValueOnce(false);
 
       const result = await fm.cleanup('/fake/dir');
 
@@ -241,9 +228,8 @@ describe('FileManager', () => {
     });
 
     it('handles an error', async () => {
-      const fs = require('fs-extra');
-      fs.existsSync.mockReturnValueOnce(true);
-      fs.remove.mockReturnValueOnce(Promise.reject('bwapbwap'));
+      (fs.existsSync as jest.Mock).mockReturnValueOnce(true);
+      (fs.remove as jest.Mock).mockReturnValueOnce(Promise.reject('bwapbwap'));
 
       const result = await fm.cleanup('/fake/dir');
 
diff --git a/tests/renderer/npm-spec.ts b/tests/renderer/npm-spec.ts
index ec7c4481..5081f67c 100644
--- a/tests/renderer/npm-spec.ts
+++ b/tests/renderer/npm-spec.ts
@@ -13,11 +13,6 @@ import { overridePlatform, resetPlatform } from '../utils';
 import MockDecommentWorker from '../mocks/worker';
 jest.mock('decomment');
 jest.mock('../../src/utils/exec');
-jest.mock('../../src/utils/import', () => ({
-  fancyImport: async (_p: string) => {
-    return { default: require('builtin-modules') };
-  },
-}));
 
 window.Worker = MockDecommentWorker;
 
diff --git a/tests/renderer/themes-spec.tsx b/tests/renderer/themes-spec.tsx
index 00551317..f473b7a4 100644
--- a/tests/renderer/themes-spec.tsx
+++ b/tests/renderer/themes-spec.tsx
@@ -1,3 +1,4 @@
+import * as fs from 'fs-extra';
 import * as path from 'path';
 
 import {
@@ -9,9 +10,6 @@ import {
 import { DefaultThemes } from '../../src/renderer/themes-defaults';
 
 jest.mock('fs-extra');
-jest.mock('../../src/utils/import', () => ({
-  fancyImport: async (p: string) => require(p),
-}));
 
 describe('themes', () => {
   describe('activateTheme()', () => {
@@ -43,10 +41,12 @@ describe('themes', () => {
     });
 
     it('reads the themes folder for themes', async () => {
-      const fs = require('fs-extra');
-      fs.existsSync.mockReturnValue(true);
-      fs.readdir.mockReturnValueOnce(['test-theme1.json', 'test-theme2.json']);
-      fs.readJSON.mockReturnValue({ test: true });
+      (fs.existsSync as jest.Mock).mockReturnValue(true);
+      (fs.readdir as jest.Mock).mockReturnValueOnce([
+        'test-theme1.json',
+        'test-theme2.json',
+      ]);
+      (fs.readJSON as jest.Mock).mockReturnValue({ test: true });
 
       const themes = await getAvailableThemes();
 
@@ -64,19 +64,21 @@ describe('themes', () => {
     });
 
     it('handles a readdir error', async () => {
-      const fs = require('fs-extra');
-      fs.existsSync.mockReturnValueOnce(true);
-      fs.readdir.mockImplementationOnce(() => Promise.reject('Bwap'));
+      (fs.existsSync as jest.Mock).mockReturnValueOnce(true);
+      (fs.readdir as jest.Mock).mockImplementationOnce(() =>
+        Promise.reject('Bwap'),
+      );
 
       const themes = await getAvailableThemes();
       expect(themes).toHaveLength(2);
     });
 
     it('handles a readJSON error', async () => {
-      const fs = require('fs-extra');
-      fs.existsSync.mockReturnValueOnce(true);
-      fs.readdir.mockImplementationOnce(() => ['hi']);
-      fs.readJSON.mockImplementationOnce(() => Promise.reject('Bwap'));
+      (fs.existsSync as jest.Mock).mockReturnValueOnce(true);
+      (fs.readdir as jest.Mock).mockImplementationOnce(() => ['hi']);
+      (fs.readJSON as jest.Mock).mockImplementationOnce(() =>
+        Promise.reject('Bwap'),
+      );
 
       const themes = await getAvailableThemes();
       expect(themes).toHaveLength(2);
@@ -95,16 +97,19 @@ describe('themes', () => {
     });
 
     it('returns a named theme', async () => {
-      const fs = require('fs-extra');
-      fs.readJSON.mockReturnValue({ name: 'Test', common: { test: true } });
+      (fs.readJSON as jest.Mock).mockReturnValue({
+        name: 'Test',
+        common: { test: true },
+      });
 
       const theme = await getTheme('test');
       expect(theme.name).toBe('Test');
     });
 
     it('handles a read error', async () => {
-      const fs = require('fs-extra');
-      fs.readJSON.mockImplementationOnce(() => Promise.reject('Bwap'));
+      (fs.readJSON as jest.Mock).mockImplementationOnce(() =>
+        Promise.reject('Bwap'),
+      );
 
       const theme = await getTheme('test');
       expect(theme.name).toBe('Fiddle (Dark)');
@@ -128,8 +133,7 @@ describe('themes', () => {
     });
 
     it('reads the right file if ends with .json', async () => {
-      const fs = require('fs-extra');
-      fs.readJSON.mockReturnValueOnce({});
+      (fs.readJSON as jest.Mock).mockReturnValueOnce({});
 
       const theme = await readThemeFile('myfile.json');
       const expected = path.normalize(`~/.electron-fiddle/themes/myfile.json`);
@@ -139,8 +143,7 @@ describe('themes', () => {
     });
 
     it('reads the right file if does not end with .json', async () => {
-      const fs = require('fs-extra');
-      fs.readJSON.mockReturnValueOnce({});
+      (fs.readJSON as jest.Mock).mockReturnValueOnce({});
 
       const theme = await readThemeFile('myfile');
       const expected = path.normalize(`~/.electron-fiddle/themes/myfile.json`);
diff --git a/tests/utils/fancy-import-spec.ts b/tests/utils/fancy-import-spec.ts
deleted file mode 100644
index 8eac0921..00000000
--- a/tests/utils/fancy-import-spec.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { fancyImport } from '../../src/utils/import';
-
-describe('fancyImport()', () => {
-  it('imports es6 modules', async () => {
-    const name = 'fs';
-    const required = require(name);
-    const imported: any = await fancyImport(name);
-
-    // https://www.typescriptlang.org/tsconfig#esModuleInterop
-    // can inject a 'default' for compatibility, so the imports may
-    // differ from require in that respect. Remove that from the equation before comparing.
-    const importedMinusDefault = { ...imported };
-    delete importedMinusDefault.default;
-
-    expect(required).toEqual(importedMinusDefault);
-  });
-
-  it('imports cjs single-function modules', async () => {
-    const name = 'decompress';
-    const required = require(name);
-    const imported: any = await fancyImport(name);
-    expect(required).toEqual(imported.default);
-  });
-});
diff --git a/tests/utils/read-fiddle-spec.ts b/tests/utils/read-fiddle-spec.ts
index 688d5f63..5131c1c4 100644
--- a/tests/utils/read-fiddle-spec.ts
+++ b/tests/utils/read-fiddle-spec.ts
@@ -1,13 +1,11 @@
 import * as path from 'path';
-import * as fsType from 'fs-extra';
-import { fancyImport } from '../../src/utils/import';
+import * as fs from 'fs-extra';
 import { MAIN_JS_NAME } from '../../src/shared-constants';
 
 import { readFiddle } from '../../src/utils/read-fiddle';
 
 describe('read-fiddle', () => {
   beforeEach(async () => {
-    const fs = await fancyImport<typeof fsType>('fs-extra');
     (fs.existsSync as jest.Mock).mockImplementationOnce(() => true);
     (fs.readdirSync as jest.Mock).mockImplementationOnce(() => [
       'LICENSE.txt',
@@ -16,7 +14,6 @@ describe('read-fiddle', () => {
   });
 
   it('reads known content', async () => {
-    const fs = await fancyImport<typeof fsType>('fs-extra');
     (fs.readFileSync as jest.Mock).mockImplementation((filename) =>
       path.basename(filename),
     );
@@ -36,7 +33,6 @@ describe('read-fiddle', () => {
   });
 
   it('handles read errors gracefully', async () => {
-    const fs = await fancyImport<typeof fsType>('fs-extra');
     (fs.readFileSync as jest.Mock).mockImplementation(() => {
       throw new Error('bwap');
     });
@@ -56,7 +52,6 @@ describe('read-fiddle', () => {
   });
 
   it('ensures truty even when read returns null', async () => {
-    const fs = await fancyImport<typeof fsType>('fs-extra');
     (fs.readFileSync as jest.Mock).mockImplementation(() => null);
     console.warn = jest.fn();
     const folder = '/some/place';