/tests/mocks/styles.ts',
26 | },
27 | };
28 |
29 | export default config;
30 |
--------------------------------------------------------------------------------
/rtl-spec/components/commands-version-chooser.spec.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { render, screen, waitFor } from '@testing-library/react';
4 | import { userEvent } from '@testing-library/user-event';
5 |
6 | import { VersionChooser } from '../../src/renderer/components/commands-version-chooser';
7 | import { AppState } from '../../src/renderer/state';
8 | import { mockVersion1, prepareAppState } from '../test-utils/versions';
9 |
10 | describe('VersionSelect component', () => {
11 | let appState: AppState;
12 |
13 | beforeEach(() => {
14 | appState = prepareAppState();
15 |
16 | // the version selector is disabled when bisecting
17 | appState.Bisector = undefined;
18 | });
19 |
20 | it('selects a new version', async () => {
21 | const { getByRole } = render();
22 |
23 | const btnOpenVersionSelector = getByRole('button');
24 |
25 | await userEvent.click(btnOpenVersionSelector);
26 |
27 | const versionButton = screen.getByText(mockVersion1.version);
28 |
29 | await userEvent.click(versionButton);
30 |
31 | waitFor(() =>
32 | expect(btnOpenVersionSelector.textContent).toContain(
33 | mockVersion1.version,
34 | ),
35 | );
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/rtl-spec/components/editors-non-ideal-state.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { render } from '@testing-library/react';
4 | import { userEvent } from '@testing-library/user-event';
5 |
6 | import { RenderNonIdealState } from '../../src/renderer/components/editors-non-ideal-state';
7 | import { EditorMosaic } from '../../src/renderer/editor-mosaic';
8 |
9 | describe('RenderNonIdealState component', () => {
10 | let editorMosaic: EditorMosaic;
11 |
12 | beforeEach(() => {
13 | ({ editorMosaic } = window.app.state);
14 | });
15 |
16 | it('renders a non-ideal state', () => {
17 | const { getByText } = render(
18 | ,
19 | );
20 |
21 | expect(getByText('Reset editors')).toBeInTheDocument();
22 | });
23 |
24 | it('handles a click', async () => {
25 | const resetLayoutSpy = jest.spyOn(editorMosaic, 'resetLayout');
26 | const { getByRole } = render(
27 | ,
28 | );
29 | await userEvent.click(getByRole('button'));
30 |
31 | expect(resetLayoutSpy).toHaveBeenCalledTimes(1);
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/rtl-spec/test-utils/renderClassComponentWithInstanceRef.ts:
--------------------------------------------------------------------------------
1 | import { Component, ComponentClass, createElement, createRef } from 'react';
2 |
3 | import { render } from '@testing-library/react';
4 |
5 | type ComponentConstructor = new (
6 | props: P,
7 | ) => Component
;
8 |
9 | /**
10 | * Renders a class component and returns the render result alongside the
11 | * component's instance.
12 | */
13 | export function renderClassComponentWithInstanceRef<
14 | C extends ComponentConstructor = ComponentConstructor,
15 | P extends C extends ComponentClass
16 | ? Props
17 | : never = C extends ComponentClass ? Props : never,
18 | I = InstanceType,
19 | >(
20 | ClassComponent: C,
21 | props: P,
22 | ): {
23 | instance: I;
24 | renderResult: ReturnType;
25 | } {
26 | // Hack: unlike Enzyme, RTL doesn't expose class components' instances, so we
27 | // need to improvise and pass a `ref` to get access to this instance.
28 | const ref = createRef();
29 |
30 | const renderResult = render(
31 | createElement(ClassComponent, {
32 | ...props,
33 | ref,
34 | }),
35 | );
36 |
37 | return {
38 | instance: ref.current!,
39 | renderResult,
40 | };
41 | }
42 |
--------------------------------------------------------------------------------
/rtl-spec/test-utils/versions.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ElectronReleaseChannel,
3 | InstallState,
4 | VersionSource,
5 | } from '../../src/interfaces';
6 | import { VersionsMock } from '../../tests/mocks/electron-versions';
7 | import { StateMock } from '../../tests/mocks/state';
8 |
9 | const { missing } = InstallState;
10 | const { remote } = VersionSource;
11 |
12 | export const mockVersion1 = {
13 | source: remote,
14 | state: missing,
15 | version: '26.0.0',
16 | };
17 |
18 | export const mockVersion2 = {
19 | source: remote,
20 | state: missing,
21 | version: '28.0.0-unsupported',
22 | };
23 |
24 | /**
25 | * Initializes the app state with our mock versions.
26 | */
27 | export function prepareAppState() {
28 | const { state: appState } = window.app;
29 |
30 | const { mockVersions } = new VersionsMock();
31 |
32 | (appState as unknown as StateMock).initVersions('2.0.2', {
33 | ...mockVersions,
34 | [mockVersion1.version]: { ...mockVersion1 },
35 | [mockVersion2.version]: { ...mockVersion2 },
36 | });
37 |
38 | appState.channelsToShow = [
39 | ElectronReleaseChannel.stable,
40 | ElectronReleaseChannel.beta,
41 | ];
42 |
43 | return appState;
44 | }
45 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const SENTRY_DSN =
2 | 'https://966a5b01ac8d4941b81e4ebd0ab4c991@sentry.io/1882540';
3 |
4 | export const ELECTRON_DTS = 'electron.d.ts';
5 |
--------------------------------------------------------------------------------
/src/less/blueprint.less:
--------------------------------------------------------------------------------
1 | @import (inline) "~@blueprintjs/core/lib/css/blueprint.css";
2 | @import (inline) "~@blueprintjs/select/lib/css/blueprint-select.css";
3 | @import (inline) "~@blueprintjs/icons/lib/css/blueprint-icons.css";
4 | @import (inline) "~@blueprintjs/popover2/lib/css/blueprint-popover2.css";
5 | @import "~@blueprintjs/core/lib/less/variables.less";
6 |
7 | // Override some of the colors
8 | .fiddle.bp3-dark {
9 | .bp3-control input:focus ~ .bp3-control-indicator {
10 | outline: @blue3 solid 2px;
11 | }
12 |
13 | .bp3-menu,
14 | .bp3-popover .bp3-popover-content, .bp3-popover2 .bp3-popover2-content {
15 | background-color: @background-1;
16 | }
17 |
18 | .bp3-popover .bp3-popover-arrow-fill, .bp3-popover2 .bp3-popover2-arrow-fill {
19 | fill: @background-1;
20 | }
21 |
22 | .bp3-button:not([class*="bp3-intent-"]) {
23 | background-color: @background-1;
24 | }
25 |
26 | .bp3-button.bp3-minimal {
27 | background-color: unset;
28 | }
29 |
30 | .bp3-button:hover,
31 | .bp3-button.bp3-minimal:hover {
32 | background-color: rgba(138, 155, 168, 0.15);
33 | }
34 |
35 | .bp3-button:focus{
36 | outline: @blue3 solid 2px;
37 | }
38 |
39 | .bp3-menu-item.bp3-active.bp3-intent-primary {
40 | background-color: @foreground-3;
41 | }
42 |
43 | .bp3-dialog {
44 | background-color: @background-4;
45 |
46 | .bp3-dialog-header {
47 | background-color: @background-3;
48 | }
49 | }
50 |
51 | .bp3-running-text {
52 | font-size: 14px;
53 | }
54 |
55 | .bp3-alert-contents {
56 | width: 100%;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/less/components/editors.less:
--------------------------------------------------------------------------------
1 | .mosaic-tile {
2 | animation: fadein 0.15s;
3 | }
4 |
5 | .mosaic-window-toolbar {
6 | & > div {
7 | display: flex;
8 | justify-content: space-between;
9 | width: 100%;
10 | padding: 0 5px 0 5px;
11 |
12 | .mosaic-controls {
13 | transform: scale(0.75);
14 | transform-origin: right;
15 | }
16 |
17 | h5 {
18 | margin: 4px 0 0 0;
19 | }
20 |
21 | button {
22 | margin-left: 8px;
23 | }
24 | }
25 | }
26 |
27 | // TODO: support new file
28 | // update if list of Editor ID changes
29 | @editor-ids: main\.js, renderer\.js, index\.html, preload\.js, styles\.css;
30 |
31 | // takes each editor ID and increase its z-index if the parent Mosaic root
32 | // has focused__id class for that specific id.
33 | each(@editor-ids, {
34 | .focused__@{value}.mosaic .mosaic-window.@{value} {
35 | z-index: 2;
36 | }
37 | });
38 |
39 | .mosaic-window {
40 | z-index: 1;
41 | }
42 |
43 | @keyframes fadein {
44 | from { opacity: 0; }
45 | to { opacity: 1; }
46 | }
47 |
--------------------------------------------------------------------------------
/src/less/components/mosaic.less:
--------------------------------------------------------------------------------
1 | @import "../variables.less";
2 |
3 | .fiddle {
4 | .mosaic-window-toolbar {
5 | background: var(--background-1);
6 | box-shadow: inset 0 0 100px 100px rgba(53, 53, 53, 0.1);
7 | border-top: 1px solid rgba(255, 255, 255, 0.1);
8 | }
9 |
10 | .mosaic-window-toolbar.draggable:hover {
11 | box-shadow: inset 0 0 100px 100px rgba(255, 255, 255, 0.1);
12 | }
13 |
14 | .mosaic-window-title {
15 | color: var(--text-color-3);
16 | }
17 |
18 | button.mosaic-default-control {
19 | padding: 0;
20 | height: 20px;
21 | width: 20px;
22 | margin: 5px;
23 | background-color: #314246;
24 | border: 0.1rem solid #222b35;
25 | }
26 |
27 | button.mosaic-default-control:hover {
28 | background-color: #131d1f;
29 | }
30 |
31 | .mosaic-window-body {
32 | display: flex;
33 | flex-direction: row;
34 | flex-wrap: nowrap;
35 | align-items: stretch;
36 | align-content: stretch;
37 | overflow: visible;
38 | z-index: 4;
39 | background-color: @background-4;
40 | }
41 |
42 | .mosaic.mosaic-blueprint-theme {
43 | background: #bdbdbd15;
44 | }
45 |
46 | .editorContainer,
47 | .monaco-editor {
48 | width: 100%;
49 | height: 100%;
50 | }
51 |
52 | .editorContainer {
53 | background: @background-1;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/less/components/output.less:
--------------------------------------------------------------------------------
1 | @import '../variables.less';
2 |
3 | .output {
4 | font-size: 12px;
5 | font-family: @fonts-common;
6 | font-weight: 600;
7 | top: 10vh;
8 | padding: 0;
9 | height: 100%;
10 | -webkit-app-region: no-drag;
11 |
12 | .monaco-editor {
13 | color: var(--text-color-1);
14 | box-shadow: inset 0 0 100px 100px rgba(0, 0, 0, 0.15);
15 | border-top: @border;
16 |
17 | .margin,
18 | .monaco-editor-background {
19 | background: var(--background-2);
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/less/components/show-me.less:
--------------------------------------------------------------------------------
1 | .show-me-panel {
2 | padding: 10px;
3 | overflow: scroll;
4 |
5 | & > *:first-child {
6 | margin-top: 0;
7 | };
8 |
9 | .bp3-callout {
10 | margin-bottom: 10px;
11 | }
12 | }
13 |
14 | .show-me-list {
15 | display: flex;
16 | flex-direction: column;
17 | flex-wrap: wrap;
18 | list-style: none;
19 | padding: 0;
20 | max-height: 400px;
21 | }
22 |
--------------------------------------------------------------------------------
/src/less/components/sidebar.less:
--------------------------------------------------------------------------------
1 | #new-file-input {
2 | box-shadow: 0 0 0 1600px rgba(0, 0, 0, 0.1);
3 | border-radius: 0;
4 | border-left: 4px solid white;
5 | font-size: 14px;
6 | }
7 |
8 | .add-file-input .bp3-tree-node-label {
9 | overflow: visible;
10 | }
11 |
12 | .bp3-tree-node-caret-none {
13 | min-width: 8px;
14 | }
15 |
16 | .bp3-tree-node-content-1 {
17 | padding-left: 0;
18 | }
19 |
20 | .pointer {
21 | cursor: pointer;
22 | }
23 |
24 | .package-manager-result {
25 | em {
26 | font-weight: bold;
27 | }
28 | }
29 |
30 | .package-tree {
31 | .bp3-tree-node-content {
32 | .bp3-tree-node-secondary-label {
33 | min-width: 100px;
34 | }
35 | }
36 |
37 | .bp3-tree-node-list {
38 | margin: 5px 0;
39 | }
40 | }
41 |
42 | .package-tree-version-select {
43 | font-size: 10px;
44 | width: 60px;
45 | text-overflow: 'ellipsis';
46 | background: @background-1;
47 | color: @dark-gray1;
48 |
49 | .bp3-dark & {
50 | color: @white;
51 | }
52 | }
53 |
54 | .fiddle-scrollbar {
55 | overflow: auto;
56 | }
57 |
58 | .fiddle-scrollbar::-webkit-scrollbar {
59 | width: 8px;
60 | }
61 |
62 | .fiddle-scrollbar::-webkit-scrollbar-thumb {
63 | border-radius: 4px;
64 | background-color: rgba(172, 172, 172, 0.6);
65 |
66 | .bp3-dark & {
67 | background-color: rgba(172, 172, 172, 0.4);
68 | }
69 | }
70 |
71 | .fiddle-scrollbar::-webkit-scrollbar-track {
72 | background-color: transparent;
73 | }
74 |
--------------------------------------------------------------------------------
/src/less/components/tour.less:
--------------------------------------------------------------------------------
1 | .tour {
2 | position: fixed;
3 | top: 0;
4 | left: 0;
5 | height: 100vh;
6 | width: 100vw;
7 | z-index: 200;
8 | }
9 |
10 | .tour-portal {
11 | z-index: 1000;
12 | }
--------------------------------------------------------------------------------
/src/less/components/version-select.less:
--------------------------------------------------------------------------------
1 | .bp3-fill {
2 | #version-chooser {
3 | width: 100%;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/less/container.less:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: column;
4 | height: 100vh;
5 | width: 100vw;
6 | margin: 0;
7 | padding: 0;
8 | max-width: unset;
9 | }
10 |
--------------------------------------------------------------------------------
/src/less/main.less:
--------------------------------------------------------------------------------
1 | :root {
2 | color-scheme: light dark;
3 | }
4 |
5 | body {
6 | padding: 0;
7 | margin: 0;
8 | background: @background-1;
9 | overflow: hidden;
10 | display: flex;
11 | flex-direction: column;
12 | height: 100vh;
13 | }
14 |
15 | .fiddle {
16 | .drag {
17 | -webkit-app-region: drag;
18 | }
19 |
20 | .shadow {
21 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
22 | }
23 |
24 | .timestamp {
25 | margin-right: 5px;
26 | color: var(--text-color-1);
27 | opacity: 0.7;
28 | user-select: none;
29 | }
30 | }
31 |
32 | #editors {
33 | display: flex;
34 | flex-flow: row nowrap;
35 | align-items: stretch;
36 | align-content: stretch;
37 | height: 100%;
38 | }
39 |
40 | .editor {
41 | flex-grow: 1;
42 | width: 30%;
43 | }
44 |
45 | .resize {
46 | width: 6px;
47 | background: rgba(0, 0, 0, 0.2);
48 | margin-left: 9px;
49 | margin-right: 0;
50 | cursor: col-resize;
51 | flex-grow: 0;
52 | flex-shrink: 0;
53 | }
54 |
55 | #runner {
56 | height: 0;
57 | }
58 |
59 | #runner:not(:empty) {
60 | height: 200px;
61 | }
62 |
63 | /* stylelint-disable-next-line selector-class-pattern */
64 | .editorTitle span {
65 | width: 33%;
66 | display: inline-block;
67 | text-align: center;
68 | font-size: 12px;
69 | }
70 |
71 | .tabbing-hidden {
72 | visibility: hidden;
73 | }
74 |
--------------------------------------------------------------------------------
/src/less/root.less:
--------------------------------------------------------------------------------
1 | // Blueprint
2 | @import "blueprint.less";
3 |
4 | // Variables, also imported in each file
5 | @import "variables.less";
6 | @import "main.less";
7 | @import "container.less";
8 | @import "mosaic-vendor.less";
9 |
10 | // Components
11 | @import "components/commands.less";
12 | @import "components/output.less";
13 | @import "components/dialogs.less";
14 | @import "components/mosaic.less";
15 | @import "components/settings.less";
16 | @import "components/editors.less";
17 | @import "components/tour.less";
18 | @import "components/show-me.less";
19 | @import "components/version-select.less";
20 | @import "components/sidebar.less";
21 |
--------------------------------------------------------------------------------
/src/less/variables.less:
--------------------------------------------------------------------------------
1 | // We'll take these values from the theme.
2 |
3 | // Colors
4 | @foreground-1: var(--foreground-1);
5 | @foreground-2: var(--foreground-2);
6 | @foreground-3: var(--foreground-3);
7 | @background-4: var(--background-4);
8 | @background-3: var(--background-3);
9 | @background-2: var(--background-2);
10 | @background-1: var(--background-1);
11 | @border-color-2: var(--border-color-2);
12 | @border-color-1: var(--border-color-1);
13 | @border: var(--border);
14 | @text-color-1: var(--text-color-1);
15 | @text-color-2: var(--text-color-2);
16 | @text-color-3: var(--text-color-3);
17 |
18 | // Dynamically created
19 | @button-text-color: var(--button-text-color);
20 |
21 | // Fonts
22 | @fonts-common: var(--fonts-common);
23 |
24 | // Lengths
25 | @header-height: 50px;
26 |
--------------------------------------------------------------------------------
/src/main/about-panel.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'node:path';
2 |
3 | import { app } from 'electron';
4 |
5 | import { Contributor } from 'src/interfaces';
6 |
7 | import contributorsJSON from '../../static/contributors.json';
8 |
9 | /**
10 | * Sets Fiddle's About panel options on Linux and macOS
11 | */
12 | export function setupAboutPanel(): void {
13 | const contributors: Array = [];
14 | contributorsJSON.forEach((userData: Contributor) => {
15 | if (userData.name !== null && userData.name !== undefined) {
16 | contributors.push(userData.name);
17 | }
18 | });
19 |
20 | const iconPath = path.resolve(__dirname, '../assets/icons/fiddle.png');
21 |
22 | app.setAboutPanelOptions({
23 | applicationName: 'Electron Fiddle',
24 | applicationVersion: app.getVersion(),
25 | authors: contributors,
26 | copyright: '© Electron Authors',
27 | credits: 'https://github.com/electron/fiddle/graphs/contributors',
28 | iconPath,
29 | version: process.versions.electron,
30 | website: 'https://electronjs.org/fiddle',
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/constants.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'node:path';
2 |
3 | import { app } from 'electron';
4 |
5 | export const STATIC_DIR = path.resolve(__dirname, '../static');
6 |
7 | export const ELECTRON_DOWNLOAD_PATH = path.join(
8 | app.getPath('userData'),
9 | 'electron-bin',
10 | );
11 | export const ELECTRON_INSTALL_PATH = path.join(
12 | ELECTRON_DOWNLOAD_PATH,
13 | 'current',
14 | );
15 |
--------------------------------------------------------------------------------
/src/main/devtools.ts:
--------------------------------------------------------------------------------
1 | import { isDevMode } from './utils/devmode';
2 |
3 | /**
4 | * Installs developer tools if we're in dev mode.
5 | */
6 | export async function setupDevTools(): Promise {
7 | if (!isDevMode()) return;
8 |
9 | const {
10 | default: installExtension,
11 | REACT_DEVELOPER_TOOLS,
12 | } = require('electron-devtools-installer');
13 |
14 | try {
15 | const react = await installExtension(REACT_DEVELOPER_TOOLS, {
16 | loadExtensionOptions: { allowFileAccess: true },
17 | });
18 | console.log(`installDevTools: Installed ${react}`);
19 | } catch (error) {
20 | console.warn(`installDevTools: Error occurred:`, error);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/first-run.ts:
--------------------------------------------------------------------------------
1 | import { app, dialog } from 'electron';
2 |
3 | import { isFirstRun } from './utils/check-first-run';
4 | import { isDevMode } from './utils/devmode';
5 |
6 | /**
7 | * Is this the first run of Fiddle? If so, perform
8 | * tasks that we only want to do in this case.
9 | */
10 | export async function onFirstRunMaybe() {
11 | if (isFirstRun()) {
12 | await promptMoveToApplicationsFolder();
13 | }
14 | }
15 |
16 | /**
17 | * Ask the user if the app should be moved to the
18 | * applications folder.
19 | */
20 | async function promptMoveToApplicationsFolder(): Promise {
21 | if (process.platform !== 'darwin') return;
22 | if (isDevMode() || app.isInApplicationsFolder()) return;
23 |
24 | const { response } = await dialog.showMessageBox({
25 | type: 'question',
26 | buttons: ['Move to Applications Folder', 'Do Not Move'],
27 | defaultId: 0,
28 | message: 'Move to Applications Folder?',
29 | });
30 |
31 | if (response === 0) {
32 | app.moveToApplicationsFolder();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/sentry.ts:
--------------------------------------------------------------------------------
1 | import * as Sentry from '@sentry/electron/main';
2 |
3 | import { isDevMode } from './utils/devmode';
4 | import { SENTRY_DSN } from '../constants';
5 |
6 | export function initSentry() {
7 | if (!isDevMode()) {
8 | Sentry.init({ dsn: SENTRY_DSN });
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/squirrel.ts:
--------------------------------------------------------------------------------
1 | export function shouldQuit() {
2 | return require('electron-squirrel-startup');
3 | }
4 |
--------------------------------------------------------------------------------
/src/main/templates.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'node:path';
2 |
3 | import { IpcMainEvent } from 'electron';
4 |
5 | import { STATIC_DIR } from './constants';
6 | import { ipcMainManager } from './ipc';
7 | import { readFiddle } from './utils/read-fiddle';
8 | import { EditorValues } from '../interfaces';
9 | import { IpcEvents } from '../ipc-events';
10 |
11 | /**
12 | * Returns expected content for a given name.
13 | */
14 | export function getTemplateValues(name: string): Promise {
15 | const templatePath = path.join(STATIC_DIR, 'show-me', name.toLowerCase());
16 |
17 | return readFiddle(templatePath);
18 | }
19 |
20 | export function setupTemplates() {
21 | ipcMainManager.handle(
22 | IpcEvents.GET_TEMPLATE_VALUES,
23 | (_: IpcMainEvent, name: string) => getTemplateValues(name),
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/update.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Sets up the update service
3 | */
4 | export function setupUpdates() {
5 | // We delay this work by 10s to ensure that the
6 | // app doesn't have to worry about updating during launch
7 | setTimeout(() => {
8 | const { updateElectronApp } = require('update-electron-app');
9 |
10 | updateElectronApp({
11 | repo: 'electron/fiddle',
12 | updateInterval: '1 hour',
13 | });
14 | }, 10000);
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/utils/check-first-run.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'node:path';
2 |
3 | import { app } from 'electron';
4 | import * as fs from 'fs-extra';
5 |
6 | const getConfigPath = () => {
7 | const userDataPath = app.getPath('userData');
8 | return path.join(userDataPath, 'FirstRun', 'electron-app-first-run');
9 | };
10 |
11 | /**
12 | * Whether or not the app is being run for
13 | * the first time
14 | */
15 | export function isFirstRun(): boolean {
16 | const configPath = getConfigPath();
17 |
18 | try {
19 | if (fs.existsSync(configPath)) {
20 | return false;
21 | }
22 |
23 | fs.outputFileSync(configPath, '');
24 | } catch (error) {
25 | console.warn(`First run: Unable to write firstRun file`, error);
26 | }
27 |
28 | return true;
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/utils/devmode.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Are we currently running in development mode?
3 | */
4 | export function isDevMode(): boolean {
5 | return !!process.defaultApp;
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/utils/exec.ts:
--------------------------------------------------------------------------------
1 | import { exec as cp_exec } from 'node:child_process';
2 | import { promisify } from 'node:util';
3 |
4 | import shellEnv from 'shell-env';
5 |
6 | /**
7 | * On macOS & Linux, we need to fix the $PATH environment variable
8 | * so that we can call `npm`.
9 | */
10 | export const maybeFixPath = (() => {
11 | // Singleton: We don't want to do this more than once.
12 | let _shellPathCalled = false;
13 |
14 | return async (): Promise => {
15 | if (_shellPathCalled) {
16 | return;
17 | }
18 |
19 | if (process.platform !== 'win32') {
20 | const { PATH } = await shellEnv();
21 | if (PATH) {
22 | process.env.PATH = PATH;
23 | }
24 | }
25 |
26 | _shellPathCalled = true;
27 | };
28 | })();
29 |
30 | /**
31 | * Execute a command in a directory.
32 | */
33 | export async function exec(dir: string, cliArgs: string): Promise {
34 | await maybeFixPath();
35 |
36 | const { stdout } = await promisify(cp_exec)(cliArgs, {
37 | cwd: dir,
38 | maxBuffer: 200 * 1024 * 100, // 100 times the default
39 | });
40 |
41 | return stdout.trim();
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/utils/get-files.ts:
--------------------------------------------------------------------------------
1 | import { MessageChannelMain } from 'electron';
2 |
3 | import { FileTransformOperation, Files } from '../../interfaces';
4 | import { IpcEvents } from '../../ipc-events';
5 | import { ipcMainManager } from '../ipc';
6 |
7 | /**
8 | * Gets file content from the renderer
9 | */
10 | export function getFiles(
11 | window: Electron.BrowserWindow,
12 | transforms: Array,
13 | ): Promise<{ localPath?: string; files: Files }> {
14 | return new Promise((resolve) => {
15 | const { port1, port2 } = new MessageChannelMain();
16 | ipcMainManager.postMessage(
17 | IpcEvents.GET_FILES,
18 | { options: undefined, transforms },
19 | [port1],
20 | window.webContents,
21 | );
22 | port2.once('message', (event) => {
23 | resolve(event.data);
24 | port2.close();
25 | });
26 | port2.start();
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/utils/get-project-name.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'node:path';
2 |
3 | import * as namor from 'namor';
4 |
5 | /**
6 | * Returns a name for this project
7 | */
8 | export function getProjectName(localPath?: string): string {
9 | if (localPath) {
10 | return path.basename(localPath);
11 | }
12 |
13 | return namor.generate({ words: 3, numbers: 0 });
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/utils/get-username.ts:
--------------------------------------------------------------------------------
1 | import * as os from 'node:os';
2 |
3 | /**
4 | * Returns the current username
5 | */
6 | export const getUsername = (() => {
7 | let username = '';
8 |
9 | return (): string => {
10 | if (!username) {
11 | username = os.userInfo().username;
12 | }
13 |
14 | return username;
15 | };
16 | })();
17 |
--------------------------------------------------------------------------------
/src/main/utils/read-fiddle.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'node:path';
2 |
3 | import * as fs from 'fs-extra';
4 |
5 | import { EditorId, EditorValues, PACKAGE_NAME } from '../../interfaces';
6 | import { ensureRequiredFiles, isSupportedFile } from '../../utils/editor-utils';
7 |
8 | /**
9 | * Reads a Fiddle from a directory.
10 | *
11 | * @returns the loaded Fiddle
12 | */
13 | export async function readFiddle(
14 | folder: string,
15 | includePackageJson = false,
16 | ): Promise {
17 | let got: EditorValues = {};
18 |
19 | try {
20 | // TODO(dsanders11): Remove options once issue fixed:
21 | // https://github.com/isaacs/node-graceful-fs/issues/223
22 | const files = await fs.readdir(folder, { encoding: 'utf8' });
23 | const names = files.filter((f) => {
24 | if (f === 'package-lock.json') {
25 | return false;
26 | }
27 |
28 | if (f === PACKAGE_NAME) {
29 | return includePackageJson;
30 | }
31 |
32 | return isSupportedFile(f);
33 | });
34 |
35 | const values = await Promise.allSettled(
36 | names.map((name) => fs.readFile(path.join(folder, name), 'utf8')),
37 | );
38 |
39 | for (let i = 0; i < names.length; ++i) {
40 | const name = names[i] as EditorId;
41 | const value = values[i];
42 |
43 | if (value.status === 'fulfilled') {
44 | got[name] = value.value || '';
45 | } else {
46 | console.warn(`Could not read file ${name}:`, value.reason);
47 | got[name] = '';
48 | }
49 | }
50 | } catch (err) {
51 | console.warn(`Unable to read "${folder}": ${err.toString()}`);
52 | }
53 |
54 | got = ensureRequiredFiles(got);
55 | console.log(`Got Fiddle from "${folder}". Found:`, Object.keys(got).sort());
56 | return got;
57 | }
58 |
--------------------------------------------------------------------------------
/src/renderer/bisect.ts:
--------------------------------------------------------------------------------
1 | import { RunnableVersion } from '../interfaces';
2 |
3 | export class Bisector {
4 | public revList: Array;
5 | public minRev: number;
6 | public maxRev: number;
7 | private pivot: number;
8 |
9 | constructor(revList: Array) {
10 | this.getCurrentVersion = this.getCurrentVersion.bind(this);
11 | this.continue = this.continue.bind(this);
12 | this.calculatePivot = this.calculatePivot.bind(this);
13 |
14 | this.revList = revList;
15 | this.minRev = 0;
16 | this.maxRev = revList.length - 1;
17 | this.calculatePivot();
18 | }
19 |
20 | public getCurrentVersion() {
21 | return this.revList[this.pivot];
22 | }
23 |
24 | public continue(isGoodVersion: boolean) {
25 | let isBisectOver = false;
26 | if (this.maxRev - this.minRev <= 1) {
27 | isBisectOver = true;
28 | }
29 |
30 | if (isGoodVersion) {
31 | const upPivot = Math.floor((this.maxRev - this.pivot) / 2) + this.pivot;
32 | this.minRev = this.pivot;
33 | if (upPivot !== this.maxRev && upPivot !== this.pivot) {
34 | this.pivot = upPivot;
35 | } else {
36 | isBisectOver = true;
37 | }
38 | } else {
39 | const downPivot =
40 | Math.floor((this.pivot - this.minRev) / 2) + this.minRev;
41 | this.maxRev = this.pivot;
42 | if (downPivot !== this.minRev && downPivot !== this.pivot) {
43 | this.pivot = downPivot;
44 | } else {
45 | isBisectOver = true;
46 | }
47 | }
48 |
49 | if (isBisectOver) {
50 | return [this.revList[this.minRev], this.revList[this.maxRev]];
51 | } else {
52 | return this.revList[this.pivot];
53 | }
54 | }
55 |
56 | private calculatePivot() {
57 | this.pivot = Math.floor((this.maxRev - this.minRev) / 2);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/renderer/components/commands-version-chooser.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { ButtonGroup } from '@blueprintjs/core';
4 | import { observer } from 'mobx-react';
5 |
6 | import { VersionSelect } from './version-select';
7 | import { AppState } from '../state';
8 |
9 | interface VersionChooserProps {
10 | appState: AppState;
11 | }
12 |
13 | /**
14 | * A dropdown allowing the selection of Electron versions. The actual
15 | * download is managed in the state.
16 | */
17 | export const VersionChooser = observer((props: VersionChooserProps) => {
18 | const {
19 | Bisector,
20 | currentElectronVersion,
21 | isAutoBisecting,
22 | isRunning,
23 | isSettingsShowing,
24 | setVersion,
25 | } = props.appState;
26 |
27 | return (
28 |
29 | setVersion(version)}
32 | currentVersion={currentElectronVersion}
33 | disabled={
34 | !!Bisector || isAutoBisecting || isSettingsShowing || isRunning
35 | }
36 | />
37 |
38 | );
39 | });
40 |
--------------------------------------------------------------------------------
/src/renderer/components/dialogs.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { observer } from 'mobx-react';
4 |
5 | import { AddThemeDialog } from './dialog-add-theme';
6 | import { AddVersionDialog } from './dialog-add-version';
7 | import { BisectDialog } from './dialog-bisect';
8 | import { GenericDialog } from './dialog-generic';
9 | import { TokenDialog } from './dialog-token';
10 | import { Settings } from './settings';
11 | import { AppState } from '../state';
12 |
13 | interface DialogsProps {
14 | appState: AppState;
15 | }
16 |
17 | /**
18 | * Dialogs (like the GitHub PAT input).
19 | */
20 | export const Dialogs = observer(
21 | class Dialogs extends React.Component {
22 | public render() {
23 | const { appState } = this.props;
24 | const {
25 | isTokenDialogShowing,
26 | isSettingsShowing,
27 | isAddVersionDialogShowing,
28 | isThemeDialogShowing,
29 | isBisectDialogShowing,
30 | isGenericDialogShowing,
31 | } = appState;
32 | const maybeToken = isTokenDialogShowing ? (
33 |
34 | ) : null;
35 | const maybeSettings = isSettingsShowing ? (
36 |
37 | ) : null;
38 | const maybeAddLocalVersion = isAddVersionDialogShowing ? (
39 |
40 | ) : null;
41 | const maybeMonaco = isThemeDialogShowing ? (
42 |
43 | ) : null;
44 | const maybeBisect = isBisectDialogShowing ? (
45 |
46 | ) : null;
47 | const genericDialog = isGenericDialogShowing ? (
48 |
49 | ) : null;
50 |
51 | return (
52 |
53 | {maybeToken}
54 | {maybeSettings}
55 | {maybeAddLocalVersion}
56 | {maybeMonaco}
57 | {maybeBisect}
58 | {genericDialog}
59 |
60 | );
61 | }
62 | },
63 | );
64 |
--------------------------------------------------------------------------------
/src/renderer/components/editors-non-ideal-state.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { Button, NonIdealState } from '@blueprintjs/core';
4 |
5 | import { EditorMosaic } from '../editor-mosaic';
6 |
7 | type RenderNonIdealStateProps = {
8 | editorMosaic: EditorMosaic;
9 | };
10 |
11 | export function RenderNonIdealState({
12 | editorMosaic,
13 | }: RenderNonIdealStateProps) {
14 | const resolveButton = (
15 |
62 |
63 | );
64 | });
65 | }
66 |
67 | public render() {
68 | return (
69 |
70 |
Credits
71 |
72 | Electron Fiddle is, just like Electron, a free open source project
73 | welcoming contributors of all genders, cultures, and backgrounds. We
74 | would like to thank those who helped to make Electron Fiddle:
75 |
76 |
77 |
{this.renderContributors()}
78 |
79 | );
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/renderer/components/settings-general-console.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { Callout, Checkbox, FormGroup } from '@blueprintjs/core';
4 | import { observer } from 'mobx-react';
5 |
6 | import { AppState } from '../state';
7 |
8 | interface ConsoleSettingsProps {
9 | appState: AppState;
10 | }
11 |
12 | /**
13 | * Settings content to manage console-related preferences.
14 | */
15 | export const ConsoleSettings = observer(
16 | class ConsoleSettings extends React.Component {
17 | constructor(props: ConsoleSettingsProps) {
18 | super(props);
19 |
20 | this.handleClearOnRunChange = this.handleClearOnRunChange.bind(this);
21 | }
22 |
23 | /**
24 | * Handles a change on whether or not the console should be cleared
25 | * before fiddle is executed.
26 | */
27 | public handleClearOnRunChange(event: React.FormEvent) {
28 | const { checked } = event.currentTarget;
29 | this.props.appState.isClearingConsoleOnRun = checked;
30 | }
31 |
32 | public render() {
33 | const { isClearingConsoleOnRun } = this.props.appState;
34 |
35 | const clearOnRunInstructions = `
36 | Enable this option to automatically clear the console whenever you run your
37 | fiddle.`.trim();
38 |
39 | return (
40 |
41 |
Console
42 |
43 |
44 | {clearOnRunInstructions}
45 |
50 |
51 |
52 |
53 | );
54 | }
55 | },
56 | );
57 |
--------------------------------------------------------------------------------
/src/renderer/components/settings-general-package-author.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { Callout, FormGroup, InputGroup } from '@blueprintjs/core';
4 | import { observer } from 'mobx-react';
5 |
6 | import { AppState } from '../state';
7 |
8 | interface PackageAuthorSettingsProps {
9 | appState: AppState;
10 | }
11 |
12 | interface IPackageAuthorSettingsState {
13 | value: string;
14 | }
15 |
16 | /**
17 | * Settings package.json author info
18 | */
19 | export const PackageAuthorSettings = observer(
20 | class PackageAuthorSettings extends React.Component<
21 | PackageAuthorSettingsProps,
22 | IPackageAuthorSettingsState
23 | > {
24 | constructor(props: PackageAuthorSettingsProps) {
25 | super(props);
26 |
27 | this.state = {
28 | value: this.props.appState.packageAuthor,
29 | };
30 |
31 | this.handlePackageAuthorChange =
32 | this.handlePackageAuthorChange.bind(this);
33 | }
34 |
35 | /**
36 | * Set the author information in package.json when processing uploads to gist
37 | */
38 | public handlePackageAuthorChange(event: React.FormEvent) {
39 | const { value } = event.currentTarget;
40 |
41 | this.setState({
42 | value,
43 | });
44 |
45 | this.props.appState.packageAuthor = value;
46 | }
47 |
48 | public render() {
49 | const packageAuthorLabel =
50 | 'Set the package.json author field for your exported Fiddle projects.';
51 |
52 | return (
53 |
54 |
Package Author
55 |
56 |
57 | ) =>
61 | this.handlePackageAuthorChange(e)
62 | }
63 | />
64 |
65 |
66 |
67 | );
68 | }
69 | },
70 | );
71 |
--------------------------------------------------------------------------------
/src/renderer/components/settings-general.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { Divider } from '@blueprintjs/core';
4 | import { observer } from 'mobx-react';
5 |
6 | import { AppearanceSettings } from './settings-general-appearance';
7 | import { BlockAcceleratorsSettings } from './settings-general-block-accelerators';
8 | import { ConsoleSettings } from './settings-general-console';
9 | import { FontSettings } from './settings-general-font';
10 | import { GitHubSettings } from './settings-general-github';
11 | import { MirrorSettings } from './settings-general-mirror';
12 | import { PackageAuthorSettings } from './settings-general-package-author';
13 | import { AppState } from '../state';
14 |
15 | interface GeneralSettingsProps {
16 | appState: AppState;
17 | toggleHasPopoverOpen: () => void;
18 | }
19 |
20 | /**
21 | * Settings content to manage GitHub-related preferences.
22 | */
23 | export const GeneralSettings = observer(
24 | class GeneralSettings extends React.Component {
25 | public render() {
26 | return (
27 |
28 |
General Settings
29 |
this.props.toggleHasPopoverOpen()}
32 | />
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | }
48 | },
49 | );
50 |
--------------------------------------------------------------------------------
/src/renderer/components/sidebar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { Mosaic } from 'react-mosaic-component';
4 |
5 | import { SidebarFileTree } from './sidebar-file-tree';
6 | import { SidebarPackageManager } from './sidebar-package-manager';
7 | import { AppState } from '../state';
8 |
9 | export const Sidebar = ({ appState }: { appState: AppState }) => {
10 | const ELEMENT_MAP = {
11 | fileTree: ,
12 | packageManager: ,
13 | };
14 | return (
15 |
16 | renderTile={(id) => ELEMENT_MAP[id as keyof typeof ELEMENT_MAP]}
17 | initialValue={{
18 | first: 'fileTree',
19 | second: 'packageManager',
20 | direction: 'column',
21 | splitPercentage: 50,
22 | }}
23 | />
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/renderer/constants.ts:
--------------------------------------------------------------------------------
1 | export const ELECTRON_ORG = 'electron';
2 | export const ELECTRON_REPO = 'electron';
3 |
4 | export const FIDDLE_GIST_DESCRIPTION_PLACEHOLDER = 'Electron Fiddle Gist';
5 |
6 | export const PREFERS_DARK_MEDIA_QUERY = '(prefers-color-scheme: dark)';
7 |
--------------------------------------------------------------------------------
/src/renderer/main.tsx:
--------------------------------------------------------------------------------
1 | import * as monaco from 'monaco-editor';
2 |
3 | import { App } from './app';
4 | import { initSentry } from './sentry';
5 |
6 | initSentry();
7 |
8 | window.monaco = monaco;
9 | window.app = new App();
10 | window.app.setup();
11 |
--------------------------------------------------------------------------------
/src/renderer/mirror-constants.ts:
--------------------------------------------------------------------------------
1 | const sources = {
2 | DEFAULT: {
3 | electronMirror: 'https://github.com/electron/electron/releases/download/',
4 | electronNightlyMirror:
5 | 'https://github.com/electron/nightlies/releases/download/',
6 | },
7 | CHINA: {
8 | electronMirror: 'https://npmmirror.com/mirrors/electron/',
9 | electronNightlyMirror: 'https://npmmirror.com/mirrors/electron-nightly/',
10 | },
11 | CUSTOM: {
12 | electronMirror: '',
13 | electronNightlyMirror: '',
14 | },
15 | };
16 |
17 | export const ELECTRON_MIRROR = {
18 | sourceType: 'DEFAULT' as keyof typeof sources,
19 | sources,
20 | };
21 |
22 | export type Sources = keyof typeof sources;
23 | export type Mirrors = {
24 | electronMirror: string;
25 | electronNightlyMirror: string;
26 | };
27 |
--------------------------------------------------------------------------------
/src/renderer/npm-search.tsx:
--------------------------------------------------------------------------------
1 | import { SearchResponse } from '@algolia/client-search';
2 | import algoliasearch, { SearchIndex } from 'algoliasearch/lite';
3 |
4 | // See full schema: https://github.com/algolia/npm-search#schema
5 | export interface NPMSearchResult {
6 | name: string;
7 | version: string;
8 | versions: Record;
9 | _highlightResult: {
10 | name: {
11 | value: string;
12 | };
13 | };
14 | }
15 |
16 | class NPMSearch {
17 | private index: SearchIndex;
18 | private searchCache: Map>;
19 | constructor() {
20 | const client = algoliasearch(
21 | 'OFCNCOG2CU',
22 | '4efa2042cf4dba11be6e96e5c394e1a4',
23 | );
24 | this.index = client.initIndex('npm-search');
25 | this.searchCache = new Map();
26 | }
27 |
28 | /**
29 | * Finds a list of packages Algolia's npm search index.
30 | * Naively caches all queries client-side.
31 | */
32 | async search(query: string) {
33 | if (this.searchCache.has(query)) {
34 | return this.searchCache.get(query)!;
35 | } else {
36 | const result = await this.index.search(query, {
37 | hitsPerPage: 5,
38 | optionalFilters: [`objectID:${query}`],
39 | });
40 | this.searchCache.set(query, result);
41 | return result;
42 | }
43 | }
44 | }
45 |
46 | const npmSearch = new NPMSearch();
47 | export { npmSearch };
48 |
--------------------------------------------------------------------------------
/src/renderer/sentry.ts:
--------------------------------------------------------------------------------
1 | import * as Sentry from '@sentry/electron/renderer';
2 |
3 | import { SENTRY_DSN } from '../constants';
4 |
5 | export function initSentry() {
6 | if (!window.ElectronFiddle.isDevMode) {
7 | Sentry.init({ dsn: SENTRY_DSN });
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/renderer/themes.ts:
--------------------------------------------------------------------------------
1 | import { PREFERS_DARK_MEDIA_QUERY } from './constants';
2 | import { AppState } from './state';
3 | import {
4 | DefaultThemes,
5 | FiddleTheme,
6 | LoadedFiddleTheme,
7 | defaultDark,
8 | defaultLight,
9 | } from '../themes-defaults';
10 |
11 | /**
12 | * Activate a given theme (or the default)
13 | */
14 | export function activateTheme(theme: LoadedFiddleTheme) {
15 | const { monaco } = window;
16 | monaco.editor.defineTheme('main', theme.editor as any);
17 | monaco.editor.setTheme('main');
18 | }
19 |
20 | export function getCurrentTheme(): LoadedFiddleTheme {
21 | return window.matchMedia(PREFERS_DARK_MEDIA_QUERY).matches
22 | ? defaultDark
23 | : defaultLight;
24 | }
25 |
26 | /**
27 | * Returns a Fiddle theme, either a default one or by checking
28 | * the disk for a JSON file.
29 | */
30 | export async function getTheme(
31 | appState: AppState,
32 | name: string | null,
33 | ): Promise {
34 | console.log(`Themes: getTheme() loading ${name || 'default'}`);
35 |
36 | let theme: LoadedFiddleTheme | null = null;
37 |
38 | if (name === DefaultThemes.LIGHT) {
39 | theme = defaultLight;
40 | } else if (name === DefaultThemes.DARK) {
41 | theme = defaultDark;
42 | } else if (name) {
43 | theme = await window.ElectronFiddle.readThemeFile(name);
44 | }
45 |
46 | // If there is no theme, default to the current system theme
47 | // if the app is using system theme, otherwise default to dark
48 | if (!theme) {
49 | theme = appState.isUsingSystemTheme ? getCurrentTheme() : defaultDark;
50 | }
51 |
52 | return { ...theme, css: await getCssStringForTheme(theme) };
53 | }
54 |
55 | /**
56 | * Get the CSS string for a theme.
57 | */
58 | async function getCssStringForTheme(theme: FiddleTheme): Promise {
59 | let cssContent = '';
60 |
61 | Object.keys(theme.common).forEach((key: keyof typeof theme.common) => {
62 | cssContent += ` --${key}: ${theme.common[key]};\n`;
63 | });
64 |
65 | return `\n html, body {\n${cssContent} }\n`;
66 | }
67 |
--------------------------------------------------------------------------------
/src/renderer/transforms/dotfiles.ts:
--------------------------------------------------------------------------------
1 | import { Files } from '../../interfaces';
2 |
3 | /**
4 | * This transform adds dotfiles (like .gitignore)
5 | */
6 | export async function dotfilesTransform(files: Files): Promise {
7 | files.set('.gitignore', 'node_modules\nout');
8 |
9 | return files;
10 | }
11 |
--------------------------------------------------------------------------------
/src/renderer/utils/disable-download.ts:
--------------------------------------------------------------------------------
1 | import semver from 'semver';
2 |
3 | /**
4 | * disables download button for versions:
5 | * - below 11.0.0 on Apple Silicon.
6 | * - below 6.0.8 and 7.0.0 on Windows arm64
7 | * Reference: {@link https://www.electronjs.org/blog/apple-silicon}
8 | *
9 | * @param version - electron version
10 | */
11 | export function disableDownload(version: string): boolean {
12 | return (
13 | (window.ElectronFiddle.platform === 'darwin' &&
14 | window.ElectronFiddle.arch === 'arm64' &&
15 | semver.lt(version, '11.0.0')) ||
16 | (window.ElectronFiddle.platform === 'win32' &&
17 | window.ElectronFiddle.arch === 'arm64' &&
18 | !semver.satisfies(version, '>=6.0.8 || >=7.0.0'))
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/renderer/utils/editor-utils.ts:
--------------------------------------------------------------------------------
1 | import { EditorId, MAIN_CJS, MAIN_JS, MAIN_MJS } from '../../interfaces';
2 | import {
3 | ensureRequiredFiles,
4 | getEmptyContent,
5 | getSuffix,
6 | isMainEntryPoint,
7 | isSupportedFile,
8 | } from '../../utils/editor-utils';
9 |
10 | export {
11 | ensureRequiredFiles,
12 | getEmptyContent,
13 | getSuffix,
14 | isMainEntryPoint,
15 | isSupportedFile,
16 | };
17 |
18 | // The order of these fields is the order that
19 | // they'll be sorted in the mosaic
20 | const KNOWN_FILES: string[] = [
21 | MAIN_CJS,
22 | MAIN_JS,
23 | MAIN_MJS,
24 | 'renderer.cjs',
25 | 'renderer.js',
26 | 'renderer.mjs',
27 | 'index.html',
28 | 'preload.cjs',
29 | 'preload.js',
30 | 'preload.mjs',
31 | 'styles.css',
32 | ];
33 |
34 | export function isKnownFile(filename: string): boolean {
35 | return KNOWN_FILES.includes(filename);
36 | }
37 |
38 | export function getEditorTitle(id: EditorId): string {
39 | switch (id) {
40 | case 'index.html':
41 | return 'HTML (index.html)';
42 |
43 | case MAIN_CJS:
44 | case MAIN_JS:
45 | case MAIN_MJS:
46 | return `Main Process (${id})`;
47 |
48 | case 'preload.cjs':
49 | case 'preload.js':
50 | case 'preload.mjs':
51 | return `Preload (${id})`;
52 |
53 | case 'renderer.cjs':
54 | case 'renderer.js':
55 | case 'renderer.mjs':
56 | return `Renderer Process (${id})`;
57 |
58 | case 'styles.css':
59 | return 'Stylesheet (styles.css)';
60 | }
61 |
62 | return id;
63 | }
64 |
65 | // the KNOWN_FILES, in the order of that array, go first.
66 | // then everything else, sorted lexigraphically
67 | export function compareEditors(a: EditorId, b: EditorId) {
68 | const ia = KNOWN_FILES.indexOf(a);
69 | const ib = KNOWN_FILES.indexOf(b);
70 | if (ia === -1 && ib === -1) return a.localeCompare(b);
71 | if (ia === -1) return 1;
72 | if (ib === -1) return -1;
73 | return ia - ib;
74 | }
75 |
76 | export function monacoLanguage(filename: string) {
77 | const suffix = getSuffix(filename);
78 | if (suffix === 'css') return 'css';
79 | if (suffix === 'html') return 'html';
80 | if (suffix === 'json') return 'json';
81 | return 'javascript';
82 | }
83 |
--------------------------------------------------------------------------------
/src/renderer/utils/electron-name.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the correct name of ELectron for the current platform
3 | */
4 | export function getElectronNameForPlatform(): string {
5 | switch (window.ElectronFiddle.platform) {
6 | case 'win32': {
7 | return 'electron.exe';
8 | }
9 | case 'darwin': {
10 | return 'Electron.app';
11 | }
12 | default: {
13 | return 'electron';
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/renderer/utils/get-package.ts:
--------------------------------------------------------------------------------
1 | import * as fiddlePackageJSON from '../../../package.json';
2 | import { MAIN_JS, PackageJsonOptions } from '../../interfaces';
3 | import { AppState } from '../../renderer/state';
4 |
5 | export const DEFAULT_OPTIONS = {
6 | includeElectron: true,
7 | includeDependencies: true,
8 | };
9 |
10 | export function getForgeVersion(): string {
11 | return fiddlePackageJSON.devDependencies['@electron-forge/cli'];
12 | }
13 |
14 | /**
15 | * Returns the package.json for the current Fiddle
16 | */
17 | export async function getPackageJson(
18 | appState: AppState,
19 | options?: PackageJsonOptions,
20 | ): Promise {
21 | const { includeElectron, includeDependencies } = options || DEFAULT_OPTIONS;
22 | const name = await appState.getName();
23 |
24 | const devDependencies: Record = {};
25 | const dependencies: Record = {};
26 |
27 | if (includeElectron) {
28 | const packageName = appState.version?.includes('nightly')
29 | ? 'electron-nightly'
30 | : 'electron';
31 | devDependencies[packageName] = appState.version;
32 | }
33 |
34 | if (includeDependencies) {
35 | const { modules } = appState;
36 | for (const [module, version] of modules.entries()) {
37 | dependencies[module] = version;
38 | }
39 | }
40 |
41 | const entryPoint = appState.editorMosaic.mainEntryPointFile() ?? MAIN_JS;
42 |
43 | return JSON.stringify(
44 | {
45 | name,
46 | productName: name,
47 | description: 'My Electron application description',
48 | keywords: [],
49 | main: `./${entryPoint}`,
50 | version: '1.0.0',
51 | author: appState.packageAuthor,
52 | scripts: {
53 | start: 'electron .',
54 | },
55 | dependencies,
56 | devDependencies,
57 | },
58 | undefined,
59 | 2,
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/src/renderer/utils/get-version-range.ts:
--------------------------------------------------------------------------------
1 | import { semverCompare } from './sort-versions';
2 | import { RunnableVersion } from '../../interfaces';
3 |
4 | /**
5 | * An subset of `versions` sorted from oldest to newest and bounded in the range of [oldVersion..newVersion]
6 | *
7 | * @param oldVersion - first version to keep
8 | * @param newVersion - last version to keep
9 | * @param versions - the versions to make a subset of
10 | */
11 | export function getVersionRange(
12 | oldVersion: string,
13 | newVersion: string,
14 | versions: RunnableVersion[],
15 | ): RunnableVersion[] {
16 | // ensure that oldVersion is old than newVersion
17 | if (semverCompare(oldVersion, newVersion) > 0) {
18 | [oldVersion, newVersion] = [newVersion, oldVersion];
19 | }
20 |
21 | const oldIdx = versions.findIndex((v) => v.version === oldVersion);
22 | if (oldIdx === -1) {
23 | console.warn(`getVersionRange: Version not found: ${oldVersion}`);
24 | return [];
25 | }
26 |
27 | const newIdx = versions.findIndex((v) => v.version === newVersion);
28 | if (newIdx === -1) {
29 | console.warn(`getVersionRange: Version not found: ${newVersion}`);
30 | return [];
31 | }
32 |
33 | versions = versions.slice(
34 | Math.min(oldIdx, newIdx),
35 | Math.max(oldIdx, newIdx) + 1,
36 | );
37 |
38 | if (oldIdx > newIdx) {
39 | versions.reverse();
40 | }
41 |
42 | return versions;
43 | }
44 |
--------------------------------------------------------------------------------
/src/renderer/utils/highlight-text.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | /**
4 | * Highlights part of a string
5 | *
6 | * Inspired by https://github.com/palantir/blueprint/blob/develop/packages/docs-app/src/examples/select-examples/films.tsx
7 | * License: https://github.com/palantir/blueprint/blob/develop/LICENSE
8 | * Copyright 2017 Palantir Technologies, Inc. All rights reserved.
9 | */
10 | export function highlightText(
11 | text: string,
12 | query: string,
13 | ): Array | null {
14 | let lastIndex = 0;
15 |
16 | const words = query
17 | .split(/\s+/)
18 | .filter((word) => word.length > 0)
19 | .map((s) => s.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'));
20 |
21 | if (words.length === 0) return [text];
22 |
23 | const regexp = new RegExp(words.join('|'), 'gi');
24 | const tokens: Array = [];
25 |
26 | while (true) {
27 | const match = regexp.exec(text);
28 |
29 | if (!match) {
30 | break;
31 | }
32 |
33 | const length = match[0].length;
34 | const before = text.slice(lastIndex, regexp.lastIndex - length);
35 |
36 | if (before.length > 0) {
37 | tokens.push(before);
38 | }
39 |
40 | lastIndex = regexp.lastIndex;
41 | tokens.push({match[0]});
42 | }
43 |
44 | const rest = text.slice(lastIndex);
45 |
46 | if (rest.length > 0) {
47 | tokens.push(rest);
48 | }
49 |
50 | return tokens;
51 | }
52 |
--------------------------------------------------------------------------------
/src/renderer/utils/js-path.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the value of an object's property at a given path.
3 | */
4 | export function getAtPath(input: string, obj: any): any {
5 | return input.split('.').reduce((o, s) => o[s], obj);
6 | }
7 |
8 | /**
9 | * Sets a value of a property of an object at a given path
10 | */
11 | export function setAtPath(input: string, obj: any, val: any) {
12 | const pathValues = input.split('.');
13 |
14 | pathValues.reduce((o, s, i) => {
15 | if (i !== pathValues.length - 1) {
16 | return o[s];
17 | }
18 |
19 | o[s] = val;
20 | }, obj);
21 | }
22 |
--------------------------------------------------------------------------------
/src/renderer/utils/normalize-version.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Removes a possible leading "v" from a version.
3 | */
4 | export function normalizeVersion(version = ''): string {
5 | if (version.startsWith('v')) {
6 | return version.slice(1);
7 | }
8 |
9 | return version;
10 | }
11 |
--------------------------------------------------------------------------------
/src/renderer/utils/octokit.ts:
--------------------------------------------------------------------------------
1 | import { Octokit } from '@octokit/rest';
2 |
3 | import { AppState } from '../../renderer/state';
4 |
5 | let _octo: Octokit;
6 |
7 | /**
8 | * Returns a loaded Octokit. If state is passed and authentication
9 | * is available, we'll token-authenticate.
10 | */
11 | export async function getOctokit(appState?: AppState): Promise {
12 | // It's possible to load Gists without being authenticated,
13 | // but we get better rate limits when authenticated.
14 | _octo =
15 | _octo || appState?.gitHubToken
16 | ? new Octokit({
17 | auth: appState?.gitHubToken,
18 | })
19 | : new Octokit();
20 |
21 | return _octo;
22 | }
23 |
--------------------------------------------------------------------------------
/src/renderer/utils/plural-maybe.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Takes a word and an array. If the array has more than one entry,
3 | * it attaches a plural "s".
4 | */
5 | export function maybePlural(word: string, input: Array): string {
6 | if (input && input.length && input.length > 1) {
7 | return `${word}s`;
8 | }
9 |
10 | return word;
11 | }
12 |
--------------------------------------------------------------------------------
/src/renderer/utils/sort-versions.ts:
--------------------------------------------------------------------------------
1 | import * as semver from 'semver';
2 |
3 | import { RunnableVersion } from '../../interfaces';
4 |
5 | const preTags = ['nightly', 'alpha', 'beta'];
6 |
7 | /**
8 | * Sorts prerelease tags such that nightly -\> alpha -\> beta.
9 | *
10 | * @param a - Prerelease tag data for the old version.
11 | * @param b - Prerelease tag data for the new version.
12 | * @returns 0 | 1 | -1
13 | */
14 | const preCompare = (a: string[], b: string[]) => {
15 | const first = preTags.indexOf(a[0]);
16 | const second = preTags.indexOf(b[0]);
17 | if (first === second) {
18 | // Whether the prerelease tag number is the same
19 | // e.g. alpha.1 & alpha.1.
20 | if (a[1] === b[1]) return 0;
21 | return a[1] > b[1] ? 1 : -1;
22 | }
23 |
24 | return first > second ? 1 : -1;
25 | };
26 |
27 | /**
28 | * Custom semver comparator which takes into account Electron's prerelease
29 | * tag hierarchy.
30 | *
31 | * Sorts in ascending order when passed to Array.sort().
32 | *
33 | * @param a - The old Electron version.
34 | * @param b - The new Electron version.
35 | * @returns 0 | 1 | -1
36 | */
37 | export function semverCompare(
38 | a: string | semver.SemVer,
39 | b: string | semver.SemVer,
40 | ) {
41 | const pA = typeof a === 'string' ? semver.parse(a) : a;
42 | const pB = typeof b === 'string' ? semver.parse(b) : b;
43 |
44 | const sameMain = (a: semver.SemVer | null, b: semver.SemVer | null) =>
45 | a !== null && b !== null && a.compareMain(b) === 0;
46 |
47 | // Check that major.minor.patch are the same for a and b.
48 | if (a === 'v3.0.0' || b === 'v2.0.0') throw new Error('hey');
49 | if (
50 | sameMain(pA, pB) &&
51 | pA?.prerelease.length !== 0 &&
52 | pB?.prerelease.length !== 0
53 | ) {
54 | if (a === 'v3.0.0' || b === 'v3.0.0') throw new Error('hey');
55 | return preCompare(pA?.prerelease as string[], pB?.prerelease as string[]);
56 | }
57 |
58 | return semver.compare(a, b);
59 | }
60 |
61 | /**
62 | * Inplace sorting of Versions
63 | */
64 | export function sortVersions(versions: RunnableVersion[]): RunnableVersion[] {
65 | type VerSemRun = [
66 | ver: string,
67 | sem: semver.SemVer | null,
68 | run: RunnableVersion,
69 | ];
70 |
71 | const sorted = versions
72 | .map((run): VerSemRun => [run.version, semver.parse(run.version), run])
73 | .sort(
74 | ([vera, sema], [verb, semb]) =>
75 | -semverCompare(sema || vera, semb || verb),
76 | );
77 | sorted.forEach(([_1, _2, run], idx) => (versions[idx] = run));
78 | return versions;
79 | }
80 |
--------------------------------------------------------------------------------
/src/renderer/utils/toggle-monaco.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the opposite of a Monaco-style boolean.
3 | */
4 | export function toggleMonaco(input: boolean | string): boolean | string {
5 | if (input === 'off') return 'on';
6 | if (input === 'on') return 'off';
7 |
8 | return !input;
9 | }
10 |
--------------------------------------------------------------------------------
/src/templates.ts:
--------------------------------------------------------------------------------
1 | import { Templates } from './interfaces';
2 |
3 | export const SHOW_ME_TEMPLATES: Templates = {
4 | 'Electron APIs': {
5 | App: 'App',
6 | AutoUpdater: 'AutoUpdater',
7 | BrowserView: 'BrowserView',
8 | BrowserWindow: 'BrowserWindow',
9 | Clipboard: 'clipboard',
10 | ContentTracing: 'ContentTracing',
11 | Cookies: 'Cookies',
12 | CrashReporter: 'CrashReporter',
13 | Debugger: 'Debugger',
14 | DesktopCapturer: 'DesktopCapturer',
15 | Dialog: 'Dialog',
16 | GlobalShortcut: 'GlobalShortcut',
17 | IPC: 'IPC',
18 | Menu: 'Menu',
19 | NativeImage: 'NativeImage',
20 | Net: 'Net',
21 | Notification: 'Notification',
22 | PowerMonitor: 'PowerMonitor',
23 | PowerSaveBlocker: 'PowerSaveBlocker',
24 | Screen: 'Screen',
25 | Session: 'Session',
26 | Shell: 'Shell',
27 | SystemPreferences: 'systemPreferences',
28 | TouchBar: 'TouchBar',
29 | Tray: 'Tray',
30 | utilityProcess: 'utilityProcess',
31 | WebContents: 'WebContents',
32 | WebContentsView: 'WebContentsView',
33 | WebFrame: 'WebFrame',
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/src/utils/editor-utils.ts:
--------------------------------------------------------------------------------
1 | import {
2 | EditorId,
3 | EditorValues,
4 | MAIN_CJS,
5 | MAIN_JS,
6 | MAIN_MJS,
7 | } from '../interfaces';
8 |
9 | const mainEntryPointFiles = new Set([MAIN_CJS, MAIN_JS, MAIN_MJS]);
10 |
11 | const EMPTY_EDITOR_CONTENT: Record = {
12 | '.css': '/* Empty */',
13 | '.html': '',
14 | '.cjs': '// Empty',
15 | '.js': '// Empty',
16 | '.mjs': '// Empty',
17 | '.json': '{}',
18 | } as const;
19 |
20 | export function getEmptyContent(filename: string): string {
21 | return EMPTY_EDITOR_CONTENT[`.${getSuffix(filename)}` as EditorId] || '';
22 | }
23 |
24 | export function isMainEntryPoint(id: EditorId) {
25 | return mainEntryPointFiles.has(id);
26 | }
27 |
28 | export function ensureRequiredFiles(values: EditorValues): EditorValues {
29 | const mainEntryPoint = Object.keys(values).find((id: EditorId) =>
30 | mainEntryPointFiles.has(id),
31 | ) as EditorId | undefined;
32 |
33 | // If no entry point is found, default to main.js
34 | if (!mainEntryPoint) {
35 | values[MAIN_JS] = getEmptyContent(MAIN_JS);
36 | } else {
37 | values[mainEntryPoint] ||= getEmptyContent(mainEntryPoint);
38 | }
39 |
40 | return values;
41 | }
42 |
43 | export function getSuffix(filename: string) {
44 | return filename.slice(filename.lastIndexOf('.') + 1);
45 | }
46 |
47 | export function isSupportedFile(filename: string): filename is EditorId {
48 | return /\.(css|html|cjs|js|mjs|json)$/i.test(filename);
49 | }
50 |
--------------------------------------------------------------------------------
/src/utils/gist.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Get the gist ID from a string.
3 | *
4 | * Understands these formats:
5 | *- 8C5FC0C6A5153D49B5A4A56D3ED9DA8F
6 | *- 8c5fc0c6a5153d49b5a4a56d3ed9da8f
7 | *- https://gist.github.com/8c5fc0c6a5153d49b5a4a56d3ed9da8f
8 | *- https://gist.github.com/8c5fc0c6a5153d49b5a4a56d3ed9da8f/
9 | *- https://gist.github.com/ckerr/8c5fc0c6a5153d49b5a4a56d3ed9da8f
10 | *- https://gist.github.com/ckerr/8c5fc0c6a5153d49b5a4a56d3ed9da8f/
11 | */
12 | export function getGistId(rawInput: string): string | null {
13 | const id = rawInput.trim().match(/[0-9A-Fa-f]{32}/);
14 |
15 | return id?.[0] || null;
16 | }
17 |
18 | /**
19 | * Get the id of a gist from a url
20 | */
21 | export function idFromUrl(input: string): string | null {
22 | return getGistId(input);
23 | }
24 |
25 | /**
26 | * Get the url for a gist id
27 | */
28 | export function urlFromId(input?: string): string {
29 | return input ? `https://gist.github.com/${input}` : '';
30 | }
31 |
--------------------------------------------------------------------------------
/static/css/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/electron/fiddle/f5a59c8028369459d9c4290a65fb2b7e2528251d/static/css/.gitkeep
--------------------------------------------------------------------------------
/static/electron-quick-start/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Hello World!
8 |
9 |
10 | Hello World!
11 | We are using Node.js ,
12 | Chromium ,
13 | and Electron .
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/static/electron-quick-start/main.js:
--------------------------------------------------------------------------------
1 | // Modules to control application life and create native browser window
2 | const { app, BrowserWindow } = require('electron')
3 | const path = require('node:path')
4 |
5 | function createWindow () {
6 | // Create the browser window.
7 | const mainWindow = new BrowserWindow({
8 | width: 800,
9 | height: 600,
10 | webPreferences: {
11 | preload: path.join(__dirname, 'preload.js')
12 | }
13 | })
14 |
15 | // and load the index.html of the app.
16 | mainWindow.loadFile('index.html')
17 |
18 | // Open the DevTools.
19 | // mainWindow.webContents.openDevTools()
20 | }
21 |
22 | // This method will be called when Electron has finished
23 | // initialization and is ready to create browser windows.
24 | // Some APIs can only be used after this event occurs.
25 | app.whenReady().then(() => {
26 | createWindow()
27 |
28 | app.on('activate', function () {
29 | // On macOS it's common to re-create a window in the app when the
30 | // dock icon is clicked and there are no other windows open.
31 | if (BrowserWindow.getAllWindows().length === 0) createWindow()
32 | })
33 | })
34 |
35 | // Quit when all windows are closed, except on macOS. There, it's common
36 | // for applications and their menu bar to stay active until the user quits
37 | // explicitly with Cmd + Q.
38 | app.on('window-all-closed', function () {
39 | if (process.platform !== 'darwin') app.quit()
40 | })
41 |
42 | // In this file you can include the rest of your app's specific main process
43 | // code. You can also put them in separate files and require them here.
44 |
--------------------------------------------------------------------------------
/static/electron-quick-start/preload.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The preload script runs before `index.html` is loaded
3 | * in the renderer. It has access to web APIs as well as
4 | * Electron's renderer process modules and some polyfilled
5 | * Node.js functions.
6 | *
7 | * https://www.electronjs.org/docs/latest/tutorial/sandbox
8 | */
9 | window.addEventListener('DOMContentLoaded', () => {
10 | const replaceText = (selector, text) => {
11 | const element = document.getElementById(selector)
12 | if (element) element.innerText = text
13 | }
14 |
15 | for (const type of ['chrome', 'node', 'electron']) {
16 | replaceText(`${type}-version`, process.versions[type])
17 | }
18 | })
19 |
--------------------------------------------------------------------------------
/static/electron-quick-start/renderer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is loaded via the
13 |