Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: register-local-version protocol command #1652

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
8 changes: 8 additions & 0 deletions src/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ declare global {
type: 'open-template',
listener: (name: string, editorValues: EditorValues) => void,
): void;
addEventListener(
type: 'register-local-version',
listener: (info: {
name: string;
path: string;
version: string;
}) => void,
): void;
addEventListener(
type: 'saved-local-fiddle',
listener: (filePath: string) => void,
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type FileTransformOperation = 'dotfiles' | 'forge';
export enum VersionSource {
remote = 'remote',
local = 'local',
pullRequest = 'pull-request',
}

export enum GistActionType {
Expand Down Expand Up @@ -186,6 +187,7 @@ export type FiddleEvent =
| 'open-template'
| 'package-fiddle'
| 'redo-in-editor'
| 'register-local-version'
| 'run-fiddle'
| 'save-fiddle-gist'
| 'saved-local-fiddle'
Expand Down
1 change: 1 addition & 0 deletions src/ipc-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export enum IpcEvents {
SHOW_WELCOME_TOUR = 'SHOW_WELCOME_TOUR',
CLEAR_CONSOLE = 'CLEAR_CONSOLE',
LOAD_LOCAL_VERSION_FOLDER = 'LOAD_LOCAL_VERSION_FOLDER',
REGISTER_LOCAL_VERSION_FOLDER = 'REGISTER_LOCAL_VERSION_FOLDER',
BISECT_COMMANDS_TOGGLE = 'BISECT_COMMANDS_TOGGLE',
BEFORE_QUIT = 'BEFORE_QUIT',
CONFIRM_QUIT = 'CONFIRM_QUIT',
Expand Down
11 changes: 1 addition & 10 deletions src/main/dialogs.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import * as path from 'node:path';

import { Installer } from '@electron/fiddle-core';
import { BrowserWindow, dialog } from 'electron';
import * as fs from 'fs-extra';

import { ipcMainManager } from './ipc';
import { isValidElectronPath } from './utils/local-version';
import { SelectedLocalVersion } from '../interfaces';
import { IpcEvents } from '../ipc-events';

Expand All @@ -31,14 +30,6 @@ function makeLocalName(folderPath: string): string {
return `${leader} - ${buildType}`;
}

/**
* Verifies if the local electron path is valid
*/
function isValidElectronPath(folderPath: string): boolean {
const execPath = Installer.getExecPath(folderPath);
return fs.existsSync(execPath);
}

/**
* Listens to IPC events related to dialogs and message boxes
*/
Expand Down
33 changes: 33 additions & 0 deletions src/main/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { app } from 'electron';
import { openFiddle } from './files';
import { ipcMainManager } from './ipc';
import { isDevMode } from './utils/devmode';
import { isValidElectronPath } from './utils/local-version';
import { getOrCreateMainWindow } from './windows';
import { IpcEvents } from '../ipc-events';

Expand Down Expand Up @@ -65,6 +66,38 @@ const handlePotentialProtocolLaunch = (url: string) => {
return;
}
break;
// electron-fiddle://register-local-version/?name={name}&path={path}&version={version}
case 'register-local-version': {
if (pathParts.length === 1) {
const name = parsed.searchParams.get('name');
const localPath = parsed.searchParams.get('path');
const version = parsed.searchParams.get('version');

if (!(name && localPath && version)) {
console.debug('register-local-version: Missing params');
return;
}

if (!isValidElectronPath(localPath)) {
console.debug(`register-local-version: Invalid path ${localPath}`);
return;
}

const toAdd = {
name,
path: localPath,
version,
};

console.debug('register-local-version: Registering version', toAdd);

ipcMainManager.send(IpcEvents.REGISTER_LOCAL_VERSION_FOLDER, [toAdd]);
} else {
// Invalid
return;
}
break;
}
default:
return;
}
Expand Down
11 changes: 11 additions & 0 deletions src/main/utils/local-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as fs from 'node:fs';

import { Installer } from '@electron/fiddle-core';

/**
* Verifies if the local electron path is valid
*/
export function isValidElectronPath(folderPath: string): boolean {
const execPath = Installer.getExecPath(folderPath);
return fs.existsSync(execPath);
}
1 change: 1 addition & 0 deletions src/preload/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const channelMapping: Record<FiddleEvent, IpcEvents> = {
'open-settings': IpcEvents.OPEN_SETTINGS,
'open-template': IpcEvents.FS_OPEN_TEMPLATE,
'package-fiddle': IpcEvents.FIDDLE_PACKAGE,
'register-local-version': IpcEvents.REGISTER_LOCAL_VERSION_FOLDER,
'redo-in-editor': IpcEvents.REDO_IN_EDITOR,
'run-fiddle': IpcEvents.FIDDLE_RUN,
'saved-local-fiddle': IpcEvents.SAVED_LOCAL_FIDDLE,
Expand Down
24 changes: 24 additions & 0 deletions src/renderer/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
PACKAGE_NAME,
PackageJsonOptions,
SetFiddleOptions,
Version,
} from '../interfaces';
import { defaultDark, defaultLight } from '../themes-defaults';

Expand Down Expand Up @@ -137,6 +138,7 @@ export class App {
this.setupTitleListeners();
this.setupUnloadListeners();
this.setupTypeListeners();
this.setupProtocolListeners();

window.ElectronFiddle.sendReady();

Expand Down Expand Up @@ -310,4 +312,26 @@ export class App {
};
});
}

public setupProtocolListeners() {
window.ElectronFiddle.addEventListener(
'register-local-version',
async ({ name, path, version }) => {
const confirm = await this.state.showConfirmDialog({
label: `Are you sure you want to register "${path}" with version "${version}"? Only register and run it if you trust the source.`,
ok: 'Register',
});
if (!confirm) return;

const toAdd: Version = {
localPath: path,
version,
name,
};

this.state.addLocalVersion(toAdd);
this.state.setVersion(version);
},
);
}
}
37 changes: 30 additions & 7 deletions src/renderer/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
computed,
makeObservable,
observable,
runInAction,
when,
} from 'mobx';

Expand Down Expand Up @@ -975,14 +976,36 @@ export class AppState {
public async showGenericDialog(
opts: GenericDialogOptions,
): Promise<{ confirm: boolean; input: string }> {
this.genericDialogLastResult = null;
this.genericDialogOptions = opts;
this.isGenericDialogShowing = true;
// Wait for any existing dialog to be closed and cleaned up.
await when(() => {
if (
!this.isGenericDialogShowing &&
this.genericDialogLastResult === null
) {
// Set dialog immediately to prevent any other queued dialogs from
// showing.
runInAction(() => {
this.genericDialogOptions = opts;
this.isGenericDialogShowing = true;
});
return true;
}
return false;
});

// Wait for dialog to be closed.
await when(() => !this.isGenericDialogShowing);
return {
confirm: Boolean(this.genericDialogLastResult),
input: this.genericDialogLastInput || opts.defaultInput || '',
};

const confirm = Boolean(this.genericDialogLastResult);
const input = this.genericDialogLastInput || opts.defaultInput || '';

// Cleanup to allow queued dialog to show.
runInAction(() => {
this.genericDialogLastResult = null;
this.genericDialogLastInput = null;
});

return { confirm, input };
}

public async showInputDialog(opts: {
Expand Down
10 changes: 8 additions & 2 deletions src/renderer/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,16 @@ export function getElectronVersions(): Array<RunnableVersion> {
export function addLocalVersion(input: Version): Array<Version> {
const versions = getLocalVersions();

if (!versions.find((v) => v.localPath === input.localPath)) {
versions.push(input);
// Replace existing local version if it exists
const existingVersionIndex = versions.findIndex(
(v) => v.localPath === input.localPath,
);
if (existingVersionIndex > -1) {
versions.splice(existingVersionIndex, 1);
}

versions.push(input);

saveLocalVersions(versions);

return versions;
Expand Down
64 changes: 64 additions & 0 deletions tests/main/protocol-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

import * as fs from 'node:fs';
import * as path from 'node:path';

import { app } from 'electron';
import { mocked } from 'jest-mock';
Expand All @@ -13,11 +14,16 @@ import {
listenForProtocolHandler,
setupProtocolHandler,
} from '../../src/main/protocol';
import { isValidElectronPath } from '../../src/main/utils/local-version';
import { getOrCreateMainWindow, mainIsReady } from '../../src/main/windows';
import { overridePlatform, resetPlatform } from '../utils';

jest.mock('node:fs');

jest.mock('../../src/main/utils/local-version', () => ({
isValidElectronPath: jest.fn(),
}));

describe('protocol', () => {
const oldArgv = [...process.argv];

Expand Down Expand Up @@ -192,5 +198,63 @@ describe('protocol', () => {
await new Promise(process.nextTick);
expect(mainWindow.focus).toHaveBeenCalled();
});

it('handles registering local version url', () => {
mocked(isValidElectronPath).mockReturnValueOnce(true);

overridePlatform('darwin');
listenForProtocolHandler();

const handler = mocked(app.on).mock.calls[0][1];
const url = new URL('electron-fiddle://register-local-version/');
const params = {
name: 'test',
path: path.resolve(__dirname),
version: '35.0.0-local',
};
const keys = Object.keys(params) as Array<keyof typeof params>;
keys.forEach((k) => {
url.searchParams.append(k, params[k]);
});
handler({}, url.href);

expect(ipcMainManager.send).toHaveBeenCalledWith(
IpcEvents.REGISTER_LOCAL_VERSION_FOLDER,
[params],
);
});

it('handles registering local version without valid path', () => {
mocked(isValidElectronPath).mockReturnValueOnce(false);

overridePlatform('darwin');
listenForProtocolHandler();

const handler = mocked(app.on).mock.calls[0][1];
const url = new URL('electron-fiddle://register-local-version/');
const params = {
name: 'test',
path: path.resolve(__dirname),
version: '35.0.0-local',
};
const keys = Object.keys(params) as Array<keyof typeof params>;
keys.forEach((k) => {
url.searchParams.append(k, params[k]);
});
handler({}, url.href);

expect(ipcMainManager.send).not.toHaveBeenCalled();
});

it('handles registering local version with missing params', () => {
overridePlatform('darwin');
listenForProtocolHandler();

const handler = mocked(app.on).mock.calls[0][1];
const url = new URL('electron-fiddle://register-local-version/');
handler({}, url.href);

expect(ipcMainManager.send).not.toHaveBeenCalled();
});
});
});
55 changes: 55 additions & 0 deletions tests/renderer/app-spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,61 @@ describe('App component', () => {
});
});

describe('setupProtocolListeners()', () => {
let addEventListenerMock: jest.Mock;

beforeEach(() => {
addEventListenerMock = window.ElectronFiddle.addEventListener as any;
addEventListenerMock.mockClear();
});

it('registers protocol listeners', () => {
app.setupProtocolListeners();

expect(addEventListenerMock).toHaveBeenCalledWith(
'register-local-version',
expect.anything(),
);
});

it('handles registering new versions', async () => {
app.setupProtocolListeners();
const callback = addEventListenerMock.mock.calls[0][1];

app.state.showConfirmDialog = jest.fn().mockResolvedValue(true);

const addVersion = {
name: 'new-version',
path: '/version/build/path',
version: '123.0.0-local',
};
await callback(addVersion);

expect(app.state.addLocalVersion).toHaveBeenCalledWith({
name: addVersion.name,
localPath: addVersion.path,
version: addVersion.version,
});
expect(app.state.setVersion).toHaveBeenCalledWith(addVersion.version);
});

it('skips registering new versions when not confirmed', async () => {
app.setupProtocolListeners();
const callback = addEventListenerMock.mock.calls[0][1];

app.state.showConfirmDialog = jest.fn().mockResolvedValue(false);

const addVersion = {
name: 'new-version',
path: '/version/build/path',
version: '123.0.0-local',
};
await callback(addVersion);

expect(app.state.addLocalVersion).not.toHaveBeenCalled();
});
});

describe('prompting to confirm replacing an unsaved fiddle', () => {
// make a second fiddle that differs from the first
const editorValues = createEditorValues();
Expand Down
Loading