• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

electron / fiddle / 23524751248

25 Mar 2026 04:20AM UTC coverage: 89.475% (+10.0%) from 79.485%
23524751248

push

github

web-flow
build(dev-deps): bump vitest to 4.1.0 (#1870)

503 of 544 branches covered (92.46%)

Branch coverage included in aggregate %.

3416 of 3836 relevant lines covered (89.05%)

46.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.37
/src/renderer/remote-loader.ts
1
import semver from 'semver';
2

3
import { ELECTRON_ORG, ELECTRON_REPO } from './constants';
4
import { AppState } from './state';
5
import { disableDownload } from './utils/disable-download';
6
import { isKnownFile, isSupportedFile } from './utils/editor-utils';
7
import { getOctokit } from './utils/octokit';
8
import { getReleaseChannel } from './versions';
9
import {
10
  EditorId,
11
  EditorValues,
12
  ElectronReleaseChannel,
13
  GenericDialogType,
14
  GistRevision,
15
  InstallState,
16
  PACKAGE_NAME,
17
  VersionSource,
18
} from '../interfaces';
19

20
export class RemoteLoader {
21
  constructor(private readonly appState: AppState) {
55✔
22
    for (const name of [
55✔
23
      'fetchExampleAndLoad',
24
      'fetchGistAndLoad',
25
      'handleLoadingFailed',
26
      'handleLoadingSuccess',
27
      'loadFiddleFromElectronExample',
28
      'loadFiddleFromGist',
29
      'setElectronVersion',
30
      'verifyReleaseChannelEnabled',
31
      'verifyRemoteLoad',
32
    ] as const) {
33
      this[name] = this[name].bind(this) as any;
495✔
34
    }
35
  }
36

37
  public async loadFiddleFromElectronExample(exampleInfo: {
38
    path: string;
39
    tag: string;
40
  }) {
41
    console.log(`Loading fiddle from Electron example`, exampleInfo);
2✔
42
    const { path, tag } = exampleInfo;
2✔
43
    const prettyName = path.replace('docs/fiddles/', '');
2✔
44
    const ok = await this.verifyRemoteLoad(
2✔
45
      `'${prettyName}' example from the Electron docs for version ${tag}`,
46
    );
47
    if (!ok) return;
2✔
48

49
    this.fetchExampleAndLoad(tag, path);
1✔
50
  }
51

52
  public async loadFiddleFromGist(gistInfo: { id: string }) {
53
    const { id } = gistInfo;
2✔
54
    const ok = await this.verifyRemoteLoad(`gist`);
2✔
55
    if (!ok) return;
2✔
56

57
    this.fetchGistAndLoad(id);
1✔
58
  }
59

60
  public async fetchExampleAndLoad(
61
    tag: string,
62
    path: string,
63
  ): Promise<boolean> {
64
    try {
3✔
65
      const octo = await getOctokit(this.appState);
3✔
66
      const folder = await octo.repos.getContents({
3✔
67
        owner: ELECTRON_REPO,
68
        repo: ELECTRON_ORG,
69
        ref: tag,
70
        path,
71
      });
72

73
      const index = tag.search(/\d/);
2✔
74
      const version = tag.substring(index);
2✔
75

76
      if (!semver.valid(version)) {
2✔
77
        throw new Error('Could not determine Electron version for example');
×
78
      }
79

80
      const ok = await this.setElectronVersion(version);
2✔
81
      if (!ok) return false;
2✔
82

83
      const values = await window.ElectronFiddle.getTemplate(
2✔
84
        this.appState.version,
85
      );
86
      if (!Array.isArray(folder.data)) {
2✔
87
        throw new Error(
1✔
88
          'The example Fiddle tried to launch is not a valid Electron example',
89
        );
90
      }
91

92
      const loaders: Array<Promise<void>> = [];
1✔
93

94
      for (const child of folder.data) {
1✔
95
        if (!child.download_url) {
6✔
96
          console.warn(`Could not find download_url for ${child.name}`);
×
97
          continue;
×
98
        }
99

100
        if (isSupportedFile(child.name)) {
6✔
101
          loaders.push(
5✔
102
            fetch(child.download_url)
103
              .then((r) => r.text())
5✔
104
              .then((t) => {
105
                values[child.name as EditorId] = t;
5✔
106
              }),
107
          );
108
        }
109
      }
110

111
      await Promise.all(loaders);
1✔
112

113
      return this.handleLoadingSuccess(values, '');
1✔
114
    } catch (error: any) {
115
      return this.handleLoadingFailed(error);
2✔
116
    }
117
  }
118

119
  public async getGistRevisions(gistId: string): Promise<GistRevision[]> {
120
    try {
4✔
121
      const octo = await getOctokit(this.appState);
4✔
122
      const { data: revisions } = await octo.gists.listCommits({
4✔
123
        gist_id: gistId,
124
      });
125

126
      const oldestRevision = revisions[revisions.length - 1];
3✔
127
      const nonEmptyRevisions = revisions.filter(
3✔
128
        (r) =>
129
          r === oldestRevision ||
7✔
130
          r.change_status.additions > 0 ||
131
          r.change_status.deletions > 0,
132
      );
133

134
      return nonEmptyRevisions.reverse().map((r, i) => {
3✔
135
        return {
6✔
136
          sha: r.version,
137
          date: r.committed_at,
138
          changes: r.change_status,
139
          title: i === 0 ? 'Created' : `Revision ${i}`,
140
        };
141
      });
142
    } catch (error: any) {
143
      this.handleLoadingFailed(error);
1✔
144
      return [];
1✔
145
    }
146
  }
147

148
  /**
149
   * Load a fiddle
150
   */
151
  public async fetchGistAndLoad(
152
    gistId: string,
153
    revision?: string,
154
  ): Promise<boolean> {
155
    try {
11✔
156
      const octo = await getOctokit(this.appState);
11✔
157
      const gist = revision
11✔
158
        ? await octo.gists.getRevision({ gist_id: gistId, sha: revision })
159
        : await octo.gists.get({ gist_id: gistId });
160

161
      const values: EditorValues = {};
10✔
162

163
      for (const [id, data] of Object.entries(gist.data.files)) {
10✔
164
        const content = data.truncated
55✔
165
          ? await fetch(data.raw_url!).then((r) => r.text())
1✔
166
          : data.content!;
167

168
        if (id === PACKAGE_NAME) {
55✔
169
          const deps: Record<string, string> = {};
5✔
170
          try {
5✔
171
            const { dependencies, devDependencies } = JSON.parse(content);
5✔
172
            Object.assign(deps, dependencies, devDependencies);
5✔
173
          } catch (e) {
174
            throw new Error('Invalid JSON found in package.json');
1✔
175
          }
176

177
          // If the gist specifies an Electron version, we want to tell Fiddle to run
178
          // it with that version by default.
179
          const electronDeps = Object.keys(deps).filter((d) =>
4✔
180
            ['electron-nightly', 'electron'].includes(d),
6✔
181
          );
182
          for (const dep of electronDeps) {
4✔
183
            // Strip off semver range prefixes, e.g:
184
            // ^1.2.0 -> 1.2.0
185
            // ~2.3.4 -> 2.3.4
186
            const index = deps[dep].search(/\d/);
4✔
187
            const version = deps[dep].substring(index);
4✔
188

189
            if (
4✔
190
              !semver.valid(version) ||
191
              !(await window.ElectronFiddle.isReleasedMajor(
192
                semver.major(version),
193
              ))
194
            ) {
195
              await this.appState.showGenericDialog({
3✔
196
                label: `The Electron version (${version}) in this gist's package.json is invalid. Falling back to last used version.`,
197
                ok: 'Close',
198
                type: GenericDialogType.warning,
199
                wantsInput: false,
200
              });
201
            } else if (disableDownload(version)) {
1✔
202
              await this.appState.showGenericDialog({
×
203
                label: `This gist's Electron version (${version}) is not available on your current OS. Falling back to last used version.`,
204
                ok: 'Close',
205
                type: GenericDialogType.warning,
206
                wantsInput: false,
207
              });
208
            } else {
209
              this.setElectronVersion(version);
1✔
210
            }
211

212
            // We want to include all dependencies except Electron.
213
            delete deps[dep];
4✔
214
          }
215

216
          this.appState.modules = new Map(Object.entries(deps));
4✔
217
        }
218

219
        // JSON files are supported, but we don't want to add package.json
220
        // or the lockfile to the visible editor array.
221
        if ([PACKAGE_NAME, 'package-lock.json'].includes(id)) continue;
54✔
222

223
        if (!isSupportedFile(id)) continue;
50✔
224

225
        if (isKnownFile(id) || (await this.confirmAddFile(id))) {
48✔
226
          values[id] = content;
48✔
227
        }
228
      }
229

230
      // If no files were populated into values, the Fiddle did not
231
      // contain any supported files. Throw an error to let the user know.
232
      if (Object.keys(values).length === 0) {
9✔
233
        throw new Error(
1✔
234
          'This Gist did not contain any supported files. Supported files must have one of the following extensions: .cjs, .js, .mjs, .css, or .html.',
235
        );
236
      }
237

238
      const result = await this.handleLoadingSuccess(values, gistId);
8✔
239

240
      // Set the active revision - either the specified revision or the latest one
241
      const activeRevision = revision || gist.data.history?.[0]?.version;
8✔
242
      if (activeRevision) {
11✔
243
        this.appState.activeGistRevision = activeRevision;
×
244
      }
245

246
      return result;
8✔
247
    } catch (error: any) {
248
      return this.handleLoadingFailed(error);
3✔
249
    }
250
  }
251

252
  public async setElectronVersion(version: string): Promise<boolean> {
253
    if (!this.appState.hasVersion(version)) {
4✔
254
      const versionToDownload = {
2✔
255
        source: VersionSource.remote,
256
        state: InstallState.missing,
257
        version,
258
      };
259

260
      try {
2✔
261
        this.appState.addNewVersions([versionToDownload]);
2✔
262
        await this.appState.downloadVersion(versionToDownload);
2✔
263
      } catch {
264
        await this.appState.removeVersion(versionToDownload);
×
265
        this.handleLoadingFailed(
×
266
          new Error(`Failed to download Electron version ${version}`),
267
        );
268
        return false;
×
269
      }
270
    }
271

272
    // check if version is part of release channel
273
    const versionReleaseChannel: ElectronReleaseChannel =
4✔
274
      getReleaseChannel(version);
275

276
    if (!this.appState.channelsToShow.includes(versionReleaseChannel)) {
4✔
277
      const ok = await this.verifyReleaseChannelEnabled(versionReleaseChannel);
1✔
278
      if (!ok) return false;
1✔
279

280
      this.appState.channelsToShow.push(versionReleaseChannel);
1✔
281
    }
282

283
    this.appState.setVersion(version);
4✔
284
    return true;
4✔
285
  }
286

287
  public confirmAddFile = (filename: string): Promise<boolean> => {
×
288
    return this.appState.showConfirmDialog({
×
289
      cancel: 'Skip',
290
      label: `Do you want to add "${filename}"?`,
291
      ok: 'Add',
292
    });
293
  };
294

295
  /**
296
   * Verifies from the user that we should be loading this fiddle.
297
   *
298
   * @param what - What are we loading from (gist, example, etc.)
299
   */
300
  public verifyRemoteLoad(what: string): Promise<boolean> {
301
    return this.appState.showConfirmDialog({
4✔
302
      label: `Are you sure you want to load this ${what}? Only load and run it if you trust the source.`,
303
      ok: 'Load',
304
    });
305
  }
306

307
  public verifyReleaseChannelEnabled(channel: string): Promise<boolean> {
308
    return this.appState.showConfirmDialog({
1✔
309
      label: `You're loading an example with a version of Electron with an unincluded release
310
              channel (${channel}). Do you want to enable the release channel to load the
311
              version of Electron from the example?`,
312
      ok: 'Enable',
313
    });
314
  }
315

316
  /**
317
   * Loading a fiddle from GitHub succeeded, let's move on.
318
   */
319
  private async handleLoadingSuccess(
320
    values: EditorValues,
321
    gistId: string,
322
  ): Promise<boolean> {
323
    await window.app.replaceFiddle(values, { gistId });
9✔
324
    return true;
9✔
325
  }
326

327
  /**
328
   * Loading a fiddle from GitHub failed - this method handles this case
329
   * gracefully.
330
   */
331
  private handleLoadingFailed(error: Error): false {
332
    const failedLabel = `Loading the fiddle failed: ${error.message}`;
6✔
333
    this.appState.showErrorDialog(
6✔
334
      this.appState.isOnline
335
        ? failedLabel
336
        : `Your computer seems to be offline. ${failedLabel}`,
337
    );
338

339
    console.warn(`Loading Fiddle failed`, error);
6✔
340
    return false;
6✔
341
  }
342
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc