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

electron / fiddle / 4407736296

pending completion
4407736296

push

github

GitHub
fix: remote loader loading incompatible versions (#1282)

935 of 1094 branches covered (85.47%)

Branch coverage included in aggregate %.

4 of 4 new or added lines in 1 file covered. (100.0%)

3458 of 3715 relevant lines covered (93.08%)

375.65 hits per line

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

88.62
/src/renderer/remote-loader.ts
1
import { InstallState } from '@electron/fiddle-core';
2✔
2
import semver from 'semver';
2✔
3

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

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

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

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

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

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

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

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

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

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

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

89
      const loaders: Array<Promise<void>> = [];
1✔
90

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

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

108
      await Promise.all(loaders);
1✔
109

110
      return this.handleLoadingSuccess(values, '');
1✔
111
    } catch (error) {
112
      return this.handleLoadingFailed(error);
2✔
113
    }
114
  }
115

116
  /**
117
   * Load a fiddle
118
   */
119
  public async fetchGistAndLoad(gistId: string): Promise<boolean> {
120
    try {
8✔
121
      const octo = await getOctokit(this.appState);
8✔
122
      const gist = await octo.gists.get({ gist_id: gistId });
8✔
123
      const values: EditorValues = {};
7✔
124

125
      for (const [id, data] of Object.entries(gist.data.files)) {
7✔
126
        if (id === PACKAGE_NAME) {
37✔
127
          const { dependencies, devDependencies } = JSON.parse(data.content);
4✔
128
          const deps: Record<string, string> = {
4✔
129
            ...dependencies,
130
            ...devDependencies,
131
          };
132

133
          // If the gist specifies an Electron version, we want to tell Fiddle to run
134
          // it with that version by default.
135
          const electronDeps = Object.keys(deps).filter((d) =>
4✔
136
            ['electron-nightly', 'electron'].includes(d),
6✔
137
          );
138
          for (const dep of electronDeps) {
4✔
139
            // Strip off semver range prefixes, e.g:
140
            // ^1.2.0 -> 1.2.0
141
            // ~2.3.4 -> 2.3.4
142
            const index = deps[dep].search(/\d/);
4✔
143
            const version = deps[dep].substring(index);
4✔
144

145
            if (
4✔
146
              !semver.valid(version) ||
8✔
147
              !isReleasedMajor(semver.major(version))
148
            ) {
149
              throw new Error(
1✔
150
                "This gist's package.json contains an invalid Electron version.",
151
              );
152
            } else if (disableDownload(version)) {
3!
153
              await this.appState.showGenericDialog({
×
154
                label: `This gist's Electron version (${version}) is not available on your current OS. Falling back to last used version.`,
155
                ok: 'Close',
156
                type: GenericDialogType.warning,
157
                wantsInput: false,
158
              });
159
            } else {
160
              this.setElectronVersion(version);
3✔
161
            }
162

163
            // We want to include all dependencies except Electron.
164
            delete deps[dep];
3✔
165
          }
166

167
          this.appState.modules = new Map(Object.entries(deps));
3✔
168
        }
169

170
        if (!isSupportedFile(id)) continue;
36✔
171

172
        if (isKnownFile(id) || (await this.confirmAddFile(id))) {
31✔
173
          values[id] = data.content;
31✔
174
        }
175
      }
176

177
      // If no files were populated into values, the Fiddle did not
178
      // contain any supported files. Throw an error to let the user know.
179
      if (Object.keys(values).length === 0) {
6✔
180
        throw new Error(
1✔
181
          'This Gist did not contain any supported files. Supported files must have one of the following extensions: .js, .css, or .html.',
182
        );
183
      }
184

185
      return this.handleLoadingSuccess(values, gistId);
5✔
186
    } catch (error) {
187
      return this.handleLoadingFailed(error);
3✔
188
    }
189
  }
190

191
  public async setElectronVersion(version: string): Promise<boolean> {
192
    if (!this.appState.hasVersion(version)) {
6✔
193
      const versionToDownload = {
4✔
194
        source: VersionSource.remote,
195
        state: InstallState.missing,
196
        version,
197
      };
198

199
      try {
4✔
200
        this.appState.addNewVersions([versionToDownload]);
4✔
201
        await this.appState.downloadVersion(versionToDownload);
4✔
202
      } catch {
203
        await this.appState.removeVersion(versionToDownload);
×
204
        this.handleLoadingFailed(
×
205
          new Error(`Failed to download Electron version ${version}`),
206
        );
207
        return false;
×
208
      }
209
    }
210

211
    // check if version is part of release channel
212
    const versionReleaseChannel: ElectronReleaseChannel = getReleaseChannel(
6✔
213
      version,
214
    );
215

216
    if (!this.appState.channelsToShow.includes(versionReleaseChannel)) {
6✔
217
      const ok = await this.verifyReleaseChannelEnabled(versionReleaseChannel);
1✔
218
      if (!ok) return false;
1!
219

220
      this.appState.channelsToShow.push(versionReleaseChannel);
1✔
221
    }
222

223
    this.appState.setVersion(version);
6✔
224
    return true;
6✔
225
  }
226

227
  public confirmAddFile = (filename: string): Promise<boolean> => {
46✔
228
    return this.appState.showConfirmDialog({
×
229
      cancel: 'Skip',
230
      label: `Do you want to add "${filename}"?`,
231
      ok: 'Add',
232
    });
233
  };
234

235
  /**
236
   * Verifies from the user that we should be loading this fiddle.
237
   *
238
   * @param {string} what What are we loading from (gist, example, etc.)
239
   */
240
  public verifyRemoteLoad(what: string): Promise<boolean> {
241
    return this.appState.showConfirmDialog({
4✔
242
      label: `Are you sure you want to load this ${what}? Only load and run it if you trust the source.`,
243
      ok: 'Load',
244
    });
245
  }
246

247
  public verifyReleaseChannelEnabled(channel: string): Promise<boolean> {
248
    return this.appState.showConfirmDialog({
1✔
249
      label: `You're loading an example with a version of Electron with an unincluded release
250
              channel (${channel}). Do you want to enable the release channel to load the
251
              version of Electron from the example?`,
252
      ok: 'Enable',
253
    });
254
  }
255

256
  /**
257
   * Loading a fiddle from GitHub succeeded, let's move on.
258
   *
259
   * @param {EditorValues} values
260
   * @param {string} gistId
261
   * @returns {Promise<boolean>}
262
   */
263
  private async handleLoadingSuccess(
264
    values: EditorValues,
265
    gistId: string,
266
  ): Promise<boolean> {
267
    await window.ElectronFiddle.app.replaceFiddle(values, { gistId });
6✔
268
    return true;
6✔
269
  }
270

271
  /**
272
   * Loading a fiddle from GitHub failed - this method handles this case
273
   * gracefully.
274
   *
275
   * @param {Error} error
276
   * @returns {boolean}
277
   */
278
  private handleLoadingFailed(error: Error): false {
279
    const failedLabel = `Loading the fiddle failed: ${error.message}`;
5✔
280
    this.appState.showErrorDialog(
5✔
281
      this.appState.isOnline
282
        ? failedLabel
5!
283
        : `Your computer seems to be offline. ${failedLabel}`,
284
    );
285

286
    console.warn(`Loading Fiddle failed`, error);
5✔
287
    return false;
5✔
288
  }
289
}
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