Skip to content

Commit efc89e9

Browse files
authored
feat: add GITHUB_FETCH_EXAMPLE to main (#1940)
* feat: add GITHUB_FETCH_EXAMPLE to IPC * refactor: use GITHUB_FETCH_EXAMPLE in the renderer * refactor: use symbolic constants for org, repo names
1 parent 34c2157 commit efc89e9

9 files changed

Lines changed: 292 additions & 124 deletions

File tree

src/ambient.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ declare global {
103103
opts?: Partial<DownloadVersionParams>,
104104
): Promise<void>;
105105
fetchVersions(): Promise<Version[]>;
106+
fetchExample(ref: string, path: string): Promise<EditorValues>;
106107
getAvailableThemes(): Promise<Array<LoadedFiddleTheme>>;
107108
getElectronTypes(ver: RunnableVersion): Promise<string | undefined>;
108109
getIsPackageManagerInstalled(

src/ipc-events.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export enum IpcEvents {
7474
FIDDLE_STOPPED = 'FIDDLE_STOPPED',
7575
VERSION_DOWNLOAD_PROGRESS = 'VERSION_DOWNLOAD_PROGRESS',
7676
VERSION_STATE_CHANGED = 'VERSION_STATE_CHANGED',
77+
GITHUB_FETCH_EXAMPLE = 'GITHUB_FETCH_EXAMPLE',
7778
GITHUB_GIST_CREATE = 'GITHUB_GIST_CREATE',
7879
GITHUB_GIST_DELETE = 'GITHUB_GIST_DELETE',
7980
GITHUB_GIST_LIST_COMMITS = 'GITHUB_GIST_LIST_COMMITS',

src/main/github.ts

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import * as fs from 'node:fs';
2-
import * as path from 'node:path';
2+
import { join as pathJoin } from 'node:path';
33

4-
import { Octokit } from '@octokit/rest';
4+
import { Octokit, RestEndpointMethodTypes } from '@octokit/rest';
55
import { IpcMainInvokeEvent, app, safeStorage } from 'electron';
66

7+
import { getTemplate } from './content';
78
import { ipcMainManager } from './ipc';
8-
import { GistRevision } from '../interfaces';
9+
import { EditorValues, GistRevision } from '../interfaces';
910
import { IpcEvents } from '../ipc-events';
11+
import { isSupportedFile } from '../utils/editor-utils';
1012

1113
// --- Input validation ---
1214

15+
const ELECTRON_ORG = 'electron';
16+
17+
const ELECTRON_REPO = 'electron';
18+
1319
const TOKEN_PATTERN =
1420
/^(ghp_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59})$/;
1521

@@ -79,7 +85,7 @@ function areValidGistFiles(
7985

8086
function getCredentialsPath(): string {
8187
const CREDENTIALS_FILE = '.github-credentials';
82-
return path.join(app.getPath('userData'), CREDENTIALS_FILE);
88+
return pathJoin(app.getPath('userData'), CREDENTIALS_FILE);
8389
}
8490

8591
function saveToken(token: string): void {
@@ -365,9 +371,64 @@ async function handleGistListCommits(
365371
}));
366372
}
367373

374+
async function handleFetchExample(
375+
_event: IpcMainInvokeEvent,
376+
params: unknown,
377+
): Promise<EditorValues> {
378+
if (typeof params !== 'object' || params === null)
379+
throw new Error('Invalid parameters.');
380+
const { ref, path } = params as Record<string, unknown>;
381+
if (typeof ref !== 'string') throw new Error('Invalid ref.');
382+
if (typeof path !== 'string') throw new Error('Invalid path.');
383+
return fetchExample(ref, path);
384+
}
385+
386+
async function fetchExample(ref: string, path: string): Promise<EditorValues> {
387+
if (!ref) throw new Error('Invalid ref.');
388+
if (!path) throw new Error('Invalid path.');
389+
390+
// `repos.getContent` returns a union; the directory variant is the array form.
391+
type RepoContentEntry = Extract<
392+
RestEndpointMethodTypes['repos']['getContent']['response']['data'],
393+
readonly unknown[]
394+
>[number];
395+
396+
const owner = ELECTRON_ORG;
397+
const repo = ELECTRON_REPO;
398+
const octo = getOctokit();
399+
400+
// Fetch the example folder listing.
401+
const folder = await octo.repos.getContent({ owner, path, ref, repo });
402+
if (!Array.isArray(folder.data))
403+
throw new Error(`${owner}:${repo}/${path}:${ref} is not a valid example`);
404+
const files = (folder.data as RepoContentEntry[]).filter(
405+
(file) =>
406+
typeof file.download_url === 'string' &&
407+
typeof file.name === 'string' &&
408+
isSupportedFile(file.name),
409+
);
410+
411+
// Get the base template for this version: 'v42.0.0' -> '42.0.0'.
412+
const version = ref.replace(/^v/, '');
413+
const values: EditorValues = { ...(await getTemplate(version)) };
414+
415+
// Download each supported file and overlay onto the template.
416+
await Promise.all(
417+
files.map(async (file) => {
418+
const resp = await fetch(file.download_url as string);
419+
if (!resp.ok)
420+
throw new Error(`Failed to download ${file.name}: ${resp.status}`);
421+
values[file.name as keyof EditorValues] = await resp.text();
422+
}),
423+
);
424+
425+
return values;
426+
}
427+
368428
// --- Setup ---
369429

370430
export function setupGitHub() {
431+
ipcMainManager.handle(IpcEvents.GITHUB_FETCH_EXAMPLE, handleFetchExample);
371432
ipcMainManager.handle(IpcEvents.GITHUB_GIST_CREATE, handleGistCreate);
372433
ipcMainManager.handle(IpcEvents.GITHUB_GIST_DELETE, handleGistDelete);
373434
ipcMainManager.handle(
@@ -386,6 +447,7 @@ export function setupGitHub() {
386447

387448
// Exported for testing
388449
export const testing = {
450+
fetchExample,
389451
handleGistCreate,
390452
handleGistDelete,
391453
handleGistListCommits,

src/preload/preload.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ export async function setupFiddleGlobal() {
107107
fetchVersions() {
108108
return ipcRenderer.invoke(IpcEvents.FETCH_VERSIONS);
109109
},
110+
fetchExample: (ref: string, path: string) =>
111+
ipcRenderer.invoke(IpcEvents.GITHUB_FETCH_EXAMPLE, { ref, path }),
110112
getElectronTypes(ver: RunnableVersion) {
111113
return ipcRenderer.invoke(IpcEvents.GET_ELECTRON_TYPES, ver);
112114
},

src/renderer/constants.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
export const ELECTRON_ORG = 'electron';
2-
export const ELECTRON_REPO = 'electron';
3-
41
export const FIDDLE_GIST_DESCRIPTION_PLACEHOLDER = 'Electron Fiddle Gist';
52

63
export const PREFERS_DARK_MEDIA_QUERY = '(prefers-color-scheme: dark)';

src/renderer/remote-loader.ts

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import semver from 'semver';
22

3-
import { ELECTRON_ORG, ELECTRON_REPO } from './constants';
43
import { AppState } from './state';
54
import { disableDownload } from './utils/disable-download';
65
import { isKnownFile, isSupportedFile } from './utils/editor-utils';
76
import { getOctokit } from './utils/octokit';
87
import { getReleaseChannel } from './versions';
98
import {
10-
EditorId,
119
EditorValues,
1210
ElectronReleaseChannel,
1311
GenericDialogType,
@@ -62,14 +60,6 @@ export class RemoteLoader {
6260
path: string,
6361
): Promise<boolean> {
6462
try {
65-
const octo = await getOctokit(this.appState);
66-
const folder = await octo.repos.getContent({
67-
owner: ELECTRON_REPO,
68-
repo: ELECTRON_ORG,
69-
ref: tag,
70-
path,
71-
});
72-
7363
const index = tag.search(/\d/);
7464
const version = tag.substring(index);
7565

@@ -80,35 +70,7 @@ export class RemoteLoader {
8070
const ok = await this.setElectronVersion(version);
8171
if (!ok) return false;
8272

83-
const values = await window.ElectronFiddle.getTemplate(
84-
this.appState.version,
85-
);
86-
if (!Array.isArray(folder.data)) {
87-
throw new Error(
88-
'The example Fiddle tried to launch is not a valid Electron example',
89-
);
90-
}
91-
92-
const loaders: Array<Promise<void>> = [];
93-
94-
for (const child of folder.data) {
95-
if (!child.download_url) {
96-
console.warn(`Could not find download_url for ${child.name}`);
97-
continue;
98-
}
99-
100-
if (isSupportedFile(child.name)) {
101-
loaders.push(
102-
fetch(child.download_url)
103-
.then((r) => r.text())
104-
.then((t) => {
105-
values[child.name as EditorId] = t;
106-
}),
107-
);
108-
}
109-
}
110-
111-
await Promise.all(loaders);
73+
const values = await window.ElectronFiddle.fetchExample(tag, path);
11274

11375
return this.handleLoadingSuccess(values, '');
11476
} catch (error: any) {

0 commit comments

Comments
 (0)