Compare commits

...
Author SHA1 Message Date
Felix Rieseberg 0a423bc294 feat: Add progress to settings page, too 2020-03-24 11:40:09 -07:00
Felix Rieseberg 98a358b756 Merge pull request #358 from electron/download-progress
Refactor BinaryManager, add download progress indicator
2020-03-24 11:14:55 -07:00
Felix Rieseberg 9b55b2ef26 Merge pull request #357 from electron/no-notarize
build: Only notarize during make
2020-03-23 10:45:16 -07:00
Felix Rieseberg 5b91177348 Merge pull request #359 from electron/update-releases
build: update Electron releases JSON
2020-03-23 10:44:51 -07:00
malept f8c43a07f7 build: update Electron releases JSON 2020-03-23 00:04:50 +00:00
Felix Rieseberg 5eba9cdbe2 feat: A fancy spinner for downloading 2020-03-17 18:11:05 -07:00
Felix Rieseberg 8ee42c290e test: Fix the tests post-refactor 2020-03-17 17:58:25 -07:00
Felix Rieseberg c2b3954dfa chore: Refactor BinaryManager, no more classes 2020-03-17 17:07:25 -07:00
Felix Rieseberg e61599e9c5 chore: Better named interfaces 2020-03-17 16:24:58 -07:00
Felix RiesebergandMark Lee 1fed772a27 Update forge.config.js
Co-Authored-By: Mark Lee <malept@users.noreply.github.com>
2020-03-17 16:13:32 -07:00
Felix RiesebergandMark Lee de85d0f324 Update forge.config.js
Co-Authored-By: Mark Lee <malept@users.noreply.github.com>
2020-03-17 16:13:16 -07:00
Erick Zhao c89671d99d feat: improved bisect commands UX (#352) 2020-03-16 10:55:34 -07:00
Felix Rieseberg 155b901ade build: Only notarize during make 2020-03-16 10:47:00 -07:00
github-actions[bot]andmalept 8e098deab8 build: update Electron releases JSON (#356)
Co-authored-by: malept <electron-bot@users.noreply.github.com>
2020-03-16 02:18:36 +02:00
dependabot[bot] 2b5ea7d467 build(deps): bump acorn from 5.7.1 to 5.7.4
Bumps [acorn](https://github.com/acornjs/acorn) from 5.7.1 to 5.7.4.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/5.7.1...5.7.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-13 23:49:10 -07:00
Vlad Hashimoto 91efa5a009 v0.13.1 2020-03-13 16:45:08 -07:00
Erick Zhao 4ec5bec719 fix: load unsave handlers only after editors mount (#351) 2020-03-12 11:53:29 -07:00
Erick Zhao 84ac41f147 feat: more sensible version filtering (#344) 2020-03-12 11:46:32 -07:00
Erick Zhao e9a2985291 fix: preserve 'downloading' version state post-refresh (#345) 2020-03-12 11:46:24 -07:00
Erick ZhaoandFelix Rieseberg 9b28919203 feat: show inactive window on quit warning (#348)
Co-authored-by: Felix Rieseberg <felix@felixrieseberg.com>
2020-03-12 11:13:08 -07:00
Erick Zhao 8d43836787 fix: ensure you have at least one editor open (#347) 2020-03-11 12:54:16 -07:00
Shelley Vohr 48d0dadf5d chore: fix test:report bailing (#349) 2020-03-11 12:32:03 -07:00
Erick Zhao 2f9ef21251 fix: ensure hidden panels work on first load (#346) 2020-03-11 12:12:09 -07:00
Erick Zhao 8e4238275c refactor: grab release list from unpkg (#339) 2020-03-09 11:45:54 -07:00
Shelley Vohr 32d5ea99aa chore: add test report generation helper (#340) 2020-03-09 09:28:43 -07:00
Felix Rieseberg da2e17cd37 Merge pull request #342 from electron/update-releases
build: update Electron releases JSON
2020-03-08 18:13:01 -07:00
malept 539f5bce97 build: update Electron releases JSON 2020-03-09 00:06:03 +00:00
Mark Lee 228552c8a0 build: create a PR for releases.json updates instead of trying to push to master (#341)
* Move expected version count & latest Electron version to its own
  fixture file
* Automatically update expected version count & latest Electron version
  when updating the JSON file
* Throw an error if a rejection occurs when the fetch releases script
  is run
* reduce releases update frequency to once a week
2020-03-06 16:17:42 -08:00
Felix Rieseberg ed8d76a54a fix: Sentry is a runtime dependency 2020-03-02 17:14:29 -08:00
Felix Rieseberg 2f4d47a0ab 0.13.0 2020-03-02 16:14:25 -08:00
Mark Lee 9b443de137 build: auto-update the Electron Releases JSON file every day (#331) 2020-03-02 16:08:39 -08:00
Felix Rieseberg efb5bf0e57 Merge pull request #334 from electron/travis-cleanup
build: cache/install node deps per Travis docs, remove deprecated option
2020-03-02 16:03:59 -08:00
Mark Lee c420747bb9 build: cache/install node dependencies per Travis docs, remove deprecated option 2020-03-02 15:40:23 -08:00
Felix Rieseberg 33223e7b37 Merge pull request #333 from electron/use-bionic
build: Use Ubuntu 18.04 in Travis CI
2020-03-02 15:35:04 -08:00
Felix Rieseberg 86cc0c62a1 build: Use Bionic 2020-03-02 14:50:40 -08:00
Felix Rieseberg 96bd2e36dd Merge pull request #330 from electron/update-dependencies
Update dependencies
2020-03-02 13:02:16 -08:00
Felix Rieseberg 9db527773b test: Fix the tests 2020-02-27 17:43:16 -08:00
Felix Rieseberg 9da1acafd9 wip 2020-02-27 16:56:51 -08:00
Felix Rieseberg 5a05d34776 chore: Update local Electron releases 2020-02-27 14:42:39 -08:00
Felix Rieseberg 6b9d2b66e2 wrench: Update dependencies 2020-02-27 14:41:44 -08:00
Felix Rieseberg d4096a6de8 Merge pull request #329 from electron/less-listeners
chore: Unregister various listeners
2020-02-26 12:28:55 -08:00
Erick Zhao 3709d122a6 feat: add CSS support (#327) 2020-02-22 11:48:11 -08:00
Felix Rieseberg 0368cda57a Merge pull request #328 from electron/fix/remove-reload
fix: remove broken "Reload" menu item
2020-02-22 08:53:34 -08:00
Felix Rieseberg 433cb6acd5 chore: Use node environment where available 2020-02-22 08:50:56 -08:00
Felix Rieseberg 56dceaefb0 chore: Don't collect coverage locally 2020-02-22 08:35:35 -08:00
Erick Zhao 80da23c4e4 fix: remove broken "Reload" menu item 2020-02-21 10:09:41 -08:00
Felix Rieseberg 353968ef8e Merge pull request #326 from electron/bisect-constraint
feat: Add some help to the bisect
2020-02-19 13:29:05 -08:00
Felix RiesebergandErick Zhao 64ece1b364 Apply suggestions from code review
Co-Authored-By: Erick Zhao <erick@hotmail.ca>
2020-02-19 12:29:20 -08:00
Felix Rieseberg 13383f7345 chore: Unregister various listeners 2020-02-15 20:50:57 -08:00
Felix Rieseberg cae7222636 feat: Add some help to the bisect 2020-02-15 20:11:08 -08:00
Felix Rieseberg 2aabe80f2b Merge pull request #324 from electron/fix-bisect
fix: Reorganize the bisect helper
2020-02-15 18:54:11 -08:00
Erick Zhao d0683f1392 fix: prevent other editors from clipping tooltips (#294) 2020-02-13 11:45:42 -08:00
Felix Rieseberg 389352cefa test: Get some of that coverage back 2020-02-12 20:50:18 -08:00
Felix Rieseberg d04b654c3e fix: Reorganize the bisect helper 2020-02-12 14:24:57 -08:00
Felix Rieseberg 1369c9b35c Merge pull request #318 from electron/sentry
chore: add sentry for crash / js error reporting
2020-02-11 07:56:12 -08:00
Samuel Attard 9957090d25 Update electron.ts 2020-02-05 15:14:25 -08:00
Samuel Attard 8a002fb0a8 chore: add sentry for crash / js error reporting 2020-02-05 13:15:02 -08:00
Mark Lee cae8dde4c8 refactor(show-me): avoid using a global mainWindow variable (#314) 2020-02-03 14:19:04 -08:00
Jeremy Apthorp cb176534f9 fix: stop holding a reference to mainWindow (#312) 2020-01-30 09:37:34 -08:00
Erick Zhao f7fa9a8ffc chore: merge pull request #306 from issacgerges/bisect-success-dialog
feat: bisect success dialog
2020-01-29 09:54:24 -08:00
Issac Gerges 347384a547 chore: improve GenericDialog test coverage 2020-01-28 19:22:58 -08:00
Issac Gerges e272efb3b9 fix: add quoted escapes to paths to prevent param expansion on zsh. Remove dead code. 2020-01-28 19:16:22 -08:00
Mark LeeandFelix Rieseberg 5255bcb842 chore: add contributing guide (#311)
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2020-01-28 18:51:54 -08:00
Felix Rieseberg d3cd53f3e0 Merge pull request #309 from electron/erick/pong-prints-pong
fix: print correct output in ipcRenderer show-me
2020-01-28 17:45:51 -08:00
Erick Zhao 6d28c4c81b fix: print correct output in ipcRender show-me 2020-01-28 14:55:56 -08:00
Issac Gerges a45354e1af chore: quote lint glob to prevent zsh to parse it as parameter expansion 2020-01-27 18:25:50 -08:00
Issac Gerges 28245d36ba refactor: rename DialogType 'message' to 'success' 2020-01-27 14:48:30 -08:00
Issac Gerges 582ef280b5 Merge remote-tracking branch 'upstream/master' into bisect-dialog 2020-01-27 13:00:48 -08:00
Issac Gerges 56b7f2ffb7 refactor: replace warning/confirmation dialogs with generic dialog and type parameter 2020-01-27 12:58:11 -08:00
issacgerges cb3ba4f0a7 chore: Add issacgerges as a contributor (#304) 2020-01-26 13:57:32 -08:00
Felix Rieseberg e238b10f1e Merge pull request #303 from issacgerges/fix-remote-warning
fix: Make example warning more descriptive
2020-01-26 08:59:37 -08:00
Felix Rieseberg bbc22d9ca8 Merge pull request #301 from issacgerges/support-original-fs
fix: Add original-fs to ignored modules
2020-01-26 08:58:34 -08:00
issacgerges 7b3c960c1f chore: fix autoUpdater example to remove spurious event (#302)
Fixes #283
2020-01-25 22:44:06 -08:00
Issac Gerges 4dbc74fccb fix: Make example warning more descriptive by adding path, remove repeated text. Fixes #262. 2020-01-25 22:02:57 -08:00
Issac Gerges f71b1bc8d4 fix: Add original-fs to ignored modules. Fixes #231 2020-01-25 11:36:25 -08:00
Issac Gerges 00c2e1f9a6 style: fixup jsdoc for BisectDialog 2020-01-25 10:28:43 -08:00
Issac Gerges 50f69613dd feat: Add bisect complete dialog. Fixes #299. 2020-01-25 10:28:35 -08:00
Felix Rieseberg 4a23b53b1b 0.12.0 2020-01-15 12:38:05 -08:00
Felix Rieseberg dcd21935a9 Merge pull request #293 from electron/fix-loading-preload
fix: Create initial backup for preload editor
2020-01-15 12:36:43 -08:00
Felix Rieseberg ff37d5cfb9 test: Add tests for new code 2020-01-15 12:10:52 -08:00
Felix Rieseberg bb8053d86d fix: Create a backup for the preload editor 2020-01-15 12:09:59 -08:00
Mark Lee 5efaa54255 feat: publish preload scripts to GitHub gists (#292)
* fix: update welcome tour to mention preload scripts
* chore: update doc comments
2020-01-14 13:41:57 -08:00
Felix Rieseberg b38507230d Merge pull request #291 from electron/update-deps
chore: Update various dependencies
2020-01-14 13:30:30 -08:00
Felix Rieseberg 7451083c70 test: Fix one more test case 2020-01-14 12:16:48 -08:00
Felix Rieseberg be46780674 build: Update tslint rules 2020-01-13 10:55:01 -08:00
Felix Rieseberg 99b5d067df build: Update stylelint 2020-01-13 10:54:25 -08:00
Felix Rieseberg f34eb225ea feat: Update fallback releases 2020-01-13 10:52:49 -08:00
Felix Rieseberg 18cc36a837 feat: Update blueprint 2020-01-13 10:52:23 -08:00
Felix Rieseberg da403e3cbd feat: Update various dependencies 2020-01-13 10:51:04 -08:00
Felix Rieseberg b4431cf426 Merge pull request #285 from electron/direct-fetch-builtins
refactor: directly pull Node.js builtins
2020-01-13 10:46:01 -08:00
Felix Rieseberg e714269b9b Merge pull request #286 from electron/fix-jest-transform
test: avoid transforming JSON files
2020-01-13 10:44:57 -08:00
Felix Rieseberg b9518a63f8 Merge pull request #287 from Loseshape0/patch-1
Update Years
2020-01-13 10:44:28 -08:00
Felix Rieseberg 0627801bb0 Merge pull request #288 from electron/allow-execution-flags
feat: allow users to specify cli flags
2020-01-13 10:43:57 -08:00
Shelley Vohr 3adeece64d spec: fix small test details 2020-01-06 10:52:07 -05:00
Shelley Vohr aed45078c5 spec: update execution settings snapshot 2020-01-06 10:39:10 -05:00
Shelley Vohr 55bfb0241b add handleExecutionFlagChange spec 2020-01-04 21:13:52 -05:00
Shelley Vohr 7e90679706 feat: allow users to specify cli flags 2020-01-04 10:46:59 -05:00
Loseshape0 4897bdbf84 Update Years 2020-01-01 17:41:48 -10:00
Mark Lee 7a02f8cacd test: avoid transforming JSON files 2019-12-27 22:49:16 -08:00
Shelley Vohr a65d594fbc refactor: directly pull Node.js builtins 2019-12-19 21:17:38 -08:00
Felix Rieseberg f32fce2f80 Merge pull request #281 from electron/fix-dropdowns
fix: Incorrect dropdowns
2019-12-12 14:56:25 -05:00
Felix Rieseberg 26e054702a fix: Incorrect dropdowns 2019-12-12 14:23:11 -05:00
Felix Rieseberg 16f27132cb Revert "First basis for having Node in there, too"
This reverts commit 7613176f69.
2019-12-12 14:06:34 -05:00
Felix RiesebergandTierney Cyren 7613176f69 First basis for having Node in there, too
Co-authored-by: Tierney Cyren <ticyren@microsoft.com>
2019-12-11 12:19:16 -05:00
dependabot[bot] 470bea4fca build(deps): bump serialize-to-js from 3.0.0 to 3.0.1 (#280)
Bumps [serialize-to-js](https://github.com/commenthol/serialize-to-js) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/commenthol/serialize-to-js/releases)
- [Commits](https://github.com/commenthol/serialize-to-js/compare/v3.0.0...v3.0.1)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-10 00:30:59 -08:00
Felix Rieseberg 363febdb73 Merge pull request #279 from electron/notarize
build: Notarize the build with Apple
2019-12-04 13:43:38 -08:00
Felix Rieseberg 062ca4e205 build: Update to electron-forge b46 2019-12-04 10:44:13 -08:00
Felix Rieseberg 49053342a5 Merge pull request #278 from electron/electron-713
chore: Update Electron to 7.1.3
2019-12-04 10:09:10 -08:00
Felix Rieseberg fd582c67a8 build: Use a later macOS image 2019-12-03 18:08:51 -08:00
Felix Rieseberg 706b18808b build: Notarize the build with Apple 2019-12-03 17:52:52 -08:00
Felix Rieseberg a6ae8d5376 chore: Update Electron to 7.1.3 2019-12-03 17:45:02 -08:00
Erick Zhao 5034f8c853 feat: add bisect helper (#257) 2019-11-22 18:46:58 -05:00
Letra e916247013 fix: ensure dev tools only open in dev mode (#277) 2019-11-15 09:22:14 -05:00
Felix Rieseberg c052686de4 0.11.1 2019-11-12 14:39:38 +01:00
Erick Zhao 31aacef131 fix: update folder selection dialog for Electron 7 (#271) 2019-11-05 16:51:04 -05:00
Erick Zhao 932369b705 fix: update deprecated app.setName API (#273) 2019-11-05 16:24:43 -05:00
Shelley Vohr b835c34786 fix: update dialog example for promisification (#272) 2019-11-03 11:36:00 -05:00
Felix Rieseberg c9e1ba815a 0.11.0 2019-10-31 11:49:23 -04:00
Felix Rieseberg 374a85e2f6 Merge pull request #267 from electron/updates-updates
feat: Electron 7 (and more updates)
2019-10-27 11:26:14 -07:00
Felix Rieseberg 4282d0b93d fix: Better typing 2019-10-27 11:16:55 -07:00
Felix Rieseberg 549862b3bb chore: npm audit fix 2019-10-27 11:13:02 -07:00
Felix Rieseberg 4691c1b494 chore: Use a lockfile with latest npm 2019-10-27 11:10:36 -07:00
Felix Rieseberg 8125abc657 build: Update AppVeyor to Node 12 2019-10-27 11:01:39 -07:00
Felix Rieseberg 8cba7739f5 fix: Prepare for Electron 7 2019-10-25 11:59:05 -07:00
Felix Rieseberg 0b39fe40b1 chore: Update all the packages 2019-10-25 11:37:34 -07:00
Felix Rieseberg cae078ffe6 Merge pull request #266 from electron/ignored-commented-out-modules
fix: don't install commented-out requires
2019-10-15 18:14:33 -07:00
Ankit Arun bfb931435f fix: add whitespace to Welcome Tour (#263) 2019-10-14 20:48:40 -04:00
Shelley Vohr c878f72f2e fix: don't install commented-out requires 2019-10-14 13:59:00 -07:00
Felix Rieseberg 32494c057a Merge pull request #254 from electron/preload
feat: Enable support for a `preload.js`
2019-10-05 14:52:58 -10:00
Jason Zeng e732685fff fix: link to docs for content tracing show-me (#256) 2019-09-25 10:16:44 -04:00
Felix Rieseberg d9d5e0cbc9 test: Fix the tests 2019-09-17 16:14:38 -07:00
Felix Rieseberg 0e1d7b541e feat: If a file is newly empty, remove it 2019-09-17 15:59:27 -07:00
Felix Rieseberg 8a602d94b7 feat: Enable support for a preload.js 2019-09-17 15:50:49 -07:00
Erick Zhao 155889dbcc fix: ensure state consistency when replacing Fiddle (#253) 2019-09-16 14:53:53 -04:00
Felix Rieseberg 56ac6662b6 Merge pull request #248 from electron/add-new-themes
feat: ability to add new editor themes to Fiddle
2019-09-10 13:40:08 -04:00
Felix Rieseberg adb196549b chore: Bump version to 0.10 2019-09-08 15:45:59 -07:00
Felix Rieseberg 999facf062 Merge pull request #250 from electron/fix-console-stuff
fix: Console button / display issues
2019-09-08 15:39:55 -07:00
Felix Rieseberg fa0115f2db fix: Correctly toggle console state 2019-09-08 15:29:15 -07:00
Carlos Valdez c0a4c0e48d remove unused import 2019-09-07 11:05:13 -05:00
Carlos Valdez 3a5d2205fe remove unused imports 2019-09-07 11:03:27 -05:00
Carlos Valdez 1083b8f859 add warning dialog 2019-09-07 11:03:11 -05:00
Carlos Valdez 252d4c1de6 remove old code (yikes) 2019-09-07 10:25:10 -05:00
Carlos Valdez 0c44d2c9e4 remove check 2019-09-05 23:54:11 -05:00
Carlos Valdez 5eebc06514 add if statement inside try catch 2019-09-05 18:27:04 -05:00
Carlos Valdez eac1423177 remove comment 2019-09-05 18:18:44 -05:00
Carlos Valdez ae88780281 remove comment 2019-09-05 18:07:56 -05:00
Carlos Valdez 4bf9d48d70 remove commet 2019-09-05 18:07:36 -05:00
Carlos Valdez df6d4b5a9a remove monaco from observable variable name 2019-09-05 17:41:49 -05:00
Carlos Valdez 68202e2b23 change file name and change import 2019-09-05 17:23:40 -05:00
Felix Rieseberg acd5b35ca1 fix: Console button / display issues 2019-08-31 07:07:20 -07:00
Felix Rieseberg fae1ca3138 Merge pull request #249 from electron/update-dependencies
chore: Update all dependencies
2019-08-31 00:25:17 +02:00
Felix Rieseberg bd220d2690 chore: Update more dependencies 2019-08-30 15:07:07 -07:00
Felix Rieseberg d31f1016b8 test: Update all the tests 2019-08-30 15:05:07 -07:00
Felix Rieseberg 3b52be0f79 chore: Update templates for Standard 2019-08-28 09:44:10 +02:00
Felix Rieseberg 0e11c56f7f chore: Update all dependencies 2019-08-28 09:43:52 +02:00
Carlos Valdez 8019981d1a missing ; 2019-08-19 13:29:09 -07:00
Carlos Valdez 403a58e216 update snapshot 2019-08-19 13:28:42 -07:00
Carlos Valdez 3341d0ccaa fix json parsing 2019-08-19 11:13:32 -07:00
Carlos Valdez d42e83100a need to work on successful input 2019-08-16 17:58:27 -07:00
Carlos Valdez 50e9883d82 add new popup using react! 😰 2019-08-16 17:37:22 -07:00
Carlos Valdez f5aedac6a3 change wording of import monaco themes and add better formatting 2019-08-15 11:09:13 -07:00
Carlos Valdez a3f4aa8ed3 remove old code 2019-08-15 10:02:30 -07:00
Carlos Valdez 9c839aaad8 remove sad code 2019-08-15 09:57:30 -07:00
Carlos Valdez 3a7ca8ccd5 Merge branch 'add-new-themes' of github.com:electron/fiddle into add-new-themes 2019-08-15 09:56:15 -07:00
Carlos Valdez fef42293bc account for user checking an incorrect file 2019-08-15 09:56:02 -07:00
Felix Rieseberg 1f9db42c80 Merge pull request #247 from electron/feat/local-version-intellisense
feat: intellisense for local builds
2019-08-14 14:51:18 -07:00
Carlos Valdez 8e5657ef2d the coolest recursive function thanks to Samuel 2019-08-14 13:34:43 -07:00
Carlos Valdez a94105dd27 the coolest recursive function thanks to Samuel 2019-08-14 13:34:29 -07:00
Carlos Valdez cf214dc0d0 remove bad method 2019-08-14 11:03:54 -07:00
Carlos Valdez 6dfdf00437 add in new function and button 2019-08-14 11:03:36 -07:00
Carlos Valdez b3743a4c73 Merge branch 'add-new-themes' of github.com:electron/fiddle into add-new-themes 2019-08-13 17:07:15 -07:00
Carlos Valdez 3c0d6c2293 fix my mess 2019-08-13 17:06:23 -07:00
CarlosandErick Zhao 89b589fd2e Update tests/renderer/components/__snapshots__/settings-general-appearance-spec.tsx.snap
Co-Authored-By: Erick Zhao <erick@hotmail.ca>
2019-08-13 16:53:48 -07:00
CarlosandErick Zhao 42c577988d Update src/renderer/components/settings-general-appearance.tsx
Co-Authored-By: Erick Zhao <erick@hotmail.ca>
2019-08-13 16:53:37 -07:00
CarlosandErick Zhao d0e47b6b4d Update src/renderer/components/settings-general-appearance.tsx
Co-Authored-By: Erick Zhao <erick@hotmail.ca>
2019-08-13 16:41:34 -07:00
CarlosandErick Zhao f44a127012 Update tests/renderer/components/__snapshots__/settings-general-appearance-spec.tsx.snap
Co-Authored-By: Erick Zhao <erick@hotmail.ca>
2019-08-13 16:41:22 -07:00
Carlos Valdez fa2f5454ce don't throw! 2019-08-13 11:24:12 -07:00
Carlos Valdez 1021bb8b2b remove console.warn 2019-08-13 10:34:40 -07:00
Carlos Valdez cf4ad36ca2 remove optional arg 2019-08-13 10:33:38 -07:00
Carlos Valdez 4fe9c627b5 if json has name, set that as the filename otherwise take the filename 2019-08-13 10:25:21 -07:00
Carlos Valdez 262e636e8d update snapshot 2019-08-12 17:53:26 -07:00
Carlos Valdez bbe79ed6e2 added description 2019-08-12 15:32:46 -07:00
Carlos Valdez bc96a5013a added new VS Code option 2019-08-12 15:32:37 -07:00
Erick Zhao 66ba7b74d5 test: add spec 2019-08-12 10:13:08 -07:00
Erick Zhao 38e5aa98e3 test: remove '.only' 2019-08-09 13:06:28 -07:00
Erick Zhao a91504b173 feat: local typedefs 2019-08-09 13:05:07 -07:00
Erick Zhao 4685e88c86 style: run linter 2019-08-09 11:27:08 -07:00
Erick Zhao e0bb82d919 fix: throw appropriately if cannot fetch types 2019-08-08 15:43:14 -07:00
Erick Zhao 7035be3286 fix: adjust styling to avoid hiding intellisense (#243) 2019-07-30 10:43:44 -07:00
Erick Zhao c961cfadb8 chore: merge in settings UX improvements 2019-07-30 10:43:10 -07:00
Erick Zhao 7267a2ee88 fix: load empty files upon opening a fiddle (#241)
* fix: load empty files upon opening a fiddle
* test: add spec for no value passed to setValue
2019-07-30 10:42:16 -07:00
Erick Zhao bc03382dc5 test: test for event listener removal upon unmount 2019-07-29 08:48:59 -07:00
Erick Zhao 891c33ba68 fix: refresh theme list upon creation 2019-07-28 20:02:35 -07:00
Erick Zhao e7260c74a4 feat: close settings panel on Esc 2019-07-28 20:02:35 -07:00
Erick Zhao ed694a7044 chore: describe correct component in spec 2019-07-28 20:02:35 -07:00
Erick Zhao 14fc77c08c chore: 'Exection' -> 'Execution' 2019-07-28 20:02:35 -07:00
Erick Zhao f3079648cf fix: clean up typedefs when removing binaries (#240) 2019-07-26 18:03:57 -07:00
Felix Rieseberg 7272ec7899 Merge pull request #239 from electron/fix/consistent-tab-size
fix: enforce 2-space indents in all editors
2019-07-26 10:25:46 -07:00
Erick Zhao ac9ac701f7 test: add spec for forced tab size 2019-07-25 13:16:56 -07:00
Erick Zhao 7fce572823 fix: enforce tab-size 2 2019-07-25 12:40:53 -07:00
Erick Zhao 6186a80dad chore: 'monoaco' -> 'monaco' (#238) 2019-07-25 11:53:20 -07:00
Mark Lee ade6576285 build: upgrade Electron Forge to 6.0.0-beta.42 (#234) 2019-07-17 23:22:06 -07:00
Felix Rieseberg 61c58277f3 Merge pull request #233 from electron/chore/toogle
chore: `toogle` -> `toggle`
2019-07-17 19:48:00 -07:00
Erick Zhao c361d34a6d chore: toogle -> toggle 2019-07-16 17:54:02 -07:00
Erick Zhao 5887a6615a Merge pull request #232 from electron/feat/protocol-force-electron-version
feat: force electron version when loading from example protocol
2019-07-16 10:34:05 -07:00
Erick Zhao 7ba4cf5be7 style: run linter 2019-07-15 16:47:36 -07:00
Erick Zhao 0daf872d2b chore: conflict resolution w/ confirm prompt 2019-07-15 16:34:05 -07:00
Erick Zhao 75ec8ee9b1 chore: merge in master 2019-07-15 16:24:00 -07:00
Erick Zhao 183c152e40 test: create spec for remote-loader 2019-07-15 16:18:04 -07:00
Erick Zhao d6a807ba41 refactor: move version checking to its own fn 2019-07-15 15:23:30 -07:00
Carlos 24e0abf667 Merge pull request #230 from electron/warning-prompt
feat: Warning prompt
2019-07-15 14:05:37 -07:00
Carlos Valdez eb8261e240 remove obsolete snapshot 2019-07-15 13:38:20 -07:00
Erick Zhao 84cc07da90 feat: load examples with specific electron version 2019-07-15 13:21:44 -07:00
Erick Zhao b0f3837b47 refactor: move remote loading to its own file 2019-07-15 13:20:56 -07:00
Carlos Valdez e29664ca45 fix added event spec 2019-07-15 13:18:55 -07:00
Carlos Valdez ef36ee39f6 - 2019-07-15 11:14:56 -07:00
Carlos Valdez 73dadb815e removed spec test 2019-07-15 11:14:45 -07:00
Carlos Valdez 4945501703 fix test spec 2019-07-15 11:02:08 -07:00
Erick Zhao 77c87fbdeb feat: console resizing (#226)
Adds a `react-mosaic` wrapper to the console component to enable resizing in a similar
fashion to the editor components.
2019-07-15 10:33:01 -07:00
Carlos Valdez 2ef5b42f4a solved conflicts on git rebase 2019-07-15 10:31:07 -07:00
Carlos Valdez 9c4bc2ba52 fix test-spec failing 2019-07-15 10:30:31 -07:00
Carlos Valdez 80da4e73e3 fixed specs 2019-07-15 10:30:31 -07:00
Carlos Valdez eadd58d149 fix test 2019-07-15 10:28:09 -07:00
Carlos Valdez 1fc82f8f6d update specs 2019-07-15 10:28:09 -07:00
CarlosandFelix Rieseberg 9bd7043b2c Update src/renderer/components/dialog-confirm.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-15 10:28:09 -07:00
Carlos aaeef6fd06 Update commands-address-bar.tsx 2019-07-15 10:28:09 -07:00
CarlosandFelix Rieseberg 1195902bd5 Update src/renderer/components/dialog-warning.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-15 10:28:09 -07:00
CarlosandFelix Rieseberg c022d0f33d Update src/renderer/components/dialog-warning.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-15 10:28:09 -07:00
CarlosandFelix Rieseberg 996ae775f3 Update src/renderer/components/commands-address-bar.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-15 10:28:09 -07:00
Carlos Valdez ed44dda8fb better label 2019-07-15 10:28:09 -07:00
Carlos Valdez effba13e73 add to dialog class 2019-07-15 10:28:09 -07:00
Carlos Valdez 65886c3b52 added dialog-confirm prompt for users 2019-07-15 10:28:09 -07:00
Carlos Valdez 8bd4dd366a fix test-spec failing 2019-07-15 10:16:46 -07:00
Erick Zhao 4e19578e34 feat: add support for unchecked release channels 2019-07-14 18:45:18 -07:00
Erick Zhao c7fd0bc95e feat: force electron version when load w/ protocol 2019-07-14 18:45:18 -07:00
Erick Zhao 29e2f03b1a feat: allow version utility fn to accept a string 2019-07-14 18:45:18 -07:00
Felix Rieseberg 4cd6f330da Merge pull request #227 from electron/deermichel/error-msg
chore: add offline notice when loading fiddles
2019-07-13 16:10:27 -07:00
Carlos Valdez 79521eac7c fixed specs 2019-07-12 16:03:28 -07:00
Carlos Valdez 2736499d68 fix test 2019-07-12 15:36:14 -07:00
Carlos Valdez 1cbb781e8d update specs 2019-07-12 15:25:02 -07:00
CarlosandFelix Rieseberg e849a49f6f Update src/renderer/components/dialog-confirm.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-12 14:48:40 -07:00
Carlos 0114d2c1eb Update commands-address-bar.tsx 2019-07-12 14:48:09 -07:00
CarlosandFelix Rieseberg 365537012f Update src/renderer/components/dialog-warning.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-12 14:45:00 -07:00
CarlosandFelix Rieseberg 3e435cb64d Update src/renderer/components/dialog-warning.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-12 14:44:55 -07:00
CarlosandFelix Rieseberg 6981382793 Update src/renderer/components/commands-address-bar.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-12 14:42:53 -07:00
Felix Rieseberg f293ba84d1 Merge pull request #229 from electron/feat/protocol-dev-mode-support
feat: add dev mode support for protocol
2019-07-12 14:41:05 -07:00
Carlos Valdez 55cf507a96 better label 2019-07-12 14:23:21 -07:00
Carlos Valdez e71c2439cd add to dialog class 2019-07-12 13:49:52 -07:00
Carlos Valdez 9bd253951c added dialog-confirm prompt for users 2019-07-12 13:49:40 -07:00
Erick Zhao eda08c9aea feat: change args passed in to support yarn
NPM and Yarn both forward arguments via `process.env.npm_config_argv`.
The URL would be in the `remain` property in NPM, but in `original`
in both NPM and Yarn.
2019-07-12 11:50:35 -07:00
Felix Rieseberg 9414f66893 Merge pull request #228 from electron/no-prerelease
build: Don't mark releases as prereleases
2019-07-12 11:44:40 -07:00
Erick Zhao f4725b5dd8 feat: add dev mode support for protocol 2019-07-12 11:35:18 -07:00
Felix Rieseberg fd8be47494 build: Don't mark releases as prereleases 2019-07-12 11:10:09 -07:00
Micha Hanselmann 2cebe2ca7c add more verbose msg when offline 2019-07-12 11:07:51 -07:00
Felix Rieseberg 74135bab67 0.9.0 2019-07-08 10:53:09 -07:00
Felix Rieseberg a856340d01 Merge pull request #225 from electron/update-dependencies
chore: Update the dependencies
2019-07-08 10:52:25 -07:00
Felix Rieseberg bd8939c358 chore: Update the dependencies 2019-07-07 17:15:43 -07:00
Felix Rieseberg 9d3f3d61eb Merge pull request #222 from erickzhao/fix/browserwindow-showme-script
fix: correct button code for BrowserWindow show-me
2019-07-06 12:46:07 -07:00
Felix Rieseberg bdeaffcb7d Merge pull request #223 from DeerMichel/deermichel/nightlies
fix: fetch nightlies from `electron-nightlies` channel
2019-07-06 12:45:40 -07:00
Andrew Cherniavskii 344e1df375 fix: console message in powermonitor example 2019-07-06 09:34:03 -07:00
Micha Hanselmann d5b52dcae7 fix nightlies fetch 2019-07-05 13:48:07 -07:00
Erick Zhao 0faeb24761 fix: correct button code for BrowserWindow show-me 2019-07-03 15:23:07 -07:00
Micha Hanselmann 284b579d0c fix out-of-scope tray ref (#221) 2019-07-03 11:14:39 -05:00
Felix Rieseberg 5a1c24cdda Merge pull request #220 from erickzhao/feat/publish-ux-fixes
feat: clarify UX for Publish Gist feature
2019-07-02 10:13:02 -07:00
Erick Zhao 024d48a273 test: add spec for publishing gist disabled form 2019-07-01 22:54:44 -07:00
Erick Zhao 32155d652d chore: update enzyme version 2019-07-01 22:54:04 -07:00
Erick Zhao 874a8bb0f0 test: Move isPublishing from local state to store 2019-07-01 21:15:18 -07:00
Erick Zhao 73470640c7 test: Update snapshots to match DOM changes 2019-07-01 21:15:18 -07:00
Erick Zhao 679f731f4c chore: Consistent use of "publish" keyword 2019-07-01 21:15:18 -07:00
Erick Zhao a41ad11f8c chore: Remove "public" from gist setting callout
Users can either set their published Gists as public or private, so it's
best to remove the "public" qualifier from the callout.
2019-07-01 21:15:18 -07:00
Erick Zhao 7cc6d38dc2 feat: Disable fields while publishing gists 2019-07-01 21:15:18 -07:00
Felix Rieseberg 1e6ee7ab37 Merge pull request #216 from drewswaycool/add-missing-spaces
adds missing spaces
2019-06-27 15:43:25 -07:00
Felix Rieseberg c50c31ad80 Merge pull request #219 from electron/bypass-contributor-check
build: Don't throw if missing contributors in PR
2019-06-27 15:35:20 -07:00
Felix Rieseberg 5f8557fb13 build: Don't throw if missing contributors in PR 2019-06-24 19:51:34 -07:00
Felix Rieseberg 47b20fa3e4 Merge pull request #218 from DeerMichel/deermichel/clear-console
feat: add ability to clear console output
2019-06-24 19:47:21 -07:00
Micha Hanselmann b5c8ed60a9 remove clear console top bar button 2019-06-17 21:16:23 -07:00
Micha Hanselmann 5e3d382b85 minor style fix 2019-06-17 09:41:57 -07:00
Micha Hanselmann fed0107dd1 i'm a contributer now... 2019-06-16 00:11:31 -07:00
Micha Hanselmann ca9b9c633a add clear-on-run test 2019-06-15 23:50:36 -07:00
Micha Hanselmann fb4c6b3f33 add ui tests for console settings 2019-06-15 23:38:29 -07:00
Micha Hanselmann aa51c0cc5c implement clear on run 2019-06-15 23:28:20 -07:00
Micha Hanselmann e5236c5500 add settings ui for clear-on-run option 2019-06-15 23:22:49 -07:00
Micha Hanselmann 321f7a8bbc fix top bar buttons test 2019-06-15 23:02:28 -07:00
Micha Hanselmann 1ff7a3fe16 fix context menu test 2019-06-15 23:00:04 -07:00
Micha Hanselmann a1c74c018d add test for clearConsole 2019-06-15 22:53:48 -07:00
Micha Hanselmann 5df39ea51a add context menu entry to clear console 2019-06-15 22:41:44 -07:00
Micha Hanselmann 8a9591be9f add clear console button to top bar 2019-06-15 22:37:25 -07:00
Micha Hanselmann 3cc8fd41f6 add clearConsole method 2019-06-15 22:29:32 -07:00
Drew McMurry 25df3dd989 adds missing spaces 2019-06-14 15:38:39 -06:00
Felix Rieseberg abe7bdab23 Merge pull request #213 from electron/fix-touch-bar-example
fix: correct touch bar constructor in example
2019-06-11 21:16:44 -04:00
Shelley Vohr 16bea01ba5 fix: correct touch bar constructor in example 2019-06-11 14:17:44 -07:00
Felix Rieseberg 7417a82931 0.8.1 2019-06-11 11:35:40 -04:00
Felix Rieseberg 4c41db8595 Merge pull request #212 from electron/no-download-test
test: Mock electron-download
2019-06-11 17:35:23 +02:00
Felix Rieseberg 5da87ffca3 Merge pull request #211 from electron/fix-menu
fix: Don't attempt to set about panel on Windows
2019-06-11 17:05:07 +02:00
Felix Rieseberg 919e9cb6b6 test: Mock electron-download 2019-06-11 11:02:09 -04:00
Felix Rieseberg 14279283d6 fix: Don't attempt to set about panel on Windows 2019-06-11 10:51:42 -04:00
Felix Rieseberg 77a98112a1 Merge pull request #209 from amorist/fix-submit-token
Fix: submit token
2019-06-11 16:43:05 +02:00
amorist 08fb3f52bf fixed submit token 2019-06-11 14:27:47 +08:00
Felix Rieseberg b37ba895e5 0.8.0 2019-06-10 12:09:48 -04:00
209 changed files with 17794 additions and 8026 deletions
+31
View File
@@ -0,0 +1,31 @@
const { Octokit } = require('@octokit/action')
const octokit = new Octokit()
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/')
async function main () {
const prs = await octokit.pulls.list({
owner,
repo,
state: 'open',
head: `${owner}:update-releases`
})
if (prs.data.length === 0) {
const pr = await octokit.pulls.create({
owner,
repo,
title: 'build: update Electron releases JSON',
base: 'master',
head: 'update-releases',
body: 'Auto-update from GitHub Actions.',
maintainer_can_modify: true
})
console.log('Pull request created:', pr.html_url)
} else {
console.log('Pull request updated:', prs.data[0].html_url)
}
}
main()
+51
View File
@@ -0,0 +1,51 @@
name: Auto-update Releases JSON file
on:
schedule:
- cron: '0 0 * * 1'
jobs:
autoupdate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Fetch git branches
run: git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
- uses: actions/setup-node@v1
with:
node-version: '12.x'
- name: Get npm cache directory
id: npm-cache
run: |
echo "::set-output name=dir::$(npm config get cache)"
- uses: actions/cache@v1
with:
path: ${{ steps.npm-cache.outputs.dir }}
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm ci
- name: Switch to release update branch
run: |
if git branch --remotes | grep -q origin/update-releases; then
git checkout update-releases
else
git checkout -b update-releases
fi
- name: Update Releases JSON
run: npm run electron-releases
- name: Commit Changes to Releases JSON
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "machine github.com login $GITHUB_ACTOR password $GITHUB_TOKEN" > ~/.netrc
chmod 600 ~/.netrc
git add static/releases.json tests/fixtures/releases-metadata.json
if test -n "$(git status -s)"; then
git config user.name "$GITHUB_ACTOR"
git config user.email "electron-bot@users.noreply.github.com"
git diff --cached
git commit -m "build: update Electron releases JSON"
git push origin update-releases
node --unhandled-rejections=strict .github/actions/create_releases_pr.js
else
echo No update needed
fi
+3
View File
@@ -31,6 +31,9 @@ yarn-error.log
# Coverage
coverage
# Test report
report.json
# npm package
/npm/dist
/npm/path.txt
+4 -5
View File
@@ -3,13 +3,12 @@ node_js: "12"
os:
- linux
- osx
dist: trusty
osx_image: xcode8.3
sudo: false
dist: bionic
osx_image: xcode10
cache:
npm: true
directories:
- node_modules
- $HOME/.cache/electron
addons:
@@ -24,7 +23,7 @@ branches:
- /^v\d+\.\d+\.\d+/
install:
- npm install
- npm ci
- |
if [[ "$TRAVIS_OS_NAME" == "osx" && "$TRAVIS_SECURE_ENV_VARS" == "true" ]]; then
export CERTIFICATE_P12=cert.p12;
+57
View File
@@ -0,0 +1,57 @@
# Contributing to Electron Fiddle
Electron Fiddle is a community-driven project, overseen by the [Electron Ecosystem Working
Group](https://github.com/electron/governance/tree/master/wg-ecosystem#readme). As such, we welcome
and encourage all sorts of contributions. They include, but are not limited to:
- Constructive feedback
- Bug reports / technical issues
- Documentation changes
- Feature requests
- [Pull requests](#filing-pull-requests)
We strongly suggest that before filing an issue, you search through the existing issues to see
if it has already been filed by someone else.
This project is a part of the Electron ecosystem. As such, all contributions to this project follow
[Electron's code of conduct](https://github.com/electron/electron/blob/master/CODE_OF_CONDUCT.md)
where appropriate.
## Contribution Suggestions
We use the label [`good first issue`](https://github.com/electron/fiddle/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) in the issue tracker to denote fairly-well-scoped-out bugs or feature requests that the community can pick up and work on. If any of those labeled issues do not have enough information, please feel free to ask constructive questions. (This applies to any open issue.)
## Filing Pull Requests
Here are some things to keep in mind as you file pull requests to fix bugs, add new features, etc.:
* If you're unfamiliar with forking, branching, and pull requests, please see [GitHub's extensive
documentation](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests).
* Travis CI and AppVeyor are used to make sure that any new or changed code meets the project's
style guidelines, and that the project's testsuite passes for each new commit.
* Unless it's impractical, please write tests for your changes. This will help us so that we can
spot regressions much easier.
* When creating commit messages and pull request titles, please adhere to the [conventional
commits](https://www.conventionalcommits.org/en/v1.0.0/) standard.
* Please **do not** bump the version number in your pull requests, the maintainers will do that.
Feel free to indicate whether the changes require a major, minor, or patch version bump, as
prescribed by the [semantic versioning specification](http://semver.org/).
### Running Fiddle From Source
```sh
git clone https://github.com/electron/fiddle
cd fiddle
npm install
npm start
```
### Running Tests
```sh
npm test
```
## Release Process
<!-- TODO @felix add your release process here 😁 -->
+2 -2
View File
@@ -1,4 +1,4 @@
Copyright (c) 2013-2018 GitHub Inc.
Copyright (c) 2013-2020 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
@@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+2 -2
View File
@@ -5,8 +5,8 @@
Electron Fiddle lets you create and play with small Electron experiments. It
greets you with a quick-start template after opening change a few things,
choose the version of Electron you want to run it with, and play around. Then,
save your Fiddle either as a GitHub Gist or to a local folder. Once pushed to
GitHub, anyone can quickly try your Fiddle out by just entering it in the
save your Fiddle either as a GitHub Gist or to a local folder. Once published
on GitHub, anyone can quickly try your Fiddle out by just entering it in the
address bar.
<h3 align="center">
+1 -1
View File
@@ -1,6 +1,6 @@
environment:
matrix:
- nodejs_version: "10"
- nodejs_version: "12"
init:
- git config --global core.symlinks true
+46 -3
View File
@@ -7,7 +7,7 @@ const packageJson = require('./package.json')
const { version } = packageJson
const iconDir = path.resolve(__dirname, 'assets', 'icons')
module.exports = {
const config = {
hooks: {
generateAssets: require('./tools/generateAssets')
},
@@ -32,7 +32,12 @@ module.exports = {
OriginalFilename: 'Electron Fiddle',
},
osxSign: {
identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)'
identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)',
'hardened-runtime': true,
'gatekeeper-assess': false,
'entitlements': 'static/entitlements.plist',
'entitlements-inherit': 'static/entitlements.plist',
'signature-flags': 'library'
}
},
makers: [
@@ -90,8 +95,46 @@ module.exports = {
name: 'fiddle'
},
draft: true,
prerelease: true
prerelease: false
}
}
]
}
function notarizeMaybe() {
if (process.platform !== 'darwin') {
return;
}
if (process.env.npm_lifecycle_event !== 'publish') {
console.log(`Not in "publish" mode, skipping notarization`);
return;
}
if (!process.env.CI) {
console.log(`Not in CI, skipping notarization`);
return;
}
if (!process.env.TRAVIS_TAG && !process.env.NOTARIZE_WITHOUT_TAG) {
console.log(`Not a tag, not notarizing`);
return;
}
if (!process.env.APPLE_ID || !process.env.APPLE_ID_PASSWORD) {
console.warn('Should be notarizing, but environment variables APPLE_ID or APPLE_ID_PASSWORD are missing!');
return;
}
config.packagerConfig.osxNotarize = {
appBundleId: 'com.electron.fiddle',
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASSWORD,
ascProvider: 'LT94ZKYDCJ'
}
}
notarizeMaybe()
// Finally, export it
module.exports = config
+5 -2
View File
@@ -1,12 +1,15 @@
{
"transform": {
".(js|jsx|ts|tsx)": "ts-jest"
"\\.(js|jsx|ts|tsx)$": "ts-jest"
},
"testURL": "http://localhost",
"testRegex": "(-spec)\\.(ts|tsx)$",
"bail": true,
"resetMocks": true,
"bail": true,
"resetModules": true,
"snapshotSerializers": [
"enzyme-to-json/serializer"
],
"setupFilesAfterEnv": [
"<rootDir>/tests/setup.js"
],
+9891 -5331
View File
File diff suppressed because it is too large Load Diff
+63 -62
View File
@@ -1,29 +1,30 @@
{
"name": "electron-fiddle",
"productName": "Electron Fiddle",
"version": "0.7.0",
"version": "0.13.1",
"description": "The easiest way to get started with Electron",
"repository": "https://github.com/electron/fiddle",
"main": "./dist/src/main/main",
"scripts": {
"contributors": "node ./tools/contributors.js",
"less": "node ./tools/lessc.js",
"lint:style": "stylelint ./src/less/*.less --fix",
"lint:style": "stylelint \"./src/less/*.less\" --fix",
"lint:ts": "tslint -c tslint.json -p tsconfig.json -e \"node_modules/**/*.ts\" --fix",
"lint:tests": "tslint ./tests/**/*.ts{,x} -c tslint.json --fix",
"lint:templates": "standard ./static/show-me/**/*.js",
"lint": "npm-run-all lint:*",
"lint:tests": "tslint \"./tests/**/*.ts{,x}\" -c tslint.json --fix",
"lint:templates": "standard \"./static/show-me/**/*.js\"",
"lint": "npm-run-all \"lint:*\"",
"make": "electron-forge make",
"package": "electron-forge package",
"parcel:build": "node ./tools/parcel-build.js",
"parcel:watch": "node ./tools/parcel-watch.js",
"publish": "electron-forge publish",
"start": "rimraf ./dist && electron-forge start",
"test": "jest --config=jest.json --coverage",
"test": "jest --config=jest.json",
"test:ci": "jest --config=jest.json --coverage --runInBand",
"test:coverage": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
"tsc": "node ./tools/tsc.js",
"electron-releases": "node ./tools/fetch-releases.js"
"test:report": "jest --config=jest.json --json --bail=false --outputFile=report.json | true",
"tsc": "tsc --noEmit -p .",
"electron-releases": "node --unhandled-rejections=strict ./tools/fetch-releases.js"
},
"keywords": [
"Electron",
@@ -36,73 +37,73 @@
"forge": "./forge.config.js"
},
"dependencies": {
"@blueprintjs/core": "^3.15.1",
"@blueprintjs/select": "^3.8.0",
"@octokit/rest": "^16.25.6",
"builtin-modules": "^3.1.0",
"@blueprintjs/core": "^3.24.0",
"@blueprintjs/select": "^3.12.0",
"@electron/get": "^1.9.0",
"@octokit/rest": "^16.43.1",
"@sentry/electron": "^1.2.1",
"classnames": "^2.2.6",
"electron-default-menu": "^1.0.1",
"electron-devtools-installer": "^2.2.4",
"electron-download": "^4.1.1",
"electron-squirrel-startup": "^1.0.0",
"extract-zip": "^1.6.7",
"fix-path": "^2.1.0",
"fs-extra": "^8.0.1",
"mobx": "^5.9.4",
"mobx-react": "^5.4.4",
"monaco-editor": "^0.17.0",
"fix-path": "^3.0.0",
"fs-extra": "^8.1.0",
"mobx": "^5.15.4",
"mobx-react": "^6.1.8",
"monaco-editor": "^0.20.0",
"monaco-loader": "1.0.0",
"namor": "^1.1.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-mosaic-component": "^2.1.0",
"semver": "^6.0.0",
"namor": "^2.0.2",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"react-mosaic-component": "^3.2.0",
"semver": "^7.1.3",
"tmp": "0.1.0",
"tslib": "^1.9.3",
"update-electron-app": "^1.3.0"
"tslib": "^1.11.1",
"update-electron-app": "^1.5.0"
},
"devDependencies": {
"@babel/core": "^7.4.4",
"@electron-forge/cli": "^6.0.0-beta.34",
"@electron-forge/maker-deb": "^6.0.0-beta.39",
"@electron-forge/maker-rpm": "^6.0.0-beta.39",
"@electron-forge/maker-squirrel": "^6.0.0-beta.39",
"@electron-forge/maker-zip": "^6.0.0-beta.39",
"@electron-forge/publisher-github": "^6.0.0-beta.34",
"@types/builtin-modules": "^3.1.1",
"@types/classnames": "^2.2.7",
"@types/enzyme": "^3.9.2",
"@types/fs-extra": "^7.0.0",
"@types/jest": "^24.0.13",
"@babel/core": "^7.8.6",
"@electron-forge/cli": "^6.0.0-beta.50",
"@electron-forge/maker-deb": "^6.0.0-beta.50",
"@electron-forge/maker-rpm": "^6.0.0-beta.50",
"@electron-forge/maker-squirrel": "^6.0.0-beta.50",
"@electron-forge/maker-zip": "^6.0.0-beta.50",
"@electron-forge/publisher-github": "^6.0.0-beta.50",
"@octokit/action": "^2.0.0",
"@types/classnames": "^2.2.9",
"@types/enzyme": "^3.10.5",
"@types/fs-extra": "^8.1.0",
"@types/jest": "^25.1.3",
"@types/log-symbols": "^3.0.0",
"@types/node": "^10.11.1",
"@types/react": "^16.8.17",
"@types/react-dom": "^16.8.4",
"@types/semver": "^6.0.0",
"@types/node": "^13.7.6",
"@types/react": "^16.9.23",
"@types/react-dom": "^16.9.5",
"@types/semver": "^7.1.0",
"@types/tmp": "0.1.0",
"chokidar": "^2.1.6",
"coveralls": "^3.0.3",
"electron": "5.0.1",
"enzyme": "^3.9.0",
"enzyme-adapter-react-16": "^1.13.0",
"enzyme-to-json": "^3.3.5",
"jest": "^24.8.0",
"jest-fetch-mock": "^2.1.2",
"less": "^3.9.0",
"chokidar": "^3.3.1",
"coveralls": "^3.0.9",
"electron": "8.0.2",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"enzyme-to-json": "^3.4.4",
"fetch-mock-jest": "^1.1.0-beta.3",
"jest": "^25.1.0",
"less": "^3.11.1",
"log-symbols": "^3.0.0",
"node-abi": "^2.8.0",
"node-abi": "^2.15.0",
"node-fetch": "^2.6.0",
"npm-run-all": "^4.1.5",
"parcel-bundler": "^1.12.3",
"react-test-renderer": "^16.8.6",
"rimraf": "^2.6.3",
"standard": "^12.0.1",
"stylelint": "^10.0.1",
"stylelint-config-standard": "^18.3.0",
"ts-jest": "^24.0.2",
"tslint": "^5.16.0",
"tslint-microsoft-contrib": "^6.1.1",
"tslint-react": "^4.0.0",
"typescript": "^3.4.5"
"parcel-bundler": "^1.12.4",
"react-test-renderer": "^16.13.0",
"rimraf": "^3.0.0",
"standard": "^14.3.1",
"stylelint": "^13.2.0",
"stylelint-config-standard": "^20.0.0",
"ts-jest": "^25.2.1",
"tslint": "^6.0.0",
"tslint-microsoft-contrib": "^6.2.0",
"tslint-react": "^4.2.0",
"typescript": "^3.8.2"
}
}
+1
View File
@@ -3,6 +3,7 @@ export const html = `<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<link rel="stylesheet" type="text/css" href="./styles.css">
</head>
<body>
<h1>Hello World!</h1>
+2 -14
View File
@@ -3,13 +3,9 @@ const {app, BrowserWindow} = require('electron')
const path = require('path')
const url = require('url')
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({width: 800, height: 600})
const mainWindow = new BrowserWindow({width: 800, height: 600})
// and load the index.html of the app.
mainWindow.loadURL(url.format({
@@ -20,14 +16,6 @@ function createWindow () {
// Open the DevTools.
// mainWindow.webContents.openDevTools()
// Emitted when the window is closed.
mainWindow.on('closed', function () {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null
})
}
// This method will be called when Electron has finished
@@ -47,7 +35,7 @@ app.on('window-all-closed', function () {
app.on('activate', function () {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
+2 -14
View File
@@ -1,13 +1,9 @@
export const main = `// Modules to control application life and create native browser window
const {app, BrowserWindow} = require('electron')
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
@@ -20,14 +16,6 @@ function createWindow () {
// Open the DevTools.
// mainWindow.webContents.openDevTools()
// Emitted when the window is closed.
mainWindow.on('closed', function () {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null
})
}
// This method will be called when Electron has finished
@@ -47,7 +35,7 @@ app.on('window-all-closed', function () {
app.on('activate', function () {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
+25 -12
View File
@@ -4,17 +4,18 @@ export type Files = Map<string, string>;
export type FileTransform = (files: Files) => Promise<Files>;
export enum ElectronVersionState {
export enum VersionState {
ready = 'ready',
downloading = 'downloading',
unzipping = 'unzipping',
unknown = 'unknown'
}
export enum ElectronVersionSource {
export enum VersionSource {
remote = 'remote',
local = 'local'
}
export interface NpmVersion {
export interface Version {
version: string;
name?: string;
localPath?: string;
@@ -24,18 +25,21 @@ export interface EditorValues {
main: string;
renderer: string;
html: string;
preload: string;
css: string;
package?: string;
}
export interface ElectronVersion extends NpmVersion {
state: ElectronVersionState;
source: ElectronVersionSource;
export interface RunnableVersion extends Version {
state: VersionState;
source: VersionSource;
downloadProgress?: number;
}
export interface SetFiddleOptions {
values: EditorValues;
filePath?: string;
templateName?: string;
gistId?: string;
}
export interface OutputEntry {
@@ -49,7 +53,8 @@ export interface OutputOptions {
isNotPre?: boolean;
}
export interface WarningDialogTexts {
export interface GenericDialogOptions {
type: GenericDialogType;
ok?: string;
cancel?: string;
label: string;
@@ -59,11 +64,19 @@ export interface Templates {
[index: string]: string | Templates;
}
export const enum GenericDialogType {
'confirm' = 'confirm',
'warning' = 'warning',
'success' = 'success',
}
// Editors
export const enum EditorId {
'main' = 'main',
'renderer' = 'renderer',
'html' = 'html'
'html' = 'html',
'preload' = 'preload',
'css' = 'css'
}
// Panels that can show up as a mosaic
@@ -73,9 +86,9 @@ export const enum PanelId {
export type MosaicId = EditorId | PanelId;
export const ALL_EDITORS = [ EditorId.main, EditorId.renderer, EditorId.html ];
export const ALL_PANELS = [ PanelId.docsDemo ];
export const ALL_MOSAICS = [ ...ALL_EDITORS, ...ALL_PANELS ];
export const ALL_EDITORS = [EditorId.main, EditorId.renderer, EditorId.preload, EditorId.html, EditorId.css];
export const ALL_PANELS = [PanelId.docsDemo];
export const ALL_MOSAICS = [...ALL_EDITORS, ...ALL_PANELS];
export type ArrowPosition = 'top' | 'left' | 'bottom' | 'right';
+15 -3
View File
@@ -17,13 +17,22 @@ export enum IpcEvents {
FS_SAVE_FIDDLE_DIALOG = 'FS_SAVE_FIDDLE_DIALOG',
FS_SAVE_FIDDLE_ERROR = 'FS_SAVE_FIDDLE_ERROR',
SHOW_WARNING_DIALOG = 'SHOW_WARNING_DIALOG',
SHOW_WELCOME_TOUR = 'SHOW_WELCOME_TOUR'
SHOW_CONFIRMATION_DIALOG = 'SHOW_CONFIRMATION_DIALOG',
SHOW_WELCOME_TOUR = 'SHOW_WELCOME_TOUR',
CLEAR_CONSOLE = 'CLEAR_CONSOLE',
LOAD_LOCAL_VERSION_FOLDER = 'LOAD_LOCAL_VERSION_FOLDER',
SHOW_LOCAL_VERSION_FOLDER_DIALOG = 'SHOW_LOCAL_VERSION_FOLDER_DIALOG',
BISECT_COMMANDS_TOGGLE = 'BISECT_COMMANDS_TOGGLE',
SHOW_INACTIVE = 'SHOW_INACTIVE'
}
export const ipcMainEvents = [
IpcEvents.FS_SAVE_FIDDLE_DIALOG,
IpcEvents.FS_SAVE_FIDDLE,
IpcEvents.SHOW_WARNING_DIALOG
IpcEvents.SHOW_WARNING_DIALOG,
IpcEvents.SHOW_CONFIRMATION_DIALOG,
IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG,
IpcEvents.SHOW_INACTIVE
];
export const ipcRendererEvents = [
@@ -42,7 +51,10 @@ export const ipcRendererEvents = [
IpcEvents.FS_SAVE_FIDDLE_AS,
IpcEvents.FS_SAVE_FIDDLE_FORGE,
IpcEvents.FS_SAVE_FIDDLE_GIST,
IpcEvents.SHOW_WELCOME_TOUR
IpcEvents.SHOW_WELCOME_TOUR,
IpcEvents.CLEAR_CONSOLE,
IpcEvents.LOAD_LOCAL_VERSION_FOLDER,
IpcEvents.BISECT_COMMANDS_TOGGLE
];
export const WEBCONTENTS_READY_FOR_IPC_SIGNAL = 'WEBCONTENTS_READY_FOR_IPC_SIGNAL';
@@ -1,3 +0,0 @@
button.version-chooser.bp3-button {
border-radius: 3px 0 0 3px;
}
+8 -1
View File
@@ -2,7 +2,6 @@
header {
box-shadow: 0 0 0 1px rgba(16, 22, 26, 0.2), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.4);
//border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background-color: @background-3;
-webkit-app-region: drag;
}
@@ -20,6 +19,7 @@ header {
select {
-webkit-app-region: no-drag;
margin-bottom: 0;
white-space: nowrap;
}
select {
@@ -27,6 +27,13 @@ header {
margin-left: 5px;
}
fieldset {
border: 0;
padding: 0;
margin: 0;
min-width: 0;
}
& > div {
display: flex;
+15
View File
@@ -24,6 +24,21 @@
}
}
// update if list of Editor ID changes
@editor-ids: main, renderer, html, preload;
// takes each editor ID and increase its z-index if the parent Mosiac root
// has focused__id class for that specific id.
each(@editor-ids, {
.focused__@{value}.mosaic .mosaic-window.@{value} {
z-index: 2;
}
});
.mosaic-window {
z-index: 1;
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
+3 -8
View File
@@ -7,20 +7,15 @@
color: var(--text-color-1);
background: var(--background-2);
box-shadow: inset 0 0 100px 100px rgba(0, 0, 0, 0.15);
border-top: @border;
top: 10vh;
padding: 0;
padding-top: 10px;
padding-left: 10px;
height: 0;
height: 100%;
overflow-y: scroll;
transition: height 0.2s linear, padding 0.2s linear;
-webkit-app-region: no-drag;
&.showing {
border-top: @border;
padding-top: 10px;
height: 150px;
}
p {
margin-bottom: 0;
white-space: pre;
+4
View File
@@ -52,6 +52,10 @@
.action {
text-align: right;
}
.bp3-progress-bar {
margin-top: 8px;
}
}
.settings-content {
+5
View File
@@ -0,0 +1,5 @@
.bp3-fill {
.version-chooser {
width: 100%;
}
}
-1
View File
@@ -188,7 +188,6 @@
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 0 1px rgba(0, 0, 0, 0.2);
}
+1 -1
View File
@@ -16,5 +16,5 @@
@import "components/chrome-mac.less";
@import "components/editors.less";
@import "components/tour.less";
@import "components/commands-version-chooser.less";
@import "components/show-me.less";
@import "components/version-select.less";
+5
View File
@@ -15,6 +15,11 @@ export function getRunItems(): Array<MenuItemConstructorOptions> {
label: 'Run Fiddle',
click: () => ipcMainManager.send(IpcEvents.FIDDLE_RUN)
},
{
id: 'clear_console',
label: 'Clear Console',
click: () => ipcMainManager.send(IpcEvents.CLEAR_CONSOLE)
},
{
type: 'separator'
}
+1 -1
View File
@@ -7,7 +7,7 @@ import { isDevMode } from '../utils/devmode';
* @returns {Promise<void>}
*/
export async function setupDevTools(): Promise<void> {
if (!isDevMode) return;
if (!isDevMode()) return;
const {
default: installExtension,
+35 -1
View File
@@ -1,4 +1,4 @@
import { dialog } from 'electron';
import { dialog, IpcMainEvent } from 'electron';
import { IpcEvents } from '../ipc-events';
import { ipcMainManager } from './ipc';
import { getOrCreateMainWindow } from './windows';
@@ -12,6 +12,15 @@ export function setupDialogs() {
ipcMainManager.on(IpcEvents.SHOW_WARNING_DIALOG, (_event, args) => {
showWarningDialog(args);
});
ipcMainManager.on(IpcEvents.SHOW_CONFIRMATION_DIALOG, (_event, args) => {
showConfirmationDialog(args);
});
ipcMainManager.on(IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG, async (event) => {
await showOpenDialog(event);
});
}
/**
@@ -25,3 +34,28 @@ function showWarningDialog(args: Electron.MessageBoxOptions) {
...args
});
}
/**
* Shows a confirmation dialog
*
* @param {Electron.MessageBoxOptions} args
*/
function showConfirmationDialog(args: Electron.MessageBoxOptions) {
dialog.showMessageBox(getOrCreateMainWindow(), {
type: 'warning',
...args
});
}
async function showOpenDialog(event: IpcMainEvent) {
const { filePaths } = await dialog.showOpenDialog({
title: 'Open Folder',
properties: ['openDirectory']
});
if (!filePaths || filePaths.length < 1) {
return;
}
event.reply(IpcEvents.LOAD_LOCAL_VERSION_FOLDER, [filePaths[0]]);
}
+21 -21
View File
@@ -19,41 +19,41 @@ export function setupFileListeners() {
* Shows the "Open Fiddle" dialog and forwards
* the path to the renderer
*/
export function showOpenDialog() {
dialog.showOpenDialog({
export async function showOpenDialog() {
const { filePaths } = await dialog.showOpenDialog({
title: 'Open Fiddle',
properties: ['openDirectory']
}, (filePaths) => {
if (!filePaths || filePaths.length < 1) {
return;
}
ipcMainManager.send(IpcEvents.FS_OPEN_FIDDLE, [ filePaths[0] ]);
});
if (!filePaths || filePaths.length < 1) {
return;
}
ipcMainManager.send(IpcEvents.FS_OPEN_FIDDLE, [ filePaths[0] ]);
}
/**
* Shows the "Save Fiddle" dialog and forwards
* the path to the renderer
*/
export function showSaveDialog(event?: IpcEvents, as?: string) {
export async function showSaveDialog(event?: IpcEvents, as?: string) {
// We want to save to a folder, so we'll use an open dialog here
dialog.showOpenDialog({
const { filePaths } = await dialog.showOpenDialog({
buttonLabel: 'Save here',
properties: ['openDirectory', 'createDirectory'],
title: `Save Fiddle${as ? ` as ${as}` : ''}`
}, async (filePaths) => {
if (!filePaths || filePaths.length < 1) {
return;
}
console.log(`Asked to save to ${filePaths[0]}`);
// Let's confirm real quick if we want this
if (await ensureSaveTargetEmpty(filePaths[0])) {
ipcMainManager.send(event || IpcEvents.FS_SAVE_FIDDLE, [ filePaths[0] ]);
}
});
if (!filePaths || filePaths.length < 1) {
return;
}
console.log(`Asked to save to ${filePaths[0]}`);
// Let's confirm real quick if we want this
if (await ensureSaveTargetEmpty(filePaths[0])) {
ipcMainManager.send(event || IpcEvents.FS_SAVE_FIDDLE, [ filePaths[0] ]);
}
}
/**
+8 -8
View File
@@ -7,9 +7,9 @@ import { isDevMode } from '../utils/devmode';
* Is this the first run of Fiddle? If so, perform
* tasks that we only want to do in this case.
*/
export function onFirstRunMaybe() {
export async function onFirstRunMaybe() {
if (isFirstRun()) {
promptMoveToApplicationsFolder();
await promptMoveToApplicationsFolder();
}
}
@@ -17,18 +17,18 @@ export function onFirstRunMaybe() {
* Ask the user if the app should be moved to the
* applications folder.
*/
function promptMoveToApplicationsFolder(): void {
async function promptMoveToApplicationsFolder(): Promise<void> {
if (process.platform !== 'darwin') return;
if (isDevMode() || app.isInApplicationsFolder()) return;
dialog.showMessageBox({
const { response } = await dialog.showMessageBox({
type: 'question',
buttons: ['Move to Applications Folder', 'Do Not Move'],
defaultId: 0,
message: 'Move to Applications Folder?',
}, (response) => {
if (response === 0) {
app.moveToApplicationsFolder();
}
});
if (response === 0) {
app.moveToApplicationsFolder();
}
}
+4 -1
View File
@@ -1,5 +1,6 @@
import { ipcMain } from 'electron';
import { EventEmitter } from 'events';
import { IpcEvents, ipcMainEvents, WEBCONTENTS_READY_FOR_IPC_SIGNAL } from '../ipc-events';
import { getOrCreateMainWindow } from './windows';
@@ -19,11 +20,13 @@ export class IpcMainManager extends EventEmitter {
super();
ipcMainEvents.forEach((name) => {
ipcMain.removeAllListeners(name);
ipcMain.on(name, (...args: Array<any>) => this.emit(name, ...args));
});
ipcMain.on(WEBCONTENTS_READY_FOR_IPC_SIGNAL, (event: Electron.Event) => {
ipcMain.on(WEBCONTENTS_READY_FOR_IPC_SIGNAL, (event: Electron.IpcMainEvent) => {
this.readyWebContents.add(event.sender);
const queue = this.messageQueue.get(event.sender);
this.messageQueue.delete(event.sender);
if (!queue) return;
+5 -2
View File
@@ -1,3 +1,6 @@
import { initSentry } from '../sentry';
initSentry();
import { app } from 'electron';
import { isDevMode } from '../utils/devmode';
@@ -15,7 +18,7 @@ import { getOrCreateMainWindow } from './windows';
* the method that takes care of booting the application.
*/
export async function onReady() {
onFirstRunMaybe();
await onFirstRunMaybe();
if (!isDevMode()) process.env.NODE_ENV = 'production';
getOrCreateMainWindow();
@@ -68,7 +71,7 @@ export function main() {
}
// Set the app's name
app.setName('Electron Fiddle');
app.name = 'Electron Fiddle';
// Ensure that there's only ever one Fiddle running
listenForProtocolHandler();
+17 -10
View File
@@ -144,7 +144,7 @@ function getShowMeMenuItem(key: string, item: string | Templates): MenuItemConst
if (typeof item === 'string') {
return {
label: key,
click: () => ipcMainManager.send(IpcEvents.FS_OPEN_TEMPLATE, [ key ])
click: () => ipcMainManager.send(IpcEvents.FS_OPEN_TEMPLATE, [key])
};
}
@@ -160,10 +160,10 @@ function getShowMeMenu(): MenuItemConstructorOptions {
const showMeMenu: Array<MenuItemConstructorOptions> = Object.keys(SHOW_ME_TEMPLATES)
.map((key) => getShowMeMenuItem(key, SHOW_ME_TEMPLATES[key]));
return {
label: 'Show Me',
submenu: showMeMenu
};
return {
label: 'Show Me',
submenu: showMeMenu
};
}
/**
@@ -206,7 +206,7 @@ function getFileMenu(): MenuItemConstructorOptions {
type: 'separator'
},
{
label: 'Save to Gist',
label: 'Publish to Gist',
click: () => ipcMainManager.send(IpcEvents.FS_SAVE_FIDDLE_GIST),
},
{
@@ -247,15 +247,22 @@ export function setupMenu() {
// Tweak "View" menu
if (label === 'View' && isSubmenu(item.submenu)) {
item.submenu = item.submenu.filter((subItem) => subItem.label !== 'Toggle Developer Tools'); // Remove "Toggle Developer Tools"
item.submenu.push({ type: 'separator' }, { role: 'resetzoom' }, { role: 'zoomin' }, { role: 'zoomout' }); // Add zooming actions
// remove "Reload" (has weird behaviour) and "Toggle Developer Tools"
item.submenu = item.submenu
.filter((subItem) => subItem.label !== 'Toggle Developer Tools' && subItem.label !== 'Reload');
item.submenu.push({ type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' }); // Add zooming actions
item.submenu.push({ type: 'separator' }, {
label: 'Toggle Soft Wrap',
click: () => ipcMainManager.send(IpcEvents.MONACO_TOGGLE_OPTION, [ 'wordWrap' ]),
click: () => ipcMainManager.send(IpcEvents.MONACO_TOGGLE_OPTION, ['wordWrap']),
});
item.submenu.push({ type: 'separator' }, {
label: 'Toggle Mini Map',
click: () => ipcMainManager.send(IpcEvents.MONACO_TOGGLE_OPTION, [ 'minimap.enabled' ]),
click: () => ipcMainManager.send(IpcEvents.MONACO_TOGGLE_OPTION, ['minimap.enabled']),
});
item.submenu.push({ type: 'separator' }, {
label: 'Toggle Bisect Helper',
click: () => ipcMainManager.send(IpcEvents.BISECT_COMMANDS_TOGGLE),
accelerator: 'CommandorControl+Shift+B',
});
}
+12 -1
View File
@@ -4,6 +4,7 @@ import * as path from 'path';
import * as nodeUrl from 'url';
import { IpcEvents } from '../ipc-events';
import { isDevMode } from '../utils/devmode';
import { ipcMainManager } from './ipc';
const PROTOCOL = 'electron-fiddle';
@@ -65,17 +66,27 @@ export const scanArgv = (argv: Array<string>) => {
}
};
export const scanNpmArgv = (argv: string) => {
const parsedArgv = JSON.parse(argv);
const { original: args } = parsedArgv;
scanArgv(args);
};
export const listenForProtocolHandler = () => {
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) app.quit();
app.removeAllListeners('open-url');
app.on('open-url', (_, url) => {
if (url.startsWith(`${PROTOCOL}://`)) {
handlePotentialProtocolLaunch(url);
}
});
if (process.platform === 'win32') {
// pass protocol URL via npm start args in dev mode
if (isDevMode() && process.env.npm_config_argv) {
scanNpmArgv(process.env.npm_config_argv);
} else if (process.platform === 'win32') {
scanArgv(process.argv);
}
};
+6
View File
@@ -1,5 +1,7 @@
import { BrowserWindow, shell } from 'electron';
import { IpcEvents } from '../ipc-events';
import { createContextMenu } from './context-menu';
import { ipcMainManager } from './ipc';
// Keep a global reference of the window objects, if we don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
@@ -61,6 +63,10 @@ export function createMainWindow(): Electron.BrowserWindow {
shell.openExternal(url);
});
ipcMainManager.on(IpcEvents.SHOW_INACTIVE, () => {
browserWindow.showInactive();
});
browserWindows.push(browserWindow);
return browserWindow;
+84 -57
View File
@@ -1,14 +1,25 @@
import { initSentry } from '../sentry';
initSentry();
import { when } from 'mobx';
import * as MonacoType from 'monaco-editor';
import { ipcRenderer } from 'electron';
import { ALL_EDITORS, EditorId, EditorValues } from '../interfaces';
import {
ALL_EDITORS,
EditorId,
EditorValues,
GenericDialogType,
SetFiddleOptions
} from '../interfaces';
import { WEBCONTENTS_READY_FOR_IPC_SIGNAL } from '../ipc-events';
import { updateEditorLayout } from '../utils/editor-layout';
import { getEditorValue } from '../utils/editor-value';
import { getPackageJson, PackageJsonOptions } from '../utils/get-package';
import { getTitle } from '../utils/get-title';
import { isEditorBackup } from '../utils/type-checks';
import { FileManager } from './file-manager';
import { RemoteLoader } from './remote-loader';
import { Runner } from './runner';
import { appState } from './state';
import { getTheme } from './themes';
@@ -25,65 +36,95 @@ export class App {
public monaco: typeof MonacoType | null = null;
public state = appState;
public fileManager = new FileManager(appState);
public remoteLoader = new RemoteLoader(appState);
public runner = new Runner(appState);
public touchBarManager: TouchBarManager | undefined;
constructor() {
this.getValues = this.getValues.bind(this);
this.setValues = this.setValues.bind(this);
this.getEditorValues = this.getEditorValues.bind(this);
this.setEditorValues = this.setEditorValues.bind(this);
if (process.platform === 'darwin') {
this.touchBarManager = new TouchBarManager(appState);
}
}
public async replaceFiddle(
editorValues: Partial<EditorValues>,
{ filePath, gistId, templateName }: Partial<SetFiddleOptions>
) {
// if unsaved, prompt user to make sure they're okay with overwriting and changing directory
if (this.state.isUnsaved) {
this.state.setGenericDialogOptions({
type: GenericDialogType.warning,
label: `Opening this Fiddle will replace your unsaved changes. Do you want to proceed?`,
ok: 'Yes'
});
this.state.isGenericDialogShowing = true;
await when(() => !this.state.isGenericDialogShowing);
if (!this.state.genericDialogLastResult) {
return false;
}
}
// set values once prompt approves
await this.setEditorValues(editorValues);
document.title = getTitle(this.state);
this.state.gistId = gistId || '';
this.state.localPath = filePath;
this.state.templateName = templateName;
// once loaded, we have a "saved" state
this.state.isUnsaved = false;
return true;
}
/**
* Sets the values on all three editors.
* Sets the contents of all editor panes.
*
* @param {EditorValues} values
* @param {warn} warn - Should we warn before overwriting unsaved data?
*/
public async setValues(values: Partial<EditorValues>, warn: boolean = true): Promise<boolean> {
public async setEditorValues(values: Partial<EditorValues>): Promise<void> {
const { ElectronFiddle: fiddle } = window;
if (!fiddle) {
throw new Error('Fiddle not ready');
}
if (this.state.isUnsaved && warn) {
this.state.isWarningDialogShowing = true;
await when(() => !this.state.isWarningDialogShowing);
if (!this.state.warningDialogLastResult) {
return false;
}
}
for (const name of ALL_EDITORS) {
const editor = fiddle.editors[name];
const backup = this.state.closedPanels[name];
if (isEditorBackup(backup) && backup.model && values[name]) {
// The editor does not exist, attempt to set it on the backup
backup.model.setValue(values[name]!);
} else if (editor && editor.setValue && values[name]) {
// The editor exists, set the value directly
editor.setValue(values[name]!);
if (typeof values[name] !== 'undefined') {
if (isEditorBackup(backup)) {
// The editor does not exist, attempt to set it on the backup.
// If there's a model, we'll do it on the model. Else, we'll
// set the value.
if (backup.model) {
backup.model.setValue(values[name]!);
} else {
backup.value = values[name]!;
}
} else if (editor && editor.setValue) {
// The editor exists, set the value directly
editor.setValue(values[name]!);
}
}
}
this.state.isUnsaved = false;
this.setupUnsavedOnChangeListener();
return true;
}
/**
* Gets the values on all three editors.
* Retrieves the contents of all editor panes.
*
* @returns {EditorValues}
*/
public async getValues(options?: PackageJsonOptions): Promise<EditorValues> {
public async getEditorValues(
options?: PackageJsonOptions
): Promise<EditorValues> {
const { ElectronFiddle: fiddle } = window;
if (!fiddle) {
@@ -91,12 +132,14 @@ export class App {
}
const values: EditorValues = {
css: getEditorValue(EditorId.css),
html: getEditorValue(EditorId.html),
main: getEditorValue(EditorId.main),
renderer: getEditorValue(EditorId.renderer),
preload: getEditorValue(EditorId.preload),
renderer: getEditorValue(EditorId.renderer)
};
if (options && options.include !== false) {
if (options && options.include !== false) {
values.package = await getPackageJson(this.state, values, options);
}
@@ -112,16 +155,18 @@ export class App {
const React = await import('react');
const { render } = await import('react-dom');
const { Header } = await import('./components/header');
const { Dialogs } = await import('./components/dialogs');
const { Editors } = await import('./components/editors');
const { OutputEditorsWrapper } = await import(
'./components/output-editors-wrapper'
);
const { Header } = await import('./components/header');
const className = `${process.platform} container`;
const app = (
<div className={className}>
<Header appState={this.state} />
<Dialogs appState={this.state} />
<Editors appState={this.state} />
<Header appState={this.state} />
<OutputEditorsWrapper appState={this.state} />
</div>
);
@@ -131,37 +176,18 @@ export class App {
ipcRenderer.send(WEBCONTENTS_READY_FOR_IPC_SIGNAL);
// Todo: A timer here is terrible. Let's fix this
// and ensure we actually do it once Editors have mounted.
setTimeout(() => {
this.setupUnsavedOnChangeListener();
}, 1500);
return rendered;
}
/**
* If the editor is changed for the first time, we'll
* set `isUnsaved` to true. That way, the app can warn you
* if you're about to throw things away.
*/
public setupUnsavedOnChangeListener() {
Object.keys(window.ElectronFiddle.editors).forEach((key) => {
const editor = window.ElectronFiddle.editors[key];
const disposable = editor.onDidChangeModelContent(() => {
this.state.isUnsaved = true;
disposable.dispose();
});
});
}
/**
* Loads theme CSS into the HTML document.
*
* @returns {Promise<void>}
*/
public async setupTheme(): Promise<void> {
const tag: HTMLStyleElement | null = document.querySelector('style#fiddle-theme');
const tag: HTMLStyleElement | null = document.querySelector(
'style#fiddle-theme'
);
const theme = await getTheme(this.state.theme);
if (tag && theme.css) {
@@ -185,6 +211,7 @@ export class App {
}
window.ElectronFiddle = window.ElectronFiddle || {};
window.ElectronFiddle.contentChangeListeners = window.ElectronFiddle.contentChangeListeners || [];
window.ElectronFiddle.contentChangeListeners =
window.ElectronFiddle.contentChangeListeners || [];
window.ElectronFiddle.app = window.ElectronFiddle.app || new App();
window.ElectronFiddle.app.setup();
+235 -179
View File
@@ -1,203 +1,259 @@
import * as fsType from 'fs-extra';
import * as path from 'path';
import { VersionState } from '../interfaces';
import { fancyImport } from '../utils/import';
import { normalizeVersion } from '../utils/normalize-version';
import { USER_DATA_PATH } from './constants';
import { removeTypeDefsForVersion } from './fetch-types';
import { AppState } from './state';
/**
* The binary manager takes care of downloading Electron versions
* General setup, called with a version. Is called during construction
* to ensure that we always have or download at least one version.
*
* @export
* @class BinaryManager
* @param {string} iVersion
* @returns {Promise<void>}
*/
export class BinaryManager {
public state: Record<string, 'ready' | 'downloading'> = {};
export async function setupBinary(appState: AppState, iVersion: string): Promise<void> {
const version = normalizeVersion(iVersion);
const fs = await fancyImport<typeof fsType>('fs-extra');
/**
* Remove a version from disk. Does not update state. We'll try up to
* four times before giving up if an error occurs.
*
* @param {string} iVersion
* @param {number} iteration
* @returns {Promise<void>}
*/
public async remove(iVersion: string, i: number = 0): Promise<void> {
const version = normalizeVersion(iVersion);
const fs = await fancyImport<typeof fsType>('fs-extra');
await fs.mkdirp(getDownloadPath(version));
const { state } = appState.versions[version];
if (state === VersionState.downloading || state === VersionState.unzipping) {
console.log(`Binary: Electron ${version} already downloading.`);
return;
}
if (await getIsDownloaded(version)) {
console.log(`Binary: Electron ${version} already downloaded.`);
appState.versions[version].state = VersionState.ready;
return;
}
console.log(`Binary: Electron ${version} not present, downloading`);
appState.versions[version].state = VersionState.downloading;
const zipPath = await download(appState, version);
const extractPath = getDownloadPath(version);
console.log(`Binary: Electron ${version} downloaded, now unpacking to ${extractPath}`);
try {
appState.versions[version].state = VersionState.unzipping;
// Ensure the target path is empty
await fs.emptyDir(extractPath);
const electronFiles = await unzip(zipPath, extractPath);
console.log(`Unzipped ${version}`, electronFiles);
} catch (error) {
console.warn(`Failure while unzipping ${version}`, error);
appState.versions[version].state = VersionState.unknown;
return;
}
appState.versions[version].state = VersionState.ready;
}
/**
* Remove a version from disk. Does not update state. We'll try up to
* four times before giving up if an error occurs.
*
* @param {string} iVersion
* @returns {Promise<void>}
*/
export async function removeBinary(iVersion: string) {
const version = normalizeVersion(iVersion);
const fs = await fancyImport<typeof fsType>('fs-extra');
let isDeleted = false;
// utility to re-run removal functions upon failure
// due to windows filesystem lockfile jank
const rerunner = async (func: () => Promise<void>, counter: number = 1) => {
try {
if (await this.getIsDownloaded(version)) {
// This is necessary since we're messing with .asar files inside
// the Electron binaries. Electron, powering Fiddle, will try to
// "correct" our calls, but we don't want that right here.
process.noAsar = true;
await fs.remove(this.getDownloadPath(version));
process.noAsar = false;
}
await func();
} catch (error) {
console.warn(`Binary Manager: Tried to remove ${version}, but failed`, error);
if (i < 3) {
console.log(`Binary Manager: Trying again`);
return this.remove(version, i + 1);
console.warn(`Binary Manager: failed to run ${func.name} for ${version}, but failed`, error);
if (counter < 4) {
console.log(`Binary Manager: Trying again to run ${func.name}`);
await rerunner(func, counter + 1);
}
}
}
/**
* General setup, called with a version. Is called during construction
* to ensure that we always have or download at least one version.
*
* @param {string} iVersion
* @returns {Promise<void>}
*/
public async setup(iVersion: string): Promise<void> {
const version = normalizeVersion(iVersion);
const fs = await fancyImport<typeof fsType>('fs-extra');
const { promisify } = await import('util');
const eDownload = promisify(require('electron-download'));
await fs.mkdirp(this.getDownloadPath(version));
if (this.state[version] === 'downloading') {
console.log(`BinaryManager: Electron ${version} already downloading.`);
return;
}
if (await this.getIsDownloaded(version)) {
console.log(`BinaryManager: Electron ${version} already downloaded.`);
this.state[version] = 'ready';
return;
}
console.log(`BinaryManager: Electron ${version} not present, downloading`);
this.state[version] = 'downloading';
const zipPath = await eDownload({ version });
const extractPath = this.getDownloadPath(version);
console.log(`BinaryManager: Electron ${version} downloaded, now unpacking to ${extractPath}`);
try {
// Ensure the target path is empty
await fs.emptyDir(extractPath);
const electronFiles = await this.unzip(zipPath, extractPath);
console.log(`Unzipped ${version}`, electronFiles);
} catch (error) {
console.warn(`Failure while unzipping ${version}`, error);
// Todo: Handle this case
}
this.state[version] = 'ready';
}
/**
* Gets the expected path for the binary of a given Electron version
*
* @param {string} version
* @param {string} dir
* @returns {string}
*/
public getElectronBinaryPath(
version: string,
dir: string = this.getDownloadPath(version),
): string {
switch (process.platform) {
case 'darwin':
return path.join(dir, 'Electron.app/Contents/MacOS/Electron');
case 'freebsd':
case 'linux':
return path.join(dir, 'electron');
case 'win32':
return path.join(dir, 'electron.exe');
default:
throw new Error(`Electron builds are not available for ${process.platform}`);
}
}
/**
* Returns an array of all versions downloaded to disk
*
* @returns {Promise<Array<string>>}
*/
public async getDownloadedVersions(): Promise<Array<string>> {
const fs = await fancyImport<typeof fsType>('fs-extra');
const downloadPath = path.join(USER_DATA_PATH, 'electron-bin');
console.log(`BinaryManager: Checking for downloaded versions`);
try {
const directories = await fs.readdir(downloadPath);
const knownVersions: Array<string> = [];
for (const directory of directories) {
if (await this.getIsDownloaded(directory)) {
knownVersions.push(directory);
}
}
return knownVersions;
} catch (error) {
console.warn(`Could not read known Electron versions`);
return [];
}
}
/**
* Did we already download a given version?
*
* @param {string} version
* @param {string} dir
* @returns {boolean}
*/
public async getIsDownloaded(version: string, dir?: string): Promise<boolean> {
const expectedPath = this.getElectronBinaryPath(version, dir);
const fs = await fancyImport<typeof fsType>('fs-extra');
return fs.existsSync(expectedPath);
}
/**
* Gets the expected path for a given Electron version
*
* @param {string} version
* @returns {string}
*/
private getDownloadPath(version: string): string {
return path.join(USER_DATA_PATH, 'electron-bin', version);
}
/**
* Unzips an electron package so that we can actually use it.
*
* @param {string} zipPath
* @param {string} extractPath
* @returns {Promise<void>}
*/
private unzip(zipPath: string, extractPath: string): Promise<void> {
return new Promise(async (resolve, reject) => {
const extract = (await fancyImport<any>('extract-zip')).default;
};
const binaryCleaner = async () => {
if (await getIsDownloaded(version)) {
// This is necessary since we're messing with .asar files inside
// the Electron binaries. Electron, powering Fiddle, will try to
// "correct" our calls, but we don't want that right here.
process.noAsar = true;
await fs.remove(getDownloadPath(version));
process.noAsar = false;
const options = {
dir: extractPath,
};
isDeleted = true;
}
};
extract(zipPath, options, (error: Error) => {
if (error) {
reject(error);
return;
}
const typeDefsCleaner = async () => {
await removeTypeDefsForVersion(version);
};
console.log(`BinaryManager: Unpacked!`);
process.noAsar = false;
await rerunner(binaryCleaner);
resolve();
});
});
if (isDeleted) {
await rerunner(typeDefsCleaner);
}
}
/* Did we already download a given version?
*
* @param {string} version
* @param {string} dir
* @returns {boolean}
*/
export async function getIsDownloaded(version: string, dir?: string): Promise<boolean> {
const expectedPath = getElectronBinaryPath(version, dir);
const fs = await fancyImport<typeof fsType>('fs-extra');
return fs.existsSync(expectedPath);
}
/**
* Gets the expected path for the binary of a given Electron version
*
* @param {string} version
* @param {string} dir
* @returns {string}
*/
export function getElectronBinaryPath(
version: string,
dir: string = getDownloadPath(version),
): string {
switch (process.platform) {
case 'darwin':
return path.join(dir, 'Electron.app/Contents/MacOS/Electron');
case 'freebsd':
case 'linux':
return path.join(dir, 'electron');
case 'win32':
return path.join(dir, 'electron.exe');
default:
throw new Error(`Electron builds are not available for ${process.platform}`);
}
}
export function getDownloadingVersions(appState: AppState) {
return Object.entries(appState.versions)
.filter(([_, { state }]) => state === 'downloading')
.map(([version, _]) => version);
}
/**
* Returns an array of all versions downloaded to disk
*
* @returns {Promise<Array<string>>}
*/
export async function getDownloadedVersions(): Promise<Array<string>> {
const fs = await fancyImport<typeof fsType>('fs-extra');
const downloadPath = path.join(USER_DATA_PATH, 'electron-bin');
console.log(`Binary: Checking for downloaded versions`);
try {
const directories = await fs.readdir(downloadPath);
const knownVersions: Array<string> = [];
for (const directory of directories) {
if (await getIsDownloaded(directory)) {
knownVersions.push(directory);
}
}
return knownVersions;
} catch (error) {
console.warn(`Could not read known Electron versions`);
return [];
}
}
/**
* Download an Electron version.
*
* @param {AppState} appState
* @param {string} version
* @returns {Promise<string>}
*/
async function download(appState: AppState, version: string): Promise<string> {
const { download: electronDownload } = await import('@electron/get');
const getProgressCallback = (progress: Progress) => {
const roundedProgress = Math.round(progress.percent * 100) / 100;
if (roundedProgress !== appState.versions[version].downloadProgress) {
console.debug(`Binary: Version ${version} download progress: ${progress.percent}`);
appState.versions[version].downloadProgress = roundedProgress;
}
};
if (!appState.versions[version]) {
throw new Error(`Version ${version} does not exist in state, cannot download`);
}
const zipFilePath = await electronDownload(version, {
downloadOptions: {
quiet: true,
getProgressCallback
}
});
return zipFilePath;
}
/**
* Gets the expected path for a given Electron version
*
* @param {string} version
* @returns {string}
*/
function getDownloadPath(version: string): string {
return path.join(USER_DATA_PATH, 'electron-bin', version);
}
/**
* Unzips an electron package so that we can actually use it.
*
* @param {string} zipPath
* @param {string} extractPath
* @returns {Promise<void>}
*/
function unzip(zipPath: string, extractPath: string): Promise<void> {
return new Promise(async (resolve, reject) => {
const extract = (await fancyImport<any>('extract-zip')).default;
process.noAsar = true;
const options = {
dir: extractPath,
};
extract(zipPath, options, (error: Error) => {
if (error) {
reject(error);
return;
}
console.log(`Binary: Unpacked!`);
process.noAsar = false;
resolve();
});
});
}
interface Progress {
percent: number;
transferred: number;
total: number;
}
+58
View File
@@ -0,0 +1,58 @@
import { RunnableVersion } from '../interfaces';
export class Bisector {
public revList: Array<RunnableVersion>;
public minRev: number;
public maxRev: number;
private pivot: number;
constructor(revList: Array<RunnableVersion>) {
this.getCurrentVersion = this.getCurrentVersion.bind(this);
this.continue = this.continue.bind(this);
this.calculatePivot = this.calculatePivot.bind(this);
this.revList = revList;
this.minRev = 0;
this.maxRev = revList.length - 1;
this.calculatePivot();
}
public getCurrentVersion() {
return this.revList[this.pivot];
}
public continue(isGoodVersion: boolean) {
let isBisectOver = false;
if (this.maxRev - this.minRev <= 1) {
isBisectOver = true;
}
if (isGoodVersion) {
const upPivot = Math.floor((this.maxRev - this.pivot) / 2) + this.pivot;
this.minRev = this.pivot;
if (upPivot !== this.maxRev && upPivot !== this.pivot) {
this.pivot = upPivot;
} else {
isBisectOver = true;
}
} else {
const downPivot = Math.floor((this.pivot - this.minRev) / 2) + this.minRev;
this.maxRev = this.pivot;
if (downPivot !== this.minRev && downPivot !== this.pivot) {
this.pivot = downPivot;
} else {
isBisectOver = true;
}
}
if (isBisectOver) {
return [this.revList[this.minRev], this.revList[this.maxRev]];
} else {
return this.revList[this.pivot];
}
}
private calculatePivot() {
this.pivot = Math.floor((this.maxRev - this.minRev) / 2);
}
}
+36 -184
View File
@@ -1,16 +1,11 @@
import { Button, InputGroup, Intent } from '@blueprintjs/core';
import classnames from 'classnames';
import { reaction, when } from 'mobx';
import { reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { EditorId, EditorValues } from '../../interfaces';
import { IpcEvents } from '../../ipc-events';
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../../shared-constants';
import { getTitle } from '../../utils/get-title';
import { idFromUrl, urlFromId } from '../../utils/gist';
import { getOctokit } from '../../utils/octokit';
import { getContent } from '../content';
import { ipcRendererManager } from '../ipc';
import { AppState } from '../state';
@@ -20,26 +15,32 @@ export interface AddressBarProps {
export interface AddressBarState {
value: string;
loaders: {
gist: any;
example: any;
};
}
@observer
export class AddressBar extends React.Component<AddressBarProps, AddressBarState> {
constructor(props: AddressBarProps) {
super(props);
this.fetchExampleAndLoad = this.fetchExampleAndLoad.bind(this);
this.fetchGistAndLoad = this.fetchGistAndLoad.bind(this);
this.loadFiddleFromGist = this.loadFiddleFromGist.bind(this);
this.loadFiddleFromElectronExample = this.loadFiddleFromElectronExample.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
this.submit = this.submit.bind(this);
this.verifyRemoteLoad = this.verifyRemoteLoad.bind(this);
const { gistId } = this.props.appState;
const value = gistId ? urlFromId(gistId) : '';
this.state = { value };
const { remoteLoader } = window.ElectronFiddle.app;
this.state = {
value,
loaders: {
gist: remoteLoader.loadFiddleFromGist.bind(remoteLoader),
example: remoteLoader.loadFiddleFromElectronExample.bind(remoteLoader)
}
};
}
/**
@@ -59,8 +60,9 @@ export class AddressBar extends React.Component<AddressBarProps, AddressBarState
* @memberof AddressBar
*/
public submit() {
const { remoteLoader } = window.ElectronFiddle.app;
if (this.state.value) {
this.fetchGistAndLoad(idFromUrl(this.state.value) || this.state.value);
remoteLoader.fetchGistAndLoad(idFromUrl(this.state.value) || this.state.value);
}
}
@@ -68,48 +70,20 @@ export class AddressBar extends React.Component<AddressBarProps, AddressBarState
* Once the component mounts, we'll subscribe to gistId changes
*/
public componentDidMount() {
const { appState } = this.props;
const { loaders } = this.state;
reaction(
() => this.props.appState.gistId,
() => appState.gistId,
(gistId: string) => this.setState({ value: urlFromId(gistId) })
);
ipcRendererManager.on(IpcEvents.LOAD_GIST_REQUEST, this.loadFiddleFromGist);
ipcRendererManager.on(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, this.loadFiddleFromElectronExample);
ipcRendererManager.on(IpcEvents.LOAD_GIST_REQUEST, loaders.gist);
ipcRendererManager.on(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, loaders.example);
}
public componentWillUnmount() {
ipcRendererManager.removeListener(IpcEvents.LOAD_GIST_REQUEST, this.loadFiddleFromGist);
ipcRendererManager.removeListener(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, this.loadFiddleFromElectronExample);
}
public async loadFiddleFromElectronExample(_: any, exampleInfo: { path: string; ref: string }) {
console.log(`Loading fiddle from Electron example`, _, exampleInfo);
const ok = await this.verifyRemoteLoad('example from the Electron docs');
if (!ok) return;
this.fetchExampleAndLoad(exampleInfo.ref, exampleInfo.path);
}
public async loadFiddleFromGist(_: any, gistInfo: { id: string }) {
const ok = await this.verifyRemoteLoad('gist');
if (!ok) return;
this.fetchGistAndLoad(gistInfo.id);
}
/**
* Verifies from the user that we should be loading this fiddle
*
* @param what What are we loading from (gist, example, etc.)
*/
public async verifyRemoteLoad(what: string): Promise<boolean> {
const { appState } = this.props;
appState.setWarningDialogTexts({
label: `Are you sure you sure you want to load this ${what}? Only load and run it if you trust the source`
});
appState.isWarningDialogShowing = true;
await when(() => !appState.isWarningDialogShowing);
return !!appState.warningDialogLastResult;
const { loaders } = this.state;
ipcRendererManager.removeListener(IpcEvents.LOAD_GIST_REQUEST, loaders.gist);
ipcRendererManager.removeListener(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, loaders.example);
}
/**
@@ -121,85 +95,6 @@ export class AddressBar extends React.Component<AddressBarProps, AddressBarState
this.setState({ value: event.target.value });
}
public async fetchExampleAndLoad(ref: string, path: string): Promise<boolean> {
const { appState } = this.props;
try {
const octo = await getOctokit(this.props.appState);
// TODO: Maybe we should fetch the package.json for the given ref to use the correct version of Electron?
const folder = await octo.repos.getContents({
owner: 'electron',
repo: 'electron',
ref,
path,
});
const values = {
html: await getContent(EditorId.html, appState.version),
renderer: await getContent(EditorId.renderer, appState.version),
main: await getContent(EditorId.main, appState.version),
};
const loaders: Array<Promise<void>> = [];
if (!Array.isArray(folder.data)) {
throw new Error('The example Fiddle tried to launch is not a valid Electron example');
}
for (const child of folder.data) {
switch (child.name) {
case MAIN_JS_NAME:
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.main = t; })
);
break;
case INDEX_HTML_NAME:
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.html = t; })
);
break;
case RENDERER_JS_NAME:
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.renderer = t; })
);
break;
default:
break;
}
}
await Promise.all(loaders);
return this.handleLoadingSuccess(values, '');
} catch (error) {
return this.handleLoadingFailed(error);
}
}
/**
* Load a fiddle
*
* @returns {Promise<boolean>}
* @memberof AddressBar
*/
public async fetchGistAndLoad(gistId: string): Promise<boolean> {
try {
const octo = await getOctokit(this.props.appState);
const gist = await octo.gists.get({ gist_id: gistId });
return this.handleLoadingSuccess({
html: gist.data.files[INDEX_HTML_NAME].content,
main: gist.data.files[MAIN_JS_NAME].content,
renderer: gist.data.files[RENDERER_JS_NAME].content,
}, gistId);
} catch (error) {
return this.handleLoadingFailed(error);
}
}
public renderLoadButton(isValueCorrect: boolean): JSX.Element {
return (
<Button
@@ -212,68 +107,25 @@ export class AddressBar extends React.Component<AddressBarProps, AddressBarState
}
public render() {
const { isUnsaved } = this.props.appState;
const { isUnsaved, isPublishing } = this.props.appState;
const { value } = this.state;
const isCorrect = /https:\/\/gist\.github\.com\/(.+)$/.test(value);
const className = classnames('address-bar', isUnsaved, { empty: !value });
return (
<form className={className} onSubmit={this.handleSubmit}>
<InputGroup
key='addressbar'
leftIcon='geosearch'
intent={isCorrect || !value ? undefined : Intent.DANGER}
onChange={this.handleChange}
placeholder='https://gist.github.com/...'
value={value}
rightElement={this.renderLoadButton(isCorrect)}
/>
<fieldset disabled={isPublishing}>
<InputGroup
key='addressbar'
leftIcon='geosearch'
intent={isCorrect || !value ? undefined : Intent.DANGER}
onChange={this.handleChange}
placeholder='https://gist.github.com/...'
value={value}
rightElement={this.renderLoadButton(isCorrect)}
/>
</fieldset>
</form>
);
}
/**
* Loading a fiddle from GitHub succeeded, let's move on.
*
* @private
* @param {Partial<EditorValues>} values
* @param {string} gistId
* @returns {boolean}
*/
private async handleLoadingSuccess(values: Partial<EditorValues>, gistId: string): Promise<boolean> {
const { appState } = this.props;
appState.setWarningDialogTexts({
label: 'Loading the fiddle will replace your current unsaved changes. Do you want to discard them?'
});
await window.ElectronFiddle.app.setValues(values);
document.title = getTitle(appState);
appState.gistId = gistId;
appState.localPath = undefined;
appState.templateName = undefined;
return true;
}
/**
* Loading a fiddle from GitHub failed - this method handles this case
* gracefully.
*
* @private
* @param {Error} error
* @returns {boolean}
*/
private handleLoadingFailed(error: Error): false {
this.props.appState.setWarningDialogTexts({
label: `Loading the fiddle failed: ${error}`,
cancel: undefined
});
this.props.appState.toogleWarningDialog();
console.warn(`Loading Fiddle failed`, error);
return false;
}
}
@@ -0,0 +1,85 @@
import { Button } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { GenericDialogType, VersionState } from '../../../src/interfaces';
import { AppState } from '../state';
interface BisectHandlerProps {
appState: AppState;
}
@observer
export class BisectHandler extends React.Component<BisectHandlerProps> {
constructor(props: BisectHandlerProps) {
super(props);
this.continueBisect = this.continueBisect.bind(this);
this.terminateBisect = this.terminateBisect.bind(this);
}
public continueBisect(isGood: boolean) {
window.ElectronFiddle.app.runner.stop();
const { appState } = this.props;
const response = appState.Bisector!.continue(isGood);
if (Array.isArray(response)) {
this.terminateBisect();
const [minRev, maxRev] = response;
const [minVer, maxVer] = [minRev.version, maxRev.version];
const message = `Check between versions ${minVer} and ${maxVer}.`;
appState.pushOutput(`[BISECT] Complete: ${message}`);
appState.setGenericDialogOptions({
type: GenericDialogType.success,
label: `Bisect complete. ${message}`,
cancel: undefined
});
appState.isGenericDialogShowing = true;
} else {
appState.setVersion(response.version);
}
}
public terminateBisect() {
const { appState } = this.props;
appState.Bisector = undefined;
}
public render() {
const { appState } = this.props;
if (!!appState.Bisector) {
const isDownloading = appState.currentElectronVersion.state === VersionState.downloading;
return (
<>
<Button
icon={'thumbs-up'}
onClick={() => this.continueBisect(true)}
disabled={isDownloading}
/>
<Button
icon={'thumbs-down'}
onClick={() => this.continueBisect(false)}
disabled={isDownloading}
/>
<Button
icon={'cross'}
onClick={this.terminateBisect}
>
Cancel Bisect
</Button>
</>
);
} else {
return (
<Button
icon='git-branch'
text='Bisect'
onClick={appState.toggleBisectDialog}
/>
);
}
}
}
@@ -77,6 +77,7 @@ export class EditorDropdown extends React.Component<EditorDropdownProps, EditorD
text={TITLE_MAP[id]}
id={id}
onClick={this.onItemClick}
disabled={appState.mosaicArrangement === id} // can't hide last editor panel
/>
);
}
@@ -4,7 +4,7 @@ import * as React from 'react';
import { when } from 'mobx';
import { IpcEvents } from '../../ipc-events';
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../../shared-constants';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME, STYLES_CSS_NAME } from '../../shared-constants';
import { getOctokit } from '../../utils/octokit';
import { ipcRendererManager } from '../ipc';
import { AppState } from '../state';
@@ -13,10 +13,6 @@ export interface PublishButtonProps {
appState: AppState;
}
export interface PublishButtonState {
isPublishing: boolean;
}
/**
* The "publish" button takes care of logging you in.
*
@@ -25,11 +21,9 @@ export interface PublishButtonState {
* @extends {React.Component<PublishButtonProps, PublishButtonState>}
*/
@observer
export class PublishButton extends React.Component<PublishButtonProps, PublishButtonState> {
export class PublishButton extends React.Component<PublishButtonProps> {
constructor(props: PublishButtonProps) {
super(props);
this.state = { isPublishing: false };
this.handleClick = this.handleClick.bind(this);
this.publishFiddle = this.publishFiddle.bind(this);
this.setPrivate = this.setPrivate.bind(this);
@@ -40,6 +34,10 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
ipcRendererManager.on(IpcEvents.FS_SAVE_FIDDLE_GIST, this.handleClick);
}
public componentWillUnmount() {
ipcRendererManager.off(IpcEvents.FS_SAVE_FIDDLE_GIST, this.handleClick);
}
/**
* When the user clicks the publish button, we either show the
* authentication dialog or publish right away.
@@ -71,12 +69,13 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
* and update all related properties in the app state.
*/
public async publishFiddle(): Promise<void> {
this.setState({ isPublishing: true });
const { appState } = this.props;
appState.isPublishing = true;
const octo = await getOctokit(this.props.appState);
const { gitHubPublishAsPublic } = this.props.appState;
const options = { includeDependencies: true, includeElectron: true };
const values = await window.ElectronFiddle.app.getValues(options);
const values = await window.ElectronFiddle.app.getEditorValues(options);
try {
const gist = await octo.gists.create({
@@ -92,10 +91,16 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
[RENDERER_JS_NAME]: {
content: values.renderer || '// Empty',
},
[PRELOAD_JS_NAME]: {
content: values.preload || '// Empty',
},
[STYLES_CSS_NAME]: {
content: values.css || '/* Empty */',
},
},
} as any); // Note: GitHub messed up, GistsCreateParamsFiles is an incorrect interface
this.props.appState.gistId = gist.data.id;
appState.gistId = gist.data.id;
console.log(`Publish Button: Publishing done`, { gist });
} catch (error) {
@@ -109,7 +114,7 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
ipcRendererManager.send(IpcEvents.SHOW_WARNING_DIALOG, messageBoxOptions);
}
this.setState({ isPublishing: false });
appState.isPublishing = false;
}
/**
@@ -132,7 +137,7 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
public render() {
const { gitHubPublishAsPublic } = this.props.appState;
const { isPublishing } = this.state;
const { isPublishing } = this.props.appState;
const privacyIcon = gitHubPublishAsPublic ? 'unlock' : 'lock';
const privacyMenu = (
@@ -153,24 +158,24 @@ export class PublishButton extends React.Component<PublishButtonProps, PublishBu
);
return (
<ButtonGroup className='button-publish'>
<Popover
content={privacyMenu}
position={Position.BOTTOM}
disabled={isPublishing}
>
<Button
icon={privacyIcon}
/>
</Popover>
<Button
onClick={this.handleClick}
loading={isPublishing}
disabled={isPublishing}
icon='upload'
text={isPublishing ? 'Publishing...' : 'Publish'}
/>
</ButtonGroup>
<fieldset disabled={isPublishing}>
<ButtonGroup className='button-publish'>
<Popover
content={privacyMenu}
position={Position.BOTTOM}
>
<Button
icon={privacyIcon}
/>
</Popover>
<Button
onClick={this.handleClick}
loading={isPublishing}
icon='upload'
text={isPublishing ? 'Publishing...' : 'Publish'}
/>
</ButtonGroup>
</fieldset>
);
}
+9 -6
View File
@@ -2,7 +2,7 @@ import { Button, IButtonProps, Spinner } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ElectronVersionState } from '../../interfaces';
import { VersionState } from '../../interfaces';
import { AppState } from '../state';
export interface RunnerState {
@@ -25,13 +25,17 @@ export class Runner extends React.Component<RunnerProps, RunnerState> {
const { isRunning, currentElectronVersion } = this.props.appState;
const state = currentElectronVersion && currentElectronVersion.state;
const props: IButtonProps = { className: 'button-run' };
const props: IButtonProps = { className: 'button-run', disabled: true };
if (state === ElectronVersionState.downloading) {
if (state === VersionState.downloading) {
props.text = 'Downloading';
props.disabled = true;
props.icon = <Spinner size={16} value={currentElectronVersion?.downloadProgress} />;
} else if (state === VersionState.unzipping) {
props.text = 'Unzipping';
props.icon = <Spinner size={16} />;
} else if (state === ElectronVersionState.ready) {
} else if (state === VersionState.ready) {
props.disabled = false;
if (isRunning) {
props.active = true;
props.text = 'Stop';
@@ -44,7 +48,6 @@ export class Runner extends React.Component<RunnerProps, RunnerState> {
}
} else {
props.text = 'Checking status';
props.disabled = true;
props.icon = <Spinner size={16} />;
}
@@ -1,95 +1,9 @@
import { Button, MenuItem } from '@blueprintjs/core';
import { ItemPredicate, ItemRenderer, Select } from '@blueprintjs/select';
import { ButtonGroup } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ElectronVersion, ElectronVersionSource, ElectronVersionState } from '../../interfaces';
import { highlightText } from '../../utils/highlight-text';
import { sortedElectronMap } from '../../utils/sorted-electron-map';
import { AppState } from '../state';
import { getReleaseChannel } from '../versions';
const ElectronVersionSelect = Select.ofType<ElectronVersion>();
/**
* Helper method: Returns the <Select /> label for an Electron
* version.
*
* @param {ElectronVersion} { source, state }
* @returns {string}
*/
export function getItemLabel({ source, state, name }: ElectronVersion): string {
let label = '';
if (source === ElectronVersionSource.local) {
label = name || 'Local';
} else {
if (state === ElectronVersionState.unknown) {
label = `Not downloaded`;
} else if (state === ElectronVersionState.ready) {
label = `Downloaded`;
} else if (state === ElectronVersionState.downloading) {
label = `Downloading`;
}
}
return label;
}
/**
* Helper method: Returns the <Select /> icon for an Electron
* version.
*
* @param {ElectronVersion} { state }
* @returns
*/
export function getItemIcon({ state }: ElectronVersion) {
return state === 'ready'
? 'saved'
: state === 'downloading' ? 'cloud-download' : 'cloud';
}
/**
* Helper method: Returns the <Select /> predicate for an Electron
* version.
*
* @param {string} query
* @param {ElectronVersion} { version }
* @returns
*/
export const filterItem: ItemPredicate<ElectronVersion> = (query, { version }) => {
return version.toLowerCase().includes(query.toLowerCase());
};
/**
* Helper method: Returns the <Select /> <MenuItem /> for Electron
* versions.
*
* @param {ElectronVersion} item
* @param {IItemRendererProps} { handleClick, modifiers, query }
* @returns
*/
export const renderItem: ItemRenderer<ElectronVersion> = (item, { handleClick, modifiers, query }) => {
if (!modifiers.matchesPredicate) {
return null;
}
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
text={highlightText(item.version, query)}
key={item.version}
onClick={handleClick}
label={getItemLabel(item)}
icon={getItemIcon(item)}
/>
);
};
export interface VersionChooserState {
value: string;
}
import { VersionSelect } from './version-select';
export interface VersionChooserProps {
appState: AppState;
@@ -98,70 +12,18 @@ export interface VersionChooserProps {
/**
* A dropdown allowing the selection of Electron versions. The actual
* download is managed in the state.
*
* @class VersionChooser
* @extends {React.Component<VersionChooserProps, VersionChooserState>}
*/
@observer
export class VersionChooser extends React.Component<VersionChooserProps, VersionChooserState> {
constructor(props: VersionChooserProps) {
super(props);
export const VersionChooser = observer((props: VersionChooserProps) => {
const { currentElectronVersion, Bisector, setVersion } = props.appState;
this.onItemSelect = this.onItemSelect.bind(this);
}
/**
* Handle change, which usually means that we'd like update
* the selection version.
*
* @param {React.ChangeEvent<HTMLSelectElement>} event
*/
public onItemSelect({ version }: ElectronVersion) {
this.props.appState.setVersion(version);
}
public getItems(): Array<ElectronVersion> {
const { versions, versionsToShow, statesToShow } = this.props.appState;
return sortedElectronMap<ElectronVersion>(versions, (_key, item) => item)
.filter((item) => {
if (!item) {
return false;
}
// Check if we want to show the version
if (!versionsToShow.includes(getReleaseChannel(item))) {
return false;
}
// Check if we want to show the state
if (!statesToShow.includes(item.state)) {
return false;
}
return true;
});
}
public render() {
const { currentElectronVersion } = this.props.appState;
const { version } = currentElectronVersion;
return (
<ElectronVersionSelect
filterable={true}
items={this.getItems()}
itemRenderer={renderItem}
itemPredicate={filterItem}
onItemSelect={this.onItemSelect}
noResults={<MenuItem disabled={true} text='No results.' />}
>
<Button
className='version-chooser'
text={`Electron v${version}`}
icon={getItemIcon(currentElectronVersion)}
/>
</ElectronVersionSelect>
);
}
}
return (
<ButtonGroup>
<VersionSelect
appState={props.appState}
onVersionSelect={({ version }) => setVersion(version)}
currentVersion={currentElectronVersion}
disabled={!!Bisector}
/>
</ButtonGroup>
);
});
+11
View File
@@ -4,6 +4,7 @@ import * as React from 'react';
import { AppState } from '../state';
import { AddressBar } from './commands-address-bar';
import { BisectHandler } from './commands-bisect';
import { EditorDropdown } from './commands-editors';
import { PublishButton } from './commands-publish-button';
import { Runner } from './commands-runner';
@@ -28,6 +29,7 @@ export class Commands extends React.Component<CommandsProps, {}> {
public render() {
const { appState } = this.props;
const { isBisectCommandShowing: isBisectCommandShowing } = appState;
return (
<div className='commands'>
@@ -36,6 +38,15 @@ export class Commands extends React.Component<CommandsProps, {}> {
<VersionChooser appState={appState} />
<Runner appState={appState} />
</ControlGroup>
{
// tslint:disable-next-line jsx-no-multiline-js
isBisectCommandShowing &&
(
<ControlGroup fill={true} vertical={false}>
<BisectHandler appState={appState} />
</ControlGroup>
)
}
<ControlGroup fill={true} vertical={false}>
<Button
active={appState.isConsoleShowing}
@@ -0,0 +1,164 @@
import { Button, Dialog, FileInput } from '@blueprintjs/core';
import { shell } from 'electron';
import * as fsType from 'fs-extra';
import { observer } from 'mobx-react';
import * as MonacoType from 'monaco-editor';
import * as path from 'path';
import * as React from 'react';
import { getTheme, THEMES_PATH } from '../themes';
import { GenericDialogType } from '../../../src/interfaces';
import { AppState } from '../state';
import { defaultDark, LoadedFiddleTheme } from '../themes-defaults';
export interface AddThemeDialogProps {
appState: AppState;
}
export interface AddThemeDialogState {
file?: File;
}
/**
* The "add monaco theme" dialog allows users to add custom editor themes.
*
* @class AddThemeDialog
* @extends {React.Component<AddThemeDialogProps, AddThemeDialogState>}
*/
@observer
export class AddThemeDialog extends React.Component<AddThemeDialogProps, AddThemeDialogState> {
public resetState = { file: undefined };
constructor(props: AddThemeDialogProps) {
super(props);
this.state = this.resetState;
this.onSubmit = this.onSubmit.bind(this);
this.onClose = this.onClose.bind(this);
this.onChangeFile = this.onChangeFile.bind(this);
this.reset = this.reset.bind(this);
}
/**
* Handles a change of the file input
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public async onChangeFile(event: React.FormEvent<HTMLInputElement>) {
const { files } = event.target as any;
const file = files && files[0] ? files[0] : undefined;
this.setState({ file });
}
/**
* Handles the submission of the dialog
*
* @returns {Promise<void>}
*/
public async onSubmit(): Promise<void> {
const { file } = this.state;
const defaultTheme = !!this.props.appState.theme ? await getTheme(this.props.appState.theme) : defaultDark;
if (!file) return;
try {
const editor = fsType.readJSONSync(file.path);
if (!editor.base && !editor.rules) throw Error('File does not match specifications'); // has to have these attributes
defaultTheme.editor = editor as Partial<MonacoType.editor.IStandaloneThemeData>;
const newTheme = defaultTheme;
const name = editor.name ? editor.name : file.name;
await this.createNewThemeFromMonaco(name, newTheme);
} catch (error) {
this.props.appState.setGenericDialogOptions({
type: GenericDialogType.warning,
label: `Error: ${error}, please pick a different file.`
});
this.props.appState.isGenericDialogShowing = true;
return;
}
this.onClose();
return;
}
public async createNewThemeFromMonaco(name: string, newTheme: LoadedFiddleTheme): Promise<boolean> {
if (!name) return false;
const themePath = path.join(THEMES_PATH, `${name}`);
await fsType.outputJSON(themePath, {
...newTheme,
name,
}, {spaces: 2});
this.props.appState.setTheme(themePath);
shell.showItemInFolder(themePath);
return true;
}
get buttons() {
const canSubmit = !!this.state.file;
return [
(
<Button
icon='add'
key='submit'
disabled={!canSubmit}
onClick={this.onSubmit}
text='Add'
/>
), (
<Button
icon='cross'
key='cancel'
onClick={this.onClose}
text='Cancel'
/>
)
];
}
public onClose() {
this.props.appState.isThemeDialogShowing = false;
this.reset();
}
public render() {
const { isThemeDialogShowing } = this.props.appState;
const inputProps = { accept: '.json' };
const { file } = this.state;
const text = file && file.path ? file.path : `Select the Monaco file...`;
return (
<Dialog
isOpen={isThemeDialogShowing}
onClose={this.onClose}
title='Add theme'
className='dialog-add-version'
>
<div className='bp3-dialog-body'>
<FileInput
onInputChange={this.onChangeFile}
inputProps={inputProps as any}
text={text}
/>
<br />
</div>
<div className='bp3-dialog-footer'>
<div className='bp3-dialog-footer-actions'>
{this.buttons}
</div>
</div>
</Dialog>
);
}
/**
* Reset this component's state
*/
private reset(): void {
this.setState(this.resetState);
return;
}
}
+34 -27
View File
@@ -5,8 +5,11 @@ import * as path from 'path';
import * as React from 'react';
import * as semver from 'semver';
import { NpmVersion } from '../../interfaces';
import { Version } from '../../interfaces';
import { IpcEvents } from '../../ipc-events';
import { getElectronNameForPlatform } from '../../utils/electron-name';
import { getIsDownloaded } from '../binary';
import { ipcRendererManager } from '../ipc';
import { AppState } from '../state';
export interface AddVersionDialogProps {
@@ -16,7 +19,7 @@ export interface AddVersionDialogProps {
export interface AddVersionDialogState {
isValidElectron: boolean;
isValidVersion: boolean;
file?: File;
folderPath?: string;
version: string;
}
@@ -39,8 +42,15 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
this.onSubmit = this.onSubmit.bind(this);
this.onClose = this.onClose.bind(this);
this.onChangeFile = this.onChangeFile.bind(this);
this.onChangeVersion = this.onChangeVersion.bind(this);
ipcRendererManager.on(IpcEvents.LOAD_LOCAL_VERSION_FOLDER, (_event, [file]) => {
this.setFolderPath(file);
});
}
public componentWillUnmount() {
ipcRendererManager.removeAllListeners(IpcEvents.LOAD_LOCAL_VERSION_FOLDER);
}
/**
@@ -48,16 +58,10 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public async onChangeFile(event: React.FormEvent<HTMLInputElement>) {
const { files } = event.target as any;
const { binaryManager } = this.props.appState;
const file = files && files[0] ? files[0] : undefined;
public async setFolderPath(folderPath: string) {
const isValidElectron = !!await getIsDownloaded('custom', folderPath);
const isValidElectron = !!(file
&& file.path
&& await binaryManager.getIsDownloaded('custom', file.path));
this.setState({ file, isValidElectron });
this.setState({ folderPath, isValidElectron });
}
public onChangeVersion(event: React.ChangeEvent<HTMLInputElement>) {
@@ -67,7 +71,7 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
this.setState({
version,
isValidVersion,
});
});
}
/**
@@ -76,18 +80,18 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
* @returns {Promise<void>}
*/
public async onSubmit(): Promise<void> {
const { file, version } = this.state;
const { folderPath, version } = this.state;
if (!file) return;
if (!folderPath) return;
const name = file.path
const name = folderPath
.slice(-20)
.split(path.sep)
.slice(1)
.join(path.sep);
const toAdd: NpmVersion = {
localPath: file.path,
const toAdd: Version = {
localPath: folderPath,
version,
name
};
@@ -129,12 +133,16 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
public render() {
const { isAddVersionDialogShowing } = this.props.appState;
const inputProps = { webkitdirectory: 'true' };
const { file } = this.state;
const inputProps = {
onClick: (e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
e.preventDefault();
ipcRendererManager.send(IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG);
}
};
const { folderPath } = this.state;
const text = file && file.path
? file.path
: `Select the folder containing ${getElectronNameForPlatform()}...`;
const text = folderPath ||
`Select the folder containing ${getElectronNameForPlatform()}...`;
return (
<Dialog
@@ -145,7 +153,6 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
>
<div className='bp3-dialog-body'>
<FileInput
onInputChange={this.onChangeFile}
id='custom-electron-version'
inputProps={inputProps as any}
text={text}
@@ -163,9 +170,9 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
}
private renderPath(): JSX.Element | null {
const { file, isValidElectron } = this.state;
const { folderPath, isValidElectron } = this.state;
if (!file || !file.path) return null;
if (!folderPath) return null;
const info = isValidElectron
? `We found an ${getElectronNameForPlatform()} in this folder.`
@@ -208,7 +215,7 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
isValidElectron: false,
isValidVersion: false,
version: '',
file: undefined
folderPath: undefined
});
}
}
+236
View File
@@ -0,0 +1,236 @@
import { Button, ButtonGroup, Callout, Dialog, Label } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { RunnableVersion } from '../../interfaces';
import { Bisector } from '../bisect';
import { AppState } from '../state';
import { VersionSelect } from './version-select';
export interface BisectDialogProps {
appState: AppState;
}
export interface BisectDialogState {
startIndex: number;
endIndex: number;
allVersions: Array<RunnableVersion>;
showHelp?: boolean;
}
/**
* The "add version" dialog allows users to add custom builds of Electron.
*
* @class BisectDialog
* @extends {React.Component<BisectDialogProps, BisectDialogState>}
*/
@observer
export class BisectDialog extends React.Component<BisectDialogProps, BisectDialogState> {
constructor(props: BisectDialogProps) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
this.onClose = this.onClose.bind(this);
this.onBeginSelect = this.onBeginSelect.bind(this);
this.onEndSelect = this.onEndSelect.bind(this);
this.showHelp = this.showHelp.bind(this);
this.isEarliestItemDisabled = this.isEarliestItemDisabled.bind(this);
this.isLatestItemDisabled = this.isLatestItemDisabled.bind(this);
this.state = {
allVersions: this.props.appState.versionsToShow,
startIndex: 10,
endIndex: 0
};
}
public onBeginSelect(version: RunnableVersion) {
this.setState({ startIndex: this.state.allVersions.indexOf(version) });
}
public onEndSelect(version: RunnableVersion) {
this.setState({ endIndex: this.state.allVersions.indexOf(version) });
}
/**
* Handles the submission of the dialog
*
* @returns {Promise<void>}
*/
public async onSubmit(): Promise<void> {
const { endIndex, startIndex, allVersions } = this.state;
const { appState } = this.props;
if (endIndex === undefined || startIndex === undefined) {
return;
}
const bisectRange = allVersions
.slice(endIndex, startIndex + 1);
appState.Bisector = new Bisector(bisectRange);
const initialBisectPivot = appState.Bisector.getCurrentVersion().version;
appState.setVersion(initialBisectPivot);
this.onClose();
}
/**
* Closes the dialog
*/
public onClose() {
this.props.appState.isBisectDialogShowing = false;
}
/**
* Shows the additional help
*/
public showHelp() {
this.setState({ showHelp: true });
}
/**
* Can we get this show on the road?
*/
get canSubmit(): boolean {
return this.state.startIndex > this.state.endIndex;
}
/**
* Renders the buttons
*/
get buttons() {
return [
(
<Button
icon='play'
key='submit'
disabled={!this.canSubmit}
onClick={this.onSubmit}
text='Begin'
/>
), (
<Button
icon='cross'
key='cancel'
onClick={this.onClose}
text='Cancel'
/>
)
];
}
/**
* Renders the help
*/
get help() {
let moreHelp = (
<Button
icon='help'
text='Show help'
onClick={this.showHelp}
/>
);
if (this.state.showHelp) {
moreHelp = (
<>
<p>
First, write a fiddle that reproduces a bug or an issue. Then, select the earliest version to
start your search with. Typically, that's the "last known good" version that did not have the bug.
Then, select that latest version to end the search with, usually the "first known bad" version.
</p>
<p>
Once you begin your bisect, Fiddle will run your fiddle with a number of Electron versions, closing
in on the version that introduced the bug. Once completed, you will know which Electron version
introduced your issue.
</p>
</>
);
}
return (
<Callout style={{ marginTop: 0, marginBottom: '1rem' }}>
<p>
A "bisect" is a popular method <a href='https://git-scm.com/docs/git-bisect' target='_blank'>
borrowed from <code>git</code></a> for learning which version of Electron introduced a bug. This
tool helps you perform a bisect.
</p>
{moreHelp}
</Callout>
);
}
public render() {
const { isBisectDialogShowing } = this.props.appState;
const { startIndex, endIndex, allVersions } = this.state;
return (
<Dialog
isOpen={isBisectDialogShowing}
onClose={this.onClose}
title='Start a bisect session'
className='dialog-add-version'
>
<div className='bp3-dialog-body'>
{this.help}
<Label>
Earliest Version (Last "known good" version)
<ButtonGroup fill={true}>
<VersionSelect
currentVersion={allVersions[startIndex]}
appState={this.props.appState}
onVersionSelect={this.onBeginSelect}
itemDisabled={this.isEarliestItemDisabled}
/>
</ButtonGroup>
</Label>
<Label>
Latest Version (First "known bad" version)
<ButtonGroup fill={true}>
<VersionSelect
currentVersion={allVersions[endIndex]}
appState={this.props.appState}
onVersionSelect={this.onEndSelect}
itemDisabled={this.isLatestItemDisabled}
/>
</ButtonGroup>
</Label>
</div>
<div className='bp3-dialog-footer'>
<div className='bp3-dialog-footer-actions'>
{this.buttons}
</div>
</div>
</Dialog>
);
}
/**
* Should an item in the "earliest version" dropdown be disabled?
*
* @param {RunnableVersion} version
* @returns {boolean}
*/
public isEarliestItemDisabled(version: RunnableVersion): boolean {
const { allVersions, endIndex } = this.state;
// In the array, "newer" versions will have a lower index.
// 0: 5.0.0
// 1: 4.0.0
// 2: 3.0.0
// ...
return allVersions.indexOf(version) < endIndex + 1;
}
/**
* Should an item in the "latest version" dropdown be disabled?
*
* @param {RunnableVersion} version
* @returns {boolean}
*/
public isLatestItemDisabled(version: RunnableVersion): boolean {
const { allVersions, startIndex } = this.state;
return allVersions.indexOf(version) > startIndex - 1;
}
}
@@ -0,0 +1,73 @@
import { Alert, IconName, Intent} from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { GenericDialogType } from '../../../src/interfaces';
import { AppState } from '../state';
export interface GenericDialogProps {
appState: AppState;
}
export interface GenericDialogState {
}
/**
* The token dialog prompts the user to either continue or cancel the operation.
*
* @export
* @class GenericDialog
* @extends {React.Component<GenericDialogProps, GenericDialogState>}
*/
@observer
export class GenericDialog extends React.Component<GenericDialogProps, GenericDialogState> {
constructor(props: GenericDialogProps) {
super(props);
this.onClose = this.onClose.bind(this);
}
public onClose(result: boolean) {
this.props.appState.genericDialogLastResult = result;
this.props.appState.toggleGenericDialog();
}
public render() {
const { isGenericDialogShowing, genericDialogOptions } = this.props.appState;
const {type, ok, cancel, label} = genericDialogOptions;
let intent: Intent;
let icon: IconName;
switch (type) {
case GenericDialogType.warning:
intent = Intent.DANGER;
icon = 'warning-sign';
break;
case GenericDialogType.confirm:
intent = Intent.PRIMARY;
icon = 'help';
break;
case GenericDialogType.success:
intent = Intent.SUCCESS;
icon = 'info-sign';
break;
default:
intent = Intent.NONE;
icon = 'help';
break;
}
return (
<Alert
isOpen={isGenericDialogShowing}
onClose={this.onClose}
icon={icon}
confirmButtonText={ok}
cancelButtonText={cancel}
intent={intent}
>
<p>{label}</p>
</Alert>
);
}
}
+2 -3
View File
@@ -56,6 +56,7 @@ export class TokenDialog extends React.Component<TokenDialogProps, TokenDialogSt
public async onSubmitToken(): Promise<void> {
if (!this.state.tokenInput) return;
this.setState({ verifying: true });
this.props.appState.gitHubToken = this.state.tokenInput;
const octo = await getOctokit(this.props.appState);
@@ -64,16 +65,14 @@ export class TokenDialog extends React.Component<TokenDialogProps, TokenDialogSt
this.props.appState.gitHubAvatarUrl = data.avatar_url;
this.props.appState.gitHubLogin = data.login;
this.props.appState.gitHubName = data.name;
this.setState({ verifying: false, error: false });
} catch (error) {
console.warn(`Authenticating against GitHub failed`, error);
this.setState({ verifying: false, error: true });
return;
}
this.props.appState.gitHubToken = this.state.tokenInput;
this.props.appState.isTokenDialogShowing = false;
this.setState({ verifying: false, error: false });
}
/**
@@ -1,52 +0,0 @@
import { Alert, Intent } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { AppState } from '../state';
export interface WarningDialogProps {
appState: AppState;
}
export interface WarningDialogState {
}
/**
* The token dialog asks the user for a GitHub Personal Access Token.
* It's also responsible for checking if the token is correct.
*
* @export
* @class WarningDialog
* @extends {React.Component<WarningDialogProps, WarningDialogState>}
*/
@observer
export class WarningDialog extends React.Component<WarningDialogProps, WarningDialogState> {
constructor(props: WarningDialogProps) {
super(props);
this.onClose = this.onClose.bind(this);
}
public onClose(result: boolean) {
this.props.appState.warningDialogLastResult = result;
this.props.appState.toogleWarningDialog();
}
public render() {
const { isWarningDialogShowing, warningDialogTexts } = this.props.appState;
return (
<Alert
isOpen={isWarningDialogShowing}
onClose={this.onClose}
icon='warning-sign'
confirmButtonText={warningDialogTexts.ok}
cancelButtonText={warningDialogTexts.cancel}
intent={Intent.DANGER}
>
<p>{warningDialogTexts.label}</p>
</Alert>
);
}
}
+22 -3
View File
@@ -2,9 +2,11 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { AppState } from '../state';
import { AddThemeDialog } from './dialog-add-theme';
import { AddVersionDialog } from './dialog-add-version';
import { BisectDialog } from './dialog-bisect';
import { GenericDialog } from './dialog-generic';
import { TokenDialog } from './dialog-token';
import { WarningDialog } from './dialog-warning';
import { Settings } from './settings';
export interface DialogsProps {
@@ -21,7 +23,14 @@ export interface DialogsProps {
export class Dialogs extends React.Component<DialogsProps, {}> {
public render() {
const { appState } = this.props;
const { isTokenDialogShowing, isSettingsShowing, isAddVersionDialogShowing } = appState;
const {
isTokenDialogShowing,
isSettingsShowing,
isAddVersionDialogShowing,
isThemeDialogShowing,
isBisectDialogShowing,
isGenericDialogShowing
} = appState;
const maybeToken = isTokenDialogShowing
? <TokenDialog key='dialogs' appState={appState} />
: null;
@@ -31,13 +40,23 @@ export class Dialogs extends React.Component<DialogsProps, {}> {
const maybeAddLocalVersion = isAddVersionDialogShowing
? <AddVersionDialog key='add-version-dialog' appState={appState} />
: null;
const maybeMonaco = isThemeDialogShowing ? <AddThemeDialog appState={appState} />
: null;
const maybeBisect = isBisectDialogShowing
? <BisectDialog key='bisect-dialog' appState={appState} />
: null;
const genericDialog = isGenericDialogShowing
? <GenericDialog appState={appState} />
: null;
return (
<div key='dialogs' className='dialogs'>
{maybeToken}
{maybeSettings}
{maybeAddLocalVersion}
<WarningDialog appState={appState} />
{maybeMonaco}
{maybeBisect}
{genericDialog}
</div>
);
}
+46 -10
View File
@@ -11,11 +11,12 @@ import { AppState } from '../state';
export interface EditorProps {
appState: AppState;
monaco: typeof MonacoType;
monoacoOptions: MonacoType.editor.IEditorOptions;
monacoOptions: MonacoType.editor.IEditorOptions;
id: EditorId;
options?: Partial<MonacoType.editor.IEditorConstructionOptions>;
editorDidMount?: (editor: MonacoType.editor.IStandaloneCodeEditor) => void;
onChange?: (value: string, event: MonacoType.editor.IModelContentChangedEvent) => void;
setFocused: (id: EditorId) => void;
}
export class Editor extends React.Component<EditorProps> {
@@ -28,7 +29,16 @@ export class Editor extends React.Component<EditorProps> {
constructor(props: EditorProps) {
super(props);
this.language = props.id === 'html' ? 'html' : 'javascript';
switch (props.id) {
case 'html':
this.language = 'html';
break;
case 'css':
this.language = 'css';
break;
default:
this.language = 'javascript';
}
}
public shouldComponentUpdate() {
@@ -68,7 +78,7 @@ export class Editor extends React.Component<EditorProps> {
* Initialize Monaco.
*/
public async initMonaco() {
const { monaco, monoacoOptions } = this.props;
const { monaco, monacoOptions: monacoOptions } = this.props;
const ref = this.containerRef.current;
if (ref) {
@@ -77,7 +87,13 @@ export class Editor extends React.Component<EditorProps> {
theme: 'main',
contextmenu: false,
model: null,
...monoacoOptions
...monacoOptions
});
// mark this editor as focused whenever it is
this.editor.onDidFocusEditorText(() => {
const { id, setFocused } = this.props;
setFocused(id);
});
await this.editorDidMount(this.editor);
@@ -98,6 +114,23 @@ export class Editor extends React.Component<EditorProps> {
return <div className='editorContainer' ref={this.containerRef} />;
}
/**
* Create a model and attach it to the editor
*
* @private
* @param {string} value
*/
private async createModel(value: string) {
const { monaco } = this.props;
const model = monaco.editor.createModel(value, this.language);
model.updateOptions({
tabSize: 2
});
this.editor.setModel(model);
}
/**
* Sets the content on the editor, including the model and the view state.
*
@@ -105,24 +138,27 @@ export class Editor extends React.Component<EditorProps> {
* @memberof Editor
*/
private async setContent() {
const { appState, id, monaco } = this.props;
const { appState, id } = this.props;
const { version } = appState;
const backup = appState.getAndRemoveEditorValueBackup(id);
if (backup && backup.model) {
if (backup) {
console.log(`Editor: Backup found, restoring state`);
if (backup.viewState) {
this.editor.restoreViewState(backup.viewState);
}
this.editor.setModel(backup.model);
// If there's a model, use the model. No model? Use the value
if (backup.model) {
this.editor.setModel(backup.model);
} else {
this.createModel(backup.value ?? '');
}
} else {
const value = await getContent(id, version);
const model = monaco.editor.createModel(value, this.language);
this.editor.setModel(model);
await this.createModel(value);
}
}
}
@@ -1,7 +1,7 @@
import * as React from 'react';
import { Button } from '@blueprintjs/core';
import { MosaicWindowContext } from 'react-mosaic-component';
import { MosaicContext, MosaicRootActions, MosaicWindowContext } from 'react-mosaic-component';
import { DocsDemoPage, MosaicId } from '../../interfaces';
import { AppState } from '../state';
@@ -11,86 +11,78 @@ export interface ToolbarButtonProps {
id: MosaicId;
}
export class MaximizeButton extends React.PureComponent<ToolbarButtonProps> {
public static contextTypes = MosaicWindowContext;
public context: MosaicWindowContext<MosaicId>;
export abstract class ToolbarButton extends React.PureComponent<ToolbarButtonProps> {
public static contextType = MosaicWindowContext;
public context: MosaicWindowContext;
constructor(props: ToolbarButtonProps) {
super(props);
this.expand = this.expand.bind(this);
}
public render() {
return (
<MosaicContext.Consumer>
{({ mosaicActions }) => this.createButton(mosaicActions)}
</MosaicContext.Consumer>
);
}
/**
* Create a button that performs the actual action
*/
public abstract createButton(_mosaicActions: MosaicRootActions<any>): React.ReactNode;
}
export class MaximizeButton extends ToolbarButton {
/**
* Create a button that can expand this panel
*/
public createButton(mosaicActions: MosaicRootActions<any>) {
const onClick = () => {
mosaicActions.expand(this.context.mosaicWindowActions.getPath());
};
return (
<Button
icon='maximize'
className='bp3-small'
onClick={this.expand}
onClick={onClick}
/>
);
}
/**
* Expand this panel
*/
public expand() {
const path = this.context.mosaicWindowActions.getPath();
return this.context.mosaicActions.expand(path);
}
}
export class RemoveButton extends React.PureComponent<ToolbarButtonProps> {
public static contextTypes = MosaicWindowContext;
public context: MosaicWindowContext<MosaicId>;
export class RemoveButton extends ToolbarButton {
/**
* Create a button that can remove this panel
*/
public createButton(_mosaicActions: MosaicRootActions<any>) {
const onClick = () => this.props.appState.hideAndBackupMosaic(this.props.id);
constructor(props: ToolbarButtonProps) {
super(props);
this.remove = this.remove.bind(this);
}
public render() {
return (
<Button
icon='cross'
className='bp3-small'
onClick={this.remove}
onClick={onClick}
/>
);
}
/**
* Remove this panel
*/
public remove() {
this.props.appState.hideAndBackupMosaic(this.props.id);
}
}
export class DocsDemoGoHomeButton extends React.PureComponent<ToolbarButtonProps> {
public static contextTypes = MosaicWindowContext;
public context: MosaicWindowContext<MosaicId>;
export class DocsDemoGoHomeButton extends ToolbarButton {
/**
* Create a button that can remove this panel
*/
public createButton(_mosaicActions: MosaicRootActions<any>) {
const onClick = () => this.props.appState.currentDocsDemoPage = DocsDemoPage.DEFAULT;
constructor(props: ToolbarButtonProps) {
super(props);
this.goHome = this.goHome.bind(this);
}
public render() {
return (
<Button
icon='home'
className='bp3-small'
onClick={this.goHome}
onClick={onClick}
text='Overview'
/>
);
}
/**
* Remove this panel
*/
public goHome() {
this.props.appState.currentDocsDemoPage = DocsDemoPage.DEFAULT;
}
}
+51 -29
View File
@@ -2,7 +2,7 @@ import { reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as MonacoType from 'monaco-editor';
import * as React from 'react';
import { Mosaic, MosaicNode, MosaicWindow, MosaicWindowProps } from 'react-mosaic-component';
import { Mosaic, MosaicBranch, MosaicNode, MosaicWindow, MosaicWindowProps } from 'react-mosaic-component';
import { EditorId, MosaicId, PanelId } from '../../interfaces';
import { IpcEvents } from '../../ipc-events';
@@ -27,14 +27,13 @@ const defaultMonacoOptions: MonacoType.editor.IEditorOptions = {
wordWrap: 'on'
};
const ViewIdMosaic = Mosaic.ofType<EditorId>() as any;
const ViewIdMosaicWindow = MosaicWindow.ofType<EditorId>() as any;
export const TITLE_MAP: Record<MosaicId, string> = {
main: 'Main Process (main.js)',
renderer: 'Renderer Process (renderer.js)',
preload: 'Preload (preload.js)',
html: 'HTML (index.html)',
docsDemo: 'Docs & Demos'
css: 'Stylesheet (styles.css)',
docsDemo: 'Docs & Demos',
};
export interface EditorsProps {
@@ -45,6 +44,7 @@ export interface EditorsState {
monaco?: typeof MonacoType;
isMounted?: boolean;
monacoOptions: MonacoType.editor.IEditorOptions;
focused?: EditorId;
}
@observer
@@ -63,10 +63,9 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
this.renderEditor = this.renderEditor.bind(this);
this.renderTile = this.renderTile.bind(this);
this.renderGenericPanel = this.renderGenericPanel.bind(this);
this.setFocused = this.setFocused.bind(this);
this.state = { monacoOptions: defaultMonacoOptions };
this.loadMonaco();
}
/**
@@ -74,7 +73,7 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
*
* @memberof Editors
*/
public componentDidMount() {
public async componentDidMount() {
ipcRendererManager.on(IpcEvents.MONACO_EXECUTE_COMMAND, (_event, cmd: string) => {
this.executeCommand(cmd);
});
@@ -82,17 +81,11 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
ipcRendererManager.on(IpcEvents.FS_NEW_FIDDLE, async (_event) => {
const { version } = this.props.appState;
this.props.appState.setWarningDialogTexts({
label: 'Your current fiddle is unsaved. Do you want to discard it?'
});
window.ElectronFiddle.app.fileManager.setFiddle({
values: {
html: await getContent(EditorId.html, version),
renderer: await getContent(EditorId.renderer, version),
main: await getContent(EditorId.main, version),
}
});
await window.ElectronFiddle.app.replaceFiddle({
html: await getContent(EditorId.html, version),
renderer: await getContent(EditorId.renderer, version),
main: await getContent(EditorId.main, version),
}, {});
});
ipcRendererManager.on(IpcEvents.MONACO_TOGGLE_OPTION, (_event, cmd: string) => {
@@ -100,10 +93,16 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
});
this.setState({ isMounted: true });
await this.loadMonaco();
this.props.appState.isUnsaved = false;
}
public componentWillUnmount() {
this.disposeLayoutAutorun();
ipcRendererManager.removeAllListeners(IpcEvents.MONACO_EXECUTE_COMMAND);
ipcRendererManager.removeAllListeners(IpcEvents.FS_NEW_FIDDLE);
ipcRendererManager.removeAllListeners(IpcEvents.MONACO_TOGGLE_OPTION);
}
/**
@@ -168,10 +167,22 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
public renderToolbar(
{ title }: MosaicWindowProps<MosaicId>, id: MosaicId
): JSX.Element {
const { appState } = this.props;
const docsDemoGoHomeMaybe = id === PanelId.docsDemo
? <DocsDemoGoHomeButton id={id} appState={this.props.appState} />
? <DocsDemoGoHomeButton id={id} appState={appState} />
: null;
// only show toolbar controls if we have more than 1 visible editor
// Mosaic arrangement is type string if 1 editor, object otherwise
const toolbarControlsMaybe =
(typeof appState.mosaicArrangement !== 'string') &&
(
<>
<MaximizeButton id={id} appState={appState} />
<RemoveButton id={id} appState={appState} />
</>
);
return (
<div>
{/* Left */}
@@ -185,8 +196,7 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
{/* Right */}
<div className='mosaic-controls'>
{docsDemoGoHomeMaybe}
<MaximizeButton id={id} appState={this.props.appState} />
<RemoveButton id={id} appState={this.props.appState} />
{toolbarControlsMaybe}
</div>
</div>
);
@@ -199,21 +209,21 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
* @param {string} path
* @returns {JSX.Element | null}
*/
public renderTile(id: MosaicId, path: string): JSX.Element | null {
public renderTile(id: MosaicId, path: Array<MosaicBranch>): JSX.Element {
const { appState } = this.props;
const content = isEditorId(id)
? this.renderEditor(id)
: this.renderGenericPanel(id, appState);
return (
<ViewIdMosaicWindow
<MosaicWindow<EditorId>
className={id}
path={path}
title={TITLE_MAP[id]}
renderToolbar={(props: MosaicWindowProps<MosaicId>) => this.renderToolbar(props, id)}
>
{content}
</ViewIdMosaicWindow>
</MosaicWindow>
);
}
@@ -244,7 +254,8 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
id={id}
monaco={monaco!}
appState={appState}
monoacoOptions={defaultMonacoOptions}
monacoOptions={defaultMonacoOptions}
setFocused={this.setFocused}
/>
);
}
@@ -256,7 +267,8 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
if (!monaco) return null;
return (
<ViewIdMosaic
<Mosaic<EditorId | PanelId>
className={`focused__${this.state.focused}`}
onChange={this.onChange}
value={appState.mosaicArrangement}
zeroStateView={renderNonIdealState(appState)}
@@ -279,7 +291,7 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
* We're doing things a bit roundabout to ensure that we're not overloading the
* mobx state with a gigantic Monaco tree.
*/
public async loadMonaco(): Promise<void> {
public async loadMonaco() {
const { app } = window.ElectronFiddle;
const loader = require('monaco-loader');
const monaco = app.monaco || await loader();
@@ -297,6 +309,16 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
this.setState({ monaco });
}
activateTheme(monaco, undefined, this.props.appState.theme);
await activateTheme(monaco, undefined, this.props.appState.theme);
}
/**
* Sets the currently-focused editor. This will impact the editor's
* z-index, ensuring that its intellisense menus don't get clipped
* by the other editor windows.
* @param id Editor ID
*/
public setFocused(id: EditorId): void {
this.setState({ focused: id });
}
}
-2
View File
@@ -3,7 +3,6 @@ import * as React from 'react';
import { AppState } from '../state';
import { ChromeMac } from './chrome-mac';
import { Commands } from './commands';
import { Output } from './output';
import { WelcomeTour } from './tour-welcome';
export interface HeaderProps {
@@ -23,7 +22,6 @@ export class Header extends React.Component<HeaderProps, {}> {
<ChromeMac appState={this.props.appState} />
<header id='header'>
<Commands key='commands' appState={this.props.appState} />
<Output key='output' appState={this.props.appState} />
</header>
<WelcomeTour appState={this.props.appState} />
</>
@@ -0,0 +1,59 @@
import * as React from 'react';
import { Mosaic, MosaicNode } from 'react-mosaic-component';
import { AppState } from '../state';
import { Editors } from './editors';
import { Output } from './output';
export interface WrapperProps {
appState: AppState;
}
export interface WrapperState {
mosaicArrangement: MosaicNode<WrapperMosaicId>;
}
export type WrapperMosaicId = 'output' | 'editors';
export class OutputEditorsWrapper extends React.Component<WrapperProps, WrapperState> {
private MOSAIC_ELEMENTS = {
output: <Output appState={this.props.appState} />,
editors: <Editors appState={this.props.appState} />
};
constructor(props: any) {
super(props);
this.state = {
mosaicArrangement: {
direction: 'column',
first: 'output',
second: 'editors',
splitPercentage: 25,
}
};
}
public render() {
return (
<>
<Mosaic<WrapperMosaicId>
renderTile={(id: string) => this.MOSAIC_ELEMENTS[id]}
resize={{minimumPaneSizePercentage: 0}}
value={this.state.mosaicArrangement}
onChange={this.onChange}
/>
</>
);
}
private onChange = (currentNode: any) => {
const isConsoleShowing = currentNode.splitPercentage !== 0;
if (isConsoleShowing !== this.props.appState.isConsoleShowing) {
this.props.appState.isConsoleShowing = isConsoleShowing;
}
this.setState({mosaicArrangement: currentNode});
}
}
+30 -4
View File
@@ -1,8 +1,12 @@
import { autorun } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { MosaicContext } from 'react-mosaic-component';
import { OutputEntry } from '../../interfaces';
import { AppState } from '../state';
import { WrapperMosaicId } from './output-editors-wrapper';
export interface CommandsProps {
appState: AppState;
@@ -19,6 +23,8 @@ export interface CommandsProps {
*/
@observer
export class Output extends React.Component<CommandsProps, {}> {
public static contextType = MosaicContext;
public context: MosaicContext<WrapperMosaicId>;
private outputRef = React.createRef<HTMLDivElement>();
constructor(props: CommandsProps) {
@@ -28,6 +34,23 @@ export class Output extends React.Component<CommandsProps, {}> {
this.renderEntry = this.renderEntry.bind(this);
}
public componentDidMount() {
autorun(() => {
const { isConsoleShowing } = this.props.appState;
// this context should always exist, but mocking context in enzyme
// is not fully supported, so this condition makes the tests pass
if (this.context.mosaicActions && this.context.mosaicActions.expand) {
if (!isConsoleShowing) {
this.context.mosaicActions.expand(['first'], 0);
} else {
this.context.mosaicActions.expand(['first'], 25);
}
}
});
}
public componentDidUpdate() {
if (this.outputRef.current) {
this.outputRef.current.scrollTop = this.outputRef.current.scrollHeight;
@@ -71,9 +94,12 @@ export class Output extends React.Component<CommandsProps, {}> {
));
}
public render() {
const { isConsoleShowing, output } = this.props.appState;
const className = isConsoleShowing ? 'output showing' : 'output';
public render(): JSX.Element | null {
const { output, isConsoleShowing } = this.props.appState;
if (!isConsoleShowing) {
return null;
}
// The last 1000 lines
const lines = output
@@ -81,7 +107,7 @@ export class Output extends React.Component<CommandsProps, {}> {
.map(this.renderEntry);
return (
<div className={className} ref={this.outputRef}>
<div className='output' ref={this.outputRef}>
{lines}
</div>
);
+80 -60
View File
@@ -7,12 +7,14 @@ import {
HTMLTable,
IButtonProps,
Icon,
IconName
IconName,
Tooltip,
ProgressBar
} from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ElectronVersion, ElectronVersionSource, ElectronVersionState } from '../../interfaces';
import { RunnableVersion, VersionSource, VersionState } from '../../interfaces';
import { normalizeVersion } from '../../utils/normalize-version';
import { sortedElectronMap } from '../../utils/sorted-electron-map';
import { AppState } from '../state';
@@ -69,7 +71,7 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
if (!checked) {
appState.statesToShow = appState.statesToShow.filter((s) => s !== id);
} else {
appState.statesToShow.push(id as ElectronVersionState);
appState.statesToShow.push(id as VersionState);
}
}
@@ -85,9 +87,9 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
const { appState } = this.props;
if (!checked) {
appState.versionsToShow = appState.versionsToShow.filter((c) => c !== id);
appState.channelsToShow = appState.channelsToShow.filter((c) => c !== id);
} else {
appState.versionsToShow.push(id as ElectronReleaseChannel);
appState.channelsToShow.push(id as ElectronReleaseChannel);
}
}
@@ -202,7 +204,7 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
*/
private renderVersionStateOptions(): JSX.Element {
const { appState } = this.props;
const getIsChecked = (state: ElectronVersionState) => {
const getIsChecked = (state: VersionState) => {
return appState.statesToShow.includes(state);
};
@@ -210,22 +212,29 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
<FormGroup
label='Include Electron versions that are:'
>
<Tooltip
content='Always enabled'
position='bottom'
intent='primary'
>
<Checkbox
checked={getIsChecked(VersionState.ready)}
label='Ready'
id='ready'
onChange={this.handleStateChange}
inline={true}
disabled={true}
/>
</Tooltip>
<Checkbox
checked={getIsChecked(ElectronVersionState.downloading)}
checked={getIsChecked(VersionState.downloading)}
label='Downloading'
id='downloading'
onChange={this.handleStateChange}
inline={true}
/>
<Checkbox
checked={getIsChecked(ElectronVersionState.ready)}
label='Downloaded'
id='ready'
onChange={this.handleStateChange}
inline={true}
/>
<Checkbox
checked={getIsChecked(ElectronVersionState.unknown)}
checked={getIsChecked(VersionState.unknown)}
label='Not Downloaded'
id='unknown'
onChange={this.handleStateChange}
@@ -243,42 +252,47 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
*/
private renderVersionChannelOptions(): JSX.Element {
const { appState } = this.props;
const getIsChecked = (channel: ElectronReleaseChannel) => {
return appState.versionsToShow.includes(channel);
return appState.channelsToShow.includes(channel);
};
const getIsCurrentVersionReleaseChannel = (channel: ElectronReleaseChannel) => {
return getReleaseChannel(appState.version) === channel;
};
const channels = {
stable: ElectronReleaseChannel.stable,
beta: ElectronReleaseChannel.beta,
nightly: ElectronReleaseChannel.nightly,
unsupported: ElectronReleaseChannel.unsupported
};
return (
<FormGroup
label='Include Electron versions from these release channels:'
>
<Checkbox
checked={getIsChecked(ElectronReleaseChannel.stable)}
label='Stable'
id='Stable'
onChange={this.handleChannelChange}
inline={true}
/>
<Checkbox
checked={getIsChecked(ElectronReleaseChannel.beta)}
label='Beta'
id='Beta'
onChange={this.handleChannelChange}
inline={true}
/>
<Checkbox
checked={getIsChecked(ElectronReleaseChannel.nightly)}
label='Nightly'
id='Nightly'
onChange={this.handleChannelChange}
inline={true}
/>
<Checkbox
checked={getIsChecked(ElectronReleaseChannel.unsupported)}
label='Unsupported'
id='Unsupported'
onChange={this.handleChannelChange}
inline={true}
/>
{
// tslint:disable-next-line:jsx-no-multiline-js
Object.entries(channels).map(([_, channel]) => (
<Tooltip
content={`Can't disable channel of selected version (${appState.version})`}
disabled={!getIsCurrentVersionReleaseChannel(channel)}
position='bottom'
intent='primary'
key={channel}
>
<Checkbox
checked={getIsChecked(channel)}
label={channel}
id={channel}
onChange={this.handleChannelChange}
disabled={getIsCurrentVersionReleaseChannel(channel)}
inline={true}
/>
</Tooltip>
))
}
</FormGroup>
);
}
@@ -316,11 +330,11 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
* @returns {Array<JSX.Element>}
*/
private renderTableRows(): Array<JSX.Element | null> {
const { versions, versionsToShow, statesToShow } = this.props.appState;
const { versions, channelsToShow, statesToShow } = this.props.appState;
return sortedElectronMap<JSX.Element | null>(versions, (key, item) => {
// Check if we want to show the version
if (!versionsToShow.includes(getReleaseChannel(item))) {
if (!channelsToShow.includes(getReleaseChannel(item))) {
return null;
}
@@ -342,25 +356,25 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
/**
* Returns a human-readable state indicator for an Electron version
*
* @param {ElectronVersion} item
* @param {RunnableVersion} item
* @returns {JSX.Element}
*/
private renderHumanState(item: ElectronVersion): JSX.Element {
private renderHumanState(item: RunnableVersion): JSX.Element {
const { state } = item;
let icon: IconName = 'box';
if (state === VersionState.downloading || state === VersionState.unzipping) {
return <ProgressBar value={item.downloadProgress} />;
}
let humanState = 'Downloaded';
if (state === ElectronVersionState.downloading) {
icon = 'cloud-download';
humanState = 'Downloading';
} else if (state === ElectronVersionState.unknown) {
icon = 'cloud';
if (state === VersionState.unknown) {
humanState = 'Not downloaded';
}
return (
<span>
<Icon icon={icon} /> {humanState}
{humanState}
</span>
);
}
@@ -370,10 +384,10 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
*
* @private
* @param {string} key
* @param {ElectronVersion} item
* @param {RunnableVersion} item
* @returns {JSX.Element}
*/
private renderAction(key: string, item: ElectronVersion): JSX.Element {
private renderAction(key: string, item: RunnableVersion): JSX.Element {
const { state, source } = item;
const { appState } = this.props;
const buttonProps: IButtonProps = {
@@ -382,17 +396,23 @@ export class ElectronSettings extends React.Component<ElectronSettingsProps, Ele
};
// Already downloaded
if (state === 'ready') {
if (state === VersionState.ready) {
buttonProps.disabled = false;
buttonProps.onClick = () => appState.removeVersion(key);
buttonProps.icon = 'trash';
buttonProps.text = source === ElectronVersionSource.local
buttonProps.text = source === VersionSource.local
? 'Remove'
: 'Delete';
} else if (state === 'downloading') {
} else if (state === VersionState.downloading) {
buttonProps.disabled = true;
buttonProps.loading = true;
buttonProps.text = 'Downloading';
buttonProps.icon = 'cloud-download';
} else if (state === VersionState.unzipping) {
buttonProps.disabled = true;
buttonProps.loading = true;
buttonProps.text = 'Unzipping';
buttonProps.icon = 'cloud-download';
} else {
buttonProps.disabled = false;
buttonProps.loading = false;
+41 -3
View File
@@ -1,4 +1,4 @@
import { Callout, Checkbox, FormGroup } from '@blueprintjs/core';
import { Callout, Checkbox, FormGroup, InputGroup } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -21,6 +21,7 @@ export class ExecutionSettings extends React.Component<ExecutionSettingsProps, {
this.handleDeleteDataChange = this.handleDeleteDataChange.bind(this);
this.handleElectronLoggingChange = this.handleElectronLoggingChange.bind(this);
this.handleExecutionFlagChange = this.handleExecutionFlagChange.bind(this);
}
/**
@@ -48,8 +49,25 @@ export class ExecutionSettings extends React.Component<ExecutionSettingsProps, {
this.props.appState.isEnablingElectronLogging = checked;
}
/**
* Handles a change in the execution flags run with the Electron executable
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public handleExecutionFlagChange(
event: React.FormEvent<HTMLInputElement>
) {
const { value } = event.currentTarget;
const flags = value.split('|');
this.props.appState.executionFlags = flags;
}
public render() {
const { isKeepingUserDataDirs, isEnablingElectronLogging } = this.props.appState;
const {
isKeepingUserDataDirs,
isEnablingElectronLogging,
executionFlags = []
} = this.props.appState;
const deleteUserDirLabel = `
Whenever Electron runs, it creates a user data directory for cookies, the cache,
@@ -63,7 +81,7 @@ export class ExecutionSettings extends React.Component<ExecutionSettingsProps, {
return (
<div>
<h2>Exection</h2>
<h2>Execution</h2>
<Callout>
These advanced settings control how Electron Fiddle executes your fiddles.
</Callout>
@@ -87,6 +105,26 @@ export class ExecutionSettings extends React.Component<ExecutionSettingsProps, {
/>
</FormGroup>
</Callout>
<br />
<Callout>
<FormGroup>
<p>
Electron allows starting the executable with <a
href='https://electronjs.org/docs/api/chrome-command-line-switches'
>
user-provided flags
</a>
, such as '--js-flags=--expose-gc'. Those can be added here as bar-separated (|)
flags to run when you start your Fiddles.
</p>
<br />
<InputGroup
placeholder='--js-flags=--expose-gc|--lang=es'
value={executionFlags.join('|')}
onChange={this.handleExecutionFlagChange}
/>
</FormGroup>
</Callout>
</div>
);
}
@@ -19,7 +19,7 @@ const ThemeSelect = Select.ofType<LoadedFiddleTheme>();
* version.
*
* @param {string} query
* @param {ElectronVersion} { version }
* @param {RunnableVersion} { version }
* @returns
*/
export const filterItem: ItemPredicate<LoadedFiddleTheme> = (query, { name }) => {
@@ -31,7 +31,7 @@ export const filterItem: ItemPredicate<LoadedFiddleTheme> = (query, { name }) =>
* Helper method: Returns the <Select /> <MenuItem /> for Electron
* versions.
*
* @param {ElectronVersion} item
* @param {RunnableVersion} item
* @param {IItemRendererProps} { handleClick, modifiers, query }
* @returns
*/
@@ -76,6 +76,7 @@ export class AppearanceSettings extends React.Component<
this.handleChange = this.handleChange.bind(this);
this.openThemeFolder = this.openThemeFolder.bind(this);
this.handleAddTheme = this.handleAddTheme.bind(this);
this.state = {
themes: []
@@ -111,8 +112,9 @@ export class AppearanceSettings extends React.Component<
* @memberof AppearanceSettings
*/
public async createNewThemeFromCurrent(): Promise<boolean> {
const { appState } = this.props;
const fs = await fancyImport<typeof fsType>('fs-extra');
const theme = await getTheme(this.props.appState.theme);
const theme = await getTheme(appState.theme);
try {
const namor = await fancyImport<any>('namor');
@@ -128,6 +130,8 @@ export class AppearanceSettings extends React.Component<
shell.showItemInFolder(themePath);
this.setState({themes: await getAvailableThemes()});
return true;
} catch (error) {
console.warn(`Themes: Failed to create new theme from current`, error);
@@ -190,13 +194,29 @@ export class AppearanceSettings extends React.Component<
</a>. The easiest way to get started is to clone one of the two existing
themes and to add your own colors.
</p>
<p>
Additionally, if you wish to import a Monaco Editor theme, pick your JSON file and Fiddle will attempt to import it.
</p>
<Button
onClick={this.createNewThemeFromCurrent}
text='Create theme from current selection'
icon='duplicate'
/>
<Button
icon='document-open'
onClick={this.handleAddTheme}
text='Add a Monaco Editor theme'
/>
</Callout>
</div>
);
}
/**
* Opens the "add monaco theme" dialog
*/
public handleAddTheme(): void {
this.props.appState.toggleAddMonacoThemeDialog();
}
}
@@ -0,0 +1,60 @@
import { Callout, Checkbox, FormGroup } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { AppState } from '../state';
export interface ConsoleSettingsProps {
appState: AppState;
}
/**
* Settings content to manage console-related preferences.
*
* @class ConsoleSettings
* @extends {React.Component<ConsoleSettingsProps, {}>}
*/
@observer
export class ConsoleSettings extends React.Component<ConsoleSettingsProps, {}> {
constructor(props: ConsoleSettingsProps) {
super(props);
this.handleClearOnRunChange = this.handleClearOnRunChange.bind(this);
}
/**
* Handles a change on whether or not the console should be cleared
* before fiddle is executed.
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public handleClearOnRunChange(
event: React.FormEvent<HTMLInputElement>
) {
const { checked } = event.currentTarget;
this.props.appState.isClearingConsoleOnRun = checked;
}
public render() {
const { isClearingConsoleOnRun } = this.props.appState;
const clearOnRunLabel = `
Enable this option to automatically clear the console whenever you run your
fiddle.`.trim();
return (
<div>
<h4>Console</h4>
<Callout>
<FormGroup label={clearOnRunLabel}>
<Checkbox
checked={isClearingConsoleOnRun}
label='Clear on run.'
onChange={this.handleClearOnRunChange}
/>
</FormGroup>
</Callout>
</div>
);
}
}
@@ -31,7 +31,7 @@ export class GitHubSettings extends React.Component<GitHubSettingsProps, {}> {
return (
<Callout>
<p>
Your fiddles can be published as public GitHub Gists -
Your fiddles can be published as GitHub Gists -
that way you can share your fiddles with the world!
</p>
<Button onClick={this.signIn} icon='log-in' text='Sign in'/>
@@ -4,6 +4,7 @@ import * as React from 'react';
import { AppState } from '../state';
import { AppearanceSettings } from './settings-general-appearance';
import { ConsoleSettings } from './settings-general-console';
import { GitHubSettings } from './settings-general-github';
export interface GeneralSettingsProps {
@@ -24,6 +25,8 @@ export class GeneralSettings extends React.Component<GeneralSettingsProps, {}> {
<h2>General Settings</h2>
<AppearanceSettings appState={this.props.appState} />
<Divider />
<ConsoleSettings appState={this.props.appState} />
<Divider />
<GitHubSettings appState={this.props.appState} />
</div>
);
+20
View File
@@ -44,6 +44,16 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
this.state = {
section: SettingsSections.General
};
this.closeSettingsPanel = this.closeSettingsPanel.bind(this);
}
public componentDidMount() {
window.addEventListener('keyup', this.closeSettingsPanel, true);
}
public componentWillUnmount() {
window.removeEventListener('keyup', this.closeSettingsPanel);
}
/**
@@ -138,4 +148,14 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
return 'cog';
}
/**
* Trigger closing of the settings panel upon Esc
*/
private closeSettingsPanel(event: KeyboardEvent) {
const { appState } = this.props;
if (event.code === 'Escape') {
appState.isSettingsShowing = false;
}
}
}
+1 -1
View File
@@ -60,7 +60,7 @@ export function ShowMeApp(_props: any): JSX.Element {
let result = '';
pathsToQuery.forEach((item) => {
result += `${item}: ${remote.app.getPath(item)}\n`;
result += `${item}: ${remote.app.getPath(item as any)}\n`;
});
setPaths(result);
+6 -6
View File
@@ -29,8 +29,8 @@ export function getWelcomeTour(): Set<TourScriptStep> {
<>
<p>
Electron Fiddle allows you to build little experiments and mini-apps with
Electron. Each Fiddle has three files: A main script, a renderer script,
and an HTML file.
Electron. Each Fiddle has at least three of these files: A main script, a
renderer script, a preload script, and an HTML file.
</p>
<p>
If you <code>require()</code> a module, Fiddle will install
@@ -115,7 +115,7 @@ export function getWelcomeTour(): Set<TourScriptStep> {
a Node.js application is started. The main script runs in the "main
process". To display a user interface, the main process creates renderer
processes  usually in the form of windows, which Electron calls
<code>BrowserWindow</code>.
&nbsp;<code>BrowserWindow</code>.
</p>
<p>
To get started, pretend that the main process is just like a Node.js
@@ -137,10 +137,10 @@ export function getWelcomeTour(): Set<TourScriptStep> {
content: (
<p>
In the default fiddle, this HTML file is loaded in the
<code>BrowserWindow</code>. Any HTML, CSS, or JavaScript that works
&nbsp;<code>BrowserWindow</code>. Any HTML, CSS, or JavaScript that works
in a browser will work here, too. In addition, Electron allows you
to execute Node.js code. Take a close look at the
<code>&lt;script /&gt;</code> tag and notice how we can call <code>
&nbsp;<code>&lt;script /&gt;</code> tag and notice how we can call <code>
require()</code> like we would in Node.js.
</p>
)
@@ -157,7 +157,7 @@ export function getWelcomeTour(): Set<TourScriptStep> {
</p>
<p>
By the way: If you want to use an <code>npm</code> module here, just
<code>require</code> it. Electron Fiddle will automatically detect that you
&nbsp;<code>require</code> it. Electron Fiddle will automatically detect that you
requested a module and install it as soon as you run your fiddle.
</p>
</>
+134
View File
@@ -0,0 +1,134 @@
import { Button, IButtonGroupProps, MenuItem } from '@blueprintjs/core';
import { ItemPredicate, ItemRenderer, Select } from '@blueprintjs/select';
import { observer } from 'mobx-react';
import * as React from 'react';
import { RunnableVersion, VersionSource, VersionState } from '../../interfaces';
import { highlightText } from '../../utils/highlight-text';
import { AppState } from '../state';
const ElectronVersionSelect = Select.ofType<RunnableVersion>();
/**
* Helper method: Returns the <Select /> label for an Electron
* version.
*
* @param {RunnableVersion} { source, state }
* @returns {string}
*/
export function getItemLabel({ source, state, name }: RunnableVersion): string {
let label = '';
if (source === VersionSource.local) {
label = name || 'Local';
} else {
if (state === VersionState.unknown) {
label = `Not downloaded`;
} else if (state === VersionState.ready) {
label = `Downloaded`;
} else if (state === VersionState.downloading) {
label = `Downloading`;
}
}
return label;
}
/**
* Helper method: Returns the <Select /> icon for an Electron
* version.
*
* @param {RunnableVersion} { state }
* @returns
*/
export function getItemIcon({ state }: RunnableVersion) {
return state === 'ready'
? 'saved'
: state === 'downloading' ? 'cloud-download' : 'cloud';
}
/**
* Helper method: Returns the <Select /> predicate for an Electron
* version.
*
* @param {string} query
* @param {RunnableVersion} { version }
* @returns
*/
export const filterItem: ItemPredicate<RunnableVersion> = (query, { version }) => {
return version.toLowerCase().includes(query.toLowerCase());
};
/**
* Helper method: Returns the <Select /> <MenuItem /> for Electron
* versions.
*
* @param {RunnableVersion} item
* @param {IItemRendererProps} { handleClick, modifiers, query }
* @returns
*/
export const renderItem: ItemRenderer<RunnableVersion> = (item, { handleClick, modifiers, query }) => {
if (!modifiers.matchesPredicate) {
return null;
}
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
text={highlightText(item.version, query)}
key={item.version}
onClick={handleClick}
label={getItemLabel(item)}
icon={getItemIcon(item)}
/>
);
};
export interface VersionSelectState {
value: string;
}
export interface VersionSelectProps {
appState: AppState;
disabled?: boolean;
currentVersion: RunnableVersion;
onVersionSelect: (version: RunnableVersion) => void;
buttonGroupProps?: IButtonGroupProps;
itemDisabled?: keyof RunnableVersion | ((item: RunnableVersion, index: number) => boolean);
}
/**
* A dropdown allowing the selection of Electron versions. The actual
* download is managed in the state.
*
* @class VersionSelect
* @extends {React.Component<VersionSelectProps, VersionSelectState>}
*/
@observer
export class VersionSelect extends React.Component<VersionSelectProps, VersionSelectState> {
public render() {
const { currentVersion, itemDisabled } = this.props;
const { version } = currentVersion;
return (
<ElectronVersionSelect
filterable={true}
items={this.props.appState.versionsToShow}
itemRenderer={renderItem}
itemPredicate={filterItem}
itemDisabled={itemDisabled}
onItemSelect={this.props.onVersionSelect}
noResults={<MenuItem disabled={true} text='No results.' />}
disabled={!!this.props.disabled}
>
<Button
className='version-chooser'
text={`Electron v${version}`}
icon={getItemIcon(currentVersion)}
disabled={!!this.props.disabled}
/>
</ElectronVersionSelect>
);
}
}
+8
View File
@@ -3,6 +3,7 @@ import * as path from 'path';
import { MosaicNode } from 'react-mosaic-component';
import { EditorId, MosaicId } from '../interfaces';
import { EditorBackup } from '../utils/editor-backup';
// Reminder: When testing, this file is mocked in tests/setup.js
@@ -18,3 +19,10 @@ export const DEFAULT_MOSAIC_ARRANGEMENT: MosaicNode<MosaicId> = {
second: EditorId.html,
}
};
export const DEFAULT_CLOSED_PANELS: Partial<Record<MosaicId, EditorBackup | true>> = {
docsDemo: true
};
export const ELECTRON_ORG = 'electron';
export const ELECTRON_REPO = 'electron';
+2 -2
View File
@@ -22,7 +22,7 @@ export async function getContent(
if (name === EditorId.main) {
// We currently only distinguish between loadFile
// and loadURL. Todo: Properly version the quick-start.
// and loadURL. TODO: Properly version the quick-start.
if (version && version.startsWith('1.')) {
return (await import('../content/main-1-x-x')).main;
}
@@ -42,7 +42,7 @@ export async function getContent(
export async function isContentUnchanged(name: EditorId): Promise<boolean> {
if (!window.ElectronFiddle || !window.ElectronFiddle.app) return false;
const values = await window.ElectronFiddle.app.getValues({ include: false });
const values = await window.ElectronFiddle.app.getEditorValues({ include: false });
// Handle main case, which needs to check both possible versions
if (name === EditorId.main) {
+79 -21
View File
@@ -2,30 +2,61 @@ import * as fsType from 'fs-extra';
import * as MonacoType from 'monaco-editor';
import * as path from 'path';
import { RunnableVersion, VersionSource } from '../interfaces';
import { callIn } from '../utils/call-in';
import { fancyImport } from '../utils/import';
import { normalizeVersion } from '../utils/normalize-version';
import { USER_DATA_PATH } from './constants';
const definitionPath = path.join(USER_DATA_PATH, 'electron-typedef');
/**
* Fetch TypeScript definitions for the current version of Electron (online)
*
* @param {string} version
* @returns {Promise<string>}
*/
export function fetchTypeDefinitions(version: string): Promise<string> {
export async function fetchTypeDefinitions(version: string): Promise<string> {
const url = `https://unpkg.com/electron@${version}/electron.d.ts`;
return window.fetch(url)
.then((response) => response.text())
.catch((error) => {
console.warn(`Fetch Types: Could not fetch definitions`, error);
return '';
});
let text: string;
try {
const response = await window.fetch(url);
text = await response.text();
} catch (error) {
console.warn(`Fetch Types: Could not fetch definitions`, error);
return '';
}
// for invalid packa
if (text.includes('Cannot find package')) {
console.warn(`Fetch Types: ${text}`);
return '';
} else {
return text;
}
}
/**
* Removes the type definition for a given version
*
* @param version
*/
export async function removeTypeDefsForVersion(version: string) {
const fs = await fancyImport<typeof fsType>('fs-extra');
const _version = normalizeVersion(version);
const typeDefsDir = path.dirname(getOfflineTypeDefinitionPath(_version));
if (fs.existsSync(typeDefsDir)) {
try {
await fs.remove(typeDefsDir);
} catch (error) {
throw error;
}
}
}
/**
* Get the path for offline TypeScript definitions
*
@@ -48,27 +79,25 @@ export async function getOfflineTypeDefinitions(version: string): Promise<boolea
}
/**
* Get TypeScript defintions for a version of Electron. If none can't be
* Get TypeScript definitions for a version of Electron. If none can be
* found, returns null.
*
* @param {string} version
* @returns {void}
*/
export async function getTypeDefinitions(version: string): Promise<string | null> {
export async function getDownloadedVersionTypeDefs(version: RunnableVersion): Promise<string | null> {
const fs = await fancyImport<typeof fsType>('fs-extra');
await fs.mkdirp(definitionPath);
await fs.mkdirp(definitionPath);
const offlinePath = getOfflineTypeDefinitionPath(version.version);
const offlinePath = getOfflineTypeDefinitionPath(version);
if (await getOfflineTypeDefinitions(version)) {
if (await getOfflineTypeDefinitions(version.version)) {
try {
return await fs.readFile(offlinePath, 'utf-8');
} catch (error) {
return null;
}
} else {
const typeDefs = await fetchTypeDefinitions(version);
const typeDefs = await fetchTypeDefinitions(version.version);
if (typeDefs && typeDefs.length > 0) {
try {
await fs.outputFile(offlinePath, typeDefs);
@@ -79,17 +108,27 @@ export async function getTypeDefinitions(version: string): Promise<string | null
return typeDefs;
}
}
return null;
}
}
export async function getLocalVersionTypeDefs(version: RunnableVersion) {
if (version.source === VersionSource.local && !!version.localPath) {
const fs = await fancyImport<typeof fsType>('fs-extra');
const typesPath = getLocalTypePathForVersion(version);
if (!!typesPath && fs.existsSync(typesPath)) {
return fs.readFile(typesPath, 'utf-8');
}
}
return null;
}
/**
* Tries to update the editor with type definitions.
*
* @param {string} version
*/
export async function updateEditorTypeDefinitions(version: string, i: number = 0): Promise<void> {
export async function updateEditorTypeDefinitions(version: RunnableVersion, i: number = 0): Promise<void> {
const defer = async (): Promise<void> => {
if (i > 10) {
console.warn(`Fetch Types: Failed, dependencies do not exist`);
@@ -106,17 +145,36 @@ export async function updateEditorTypeDefinitions(version: string, i: number = 0
const { app } = window.ElectronFiddle;
const monaco: typeof MonacoType = app.monaco!;
const typeDefDisposable: MonacoType.IDisposable = app.typeDefDisposable!;
const typeDefs = await getTypeDefinitions(version);
const getTypeDefs = (version.source === VersionSource.local) ?
getLocalVersionTypeDefs : getDownloadedVersionTypeDefs;
const typeDefs = await getTypeDefs(version);
if (typeDefDisposable) {
typeDefDisposable.dispose();
}
if (typeDefs) {
console.log(`Fetch Types: Updating Monaco types with electron.d.ts@${version}`);
console.log(`Fetch Types: Updating Monaco types with electron.d.ts@${version.version}`);
const disposable = monaco.languages.typescript.javascriptDefaults.addExtraLib(typeDefs);
window.ElectronFiddle.app.typeDefDisposable = disposable;
} else {
console.log(`Fetch Types: No type definitions for ${version} 😢`);
console.log(`Fetch Types: No type definitions for ${version.version} 😢`);
}
}
export function getLocalTypePathForVersion(version: RunnableVersion) {
if (version.localPath) {
return path.join(
version.localPath,
'gen',
'electron',
'tsc',
'typings',
'electron.d.ts'
);
} else {
return null;
}
}
+45 -33
View File
@@ -1,11 +1,10 @@
import * as fsType from 'fs-extra';
import * as path from 'path';
import { EditorValues, Files, FileTransform, SetFiddleOptions } from '../interfaces';
import { EditorValues, Files, FileTransform } from '../interfaces';
import { IpcEvents } from '../ipc-events';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, RENDERER_JS_NAME } from '../shared-constants';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME, STYLES_CSS_NAME } from '../shared-constants';
import { DEFAULT_OPTIONS, PackageJsonOptions } from '../utils/get-package';
import { getTitle } from '../utils/get-title';
import { fancyImport } from '../utils/import';
import { ipcRendererManager } from './ipc';
import { AppState } from './state';
@@ -18,6 +17,11 @@ export class FileManager {
this.openFiddle = this.openFiddle.bind(this);
this.saveFiddle = this.saveFiddle.bind(this);
ipcRendererManager.removeAllListeners(IpcEvents.FS_OPEN_FIDDLE);
ipcRendererManager.removeAllListeners(IpcEvents.FS_OPEN_TEMPLATE);
ipcRendererManager.removeAllListeners(IpcEvents.FS_SAVE_FIDDLE);
ipcRendererManager.removeAllListeners(IpcEvents.FS_SAVE_FIDDLE_FORGE);
ipcRendererManager.on(IpcEvents.FS_OPEN_FIDDLE, (_event, filePath) => {
this.openFiddle(filePath);
});
@@ -38,13 +42,12 @@ export class FileManager {
/**
* Opens a template.
*
* @param {string} name
* @param {string} templateName
* @memberof FileManager
*/
public async openTemplate(name: string) {
const values = await getTemplateValues(name);
this.setFiddle({ values, templateName: name });
public async openTemplate(templateName: string) {
const editorValues = await getTemplateValues(templateName);
await window.ElectronFiddle.app.replaceFiddle(editorValues, {templateName});
}
/**
@@ -60,34 +63,16 @@ export class FileManager {
// We'll do our best and will likely have to
// rewrite this once we support multiple files
const values: EditorValues = {
const editorValues: EditorValues = {
html: await this.readFile(path.join(filePath, INDEX_HTML_NAME)),
main: await this.readFile(path.join(filePath, MAIN_JS_NAME)),
renderer: await this.readFile(path.join(filePath, RENDERER_JS_NAME)),
preload: await this.readFile(path.join(filePath, PRELOAD_JS_NAME)),
css: await this.readFile(path.join(filePath, STYLES_CSS_NAME))
};
this.setFiddle({ values, filePath });
}
/**
* Got values? This method will load them.
*
* @param {SetFiddleOptions} options
* @memberof FileManager
*/
public async setFiddle({ values, filePath, templateName }: SetFiddleOptions) {
this.appState.gistId = '';
this.appState.isMyGist = false;
this.appState.localPath = filePath;
this.appState.templateName = templateName;
this.appState.setWarningDialogTexts({
label: `Opening this fiddle will replace your unsaved changes. Do you want to proceed?`,
ok: 'Yes'
});
await window.ElectronFiddle.app.setValues(values);
document.title = getTitle(this.appState);
window.ElectronFiddle.app.replaceFiddle(editorValues, {filePath});
}
/**
@@ -109,7 +94,15 @@ export class FileManager {
const files = await this.getFiles(undefined, ...transforms);
for (const [fileName, content] of files) {
await this.saveFile(path.join(pathToSave, fileName), content);
const savePath = path.join(pathToSave, fileName);
// If the file has content, save it to disk. If there's no
// content in the file, remove a file that possibly exists.
if (content) {
await this.saveFile(savePath, content);
} else {
await this.removeFile(savePath);
}
}
if (pathToSave !== localPath) {
@@ -117,7 +110,6 @@ export class FileManager {
}
this.appState.isUnsaved = false;
window.ElectronFiddle.app.setupUnsavedOnChangeListener();
}
}
@@ -132,12 +124,14 @@ export class FileManager {
*/
public async getFiles(options?: PackageJsonOptions, ...transforms: Array<FileTransform>): Promise<Files> {
const pOptions = typeof options === 'object' ? options : DEFAULT_OPTIONS;
const values = await window.ElectronFiddle.app.getValues(pOptions);
const values = await window.ElectronFiddle.app.getEditorValues(pOptions);
let output: Files = new Map();
output.set(RENDERER_JS_NAME, values.renderer);
output.set(MAIN_JS_NAME, values.main);
output.set(INDEX_HTML_NAME, values.html);
output.set(PRELOAD_JS_NAME, values.preload);
output.set(STYLES_CSS_NAME, values.css);
output.set(PACKAGE_NAME, values.package!);
for (const transform of transforms) {
@@ -240,4 +234,22 @@ export class FileManager {
ipcRendererManager.send(IpcEvents.FS_SAVE_FIDDLE_ERROR, [filePath]);
}
}
/**
* Safely attempts to remove a file, doesn't crash the app if
* it fails.
*
* @param {string} filePath
* @returns {string}
* @memberof FileManager
*/
private async removeFile(filePath: string): Promise<void> {
try {
const fs = await fancyImport<typeof fsType>('fs-extra');
return await fs.remove(filePath);
} catch (error) {
console.log(`FileManager: Could not remove ${filePath}`, error);
ipcRendererManager.send(IpcEvents.FS_SAVE_FIDDLE_ERROR, [filePath]);
}
}
}
+1
View File
@@ -15,6 +15,7 @@ export class IpcRendererManager extends EventEmitter {
super();
ipcRendererEvents.forEach((name) => {
ipcRenderer.removeAllListeners(name);
ipcRenderer.on(name, (...args: Array<any>) => this.emit(name, ...args));
});
}
+10 -5
View File
@@ -1,6 +1,8 @@
import { EditorValues } from '../interfaces';
import { exec } from '../utils/exec';
const { builtinModules } = require('module');
export interface NpmOperationOptions {
dir: string;
}
@@ -11,12 +13,12 @@ export let isInstalled: boolean | null = null;
/* perhaps we can expose this to the settings module?*/
const ignoredModules: Array<string> = [
'electron',
// tslint:disable-next-line:no-submodule-imports
...require('builtin-modules/static')
'original-fs',
...builtinModules
];
/* regular expression to both match and extract module names */
const requiregx = /require\(['"](.*?)['"]\)/gm;
const requiregx = /^.*require\(['"](.*?)['"]\)/gm;
/*
@@ -91,8 +93,11 @@ export function findModules(input: string): Array<string> {
/* grab all global require matches in the text */
// tslint:disable-next-line:no-conditional-assignment
while (match = (requiregx.exec(input) || null)) {
const mod = match[1];
modules.push(mod);
// ensure commented-out requires aren't downloaded
if (!match[0].startsWith('//')) {
const mod = match[1];
modules.push(mod);
}
}
/* map and reduce */
+273
View File
@@ -0,0 +1,273 @@
import { Octokit } from '@octokit/rest';
import { when } from 'mobx';
import { EditorId, EditorValues, GenericDialogType } from '../interfaces';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME, STYLES_CSS_NAME } from '../shared-constants';
import { getOctokit } from '../utils/octokit';
import { sortedElectronMap } from '../utils/sorted-electron-map';
import { ELECTRON_ORG, ELECTRON_REPO } from './constants';
import { getContent } from './content';
import { AppState } from './state';
import { ElectronReleaseChannel, getReleaseChannel } from './versions';
export class RemoteLoader {
constructor(private readonly appState: AppState) {
this.loadFiddleFromElectronExample.bind(this);
this.loadFiddleFromGist.bind(this);
this.verifyRemoteLoad.bind(this);
this.verifyReleaseChannelEnabled.bind(this);
this.fetchExampleAndLoad.bind(this);
this.fetchGistAndLoad.bind(this);
this.setElectronVersionWithRef.bind(this);
this.getPackageVersionFromRef.bind(this);
this.handleLoadingSuccess.bind(this);
this.handleLoadingFailed.bind(this);
}
public async loadFiddleFromElectronExample(_: any, exampleInfo: { path: string; ref: string }) {
console.log(`Loading fiddle from Electron example`, _, exampleInfo);
const { path, ref } = exampleInfo;
const prettyName = path.replace('docs/fiddles/', '');
const ok = await this.verifyRemoteLoad(`'${prettyName}' example from the Electron docs for version ${ref}`);
if (!ok) return;
this.fetchExampleAndLoad(ref, path);
}
public async loadFiddleFromGist(_: any, gistInfo: { id: string }) {
const { id } = gistInfo;
const ok = await this.verifyRemoteLoad(`gist`);
if (!ok) return;
this.fetchGistAndLoad(id);
}
public async fetchExampleAndLoad(ref: string, path: string): Promise<boolean> {
try {
const octo = await getOctokit(this.appState);
const folder = await octo.repos.getContents({
owner: ELECTRON_REPO,
repo: ELECTRON_ORG,
ref,
path,
});
const ok = await this.setElectronVersionWithRef(ref);
if (!ok) return false;
const values = {
html: await getContent(EditorId.html, this.appState.version),
renderer: await getContent(EditorId.renderer, this.appState.version),
main: await getContent(EditorId.main, this.appState.version),
preload: await getContent(EditorId.preload, this.appState.version),
css: await getContent(EditorId.css, this.appState.version)
};
const loaders: Array<Promise<void>> = [];
if (!Array.isArray(folder.data)) {
throw new Error('The example Fiddle tried to launch is not a valid Electron example');
}
for (const child of folder.data) {
if (!child.download_url) {
console.warn(`Could not find download_url for ${child.name}`);
continue;
}
switch (child.name) {
case MAIN_JS_NAME:
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.main = t; })
);
break;
case INDEX_HTML_NAME:
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.html = t; })
);
break;
case RENDERER_JS_NAME:
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.renderer = t; })
);
break;
case PRELOAD_JS_NAME:
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.preload = t; })
);
case STYLES_CSS_NAME:
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.css = t; })
);
break;
default:
break;
}
}
await Promise.all(loaders);
return this.handleLoadingSuccess(values, '');
} catch (error) {
return this.handleLoadingFailed(error);
}
}
/**
* Get data from a gist. If it doesn't exist, return an empty string.
*
* @param {Octokit.Response<Octokit.GistsGetResponse>} gist
* @param {string} name
* @returns {string}
* @memberof RemoteLoader
*/
public getContentOrEmpty(gist: Octokit.Response<Octokit.GistsGetResponse>, name: string): string {
try {
return gist.data.files[name].content;
} catch (error) {
return '';
}
}
/**
* Load a fiddle
*
* @returns {Promise<boolean>}
* @memberof RemoteLoader
*/
public async fetchGistAndLoad(gistId: string): Promise<boolean> {
try {
const octo = await getOctokit(this.appState);
const gist = await octo.gists.get({ gist_id: gistId });
return this.handleLoadingSuccess({
html: this.getContentOrEmpty(gist, INDEX_HTML_NAME),
main: this.getContentOrEmpty(gist, MAIN_JS_NAME),
renderer: this.getContentOrEmpty(gist, RENDERER_JS_NAME),
preload: this.getContentOrEmpty(gist, PRELOAD_JS_NAME),
css: this.getContentOrEmpty(gist, STYLES_CSS_NAME)
}, gistId);
} catch (error) {
return this.handleLoadingFailed(error);
}
}
public async setElectronVersionWithRef(ref: string): Promise<boolean> {
const version = await this.getPackageVersionFromRef(ref);
const supportedVersions = sortedElectronMap(this.appState.versions, (k) => k);
if (!supportedVersions.includes(version)) {
this.handleLoadingFailed(new Error('Version of Electron in example not supported'));
return false;
}
// check if version is part of release channel
const versionReleaseChannel: ElectronReleaseChannel = getReleaseChannel(version);
if (!this.appState.channelsToShow.includes(versionReleaseChannel)) {
const ok = await this.verifyReleaseChannelEnabled(versionReleaseChannel);
if (!ok) return false;
this.appState.channelsToShow.push(versionReleaseChannel);
}
this.appState.setVersion(version);
return true;
}
public async getPackageVersionFromRef(ref: string): Promise<string> {
const octo = await getOctokit(this.appState);
const { data: packageJsonData } = await octo.repos.getContents({
owner: ELECTRON_ORG,
repo: ELECTRON_REPO,
ref,
path: 'package.json'
});
if (!Array.isArray(packageJsonData) && !!packageJsonData.content) {
const packageJsonString = Buffer.from(packageJsonData.content, 'base64').toString('utf8');
const { version } = JSON.parse(packageJsonString);
return version;
} else {
console.error(`getPackageVersionFromRef: Received unexpected response from GitHub, could not parse version`, {
packageJsonData
});
return '0.0.0';
}
}
/**
* Verifies from the user that we should be loading this fiddle
*
* @param what What are we loading from (gist, example, etc.)
*/
public async verifyRemoteLoad(what: string): Promise<boolean> {
this.appState.setGenericDialogOptions({
type: GenericDialogType.confirm,
label: `Are you sure you want to load this ${what}? Only load and run it if you trust the source.`
});
this.appState.isGenericDialogShowing = true;
await when(() => !this.appState.isGenericDialogShowing);
return !!this.appState.genericDialogLastResult;
}
public async verifyReleaseChannelEnabled(channel: string): Promise<boolean> {
this.appState.setGenericDialogOptions({
type: GenericDialogType.warning,
label: `You're loading an example with a version of Electron with an unincluded release
channel (${channel}). Do you want to enable the release channel to load the
version of Electron from the example?`
});
this.appState.isGenericDialogShowing = true;
await when(() => !this.appState.isGenericDialogShowing);
return !!this.appState.genericDialogLastResult;
}
/**
* Loading a fiddle from GitHub succeeded, let's move on.
*
* @param {Partial<EditorValues>} values
* @param {string} gistId
* @returns {boolean}
*/
private async handleLoadingSuccess(values: Partial<EditorValues>, gistId: string): Promise<boolean> {
await window.ElectronFiddle.app.replaceFiddle(values, { gistId });
return true;
}
/**
* Loading a fiddle from GitHub failed - this method handles this case
* gracefully.
*
* @param {Error} error
* @returns {boolean}
*/
private handleLoadingFailed(error: Error): false {
if (navigator.onLine) {
this.appState.setGenericDialogOptions({
type: GenericDialogType.warning,
label: `Loading the fiddle failed: ${error}`,
cancel: undefined
});
} else {
this.appState.setGenericDialogOptions({
type: GenericDialogType.warning,
label: `Loading the fiddle failed. Your computer seems to be offline. Error: ${error}`,
cancel: undefined
});
}
this.appState.toggleGenericDialog();
console.warn(`Loading Fiddle failed`, error);
return false;
}
}
+21 -12
View File
@@ -6,6 +6,7 @@ import { IpcEvents } from '../ipc-events';
import { getAppDataDir } from '../utils/app-data-dir';
import { PackageJsonOptions } from '../utils/get-package';
import { maybePlural } from '../utils/plural-maybe';
import { getElectronBinaryPath, getIsDownloaded } from './binary';
import { ipcRendererManager } from './ipc';
import { findModulesInEditors, getIsNpmInstalled, installModules, npmRun } from './npm';
import { AppState } from './state';
@@ -22,6 +23,11 @@ export class Runner {
this.run = this.run.bind(this);
this.stop = this.stop.bind(this);
ipcRendererManager.removeAllListeners(IpcEvents.FIDDLE_RUN);
ipcRendererManager.removeAllListeners(IpcEvents.FIDDLE_PACKAGE);
ipcRendererManager.removeAllListeners(IpcEvents.FIDDLE_MAKE);
ipcRendererManager.on(IpcEvents.FIDDLE_RUN, this.run);
ipcRendererManager.on(IpcEvents.FIDDLE_PACKAGE, () => {
this.performForgeOperation(ForgeCommands.PACKAGE);
@@ -37,14 +43,17 @@ export class Runner {
* @returns {Promise<boolean>}
*/
public async run(): Promise<boolean> {
const { fileManager, getValues } = window.ElectronFiddle.app;
const { fileManager, getEditorValues } = window.ElectronFiddle.app;
const options = { includeDependencies: false, includeElectron: false };
const { binaryManager, currentElectronVersion } = this.appState;
const { currentElectronVersion } = this.appState;
const { version, localPath } = currentElectronVersion;
if (this.appState.isClearingConsoleOnRun) {
this.appState.clearConsole();
}
this.appState.isConsoleShowing = true;
const values = await getValues(options);
const values = await getEditorValues(options);
const dir = await this.saveToTemp(options);
if (!dir) return false;
@@ -57,7 +66,7 @@ export class Runner {
return false;
}
const isReady = await binaryManager.getIsDownloaded(version, localPath);
const isReady = await getIsDownloaded(version, localPath);
if (!isReady) {
console.warn(`Runner: Binary ${version} not ready`);
@@ -176,9 +185,9 @@ export class Runner {
* @memberof Runner
*/
public async execute(dir: string): Promise<void> {
const { currentElectronVersion, pushOutput, binaryManager } = this.appState;
const { currentElectronVersion, pushOutput } = this.appState;
const { version, localPath } = currentElectronVersion;
const binaryPath = binaryManager.getElectronBinaryPath(version, localPath);
const binaryPath = getElectronBinaryPath(version, localPath);
console.log(`Runner: Binary ${binaryPath} ready, launching`);
const env = { ...process.env };
@@ -190,15 +199,15 @@ export class Runner {
delete env.ELECTRON_ENABLE_STACK_DUMPING;
}
this.child = spawn(binaryPath, [ dir, '--inspect' ], {
cwd: dir,
env,
});
// Add user-specified cli flags if any have been set.
const options = [ dir, '--inspect' ].concat(this.appState.executionFlags);
this.child = spawn(binaryPath, options, { cwd: dir, env });
this.appState.isRunning = true;
pushOutput(`Electron v${version} started.`);
this.child.stdout.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
this.child.stderr.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
this.child.stdout!.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
this.child.stderr!.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
this.child.on('close', async (code) => {
const withCode = typeof code === 'number'
? ` with code ${code.toString()}.`
+189 -80
View File
@@ -1,3 +1,4 @@
import * as fsType from 'fs-extra';
import { action, autorun, computed, observable, when } from 'mobx';
import { MosaicNode } from 'react-mosaic-component';
@@ -5,33 +6,39 @@ import {
ALL_MOSAICS,
DocsDemoPage,
EditorId,
ElectronVersion,
ElectronVersionSource,
ElectronVersionState,
GenericDialogOptions,
GenericDialogType,
MosaicId,
NpmVersion,
OutputEntry,
OutputOptions,
WarningDialogTexts
RunnableVersion,
Version,
VersionSource,
VersionState
} from '../interfaces';
import { IpcEvents } from '../ipc-events';
import { arrayToStringMap } from '../utils/array-to-stringmap';
import { EditorBackup, getEditorBackup } from '../utils/editor-backup';
import { createMosaicArrangement, getVisibleMosaics } from '../utils/editors-mosaic-arrangement';
import { getName } from '../utils/get-title';
import { fancyImport } from '../utils/import';
import { normalizeVersion } from '../utils/normalize-version';
import { isEditorBackup, isEditorId, isPanelId } from '../utils/type-checks';
import { BinaryManager } from './binary';
import { DEFAULT_MOSAIC_ARRANGEMENT } from './constants';
import { getDownloadedVersions, getDownloadingVersions, removeBinary, setupBinary } from './binary';
import { Bisector } from './bisect';
import { DEFAULT_CLOSED_PANELS, DEFAULT_MOSAIC_ARRANGEMENT } from './constants';
import { getContent, isContentUnchanged } from './content';
import { updateEditorTypeDefinitions } from './fetch-types';
import { getLocalTypePathForVersion, updateEditorTypeDefinitions } from './fetch-types';
import { ipcRendererManager } from './ipc';
import { activateTheme } from './themes';
import { sortedElectronMap } from '../utils/sorted-electron-map';
import {
addLocalVersion,
ElectronReleaseChannel,
getDefaultVersion,
getElectronVersions,
getReleaseChannel,
getUpdatedElectronVersions,
saveLocalVersions
} from './versions';
@@ -68,44 +75,50 @@ export class AppState {
@observable public gitHubLogin: string | null = localStorage.getItem('gitHubLogin');
@observable public gitHubToken: string | null = localStorage.getItem('gitHubToken') || null;
@observable public gitHubPublishAsPublic: boolean = !!this.retrieve('gitHubPublishAsPublic');
@observable public versionsToShow: Array<ElectronReleaseChannel> =
this.retrieve('versionsToShow') as Array<ElectronReleaseChannel>
|| [ ElectronReleaseChannel.stable, ElectronReleaseChannel.beta ];
@observable public statesToShow: Array<ElectronVersionState> =
this.retrieve('statesToShow') as Array<ElectronVersionState>
|| [ ElectronVersionState.downloading, ElectronVersionState.ready, ElectronVersionState.unknown ];
@observable public channelsToShow: Array<ElectronReleaseChannel> =
this.retrieve('channelsToShow') as Array<ElectronReleaseChannel>
|| [ElectronReleaseChannel.stable, ElectronReleaseChannel.beta];
@observable public statesToShow: Array<VersionState> =
this.retrieve('statesToShow') as Array<VersionState>
|| [VersionState.downloading, VersionState.ready, VersionState.unknown];
@observable public isKeepingUserDataDirs: boolean = !!this.retrieve('isKeepingUserDataDirs');
@observable public isEnablingElectronLogging: boolean = !!this.retrieve('isEnablingElectronLogging');
@observable public binaryManager: BinaryManager = new BinaryManager();
@observable public isClearingConsoleOnRun: boolean = !!this.retrieve('isClearingConsoleOnRun');
@observable public executionFlags: Array<string> =
this.retrieve('executionFlags') as Array<string> === null ?
[] : this.retrieve('executionFlags') as Array<string>;
// -- Various session-only state ------------------
@observable public gistId: string = '';
@observable public isMyGist: boolean = false;
@observable public versions: Record<string, ElectronVersion> = arrayToStringMap(knownVersions);
@observable public versions: Record<string, RunnableVersion> = arrayToStringMap(knownVersions);
@observable public output: Array<OutputEntry> = [];
@observable public localPath: string | undefined;
@observable public isUpdatingElectronVersions = false;
@observable public warningDialogTexts = { label: '', ok: 'Okay', cancel: 'Cancel' };
@observable public warningDialogLastResult: boolean | null = null;
@observable public isRunning = false;
@observable public genericDialogOptions = { type: GenericDialogType.warning, label: '', ok: 'Okay', cancel: 'Cancel' };
@observable public genericDialogLastResult: boolean | null = null;
@observable public mosaicArrangement: MosaicNode<MosaicId> | null = DEFAULT_MOSAIC_ARRANGEMENT;
@observable public templateName: string | undefined;
@observable public currentDocsDemoPage: DocsDemoPage = DocsDemoPage.DEFAULT;
@observable public localTypeWatcher: fsType.FSWatcher | undefined;
@observable public Bisector: Bisector | undefined;
@observable public isPublishing: boolean = false;
@observable public isRunning: boolean = false;
@observable public isUnsaved: boolean;
@observable public isUpdatingElectronVersions: boolean = false;
// -- Various "isShowing" settings ------------------
@observable public isBisectCommandShowing: boolean;
@observable public isConsoleShowing: boolean = false;
@observable public isTokenDialogShowing: boolean = false;
@observable public isWarningDialogShowing: boolean = false;
@observable public isGenericDialogShowing: boolean = false;
@observable public isSettingsShowing: boolean = false;
@observable public isUnsaved: boolean = false;
@observable public isBisectDialogShowing: boolean = false;
@observable public isAddVersionDialogShowing: boolean = false;
@observable public isThemeDialogShowing: boolean = false;
@observable public isTourShowing: boolean = !localStorage.getItem('hasShownTour');
// -- Editor Values stored when we close the editor ------------------
@observable public closedPanels: Partial<Record<MosaicId, EditorBackup | true>> = {
docsDemo: true // Closed by default
};
@observable public closedPanels: Partial<Record<MosaicId, EditorBackup | true>> = DEFAULT_CLOSED_PANELS;
private outputBuffer: string = '';
private name: string;
@@ -119,16 +132,27 @@ export class AppState {
this.setVersion = this.setVersion.bind(this);
this.showTour = this.showTour.bind(this);
this.signOutGitHub = this.signOutGitHub.bind(this);
this.toggleBisectCommands = this.toggleBisectCommands.bind(this);
this.toggleAuthDialog = this.toggleAuthDialog.bind(this);
this.toggleConsole = this.toggleConsole.bind(this);
this.clearConsole = this.clearConsole.bind(this);
this.toggleSettings = this.toggleSettings.bind(this);
this.toggleBisectDialog = this.toggleBisectDialog.bind(this);
this.updateElectronVersions = this.updateElectronVersions.bind(this);
ipcRendererManager.removeAllListeners(IpcEvents.OPEN_SETTINGS);
ipcRendererManager.removeAllListeners(IpcEvents.SHOW_WELCOME_TOUR);
ipcRendererManager.removeAllListeners(IpcEvents.CLEAR_CONSOLE);
ipcRendererManager.removeAllListeners(IpcEvents.BISECT_COMMANDS_TOGGLE);
ipcRendererManager.on(IpcEvents.OPEN_SETTINGS, this.toggleSettings);
ipcRendererManager.on(IpcEvents.SHOW_WELCOME_TOUR, this.showTour);
ipcRendererManager.on(IpcEvents.CLEAR_CONSOLE, this.clearConsole);
ipcRendererManager.on(IpcEvents.BISECT_COMMANDS_TOGGLE, this.toggleBisectCommands);
// Setup auto-runs
autorun(() => this.save('theme', this.theme));
autorun(() => this.save('isClearingConsoleOnRun', this.isClearingConsoleOnRun));
autorun(() => this.save('gitHubAvatarUrl', this.gitHubAvatarUrl));
autorun(() => this.save('gitHubLogin', this.gitHubLogin));
autorun(() => this.save('gitHubName', this.gitHubName));
@@ -136,24 +160,29 @@ export class AppState {
autorun(() => this.save('gitHubPublishAsPublic', this.gitHubPublishAsPublic));
autorun(() => this.save('isKeepingUserDataDirs', this.isKeepingUserDataDirs));
autorun(() => this.save('isEnablingElectronLogging', this.isEnablingElectronLogging));
autorun(() => this.save('executionFlags', this.executionFlags));
autorun(() => this.save('version', this.version));
autorun(() => this.save('versionsToShow', this.versionsToShow));
autorun(() => this.save('channelsToShow', this.channelsToShow));
autorun(() => this.save('statesToShow', this.statesToShow));
autorun(() => {
if (this.isUnsaved) {
if (typeof this.isUnsaved === 'undefined') return;
if (!!this.isUnsaved) {
window.onbeforeunload = () => {
this.setWarningDialogTexts({
ipcRendererManager.send(IpcEvents.SHOW_INACTIVE);
this.setGenericDialogOptions({
type: GenericDialogType.warning,
label: `The current Fiddle is unsaved. Do you want to exit anyway?`,
ok: 'Quit'
});
this.isWarningDialogShowing = true;
this.isGenericDialogShowing = true;
// We'll wait until the warning dialog was closed
when(() => !this.isWarningDialogShowing).then(() => {
when(() => !this.isGenericDialogShowing).then(() => {
// The user confirmed, let's close for real.
if (this.warningDialogLastResult) {
if (this.genericDialogLastResult) {
window.onbeforeunload = null;
// Should we just close or quit?
@@ -172,6 +201,15 @@ export class AppState {
};
} else {
window.onbeforeunload = null;
// set up editor listeners to verify if unsaved
Object.keys(window.ElectronFiddle.editors).forEach((key) => {
const editor = window.ElectronFiddle.editors[key];
const disposable = editor.onDidChangeModelContent(() => {
this.isUnsaved = true;
disposable.dispose();
});
});
}
});
@@ -183,10 +221,10 @@ export class AppState {
}
/**
* Returns the current ElectronVersion or the first
* Returns the current RunnableVersion or the first
* one that can be found.
*/
@computed get currentElectronVersion(): ElectronVersion {
@computed get currentElectronVersion(): RunnableVersion {
if (this.versions[this.version]) {
return this.versions[this.version];
} else {
@@ -194,6 +232,31 @@ export class AppState {
}
}
/**
* Returns an array of Electron versions to show given the
* current settings for states and channels to display
*/
@computed get versionsToShow(): Array<RunnableVersion> {
return sortedElectronMap<RunnableVersion>(this.versions, (_key, item) => item)
.filter((item) => {
if (!item) {
return false;
}
// Check if we want to show the version
if (!this.channelsToShow.includes(getReleaseChannel(item))) {
return false;
}
// Check if we want to show the state
if (!this.statesToShow.includes(item.state)) {
return false;
}
return true;
});
}
/**
* Update the Electron versions: First, fetch them from GitHub,
* then update their respective downloaded state.
@@ -226,22 +289,41 @@ export class AppState {
this.isConsoleShowing = !this.isConsoleShowing;
}
@action public clearConsole() {
this.output = [];
}
@action public toggleBisectCommands() {
// guard against hiding the commands when executing a bisect
if (!this.Bisector && !this.isBisectDialogShowing) {
this.isBisectCommandShowing = !this.isBisectCommandShowing;
}
}
@action public toggleAddVersionDialog() {
this.isAddVersionDialogShowing = !this.isAddVersionDialogShowing;
}
@action public toggleAddMonacoThemeDialog() {
this.isThemeDialogShowing = !this.isThemeDialogShowing;
}
@action public toggleAuthDialog() {
this.isTokenDialogShowing = !this.isTokenDialogShowing;
}
@action public toogleWarningDialog() {
this.isWarningDialogShowing = !this.isWarningDialogShowing;
@action public toggleGenericDialog() {
this.isGenericDialogShowing = !this.isGenericDialogShowing;
if (this.isWarningDialogShowing) {
this.warningDialogLastResult = null;
if (this.isGenericDialogShowing) {
this.genericDialogLastResult = null;
}
}
@action public toggleBisectDialog() {
this.isBisectDialogShowing = !this.isBisectDialogShowing;
}
@action public toggleSettings() {
// We usually don't lose editor focus,
// so you can still type. Let's force-blur.
@@ -267,27 +349,28 @@ export class AppState {
window.ElectronFiddle.app.setupTheme();
}
@action public setWarningDialogTexts(input: WarningDialogTexts) {
this.warningDialogTexts = {
@action public setGenericDialogOptions(opts: GenericDialogOptions) {
this.genericDialogOptions = {
type: GenericDialogType.warning,
ok: 'Okay',
cancel: 'Cancel',
...input
...opts
};
}
@action public addLocalVersion(input: NpmVersion) {
@action public addLocalVersion(input: Version) {
addLocalVersion(input);
this.versions = arrayToStringMap(getElectronVersions());
this.updateDownloadedVersionState();
}
/**
* Remove a version of Electron
*
* @param {string} input
* @returns {Promise<void>}
*/
/**
* Remove a version of Electron
*
* @param {string} input
* @returns {Promise<void>}
*/
@action public async removeVersion(input: string) {
const version = normalizeVersion(input);
const release = this.versions[version];
@@ -304,7 +387,7 @@ export class AppState {
const updatedVersions = { ...this.versions };
// Actually remove
if (release && release.source === ElectronVersionSource.local) {
if (release && release.source === VersionSource.local) {
delete updatedVersions[version];
const versionsAsArray = Object
@@ -313,49 +396,48 @@ export class AppState {
saveLocalVersions(versionsAsArray);
} else {
await this.binaryManager.remove(version);
updatedVersions[version].state = ElectronVersionState.unknown;
await removeBinary(version);
updatedVersions[version].state = VersionState.unknown;
}
this.versions = updatedVersions;
this.updateDownloadedVersionState();
}
/**
* Download a version of Electron.
*
* @param {string} input
* @returns {Promise<void>}
*/
/**
* Download a version of Electron.
*
* @param {string} input
* @returns {Promise<void>}
*/
@action public async downloadVersion(input: string) {
const version = normalizeVersion(input);
console.log(`State: Downloading Electron ${version}`);
const release = this.versions[version] || { state: '', source: '' };
const isLocal = release.source === ElectronVersionSource.local;
const isLocal = release.source === VersionSource.local;
const isReady = release.state === 'ready';
// Fetch new binaries, maybe?
if (!isLocal && !isReady) {
console.log(`State: Instructing BinaryManager to fetch v${version}`);
console.log(`State: Fetching v${version}`);
const updatedVersions = { ...this.versions };
updatedVersions[version] = updatedVersions[version] || {};
updatedVersions[version].state = ElectronVersionState.downloading;
this.versions = updatedVersions;
await this.binaryManager.setup(version);
await setupBinary(this, version);
this.updateDownloadedVersionState();
} else {
console.log(`State: Version ${version} already downloaded, doing nothing.`);
}
}
/**
* Select a version of Electron (and download it if necessary).
*
* @param {string} input
* @returns {Promise<void>}
*/
/**
* Select a version of Electron (and download it if necessary).
*
* @param {string} input
* @returns {Promise<void>}
*/
@action public async setVersion(input: string) {
const version = normalizeVersion(input);
@@ -373,30 +455,56 @@ export class AppState {
// Should we update the editor?
if (await isContentUnchanged(EditorId.main)) {
const main = await getContent(EditorId.main, version);
window.ElectronFiddle.app.setValues({ main }, false);
window.ElectronFiddle.app.setEditorValues({ main });
}
// Update TypeScript definitions
updateEditorTypeDefinitions(version);
const versionObject = this.versions[version];
if (versionObject.source === VersionSource.local) {
const fs = await fancyImport<typeof fsType>('fs-extra');
const typePath = getLocalTypePathForVersion(versionObject);
console.info(`TypeDefs: Watching file for local version ${version} at path ${typePath}`);
this.localTypeWatcher = fs.watch(typePath!, async () => {
console.info(`TypeDefs: Noticed file change at ${typePath}. Updating editor typedefs.`);
await updateEditorTypeDefinitions(versionObject);
});
} else {
if (!!this.localTypeWatcher) {
console.info(`TypeDefs: Switched to downloaded version ${version}. Unwatching local typedefs.`);
this.localTypeWatcher.close();
this.localTypeWatcher = undefined;
}
}
await updateEditorTypeDefinitions(versionObject);
// Fetch new binaries, maybe?
await this.downloadVersion(version);
}
/**
* Go and check which versions have already been downloaded.
*
* @returns {Promise<void>}
*/
/**
* Go and check which versions have already been downloaded.
*
* @returns {Promise<void>}
*/
@action public async updateDownloadedVersionState(): Promise<void> {
const downloadedVersions = await this.binaryManager.getDownloadedVersions();
const updatedVersions = { ...this.versions };
(downloadedVersions || []).forEach((version) => {
// Keep state of currently downloading binaries first
const downloadingVersions = getDownloadingVersions(this);
(downloadingVersions || []).forEach((version) => {
if (updatedVersions[version]) {
updatedVersions[version].state = ElectronVersionState.ready;
updatedVersions[version].state = VersionState.downloading;
}
});
const downloadedVersions = await getDownloadedVersions();
(downloadedVersions || []).forEach((version) => {
if (updatedVersions[version]) {
updatedVersions[version].state = VersionState.ready;
}
});
console.log(`State: Updated version state`, updatedVersions);
this.versions = updatedVersions;
@@ -426,7 +534,7 @@ export class AppState {
let strData = data.toString();
const { isNotPre, bypassBuffer } = options;
// Todo: This drops the first part of the buffer... is that fully expected?
// TODO: This drops the first part of the buffer... is that fully expected?
if (process.platform === 'win32' && bypassBuffer === false) {
this.outputBuffer += strData;
strData = this.outputBuffer;
@@ -524,7 +632,7 @@ export class AppState {
*/
@action public showMosaic(id: MosaicId) {
const currentlyVisible = getVisibleMosaics(this.mosaicArrangement);
this.setVisibleMosaics([ ...currentlyVisible, id ]);
this.setVisibleMosaics([...currentlyVisible, id]);
}
/**
@@ -539,6 +647,7 @@ export class AppState {
this.isTourShowing = false;
this.isConsoleShowing = false;
this.isAddVersionDialogShowing = false;
this.isThemeDialogShowing = false;
if (additionalOptions) {
for (const key in additionalOptions) {
+5 -4
View File
@@ -2,12 +2,11 @@ import * as fsExtraType from 'fs-extra';
import * as pathType from 'path';
import { EditorValues } from '../interfaces';
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../shared-constants';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME, STYLES_CSS_NAME } from '../shared-constants';
import { fancyImport } from '../utils/import';
/**
* Returns expected content for a given name. Currently synchronous,
* but probably shouldn't be.
* Returns expected content for a given name.
*
* @param {string} name
* @returns {Promise<EditorValues>}
@@ -41,6 +40,8 @@ export async function getTemplateValues(name: string): Promise<EditorValues> {
return {
renderer: await getFile(RENDERER_JS_NAME),
main: await getFile(MAIN_JS_NAME),
html: await getFile(INDEX_HTML_NAME)
html: await getFile(INDEX_HTML_NAME),
preload: await getFile(PRELOAD_JS_NAME),
css: await getFile(STYLES_CSS_NAME)
};
}
+3 -3
View File
@@ -5,7 +5,7 @@ import {
} from 'electron';
import { autorun } from 'mobx';
import { ElectronVersion, ElectronVersionState } from '../interfaces';
import { RunnableVersion, VersionState } from '../interfaces';
import { getNiceGreeting } from '../utils/nice-greeting';
import { sortedElectronMap } from '../utils/sorted-electron-map';
import { AppState } from './state';
@@ -22,11 +22,11 @@ const {
* Helper method: Returns an icon (emoji, lol) for an Electron
* version.
*
* @param {Partial<ElectronVersion>} { state }
* @param {Partial<RunnableVersion>} { state }
* @returns {string}
*/
export function getItemIcon(
{ state }: Partial<ElectronVersion> = { state: ElectronVersionState.unknown }
{ state }: Partial<RunnableVersion> = { state: VersionState.unknown }
) {
return state === 'ready'
? '💾'
+53 -49
View File
@@ -1,4 +1,5 @@
import { ElectronVersion, ElectronVersionSource, ElectronVersionState, NpmVersion } from '../interfaces';
import semver from 'semver';
import { RunnableVersion, Version, VersionSource, VersionState } from '../interfaces';
import { normalizeVersion } from '../utils/normalize-version';
export const enum ElectronReleaseChannel {
@@ -11,11 +12,11 @@ export const enum ElectronReleaseChannel {
/**
* Returns a sensible default version string.
*
* @param {Array<ElectronVersion>} knownVersions
* @param {Array<RunnableVersion>} knownVersions
* @returns {string}
*/
export function getDefaultVersion(
knownVersions: Array<ElectronVersion> = []
knownVersions: Array<RunnableVersion> = []
): string {
const ls = localStorage.getItem('version');
@@ -45,13 +46,13 @@ export function getDefaultVersion(
* Return the release channel for a given input
* version.
*
* @param {NpmVersion} input
* @param {Version | string} input
* @returns {ElectronReleaseChannel}
*/
export function getReleaseChannel(
input: NpmVersion
input: Version | string
): ElectronReleaseChannel {
const tag = input.version || '';
const tag = (typeof input === 'string') ? input : (input.version || '');
if (tag.includes('beta')) {
return ElectronReleaseChannel.beta;
@@ -78,17 +79,17 @@ export const enum VersionKeys {
* Retrieve Electron versions from localStorage.
*
* @param {VersionKeys} key
* @param {() => Array<NpmVersion>} fallbackMethod
* @returns {Array<NpmVersion>}
* @param {() => Array<Version>} fallbackMethod
* @returns {Array<Version>}
*/
function getVersions(
key: VersionKeys, fallbackMethod: () => Array<NpmVersion>
): Array<NpmVersion> {
key: VersionKeys, fallbackMethod: () => Array<Version>
): Array<Version> {
const fromLs = window.localStorage.getItem(key);
if (fromLs) {
try {
let result: Array<NpmVersion> = JSON.parse(fromLs);
let result: Array<Version> = JSON.parse(fromLs);
if (!isExpectedFormat(result)) {
// Known versions can just be downloaded again.
@@ -114,9 +115,9 @@ function getVersions(
* Save an array of GitHubVersions to localStorage.
*
* @param {VersionKeys} key
* @param {Array<NpmVersion} versions
* @param {Array<Version} versions
*/
function saveVersions(key: VersionKeys, versions: Array<NpmVersion>) {
function saveVersions(key: VersionKeys, versions: Array<Version>) {
const stringified = JSON.stringify(versions);
window.localStorage.setItem(key, stringified);
}
@@ -124,35 +125,35 @@ function saveVersions(key: VersionKeys, versions: Array<NpmVersion>) {
/**
* Return both known as well as local versions.
*
* @returns {Array<NpmVersion>}
* @returns {Array<Version>}
*/
export function getElectronVersions(): Array<ElectronVersion> {
const known: Array<ElectronVersion> = getKnownVersions().map((version) => {
export function getElectronVersions(): Array<RunnableVersion> {
const known: Array<RunnableVersion> = getKnownVersions().map((version) => {
return {
...version,
source: ElectronVersionSource.remote,
state: ElectronVersionState.unknown
source: VersionSource.remote,
state: VersionState.unknown
};
});
const local: Array<ElectronVersion> = getLocalVersions().map((version) => {
const local: Array<RunnableVersion> = getLocalVersions().map((version) => {
return {
...version,
source: ElectronVersionSource.local,
state: ElectronVersionState.ready
source: VersionSource.local,
state: VersionState.ready
};
});
return [ ...known, ...local ];
return [...known, ...local];
}
/**
* Add a version to the local versions
*
* @param {NpmVersion} input
* @returns {Array<NpmVersion>}
* @param {Version} input
* @returns {Array<Version>}
*/
export function addLocalVersion(input: NpmVersion): Array<NpmVersion> {
export function addLocalVersion(input: Version): Array<Version> {
const versions = getLocalVersions();
if (!versions.find((v) => v.localPath === input.localPath)) {
@@ -167,9 +168,9 @@ export function addLocalVersion(input: NpmVersion): Array<NpmVersion> {
/**
* Retrieves local Electron versions, configured by the user.
*
* @returns {Array<NpmVersion>}
* @returns {Array<Version>}
*/
export function getLocalVersions(): Array<NpmVersion> {
export function getLocalVersions(): Array<Version> {
const versions = getVersions(VersionKeys.local, () => []);
return versions;
@@ -178,12 +179,12 @@ export function getLocalVersions(): Array<NpmVersion> {
/**
* Saves local versions to localStorage.
*
* @param {Array<NpmVersion>} versions
* @param {Array<Version>} versions
*/
export function saveLocalVersions(versions: Array<NpmVersion | ElectronVersion>) {
export function saveLocalVersions(versions: Array<Version | RunnableVersion>) {
const filteredVersions = versions.filter((v) => {
if (isElectronVersion(v)) {
return v.source === ElectronVersionSource.local;
return v.source === VersionSource.local;
}
return true;
@@ -196,18 +197,18 @@ export function saveLocalVersions(versions: Array<NpmVersion | ElectronVersion>)
* Retrieves our best guess regarding the latest Electron versions. Tries to
* fetch them from localStorage, then from a static releases.json file.
*
* @returns {Array<NpmVersion>}
* @returns {Array<Version>}
*/
export function getKnownVersions(): Array<NpmVersion> {
export function getKnownVersions(): Array<Version> {
return getVersions(VersionKeys.known, () => require('../../static/releases.json'));
}
/**
* Saves known versions to localStorage.
*
* @param {Array<NpmVersion>} versions
* @param {Array<Version>} versions
*/
export function saveKnownVersions(versions: Array<NpmVersion>) {
export function saveKnownVersions(versions: Array<Version>) {
return saveVersions(VersionKeys.known, versions);
}
@@ -216,10 +217,10 @@ export function saveKnownVersions(versions: Array<NpmVersion>) {
* saved after.
*
* @export
* @returns {Promise<Array<ElectronVersion>>}
* @returns {Promise<Array<RunnableVersion>>}
*/
export async function getUpdatedElectronVersions(
): Promise<Array<ElectronVersion>> {
): Promise<Array<RunnableVersion>> {
try {
await fetchVersions();
} catch (error) {
@@ -232,18 +233,21 @@ export async function getUpdatedElectronVersions(
/**
* Fetch the latest known versions directly from npm.
*
* @returns {Promise<Array<NpmVersion>>}
* @returns {Promise<Array<Version>>}
*/
export async function fetchVersions() {
const response = await window.fetch(`https://registry.npmjs.org/electron`);
const response = await window.fetch('https://unpkg.com/electron-releases/lite.json');
const data = await response.json();
const versions: Record<string, any> = data.versions;
const output: Array<NpmVersion> = Object
.keys(versions)
.map((version) => ({ version }));
// pre-0.24.0 versions were technically 'atom-shell' and cannot
// be downloaded with @electron/get
const MIN_DOWNLOAD_VERSION = '0.24.0';
if (output && output.length > 0 && isExpectedFormat(output)) {
const output = data
.map(({ version }: any) => ({ version }))
.filter(({ version }: any) => semver.gte(version, MIN_DOWNLOAD_VERSION));
if (output?.length > 0 && isExpectedFormat(output)) {
console.log(`Fetched new Electron versions (Count: ${output.length})`);
saveKnownVersions(output);
}
@@ -265,9 +269,9 @@ export function isExpectedFormat(input: Array<any>): boolean {
* Migrates old versions, if necessary
*
* @param {Array<any>} input
* @returns {Array<NpmVersion>}
* @returns {Array<Version>}
*/
export function migrateVersions(input: Array<any> = []): Array<NpmVersion> {
export function migrateVersions(input: Array<any> = []): Array<Version> {
return input
.filter((item) => !!item)
.map((item) => {
@@ -281,11 +285,11 @@ export function migrateVersions(input: Array<any> = []): Array<NpmVersion> {
localPath: url
};
})
.filter((item) => !!item) as Array<NpmVersion>;
.filter((item) => !!item) as Array<Version>;
}
export function isElectronVersion(
input: NpmVersion | ElectronVersion
): input is ElectronVersion {
return (input as ElectronVersion).source !== undefined;
input: Version | RunnableVersion
): input is RunnableVersion {
return (input as RunnableVersion).source !== undefined;
}
+7
View File
@@ -0,0 +1,7 @@
import * as Sentry from '@sentry/electron';
export function initSentry() {
if (!(global as any).__JEST__) {
Sentry.init({dsn: 'https://966a5b01ac8d4941b81e4ebd0ab4c991@sentry.io/1882540'});
}
}
+2
View File
@@ -1,4 +1,6 @@
export const INDEX_HTML_NAME = 'index.html';
export const MAIN_JS_NAME = 'main.js';
export const RENDERER_JS_NAME = 'renderer.js';
export const PRELOAD_JS_NAME = 'preload.js';
export const STYLES_CSS_NAME = 'styles.css';
export const PACKAGE_NAME = 'package.json';
+5 -5
View File
@@ -1,16 +1,16 @@
import { ElectronVersion } from '../interfaces';
import { RunnableVersion } from '../interfaces';
import { normalizeVersion } from './normalize-version';
/**
* Takes an array of GitHub releases and returns a StringMap of
* Electron releases.
*
* @param {Array<ElectronVersion>} input
* @returns {Record<string, ElectronVersion>}
* @param {Array<RunnableVersion>} input
* @returns {Record<string, RunnableVersion>}
*/
export function arrayToStringMap(
input: Array<ElectronVersion>
): Record<string, ElectronVersion> {
input: Array<RunnableVersion>
): Record<string, RunnableVersion> {
const output = {};
input.forEach((version) => {
+3 -3
View File
@@ -6,9 +6,9 @@ import { getEditorValue } from './editor-value';
import { getEditorViewState } from './editor-viewstate';
export interface EditorBackup {
value: string;
model: editor.ITextModel | null;
viewState: editor.ICodeEditorViewState | null;
value?: string;
model?: editor.ITextModel | null;
viewState?: editor.ICodeEditorViewState | null;
}
/**
+5 -5
View File
@@ -3,11 +3,11 @@
* a debounced version below.
*/
function _updateEditorLayout() {
const { main, renderer, html } = window.ElectronFiddle.editors;
if (main) main.layout();
if (renderer) renderer.layout();
if (html) html.layout();
Object.keys(window.ElectronFiddle.editors).forEach((key) => {
if (window.ElectronFiddle.editors[key]) {
window.ElectronFiddle.editors[key].layout();
}
});
}
let handle: number;
+4 -4
View File
@@ -1,8 +1,8 @@
import * as GitHubType from '@octokit/rest';
import { Octokit } from '@octokit/rest';
import { AppState } from '../renderer/state';
let _Octokit: typeof GitHubType;
let _octo: GitHubType;
let _Octokit: typeof Octokit;
let _octo: Octokit;
/**
* Returns a loaded Octokit. If state is passed and authentication
@@ -13,7 +13,7 @@ let _octo: GitHubType;
*/
export async function getOctokit(
appState?: AppState
): Promise<GitHubType> {
): Promise<Octokit> {
_Octokit = _Octokit || (await import('@octokit/rest') as any).default;
_octo = _octo || new _Octokit();
+2
View File
@@ -6,6 +6,8 @@ import { AboutPanelOptionsOptions, app } from 'electron';
* @returns
*/
export function setupAboutPanel(): void {
if (process.platform === 'win32') return;
const options: AboutPanelOptionsOptions = {
applicationName: 'Electron Fiddle',
applicationVersion: app.getVersion(),
+5 -5
View File
@@ -1,18 +1,18 @@
import * as semver from 'semver';
import { ElectronVersion } from '../interfaces';
import { RunnableVersion } from '../interfaces';
/**
* Sorts Electron versions and returns the result of a
* map function.
*
* @param {Record<string, ElectronVersion>} versions
* @param {(key: string, version: ElectronVersion) => void} mapFn
* @param {Record<string, RunnableVersion>} versions
* @param {(key: string, version: RunnableVersion) => void} mapFn
* @returns {Array<T>}
*/
export function sortedElectronMap<T>(
versions: Record<string, ElectronVersion>,
mapFn: (key: string, version: ElectronVersion) => T
versions: Record<string, RunnableVersion>,
mapFn: (key: string, version: RunnableVersion) => T
) {
return Object.keys(versions)
.sort((a, b) => {
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.debugger</key>
<true/>
</dict>
</plist>
+1 -1
View File
File diff suppressed because one or more lines are too long
-4
View File
@@ -25,10 +25,6 @@ app.on('ready', () => {
console.log('The autoUpdater is checking for an update')
})
autoUpdater.on('update-available', () => {
console.log('The autoUpdater has found an update!')
})
autoUpdater.on('update-available', () => {
console.log('The autoUpdater has found an update and is now downloading it!')
})
+1 -1
View File
@@ -14,7 +14,7 @@ app.on('ready', () => {
win = null
})
let view = new BrowserView({
const view = new BrowserView({
webPreferences: {
nodeIntegration: false
}
+3 -4
View File
@@ -5,11 +5,10 @@
</head>
<body>
<h1>Hello from your BrowserWindow!</h1>
<button></button>
<button id="close-btn">Close this window</button>
<script>
document.querySelector('Close this window').onclick = () => {
window.close()
}
const button = document.querySelector('#close-btn')
button.addEventListener('click', () => window.close())
</script>
</body>
</html>
+1 -3
View File
@@ -5,10 +5,8 @@
const { app, BrowserWindow } = require('electron')
let mainWindow = null
app.on('ready', () => {
mainWindow = new BrowserWindow({
const mainWindow = new BrowserWindow({
height: 600,
width: 600,
webPreferences: {
+1 -1
View File
@@ -6,7 +6,7 @@
// the result.
//
// For more info, see:
// https://electronjs.org/docs/api/contentTracing
// https://electronjs.org/docs/api/content-tracing
const { app, contentTracing } = require('electron')
+1 -3
View File
@@ -5,10 +5,8 @@
const { app, BrowserWindow, session } = require('electron')
let mainWindow = null
app.on('ready', () => {
mainWindow = new BrowserWindow({
const mainWindow = new BrowserWindow({
height: 600,
width: 600,
webPreferences: {
+2 -4
View File
@@ -5,10 +5,8 @@
const { app, BrowserWindow } = require('electron')
let mainWindow = null
app.on('ready', () => {
mainWindow = new BrowserWindow({
const mainWindow = new BrowserWindow({
height: 600,
width: 600,
webPreferences: {
@@ -18,6 +16,6 @@ app.on('ready', () => {
mainWindow.loadFile('index.html')
mainWindow.webContents.on('crashed', () => {
console.log(`Window crashed!`)
console.log('Window crashed!')
})
})
+1 -3
View File
@@ -8,10 +8,8 @@
const { app, BrowserWindow } = require('electron')
let mainWindow = null
app.on('ready', () => {
mainWindow = new BrowserWindow({ height: 600, width: 600 })
const mainWindow = new BrowserWindow({ height: 600, width: 600 })
mainWindow.loadFile('index.html')
+1 -3
View File
@@ -6,10 +6,8 @@
const { app, BrowserWindow } = require('electron')
let mainWindow = null
app.on('ready', () => {
mainWindow = new BrowserWindow({
const mainWindow = new BrowserWindow({
height: 600,
width: 600,
webPreferences: {
+11 -10
View File
@@ -5,20 +5,21 @@
const { app, BrowserWindow, dialog } = require('electron')
let mainWindow = null
app.on('ready', () => {
mainWindow = new BrowserWindow({ height: 600, width: 600 })
const mainWindow = new BrowserWindow({ height: 600, width: 600 })
// Show an "Open File" dialog and attempt to open
// the chosen file in our window.
//
// If you provide a callback, the method will be asynchronous,
// but in thise case, we'll just use the synchronous variant.
const file = dialog.showOpenDialog({
title: 'Hello!',
dialog.showOpenDialog(mainWindow, {
properties: ['openFile']
}).then(result => {
if (result.canceled) {
console.log('Dialog was canceled')
} else {
const file = result.filePaths[0]
mainWindow.loadURL(`file://${file}`)
}
}).catch(err => {
console.log(err)
})
mainWindow.loadURL(`file://${file}`)
})

Some files were not shown because too many files have changed in this diff Show More