Skip to content
New issue

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

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

Already on GitHub? # 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